diff --git a/CMake/Target.cmake b/CMake/Target.cmake index 7f5e2737d..984b093e3 100644 --- a/CMake/Target.cmake +++ b/CMake/Target.cmake @@ -320,6 +320,11 @@ function(exp_gather_target_libs) return() endif () + if (NOT TARGET "${arg_NAME}") + set(${arg_OUTPUT} "" PARENT_SCOPE) + return() + endif () + get_target_property(target_libs ${arg_NAME} LINK_LIBRARIES) if (NOT ("${target_libs}" STREQUAL "target_libs-NOTFOUND")) foreach(target_lib ${target_libs}) @@ -363,6 +368,9 @@ function(exp_add_mirror_info_source_generation_target) endif() if (DEFINED arg_LIB) foreach (l ${arg_LIB}) + if (NOT TARGET "${l}") + continue() + endif () list(APPEND inc $) exp_gather_target_libs( NAME ${l} @@ -423,7 +431,11 @@ function(exp_add_mirror_info_source_generation_target) set(${arg_OUTPUT_TARGET_NAME} ${custom_target_name} PARENT_SCOPE) if (DEFINED arg_LIB) - add_dependencies(${custom_target_name} ${arg_LIB}) + foreach (lib ${arg_LIB}) + if (TARGET "${lib}") + add_dependencies(${custom_target_name} ${lib}) + endif () + endforeach () endif() endfunction() diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b4cc515b..c006e38c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,10 @@ set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES ${CMAKE_SOURCE_DIR}/conan_provider.cmake CA project(Explosion VERSION 0.0.1) +if (APPLE) + enable_language(OBJCXX) +endif () + option(BUILD_EDITOR "Build Explosion editor" ON) option(BUILD_SAMPLE "Build samples" ON) diff --git a/Editor/CMakeLists.txt b/Editor/CMakeLists.txt index 10b57687f..d81582a34 100644 --- a/Editor/CMakeLists.txt +++ b/Editor/CMakeLists.txt @@ -1,12 +1,20 @@ set(imgui_stdlib_path ${imgui_PACKAGE_FOLDER_RELEASE}/res/misc/cpp) file(GLOB_RECURSE editor_sources Src/*.cpp) +set(editor_platform_libs) +if (APPLE) + list(APPEND editor_sources Src/Utils/MacosPlatformUtils.mm) + set_source_files_properties(Src/Utils/MacosPlatformUtils.mm PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON) + list(APPEND editor_platform_libs "-framework Cocoa") +elseif (WIN32) + list(APPEND editor_platform_libs ole32 shell32) +endif () + exp_add_executable( NAME Editor SRC ${editor_sources} ${imgui_stdlib_path}/imgui_stdlib.cpp - LIB Core RHI Runtime glfw imgui::imgui + LIB Core RHI Runtime glfw imgui::imgui ${editor_platform_libs} INC Include ${imgui_stdlib_path} - LIB ${editor_libs} DEP_TARGET ${platform_rhi_targets} REFLECT Include ) diff --git a/Editor/Include/Editor/Frame/ProjectHubFrame.h b/Editor/Include/Editor/Frame/ProjectHubFrame.h index 1e0758ba2..7e762f387 100644 --- a/Editor/Include/Editor/Frame/ProjectHubFrame.h +++ b/Editor/Include/Editor/Frame/ProjectHubFrame.h @@ -43,19 +43,20 @@ namespace Editor { void Render(EditorWindow& inWindow, const std::string& inRhiType); private: + void RenderActionBar(EditorWindow& inWindow, const std::string& inRhiType); + void RenderRecentProjects(EditorWindow& inWindow, const std::string& inRhiType); + void RenderCreateProjectPopup(EditorWindow& inWindow, const std::string& inRhiType); CreateProjectResult CreateProject(); void OpenProject(EditorWindow& inWindow, const std::string& inProjectPath, const std::string& inRhiType); void SaveRecentProjects() const; void TouchRecentProject(const std::string& inProjectPath); Common::Path recentProjectsFile; - std::string engineVersion; std::vector projectTemplates; std::vector recentProjects; std::string projectName; std::string directory; int selectedTemplateIndex; std::string statusMessage; - std::string lastCreatedProjectPath; }; } diff --git a/Editor/Include/Editor/SceneClient.h b/Editor/Include/Editor/SceneClient.h index d30fbe216..c77d2500c 100644 --- a/Editor/Include/Editor/SceneClient.h +++ b/Editor/Include/Editor/SceneClient.h @@ -46,7 +46,7 @@ namespace Editor { private: void CreateEditorCamera(); - Common::FVec3 CameraMoveInput() const; + Common::FVec2 CameraMoveInput() const; Core::Uri levelUri; Runtime::World world; @@ -55,7 +55,6 @@ namespace Editor { Runtime::Entity editorCamera; std::set pressedKeys; bool sceneHovered; - bool sceneFocused; bool cameraLooking; bool cameraAnglesInitialized; float cameraYaw; diff --git a/Editor/Include/Editor/Utils/PlatformUtils.h b/Editor/Include/Editor/Utils/PlatformUtils.h new file mode 100644 index 000000000..9db92bd5e --- /dev/null +++ b/Editor/Include/Editor/Utils/PlatformUtils.h @@ -0,0 +1,15 @@ +// +// Created by johnk on 2026/7/11. +// + +#pragma once + +#include +#include + +namespace Editor { + class PlatformUtils final { + public: + static std::optional SelectDirectory(const std::string& inTitle, const std::string& inInitialDirectory = {}); + }; +} diff --git a/Editor/Src/EditorApplication.cpp b/Editor/Src/EditorApplication.cpp index 12372db6c..a83fa2244 100644 --- a/Editor/Src/EditorApplication.cpp +++ b/Editor/Src/EditorApplication.cpp @@ -19,6 +19,9 @@ namespace Editor::Internal { constexpr uint32_t defaultWindowWidth = 1600; constexpr uint32_t defaultWindowHeight = 900; + constexpr uint32_t projectHubWindowWidth = 560; + constexpr uint32_t projectHubWindowHeight = 720; + constexpr float defaultFontSize = 15.0f; constexpr uint32_t defaultSceneWidth = 1280; constexpr uint32_t defaultSceneHeight = 720; constexpr RHI::PixelFormat sceneColorFormat = RHI::PixelFormat::rgba8Unorm; @@ -46,6 +49,16 @@ namespace Editor::Internal { return std::format("{} - Explosion Editor", projectName); } + static uint32_t GetInitialWindowWidth(const EditorApplicationDesc& inDesc) + { + return inDesc.mode == EditorApplicationMode::projectHub ? projectHubWindowWidth : defaultWindowWidth; + } + + static uint32_t GetInitialWindowHeight(const EditorApplicationDesc& inDesc) + { + return inDesc.mode == EditorApplicationMode::projectHub ? projectHubWindowHeight : defaultWindowHeight; + } + static void SetDarkStyle() { ImGui::StyleColorsDark(); @@ -68,13 +81,14 @@ namespace Editor::Internal { ImGui::DockBuilderAddNode(inDockSpaceId, ImGuiDockNodeFlags_DockSpace); ImGui::DockBuilderSetNodeSize(inDockSpaceId, inSize); - ImGuiID sceneNodeId = inDockSpaceId; + ImGuiID sceneNodeId = 0; + ImGuiID rightColumnNodeId = 0; ImGuiID outlinerNodeId = 0; ImGuiID inspectorNodeId = 0; ImGuiID logNodeId = 0; - sceneNodeId = ImGui::DockBuilderSplitNode(sceneNodeId, ImGuiDir_Left, 0.22f, &outlinerNodeId, &sceneNodeId); - sceneNodeId = ImGui::DockBuilderSplitNode(sceneNodeId, ImGuiDir_Right, 0.28f, &inspectorNodeId, &sceneNodeId); - sceneNodeId = ImGui::DockBuilderSplitNode(sceneNodeId, ImGuiDir_Down, 0.26f, &logNodeId, &sceneNodeId); + ImGui::DockBuilderSplitNode(inDockSpaceId, ImGuiDir_Right, 0.25f, &rightColumnNodeId, &sceneNodeId); + ImGui::DockBuilderSplitNode(sceneNodeId, ImGuiDir_Down, 0.25f, &logNodeId, &sceneNodeId); + ImGui::DockBuilderSplitNode(rightColumnNodeId, ImGuiDir_Down, 0.5f, &inspectorNodeId, &outlinerNodeId); ImGui::DockBuilderDockWindow("Scene", sceneNodeId); ImGui::DockBuilderDockWindow("Outliner", outlinerNodeId); @@ -117,8 +131,8 @@ namespace Editor { window = Common::MakeUnique( EditorWindowDesc { .title = Internal::MakeWindowTitle(desc), - .width = Internal::defaultWindowWidth, - .height = Internal::defaultWindowHeight + .width = Internal::GetInitialWindowWidth(desc), + .height = Internal::GetInitialWindowHeight(desc) }); InstallCallbacks(); @@ -175,6 +189,9 @@ namespace Editor { io.BackendPlatformName = "ExplosionGLFW"; io.BackendRendererName = "ExplosionRHI"; io.IniFilename = "ExplosionEditor.ini"; + ImFontConfig defaultFontConfig; + defaultFontConfig.SizePixels = Internal::defaultFontSize; + io.FontDefault = io.Fonts->AddFontDefaultVector(&defaultFontConfig); Internal::SetDarkStyle(); } diff --git a/Editor/Src/Frame/ProjectHubFrame.cpp b/Editor/Src/Frame/ProjectHubFrame.cpp index 3fee4e338..4f265575d 100644 --- a/Editor/Src/Frame/ProjectHubFrame.cpp +++ b/Editor/Src/Frame/ProjectHubFrame.cpp @@ -14,15 +14,57 @@ #include #include #include -#include #include #include #include #include +#include -namespace Editor::Internal { +namespace Editor::ProjectHub::Internal { constexpr std::string_view templateFileExtension = ".tpl"; constexpr std::string_view cmakeMinVersion = "3.25"; + constexpr float actionButtonHeight = 58.0f; + constexpr float recentProjectHeight = 68.0f; + constexpr float contentSpacing = 12.0f; + const ImVec4 windowBackground(0.055f, 0.063f, 0.078f, 1.0f); + const ImVec4 cardBackground(0.09f, 0.102f, 0.125f, 1.0f); + const ImVec4 cardHovered(0.125f, 0.145f, 0.18f, 1.0f); + const ImVec4 accent(0.25f, 0.49f, 0.96f, 1.0f); + const ImVec4 accentHovered(0.31f, 0.55f, 1.0f, 1.0f); + const ImVec4 secondaryButton(0.12f, 0.137f, 0.17f, 1.0f); + const ImVec4 secondaryButtonHovered(0.16f, 0.18f, 0.22f, 1.0f); + + static bool RenderActionButton(const char* inLabel, const ImVec2& inSize, const ImVec4& inColor, const ImVec4& inHoveredColor) + { + ImGui::PushStyleColor(ImGuiCol_Button, inColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, inHoveredColor); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, inHoveredColor); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 9.0f); + const bool clicked = ImGui::Button(inLabel, inSize); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); + return clicked; + } + + static bool RenderRecentProject(const RecentProjectInfo& inProject) + { + ImGui::PushID(inProject.path.c_str()); + const ImVec2 cardMin = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("##RecentProject", ImVec2(ImGui::GetContentRegionAvail().x, recentProjectHeight)); + const ImVec2 cardMax = ImGui::GetItemRectMax(); + const bool hovered = ImGui::IsItemHovered(); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(cardMin, cardMax, ImGui::GetColorU32(hovered ? cardHovered : cardBackground), 8.0f); + drawList->PushClipRect(ImVec2(cardMin.x + 16.0f, cardMin.y), ImVec2(cardMax.x - 38.0f, cardMax.y), true); + drawList->AddText(ImVec2(cardMin.x + 16.0f, cardMin.y + 13.0f), ImGui::GetColorU32(ImGuiCol_Text), inProject.name.c_str()); + drawList->AddText(ImVec2(cardMin.x + 16.0f, cardMin.y + 38.0f), ImGui::GetColorU32(ImGuiCol_TextDisabled), inProject.path.c_str()); + drawList->PopClipRect(); + drawList->AddText(ImVec2(cardMax.x - 25.0f, cardMin.y + 25.0f), ImGui::GetColorU32(ImGuiCol_TextDisabled), ">"); + + ImGui::PopID(); + return ImGui::IsItemClicked(); + } static Common::Path StripTemplateExtension(const Common::Path& inRelativePath) { @@ -30,10 +72,7 @@ namespace Editor::Internal { return { str.substr(0, str.size() - templateFileExtension.size()) }; } - static Common::Result RenderProjectTemplate( - const Common::Path& inTemplateDir, - const Common::Path& inProjectDir, - const std::string& inProjectName) + static Common::Result RenderProjectTemplate(const Common::Path& inTemplateDir, const Common::Path& inProjectDir, const std::string& inProjectName) { Common::TemplateEngine templateEngine; templateEngine @@ -79,11 +118,9 @@ namespace Editor::Internal { namespace Editor { ProjectHubFrame::ProjectHubFrame() : recentProjectsFile(Core::Paths::EngineCacheDir() / "Editor" / "ProjectHub" / "RecentProjects.json") - , engineVersion(std::format("v{}.{}.{}", ENGINE_VERSION_MAJOR, ENGINE_VERSION_MINOR, ENGINE_VERSION_PATCH)) , projectName() , directory() , selectedTemplateIndex(0) - , lastCreatedProjectPath() { const Common::Path projectTemplatesRoot = Core::Paths::EngineResDir() / "Editor" / "ProjectTemplates"; (void) projectTemplatesRoot.Traverse([this](const Common::Path& inPath) -> bool { @@ -109,64 +146,170 @@ namespace Editor { const ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->WorkPos); ImGui::SetNextWindowSize(viewport->WorkSize); - ImGui::Begin("Project Hub", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); + ImGui::SetNextWindowViewport(viewport->ID); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ProjectHub::Internal::windowBackground); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(26.0f, 24.0f)); + ImGui::Begin( + "##ProjectHub", + nullptr, + ImGuiWindowFlags_NoDecoration + | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoSavedSettings + | ImGuiWindowFlags_NoBringToFrontOnFocus); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); + + RenderActionBar(inWindow, inRhiType); + ImGui::Dummy(ImVec2(0.0f, 22.0f)); + RenderRecentProjects(inWindow, inRhiType); + RenderCreateProjectPopup(inWindow, inRhiType); + ImGui::End(); + } + + void ProjectHubFrame::RenderActionBar(EditorWindow& inWindow, const std::string& inRhiType) + { + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float buttonWidth = (ImGui::GetContentRegionAvail().x - spacing) * 0.5f; + if (ProjectHub::Internal::RenderActionButton("Open", ImVec2(buttonWidth, ProjectHub::Internal::actionButtonHeight), ProjectHub::Internal::accent, ProjectHub::Internal::accentHovered)) { + if (const auto selectedDirectory = PlatformUtils::SelectDirectory("Open Explosion Project")) { + OpenProject(inWindow, *selectedDirectory, inRhiType); + } + } - ImGui::TextUnformatted("Explosion Editor"); ImGui::SameLine(); - ImGui::TextDisabled("%s", engineVersion.c_str()); - ImGui::Separator(); + if (ProjectHub::Internal::RenderActionButton("Create", ImVec2(buttonWidth, ProjectHub::Internal::actionButtonHeight), ProjectHub::Internal::secondaryButton, ProjectHub::Internal::secondaryButtonHovered)) { + statusMessage.clear(); + ImGui::OpenPopup("##CreateProjectPopup"); + } + } + + void ProjectHubFrame::RenderRecentProjects(EditorWindow& inWindow, const std::string& inRhiType) + { + ImGui::TextUnformatted("Recent projects"); + const std::string projectCount = std::to_string(recentProjects.size()); + ImGui::SameLine(ImGui::GetContentRegionMax().x - ImGui::CalcTextSize(projectCount.c_str()).x); + ImGui::TextDisabled("%s", projectCount.c_str()); + ImGui::Dummy(ImVec2(0.0f, 7.0f)); + + if (!statusMessage.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.42f, 0.42f, 1.0f)); + ImGui::TextWrapped("%s", statusMessage.c_str()); + ImGui::PopStyleColor(); + ImGui::Dummy(ImVec2(0.0f, 7.0f)); + } - ImGui::Columns(2, "ProjectHubColumns", true); - ImGui::TextUnformatted("Recent Projects"); - ImGui::BeginChild("RecentProjects", ImVec2(0.0f, -1.0f), true); + ImGui::BeginChild("##RecentProjects", ImVec2(0.0f, 0.0f), false); + std::string projectToOpen; for (const auto& project : std::views::reverse(recentProjects)) { - ImGui::PushID(project.path.c_str()); - if (ImGui::Selectable(project.name.c_str())) { - OpenProject(inWindow, project.path, inRhiType); + if (ProjectHub::Internal::RenderRecentProject(project)) { + projectToOpen = project.path; } - ImGui::TextDisabled("%s", project.path.c_str()); - ImGui::PopID(); + ImGui::Dummy(ImVec2(0.0f, ProjectHub::Internal::contentSpacing)); + } + + if (recentProjects.empty()) { + const ImVec2 emptyMin = ImGui::GetCursorScreenPos(); + const ImVec2 emptyMax(emptyMin.x + ImGui::GetContentRegionAvail().x, emptyMin.y + 118.0f); + ImGui::GetWindowDrawList()->AddRectFilled(emptyMin, emptyMax, ImGui::GetColorU32(ProjectHub::Internal::cardBackground), 8.0f); + const char* title = "No recent projects"; + const char* description = "Open an existing project or create a new one."; + ImGui::GetWindowDrawList()->AddText(ImVec2(emptyMin.x + 16.0f, emptyMin.y + 31.0f), ImGui::GetColorU32(ImGuiCol_Text), title); + ImGui::GetWindowDrawList()->AddText(ImVec2(emptyMin.x + 16.0f, emptyMin.y + 59.0f), ImGui::GetColorU32(ImGuiCol_TextDisabled), description); + ImGui::Dummy(ImVec2(0.0f, 118.0f)); } ImGui::EndChild(); - ImGui::NextColumn(); - ImGui::TextUnformatted("Create Project"); - ImGui::InputText("Name", &projectName); - ImGui::InputText("Directory", &directory); - - const char* selectedTemplate = projectTemplates.empty() ? "None" : projectTemplates[selectedTemplateIndex].name.c_str(); - if (ImGui::BeginCombo("Template", selectedTemplate)) { - for (int i = 0; i < static_cast(projectTemplates.size()); i++) { - const bool selected = i == selectedTemplateIndex; - if (ImGui::Selectable(projectTemplates[i].name.c_str(), selected)) { - selectedTemplateIndex = i; + if (!projectToOpen.empty()) { + OpenProject(inWindow, projectToOpen, inRhiType); + } + } + + void ProjectHubFrame::RenderCreateProjectPopup(EditorWindow& inWindow, const std::string& inRhiType) + { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(456.0f, 0.0f), ImGuiCond_Appearing); + ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0.075f, 0.086f, 0.105f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24.0f, 22.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 11.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); + if (ImGui::BeginPopupModal("##CreateProjectPopup", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { + ImGui::TextUnformatted("Create project"); + ImGui::TextDisabled("Start from a template and open it right away."); + ImGui::Dummy(ImVec2(0.0f, 13.0f)); + + ImGui::TextDisabled("NAME"); + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##ProjectName", "MyProject", &projectName); + ImGui::Dummy(ImVec2(0.0f, 7.0f)); + + ImGui::TextDisabled("LOCATION"); + const float browseButtonWidth = 76.0f; + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - browseButtonWidth - ImGui::GetStyle().ItemSpacing.x); + ImGui::InputTextWithHint("##ProjectDirectory", "Choose a parent folder", &directory); + ImGui::SameLine(); + if (ImGui::Button("Browse", ImVec2(browseButtonWidth, 0.0f))) { + if (const auto selectedDirectory = PlatformUtils::SelectDirectory("Choose Project Location", directory)) { + directory = *selectedDirectory; } - if (selected) { - ImGui::SetItemDefaultFocus(); + } + ImGui::Dummy(ImVec2(0.0f, 7.0f)); + + ImGui::TextDisabled("TEMPLATE"); + const char* selectedTemplate = projectTemplates.empty() + ? "No templates available" + : projectTemplates[selectedTemplateIndex].name.c_str(); + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::BeginCombo("##ProjectTemplate", selectedTemplate)) { + for (int i = 0; i < static_cast(projectTemplates.size()); i++) { + const bool selected = i == selectedTemplateIndex; + if (ImGui::Selectable(projectTemplates[i].name.c_str(), selected)) { + selectedTemplateIndex = i; + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } } + ImGui::EndCombo(); } - ImGui::EndCombo(); - } - if (ImGui::Button("Create")) { - const CreateProjectResult result = CreateProject(); - lastCreatedProjectPath = result.success ? result.projectPath : std::string(); - statusMessage = result.success ? std::format("Created {}", result.projectPath) : result.error; - } - ImGui::SameLine(); - if (ImGui::Button("Open Created") && !lastCreatedProjectPath.empty()) { - OpenProject(inWindow, lastCreatedProjectPath, inRhiType); - } - if (!statusMessage.empty()) { - ImGui::TextWrapped("%s", statusMessage.c_str()); + if (!statusMessage.empty()) { + ImGui::Dummy(ImVec2(0.0f, 9.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.42f, 0.42f, 1.0f)); + ImGui::TextWrapped("%s", statusMessage.c_str()); + ImGui::PopStyleColor(); + } + + ImGui::Dummy(ImVec2(0.0f, 15.0f)); + const float footerWidth = 178.0f; + ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - footerWidth); + if (ImGui::Button("Cancel", ImVec2(82.0f, 36.0f))) { + statusMessage.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ProjectHub::Internal::RenderActionButton("Create##Confirm", ImVec2(88.0f, 36.0f), ProjectHub::Internal::accent, ProjectHub::Internal::accentHovered)) { + const CreateProjectResult result = CreateProject(); + if (result.success) { + statusMessage.clear(); + ImGui::CloseCurrentPopup(); + OpenProject(inWindow, result.projectPath, inRhiType); + } else { + statusMessage = result.error; + } + } + ImGui::EndPopup(); } - ImGui::Columns(1); - ImGui::End(); + ImGui::PopStyleVar(4); + ImGui::PopStyleColor(); } CreateProjectResult ProjectHubFrame::CreateProject() { - if (projectName.empty() || directory.empty() || projectTemplates.empty()) { + if (projectName.empty() || directory.empty() || projectTemplates.empty() + || selectedTemplateIndex < 0 || selectedTemplateIndex >= static_cast(projectTemplates.size())) { return { .success = false, .error = "Project name, directory and template must not be empty.", .projectPath = {} }; } @@ -181,12 +324,12 @@ namespace Editor { return { .success = false, .error = std::format("Target directory '{}' already exists.", projectDir.String()), .projectPath = {} }; } - if (const auto result = Internal::RenderProjectTemplate(templateDir, projectDir, projectName); + if (const auto result = ProjectHub::Internal::RenderProjectTemplate(templateDir, projectDir, projectName); result.IsErr()) { return { .success = false, .error = result.Error(), .projectPath = {} }; } - recentProjects.emplace_back(RecentProjectInfo { projectName, projectDir.String() }); + TouchRecentProject(projectDir.String()); SaveRecentProjects(); LogInfo(ProjectHub, "created project '{}' at '{}'", projectName, projectDir.String()); return { .success = true, .error = {}, .projectPath = projectDir.String() }; @@ -203,7 +346,7 @@ namespace Editor { TouchRecentProject(inProjectPath); SaveRecentProjects(); - const std::string command = Internal::LaunchCommand(Core::Paths::ExecutablePath().String(), inProjectPath, inRhiType); + const std::string command = ProjectHub::Internal::LaunchCommand(Core::Paths::ExecutablePath().String(), inProjectPath, inRhiType); std::ignore = std::system(command.c_str()); inWindow.RequestClose(); } diff --git a/Editor/Src/SceneClient.cpp b/Editor/Src/SceneClient.cpp index 55b30861f..dcaf0f452 100644 --- a/Editor/Src/SceneClient.cpp +++ b/Editor/Src/SceneClient.cpp @@ -137,7 +137,6 @@ namespace Editor { , renderSurface(nullptr) , editorCamera(Runtime::entityNull) , sceneHovered(false) - , sceneFocused(false) , cameraLooking(false) , cameraAnglesInitialized(false) , cameraYaw(0.0f) @@ -237,8 +236,8 @@ namespace Editor { pendingLookDeltaY = 0.0f; } - const Common::FVec3 moveInput = cameraLooking ? CameraMoveInput() : Common::FVec3Consts::zero; - const bool moving = moveInput.x != 0.0f || moveInput.y != 0.0f || moveInput.z != 0.0f; + const Common::FVec2 moveInput = cameraLooking ? CameraMoveInput() : Common::FVec2Consts::zero; + const bool moving = moveInput.x != 0.0f || moveInput.y != 0.0f; if (!moving && !cameraLooking) { return; } @@ -246,18 +245,13 @@ namespace Editor { const Common::FQuat orientation = Common::FQuat(Common::FVec3Consts::unitY, Common::FRadian(cameraPitch)) * Common::FQuat(Common::FVec3Consts::unitZ, Common::FRadian(cameraYaw)); - const Common::FVec3 lookForward = orientation.RotateVector(Common::FVec3Consts::unitX); - const Common::FVec3 lookRight = orientation.RotateVector(Common::FVec3Consts::unitY); - Common::FVec3 moveForward(lookForward.x, lookForward.y, 0.0f); - Common::FVec3 moveRight(lookRight.x, lookRight.y, 0.0f); - moveForward.Normalize(); - moveRight.Normalize(); + const Common::FVec3 moveForward = orientation.RotateVector(Common::FVec3Consts::unitX); + const Common::FVec3 moveRight = orientation.RotateVector(Common::FVec3Consts::unitY); registry.Update(editorCamera, [&](Runtime::WorldTransform& transform) -> void { const float moveDelta = Internal::cameraMoveSpeed * inDeltaSeconds; transform.localToWorld.translation += moveForward * (moveInput.x * moveDelta); transform.localToWorld.translation += moveRight * (moveInput.y * moveDelta); - transform.localToWorld.translation += Common::FVec3(0.0f, 0.0f, 1.0f) * (moveInput.z * moveDelta); transform.localToWorld.rotation = orientation; }); } @@ -269,8 +263,7 @@ namespace Editor { void SceneClient::SetSceneFocused(bool inFocused) { - sceneFocused = inFocused; - if (!sceneFocused) { + if (!inFocused && !cameraLooking) { pressedKeys.clear(); } } @@ -339,15 +332,13 @@ namespace Editor { registry.Emplace(editorCamera, cameraTransform); } - Common::FVec3 SceneClient::CameraMoveInput() const + Common::FVec2 SceneClient::CameraMoveInput() const { - Common::FVec3 result(0.0f, 0.0f, 0.0f); + Common::FVec2 result(0.0f, 0.0f); if (pressedKeys.contains(GLFW_KEY_W)) { result.x += 1.0f; } if (pressedKeys.contains(GLFW_KEY_S)) { result.x -= 1.0f; } if (pressedKeys.contains(GLFW_KEY_D)) { result.y += 1.0f; } if (pressedKeys.contains(GLFW_KEY_A)) { result.y -= 1.0f; } - if (pressedKeys.contains(GLFW_KEY_E)) { result.z += 1.0f; } - if (pressedKeys.contains(GLFW_KEY_Q)) { result.z -= 1.0f; } if (result.Model() > 1.0f) { result.Normalize(); } diff --git a/Editor/Src/Utils/LinuxPlatformUtils.cpp b/Editor/Src/Utils/LinuxPlatformUtils.cpp new file mode 100644 index 000000000..a867ff65d --- /dev/null +++ b/Editor/Src/Utils/LinuxPlatformUtils.cpp @@ -0,0 +1,69 @@ +// +// Created by johnk on 2026/7/11. +// + +#include + +#if PLATFORM_LINUX + +#include +#include +#include +#include + +namespace Editor::Internal { + static std::string QuoteShellArgument(const std::string& inArgument) + { + std::string result = "'"; + for (const char character : inArgument) { + if (character == '\'') { + result += "'\\''"; + } else { + result += character; + } + } + result += "'"; + return result; + } + + static bool HasCommand(const char* inCommand) + { + return std::system((std::string("command -v ") + inCommand + " >/dev/null 2>&1").c_str()) == 0; + } + + static std::optional RunDirectoryDialog(const std::string& inCommand) + { + std::FILE* const process = popen(inCommand.c_str(), "r"); + if (process == nullptr) { + return std::nullopt; + } + + std::array buffer {}; + std::string result; + while (std::fgets(buffer.data(), static_cast(buffer.size()), process) != nullptr) { + result += buffer.data(); + } + pclose(process); + + while (!result.empty() && (result.back() == '\n' || result.back() == '\r')) { + result.pop_back(); + } + return result.empty() ? std::nullopt : std::optional(std::move(result)); + } +} + +namespace Editor { + std::optional PlatformUtils::SelectDirectory(const std::string& inTitle, const std::string& inInitialDirectory) + { + if (Internal::HasCommand("zenity")) { + const std::string initialPath = inInitialDirectory.empty() ? std::string() : inInitialDirectory + "/"; + return Internal::RunDirectoryDialog("zenity --file-selection --directory --title=" + Internal::QuoteShellArgument(inTitle) + " --filename=" + Internal::QuoteShellArgument(initialPath) + " 2>/dev/null"); + } + if (Internal::HasCommand("kdialog")) { + return Internal::RunDirectoryDialog("kdialog --getexistingdirectory " + Internal::QuoteShellArgument(inInitialDirectory) + " --title " + Internal::QuoteShellArgument(inTitle) + " 2>/dev/null"); + } + return std::nullopt; + } +} + +#endif diff --git a/Editor/Src/Utils/MacosPlatformUtils.mm b/Editor/Src/Utils/MacosPlatformUtils.mm new file mode 100644 index 000000000..7d729043d --- /dev/null +++ b/Editor/Src/Utils/MacosPlatformUtils.mm @@ -0,0 +1,35 @@ +// +// Created by johnk on 2026/7/11. +// + +#include + +#if PLATFORM_MACOS + +#import + +namespace Editor { + std::optional PlatformUtils::SelectDirectory(const std::string& inTitle, const std::string& inInitialDirectory) + { + @autoreleasepool { + NSOpenPanel* const panel = [NSOpenPanel openPanel]; + panel.title = [NSString stringWithUTF8String:inTitle.c_str()]; + panel.canChooseFiles = NO; + panel.canChooseDirectories = YES; + panel.allowsMultipleSelection = NO; + panel.canCreateDirectories = YES; + + if (!inInitialDirectory.empty()) { + NSString* const initialPath = [NSString stringWithUTF8String:inInitialDirectory.c_str()]; + panel.directoryURL = [NSURL fileURLWithPath:initialPath isDirectory:YES]; + } + + if ([panel runModal] != NSModalResponseOK) { + return std::nullopt; + } + return std::string(panel.URL.path.fileSystemRepresentation); + } + } +} + +#endif diff --git a/Editor/Src/Utils/Win32PlatformUtils.cpp b/Editor/Src/Utils/Win32PlatformUtils.cpp new file mode 100644 index 000000000..3aa0624c2 --- /dev/null +++ b/Editor/Src/Utils/Win32PlatformUtils.cpp @@ -0,0 +1,88 @@ +// +// Created by johnk on 2026/7/11. +// + +#include + +#if PLATFORM_WINDOWS + +#include + +namespace Editor::Internal { + static std::wstring Utf8ToWide(const std::string& inString) + { + if (inString.empty()) { + return {}; + } + + const int length = MultiByteToWideChar(CP_UTF8, 0, inString.data(), static_cast(inString.size()), nullptr, 0); + std::wstring result(static_cast(length), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, inString.data(), static_cast(inString.size()), result.data(), length); + return result; + } + + static std::string WideToUtf8(const wchar_t* inString) + { + const int length = WideCharToMultiByte(CP_UTF8, 0, inString, -1, nullptr, 0, nullptr, nullptr); + if (length <= 1) { + return {}; + } + + std::string result(static_cast(length), '\0'); + WideCharToMultiByte(CP_UTF8, 0, inString, -1, result.data(), length, nullptr, nullptr); + result.resize(static_cast(length - 1)); + return result; + } +} + +namespace Editor { + std::optional PlatformUtils::SelectDirectory(const std::string& inTitle, const std::string& inInitialDirectory) + { + const HRESULT initializeResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + const bool shouldUninitialize = SUCCEEDED(initializeResult); + if (FAILED(initializeResult) && initializeResult != RPC_E_CHANGED_MODE) { + return std::nullopt; + } + + std::optional result; + IFileOpenDialog* dialog = nullptr; + if (SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&dialog)))) { + FILEOPENDIALOGOPTIONS options = 0; + if (SUCCEEDED(dialog->GetOptions(&options))) { + dialog->SetOptions(options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_NOCHANGEDIR); + } + + const std::wstring title = Internal::Utf8ToWide(inTitle); + dialog->SetTitle(title.c_str()); + + if (!inInitialDirectory.empty()) { + IShellItem* initialFolder = nullptr; + const std::wstring initialDirectory = Internal::Utf8ToWide(inInitialDirectory); + if (SUCCEEDED(SHCreateItemFromParsingName(initialDirectory.c_str(), nullptr, IID_PPV_ARGS(&initialFolder)))) { + dialog->SetFolder(initialFolder); + initialFolder->Release(); + } + } + + if (SUCCEEDED(dialog->Show(nullptr))) { + IShellItem* selectedFolder = nullptr; + if (SUCCEEDED(dialog->GetResult(&selectedFolder))) { + PWSTR selectedPath = nullptr; + if (SUCCEEDED(selectedFolder->GetDisplayName(SIGDN_FILESYSPATH, &selectedPath))) { + result = Internal::WideToUtf8(selectedPath); + CoTaskMemFree(selectedPath); + } + selectedFolder->Release(); + } + } + dialog->Release(); + } + + if (shouldUninitialize) { + CoUninitialize(); + } + return result; + } +} + +#endif diff --git a/Engine/Source/Mirror/Include/Mirror/Mirror.h b/Engine/Source/Mirror/Include/Mirror/Mirror.h index c1aff6e0c..63879055a 100644 --- a/Engine/Source/Mirror/Include/Mirror/Mirror.h +++ b/Engine/Source/Mirror/Include/Mirror/Mirror.h @@ -803,6 +803,7 @@ namespace Mirror { void ForEachMemberFunction(const MemberFunctionTraverser& func) const; const TypeInfo* GetTypeInfo() const; size_t SizeOf() const; + size_t AlignOf() const; bool HasDefaultConstructor() const; const Class* GetBaseClass() const; bool IsBaseOf(const Class* derivedClass) const; @@ -853,6 +854,7 @@ namespace Mirror { Id id; const TypeInfo* typeInfo; size_t memorySize; + size_t memoryAlignment; BaseClassGetter baseClassGetter; InplaceGetter inplaceGetter; Caster caster; @@ -875,6 +877,7 @@ namespace Mirror { const TypeInfo* typeInfo; size_t memorySize; + size_t memoryAlignment; BaseClassGetter baseClassGetter; InplaceGetter inplaceGetter; Caster caster; diff --git a/Engine/Source/Mirror/Include/Mirror/Registry.h b/Engine/Source/Mirror/Include/Mirror/Registry.h index 0feac6783..95af5e2cc 100644 --- a/Engine/Source/Mirror/Include/Mirror/Registry.h +++ b/Engine/Source/Mirror/Include/Mirror/Registry.h @@ -522,6 +522,7 @@ namespace Mirror { params.id = inId; params.typeInfo = GetTypeInfo(); params.memorySize = sizeof(C); + params.memoryAlignment = alignof(C); params.baseClassGetter = []() -> const Mirror::Class* { if constexpr (std::is_void_v) { return nullptr; diff --git a/Engine/Source/Mirror/Src/Mirror.cpp b/Engine/Source/Mirror/Src/Mirror.cpp index 23005299b..3f12e8e4e 100644 --- a/Engine/Source/Mirror/Src/Mirror.cpp +++ b/Engine/Source/Mirror/Src/Mirror.cpp @@ -81,17 +81,16 @@ namespace Mirror { } Any::Any() + : arrayLength(0) + , policy(AnyPolicy::max) + , rtti(nullptr) + , info(RefInfo {}) { - Reset(); } Any::~Any() { - if (IsMemoryHolder() && rtti != nullptr) { - for (auto i = 0; i < ElementNum(); i++) { - // rtti->detor(Data(i)); - } - } + Reset(); } Any::Any(Any& inOther) @@ -613,10 +612,15 @@ namespace Mirror { void Any::Reset() { + if (rtti != nullptr && policy == AnyPolicy::memoryHolder) { + for (uint32_t i = 0; i < ElementNum(); i++) { + rtti->detor(Data(i)); + } + } arrayLength = 0; policy = AnyPolicy::max; rtti = nullptr; - info = {}; + info = RefInfo {}; } bool Any::Empty() const @@ -1430,6 +1434,7 @@ namespace Mirror { : ReflNode(std::move(params.id)) , typeInfo(params.typeInfo) , memorySize(params.memorySize) + , memoryAlignment(params.memoryAlignment) , baseClassGetter(std::move(params.baseClassGetter)) , inplaceGetter(std::move(params.inplaceGetter)) , caster(std::move(params.caster)) @@ -1628,6 +1633,11 @@ namespace Mirror { return memorySize; } + size_t Class::AlignOf() const + { + return memoryAlignment; + } + bool Class::HasDefaultConstructor() const { return HasConstructor(IdPresets::defaultCtor); diff --git a/Engine/Source/Mirror/Test/AnyTest.cpp b/Engine/Source/Mirror/Test/AnyTest.cpp index 221e87463..500279325 100644 --- a/Engine/Source/Mirror/Test/AnyTest.cpp +++ b/Engine/Source/Mirror/Test/AnyTest.cpp @@ -133,6 +133,23 @@ TEST(AnyTest, DetorTest) ASSERT_FALSE(live); } +TEST(AnyTest, HolderDetorTest) +{ + bool live = false; + { + Any value = AnyDtorTest(live); + ASSERT_TRUE(live); + } + ASSERT_FALSE(live); + + { + Any value = AnyDtorTest(live); + ASSERT_TRUE(live); + value.Reset(); + ASSERT_FALSE(live); + } +} + TEST(AnyTest, CopyCtorTest) { bool called = false; diff --git a/Engine/Source/Mirror/Test/RegistryTest.cpp b/Engine/Source/Mirror/Test/RegistryTest.cpp index 02928791f..39e21d0ab 100644 --- a/Engine/Source/Mirror/Test/RegistryTest.cpp +++ b/Engine/Source/Mirror/Test/RegistryTest.cpp @@ -761,6 +761,7 @@ TEST(RegistryTest, ClassMiscMetadataTest) ASSERT_EQ(clazz.GetTypeInfo()->id, Mirror::GetTypeInfo()->id); ASSERT_EQ(clazz.SizeOf(), sizeof(C2)); + ASSERT_EQ(clazz.AlignOf(), alignof(C2)); } TEST(RegistryTest, ClassInplaceGetObjectTest) diff --git a/Engine/Source/Render/Include/Render/RenderCache.h b/Engine/Source/Render/Include/Render/RenderCache.h index eab7e27dd..7742552f8 100644 --- a/Engine/Source/Render/Include/Render/RenderCache.h +++ b/Engine/Source/Render/Include/Render/RenderCache.h @@ -228,6 +228,7 @@ namespace Render { class SamplerCache { public: static SamplerCache& Get(RHI::Device& device); + static void Destroy(RHI::Device& device); ~SamplerCache(); Sampler* GetOrCreate(const RSamplerDesc& desc); @@ -244,6 +245,7 @@ namespace Render { class PipelineCache { public: static PipelineCache& Get(RHI::Device& device); + static void Destroy(RHI::Device& device); ~PipelineCache(); // TODO offline pipeline cache @@ -264,6 +266,7 @@ namespace Render { class ResourceViewCache { public: static ResourceViewCache& Get(RHI::Device& device); + static void Destroy(RHI::Device& device); ~ResourceViewCache(); RHI::BufferView* GetOrCreate(RHI::Buffer* buffer, const RHI::BufferViewCreateInfo& inDesc); @@ -297,6 +300,7 @@ namespace Render { class BindGroupCache { public: static BindGroupCache& Get(RHI::Device& device); + static void Destroy(RHI::Device& device); ~BindGroupCache(); RHI::BindGroup* Allocate(const RHI::BindGroupCreateInfo& inCreateInfo); @@ -313,4 +317,6 @@ namespace Render { RHI::Device& device; std::vector, AllocateFrameNumber>> bindGroups; }; + + void DestroyDeviceResources(RHI::Device& device); } diff --git a/Engine/Source/Render/Include/Render/RenderModule.h b/Engine/Source/Render/Include/Render/RenderModule.h index ec749f147..908147367 100644 --- a/Engine/Source/Render/Include/Render/RenderModule.h +++ b/Engine/Source/Render/Include/Render/RenderModule.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -30,10 +31,12 @@ namespace Render { void DeInitialize(); RHI::Device* GetDevice() const; Render::RenderThread& GetRenderThread() const; + void BeginFrame() const; Scene* NewScene() const; ViewState* NewViewState() const; View CreateView() const; - StandardRenderer CreateStandardRenderer(const StandardRenderer::Params& inParams) const; + std::future CompileShaderTypes(const std::vector& inShaderTypes, const ShaderCompileOptions& inOptions) const; + Common::UniquePtr CreateStandardRenderer(const StandardRenderer::Params& inParams) const; private: bool initialized; diff --git a/Engine/Source/Render/Include/Render/Renderer.h b/Engine/Source/Render/Include/Render/Renderer.h index a397ec211..ea9714383 100644 --- a/Engine/Source/Render/Include/Render/Renderer.h +++ b/Engine/Source/Render/Include/Render/Renderer.h @@ -30,6 +30,8 @@ namespace Render { RHI::Fence* signalFence; }; + NonCopyable(Renderer) + explicit Renderer(const Params& inParams); virtual ~Renderer(); diff --git a/Engine/Source/Render/Include/Render/ResourcePool.h b/Engine/Source/Render/Include/Render/ResourcePool.h index 4d2ba0032..7cda0a8f5 100644 --- a/Engine/Source/Render/Include/Render/ResourcePool.h +++ b/Engine/Source/Render/Include/Render/ResourcePool.h @@ -55,6 +55,7 @@ namespace Render { using DescType = typename PooledResTraits::DescType; static ResourcePool& Get(RHI::Device& device); + static void Destroy(RHI::Device& device); ResRefType Allocate(const DescType& desc); size_t Size() const; @@ -62,7 +63,10 @@ namespace Render { void Invalidate(); private: + using DeviceMap = std::unordered_map>; + explicit ResourcePool(RHI::Device& inDevice); + static DeviceMap& GetDeviceMap(); RHI::Device& device; std::vector pooledResources; @@ -145,13 +149,30 @@ namespace Render { template ResourcePool& ResourcePool::Get(RHI::Device& device) { - static std::unordered_map> deviceMap; + auto& deviceMap = GetDeviceMap(); if (!deviceMap.contains(&device)) { deviceMap.emplace(std::make_pair(&device, Common::UniquePtr(new ResourcePool(device)))); } return *deviceMap.at(&device); } + template + void ResourcePool::Destroy(RHI::Device& device) + { + auto& deviceMap = GetDeviceMap(); + if (const auto iter = deviceMap.find(&device); iter != deviceMap.end()) { + iter->second->Invalidate(); + deviceMap.erase(iter); + } + } + + template + typename ResourcePool::DeviceMap& ResourcePool::GetDeviceMap() + { + static DeviceMap deviceMap; + return deviceMap; + } + template ResourcePool::ResourcePool(RHI::Device& inDevice) : device(inDevice) diff --git a/Engine/Source/Render/Include/Render/Shader.h b/Engine/Source/Render/Include/Render/Shader.h index 8400ab66d..bf5213a42 100644 --- a/Engine/Source/Render/Include/Render/Shader.h +++ b/Engine/Source/Render/Include/Render/Shader.h @@ -366,6 +366,7 @@ namespace Render { class ShaderMap { public: static ShaderMap& Get(RHI::Device& inDevice); + static void Destroy(RHI::Device& inDevice); ~ShaderMap(); diff --git a/Engine/Source/Render/SharedSrc/RenderModule.cpp b/Engine/Source/Render/SharedSrc/RenderModule.cpp index 8cbc54757..9a16e0d51 100644 --- a/Engine/Source/Render/SharedSrc/RenderModule.cpp +++ b/Engine/Source/Render/SharedSrc/RenderModule.cpp @@ -3,7 +3,9 @@ // #include +#include #include +#include #include namespace Render { @@ -52,6 +54,8 @@ namespace Render { RenderThread::Get().Stop(); RenderWorkerThreads::Get().Stop(); + DestroyDeviceResources(*rhiDevice); + rhiInstance = nullptr; rhiDevice = nullptr; initialized = false; @@ -67,6 +71,15 @@ namespace Render { return RenderThread::Get(); } + void RenderModule::BeginFrame() const // NOLINT + { + ShaderArtifactRegistry::Get().PerformThreadCopy(); + BufferPool::Get(*rhiDevice).Forfeit(); + TexturePool::Get(*rhiDevice).Forfeit(); + ResourceViewCache::Get(*rhiDevice).Forfeit(); + BindGroupCache::Get(*rhiDevice).Forfeit(); + } + Scene* RenderModule::NewScene() const // NOLINT { return new Scene(); @@ -82,9 +95,14 @@ namespace Render { return View(); } - StandardRenderer RenderModule::CreateStandardRenderer(const StandardRenderer::Params& inParams) const // NOLINT + std::future RenderModule::CompileShaderTypes(const std::vector& inShaderTypes, const ShaderCompileOptions& inOptions) const + { + return ShaderTypeCompiler::Get().Compile(inShaderTypes, inOptions); + } + + Common::UniquePtr RenderModule::CreateStandardRenderer(const StandardRenderer::Params& inParams) const // NOLINT { - return StandardRenderer(inParams); + return Common::UniquePtr(new StandardRenderer(inParams)); } } // namespace Render diff --git a/Engine/Source/Render/Src/RenderCache.cpp b/Engine/Source/Render/Src/RenderCache.cpp index 14e26aa9e..700a885ff 100644 --- a/Engine/Source/Render/Src/RenderCache.cpp +++ b/Engine/Source/Render/Src/RenderCache.cpp @@ -9,11 +9,19 @@ #include #include +#include namespace Render::Internal { constexpr uint64_t resourceViewCacheReleaseFrameLatency = 2; constexpr uint64_t bindGroupCacheReleaseFrameLatency = 2; + template + static std::unordered_map>& GetDeviceCacheMap() + { + static std::unordered_map> map; + return map; + } + static uint64_t CombineHashes(const std::vector& hashes) { return Common::HashUtils::CityHash(hashes.data(), hashes.size() * sizeof(uint64_t)); @@ -97,6 +105,7 @@ namespace Render { class PipelineLayoutCache { public: static PipelineLayoutCache& Get(RHI::Device& device); + static void Destroy(RHI::Device& device); ~PipelineLayoutCache(); void Invalidate(); @@ -121,7 +130,7 @@ namespace Render { PipelineLayoutCache& PipelineLayoutCache::Get(RHI::Device& device) { - static std::unordered_map> map; + auto& map = Internal::GetDeviceCacheMap(); if (const auto iter = map.find(&device); iter == map.end()) { @@ -130,6 +139,11 @@ namespace Render { return *map[&device]; } + void PipelineLayoutCache::Destroy(RHI::Device& device) + { + Internal::GetDeviceCacheMap().erase(&device); + } + PipelineLayoutCache::PipelineLayoutCache(RHI::Device& inDevice) : device(inDevice) { @@ -594,7 +608,7 @@ namespace Render { SamplerCache& SamplerCache::Get(RHI::Device& device) { - static std::unordered_map> map; + auto& map = Internal::GetDeviceCacheMap(); std::unique_lock lock(mutex); const auto iter = map.find(&device); @@ -604,6 +618,11 @@ namespace Render { return *map[&device]; } + void SamplerCache::Destroy(RHI::Device& device) + { + Internal::GetDeviceCacheMap().erase(&device); + } + SamplerCache::SamplerCache(RHI::Device& inDevice) : device(inDevice) { @@ -625,7 +644,7 @@ namespace Render { PipelineCache& PipelineCache::Get(RHI::Device& device) { - static std::unordered_map> map; + auto& map = Internal::GetDeviceCacheMap(); std::unique_lock lock(mutex); if (const auto iter = map.find(&device); @@ -635,6 +654,16 @@ namespace Render { return *map[&device]; } + void PipelineCache::Destroy(RHI::Device& device) + { + auto& map = Internal::GetDeviceCacheMap(); + if (const auto iter = map.find(&device); iter != map.end()) { + iter->second->Invalidate(); + map.erase(iter); + } + PipelineLayoutCache::Destroy(device); + } + PipelineCache::PipelineCache(RHI::Device& inDevice) : device(inDevice) { @@ -673,7 +702,7 @@ namespace Render { ResourceViewCache& ResourceViewCache::Get(RHI::Device& device) { - static std::unordered_map> map; + auto& map = Internal::GetDeviceCacheMap(); std::unique_lock lock(mutex); if (!map.contains(&device)) { @@ -682,6 +711,11 @@ namespace Render { return *map.at(&device); } + void ResourceViewCache::Destroy(RHI::Device& device) + { + Internal::GetDeviceCacheMap().erase(&device); + } + ResourceViewCache::ResourceViewCache(RHI::Device& inDevice) : device(inDevice) { @@ -760,7 +794,7 @@ namespace Render { BindGroupCache& BindGroupCache::Get(RHI::Device& device) { - static std::unordered_map> map; + auto& map = Internal::GetDeviceCacheMap(); std::unique_lock lock(mutex); if (!map.contains(&device)) { @@ -769,6 +803,11 @@ namespace Render { return *map.at(&device); } + void BindGroupCache::Destroy(RHI::Device& device) + { + Internal::GetDeviceCacheMap().erase(&device); + } + BindGroupCache::~BindGroupCache() = default; RHI::BindGroup* BindGroupCache::Allocate(const RHI::BindGroupCreateInfo& inCreateInfo) @@ -800,4 +839,15 @@ namespace Render { : device(inDevice) { } + + void DestroyDeviceResources(RHI::Device& device) + { + BindGroupCache::Destroy(device); + PipelineCache::Destroy(device); + SamplerCache::Destroy(device); + ResourceViewCache::Destroy(device); + ShaderMap::Destroy(device); + BufferPool::Destroy(device); + TexturePool::Destroy(device); + } } // namespace Render diff --git a/Engine/Source/Render/Src/Shader.cpp b/Engine/Source/Render/Src/Shader.cpp index 89416e107..9053c253e 100644 --- a/Engine/Source/Render/Src/Shader.cpp +++ b/Engine/Source/Render/Src/Shader.cpp @@ -9,6 +9,14 @@ #include #include +namespace Render::Internal { + static std::unordered_map>& GetShaderMaps() + { + static std::unordered_map> instances; + return instances; + } +} + namespace Render { bool ShaderBoolVariantField::operator==(const ShaderBoolVariantField& inRhs) const { @@ -416,13 +424,18 @@ namespace Render { ShaderMap& ShaderMap::Get(RHI::Device& inDevice) { - static std::unordered_map> instances; + auto& instances = Internal::GetShaderMaps(); if (!instances.contains(&inDevice)) { instances.emplace(&inDevice, Common::UniquePtr(new ShaderMap(inDevice))); } return *instances.at(&inDevice); } + void ShaderMap::Destroy(RHI::Device& inDevice) + { + Internal::GetShaderMaps().erase(&inDevice); + } + ShaderMap::ShaderMap(RHI::Device& inDevice) : device(inDevice) { diff --git a/Engine/Source/Render/Test/ResourcePoolTest.cpp b/Engine/Source/Render/Test/ResourcePoolTest.cpp index 627571ae1..3997d9046 100644 --- a/Engine/Source/Render/Test/ResourcePoolTest.cpp +++ b/Engine/Source/Render/Test/ResourcePoolTest.cpp @@ -4,6 +4,7 @@ #include +#include #include using namespace Render; @@ -18,7 +19,10 @@ struct ResourcePoolTest : testing::Test { .AddQueueRequest(RHI::QueueRequestInfo(RHI::QueueType::graphics, 1))); } - void TearDown() override {} + void TearDown() override + { + DestroyDeviceResources(*device); + } RHI::Instance* instance; Common::UniquePtr device; diff --git a/Engine/Source/Runtime/Include/Runtime/ECS.h b/Engine/Source/Runtime/Include/Runtime/ECS.h index af7e20ba0..320912519 100644 --- a/Engine/Source/Runtime/Include/Runtime/ECS.h +++ b/Engine/Source/Runtime/Include/Runtime/ECS.h @@ -54,13 +54,14 @@ namespace Runtime::Internal { public: explicit CompRtti(CompClass inClass); void Bind(size_t inOffset); + Mirror::Any CopyConstruct(ElemPtr inElem, const Mirror::Any& inOther) const; Mirror::Any MoveConstruct(ElemPtr inElem, const Mirror::Any& inOther) const; - Mirror::Any MoveAssign(ElemPtr inElem, const Mirror::Any& inOther) const; void Destruct(ElemPtr inElem) const; Mirror::Any Get(ElemPtr inElem) const; CompClass Class() const; size_t Offset() const; size_t MemorySize() const; + size_t MemoryAlignment() const; private: using MoveConstructFunc = Mirror::Any(ElemPtr, size_t, const Mirror::Any&); @@ -77,6 +78,12 @@ namespace Runtime::Internal { class RUNTIME_API Archetype { public: explicit Archetype(const std::vector& inRttiVec); + ~Archetype(); + + Archetype(const Archetype& inOther); + Archetype(Archetype&& inOther) noexcept; + Archetype& operator=(const Archetype& inOther); + Archetype& operator=(Archetype&& inOther) noexcept; bool Contains(CompClass inClazz) const; bool ContainsAll(const std::vector& inClasses) const; @@ -103,18 +110,22 @@ namespace Runtime::Internal { const CompRtti& GetCompRtti(CompClass clazz) const; size_t Capacity() const; void Reserve(float inRatio = 1.5f); + void DestroyElements(); + void ReleaseMemory(); ElemPtr AllocateNewElemBack(); - ElemPtr ElemAt(std::vector& inMemory, size_t inIndex) const; + ElemPtr ElemAt(ElemPtr inMemory, size_t inIndex) const; ElemPtr ElemAt(size_t inIndex) const; ArchetypeId id; size_t count; size_t elemSize; + size_t elemAlignment; + size_t capacity; std::vector rttiVec; std::unordered_map rttiMap; std::unordered_map entityMap; std::unordered_map elemMap; - std::vector memory; + ElemPtr memory; }; class EntityPool { @@ -370,9 +381,9 @@ namespace Runtime { auto& Constructed(); auto& Updated(); auto& Removed(); - const auto& Constructed() const; - const auto& Updated() const; - const auto& Removed() const; + const Observer& Constructed() const; + const Observer& Updated() const; + const Observer& Removed() const; private: Observer constructedObserver; @@ -399,9 +410,9 @@ namespace Runtime { void ClearUpdated(); void ClearRemoved(); void Clear(); - const auto& Constructed() const; - const auto& Updated() const; - const auto& Removed() const; + const Observer& Constructed() const; + const Observer& Updated() const; + const Observer& Removed() const; private: Observer constructedObserver; @@ -970,8 +981,8 @@ namespace Runtime { , removedObserver(inRegistry.Observer()) { constructedObserver.ObConstructed(); - constructedObserver.ObUpdated(); - constructedObserver.ObRemoved(); + updatedObserver.ObUpdated(); + removedObserver.ObRemoved(); } template @@ -1058,19 +1069,19 @@ namespace Runtime { } template - const auto& EventsObserver::Constructed() const + const Observer& EventsObserver::Constructed() const { return constructedObserver; } template - const auto& EventsObserver::Updated() const + const Observer& EventsObserver::Updated() const { return updatedObserver; } template - const auto& EventsObserver::Removed() const + const Observer& EventsObserver::Removed() const { return removedObserver; } diff --git a/Engine/Source/Runtime/Src/Asset/Material.cpp b/Engine/Source/Runtime/Src/Asset/Material.cpp index 866f2e42f..ff0343ca0 100644 --- a/Engine/Source/Runtime/Src/Asset/Material.cpp +++ b/Engine/Source/Runtime/Src/Asset/Material.cpp @@ -311,13 +311,11 @@ namespace Runtime { return; } - // the compiler/artifact-registry singletons used here are this module's Render.Static copies, matching the - // ones the renderer executing in this module reads const RHI::RHIType rhiType = EngineHolder::Get().GetRenderModule().GetDevice()->GetGpu().GetInstance().GetRHIType(); Render::ShaderCompileOptions options; options.byteCodeType = rhiType == RHI::RHIType::directX12 ? Render::ShaderByteCodeType::dxil : Render::ShaderByteCodeType::spirv; options.withDebugInfo = static_cast(BUILD_CONFIG_DEBUG); // NOLINT - (void) Render::ShaderTypeCompiler::Get().Compile(typesToCompile, options); + (void) EngineHolder::Get().GetRenderModule().CompileShaderTypes(typesToCompile, options); } MaterialInstance::MaterialInstance(Core::Uri inUri) diff --git a/Engine/Source/Runtime/Src/ECS.cpp b/Engine/Source/Runtime/Src/ECS.cpp index dccd8bbd1..cb32cc1c7 100644 --- a/Engine/Source/Runtime/Src/ECS.cpp +++ b/Engine/Source/Runtime/Src/ECS.cpp @@ -4,6 +4,10 @@ #include +#include +#include +#include + #include #include @@ -19,6 +23,11 @@ namespace Runtime { } namespace Runtime::Internal { + static size_t AlignArchetypeOffset(size_t inOffset, size_t inAlignment) + { + return (inOffset + inAlignment - 1) & ~(inAlignment - 1); + } + static bool IsGlobalCompClass(GCompClass inClass) { return inClass->GetMetaBoolOr(MetaPresets::globalComp, false); @@ -37,18 +46,19 @@ namespace Runtime::Internal { offset = inOffset; } - Mirror::Any CompRtti::MoveConstruct(ElemPtr inElem, const Mirror::Any& inOther) const + Mirror::Any CompRtti::CopyConstruct(ElemPtr inElem, const Mirror::Any& inOther) const { auto* compBegin = static_cast(inElem) + offset; - return clazz->InplaceNewDyn(compBegin, { inOther }); + return clazz->GetConstructor(Mirror::IdPresets::copyCtor).InplaceNewDyn(compBegin, { inOther.ConstRef() }); } - Mirror::Any CompRtti::MoveAssign(ElemPtr inElem, const Mirror::Any& inOther) const + Mirror::Any CompRtti::MoveConstruct(ElemPtr inElem, const Mirror::Any& inOther) const { auto* compBegin = static_cast(inElem) + offset; - auto compRef = clazz->InplaceGetObject(compBegin); - compRef.MoveAssign(inOther); - return compRef; + if (clazz->HasConstructor(Mirror::IdPresets::moveCtor)) { + return clazz->GetConstructor(Mirror::IdPresets::moveCtor).InplaceNewDyn(compBegin, { inOther }); + } + return clazz->GetConstructor(Mirror::IdPresets::copyCtor).InplaceNewDyn(compBegin, { inOther.ConstRef() }); } void CompRtti::Destruct(ElemPtr inElem) const @@ -78,11 +88,19 @@ namespace Runtime::Internal { return clazz->SizeOf(); } + size_t CompRtti::MemoryAlignment() const + { + return clazz->AlignOf(); + } + Archetype::Archetype(const std::vector& inRttiVec) : id(0) , count(0) - , elemSize(1) + , elemSize(0) + , elemAlignment(alignof(std::max_align_t)) + , capacity(0) , rttiVec(inRttiVec) + , memory(nullptr) { rttiMap.reserve(rttiVec.size()); for (auto i = 0; i < rttiVec.size(); i++) { @@ -91,9 +109,80 @@ namespace Runtime::Internal { rttiMap.emplace(clazz, i); id += clazz->GetTypeInfo()->id; + elemAlignment = std::max(elemAlignment, rtti.MemoryAlignment()); + elemSize = AlignArchetypeOffset(elemSize, rtti.MemoryAlignment()); rtti.Bind(elemSize); elemSize += rtti.MemorySize(); } + elemSize = AlignArchetypeOffset(std::max(elemSize, static_cast(1)), elemAlignment); + } + + Archetype::~Archetype() + { + DestroyElements(); + ReleaseMemory(); + } + + Archetype::Archetype(const Archetype& inOther) + : id(inOther.id) + , count(inOther.count) + , elemSize(inOther.elemSize) + , elemAlignment(inOther.elemAlignment) + , capacity(inOther.capacity) + , rttiVec(inOther.rttiVec) + , rttiMap(inOther.rttiMap) + , entityMap(inOther.entityMap) + , elemMap(inOther.elemMap) + , memory(capacity > 0 ? ::operator new(capacity * elemSize, std::align_val_t(elemAlignment)) : nullptr) + { + for (size_t i = 0; i < count; i++) { + for (const auto& rtti : rttiVec) { + rtti.CopyConstruct(ElemAt(i), rtti.Get(inOther.ElemAt(i))); + } + } + } + + Archetype::Archetype(Archetype&& inOther) noexcept + : id(inOther.id) + , count(std::exchange(inOther.count, 0)) + , elemSize(inOther.elemSize) + , elemAlignment(inOther.elemAlignment) + , capacity(std::exchange(inOther.capacity, 0)) + , rttiVec(std::move(inOther.rttiVec)) + , rttiMap(std::move(inOther.rttiMap)) + , entityMap(std::move(inOther.entityMap)) + , elemMap(std::move(inOther.elemMap)) + , memory(std::exchange(inOther.memory, nullptr)) + { + } + + Archetype& Archetype::operator=(const Archetype& inOther) + { + if (this != &inOther) { + *this = Archetype(inOther); + } + return *this; + } + + Archetype& Archetype::operator=(Archetype&& inOther) noexcept + { + if (this == &inOther) { + return *this; + } + + DestroyElements(); + ReleaseMemory(); + id = inOther.id; + count = std::exchange(inOther.count, 0); + elemSize = inOther.elemSize; + elemAlignment = inOther.elemAlignment; + capacity = std::exchange(inOther.capacity, 0); + rttiVec = std::move(inOther.rttiVec); + rttiMap = std::move(inOther.rttiMap); + entityMap = std::move(inOther.entityMap); + elemMap = std::move(inOther.elemMap); + memory = std::exchange(inOther.memory, nullptr); + return *this; } bool Archetype::Contains(CompClass inClazz) const @@ -159,14 +248,25 @@ namespace Runtime::Internal { const auto elemIndex = entityMap.at(inEntity); ElemPtr elem = ElemAt(elemIndex); const auto lastElemIndex = count - 1; - const auto entityToLastElem = elemMap.at(lastElemIndex); ElemPtr lastElem = ElemAt(lastElemIndex); - for (const auto& rtti : rttiVec) { - rtti.MoveAssign(elem, rtti.Get(lastElem)); + + if (elemIndex == lastElemIndex) { + for (const auto& rtti : rttiVec) { + rtti.Destruct(elem); + } + } else { + for (const auto& rtti : rttiVec) { + rtti.Destruct(elem); + rtti.MoveConstruct(elem, rtti.Get(lastElem)); + rtti.Destruct(lastElem); + } + + const auto entityToLastElem = elemMap.at(lastElemIndex); + entityMap.at(entityToLastElem) = elemIndex; + elemMap.at(elemIndex) = entityToLastElem; } - entityMap.at(entityToLastElem) = elemIndex; + entityMap.erase(inEntity); - elemMap.at(elemIndex) = entityToLastElem; elemMap.erase(lastElemIndex); count--; } @@ -231,36 +331,57 @@ namespace Runtime::Internal { return rttiVec[rttiMap.at(clazz)]; } - ElemPtr Archetype::ElemAt(std::vector& inMemory, size_t inIndex) const // NOLINT + ElemPtr Archetype::ElemAt(ElemPtr inMemory, size_t inIndex) const { - return inMemory.data() + (inIndex * elemSize); + return static_cast(inMemory) + (inIndex * elemSize); } ElemPtr Archetype::ElemAt(size_t inIndex) const { - return ElemAt(const_cast&>(memory), inIndex); + return ElemAt(memory, inIndex); } size_t Archetype::Capacity() const { - return memory.size() / elemSize; + return capacity; } void Archetype::Reserve(float inRatio) { Assert(inRatio > 1.0f); const size_t newCapacity = static_cast(std::ceil(static_cast(std::max(Capacity(), static_cast(1))) * inRatio)); - std::vector newMemory(newCapacity * elemSize); + ElemPtr newMemory = ::operator new(newCapacity * elemSize, std::align_val_t(elemAlignment)); - for (auto i = 0; i < count; i++) { + for (size_t i = 0; i < count; i++) { for (const auto& rtti : rttiVec) { - void* dstElem = ElemAt(newMemory, i); - void* srcElem = ElemAt(i); + ElemPtr dstElem = ElemAt(newMemory, i); + ElemPtr srcElem = ElemAt(i); rtti.MoveConstruct(dstElem, rtti.Get(srcElem)); rtti.Destruct(srcElem); } } - memory = std::move(newMemory); + ReleaseMemory(); + memory = newMemory; + capacity = newCapacity; + } + + void Archetype::DestroyElements() + { + for (size_t i = 0; i < count; i++) { + for (const auto& rtti : rttiVec) { + rtti.Destruct(ElemAt(i)); + } + } + count = 0; + } + + void Archetype::ReleaseMemory() + { + if (memory != nullptr) { + ::operator delete(memory, std::align_val_t(elemAlignment)); + memory = nullptr; + } + capacity = 0; } void* Archetype::AllocateNewElemBack() @@ -614,17 +735,17 @@ namespace Runtime { ClearRemoved(); } - const auto& EventsObserverDyn::Constructed() const + const Observer& EventsObserverDyn::Constructed() const { return constructedObserver; } - const auto& EventsObserverDyn::Updated() const + const Observer& EventsObserverDyn::Updated() const { return updatedObserver; } - const auto& EventsObserverDyn::Removed() const + const Observer& EventsObserverDyn::Removed() const { return removedObserver; } @@ -655,6 +776,9 @@ namespace Runtime { ECRegistry& ECRegistry::operator=(const ECRegistry& inOther) { + if (this == &inOther) { + return *this; + } entities = inOther.entities; globalComps = inOther.globalComps; archetypes = inOther.archetypes; @@ -663,6 +787,9 @@ namespace Runtime { ECRegistry& ECRegistry::operator=(ECRegistry&& inOther) noexcept { + if (this == &inOther) { + return *this; + } entities = std::move(inOther.entities); globalComps = std::move(inOther.globalComps); archetypes = std::move(inOther.archetypes); @@ -685,7 +812,11 @@ namespace Runtime { void ECRegistry::Destroy(Entity inEntity) { Assert(Valid(inEntity)); - archetypes.at(entities.GetArchetype(inEntity)).EraseElem(inEntity); + Internal::Archetype& archetype = archetypes.at(entities.GetArchetype(inEntity)); + for (const auto& compRtti : archetype.GetRttiVec()) { + NotifyRemoveDyn(compRtti.Class(), inEntity); + } + archetype.EraseElem(inEntity); entities.Free(inEntity); } diff --git a/Engine/Source/Runtime/Src/Engine.cpp b/Engine/Source/Runtime/Src/Engine.cpp index eb3e9eb6b..256197199 100644 --- a/Engine/Source/Runtime/Src/Engine.cpp +++ b/Engine/Source/Runtime/Src/Engine.cpp @@ -10,9 +10,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -38,7 +35,6 @@ namespace Runtime { Engine::~Engine() { - Render::RenderWorkerThreads::Get().Stop(); renderModule->DeInitialize(); ::Core::ModuleManager::Get().Unload("Render"); @@ -69,17 +65,11 @@ namespace Runtime { Core::ThreadContext::IncFrameNumber(); - // the shader artifact copy and pool forfeits intentionally run through this module's Render.Static singleton - // copies: the renderer itself executes in this module too, so these are the instances it reads auto& renderThread = renderModule->GetRenderThread(); - renderThread.EmplaceTask([device = renderModule->GetDevice()]() -> void { + renderThread.EmplaceTask([renderModule = renderModule]() -> void { Core::ThreadContext::IncFrameNumber(); Core::Console::Get().PerformRenderThreadSettingsCopy(); - Render::ShaderArtifactRegistry::Get().PerformThreadCopy(); - Render::BufferPool::Get(*device).Forfeit(); - Render::TexturePool::Get(*device).Forfeit(); - Render::ResourceViewCache::Get(*device).Forfeit(); - Render::BindGroupCache::Get(*device).Forfeit(); + renderModule->BeginFrame(); }); for (auto* world : worlds) { @@ -112,9 +102,6 @@ namespace Runtime { Render::RenderModuleInitParams initParams; initParams.rhiType = RHI::GetRHITypeByAbbrString(inRhiTypeStr); renderModule->Initialize(initParams); - // the render module started its own module-local worker threads, render-layer code inlined into this module - // (e.g. render graph buffer uploads) binds to this module's singleton copy which must be started as well - Render::RenderWorkerThreads::Get().Start(); LogInfo(Render, "RHI type: {}", inRhiTypeStr); } diff --git a/Engine/Source/Runtime/Src/System/Render.cpp b/Engine/Source/Runtime/Src/System/Render.cpp index 428180a9d..0ea566070 100644 --- a/Engine/Source/Runtime/Src/System/Render.cpp +++ b/Engine/Source/Runtime/Src/System/Render.cpp @@ -75,12 +75,11 @@ namespace Runtime { rendererParams.signalFence = fence; auto renderer = renderModule->CreateStandardRenderer(rendererParams); - renderer.Render(inDeltaTimeSeconds); + renderer->Render(inDeltaTimeSeconds); if (window != nullptr) { window->Present(); } - // this frame's gpu work must finish before the renderer (and its recorded command buffers / - // transient resources) goes out of scope, and it keeps the shared semaphores safe to reuse + // Renderer-owned command buffers and transient resources must outlive the submitted GPU work. fence->Wait(); }); } diff --git a/Engine/Source/Runtime/Test/ECSTest.cpp b/Engine/Source/Runtime/Test/ECSTest.cpp index 66dee1313..d8caa0641 100644 --- a/Engine/Source/Runtime/Test/ECSTest.cpp +++ b/Engine/Source/Runtime/Test/ECSTest.cpp @@ -5,6 +5,50 @@ #include #include +#include + +uint32_t LifetimeComp::instanceCount = 0; + +LifetimeComp::LifetimeComp() +{ + instanceCount++; +} + +LifetimeComp::LifetimeComp(std::string inValue) + : value(std::move(inValue)) +{ + instanceCount++; +} + +LifetimeComp::LifetimeComp(const LifetimeComp& inOther) + : value(inOther.value) +{ + instanceCount++; +} + +LifetimeComp::LifetimeComp(LifetimeComp&& inOther) noexcept + : value(std::move(inOther.value)) +{ + instanceCount++; +} + +LifetimeComp& LifetimeComp::operator=(const LifetimeComp& inOther) +{ + value = inOther.value; + return *this; +} + +LifetimeComp& LifetimeComp::operator=(LifetimeComp&& inOther) noexcept +{ + value = std::move(inOther.value); + return *this; +} + +LifetimeComp::~LifetimeComp() +{ + instanceCount--; +} + TEST(ECSTest, EntityTest) { ECRegistry registry; @@ -24,11 +68,16 @@ TEST(ECSTest, EntityTest) const auto entity3 = registry.Create(); registry.Destroy(entity0); const auto entity4 = registry.Create(); + ASSERT_EQ(entity4, entity0); cur = { entity1, entity2, entity3, entity4 }; registry.Each([&](const auto e) -> void { ASSERT_TRUE(cur.contains(e)); }); + + registry.Clear(); + ASSERT_EQ(registry.Count(), 0); + ASSERT_EQ(registry.Create(), 1); } TEST(ECSTest, ComponentStaticTest) @@ -63,6 +112,15 @@ TEST(ECSTest, ComponentStaticTest) ASSERT_TRUE(constRegistry.Has(entity2)); ASSERT_EQ(constRegistry.Get(entity2).value, 3); ASSERT_EQ(constRegistry.Get(entity2).value, 4.0f); + ASSERT_EQ(registry.CompCount(entity2), 2); + std::unordered_set compClasses; + registry.CompEach(entity2, [&](CompClass clazz) -> void { + compClasses.emplace(clazz); + }); + ASSERT_EQ(compClasses, (std::unordered_set { + &Mirror::Class::Get(), + &Mirror::Class::Get() + })); const auto entity3 = registry.Create(); registry.Emplace(entity3, 5); @@ -99,6 +157,39 @@ TEST(ECSTest, ComponentStaticTest) for (const auto& [entity, compB] : view3) { ASSERT_TRUE(expected.contains(entity)); } + + ASSERT_EQ(reinterpret_cast(registry.Find(entity1)) % alignof(CompA), 0); + ASSERT_EQ(reinterpret_cast(registry.Find(entity2)) % alignof(CompB), 0); +} + +TEST(ECSTest, ComponentLifetimeTest) +{ + const auto baselineInstanceCount = LifetimeComp::instanceCount; + const std::string firstValue = "first component value that does not use the small string optimization"; + const std::string secondValue = "second component value that does not use the small string optimization"; + { + ECRegistry registry; + const auto entity0 = registry.Create(); + const auto entity1 = registry.Create(); + registry.Emplace(entity0, std::string(firstValue)); + registry.Emplace(entity1, std::string(secondValue)); + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 2); + + registry.Emplace(entity0, 1); + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 2); + ASSERT_EQ(registry.Get(entity0).value, firstValue); + ASSERT_EQ(registry.Get(entity1).value, secondValue); + + registry.Remove(entity0); + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 2); + registry.Destroy(entity1); + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 1); + ASSERT_EQ(registry.Get(entity0).value, firstValue); + + registry.Clear(); + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount); + } + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount); } TEST(ECSTest, ComponentDynamicTest) @@ -181,6 +272,10 @@ TEST(ECSTest, ComponentDynamicTest) view5.Each([&](Entity e, const CompA& compA, const CompB& compB) -> void { ASSERT_TRUE(expected.contains(e)); }); + + registry.RemoveDyn(compAClass, entity0); + ASSERT_FALSE(registry.HasDyn(compAClass, entity0)); + ASSERT_TRUE(registry.FindDyn(compAClass, entity0).Empty()); } TEST(ECSTest, GlobalComponentStaticTest) @@ -265,6 +360,11 @@ TEST(ECSTest, ComponentEventStaticTest) registry.Remove(entity1); ASSERT_EQ(count, EventCounts(2, 3, 2)); + const auto entity2 = registry.Create(); + registry.Emplace(entity2, 6); + registry.Destroy(entity2); + ASSERT_EQ(count, EventCounts(3, 3, 3)); + registry.Events().onConstructed.Unbind(constructedCallback); registry.Events().onUpdated.Unbind(updatedCallback); registry.Events().onRemove.Unbind(removeCallback); @@ -400,9 +500,75 @@ TEST(ECSTest, ObserverTest) }); ASSERT_EQ(observer.Count(), 2); expected = { entity0, entity1 }; - for (const auto e : expected) { + for (const auto e : observer) { ASSERT_TRUE(expected.contains(e)); } + + const auto popped = observer.Pop(); + ASSERT_EQ(observer.Count(), 0); + ASSERT_EQ(std::unordered_set(popped.begin(), popped.end()), expected); + + registry.Update(entity1, [](CompB& compB) -> void { + compB.value = 4.0f; + }); + size_t traversedCount = 0; + observer.EachThenClear([&](Entity e) -> void { + ASSERT_EQ(e, entity1); + traversedCount++; + }); + ASSERT_EQ(traversedCount, 1); + ASSERT_EQ(observer.Count(), 0); +} + +TEST(ECSTest, EventsObserverStaticTest) +{ + ECRegistry registry; + auto observer = registry.EventsObserver(); + const auto entity = registry.Create(); + + registry.Emplace(entity, 1); + ASSERT_EQ(observer.ConstructedCount(), 1); + ASSERT_EQ(observer.UpdatedCount(), 0); + ASSERT_EQ(observer.RemovedCount(), 0); + + registry.Update(entity, [](CompA& compA) -> void { + compA.value = 2; + }); + ASSERT_EQ(observer.ConstructedCount(), 1); + ASSERT_EQ(observer.UpdatedCount(), 1); + ASSERT_EQ(observer.RemovedCount(), 0); + + registry.Remove(entity); + ASSERT_EQ(observer.Constructed().All(), std::vector { entity }); + ASSERT_EQ(observer.Updated().All(), std::vector { entity }); + ASSERT_EQ(observer.Removed().All(), std::vector { entity }); + + observer.ClearUpdated(); + ASSERT_EQ(observer.ConstructedCount(), 1); + ASSERT_EQ(observer.UpdatedCount(), 0); + ASSERT_EQ(observer.RemovedCount(), 1); + observer.Clear(); + ASSERT_EQ(observer.ConstructedCount(), 0); + ASSERT_EQ(observer.RemovedCount(), 0); +} + +TEST(ECSTest, EventsObserverDynamicTest) +{ + const auto* compClass = &Mirror::Class::Get(); + ECRegistry registry; + auto observer = registry.EventsObserverDyn(compClass); + const auto entity = registry.Create(); + + registry.EmplaceDyn(compClass, entity, Mirror::ForwardAsArgList(1)); + registry.NotifyUpdatedDyn(compClass, entity); + registry.Destroy(entity); + + ASSERT_EQ(observer.ConstructedCount(), 1); + ASSERT_EQ(observer.UpdatedCount(), 1); + ASSERT_EQ(observer.RemovedCount(), 1); + ASSERT_EQ(observer.Constructed().All(), std::vector { entity }); + ASSERT_EQ(observer.Updated().All(), std::vector { entity }); + ASSERT_EQ(observer.Removed().All(), std::vector { entity }); } TEST(ECSTest, ECRegistryCopyTest) @@ -418,6 +584,39 @@ TEST(ECSTest, ECRegistryCopyTest) ASSERT_EQ(registry1.Get(entity1).value, 2.0f); } +TEST(ECSTest, ECRegistryValueSemanticsTest) +{ + const auto baselineInstanceCount = LifetimeComp::instanceCount; + const std::string originalValue = "original component value that does not use the small string optimization"; + { + ECRegistry registry0; + const auto entity = registry0.Create(); + registry0.Emplace(entity, std::string(originalValue)); + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 1); + + { + ECRegistry registry1 = registry0; + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 2); + registry1.Get(entity).value = "copied registry component with independent string storage"; + ASSERT_EQ(registry0.Get(entity).value, originalValue); + + ECRegistry registry2; + const auto replacedEntity = registry2.Create(); + registry2.Emplace(replacedEntity, std::string("component replaced by copy assignment")); + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 3); + registry2 = registry0; + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 3); + ASSERT_EQ(registry2.Get(entity).value, registry0.Get(entity).value); + + ECRegistry registry3 = std::move(registry2); + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 3); + ASSERT_EQ(registry3.Get(entity).value, registry0.Get(entity).value); + } + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount + 1); + } + ASSERT_EQ(LifetimeComp::instanceCount, baselineInstanceCount); +} + TEST(ECSTest, ECSRegistrySaveLoadTest) { ECArchive archive; @@ -430,6 +629,9 @@ TEST(ECSTest, ECSRegistrySaveLoadTest) registry.Emplace(entity1, 2.0f); registry.GEmplace(1); registry.GEmplace(2.0f); + const auto transientEntity = registry.Create(); + registry.Emplace(transientEntity, 3); + registry.Emplace(transientEntity); registry.Save(archive); } diff --git a/Engine/Source/Runtime/Test/ECSTest.h b/Engine/Source/Runtime/Test/ECSTest.h index efa448e95..a29240a28 100644 --- a/Engine/Source/Runtime/Test/ECSTest.h +++ b/Engine/Source/Runtime/Test/ECSTest.h @@ -4,6 +4,8 @@ #pragma once +#include + #include #include using namespace Runtime; @@ -40,6 +42,22 @@ struct EClass() CompB { EProperty() float value; }; +struct EClass() LifetimeComp { + EClassBody(LifetimeComp) + + LifetimeComp(); + explicit LifetimeComp(std::string inValue); + LifetimeComp(const LifetimeComp& inOther); + LifetimeComp(LifetimeComp&& inOther) noexcept; + LifetimeComp& operator=(const LifetimeComp& inOther); + LifetimeComp& operator=(LifetimeComp&& inOther) noexcept; + ~LifetimeComp(); + + EProperty() std::string value; + + static uint32_t instanceCount; +}; + struct EClass(globalComp) GCompA { EClassBody(GCompA) diff --git a/Sample/Rendering-BaseTexture/BaseTexture.cpp b/Sample/Rendering-BaseTexture/BaseTexture.cpp index 1057cc244..67e74325e 100644 --- a/Sample/Rendering-BaseTexture/BaseTexture.cpp +++ b/Sample/Rendering-BaseTexture/BaseTexture.cpp @@ -212,11 +212,7 @@ void BaseTexApp::OnDestroy() device->GetQueue(QueueType::graphics, 0)->Flush(fence.Get()); fence->Wait(); - BindGroupCache::Get(*device).Invalidate(); - Render::PipelineCache::Get(*device).Invalidate(); - BufferPool::Get(*device).Invalidate(); - TexturePool::Get(*device).Invalidate(); - ShaderMap::Get(*device).Invalidate(); + DestroyDeviceResources(*device); }); RenderThread::Get().Flush(); diff --git a/Sample/Rendering-SSAO/SSAOApplication.cpp b/Sample/Rendering-SSAO/SSAOApplication.cpp index 34cebbc7f..b7630cdeb 100644 --- a/Sample/Rendering-SSAO/SSAOApplication.cpp +++ b/Sample/Rendering-SSAO/SSAOApplication.cpp @@ -432,11 +432,7 @@ class SSAOApp final : public Application { device->GetQueue(QueueType::graphics, 0)->Flush(fence.Get()); fence->Wait(); - BindGroupCache::Get(*device).Invalidate(); - Render::PipelineCache::Get(*device).Invalidate(); - BufferPool::Get(*device).Invalidate(); - TexturePool::Get(*device).Invalidate(); - ShaderMap::Get(*device).Invalidate(); + DestroyDeviceResources(*device); }); RenderThread::Get().Flush(); diff --git a/Sample/Rendering-Triangle/Triangle.cpp b/Sample/Rendering-Triangle/Triangle.cpp index 4055c75be..46fb3b7d7 100644 --- a/Sample/Rendering-Triangle/Triangle.cpp +++ b/Sample/Rendering-Triangle/Triangle.cpp @@ -197,11 +197,7 @@ void TriangleApplication::OnDestroy() device->GetQueue(QueueType::graphics, 0)->Flush(fence.Get()); fence->Wait(); - BindGroupCache::Get(*device).Invalidate(); - Render::PipelineCache::Get(*device).Invalidate(); - BufferPool::Get(*device).Invalidate(); - TexturePool::Get(*device).Invalidate(); - ShaderMap::Get(*device).Invalidate(); + DestroyDeviceResources(*device); }); RenderThread::Get().Flush();