chore: sync editor worktree changes

This commit is contained in:
2026-04-05 01:25:09 +08:00
parent 061e74d98f
commit b2774f1745
27 changed files with 1014 additions and 281 deletions

View File

@@ -1,8 +1,10 @@
# XCEditor # XCEditor
`editor/` 是 XCEngine 当前随仓库维护的桌面编辑器模块。它不是套独立渲染器,而是 `D3D12` 宿主应用,用来承接引擎 `Rendering + RHI + Scene + Scripting` 主链。 `editor/` 是 XCEngine 当前随仓库维护的桌面编辑器模块。它不是第二套独立渲染器,而是 `D3D12` 宿主应用,用来承接引擎 `Rendering + RHI + Scene + Scripting` 主链。
当前 editor 已经具备: 如果你想了解整个工作区,先看仓库根目录的 [README.md](../README.md);如果你要直接改 editor 或相关引擎接线,再看 [AGENT.md](../AGENT.md)。
## 当前能力
- Scene / Game viewport 离屏渲染接入 - Scene / Game viewport 离屏渲染接入
- object-id picking 与选中描边 - object-id picking 与选中描边
@@ -10,20 +12,22 @@
- 项目根目录解析与 `Project.xcproject` 加载 - 项目根目录解析与 `Project.xcproject` 加载
- `Assets + .meta + Library` 风格项目目录接入 - `Assets + .meta + Library` 风格项目目录接入
- `ScriptComponent` 的脚本类与字段编辑入口 - `ScriptComponent` 的脚本类与字段编辑入口
- 项目脚本程序集重建与运行时重载入口
## 当前定位 ## 当前定位
如果你想理解当前 editor先把它当成三层: 理解当前 editor推荐把它拆成四层:
1. `Win32 + D3D12` 宿主窗口与 ImGui backend 1. `Win32 + D3D12` 宿主窗口与 ImGui backend
2. `ViewportHostService` 对引擎渲染链路的接线 2. `ViewportHostService` 以及 Scene View helper 对引擎渲染链路的接线
3. `panels/``Managers/``ComponentEditors/` 这些编辑器业务层 3. `panels/``Managers/``ComponentEditors/` 这些编辑器业务层
4. `Scripting/` 对项目脚本程序集和运行时状态的桥接
当前不再把 editor 视为旧式UI sample”。它已经是引擎工作区的正式入口之一。 当前不再把 editor 当成“旧式 UI sample”。它已经是引擎工作区的正式入口之一。
## 构建 ## 构建
推荐直接在仓库根目录构建,而不是单独进入 `editor/` 目录。 推荐始终在仓库根目录构建,而不是单独进入 `editor/` 目录。
### 前置要求 ### 前置要求
@@ -32,19 +36,13 @@
- CMake 3.15+ - CMake 3.15+
- Vulkan SDK - Vulkan SDK
如果要启用 Mono 脚本运行时,还需要: 如果要启用 Mono 脚本运行时,还需要:
- .NET SDK - .NET SDK
- `参考/Fermion/Fermion/external/mono` 下的 Mono 依赖 - `参考/Fermion/Fermion/external/mono`可用的 Mono 依赖
### 配置 ### 配置
```bash
cmake -S .. -B ..\build -A x64
```
更常见的做法是直接在仓库根目录运行:
```bash ```bash
cmake -S . -B build -A x64 cmake -S . -B build -A x64
``` ```
@@ -85,48 +83,99 @@ cmake --build build --config Debug --target XCEditor
cmake --build build --config Debug --target xcengine_project_managed_assemblies cmake --build build --config Debug --target xcengine_project_managed_assemblies
``` ```
该 target 会把程序集放到 成功重建后,项目脚本程序集目录通常应包含
- `project/Library/ScriptAssemblies/XCEngine.ScriptCore.dll` - `project/Library/ScriptAssemblies/XCEngine.ScriptCore.dll`
- `project/Library/ScriptAssemblies/GameScripts.dll` - `project/Library/ScriptAssemblies/GameScripts.dll`
- `project/Library/ScriptAssemblies/mscorlib.dll` - `project/Library/ScriptAssemblies/mscorlib.dll`
当前工作树如果暂时缺少 `XCEngine.ScriptCore.dll`,通常说明项目脚本程序集还没有按最新状态完成重建,而不是 editor 只需要两个程序集。
## 当前目录结构 ## 当前目录结构
```text ```text
editor/ editor/
├── CMakeLists.txt |- CMakeLists.txt
├── README.md |- README.md
├── resources/ |- bin/ # 输出目录,产物名仍为 XCEngine.exe
│ └── Icons/ |- resources/
├── src/ | `- Icons/
│ ├── Actions/ # 编辑器动作路由 `- src/
├── Commands/ # 命令与实体操作 |- Actions/ # 编辑器动作路由
├── ComponentEditors/ # Inspector 组件编辑器 |- Commands/ # 命令与实体操作
├── Core/ # 应用生命周期、日志、项目根解析、撤销等 |- ComponentEditors/ # Inspector 组件编辑器
├── Layers/ # EditorLayer 等高层组装 |- Core/ # 生命周期、日志、项目根解析、撤销、play session
├── Layout/ |- Layers/ # EditorLayer 等高层组装
│ ├── Managers/ # SceneManager / ProjectManager |- Layout/
├── panels/ # Hierarchy / Scene / Game / Inspector / Project / Console |- Managers/ # SceneManager / ProjectManager / SelectionManager
├── Platform/ # Win32 host、D3D12 backend 辅助 |- panels/
├── UI/ # ImGui bridge 与通用 widget | |- ConsolePanel.cpp
├── Utils/ | |- ConsolePanel.h
│ ├── Viewport/ | |- GameViewPanel.cpp
│ ├── Passes/ # editor viewport overlay pass | |- GameViewPanel.h
├── SceneViewportOverlayBuilder.* | |- HierarchyPanel.cpp
├── SceneViewportPicker.* | |- HierarchyPanel.h
├── SceneViewportMoveGizmo.* | |- InspectorPanel.cpp
├── SceneViewportRotateGizmo.* | |- InspectorPanel.h
├── SceneViewportScaleGizmo.* | |- MenuBar.cpp
├── ViewportHostRenderFlowUtils.h | |- MenuBar.h
└── ViewportHostService.h | |- Panel.cpp
│ ├── Application.cpp | |- Panel.h
├── Application.h | |- PanelCollection.h
├── EditorApp.rc | |- ProjectPanel.cpp
│ ├── Theme.cpp | |- ProjectPanel.h
├── Theme.h | |- SceneViewPanel.cpp
└── main.cpp | |- SceneViewPanel.h
└── bin/ | `- ViewportPanelContent.h
|- Platform/ # Win32 host、D3D12 backend 辅助
|- Scripting/ # 项目脚本程序集构建与运行时状态
|- UI/ # ImGui bridge 与通用 widget
|- Utils/
|- Viewport/
| |- Passes/
| | |- SceneViewportEditorOverlayPass.*
| | |- SceneViewportGridPass.*
| | `- SceneViewportSelectionOutlinePass.*
| |- IViewportHostService.h
| |- SceneViewportCameraController.h
| |- SceneViewportChrome.*
| |- SceneViewportEditorModes.h
| |- SceneViewportEditorOverlayData.h
| |- SceneViewportHudOverlay.*
| |- SceneViewportInteractionActions.*
| |- SceneViewportInteractionFrame.h
| |- SceneViewportInteractionResolver.*
| |- SceneViewportMath.h
| |- SceneViewportMoveGizmo.*
| |- SceneViewportNavigation.h
| |- SceneViewportOrientationGizmo.*
| |- SceneViewportOverlayBuilder.*
| |- SceneViewportOverlayFrameCache.*
| |- SceneViewportOverlayHandleBuilder.h
| |- SceneViewportOverlayHitTester.h
| |- SceneViewportOverlayProviders.*
| |- SceneViewportOverlaySpriteResources.*
| |- SceneViewportPassSpecs.h
| |- SceneViewportPicker.*
| |- SceneViewportRenderPlan.h
| |- SceneViewportResourcePaths.h
| |- SceneViewportRotateGizmo.*
| |- SceneViewportScaleGizmo.*
| |- SceneViewportShaderPaths.h # 兼容 include真实 owner 已转到 ResourcePaths
| |- SceneViewportTransformGizmoCoordinator.*
| |- SceneViewportTransformGizmoFrameBuilder.h
| |- ViewportHostRenderFlowUtils.h
| |- ViewportHostRenderTargets.h
| |- ViewportHostService.h
| |- ViewportHostSurfaceUtils.h
| `- ViewportObjectIdPicker.h
|- Application.cpp
|- Application.h
|- EditorApp.rc
|- EditorResources.h
|- Theme.cpp
|- Theme.h
`- main.cpp
``` ```
## 关键模块 ## 关键模块
@@ -140,7 +189,7 @@ editor/
- editor 初始化与关闭 - editor 初始化与关闭
- resource root 设置 - resource root 设置
- scripting runtime 初始化 - scripting runtime 初始化与重载
- ImGui backend 初始化 - ImGui backend 初始化
- `ViewportHostService` 接线 - `ViewportHostService` 接线
@@ -148,25 +197,35 @@ editor/
- `src/Core/ProjectRootResolver.h` - `src/Core/ProjectRootResolver.h`
- `src/Utils/ProjectFileUtils.h` - `src/Utils/ProjectFileUtils.h`
- `src/Managers/ProjectManager.*`
负责: 负责:
- 自动识别仓库内 `project/` - 自动识别仓库内 `project/`
- 解析 `--project` - 解析 `--project`
- 读写 `Project.xcproject` - 读写 `Project.xcproject`
- 当前 Project 面板目录与资源操作
### Viewport ### Viewport
- `src/Viewport/ViewportHostService.h` - `src/Viewport/ViewportHostService.h`
- `src/Viewport/ViewportHostRenderFlowUtils.h` - `src/Viewport/SceneViewportChrome.h`
- `src/Viewport/SceneViewportInteractionFrame.h`
- `src/Viewport/SceneViewportNavigation.h`
- `src/Viewport/SceneViewportTransformGizmoCoordinator.h`
- `src/Viewport/SceneViewportRenderPlan.h`
- `src/Viewport/SceneViewportOverlayBuilder.*` - `src/Viewport/SceneViewportOverlayBuilder.*`
- `src/Viewport/SceneViewportOverlaySpriteResources.*`
- `src/Viewport/SceneViewportResourcePaths.h`
- `src/Viewport/Passes/SceneViewportEditorOverlayPass.*` - `src/Viewport/Passes/SceneViewportEditorOverlayPass.*`
负责: 负责:
- 组装 scene/game viewport 渲染请求 - 组装 Scene / Game viewport 渲染请求
- 把 editor overlay 接入 `CameraRenderRequest::overlayPasses` - 把 editor overlay 接入 `CameraRenderRequest::overlayPasses`
- object-id picking、outline、overlay pass 等 editor 视口能力 - 处理 Scene View 的工具切换、hover / click 解析与导航输入
- 维护 gizmo overlay state、handle hit-test、HUD overlay、sprite 资源缓存与 presentation 刷新
- 提供 object-id picking、outline、grid 等 editor 视口能力
### Panels ### Panels
@@ -181,12 +240,29 @@ editor/
### Component Editors ### Component Editors
`ComponentEditors/` 当前不仅负责基础组件,也已经包含 `ScriptComponent` 的 Inspector 编辑入口。 `ComponentEditors/` 不只负责基础组件,也已经包含
- `MeshFilterComponentEditor`
- `MeshRendererComponentEditor`
- `ScriptComponentEditor`
- `AssetReferenceEditorUtils`
### Scripting
- `src/Scripting/EditorScriptAssemblyBuilder.*`
- `src/Scripting/EditorScriptAssemblyBuilderUtils.h`
- `src/Scripting/EditorScriptRuntimeStatus.h`
负责:
- 从项目脚本资产构建程序集
- 汇报脚本运行时状态
- 为 Inspector / 菜单动作提供“能否重建脚本程序集”的判断依据
## 开发约束 ## 开发约束
- editor 是宿主,不是第二套 renderer。 - editor 是宿主,不是第二套 renderer。
- 新的世界空间 overlay / gizmo不应继续堆到 ImGui world draw 路径。 - 新的世界空间 overlay / gizmo不应继续堆到旧的 ImGui world draw 路径。
- viewport 相关问题优先检查 `engine/Rendering``RenderSurface``ViewportHostService` 的接线,而不是直接在 panel 里复制渲染逻辑。 - viewport 相关问题优先检查 `engine/Rendering``RenderSurface``ViewportHostService` 的接线,而不是直接在 panel 里复制渲染逻辑。
- 与项目资源、脚本程序集、`.meta``Library` 相关的问题,不要假设 editor 仍处于“无工程状态”的旧结构。 - 与项目资源、脚本程序集、`.meta``Library` 相关的问题,不要假设 editor 仍处于“无工程状态”的旧结构。
@@ -197,7 +273,7 @@ cmake --build build --config Debug --target editor_tests
cmake --build build --config Debug --target rendering_phase_regression cmake --build build --config Debug --target rendering_phase_regression
``` ```
如果改动影响脚本类发现或 Inspector 脚本字段编辑,再补: 如果改动影响脚本类发现、脚本程序集重建或 Inspector 脚本字段编辑,再补:
```bash ```bash
cmake --build build --config Debug --target xcengine_project_managed_assemblies cmake --build build --config Debug --target xcengine_project_managed_assemblies

View File

@@ -0,0 +1,23 @@
<Theme name="XCUIDemoTheme">
<Token name="color.surface" type="color" value="#11161D" />
<Token name="color.surface.elevated" type="color" value="#1A212B" />
<Token name="color.surface.debug" type="color" value="#121820" />
<Token name="color.text.primary" type="color" value="#F2F5F8" />
<Token name="color.text.muted" type="color" value="#9AA8B7" />
<Token name="color.outline" type="color" value="#485666" />
<Token name="color.accent" type="color" value="#49B5FF" />
<Token name="color.accent.alt" type="color" value="#FF9A52" />
<Token name="color.button.text" type="color" value="#0D131A" />
<Token name="space.compact" type="float" value="6" />
<Token name="space.regular" type="float" value="10" />
<Token name="space.loose" type="float" value="14" />
<Token name="padding.panel" type="thickness" value="18" />
<Token name="padding.card" type="thickness" value="14" />
<Token name="padding.button" type="thickness" value="12,10,12,10" />
<Token name="radius.card" type="corner-radius" value="10" />
<Token name="radius.button" type="corner-radius" value="8" />
<Token name="font.body" type="float" value="14" />
<Token name="font.title" type="float" value="20" />
<Token name="font.metric" type="float" value="16" />
<Token name="line.default" type="float" value="1" />
</Theme>

View File

@@ -0,0 +1,33 @@
<View name="XCUI Demo" theme="xcui_demo_theme.xctheme" style="Root">
<Column id="root" gap="12" padding="18">
<Card id="hero" style="HeroCard">
<Row id="heroRow" gap="12">
<StatusDot id="statusDot" />
<Column id="heroCopy" gap="4" width="stretch">
<Text id="title" text="XCUI Demo" style="HeroTitle" />
<Text id="subtitle" text="Markup -> Tree -> Layout -> Style -> Input -> DrawData" style="HeroSubtitle" />
</Column>
</Row>
</Card>
<Row id="metrics" gap="12">
<Card id="metricPipeline" style="MetricCard" width="stretch">
<Text text="Pipeline" style="MetricLabel" />
<Text text="True XCUI path" style="MetricValue" />
</Card>
<Card id="metricBackend" style="MetricCard" width="stretch">
<Text text="Backend" style="MetricLabel" />
<Text text="ImGui transition" style="MetricValue" />
</Card>
</Row>
<Button id="toggleAccent" style="AccentButton" action="demo.toggleAccent">
<Text id="toggleLabel" text="Toggle Accent" style="ButtonLabel" />
</Button>
<Card id="debugCard" style="DebugCard">
<Column id="debugStack" gap="4">
<Text id="focusStatus" style="Meta" />
<Text id="treeStatus" style="Meta" />
<Text id="commandStatus" style="Meta" />
</Column>
</Card>
</Column>
</View>

View File

@@ -32,8 +32,16 @@ inline ActionBinding MakeRebuildScriptsAction(bool enabled = true) {
return MakeAction("Rebuild Script Assemblies", nullptr, false, enabled); return MakeAction("Rebuild Script Assemblies", nullptr, false, enabled);
} }
inline ActionBinding MakeMigrateSceneAssetReferencesAction(bool enabled = true) { inline ActionBinding MakeReimportSelectedAssetAction(bool enabled = true) {
return MakeAction("Migrate Scene AssetRefs", nullptr, false, enabled); return MakeAction("Reimport Selected Asset", nullptr, false, enabled);
}
inline ActionBinding MakeReimportAllAssetsAction(bool enabled = true) {
return MakeAction("Reimport All Assets", nullptr, false, enabled);
}
inline ActionBinding MakeClearLibraryAction(bool enabled = true) {
return MakeAction("Clear Library", nullptr, false, enabled);
} }
inline ActionBinding MakeNewSceneAction(bool enabled = true) { inline ActionBinding MakeNewSceneAction(bool enabled = true) {

View File

@@ -38,8 +38,16 @@ inline void ExecuteRebuildScriptAssemblies(IEditorContext& context) {
Commands::RebuildScriptAssemblies(context); Commands::RebuildScriptAssemblies(context);
} }
inline void ExecuteMigrateSceneAssetReferences(IEditorContext& context) { inline void ExecuteReimportSelectedAsset(IEditorContext& context) {
Commands::MigrateSceneAssetReferences(context); Commands::ReimportSelectedAsset(context);
}
inline void ExecuteReimportAllAssets(IEditorContext& context) {
Commands::ReimportAllAssets(context);
}
inline void ExecuteClearLibrary(IEditorContext& context) {
Commands::ClearLibrary(context);
} }
inline void ExecuteOpenScene(IEditorContext& context) { inline void ExecuteOpenScene(IEditorContext& context) {
@@ -151,10 +159,6 @@ inline void DrawFileMenuActions(IEditorContext& context) {
DrawMenuAction(MakeSaveSceneAction(canEditDocuments), [&]() { ExecuteSaveScene(context); }); DrawMenuAction(MakeSaveSceneAction(canEditDocuments), [&]() { ExecuteSaveScene(context); });
DrawMenuAction(MakeSaveSceneAsAction(canEditDocuments), [&]() { ExecuteSaveSceneAs(context); }); DrawMenuAction(MakeSaveSceneAsAction(canEditDocuments), [&]() { ExecuteSaveSceneAs(context); });
DrawMenuSeparator(); DrawMenuSeparator();
DrawMenuAction(
MakeMigrateSceneAssetReferencesAction(Commands::CanMigrateSceneAssetReferences(context)),
[&]() { ExecuteMigrateSceneAssetReferences(context); });
DrawMenuSeparator();
DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); }); DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); });
} }
@@ -178,6 +182,19 @@ inline void DrawScriptsMenuActions(IEditorContext& context) {
[&]() { ExecuteRebuildScriptAssemblies(context); }); [&]() { ExecuteRebuildScriptAssemblies(context); });
} }
inline void DrawAssetsMenuActions(IEditorContext& context) {
DrawMenuAction(
MakeReimportSelectedAssetAction(Commands::CanReimportSelectedAsset(context)),
[&]() { ExecuteReimportSelectedAsset(context); });
DrawMenuAction(
MakeReimportAllAssetsAction(Commands::CanReimportAllAssets(context)),
[&]() { ExecuteReimportAllAssets(context); });
DrawMenuSeparator();
DrawMenuAction(
MakeClearLibraryAction(Commands::CanClearLibrary(context)),
[&]() { ExecuteClearLibrary(context); });
}
inline void DrawViewMenuActions(IEditorContext& context) { inline void DrawViewMenuActions(IEditorContext& context) {
DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); }); DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); });
} }
@@ -201,6 +218,9 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo
UI::DrawMenuScope("Edit", [&]() { UI::DrawMenuScope("Edit", [&]() {
DrawEditActions(context); DrawEditActions(context);
}); });
UI::DrawMenuScope("Assets", [&]() {
DrawAssetsMenuActions(context);
});
UI::DrawMenuScope("Run", [&]() { UI::DrawMenuScope("Run", [&]() {
DrawRunMenuActions(context); DrawRunMenuActions(context);
}); });

View File

@@ -145,6 +145,40 @@ bool Application::RebuildScriptingAssemblies() {
#endif #endif
} }
bool Application::CanReimportProjectAsset(const std::string& assetPath) const {
if (assetPath.empty()) {
return false;
}
return ::XCEngine::Resources::ResourceManager::Get().CanReimportProjectAsset(assetPath.c_str());
}
bool Application::ReimportProjectAsset(const std::string& assetPath) {
if (assetPath.empty()) {
return false;
}
auto& resourceManager = ::XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
return resourceManager.ReimportProjectAsset(assetPath.c_str());
}
bool Application::ReimportAllProjectAssets() {
auto& resourceManager = ::XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
return resourceManager.RebuildProjectAssetCache();
}
bool Application::ClearProjectLibrary() {
auto& resourceManager = ::XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
return resourceManager.ClearProjectLibraryCache();
}
std::string Application::GetProjectLibraryRoot() const {
return ::XCEngine::Resources::ResourceManager::Get().GetProjectLibraryRoot().CStr();
}
bool Application::InitializeWindowRenderer(HWND hwnd) { bool Application::InitializeWindowRenderer(HWND hwnd) {
RECT clientRect = {}; RECT clientRect = {};
if (!GetClientRect(hwnd, &clientRect)) { if (!GetClientRect(hwnd, &clientRect)) {
@@ -227,13 +261,13 @@ void Application::RenderEditorFrame() {
m_imguiBackend.BeginFrame(); m_imguiBackend.BeginFrame();
m_viewportHostService.BeginFrame(); m_viewportHostService.BeginFrame();
m_layerStack.onImGuiRender(); m_layerStack.onUIRender();
UpdateWindowTitle(); UpdateWindowTitle();
ImGui::Render(); ImGui::Render();
m_windowRenderer.Render( m_windowRenderer.Render(
m_imguiBackend, m_imguiBackend,
kClearColor, kClearColor,
[this](const Rendering::RenderContext& renderContext) { [this](const Rendering::RenderContext& renderContext, const Rendering::RenderSurface&) {
if (m_editorContext) { if (m_editorContext) {
m_viewportHostService.RenderRequestedViewports(*m_editorContext, renderContext); m_viewportHostService.RenderRequestedViewports(*m_editorContext, renderContext);
} }

View File

@@ -7,6 +7,7 @@
#include "Viewport/ViewportHostService.h" #include "Viewport/ViewportHostService.h"
#include <XCEngine/Rendering/RenderContext.h> #include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <chrono> #include <chrono>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -42,8 +43,14 @@ public:
bool SwitchProject(const std::string& projectPath); bool SwitchProject(const std::string& projectPath);
bool ReloadScriptingRuntime(); bool ReloadScriptingRuntime();
bool RebuildScriptingAssemblies(); bool RebuildScriptingAssemblies();
bool CanReimportProjectAsset(const std::string& assetPath) const;
bool ReimportProjectAsset(const std::string& assetPath);
bool ReimportAllProjectAssets();
bool ClearProjectLibrary();
std::string GetProjectLibraryRoot() const;
void SaveProjectState(); void SaveProjectState();
Rendering::RenderContext GetMainRenderContext() const { return m_windowRenderer.GetRenderContext(); } Rendering::RenderContext GetMainRenderContext() const { return m_windowRenderer.GetRenderContext(); }
const Rendering::RenderSurface* GetMainRenderSurface() const { return m_windowRenderer.GetCurrentRenderSurface(); }
RHI::RHIDevice* GetMainRHIDevice() const { return m_windowRenderer.GetRHIDevice(); } RHI::RHIDevice* GetMainRHIDevice() const { return m_windowRenderer.GetRHIDevice(); }
RHI::RHISwapChain* GetMainSwapChain() const { return m_windowRenderer.GetSwapChain(); } RHI::RHISwapChain* GetMainSwapChain() const { return m_windowRenderer.GetSwapChain(); }
IViewportHostService& GetViewportHostService() { return m_viewportHostService; } IViewportHostService& GetViewportHostService() { return m_viewportHostService; }

View File

@@ -12,6 +12,8 @@
#include "Utils/FileDialogUtils.h" #include "Utils/FileDialogUtils.h"
#include "Utils/ProjectFileUtils.h" #include "Utils/ProjectFileUtils.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <algorithm> #include <algorithm>
#include <cwctype> #include <cwctype>
#include <filesystem> #include <filesystem>
@@ -331,16 +333,79 @@ inline bool RebuildScriptAssemblies(IEditorContext& context) {
return rebuilt; return rebuilt;
} }
inline bool CanMigrateSceneAssetReferences(const IEditorContext& context) { inline bool CanManageProjectAssetCache(const IEditorContext& context) {
return IsProjectDocumentEditingAllowed(context) && !context.GetProjectPath().empty(); return IsProjectDocumentEditingAllowed(context) && !context.GetProjectPath().empty();
} }
inline IProjectManager::SceneAssetReferenceMigrationReport MigrateSceneAssetReferences(IEditorContext& context) { inline Resources::ResourceManager& GetProjectResourceManager(IEditorContext& context) {
if (!CanMigrateSceneAssetReferences(context)) { auto& resourceManager = Resources::ResourceManager::Get();
return {}; resourceManager.Initialize();
if (!context.GetProjectPath().empty() &&
std::string(resourceManager.GetResourceRoot().CStr()) != context.GetProjectPath()) {
resourceManager.SetResourceRoot(context.GetProjectPath().c_str());
} }
return context.GetProjectManager().MigrateSceneAssetReferences(); return resourceManager;
}
inline bool CanReimportSelectedAsset(IEditorContext& context) {
if (!CanManageProjectAssetCache(context)) {
return false;
}
const AssetItemPtr selectedItem = context.GetProjectManager().GetSelectedItem();
return selectedItem != nullptr &&
!selectedItem->isFolder &&
GetProjectResourceManager(context).CanReimportProjectAsset(selectedItem->fullPath.c_str());
}
inline bool ReimportSelectedAsset(IEditorContext& context) {
if (!CanReimportSelectedAsset(context)) {
return false;
}
const AssetItemPtr selectedItem = context.GetProjectManager().GetSelectedItem();
const bool reimported = GetProjectResourceManager(context).ReimportProjectAsset(selectedItem->fullPath.c_str());
if (reimported) {
context.GetProjectManager().RefreshCurrentFolder();
}
return reimported;
}
inline bool CanReimportAllAssets(const IEditorContext& context) {
return CanManageProjectAssetCache(context);
}
inline bool ReimportAllAssets(IEditorContext& context) {
if (!CanReimportAllAssets(context)) {
return false;
}
const bool reimported = GetProjectResourceManager(context).RebuildProjectAssetCache();
if (reimported) {
context.GetProjectManager().RefreshCurrentFolder();
}
return reimported;
}
inline bool CanClearLibrary(const IEditorContext& context) {
return CanManageProjectAssetCache(context);
}
inline bool ClearLibrary(IEditorContext& context) {
if (!CanClearLibrary(context)) {
return false;
}
const bool cleared = GetProjectResourceManager(context).ClearProjectLibraryCache();
if (cleared) {
context.GetProjectManager().RefreshCurrentFolder();
}
return cleared;
} }
inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) { inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) {

View File

@@ -6,8 +6,7 @@ namespace Debug {
EditorConsoleSink* EditorConsoleSink::s_instance = nullptr; EditorConsoleSink* EditorConsoleSink::s_instance = nullptr;
EditorConsoleSink* EditorConsoleSink::GetInstance() { EditorConsoleSink* EditorConsoleSink::GetInstance() {
static EditorConsoleSink fallbackInstance; return s_instance;
return s_instance ? s_instance : &fallbackInstance;
} }
EditorConsoleSink::EditorConsoleSink() { EditorConsoleSink::EditorConsoleSink() {

View File

@@ -11,13 +11,6 @@ namespace Editor {
class IProjectManager { class IProjectManager {
public: public:
struct SceneAssetReferenceMigrationReport {
size_t scannedSceneCount = 0;
size_t migratedSceneCount = 0;
size_t unchangedSceneCount = 0;
size_t failedSceneCount = 0;
};
virtual ~IProjectManager() = default; virtual ~IProjectManager() = default;
virtual const std::vector<AssetItemPtr>& GetCurrentItems() const = 0; virtual const std::vector<AssetItemPtr>& GetCurrentItems() const = 0;
@@ -47,7 +40,6 @@ public:
virtual bool DeleteItem(const std::string& fullPath) = 0; virtual bool DeleteItem(const std::string& fullPath) = 0;
virtual bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) = 0; virtual bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) = 0;
virtual bool RenameItem(const std::string& sourceFullPath, const std::string& newName) = 0; virtual bool RenameItem(const std::string& sourceFullPath, const std::string& newName) = 0;
virtual SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() = 0;
virtual const std::string& GetProjectPath() const = 0; virtual const std::string& GetProjectPath() const = 0;
}; };

View File

@@ -14,6 +14,30 @@ class Scene;
namespace Editor { namespace Editor {
struct SceneLoadProgressSnapshot {
std::uint64_t revision = 0;
bool inProgress = false;
bool success = false;
bool firstFramePresented = false;
bool streamingCompleted = false;
bool interactive = false;
bool startupScene = false;
std::string operation;
std::string scenePath;
std::string message;
std::uint64_t startedAtMs = 0;
std::uint64_t structureReadyAtMs = 0;
std::uint64_t firstFrameAtMs = 0;
std::uint64_t streamingCompletedAtMs = 0;
std::uint64_t interactiveAtMs = 0;
std::uint32_t currentPendingAsyncLoads = 0;
std::uint32_t peakPendingAsyncLoads = 0;
bool HasValue() const {
return revision != 0;
}
};
class ISceneManager { class ISceneManager {
public: public:
virtual ~ISceneManager() = default; virtual ~ISceneManager() = default;
@@ -42,6 +66,8 @@ public:
virtual const std::string& GetCurrentSceneName() const = 0; virtual const std::string& GetCurrentSceneName() const = 0;
virtual ::XCEngine::Components::Scene* GetScene() = 0; virtual ::XCEngine::Components::Scene* GetScene() = 0;
virtual const ::XCEngine::Components::Scene* GetScene() const = 0; virtual const ::XCEngine::Components::Scene* GetScene() const = 0;
virtual SceneLoadProgressSnapshot GetSceneLoadProgress() const = 0;
virtual void NotifySceneViewportFramePresented(std::uint32_t pendingAsyncLoads) = 0;
virtual SceneSnapshot CaptureSceneSnapshot() const = 0; virtual SceneSnapshot CaptureSceneSnapshot() const = 0;
virtual bool RestoreSceneSnapshot(const SceneSnapshot& snapshot) = 0; virtual bool RestoreSceneSnapshot(const SceneSnapshot& snapshot) = 0;
virtual void CreateDemoScene() = 0; virtual void CreateDemoScene() = 0;

View File

@@ -32,7 +32,7 @@ void EditorLayer::onEvent(void* event) {
m_workspace.DispatchEvent(event); m_workspace.DispatchEvent(event);
} }
void EditorLayer::onImGuiRender() { void EditorLayer::onUIRender() {
m_workspace.Render(); m_workspace.Render();
} }

View File

@@ -20,7 +20,7 @@ public:
void onDetach() override; void onDetach() override;
void onUpdate(float dt) override; void onUpdate(float dt) override;
void onEvent(void* event) override; void onEvent(void* event) override;
void onImGuiRender() override; void onUIRender() override;
void SetContext(std::shared_ptr<IEditorContext> context); void SetContext(std::shared_ptr<IEditorContext> context);

View File

@@ -6,7 +6,6 @@
#include "UI/DockHostStyle.h" #include "UI/DockHostStyle.h"
#include "UI/DockTabBarChrome.h" #include "UI/DockTabBarChrome.h"
#include <fstream>
#include <imgui.h> #include <imgui.h>
#include <imgui_internal.h> #include <imgui_internal.h>
@@ -70,9 +69,7 @@ public:
} }
if (m_layoutDirty) { if (m_layoutDirty) {
if (m_forceRebuild || !HasSavedDockLayout()) {
BuildDefaultLayout(dockspaceId, viewport->WorkSize); BuildDefaultLayout(dockspaceId, viewport->WorkSize);
}
m_layoutDirty = false; m_layoutDirty = false;
m_forceRebuild = false; m_forceRebuild = false;
} }
@@ -82,31 +79,6 @@ public:
} }
private: private:
static bool HasSavedDockLayout() {
if (ImGui::GetCurrentContext() == nullptr) {
return false;
}
const char* iniFilename = ImGui::GetIO().IniFilename;
if (iniFilename == nullptr || iniFilename[0] == '\0') {
return false;
}
std::ifstream input(iniFilename, std::ios::in);
if (!input.is_open()) {
return false;
}
std::string line;
while (std::getline(input, line)) {
if (line == "[Docking][Data]") {
return true;
}
}
return false;
}
void BuildDefaultLayout(ImGuiID dockspaceId, const ImVec2& dockspaceSize) { void BuildDefaultLayout(ImGuiID dockspaceId, const ImVec2& dockspaceSize) {
ImGui::DockBuilderRemoveNode(dockspaceId); ImGui::DockBuilderRemoveNode(dockspaceId);
ImGui::DockBuilderAddNode(dockspaceId, m_dockspaceFlags | ImGuiDockNodeFlags_DockSpace); ImGui::DockBuilderAddNode(dockspaceId, m_dockspaceFlags | ImGuiDockNodeFlags_DockSpace);

View File

@@ -1,14 +1,9 @@
#include "ProjectManager.h" #include "ProjectManager.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Scene/Scene.h>
#include <filesystem> #include <filesystem>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cwctype> #include <cwctype>
#include <fstream>
#include <sstream>
#include <initializer_list> #include <initializer_list>
#include <string_view> #include <string_view>
#include <windows.h> #include <windows.h>
@@ -217,27 +212,6 @@ bool CanPreviewImageAssetExtension(std::wstring_view extension) {
}); });
} }
bool IsSceneAssetFile(const fs::path& path) {
if (!fs::is_regular_file(path)) {
return false;
}
std::wstring extension = path.extension().wstring();
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
return extension == L".xc";
}
std::string ReadFileText(const fs::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input.is_open()) {
return {};
}
std::ostringstream stream;
stream << input.rdbuf();
return stream.str();
}
} // namespace } // namespace
const std::vector<AssetItemPtr>& ProjectManager::GetCurrentItems() const { const std::vector<AssetItemPtr>& ProjectManager::GetCurrentItems() const {
@@ -522,89 +496,6 @@ bool ProjectManager::RenameItem(const std::string& sourceFullPath, const std::st
} }
} }
IProjectManager::SceneAssetReferenceMigrationReport ProjectManager::MigrateSceneAssetReferences() {
IProjectManager::SceneAssetReferenceMigrationReport report;
if (m_projectPath.empty()) {
return report;
}
const fs::path assetsPath = fs::path(Utf8PathToWstring(m_projectPath)) / L"Assets";
if (!fs::exists(assetsPath) || !fs::is_directory(assetsPath)) {
return report;
}
auto& logger = ::XCEngine::Debug::Logger::Get();
::XCEngine::Resources::ResourceManager& resourceManager = ::XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
const std::string previousRoot = resourceManager.GetResourceRoot().CStr();
const bool restoreResourceRoot = previousRoot != m_projectPath;
if (restoreResourceRoot) {
resourceManager.SetResourceRoot(m_projectPath.c_str());
}
try {
for (const fs::directory_entry& entry : fs::recursive_directory_iterator(assetsPath)) {
if (!IsSceneAssetFile(entry.path())) {
continue;
}
++report.scannedSceneCount;
const fs::path scenePath = entry.path();
const std::string scenePathUtf8 = WstringPathToUtf8(scenePath.wstring());
try {
const std::string before = ReadFileText(scenePath);
::XCEngine::Components::Scene scene;
{
::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope(resourceManager);
scene.Load(scenePathUtf8);
}
scene.Save(scenePathUtf8);
const std::string after = ReadFileText(scenePath);
if (after != before) {
++report.migratedSceneCount;
} else {
++report.unchangedSceneCount;
}
} catch (const std::exception& exception) {
++report.failedSceneCount;
logger.Error(
::XCEngine::Debug::LogCategory::FileSystem,
("Failed to migrate scene asset references: " + scenePathUtf8 + " - " + exception.what()).c_str());
} catch (...) {
++report.failedSceneCount;
logger.Error(
::XCEngine::Debug::LogCategory::FileSystem,
("Failed to migrate scene asset references: " + scenePathUtf8 + " - unknown error").c_str());
}
}
} catch (const std::exception& exception) {
logger.Error(
::XCEngine::Debug::LogCategory::FileSystem,
("Scene asset reference migration aborted: " + std::string(exception.what())).c_str());
} catch (...) {
logger.Error(
::XCEngine::Debug::LogCategory::FileSystem,
"Scene asset reference migration aborted: unknown error");
}
if (restoreResourceRoot) {
resourceManager.SetResourceRoot(previousRoot.c_str());
}
logger.Info(
::XCEngine::Debug::LogCategory::FileSystem,
("Scene asset reference migration finished. scanned=" + std::to_string(report.scannedSceneCount) +
" migrated=" + std::to_string(report.migratedSceneCount) +
" unchanged=" + std::to_string(report.unchangedSceneCount) +
" failed=" + std::to_string(report.failedSceneCount)).c_str());
RefreshCurrentFolder();
return report;
}
AssetItemPtr ProjectManager::FindCurrentItemByPath(const std::string& fullPath) const { AssetItemPtr ProjectManager::FindCurrentItemByPath(const std::string& fullPath) const {
const int index = FindCurrentItemIndex(fullPath); const int index = FindCurrentItemIndex(fullPath);
if (index < 0) { if (index < 0) {

View File

@@ -38,7 +38,6 @@ public:
bool DeleteItem(const std::string& fullPath) override; bool DeleteItem(const std::string& fullPath) override;
bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) override; bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) override;
bool RenameItem(const std::string& sourceFullPath, const std::string& newName) override; bool RenameItem(const std::string& sourceFullPath, const std::string& newName) override;
SceneAssetReferenceMigrationReport MigrateSceneAssetReferences() override;
const std::string& GetProjectPath() const override { return m_projectPath; } const std::string& GetProjectPath() const override { return m_projectPath; }

View File

@@ -7,6 +7,7 @@
#include <XCEngine/Components/CameraComponent.h> #include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h> #include <XCEngine/Components/LightComponent.h>
#include <algorithm> #include <algorithm>
#include <chrono>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
@@ -27,6 +28,48 @@ std::pair<std::string, std::string> SerializeComponent(const ::XCEngine::Compone
SceneManager::SceneManager(EventBus* eventBus) SceneManager::SceneManager(EventBus* eventBus)
: m_eventBus(eventBus) {} : m_eventBus(eventBus) {}
std::uint64_t SceneManager::GetCurrentSteadyTimeMs() {
return static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
}
void SceneManager::BeginSceneLoadProgress(
const std::string& operation,
const std::string& scenePath,
bool startupScene,
const std::string& message) {
m_sceneLoadProgress = {};
m_sceneLoadProgress.revision = m_nextSceneLoadProgressRevision++;
m_sceneLoadProgress.inProgress = true;
m_sceneLoadProgress.success = false;
m_sceneLoadProgress.startupScene = startupScene;
m_sceneLoadProgress.operation = operation;
m_sceneLoadProgress.scenePath = scenePath;
m_sceneLoadProgress.message = message;
m_sceneLoadProgress.startedAtMs = GetCurrentSteadyTimeMs();
}
void SceneManager::MarkSceneStructureReady(const std::string& message) {
if (!m_sceneLoadProgress.HasValue()) {
return;
}
m_sceneLoadProgress.success = true;
m_sceneLoadProgress.inProgress = true;
m_sceneLoadProgress.message = message;
m_sceneLoadProgress.structureReadyAtMs = GetCurrentSteadyTimeMs();
m_sceneLoadProgress.currentPendingAsyncLoads =
::XCEngine::Resources::ResourceManager::Get().GetAsyncPendingCount();
m_sceneLoadProgress.peakPendingAsyncLoads = (std::max)(
m_sceneLoadProgress.peakPendingAsyncLoads,
m_sceneLoadProgress.currentPendingAsyncLoads);
}
void SceneManager::ClearSceneLoadProgress() {
m_sceneLoadProgress = {};
}
void SceneManager::SetSceneDirty(bool dirty) { void SceneManager::SetSceneDirty(bool dirty) {
m_isSceneDirty = dirty; m_isSceneDirty = dirty;
} }
@@ -188,6 +231,7 @@ bool SceneManager::RestoreSceneSnapshot(const SceneSnapshot& snapshot) {
m_currentScenePath.clear(); m_currentScenePath.clear();
m_currentSceneName = "Untitled Scene"; m_currentSceneName = "Untitled Scene";
SetSceneDirty(snapshot.dirty); SetSceneDirty(snapshot.dirty);
ClearSceneLoadProgress();
OnSceneChanged.Invoke(); OnSceneChanged.Invoke();
if (m_eventBus) { if (m_eventBus) {
@@ -196,6 +240,11 @@ bool SceneManager::RestoreSceneSnapshot(const SceneSnapshot& snapshot) {
return true; return true;
} }
BeginSceneLoadProgress(
"Restore Scene Snapshot",
snapshot.scenePath,
false,
"Restoring scene snapshot structure...");
auto scene = std::make_unique<::XCEngine::Components::Scene>(); auto scene = std::make_unique<::XCEngine::Components::Scene>();
{ {
::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; ::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
@@ -208,6 +257,7 @@ bool SceneManager::RestoreSceneSnapshot(const SceneSnapshot& snapshot) {
m_currentScenePath = snapshot.scenePath; m_currentScenePath = snapshot.scenePath;
m_currentSceneName = m_scene ? m_scene->GetName() : "Untitled Scene"; m_currentSceneName = m_scene ? m_scene->GetName() : "Untitled Scene";
SetSceneDirty(snapshot.dirty); SetSceneDirty(snapshot.dirty);
MarkSceneStructureReady("Scene snapshot restored. Waiting for first viewport frame...");
OnSceneChanged.Invoke(); OnSceneChanged.Invoke();
if (m_eventBus) { if (m_eventBus) {
@@ -238,6 +288,7 @@ void SceneManager::NewScene(const std::string& name) {
m_currentScenePath.clear(); m_currentScenePath.clear();
m_currentSceneName = name; m_currentSceneName = name;
SetSceneDirty(true); SetSceneDirty(true);
ClearSceneLoadProgress();
OnSceneChanged.Invoke(); OnSceneChanged.Invoke();
if (m_eventBus) { if (m_eventBus) {
@@ -252,6 +303,11 @@ bool SceneManager::LoadScene(const std::string& filePath) {
return false; return false;
} }
BeginSceneLoadProgress(
"Load Scene",
filePath,
false,
"Restoring scene structure...");
auto scene = std::make_unique<::XCEngine::Components::Scene>(); auto scene = std::make_unique<::XCEngine::Components::Scene>();
{ {
::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; ::XCEngine::Resources::ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
@@ -264,6 +320,7 @@ bool SceneManager::LoadScene(const std::string& filePath) {
m_currentScenePath = filePath; m_currentScenePath = filePath;
m_currentSceneName = m_scene ? m_scene->GetName() : "Untitled Scene"; m_currentSceneName = m_scene ? m_scene->GetName() : "Untitled Scene";
SetSceneDirty(false); SetSceneDirty(false);
MarkSceneStructureReady("Scene structure restored. Waiting for first viewport frame...");
OnSceneChanged.Invoke(); OnSceneChanged.Invoke();
if (m_eventBus) { if (m_eventBus) {
@@ -310,11 +367,57 @@ bool SceneManager::SaveSceneAs(const std::string& filePath) {
bool SceneManager::LoadStartupScene(const std::string& projectPath) { bool SceneManager::LoadStartupScene(const std::string& projectPath) {
const std::string defaultScenePath = ResolveDefaultScenePath(projectPath); const std::string defaultScenePath = ResolveDefaultScenePath(projectPath);
if (IsSceneFileUsable(defaultScenePath) && LoadScene(defaultScenePath)) { if (IsSceneFileUsable(defaultScenePath) && LoadScene(defaultScenePath)) {
if (m_sceneLoadProgress.HasValue()) {
m_sceneLoadProgress.operation = "Load Startup Scene";
m_sceneLoadProgress.startupScene = true;
}
return true; return true;
} }
CreateDemoScene(); CreateDemoScene();
return SaveSceneAs(defaultScenePath); const bool saved = SaveSceneAs(defaultScenePath);
if (saved) {
BeginSceneLoadProgress(
"Load Startup Scene",
defaultScenePath,
true,
"Created default startup scene.");
MarkSceneStructureReady("Created default startup scene. Waiting for first viewport frame...");
}
return saved;
}
void SceneManager::NotifySceneViewportFramePresented(std::uint32_t pendingAsyncLoads) {
if (!m_sceneLoadProgress.HasValue() ||
!m_sceneLoadProgress.success ||
m_sceneLoadProgress.streamingCompleted) {
return;
}
const std::uint64_t nowMs = GetCurrentSteadyTimeMs();
m_sceneLoadProgress.currentPendingAsyncLoads = pendingAsyncLoads;
m_sceneLoadProgress.peakPendingAsyncLoads = (std::max)(
m_sceneLoadProgress.peakPendingAsyncLoads,
pendingAsyncLoads);
if (!m_sceneLoadProgress.firstFramePresented) {
m_sceneLoadProgress.firstFramePresented = true;
m_sceneLoadProgress.firstFrameAtMs = nowMs;
m_sceneLoadProgress.interactive = true;
m_sceneLoadProgress.interactiveAtMs = nowMs;
}
if (pendingAsyncLoads == 0) {
m_sceneLoadProgress.streamingCompleted = true;
m_sceneLoadProgress.streamingCompletedAtMs = nowMs;
m_sceneLoadProgress.inProgress = false;
m_sceneLoadProgress.message = "Scene ready.";
return;
}
m_sceneLoadProgress.inProgress = true;
m_sceneLoadProgress.message =
"Runtime streaming scene assets... (" + std::to_string(pendingAsyncLoads) + ")";
} }
void SceneManager::RenameEntity(::XCEngine::Components::GameObject::ID id, const std::string& newName) { void SceneManager::RenameEntity(::XCEngine::Components::GameObject::ID id, const std::string& newName) {
@@ -365,6 +468,7 @@ void SceneManager::CreateDemoScene() {
m_currentScenePath.clear(); m_currentScenePath.clear();
m_currentSceneName = m_scene->GetName(); m_currentSceneName = m_scene->GetName();
SetSceneDirty(true); SetSceneDirty(true);
ClearSceneLoadProgress();
::XCEngine::Components::GameObject* camera = CreateEntity("Main Camera", nullptr); ::XCEngine::Components::GameObject* camera = CreateEntity("Main Camera", nullptr);
camera->AddComponent<::XCEngine::Components::CameraComponent>(); camera->AddComponent<::XCEngine::Components::CameraComponent>();

View File

@@ -67,6 +67,8 @@ public:
const std::string& GetCurrentSceneName() const override { return m_currentSceneName; } const std::string& GetCurrentSceneName() const override { return m_currentSceneName; }
::XCEngine::Components::Scene* GetScene() override { return m_scene.get(); } ::XCEngine::Components::Scene* GetScene() override { return m_scene.get(); }
const ::XCEngine::Components::Scene* GetScene() const override { return m_scene.get(); } const ::XCEngine::Components::Scene* GetScene() const override { return m_scene.get(); }
SceneLoadProgressSnapshot GetSceneLoadProgress() const override { return m_sceneLoadProgress; }
void NotifySceneViewportFramePresented(std::uint32_t pendingAsyncLoads) override;
SceneSnapshot CaptureSceneSnapshot() const override; SceneSnapshot CaptureSceneSnapshot() const override;
bool RestoreSceneSnapshot(const SceneSnapshot& snapshot) override; bool RestoreSceneSnapshot(const SceneSnapshot& snapshot) override;
void CreateDemoScene() override; void CreateDemoScene() override;
@@ -92,6 +94,14 @@ private:
::XCEngine::Components::GameObject::ID PasteEntityRecursive(const ClipboardData& data, ::XCEngine::Components::GameObject::ID parent); ::XCEngine::Components::GameObject::ID PasteEntityRecursive(const ClipboardData& data, ::XCEngine::Components::GameObject::ID parent);
void SetSceneDirty(bool dirty); void SetSceneDirty(bool dirty);
void SyncRootEntities(); void SyncRootEntities();
static std::uint64_t GetCurrentSteadyTimeMs();
void BeginSceneLoadProgress(
const std::string& operation,
const std::string& scenePath,
bool startupScene,
const std::string& message);
void MarkSceneStructureReady(const std::string& message);
void ClearSceneLoadProgress();
static std::string GetScenesDirectory(const std::string& projectPath); static std::string GetScenesDirectory(const std::string& projectPath);
static std::string ResolveDefaultScenePath(const std::string& projectPath); static std::string ResolveDefaultScenePath(const std::string& projectPath);
static bool IsSceneFileUsable(const std::string& filePath); static bool IsSceneFileUsable(const std::string& filePath);
@@ -104,6 +114,8 @@ private:
std::string m_currentSceneName = "Untitled Scene"; std::string m_currentSceneName = "Untitled Scene";
bool m_isSceneDirty = false; bool m_isSceneDirty = false;
bool m_sceneDocumentDirtyTrackingEnabled = true; bool m_sceneDocumentDirtyTrackingEnabled = true;
SceneLoadProgressSnapshot m_sceneLoadProgress;
std::uint64_t m_nextSceneLoadProgressRevision = 1;
}; };
} }

View File

@@ -3,6 +3,7 @@
#include "UI/ImGuiBackendBridge.h" #include "UI/ImGuiBackendBridge.h"
#include <XCEngine/Rendering/RenderContext.h> #include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/RHI/RHICommandList.h> #include <XCEngine/RHI/RHICommandList.h>
#include <XCEngine/RHI/RHICommandQueue.h> #include <XCEngine/RHI/RHICommandQueue.h>
#include <XCEngine/RHI/RHIDevice.h> #include <XCEngine/RHI/RHIDevice.h>
@@ -28,6 +29,8 @@ class D3D12WindowRenderer {
public: public:
static constexpr UINT kSrvDescriptorCount = 64; static constexpr UINT kSrvDescriptorCount = 64;
static constexpr uint32_t kSwapChainBufferCount = 3; static constexpr uint32_t kSwapChainBufferCount = 3;
using RenderCallback =
std::function<void(const Rendering::RenderContext&, const Rendering::RenderSurface&)>;
bool Initialize(HWND hwnd, int width, int height) { bool Initialize(HWND hwnd, int width, int height) {
Shutdown(); Shutdown();
@@ -47,6 +50,10 @@ public:
} }
RHI::RHIDeviceDesc deviceDesc = {}; RHI::RHIDeviceDesc deviceDesc = {};
#ifdef _DEBUG
deviceDesc.enableDebugLayer = true;
deviceDesc.enableGPUValidation = true;
#endif
if (!m_device->Initialize(deviceDesc)) { if (!m_device->Initialize(deviceDesc)) {
Shutdown(); Shutdown();
return false; return false;
@@ -175,7 +182,7 @@ public:
void Render( void Render(
UI::ImGuiBackendBridge& imguiBackend, UI::ImGuiBackendBridge& imguiBackend,
const float clearColor[4], const float clearColor[4],
const std::function<void(const Rendering::RenderContext&)>& beforeUiRender = {}) { const RenderCallback& beforeUiRender = {}) {
auto* d3d12Queue = GetD3D12CommandQueue(); auto* d3d12Queue = GetD3D12CommandQueue();
auto* d3d12CommandList = GetD3D12CommandList(); auto* d3d12CommandList = GetD3D12CommandList();
if (m_swapChain == nullptr || if (m_swapChain == nullptr ||
@@ -185,16 +192,15 @@ public:
return; return;
} }
if (beforeUiRender) {
beforeUiRender(GetRenderContext());
}
const uint32_t backBufferIndex = m_swapChain->GetCurrentBackBufferIndex(); const uint32_t backBufferIndex = m_swapChain->GetCurrentBackBufferIndex();
if (backBufferIndex >= m_backBufferViews.size() || m_backBufferViews[backBufferIndex] == nullptr) { if (backBufferIndex >= m_backBufferViews.size() ||
backBufferIndex >= m_backBufferSurfaces.size() ||
m_backBufferViews[backBufferIndex] == nullptr) {
return; return;
} }
RHI::RHIResourceView* renderTargetView = m_backBufferViews[backBufferIndex]; RHI::RHIResourceView* renderTargetView = m_backBufferViews[backBufferIndex];
Rendering::RenderSurface& renderSurface = m_backBufferSurfaces[backBufferIndex];
d3d12CommandList->TransitionBarrier( d3d12CommandList->TransitionBarrier(
renderTargetView, renderTargetView,
RHI::ResourceStates::Present, RHI::ResourceStates::Present,
@@ -202,6 +208,16 @@ public:
d3d12CommandList->SetRenderTargets(1, &renderTargetView, nullptr); d3d12CommandList->SetRenderTargets(1, &renderTargetView, nullptr);
d3d12CommandList->ClearRenderTarget(renderTargetView, clearColor); d3d12CommandList->ClearRenderTarget(renderTargetView, clearColor);
// Host-owned swapchain surfaces are handed to the callback already bound and ready for rendering.
// The callback must leave the color attachment in RenderTarget state so ImGui can render after it.
if (beforeUiRender) {
beforeUiRender(GetRenderContext(), renderSurface);
// Viewport rendering can bind offscreen render targets on the shared command list.
// Rebind the swapchain backbuffer so the subsequent ImGui draw data lands on the main window.
d3d12CommandList->SetRenderTargets(1, &renderTargetView, nullptr);
}
ID3D12DescriptorHeap* descriptorHeaps[] = { m_srvHeap->GetDescriptorHeap() }; ID3D12DescriptorHeap* descriptorHeaps[] = { m_srvHeap->GetDescriptorHeap() };
d3d12CommandList->SetDescriptorHeaps(1, descriptorHeaps); d3d12CommandList->SetDescriptorHeaps(1, descriptorHeaps);
imguiBackend.RenderDrawData(d3d12CommandList->GetCommandList()); imguiBackend.RenderDrawData(d3d12CommandList->GetCommandList());
@@ -247,6 +263,19 @@ public:
return m_swapChain; return m_swapChain;
} }
const Rendering::RenderSurface* GetCurrentRenderSurface() const {
if (m_swapChain == nullptr) {
return nullptr;
}
const uint32_t backBufferIndex = m_swapChain->GetCurrentBackBufferIndex();
if (backBufferIndex >= m_backBufferSurfaces.size()) {
return nullptr;
}
return &m_backBufferSurfaces[backBufferIndex];
}
Rendering::RenderContext GetRenderContext() const { Rendering::RenderContext GetRenderContext() const {
Rendering::RenderContext context = {}; Rendering::RenderContext context = {};
context.device = m_device; context.device = m_device;
@@ -287,6 +316,7 @@ private:
} }
} }
m_backBufferViews.clear(); m_backBufferViews.clear();
m_backBufferSurfaces.clear();
} }
bool RecreateBackBufferViews() { bool RecreateBackBufferViews() {
@@ -296,6 +326,7 @@ private:
} }
m_backBufferViews.resize(kSwapChainBufferCount, nullptr); m_backBufferViews.resize(kSwapChainBufferCount, nullptr);
m_backBufferSurfaces.resize(kSwapChainBufferCount);
RHI::ResourceViewDesc viewDesc = {}; RHI::ResourceViewDesc viewDesc = {};
viewDesc.format = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm); viewDesc.format = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
@@ -309,6 +340,15 @@ private:
ReleaseBackBufferViews(); ReleaseBackBufferViews();
return false; return false;
} }
Rendering::RenderSurface& surface = m_backBufferSurfaces[backBufferIndex];
surface = Rendering::RenderSurface(
static_cast<uint32_t>(m_width),
static_cast<uint32_t>(m_height));
surface.SetColorAttachment(m_backBufferViews[backBufferIndex]);
surface.SetAutoTransitionEnabled(false);
surface.SetColorStateBefore(RHI::ResourceStates::RenderTarget);
surface.SetColorStateAfter(RHI::ResourceStates::RenderTarget);
} }
return true; return true;
@@ -325,6 +365,7 @@ private:
RHI::RHIDescriptorPool* m_srvPool = nullptr; RHI::RHIDescriptorPool* m_srvPool = nullptr;
RHI::D3D12DescriptorHeap* m_srvHeap = nullptr; RHI::D3D12DescriptorHeap* m_srvHeap = nullptr;
std::vector<RHI::RHIResourceView*> m_backBufferViews; std::vector<RHI::RHIResourceView*> m_backBufferViews;
std::vector<Rendering::RenderSurface> m_backBufferSurfaces;
UINT m_srvDescriptorSize = 0; UINT m_srvDescriptorSize = 0;
}; };

View File

@@ -29,6 +29,7 @@
#include <XCEngine/Scene/Scene.h> #include <XCEngine/Scene/Scene.h>
#include <array> #include <array>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -60,6 +61,52 @@ Math::Vector3 GetSceneViewportOrientationAxisVector(SceneViewportOrientationAxis
} }
} }
std::uint64_t GetCurrentSceneLoadStatusTimeMs() {
return static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
}
std::uint64_t ResolveSceneLoadStatusElapsedMs(const SceneLoadProgressSnapshot& status) {
if (!status.HasValue() || status.startedAtMs == 0) {
return 0;
}
if (status.inProgress) {
const std::uint64_t nowMs = GetCurrentSceneLoadStatusTimeMs();
return nowMs >= status.startedAtMs ? nowMs - status.startedAtMs : 0;
}
if (status.streamingCompletedAtMs >= status.startedAtMs && status.streamingCompletedAtMs != 0) {
return status.streamingCompletedAtMs - status.startedAtMs;
}
if (status.firstFrameAtMs >= status.startedAtMs && status.firstFrameAtMs != 0) {
return status.firstFrameAtMs - status.startedAtMs;
}
if (status.structureReadyAtMs >= status.startedAtMs && status.structureReadyAtMs != 0) {
return status.structureReadyAtMs - status.startedAtMs;
}
return 0;
}
std::string BuildViewportSceneLoadStatusText(const SceneLoadProgressSnapshot& status) {
if (!status.HasValue() || !status.inProgress || status.message.empty()) {
return {};
}
std::string text = status.message;
const std::uint64_t elapsedMs = ResolveSceneLoadStatusElapsedMs(status);
if (elapsedMs > 0) {
text += " (";
text += std::to_string(elapsedMs);
text += " ms)";
}
return text;
}
} // namespace } // namespace
class ViewportHostService : public IViewportHostService { class ViewportHostService : public IViewportHostService {
@@ -568,8 +615,10 @@ private:
MarkSceneViewportRenderSuccess(entry.renderTargets, requests[0]); MarkSceneViewportRenderSuccess(entry.renderTargets, requests[0]);
const Core::uint32 pendingAsyncLoads = Resources::ResourceManager::Get().GetAsyncPendingCount(); const Core::uint32 pendingAsyncLoads = Resources::ResourceManager::Get().GetAsyncPendingCount();
if (pendingAsyncLoads > 0) { context.GetSceneManager().NotifySceneViewportFramePresented(pendingAsyncLoads);
entry.statusText = "Loading scene assets... (" + std::to_string(pendingAsyncLoads) + ")"; if (entry.statusText.empty()) {
entry.statusText = BuildViewportSceneLoadStatusText(
context.GetSceneManager().GetSceneLoadProgress());
} }
return true; return true;
} }

View File

@@ -0,0 +1,72 @@
#pragma once
#include <XCEngine/UI/DrawData.h>
#include <memory>
#include <string>
namespace XCEngine {
namespace UI {
struct UIRect;
struct UIPoint;
} // namespace UI
namespace Editor {
namespace XCUIBackend {
struct XCUIDemoInputState {
UI::UIRect canvasRect = {};
UI::UIPoint pointerPosition = {};
bool pointerInside = false;
bool pointerPressed = false;
bool pointerReleased = false;
bool pointerDown = false;
bool windowFocused = false;
bool shortcutPressed = false;
};
struct XCUIDemoFrameStats {
bool documentsReady = false;
std::string statusMessage = {};
std::size_t dependencyCount = 0;
std::size_t elementCount = 0;
std::size_t dirtyRootCount = 0;
std::size_t drawListCount = 0;
std::size_t commandCount = 0;
std::uint64_t treeGeneration = 0;
bool accentEnabled = false;
std::string lastCommandId = {};
std::string focusedElementId = {};
std::string hoveredElementId = {};
};
struct XCUIDemoFrameResult {
UI::UIDrawData drawData = {};
XCUIDemoFrameStats stats = {};
};
class XCUIDemoRuntime {
public:
XCUIDemoRuntime();
~XCUIDemoRuntime();
XCUIDemoRuntime(XCUIDemoRuntime&& other) noexcept;
XCUIDemoRuntime& operator=(XCUIDemoRuntime&& other) noexcept;
XCUIDemoRuntime(const XCUIDemoRuntime&) = delete;
XCUIDemoRuntime& operator=(const XCUIDemoRuntime&) = delete;
bool ReloadDocuments();
const XCUIDemoFrameResult& Update(const XCUIDemoInputState& input);
const XCUIDemoFrameResult& GetFrameResult() const;
bool TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const;
private:
struct RuntimeState;
std::unique_ptr<RuntimeState> m_state;
};
} // namespace XCUIBackend
} // namespace Editor
} // namespace XCEngine

View File

@@ -430,16 +430,11 @@ const char* SerializeBlendFactor(::XCEngine::Resources::MaterialBlendFactor fact
} }
} }
void ApplyMaterialStateToResource( void ApplyResolvedMaterialStateToResource(
const InspectorPanel::MaterialAssetState& state, const InspectorPanel::MaterialAssetState& state,
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shader,
::XCEngine::Resources::Material& material) { ::XCEngine::Resources::Material& material) {
const std::string shaderPath = TrimCopy(BufferToString(state.shaderPath)); material.SetShader(shader);
if (shaderPath.empty()) {
material.SetShader(::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>());
} else {
material.SetShader(::XCEngine::Resources::ResourceManager::Get().Load<::XCEngine::Resources::Shader>(shaderPath.c_str()));
}
material.SetShaderPass(TrimCopy(BufferToString(state.shaderPass)).c_str()); material.SetShaderPass(TrimCopy(BufferToString(state.shaderPass)).c_str());
material.SetRenderQueue(state.renderQueue); material.SetRenderQueue(state.renderQueue);
material.SetRenderState(state.renderState); material.SetRenderState(state.renderState);
@@ -454,6 +449,50 @@ void ApplyMaterialStateToResource(
material.RecalculateMemorySize(); material.RecalculateMemorySize();
} }
bool TryGetLoadedShaderHandle(
const std::string& shaderPath,
::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& outShader) {
outShader.Reset();
if (shaderPath.empty()) {
return false;
}
::XCEngine::Resources::IResource* cachedResource =
::XCEngine::Resources::ResourceManager::Get().Find(shaderPath.c_str());
if (cachedResource == nullptr ||
cachedResource->GetType() != ::XCEngine::Resources::ResourceType::Shader ||
!cachedResource->IsValid()) {
return false;
}
outShader = ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>(
static_cast<::XCEngine::Resources::Shader*>(cachedResource));
return outShader.IsValid();
}
std::string BuildMaterialLoadFailureMessage(const ::XCEngine::Resources::LoadResult& result) {
if (!result.errorMessage.Empty()) {
return std::string("Material file is invalid or unavailable: ") + result.errorMessage.CStr();
}
return "Material file is empty or invalid. Showing default values until you save it.";
}
std::string BuildShaderLoadFailureMessage(
const std::string& shaderPath,
const ::XCEngine::Resources::LoadResult& result) {
std::string message = "Failed to load shader for live material update";
if (!shaderPath.empty()) {
message += ": ";
message += shaderPath;
}
if (!result.errorMessage.Empty()) {
message += " - ";
message += result.errorMessage.CStr();
}
return message;
}
std::string BuildMaterialAssetFileText(const InspectorPanel::MaterialAssetState& state) { std::string BuildMaterialAssetFileText(const InspectorPanel::MaterialAssetState& state) {
std::vector<std::string> rootEntries; std::vector<std::string> rootEntries;
@@ -596,16 +635,130 @@ void InspectorPanel::SetSubjectMode(SubjectMode mode) {
} }
void InspectorPanel::ClearMaterialAsset() { void InspectorPanel::ClearMaterialAsset() {
InvalidateMaterialAssetAsyncState();
m_selectedMaterial.Reset(); m_selectedMaterial.Reset();
m_materialAssetState.Reset(); m_materialAssetState.Reset();
} }
void InspectorPanel::InvalidateMaterialAssetAsyncState() {
++m_materialAssetLoadRevision;
++m_materialShaderLoadRevision;
m_materialShaderLoadInFlight = false;
m_pendingMaterialShaderPath.clear();
}
void InspectorPanel::PopulateMaterialAssetStateFromResource(::XCEngine::Resources::Material& material) {
m_materialAssetState.shaderPath.fill('\0');
m_materialAssetState.shaderPass.fill('\0');
m_materialAssetState.renderQueue = material.GetRenderQueue();
m_materialAssetState.renderState = material.GetRenderState();
m_materialAssetState.tags.clear();
if (::XCEngine::Resources::Shader* shader = material.GetShader()) {
CopyToCharBuffer(std::string(shader->GetPath().CStr()), m_materialAssetState.shaderPath);
}
CopyToCharBuffer(std::string(material.GetShaderPass().CStr()), m_materialAssetState.shaderPass);
m_materialAssetState.tags.reserve(material.GetTagCount());
for (Core::uint32 tagIndex = 0; tagIndex < material.GetTagCount(); ++tagIndex) {
MaterialTagEditRow row;
CopyToCharBuffer(std::string(material.GetTagName(tagIndex).CStr()), row.name);
CopyToCharBuffer(std::string(material.GetTagValue(tagIndex).CStr()), row.value);
m_materialAssetState.tags.push_back(row);
}
m_materialAssetState.loaded = true;
m_materialAssetState.dirty = false;
}
void InspectorPanel::ApplyMaterialAssetStateToSelectedMaterial() {
if (!m_selectedMaterial.IsValid() || m_selectedMaterial.Get() == nullptr) {
return;
}
::XCEngine::Resources::Material& material = *m_selectedMaterial.Get();
const std::string shaderPath = TrimCopy(BufferToString(m_materialAssetState.shaderPath));
if (shaderPath.empty()) {
++m_materialShaderLoadRevision;
m_materialShaderLoadInFlight = false;
m_pendingMaterialShaderPath.clear();
ApplyResolvedMaterialStateToResource(
m_materialAssetState,
::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>(),
material);
m_materialAssetState.errorMessage.clear();
return;
}
::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader> shaderHandle;
if (TryGetLoadedShaderHandle(shaderPath, shaderHandle)) {
++m_materialShaderLoadRevision;
m_materialShaderLoadInFlight = false;
m_pendingMaterialShaderPath.clear();
ApplyResolvedMaterialStateToResource(m_materialAssetState, shaderHandle, material);
m_materialAssetState.errorMessage.clear();
return;
}
if (m_materialShaderLoadInFlight && m_pendingMaterialShaderPath == shaderPath) {
m_materialAssetState.errorMessage =
std::string("Loading shader for live material update: ") + shaderPath;
return;
}
const std::uint64_t shaderLoadRevision = ++m_materialShaderLoadRevision;
m_materialShaderLoadInFlight = true;
m_pendingMaterialShaderPath = shaderPath;
m_materialAssetState.errorMessage =
std::string("Loading shader for live material update: ") + shaderPath;
::XCEngine::Resources::ResourceManager::Get().LoadAsync(
shaderPath.c_str(),
::XCEngine::Resources::ResourceType::Shader,
[this, shaderLoadRevision, requestedShaderPath = shaderPath](::XCEngine::Resources::LoadResult result) {
if (shaderLoadRevision != m_materialShaderLoadRevision ||
m_subjectMode != SubjectMode::MaterialAsset ||
TrimCopy(BufferToString(m_materialAssetState.shaderPath)) != requestedShaderPath) {
return;
}
m_materialShaderLoadInFlight = false;
m_pendingMaterialShaderPath.clear();
if (!m_selectedMaterial.IsValid() || m_selectedMaterial.Get() == nullptr) {
m_materialAssetState.errorMessage.clear();
return;
}
if (!result || result.resource == nullptr) {
m_materialAssetState.errorMessage =
BuildShaderLoadFailureMessage(requestedShaderPath, result);
return;
}
::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader> loadedShader(
static_cast<::XCEngine::Resources::Shader*>(result.resource));
if (!loadedShader.IsValid()) {
m_materialAssetState.errorMessage =
BuildShaderLoadFailureMessage(requestedShaderPath, result);
return;
}
ApplyResolvedMaterialStateToResource(
m_materialAssetState,
loadedShader,
*m_selectedMaterial.Get());
m_materialAssetState.errorMessage.clear();
});
}
void InspectorPanel::ReloadMaterialAsset() { void InspectorPanel::ReloadMaterialAsset() {
if (!m_selectedAssetItem || !m_context) { if (!m_selectedAssetItem || !m_context) {
ClearMaterialAsset(); ClearMaterialAsset();
return; return;
} }
InvalidateMaterialAssetAsyncState();
m_selectedMaterial.Reset(); m_selectedMaterial.Reset();
m_materialAssetState.Reset(); m_materialAssetState.Reset();
m_materialAssetState.assetFullPath = m_selectedAssetItem->fullPath; m_materialAssetState.assetFullPath = m_selectedAssetItem->fullPath;
@@ -619,38 +772,41 @@ void InspectorPanel::ReloadMaterialAsset() {
return; return;
} }
m_selectedMaterial = ::XCEngine::Resources::ResourceManager::Get().Load<::XCEngine::Resources::Material>( const std::uint64_t loadRevision = m_materialAssetLoadRevision;
m_materialAssetState.assetPath.c_str()); const std::string requestedAssetPath = m_materialAssetState.assetPath;
if (!m_selectedMaterial.IsValid()) { m_materialAssetState.errorMessage = "Loading material asset from Library...";
m_materialAssetState.errorMessage = "Material file is empty or invalid. Showing default values until you save it.";
m_materialAssetState.loaded = true; ::XCEngine::Resources::ResourceManager::Get().LoadAsync(
m_materialAssetState.dirty = false; m_materialAssetState.assetPath.c_str(),
::XCEngine::Resources::ResourceType::Material,
[this, loadRevision, requestedAssetPath](::XCEngine::Resources::LoadResult result) {
if (loadRevision != m_materialAssetLoadRevision ||
m_subjectMode != SubjectMode::MaterialAsset ||
m_materialAssetState.assetPath != requestedAssetPath) {
return; return;
} }
::XCEngine::Resources::Material* material = m_selectedMaterial.Get(); if (!result || result.resource == nullptr) {
if (material == nullptr) { m_selectedMaterial.Reset();
m_materialAssetState.errorMessage = "Material resource is unavailable. Showing default values until you save it.";
m_materialAssetState.loaded = true; m_materialAssetState.loaded = true;
m_materialAssetState.dirty = false; m_materialAssetState.dirty = false;
m_materialAssetState.errorMessage = BuildMaterialLoadFailureMessage(result);
return; return;
} }
if (::XCEngine::Resources::Shader* shader = material->GetShader()) { m_selectedMaterial = ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Material>(
CopyToCharBuffer(std::string(shader->GetPath().CStr()), m_materialAssetState.shaderPath); static_cast<::XCEngine::Resources::Material*>(result.resource));
} if (!m_selectedMaterial.IsValid() || m_selectedMaterial.Get() == nullptr) {
CopyToCharBuffer(std::string(material->GetShaderPass().CStr()), m_materialAssetState.shaderPass); m_selectedMaterial.Reset();
m_materialAssetState.renderQueue = material->GetRenderQueue();
m_materialAssetState.renderState = material->GetRenderState();
m_materialAssetState.tags.reserve(material->GetTagCount());
for (Core::uint32 tagIndex = 0; tagIndex < material->GetTagCount(); ++tagIndex) {
MaterialTagEditRow row;
CopyToCharBuffer(std::string(material->GetTagName(tagIndex).CStr()), row.name);
CopyToCharBuffer(std::string(material->GetTagValue(tagIndex).CStr()), row.value);
m_materialAssetState.tags.push_back(row);
}
m_materialAssetState.loaded = true; m_materialAssetState.loaded = true;
m_materialAssetState.dirty = false; m_materialAssetState.dirty = false;
m_materialAssetState.errorMessage = BuildMaterialLoadFailureMessage(result);
return;
}
m_materialAssetState.errorMessage.clear();
PopulateMaterialAssetStateFromResource(*m_selectedMaterial.Get());
});
} }
void InspectorPanel::InspectMaterialAsset(const AssetItemPtr& item) { void InspectorPanel::InspectMaterialAsset(const AssetItemPtr& item) {
@@ -811,16 +967,9 @@ bool InspectorPanel::SaveMaterialAsset() {
return false; return false;
} }
if (!m_selectedMaterial.IsValid()) {
m_selectedMaterial = ::XCEngine::Resources::ResourceManager::Get().Load<::XCEngine::Resources::Material>(
m_materialAssetState.assetPath.c_str());
}
if (m_selectedMaterial.IsValid()) {
ApplyMaterialStateToResource(m_materialAssetState, *m_selectedMaterial.Get());
}
m_materialAssetState.errorMessage.clear(); m_materialAssetState.errorMessage.clear();
m_materialAssetState.dirty = false; m_materialAssetState.dirty = false;
ApplyMaterialAssetStateToSelectedMaterial();
return true; return true;
} catch (...) { } catch (...) {
m_materialAssetState.errorMessage = "Saving material asset failed."; m_materialAssetState.errorMessage = "Saving material asset failed.";

View File

@@ -76,6 +76,9 @@ private:
void SetSubjectMode(SubjectMode mode); void SetSubjectMode(SubjectMode mode);
void InspectMaterialAsset(const AssetItemPtr& item); void InspectMaterialAsset(const AssetItemPtr& item);
void ClearMaterialAsset(); void ClearMaterialAsset();
void InvalidateMaterialAssetAsyncState();
void PopulateMaterialAssetStateFromResource(::XCEngine::Resources::Material& material);
void ApplyMaterialAssetStateToSelectedMaterial();
void RenderMaterialAsset(); void RenderMaterialAsset();
void RenderUnsupportedAsset(); void RenderUnsupportedAsset();
bool SaveMaterialAsset(); bool SaveMaterialAsset();
@@ -92,6 +95,10 @@ private:
AssetItemPtr m_selectedAssetItem; AssetItemPtr m_selectedAssetItem;
::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Material> m_selectedMaterial; ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Material> m_selectedMaterial;
MaterialAssetState m_materialAssetState; MaterialAssetState m_materialAssetState;
std::uint64_t m_materialAssetLoadRevision = 0;
std::uint64_t m_materialShaderLoadRevision = 0;
bool m_materialShaderLoadInFlight = false;
std::string m_pendingMaterialShaderPath;
UI::DeferredPopupState m_addComponentPopup; UI::DeferredPopupState m_addComponentPopup;
std::function<void()> m_deferredContextAction; std::function<void()> m_deferredContextAction;
}; };

View File

@@ -4,6 +4,7 @@
#include "ProjectPanel.h" #include "ProjectPanel.h"
#include "Core/IEditorContext.h" #include "Core/IEditorContext.h"
#include "Core/IProjectManager.h" #include "Core/IProjectManager.h"
#include "Core/ISceneManager.h"
#include "Core/AssetItem.h" #include "Core/AssetItem.h"
#include "Platform/Win32Utf8.h" #include "Platform/Win32Utf8.h"
#include "Utils/ProjectFileUtils.h" #include "Utils/ProjectFileUtils.h"
@@ -60,6 +61,51 @@ ImVec4 ResolveImportStatusColor(const XCEngine::Resources::AssetImportService::I
: XCEngine::Editor::UI::ConsoleErrorColor(); : XCEngine::Editor::UI::ConsoleErrorColor();
} }
std::uint64_t ResolveSceneLoadElapsedMs(const XCEngine::Editor::SceneLoadProgressSnapshot& status) {
if (!status.HasValue() || status.startedAtMs == 0) {
return 0;
}
if (status.inProgress) {
const auto nowMs = static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
return nowMs >= status.startedAtMs ? nowMs - status.startedAtMs : 0;
}
if (status.streamingCompletedAtMs >= status.startedAtMs && status.streamingCompletedAtMs != 0) {
return status.streamingCompletedAtMs - status.startedAtMs;
}
if (status.firstFrameAtMs >= status.startedAtMs && status.firstFrameAtMs != 0) {
return status.firstFrameAtMs - status.startedAtMs;
}
if (status.structureReadyAtMs >= status.startedAtMs && status.structureReadyAtMs != 0) {
return status.structureReadyAtMs - status.startedAtMs;
}
return 0;
}
std::uint64_t ResolveSceneLoadMarkerDeltaMs(std::uint64_t startMs, std::uint64_t endMs) {
return endMs >= startMs && startMs != 0 ? endMs - startMs : 0;
}
ImVec4 ResolveSceneLoadStatusColor(const XCEngine::Editor::SceneLoadProgressSnapshot& status) {
if (!status.HasValue()) {
return XCEngine::Editor::UI::ConsoleSecondaryTextColor();
}
if (status.inProgress) {
return XCEngine::Editor::UI::ConsoleWarningColor();
}
return status.success
? XCEngine::Editor::UI::ConsoleSecondaryTextColor()
: XCEngine::Editor::UI::ConsoleErrorColor();
}
std::string BuildImportStatusText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) { std::string BuildImportStatusText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
if (!status.HasValue()) { if (!status.HasValue()) {
return "Library: ready"; return "Library: ready";
@@ -76,6 +122,32 @@ std::string BuildImportStatusText(const XCEngine::Resources::AssetImportService:
return text; return text;
} }
std::string BuildSceneLoadStatusText(const XCEngine::Editor::SceneLoadProgressSnapshot& status) {
if (!status.HasValue()) {
return "Scene: ready";
}
std::string text = "Scene: ";
if (!status.success) {
text += status.message.empty() ? "failed" : status.message;
return text;
}
if (status.inProgress) {
text += status.message.empty() ? "loading..." : status.message;
} else {
text += "ready";
}
const std::uint64_t elapsedMs = ResolveSceneLoadElapsedMs(status);
if (elapsedMs > 0) {
text += " (";
text += std::to_string(elapsedMs);
text += " ms)";
}
return text;
}
std::string BuildImportStatusTooltipText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) { std::string BuildImportStatusTooltipText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
if (!status.HasValue()) { if (!status.HasValue()) {
return "No import operations have been recorded in this session."; return "No import operations have been recorded in this session.";
@@ -107,6 +179,68 @@ std::string BuildImportStatusTooltipText(const XCEngine::Resources::AssetImportS
return tooltip; return tooltip;
} }
std::string BuildSceneLoadStatusTooltipText(const XCEngine::Editor::SceneLoadProgressSnapshot& status) {
if (!status.HasValue()) {
return "No scene load operations have been recorded in this session.";
}
std::string tooltip;
tooltip += "Operation: ";
tooltip += status.operation.empty() ? "Scene Load" : status.operation;
if (!status.scenePath.empty()) {
tooltip += "\nScene: ";
tooltip += status.scenePath;
}
tooltip += "\nState: ";
tooltip += status.inProgress ? "Running" : (status.success ? "Succeeded" : "Failed");
const std::uint64_t structureMs =
ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.structureReadyAtMs);
if (structureMs > 0) {
tooltip += "\nStructure restore: ";
tooltip += std::to_string(structureMs);
tooltip += " ms";
}
const std::uint64_t firstFrameMs =
ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.firstFrameAtMs);
if (firstFrameMs > 0) {
tooltip += "\nFirst viewport frame: ";
tooltip += std::to_string(firstFrameMs);
tooltip += " ms";
}
const std::uint64_t interactiveMs =
ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.interactiveAtMs);
if (interactiveMs > 0) {
tooltip += "\nFirst interactive frame: ";
tooltip += std::to_string(interactiveMs);
tooltip += " ms";
}
const std::uint64_t streamingMs =
ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.streamingCompletedAtMs);
if (streamingMs > 0) {
tooltip += "\nRuntime streaming complete: ";
tooltip += std::to_string(streamingMs);
tooltip += " ms";
}
if (status.peakPendingAsyncLoads > 0) {
tooltip += "\nPeak async loads: ";
tooltip += std::to_string(status.peakPendingAsyncLoads);
}
if (status.inProgress && status.currentPendingAsyncLoads > 0) {
tooltip += "\nPending async loads: ";
tooltip += std::to_string(status.currentPendingAsyncLoads);
}
if (!status.message.empty()) {
tooltip += "\nStatus: ";
tooltip += status.message;
}
return tooltip;
}
template <typename Fn> template <typename Fn>
void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) { void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) {
if (!pendingAction) { if (!pendingAction) {
@@ -457,8 +591,12 @@ void ProjectPanel::Render() {
void ProjectPanel::RenderToolbar() { void ProjectPanel::RenderToolbar() {
const auto importStatus = ::XCEngine::Resources::ResourceManager::Get().GetProjectAssetImportStatus(); const auto importStatus = ::XCEngine::Resources::ResourceManager::Get().GetProjectAssetImportStatus();
const SceneLoadProgressSnapshot sceneLoadStatus =
m_context != nullptr ? m_context->GetSceneManager().GetSceneLoadProgress() : SceneLoadProgressSnapshot{};
const std::string importStatusText = BuildImportStatusText(importStatus); const std::string importStatusText = BuildImportStatusText(importStatus);
const std::string importStatusTooltip = BuildImportStatusTooltipText(importStatus); const std::string importStatusTooltip = BuildImportStatusTooltipText(importStatus);
const std::string sceneLoadStatusText = BuildSceneLoadStatusText(sceneLoadStatus);
const std::string sceneLoadStatusTooltip = BuildSceneLoadStatusTooltipText(sceneLoadStatus);
UI::PanelToolbarScope toolbar( UI::PanelToolbarScope toolbar(
"ProjectToolbar", "ProjectToolbar",
@@ -473,8 +611,9 @@ void ProjectPanel::RenderToolbar() {
} }
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f));
if (ImGui::BeginTable("##ProjectToolbarLayout", 2, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) { if (ImGui::BeginTable("##ProjectToolbarLayout", 3, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("##ImportStatus", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("##ImportStatus", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##SceneLoadStatus", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 220.0f); ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 220.0f);
ImGui::TableNextRow(); ImGui::TableNextRow();
@@ -492,6 +631,19 @@ void ProjectPanel::RenderToolbar() {
ImGui::EndTooltip(); ImGui::EndTooltip();
} }
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::PushStyleColor(ImGuiCol_Text, ResolveSceneLoadStatusColor(sceneLoadStatus));
ImGui::TextUnformatted(sceneLoadStatusText.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered() && !sceneLoadStatusTooltip.empty()) {
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 36.0f);
ImGui::TextUnformatted(sceneLoadStatusTooltip.c_str());
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
ImGui::TableNextColumn(); ImGui::TableNextColumn();
UI::ToolbarSearchField("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer)); UI::ToolbarSearchField("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer));

View File

@@ -83,7 +83,9 @@ inline void RenderViewportInteractionSurface(
result.clickedMiddle = ImGui::IsItemClicked(ImGuiMouseButton_Middle); result.clickedMiddle = ImGui::IsItemClicked(ImGuiMouseButton_Middle);
} }
inline ViewportPanelContentResult RenderViewportPanelContent(IEditorContext& context, EditorViewportKind kind) { inline ViewportPanelContentResult RenderViewportPanelContent(
IEditorContext& context,
EditorViewportKind kind) {
ViewportPanelContentResult result = {}; ViewportPanelContentResult result = {};
UI::CollapsePanelSectionSpacing(); UI::CollapsePanelSectionSpacing();
result.availableSize = ImGui::GetContentRegionAvail(); result.availableSize = ImGui::GetContentRegionAvail();

View File

@@ -14,7 +14,7 @@ public:
virtual void onDetach() {} virtual void onDetach() {}
virtual void onUpdate(float dt) {} virtual void onUpdate(float dt) {}
virtual void onEvent(void* event) {} virtual void onEvent(void* event) {}
virtual void onImGuiRender() {} virtual void onUIRender() {}
const std::string& getName() const { return m_name; } const std::string& getName() const { return m_name; }

View File

@@ -20,7 +20,7 @@ public:
void onUpdate(float dt); void onUpdate(float dt);
void onEvent(void* event); void onEvent(void* event);
void onImGuiRender(); void onUIRender();
void onAttach(); void onAttach();
void onDetach(); void onDetach();
@@ -78,9 +78,9 @@ inline void LayerStack::onEvent(void* event) {
} }
} }
inline void LayerStack::onImGuiRender() { inline void LayerStack::onUIRender() {
for (auto& layer : m_layers) { for (auto& layer : m_layers) {
layer->onImGuiRender(); layer->onUIRender();
} }
} }