diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index e27f0d3b..0907e78c 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -42,6 +42,10 @@ Old `editor` replacement is explicitly out of scope for this phase. - ImGui factory entry points now live only in `LegacyImGuiHostInterop.h` - `XCUIEditorFontSetup.h` no longer exposes `ImFont` / `ImFontAtlas`; the public API is now a current-context font bootstrap helper - `XCUIStandaloneTextAtlasProvider.h` no longer depends on the ImGui-oriented font bootstrap header +- The generic shell command/state boundary is now narrower as well: + - `XCUIShellChromeState` no longer carries the legacy host demo-window toggle or command id on the generic shell-state surface + - `Application.h` no longer exposes that legacy demo command through generic `ShellCommandIds`, `ShellCommandBindings`, or `RegisterShellViewCommands(...)` + - the legacy demo window toggle now lives as a compatibility-only command inside `ApplicationLegacyImGui.cpp` - Old `editor` replacement remains deferred; all active execution still stays inside XCUI shared code and `new_editor`. ## Three-Layer Status @@ -120,7 +124,7 @@ Current gap: - `XCUIDemoRuntime` now bridges pointer activation, text-edit commands, and shortcut-triggered commands through a unified command path, and `DrainPendingCommandIds()` now preserves mixed pointer/text/shortcut ordering. - `new_editor` now also has a pure `XCUIShellChromeState` model covering panel visibility, hosted-preview mode, and shell-level view toggles without depending on ImGui, `Application`, or the old editor. - `XCUIShellChromeState` hosted-preview mode naming is now backend-neutral (`HostedPresenter` / `NativeOffscreen`) instead of encoding `ImGui` into the XCUI shell model. -- The shell chrome view-toggle model no longer encodes the legacy Dear ImGui demo window as a generic `ImGuiDemoWindow` concept; that compatibility-only toggle is now named `LegacyHostDemoWindow` / `new_editor.view.legacy_host_demo` at the editor-layer seam. +- The shell chrome view-toggle model no longer carries the legacy host demo window at all on the generic XCUI seam; that command now lives only inside the compatibility-only legacy host implementation. - `new_editor` now also has a concrete `NativeWindowUICompositor` path and native-focused compositor tests, so the window compositor seam is no longer ImGui-only. - `Application` now also has a native XCUI shell path that: - becomes the default `new_editor` startup path diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index a8fcd671..9263e805 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -607,7 +607,9 @@ if(MSVC) target_compile_options(XCEngine PRIVATE /FS $<$:/Z7>) - target_link_libraries(XCEngine PUBLIC delayimp) + target_link_libraries(XCEngine PUBLIC + delayimp + d3dcompiler) target_link_options(XCEngine INTERFACE "/DELAYLOAD:assimp-vc143-mt.dll") set_target_properties(XCEngine PROPERTIES MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>" diff --git a/new_editor/src/Application.h b/new_editor/src/Application.h index e55e0af9..f0e15f73 100644 --- a/new_editor/src/Application.h +++ b/new_editor/src/Application.h @@ -60,8 +60,6 @@ public: ::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleXCUIDemoPanel; static constexpr const char* ToggleXCUILayoutLabPanel = ::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleXCUILayoutLabPanel; - static constexpr const char* ToggleLegacyHostDemoWindow = - ::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleLegacyHostDemoWindow; static constexpr const char* ToggleNativeBackdrop = ::XCEngine::Editor::XCUIBackend::XCUIShellChromeCommandIds::ToggleNativeBackdrop; static constexpr const char* TogglePulseAccent = @@ -81,8 +79,6 @@ public: std::function setXCUIDemoPanelVisible = {}; std::function getXCUILayoutLabPanelVisible = {}; std::function setXCUILayoutLabPanelVisible = {}; - std::function getLegacyHostDemoWindowVisible = {}; - std::function setLegacyHostDemoWindowVisible = {}; std::function getNativeBackdropVisible = {}; std::function setNativeBackdropVisible = {}; std::function getPulseAccentEnabled = {}; @@ -213,15 +209,6 @@ public: ctrlOnly, true, false } }); - bindToggleCommand( - ShellCommandIds::ToggleLegacyHostDemoWindow, - bindings.getLegacyHostDemoWindowVisible, - bindings.setLegacyHostDemoWindowVisible, - { XCUIEditorCommandAccelerator{ - static_cast(KeyCode::Three), - ctrlOnly, - true, - false } }); bindToggleCommand( ShellCommandIds::ToggleNativeBackdrop, bindings.getNativeBackdropVisible, @@ -469,6 +456,7 @@ private: ::XCEngine::Editor::XCUIBackend::NativeXCUIPanelCanvasHost m_nativeDemoCanvasHost; ::XCEngine::Editor::XCUIBackend::NativeXCUIPanelCanvasHost m_nativeLayoutCanvasHost; ShellPanelId m_nativeActivePanel = ShellPanelId::XCUIDemo; + bool m_legacyHostDemoWindowVisible = false; bool m_nativeDemoReloadSucceeded = false; bool m_nativeLayoutReloadSucceeded = false; ::XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime m_nativeOverlayRuntime; diff --git a/new_editor/src/ApplicationLegacyImGui.cpp b/new_editor/src/ApplicationLegacyImGui.cpp index 9fa53be7..25cffc15 100644 --- a/new_editor/src/ApplicationLegacyImGui.cpp +++ b/new_editor/src/ApplicationLegacyImGui.cpp @@ -15,6 +15,7 @@ namespace NewEditor { namespace { constexpr float kLegacyHostClearColor[4] = { 0.08f, 0.09f, 0.11f, 1.0f }; +constexpr const char* kLegacyHostDemoWindowCommandId = "new_editor.view.legacy_host_demo"; std::uint64_t MakeLegacyHostFrameTimestampNanoseconds() { return static_cast( @@ -65,6 +66,37 @@ const char* GetHostedPreviewStateLabel( return "idle"; } +void RegisterLegacyHostDemoWindowCommand( + ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandRouter& router, + const std::function& getter, + const std::function& setter) { + if (!getter || !setter) { + return; + } + + using ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandAccelerator; + using ::XCEngine::Editor::XCUIBackend::XCUIEditorCommandDefinition; + using ::XCEngine::Input::KeyCode; + + XCUIEditorCommandDefinition definition = {}; + definition.commandId = kLegacyHostDemoWindowCommandId; + definition.isEnabled = [getter, setter]() { + return static_cast(getter) && static_cast(setter); + }; + definition.invoke = [getter, setter]() { + setter(!getter()); + }; + definition.accelerators = { + XCUIEditorCommandAccelerator{ + static_cast(KeyCode::Three), + { false, true, false, false }, + true, + false, + }, + }; + router.RegisterCommand(definition); +} + } // namespace Application::Application() = default; @@ -155,12 +187,6 @@ void Application::ConfigureShellCommandRouter() { m_layoutLabPanel->SetVisible(visible); } }; - bindings.getLegacyHostDemoWindowVisible = [this]() { - return IsShellViewToggleEnabled(ShellViewToggleId::LegacyHostDemoWindow); - }; - bindings.setLegacyHostDemoWindowVisible = [this](bool visible) { - SetShellViewToggleEnabled(ShellViewToggleId::LegacyHostDemoWindow, visible); - }; bindings.getNativeBackdropVisible = [this]() { return IsShellViewToggleEnabled(ShellViewToggleId::NativeBackdrop); }; @@ -204,6 +230,10 @@ void Application::ConfigureShellCommandRouter() { bindings.onHostedPreviewModeChanged = [this]() { ConfigureHostedPreviewPresenters(); }; Application::RegisterShellViewCommands(m_shellCommandRouter, bindings); + RegisterLegacyHostDemoWindowCommand( + m_shellCommandRouter, + [this]() { return m_legacyHostDemoWindowVisible; }, + [this](bool visible) { m_legacyHostDemoWindowVisible = visible; }); } ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta @@ -308,9 +338,9 @@ void Application::RenderLegacyImGuiUiFrame() { m_layoutLabPanel->RenderIfVisible(); } - bool showLegacyHostDemoWindow = IsShellViewToggleEnabled(ShellViewToggleId::LegacyHostDemoWindow); + bool showLegacyHostDemoWindow = m_legacyHostDemoWindowVisible; if (::XCEngine::Editor::XCUIBackend::RenderLegacyImGuiDemoWindow(showLegacyHostDemoWindow)) { - SetShellViewToggleEnabled(ShellViewToggleId::LegacyHostDemoWindow, showLegacyHostDemoWindow); + m_legacyHostDemoWindowVisible = showLegacyHostDemoWindow; } SyncShellChromePanelStateFromPanels(); @@ -371,8 +401,8 @@ void Application::RenderShellChrome() { drawCommandMenuItem( "Legacy Host Demo", "Ctrl+3", - IsShellViewToggleEnabled(ShellViewToggleId::LegacyHostDemoWindow), - ShellCommandIds::ToggleLegacyHostDemoWindow); + m_legacyHostDemoWindowVisible, + kLegacyHostDemoWindowCommandId); ImGui::Separator(); drawCommandMenuItem( "Native Backdrop", diff --git a/new_editor/src/XCUIBackend/XCUIShellChromeState.cpp b/new_editor/src/XCUIBackend/XCUIShellChromeState.cpp index 8305ae88..75f1b618 100644 --- a/new_editor/src/XCUIBackend/XCUIShellChromeState.cpp +++ b/new_editor/src/XCUIBackend/XCUIShellChromeState.cpp @@ -13,7 +13,6 @@ constexpr std::size_t ToIndex(XCUIShellPanelId panelId) { constexpr std::string_view kViewMenuLabel = "View"; constexpr std::string_view kXCUIDemoShortcut = "Ctrl+1"; constexpr std::string_view kXCUILayoutLabShortcut = "Ctrl+2"; -constexpr std::string_view kLegacyHostDemoShortcut = "Ctrl+3"; constexpr std::string_view kNativeBackdropShortcut = "Ctrl+Shift+B"; constexpr std::string_view kPulseAccentShortcut = "Ctrl+Shift+P"; constexpr std::string_view kNativeXCUIOverlayShortcut = "Ctrl+Shift+O"; @@ -164,8 +163,6 @@ bool XCUIShellChromeState::ToggleHostedPreviewMode(XCUIShellPanelId panelId) { bool XCUIShellChromeState::GetViewToggle(XCUIShellViewToggleId toggleId) const { switch (toggleId) { - case XCUIShellViewToggleId::LegacyHostDemoWindow: - return m_viewToggles.legacyHostDemoWindowVisible; case XCUIShellViewToggleId::NativeBackdrop: return m_viewToggles.nativeBackdropVisible; case XCUIShellViewToggleId::PulseAccent: @@ -183,9 +180,6 @@ bool XCUIShellChromeState::GetViewToggle(XCUIShellViewToggleId toggleId) const { bool XCUIShellChromeState::SetViewToggle(XCUIShellViewToggleId toggleId, bool enabled) { bool* target = nullptr; switch (toggleId) { - case XCUIShellViewToggleId::LegacyHostDemoWindow: - target = &m_viewToggles.legacyHostDemoWindowVisible; - break; case XCUIShellViewToggleId::NativeBackdrop: target = &m_viewToggles.nativeBackdropVisible; break; @@ -227,9 +221,6 @@ bool XCUIShellChromeState::InvokeCommand(std::string_view commandId) { if (commandId == XCUIShellChromeCommandIds::ToggleXCUILayoutLabPanel) { return TogglePanelVisible(XCUIShellPanelId::XCUILayoutLab); } - if (commandId == XCUIShellChromeCommandIds::ToggleLegacyHostDemoWindow) { - return ToggleViewToggle(XCUIShellViewToggleId::LegacyHostDemoWindow); - } if (commandId == XCUIShellChromeCommandIds::ToggleNativeBackdrop) { return ToggleViewToggle(XCUIShellViewToggleId::NativeBackdrop); } @@ -288,15 +279,6 @@ bool XCUIShellChromeState::TryGetCommandDescriptor( return true; } - if (commandId == GetViewToggleCommandId(XCUIShellViewToggleId::LegacyHostDemoWindow)) { - outDescriptor.label = "Legacy Host Demo"; - outDescriptor.shortcut = kLegacyHostDemoShortcut; - outDescriptor.commandId = commandId; - outDescriptor.checkable = true; - outDescriptor.checked = GetViewToggle(XCUIShellViewToggleId::LegacyHostDemoWindow); - outDescriptor.enabled = true; - return true; - } if (commandId == GetViewToggleCommandId(XCUIShellViewToggleId::NativeBackdrop)) { outDescriptor.label = "Native Backdrop"; outDescriptor.shortcut = kNativeBackdropShortcut; @@ -358,7 +340,7 @@ bool XCUIShellChromeState::TryGetCommandDescriptor( XCUIShellMenuDescriptor XCUIShellChromeState::BuildViewMenuDescriptor() const { XCUIShellMenuDescriptor descriptor = {}; descriptor.label = kViewMenuLabel; - descriptor.items.reserve(10u); + descriptor.items.reserve(9u); const auto appendCommandItem = [this, &descriptor](std::string_view commandId) { XCUIShellCommandDescriptor commandDescriptor = {}; @@ -374,7 +356,6 @@ XCUIShellMenuDescriptor XCUIShellChromeState::BuildViewMenuDescriptor() const { appendCommandItem(GetPanelVisibilityCommandId(XCUIShellPanelId::XCUIDemo)); appendCommandItem(GetPanelVisibilityCommandId(XCUIShellPanelId::XCUILayoutLab)); - appendCommandItem(GetViewToggleCommandId(XCUIShellViewToggleId::LegacyHostDemoWindow)); descriptor.items.push_back({ XCUIShellMenuItemKind::Separator, {} }); @@ -414,8 +395,6 @@ std::string_view XCUIShellChromeState::GetPanelPreviewModeCommandId(XCUIShellPan std::string_view XCUIShellChromeState::GetViewToggleCommandId(XCUIShellViewToggleId toggleId) { switch (toggleId) { - case XCUIShellViewToggleId::LegacyHostDemoWindow: - return XCUIShellChromeCommandIds::ToggleLegacyHostDemoWindow; case XCUIShellViewToggleId::NativeBackdrop: return XCUIShellChromeCommandIds::ToggleNativeBackdrop; case XCUIShellViewToggleId::PulseAccent: diff --git a/new_editor/src/XCUIBackend/XCUIShellChromeState.h b/new_editor/src/XCUIBackend/XCUIShellChromeState.h index 8c40463d..42e83f45 100644 --- a/new_editor/src/XCUIBackend/XCUIShellChromeState.h +++ b/new_editor/src/XCUIBackend/XCUIShellChromeState.h @@ -16,8 +16,7 @@ enum class XCUIShellPanelId : std::uint8_t { }; enum class XCUIShellViewToggleId : std::uint8_t { - LegacyHostDemoWindow = 0, - NativeBackdrop, + NativeBackdrop = 0, PulseAccent, NativeXCUIOverlay, HostedPreviewHud, @@ -46,7 +45,6 @@ struct XCUIShellPanelChromeState { }; struct XCUIShellViewToggleState { - bool legacyHostDemoWindowVisible = false; bool nativeBackdropVisible = true; bool pulseAccentEnabled = true; bool nativeXCUIOverlayVisible = true; @@ -56,7 +54,6 @@ struct XCUIShellViewToggleState { struct XCUIShellChromeCommandIds { static constexpr const char* ToggleXCUIDemoPanel = "new_editor.view.xcui_demo"; static constexpr const char* ToggleXCUILayoutLabPanel = "new_editor.view.xcui_layout_lab"; - static constexpr const char* ToggleLegacyHostDemoWindow = "new_editor.view.legacy_host_demo"; static constexpr const char* ToggleNativeBackdrop = "new_editor.view.native_backdrop"; static constexpr const char* TogglePulseAccent = "new_editor.view.pulse_accent"; static constexpr const char* ToggleNativeXCUIOverlay = "new_editor.view.native_xcui_overlay"; diff --git a/tests/NewEditor/test_application_shell_command_bindings.cpp b/tests/NewEditor/test_application_shell_command_bindings.cpp index a1b5e0fc..c345364f 100644 --- a/tests/NewEditor/test_application_shell_command_bindings.cpp +++ b/tests/NewEditor/test_application_shell_command_bindings.cpp @@ -71,12 +71,6 @@ struct ShellCommandHarness { bindings.setXCUILayoutLabPanelVisible = [this](bool visible) { panels[ToPanelIndex(Application::ShellPanelId::XCUILayoutLab)].visible = visible; }; - bindings.getLegacyHostDemoWindowVisible = [this]() { - return viewToggles.legacyHostDemoWindowVisible; - }; - bindings.setLegacyHostDemoWindowVisible = [this](bool visible) { - viewToggles.legacyHostDemoWindowVisible = visible; - }; bindings.getNativeBackdropVisible = [this]() { return viewToggles.nativeBackdropVisible; }; @@ -134,7 +128,6 @@ TEST(ApplicationShellCommandBindingsTest, RegisterShellViewCommandsInvokesBoundT EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleXCUIDemoPanel)); EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleXCUILayoutLabPanel)); - EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleLegacyHostDemoWindow)); EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeBackdrop)); EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::TogglePulseAccent)); EXPECT_TRUE(router.HasCommand(Application::ShellCommandIds::ToggleNativeXCUIOverlay)); @@ -144,8 +137,6 @@ TEST(ApplicationShellCommandBindingsTest, RegisterShellViewCommandsInvokesBoundT EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleXCUIDemoPanel)); EXPECT_FALSE(harness.Panel(Application::ShellPanelId::XCUIDemo).visible); - EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleLegacyHostDemoWindow)); - EXPECT_TRUE(harness.viewToggles.legacyHostDemoWindowVisible); EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeBackdrop)); EXPECT_FALSE(harness.viewToggles.nativeBackdropVisible); EXPECT_TRUE(router.InvokeCommand(Application::ShellCommandIds::ToggleNativeXCUIOverlay)); diff --git a/tests/NewEditor/test_xcui_shell_chrome_state.cpp b/tests/NewEditor/test_xcui_shell_chrome_state.cpp index 67ebaf8a..fd2db4bb 100644 --- a/tests/NewEditor/test_xcui_shell_chrome_state.cpp +++ b/tests/NewEditor/test_xcui_shell_chrome_state.cpp @@ -17,7 +17,6 @@ TEST(XCUIShellChromeStateTest, DefaultsMatchCurrentShellChromeConfiguration) { XCUIShellChromeState state = {}; const auto& viewToggles = state.GetViewToggles(); - EXPECT_FALSE(viewToggles.legacyHostDemoWindowVisible); EXPECT_TRUE(viewToggles.nativeBackdropVisible); EXPECT_TRUE(viewToggles.pulseAccentEnabled); EXPECT_TRUE(viewToggles.nativeXCUIOverlayVisible); @@ -118,10 +117,6 @@ TEST(XCUIShellChromeStateTest, HostedPreviewStateSeparatesEnablementFromRequeste TEST(XCUIShellChromeStateTest, ViewToggleMutatorsOnlyFlipRequestedFlags) { XCUIShellChromeState state = {}; - EXPECT_TRUE(state.SetViewToggle(XCUIShellViewToggleId::LegacyHostDemoWindow, true)); - EXPECT_TRUE(state.GetViewToggle(XCUIShellViewToggleId::LegacyHostDemoWindow)); - EXPECT_FALSE(state.SetViewToggle(XCUIShellViewToggleId::LegacyHostDemoWindow, true)); - EXPECT_TRUE(state.ToggleViewToggle(XCUIShellViewToggleId::HostedPreviewHud)); EXPECT_FALSE(state.GetViewToggle(XCUIShellViewToggleId::HostedPreviewHud)); EXPECT_TRUE(state.GetViewToggle(XCUIShellViewToggleId::NativeBackdrop)); @@ -190,7 +185,7 @@ TEST(XCUIShellChromeStateTest, ViewMenuDescriptorMatchesCurrentApplicationOrderi const auto menu = state.BuildViewMenuDescriptor(); ASSERT_EQ(menu.label, "View"); - ASSERT_EQ(menu.items.size(), 10u); + ASSERT_EQ(menu.items.size(), 9u); ASSERT_EQ(menu.items[0].kind, XCUIShellMenuItemKind::Command); EXPECT_EQ(menu.items[0].command.label, "XCUI Demo"); @@ -203,42 +198,37 @@ TEST(XCUIShellChromeStateTest, ViewMenuDescriptorMatchesCurrentApplicationOrderi EXPECT_EQ(menu.items[1].command.shortcut, "Ctrl+2"); EXPECT_EQ(menu.items[1].command.commandId, XCUIShellChromeCommandIds::ToggleXCUILayoutLabPanel); - ASSERT_EQ(menu.items[2].kind, XCUIShellMenuItemKind::Command); - EXPECT_EQ(menu.items[2].command.label, "Legacy Host Demo"); - EXPECT_EQ(menu.items[2].command.shortcut, "Ctrl+3"); - EXPECT_EQ(menu.items[2].command.commandId, XCUIShellChromeCommandIds::ToggleLegacyHostDemoWindow); + EXPECT_EQ(menu.items[2].kind, XCUIShellMenuItemKind::Separator); - EXPECT_EQ(menu.items[3].kind, XCUIShellMenuItemKind::Separator); + ASSERT_EQ(menu.items[3].kind, XCUIShellMenuItemKind::Command); + EXPECT_EQ(menu.items[3].command.label, "Native Backdrop"); + EXPECT_EQ(menu.items[3].command.shortcut, "Ctrl+Shift+B"); + EXPECT_EQ(menu.items[3].command.commandId, XCUIShellChromeCommandIds::ToggleNativeBackdrop); ASSERT_EQ(menu.items[4].kind, XCUIShellMenuItemKind::Command); - EXPECT_EQ(menu.items[4].command.label, "Native Backdrop"); - EXPECT_EQ(menu.items[4].command.shortcut, "Ctrl+Shift+B"); - EXPECT_EQ(menu.items[4].command.commandId, XCUIShellChromeCommandIds::ToggleNativeBackdrop); + EXPECT_EQ(menu.items[4].command.label, "Pulse Accent"); + EXPECT_EQ(menu.items[4].command.shortcut, "Ctrl+Shift+P"); + EXPECT_EQ(menu.items[4].command.commandId, XCUIShellChromeCommandIds::TogglePulseAccent); ASSERT_EQ(menu.items[5].kind, XCUIShellMenuItemKind::Command); - EXPECT_EQ(menu.items[5].command.label, "Pulse Accent"); - EXPECT_EQ(menu.items[5].command.shortcut, "Ctrl+Shift+P"); - EXPECT_EQ(menu.items[5].command.commandId, XCUIShellChromeCommandIds::TogglePulseAccent); + EXPECT_EQ(menu.items[5].command.label, "Native XCUI Overlay"); + EXPECT_EQ(menu.items[5].command.shortcut, "Ctrl+Shift+O"); + EXPECT_EQ(menu.items[5].command.commandId, XCUIShellChromeCommandIds::ToggleNativeXCUIOverlay); ASSERT_EQ(menu.items[6].kind, XCUIShellMenuItemKind::Command); - EXPECT_EQ(menu.items[6].command.label, "Native XCUI Overlay"); - EXPECT_EQ(menu.items[6].command.shortcut, "Ctrl+Shift+O"); - EXPECT_EQ(menu.items[6].command.commandId, XCUIShellChromeCommandIds::ToggleNativeXCUIOverlay); + EXPECT_EQ(menu.items[6].command.label, "Hosted Preview HUD"); + EXPECT_EQ(menu.items[6].command.shortcut, "Ctrl+Shift+H"); + EXPECT_EQ(menu.items[6].command.commandId, XCUIShellChromeCommandIds::ToggleHostedPreviewHud); ASSERT_EQ(menu.items[7].kind, XCUIShellMenuItemKind::Command); - EXPECT_EQ(menu.items[7].command.label, "Hosted Preview HUD"); - EXPECT_EQ(menu.items[7].command.shortcut, "Ctrl+Shift+H"); - EXPECT_EQ(menu.items[7].command.commandId, XCUIShellChromeCommandIds::ToggleHostedPreviewHud); + EXPECT_EQ(menu.items[7].command.label, "Native Demo Panel Preview"); + EXPECT_EQ(menu.items[7].command.shortcut, "Ctrl+Alt+1"); + EXPECT_EQ(menu.items[7].command.commandId, XCUIShellChromeCommandIds::ToggleNativeDemoPanelPreview); ASSERT_EQ(menu.items[8].kind, XCUIShellMenuItemKind::Command); - EXPECT_EQ(menu.items[8].command.label, "Native Demo Panel Preview"); - EXPECT_EQ(menu.items[8].command.shortcut, "Ctrl+Alt+1"); - EXPECT_EQ(menu.items[8].command.commandId, XCUIShellChromeCommandIds::ToggleNativeDemoPanelPreview); - - ASSERT_EQ(menu.items[9].kind, XCUIShellMenuItemKind::Command); - EXPECT_EQ(menu.items[9].command.label, "Native Layout Lab Preview"); - EXPECT_EQ(menu.items[9].command.shortcut, "Ctrl+Alt+2"); - EXPECT_EQ(menu.items[9].command.commandId, XCUIShellChromeCommandIds::ToggleNativeLayoutLabPreview); + EXPECT_EQ(menu.items[8].command.label, "Native Layout Lab Preview"); + EXPECT_EQ(menu.items[8].command.shortcut, "Ctrl+Alt+2"); + EXPECT_EQ(menu.items[8].command.commandId, XCUIShellChromeCommandIds::ToggleNativeLayoutLabPreview); } TEST(XCUIShellChromeStateTest, ViewMenuDescriptorCheckedStateTracksShellStateChanges) { @@ -254,9 +244,9 @@ TEST(XCUIShellChromeStateTest, ViewMenuDescriptorCheckedStateTracksShellStateCha const auto menu = state.BuildViewMenuDescriptor(); EXPECT_FALSE(menu.items[0].command.checked); + EXPECT_FALSE(menu.items[6].command.checked); EXPECT_FALSE(menu.items[7].command.checked); - EXPECT_FALSE(menu.items[8].command.checked); - EXPECT_TRUE(menu.items[9].command.checked); + EXPECT_TRUE(menu.items[8].command.checked); } TEST(XCUIShellChromeStateTest, PanelCommandIdHelpersMatchCurrentShellCommands) { @@ -275,9 +265,6 @@ TEST(XCUIShellChromeStateTest, PanelCommandIdHelpersMatchCurrentShellCommands) { } TEST(XCUIShellChromeStateTest, ViewToggleCommandIdHelpersMatchCurrentShellCommands) { - EXPECT_EQ( - XCUIShellChromeState::GetViewToggleCommandId(XCUIShellViewToggleId::LegacyHostDemoWindow), - XCUIShellChromeCommandIds::ToggleLegacyHostDemoWindow); EXPECT_EQ( XCUIShellChromeState::GetViewToggleCommandId(XCUIShellViewToggleId::NativeBackdrop), XCUIShellChromeCommandIds::ToggleNativeBackdrop); diff --git a/tests/Rendering/integration/RenderingIntegrationImageAssert.h b/tests/Rendering/integration/RenderingIntegrationImageAssert.h new file mode 100644 index 00000000..0dbac2f8 --- /dev/null +++ b/tests/Rendering/integration/RenderingIntegrationImageAssert.h @@ -0,0 +1,176 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +namespace RenderingIntegrationTestUtils { + +struct PpmImage { + uint32_t width = 0; + uint32_t height = 0; + std::vector rgb; + + std::array GetPixel(uint32_t x, uint32_t y) const { + const size_t index = (static_cast(y) * width + x) * 3u; + return { rgb[index + 0], rgb[index + 1], rgb[index + 2] }; + } +}; + +inline std::filesystem::path GetExecutableDirectory() { + char exePath[MAX_PATH] = {}; + const DWORD length = GetModuleFileNameA(nullptr, exePath, MAX_PATH); + if (length == 0 || length >= MAX_PATH) { + return std::filesystem::current_path(); + } + + return std::filesystem::path(exePath).parent_path(); +} + +inline std::filesystem::path ResolveRuntimePath(const char* path) { + std::filesystem::path resolved(path); + if (resolved.is_absolute()) { + return resolved; + } + + return GetExecutableDirectory() / resolved; +} + +inline std::string ReadNextPpmToken(std::istream& stream) { + std::string token; + while (stream >> token) { + if (!token.empty() && token[0] == '#') { + std::string ignored; + std::getline(stream, ignored); + continue; + } + + return token; + } + + return {}; +} + +inline PpmImage LoadPpmImage(const char* path) { + const std::filesystem::path resolvedPath = ResolveRuntimePath(path); + std::ifstream file(resolvedPath, std::ios::binary); + EXPECT_TRUE(file.is_open()) << resolvedPath.string(); + + PpmImage image; + if (!file.is_open()) { + return image; + } + + const std::string magic = ReadNextPpmToken(file); + EXPECT_EQ(magic, "P6"); + if (magic != "P6") { + return image; + } + + const std::string widthToken = ReadNextPpmToken(file); + const std::string heightToken = ReadNextPpmToken(file); + const std::string maxValueToken = ReadNextPpmToken(file); + EXPECT_FALSE(widthToken.empty()); + EXPECT_FALSE(heightToken.empty()); + EXPECT_FALSE(maxValueToken.empty()); + if (widthToken.empty() || heightToken.empty() || maxValueToken.empty()) { + return image; + } + + image.width = static_cast(std::stoul(widthToken)); + image.height = static_cast(std::stoul(heightToken)); + EXPECT_EQ(std::stoul(maxValueToken), 255u); + + file.get(); + image.rgb.resize(static_cast(image.width) * image.height * 3u); + file.read(reinterpret_cast(image.rgb.data()), static_cast(image.rgb.size())); + EXPECT_EQ(file.gcount(), static_cast(image.rgb.size())); + return image; +} + +inline int PixelLuminance(const std::array& pixel) { + return static_cast(pixel[0]) + static_cast(pixel[1]) + static_cast(pixel[2]); +} + +inline void AssertPixelInBounds(const PpmImage& image, uint32_t x, uint32_t y, const char* label) { + ASSERT_LT(x, image.width) << label; + ASSERT_LT(y, image.height) << label; +} + +inline void ExpectPixelNear( + const PpmImage& image, + uint32_t x, + uint32_t y, + const std::array& expected, + uint8_t tolerance, + const char* label) { + AssertPixelInBounds(image, x, y, label); + + const std::array actual = image.GetPixel(x, y); + for (size_t channel = 0; channel < actual.size(); ++channel) { + const int delta = std::abs(static_cast(actual[channel]) - static_cast(expected[channel])); + EXPECT_LE(delta, static_cast(tolerance)) + << label << " @ (" << x << ", " << y << ") channel=" << channel; + } +} + +inline void ExpectPixelLuminanceAtLeast( + const PpmImage& image, + uint32_t x, + uint32_t y, + int minLuminance, + const char* label) { + AssertPixelInBounds(image, x, y, label); + + const std::array actual = image.GetPixel(x, y); + EXPECT_GE(PixelLuminance(actual), minLuminance) << label << " @ (" << x << ", " << y << ")"; +} + +inline void ExpectPixelLuminanceAtMost( + const PpmImage& image, + uint32_t x, + uint32_t y, + int maxLuminance, + const char* label) { + AssertPixelInBounds(image, x, y, label); + + const std::array actual = image.GetPixel(x, y); + EXPECT_LE(PixelLuminance(actual), maxLuminance) << label << " @ (" << x << ", " << y << ")"; +} + +inline void ExpectPixelChannelDominates( + const PpmImage& image, + uint32_t x, + uint32_t y, + size_t dominantChannel, + int minMargin, + const char* label) { + AssertPixelInBounds(image, x, y, label); + ASSERT_LT(dominantChannel, static_cast(3)) << label; + + const std::array actual = image.GetPixel(x, y); + int maxOtherChannel = 0; + for (size_t channel = 0; channel < actual.size(); ++channel) { + if (channel == dominantChannel) { + continue; + } + + maxOtherChannel = std::max(maxOtherChannel, static_cast(actual[channel])); + } + + EXPECT_GE(static_cast(actual[dominantChannel]) - maxOtherChannel, minMargin) + << label << " @ (" << x << ", " << y << ")"; +} + +} // namespace RenderingIntegrationTestUtils diff --git a/tests/Rendering/integration/multi_light_scene/main.cpp b/tests/Rendering/integration/multi_light_scene/main.cpp index 187ffe8b..5f3cbbe5 100644 --- a/tests/Rendering/integration/multi_light_scene/main.cpp +++ b/tests/Rendering/integration/multi_light_scene/main.cpp @@ -3,6 +3,7 @@ #include +#include "../RenderingIntegrationImageAssert.h" #include "../RenderingIntegrationMain.h" #include @@ -45,6 +46,18 @@ constexpr const char* kVulkanScreenshot = "multi_light_scene_vulkan.ppm"; constexpr uint32_t kFrameWidth = 1280; constexpr uint32_t kFrameHeight = 720; +void ExpectMultiLightKeyPixels(const RenderingIntegrationTestUtils::PpmImage& image) { + using namespace RenderingIntegrationTestUtils; + + ExpectPixelLuminanceAtMost(image, 320, 320, 24, "background remains dark"); + ExpectPixelLuminanceAtLeast(image, 625, 285, 540, "left point-light highlight"); + ExpectPixelChannelDominates(image, 625, 285, 0, 80, "left point-light highlight stays warm"); + ExpectPixelLuminanceAtLeast(image, 640, 315, 470, "center cube receives main light"); + ExpectPixelChannelDominates(image, 640, 315, 0, 40, "center cube keeps warm directional tint"); + ExpectPixelLuminanceAtLeast(image, 758, 311, 300, "right cube receives cool fill"); + ExpectPixelChannelDominates(image, 758, 311, 2, 20, "right cube keeps cool fill tint"); +} + void AppendQuadFace( std::vector& vertices, std::vector& indices, @@ -459,6 +472,8 @@ TEST_P(MultiLightSceneTest, RenderMultiLightScene) { if (frameCount >= targetFrameCount) { commandQueue->WaitForIdle(); ASSERT_TRUE(TakeScreenshot(screenshotFilename)); + const auto screenshotImage = RenderingIntegrationTestUtils::LoadPpmImage(screenshotFilename); + ExpectMultiLightKeyPixels(screenshotImage); ASSERT_TRUE(CompareWithGoldenTemplate(screenshotFilename, "GT.ppm", static_cast(comparisonThreshold))); break; } diff --git a/tests/Rendering/integration/spot_light_scene/main.cpp b/tests/Rendering/integration/spot_light_scene/main.cpp index a5cee3bb..ff1b261f 100644 --- a/tests/Rendering/integration/spot_light_scene/main.cpp +++ b/tests/Rendering/integration/spot_light_scene/main.cpp @@ -3,6 +3,7 @@ #include +#include "../RenderingIntegrationImageAssert.h" #include "../RenderingIntegrationMain.h" #include @@ -45,6 +46,20 @@ constexpr const char* kVulkanScreenshot = "spot_light_scene_vulkan.ppm"; constexpr uint32_t kFrameWidth = 1280; constexpr uint32_t kFrameHeight = 720; +void ExpectSpotLightKeyPixels(const RenderingIntegrationTestUtils::PpmImage& image) { + using namespace RenderingIntegrationTestUtils; + + ExpectPixelLuminanceAtMost(image, 320, 320, 18, "background remains unlit"); + ExpectPixelLuminanceAtLeast(image, 640, 320, 700, "spot hot area stays bright"); + ExpectPixelNear(image, 640, 320, { 255, 255, 203 }, 2, "spot hot area color"); + ExpectPixelLuminanceAtLeast(image, 676, 289, 560, "upper cone receives spotlight"); + ExpectPixelChannelDominates(image, 676, 289, 0, 40, "upper cone keeps warm tint"); + ExpectPixelLuminanceAtLeast(image, 700, 376, 540, "ground inside cone stays lit"); + ExpectPixelChannelDominates(image, 700, 376, 0, 30, "ground inside cone stays warm"); + ExpectPixelLuminanceAtMost(image, 760, 320, 210, "edge cube outside cone stays dark"); + ExpectPixelLuminanceAtMost(image, 640, 406, 160, "ground outside cone stays dark"); +} + void AppendQuadFace( std::vector& vertices, std::vector& indices, @@ -420,6 +435,8 @@ TEST_P(SpotLightSceneTest, RenderSpotLightScene) { if (frameCount >= targetFrameCount) { commandQueue->WaitForIdle(); ASSERT_TRUE(TakeScreenshot(screenshotFilename)); + const auto screenshotImage = RenderingIntegrationTestUtils::LoadPpmImage(screenshotFilename); + ExpectSpotLightKeyPixels(screenshotImage); ASSERT_TRUE(CompareWithGoldenTemplate(screenshotFilename, "GT.ppm", static_cast(comparisonThreshold))); break; }