diff --git a/editor/AGENTS.md b/editor/AGENTS.md index b2c16ef7..4248a22c 100644 --- a/editor/AGENTS.md +++ b/editor/AGENTS.md @@ -133,9 +133,19 @@ EditorWindowManager / coordinators -> create EditorWindowRuntimeController -> wrap it in app-owned EditorWindowInstance -> EditorWindowHostRuntime::CreateHostWindow(windowInstance, ...) - -> Win32 EditorWindow owns HWND/input/chrome and calls back through native-peer contract + -> Win32 EditorWindow owns HWND/input/chrome and supplies native snapshots + -> EditorWindowInstance renders from snapshots and returns native frame commands ``` +The native peer contract for frame/runtime data is data-shaped. Native surface +creation crosses through `EditorNativeWindowRuntimeSurface`; per-frame native +facts cross through `EditorNativeWindowFrameSnapshot`; frame-side native +effects cross back through `EditorNativeWindowFrameCommands`. Do not add +one-off native-peer getters for normal frame data such as HWND, render size, +DPI, pending input, cursor point, workspace bounds, title-bar mode, or cursor +application. Extend the snapshot/command structs when the frame contract needs +new data. + ## Window Authority Model `EditorWindowSystem` owns the authoritative `UIEditorWindowWorkspaceSet` through @@ -253,6 +263,15 @@ logic. - Do not let Win32 host code create workspace or utility content directly. It should receive an app-owned `EditorHostWindow`/`EditorWindowInstance` created by app windowing, not an `EditorWindowRuntimeController`. +- Keep `EditorWindowNativePeer` frame/runtime communication data-shaped: + use `EditorNativeWindowRuntimeSurface`, `EditorNativeWindowFrameSnapshot`, + `EditorNativeWindowFrameCommands`, and small metrics snapshots instead of + exposing individual HWND, DPI, size, input, cursor, or title-bar getters to + app windowing. +- Do not let Win32 host code call `EditorWindowRuntimeController` or directly + drive editor runtime frames. Native messages may request immediate frames + through `EditorWindowHostCoordinator`; app windowing owns the render/update + step. - Use direct authoritative workspace binding for ordinary in-window workspace mutations. - Use synchronization plans for operations that create, close, replace, or @@ -295,6 +314,13 @@ The native-peer ownership cut has also been made: `EditorWindowManager` owns Win32 platform code must stay free of `Windowing/Runtime/EditorWindowRuntimeController.h`. +The native frame contract cut has also been made: frame/runtime initialization +no longer crosses the boundary through granular native-peer getters such as +HWND, DPI, size, input, cursor, workspace bounds, and title-bar mode. Win32 +captures those native facts into `EditorNativeWindowRuntimeSurface` and +`EditorNativeWindowFrameSnapshot`; app windowing renders from the snapshot and +returns native side effects through `EditorNativeWindowFrameCommands`. + The renderer ownership cut has also been made: app windowing no longer owns the concrete D3D12 window render loop or Win32 surface setup. `EditorWindowRuntimeController` consumes a backend-neutral @@ -315,10 +341,11 @@ enough to expose. Do not move frame ownership back into the Win32 host. Do not take another library-boundary architecture cut just to keep carving this -area. After the native-peer ownership boundary, the next default improvement -should be boundary guardrails, such as checks that keep `editor/app/Windowing` -free of `Rendering/D3D12`, `Platform/Win32`, `windows.h`, and HWND types, and -keep `editor/app/Platform/Win32` free of `Rendering/D3D12` and +area. After the native-peer and native-frame-contract cuts, the next default +improvement should be boundary guardrails, such as checks that keep +`editor/app/Windowing` free of `Rendering/D3D12`, `Platform/Win32`, +`windows.h`, HWND types, and one-off native-peer frame getters, and keep +`editor/app/Platform/Win32` free of `Rendering/D3D12` and `Windowing/Runtime/EditorWindowRuntimeController.h`. Consider another structural source-directory cut only when there is concrete pressure, such as another native host, another render backend, or headless editor/window tests; @@ -403,6 +430,10 @@ Start with these files for editor/windowing work: native peer side for HWND/input/chrome/message work. `CreateHostWindow(...)` binds a native peer to an existing `EditorHostWindow`; it does not receive a runtime controller. +- The native frame contract cut is sealed: app windowing consumes + `EditorNativeWindowRuntimeSurface` and `EditorNativeWindowFrameSnapshot` + instead of calling granular native-peer getters for frame/runtime data, and + Win32 applies frame side effects through `EditorNativeWindowFrameCommands`. - The concrete renderer cut is sealed: app windowing consumes `Rendering::Host::EditorWindowRenderRuntime` and `Rendering::Host::EditorWindowRenderRuntimeFactory`, while the current D3D12 diff --git a/editor/app/Platform/Win32/Windowing/EditorWindow.cpp b/editor/app/Platform/Win32/Windowing/EditorWindow.cpp index a64a63e6..3255d71c 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindow.cpp +++ b/editor/app/Platform/Win32/Windowing/EditorWindow.cpp @@ -156,6 +156,81 @@ bool EditorWindow::HasLiveHostWindow() const { return hwnd != nullptr && IsWindow(hwnd); } +EditorNativeWindowMetrics EditorWindow::CaptureMetrics() const { + return EditorNativeWindowMetrics{ + .dpiScale = GetDpiScale(), + }; +} + +bool EditorWindow::CaptureRuntimeSurface( + const EditorHostWindow& window, + EditorNativeWindowRuntimeSurface& outSurface) { + (void)window; + outSurface = {}; + const HWND hwnd = m_session->GetHwnd(); + if (hwnd == nullptr || !IsWindow(hwnd)) { + return false; + } + + std::uint32_t clientWidth = 0u; + std::uint32_t clientHeight = 0u; + if (!QueryCurrentClientPixelSize(clientWidth, clientHeight)) { + clientWidth = 1u; + clientHeight = 1u; + } + + outSurface.nativeWindowHandle = hwnd; + outSurface.widthPixels = clientWidth; + outSurface.heightPixels = clientHeight; + outSurface.dpiScale = GetDpiScale(); + return true; +} + +bool EditorWindow::CaptureFrameSnapshot( + const EditorHostWindow& window, + const UIEditorShellInteractionState& shellState, + EditorNativeWindowFrameSnapshot& outSnapshot) { + outSnapshot = {}; + if (!HasLiveHostWindow()) { + return false; + } + + std::uint32_t pixelWidth = 0u; + std::uint32_t pixelHeight = 0u; + if (!ResolveRenderClientPixelSize(pixelWidth, pixelHeight)) { + return false; + } + + SyncShellCapturedPointerButtonsFromSystemState(shellState); + + const float width = PixelsToDips(static_cast(pixelWidth)); + const float height = PixelsToDips(static_cast(pixelHeight)); + const bool useDetachedTitleBarTabStrip = + ShouldUseDetachedTitleBarTabStrip(window); + + outSnapshot.widthPixels = pixelWidth; + outSnapshot.heightPixels = pixelHeight; + outSnapshot.widthDips = width; + outSnapshot.heightDips = height; + outSnapshot.dpiScale = GetDpiScale(); + outSnapshot.workspaceBounds = ResolveWorkspaceBounds(window, width, height); + outSnapshot.inputEvents = TakePendingInputEvents(); + outSnapshot.cursorScreenPoint = QueryCursorScreenPoint(); + outSnapshot.useDetachedTitleBarTabStrip = useDetachedTitleBarTabStrip; + return outSnapshot.IsValid(); +} + +void EditorWindow::ApplyFrameCommands( + const EditorHostWindow& window, + const EditorNativeWindowFrameCommands& commands) { + if (commands.applyShellRuntimePointerCapture) { + ApplyShellRuntimePointerCapture(window); + } + if (commands.applyCurrentCursor) { + ApplyCurrentCursor(); + } +} + const std::wstring& EditorWindow::GetTitle() const { return m_owner.GetTitle(); } @@ -194,10 +269,6 @@ void EditorWindow::InvalidateHostWindow() const { } } -void* EditorWindow::GetNativeWindowHandle() const { - return m_session->GetHwnd(); -} - void EditorWindow::PrepareRuntimeInitialization(EditorHostWindow& window) { (void)window; Host::RefreshBorderlessWindowDwmDecorations(m_session->GetHwnd()); diff --git a/editor/app/Platform/Win32/Windowing/EditorWindow.h b/editor/app/Platform/Win32/Windowing/EditorWindow.h index 467d3438..7f1b98ce 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindow.h +++ b/editor/app/Platform/Win32/Windowing/EditorWindow.h @@ -83,32 +83,22 @@ public: bool IsClosing() const; bool IsDestroyed() const; bool HasLiveHostWindow() const override; + EditorNativeWindowMetrics CaptureMetrics() const override; + bool CaptureRuntimeSurface( + const EditorHostWindow& window, + EditorNativeWindowRuntimeSurface& outSurface) override; + bool CaptureFrameSnapshot( + const EditorHostWindow& window, + const UIEditorShellInteractionState& shellState, + EditorNativeWindowFrameSnapshot& outSnapshot) override; + void ApplyFrameCommands( + const EditorHostWindow& window, + const EditorNativeWindowFrameCommands& commands) override; const std::wstring& GetTitle() const; std::string_view GetCachedTitleText() const; const EditorWorkspaceWindowProjection* TryGetWorkspaceProjection() const; EditorWindowDockHostBinding* TryGetDockHostBinding(); const EditorWindowDockHostBinding* TryGetDockHostBinding() const; - void* GetNativeWindowHandle() const override; - bool QueryCurrentClientPixelSize( - std::uint32_t& outWidth, - std::uint32_t& outHeight) const override; - bool ResolveRenderClientPixelSize( - std::uint32_t& outWidth, - std::uint32_t& outHeight) const override; - float GetDpiScale() const override; - float PixelsToDips(float pixels) const override; - ::XCEngine::UI::UIRect ResolveWorkspaceBounds( - const EditorHostWindow& window, - float clientWidthDips, - float clientHeightDips) const override; - bool ShouldUseDetachedTitleBarTabStrip( - const EditorHostWindow& window) const override; - std::vector<::XCEngine::UI::UIInputEvent> TakePendingInputEvents() override; - std::optional QueryCursorScreenPoint() const override; - void SyncShellCapturedPointerButtonsFromSystemState( - const UIEditorShellInteractionState& shellState) override; - void ApplyShellRuntimePointerCapture(const EditorHostWindow& window) override; - bool ApplyCurrentCursor() const override; void AppendChrome( const EditorHostWindow& window, ::XCEngine::UI::UIDrawList& drawList, @@ -150,6 +140,26 @@ private: EditorWindowFrameTransferRequests ConsumeQueuedCompletedImmediateFrameTransferRequests(); void RequestSkipNextSteadyStateFrame() override; bool ConsumeSkipNextSteadyStateFrame() override; + bool QueryCurrentClientPixelSize( + std::uint32_t& outWidth, + std::uint32_t& outHeight) const; + bool ResolveRenderClientPixelSize( + std::uint32_t& outWidth, + std::uint32_t& outHeight) const; + float GetDpiScale() const; + float PixelsToDips(float pixels) const; + ::XCEngine::UI::UIRect ResolveWorkspaceBounds( + const EditorHostWindow& window, + float clientWidthDips, + float clientHeightDips) const; + bool ShouldUseDetachedTitleBarTabStrip( + const EditorHostWindow& window) const; + std::vector<::XCEngine::UI::UIInputEvent> TakePendingInputEvents(); + std::optional QueryCursorScreenPoint() const; + void SyncShellCapturedPointerButtonsFromSystemState( + const UIEditorShellInteractionState& shellState); + void ApplyShellRuntimePointerCapture(const EditorHostWindow& window); + bool ApplyCurrentCursor() const; bool OnResize(UINT width, UINT height); void OnEnterSizeMove(); bool OnExitSizeMove(); diff --git a/editor/app/Windowing/EditorWindowInstance.cpp b/editor/app/Windowing/EditorWindowInstance.cpp index 307b31e7..8b1d59b9 100644 --- a/editor/app/Windowing/EditorWindowInstance.cpp +++ b/editor/app/Windowing/EditorWindowInstance.cpp @@ -167,7 +167,7 @@ bool EditorWindowInstance::TryResolveDockTabDragHotspot( return false; } - const float dpiScale = m_nativePeer->GetDpiScale(); + const float dpiScale = m_nativePeer->CaptureMetrics().dpiScale; outHotspot.x = static_cast(std::lround(hotspotDips.x * dpiScale)); outHotspot.y = static_cast(std::lround(hotspotDips.y * dpiScale)); return true; @@ -286,31 +286,31 @@ bool EditorWindowInstance::IsRenderReady() const { bool EditorWindowInstance::InitializeRuntime( const EditorHostWindowRuntimeInitializationParams& params) { - if (m_nativePeer == nullptr || m_nativePeer->GetNativeWindowHandle() == nullptr) { + if (m_nativePeer == nullptr) { AppendUIEditorRuntimeTrace("app", "window initialize skipped: native window is null"); return false; } m_nativePeer->PrepareRuntimeInitialization(*this); - m_runtime->SetDpiScale(m_nativePeer->GetDpiScale()); + EditorNativeWindowRuntimeSurface runtimeSurface = {}; + if (!m_nativePeer->CaptureRuntimeSurface(*this, runtimeSurface) || + !runtimeSurface.IsValid()) { + AppendUIEditorRuntimeTrace("app", "window initialize skipped: native surface is invalid"); + return false; + } + + m_runtime->SetDpiScale(runtimeSurface.dpiScale); std::ostringstream dpiTrace = {}; - dpiTrace << "initial dpiScale=" << m_nativePeer->GetDpiScale(); + dpiTrace << "initial dpiScale=" << runtimeSurface.dpiScale; AppendUIEditorRuntimeTrace("window", dpiTrace.str()); MarkInitializing(); - std::uint32_t clientWidth = 0u; - std::uint32_t clientHeight = 0u; - if (!m_nativePeer->QueryCurrentClientPixelSize(clientWidth, clientHeight)) { - clientWidth = 1u; - clientHeight = 1u; - } - const bool initialized = m_runtime->Initialize( Rendering::Host::EditorWindowRenderRuntimeSurface{ - .nativeWindowHandle = m_nativePeer->GetNativeWindowHandle(), - .widthPixels = clientWidth, - .heightPixels = clientHeight, + .nativeWindowHandle = runtimeSurface.nativeWindowHandle, + .widthPixels = runtimeSurface.widthPixels, + .heightPixels = runtimeSurface.heightPixels, }, params.repoRoot, params.captureRoot, @@ -329,34 +329,32 @@ EditorWindowFrameTransferRequests EditorWindowInstance::RenderHostFrame( return {}; } - std::uint32_t pixelWidth = 0u; - std::uint32_t pixelHeight = 0u; - if (!m_nativePeer->ResolveRenderClientPixelSize(pixelWidth, pixelHeight)) { + EditorNativeWindowFrameSnapshot frameSnapshot = {}; + if (!m_nativePeer->CaptureFrameSnapshot( + *this, + m_runtime->GetShellInteractionState(), + frameSnapshot) || + !frameSnapshot.IsValid()) { return {}; } - const float width = m_nativePeer->PixelsToDips(static_cast(pixelWidth)); - const float height = m_nativePeer->PixelsToDips(static_cast(pixelHeight)); - const UIRect workspaceBounds = - m_nativePeer->ResolveWorkspaceBounds(*this, width, height); - UIDrawData drawData = {}; UIDrawList& backgroundDrawList = drawData.EmplaceDrawList("XCEditorWindow.Surface"); backgroundDrawList.AddFilledRect( - UIRect(0.0f, 0.0f, width, height), + UIRect(0.0f, 0.0f, frameSnapshot.widthDips, frameSnapshot.heightDips), kShellSurfaceColor); EditorWindowFrameTransferRequests transferRequests = {}; if (m_runtime->IsEditorContextValid()) { transferRequests = - RenderRuntimeFrame(globalTabDragActive, workspaceBounds, drawData); + RenderRuntimeFrame(globalTabDragActive, frameSnapshot, drawData); } else { UIDrawList& invalidDrawList = drawData.EmplaceDrawList("XCEditorWindow.Invalid"); m_runtime->AppendInvalidFrame(invalidDrawList); } UIDrawList& windowChromeDrawList = drawData.EmplaceDrawList("XCEditorWindow.Chrome"); - m_nativePeer->AppendChrome(*this, windowChromeDrawList, width); + m_nativePeer->AppendChrome(*this, windowChromeDrawList, frameSnapshot.widthDips); const auto presentResult = m_runtime->Present(drawData); if (!presentResult.warning.empty()) { @@ -441,39 +439,37 @@ void EditorWindowInstance::UpdateCachedTitleText() { EditorWindowFrameTransferRequests EditorWindowInstance::RenderRuntimeFrame( bool globalTabDragActive, - const UIRect& workspaceBounds, + const EditorNativeWindowFrameSnapshot& frameSnapshot, UIDrawData& drawData) { if (m_nativePeer == nullptr) { return {}; } - m_nativePeer->SyncShellCapturedPointerButtonsFromSystemState( - m_runtime->GetShellInteractionState()); - std::vector<::XCEngine::UI::UIInputEvent> frameEvents = - m_nativePeer->TakePendingInputEvents(); m_runtime->PrepareEditorContext(); const auto frameContext = m_runtime->BeginFrame(); if (!frameContext.warning.empty()) { AppendUIEditorRuntimeTrace("viewport", frameContext.warning); } - const bool useDetachedTitleBarTabStrip = - m_nativePeer->ShouldUseDetachedTitleBarTabStrip(*this); const EditorWindowFrameTransferRequests transferRequests = m_runtime->UpdateAndAppend( - workspaceBounds, - frameEvents, - m_nativePeer->QueryCursorScreenPoint(), + frameSnapshot.workspaceBounds, + frameSnapshot.inputEvents, + frameSnapshot.cursorScreenPoint, IsPrimary(), globalTabDragActive, - useDetachedTitleBarTabStrip, + frameSnapshot.useDetachedTitleBarTabStrip, drawData); if (frameContext.canRenderViewports) { m_runtime->RenderRequestedViewports(frameContext.renderContext); } - m_nativePeer->ApplyShellRuntimePointerCapture(*this); - m_nativePeer->ApplyCurrentCursor(); + m_nativePeer->ApplyFrameCommands( + *this, + EditorNativeWindowFrameCommands{ + .applyShellRuntimePointerCapture = true, + .applyCurrentCursor = true, + }); return transferRequests; } diff --git a/editor/app/Windowing/EditorWindowInstance.h b/editor/app/Windowing/EditorWindowInstance.h index ec13c566..ca07ac65 100644 --- a/editor/app/Windowing/EditorWindowInstance.h +++ b/editor/app/Windowing/EditorWindowInstance.h @@ -100,7 +100,7 @@ private: void UpdateCachedTitleText(); EditorWindowFrameTransferRequests RenderRuntimeFrame( bool globalTabDragActive, - const ::XCEngine::UI::UIRect& workspaceBounds, + const EditorNativeWindowFrameSnapshot& frameSnapshot, ::XCEngine::UI::UIDrawData& drawData); std::string m_windowId = {}; diff --git a/editor/app/Windowing/Host/EditorWindowHostInterfaces.h b/editor/app/Windowing/Host/EditorWindowHostInterfaces.h index e08c7c5b..9d5976f3 100644 --- a/editor/app/Windowing/Host/EditorWindowHostInterfaces.h +++ b/editor/app/Windowing/Host/EditorWindowHostInterfaces.h @@ -43,6 +43,43 @@ struct EditorHostWindowRuntimeInitializationParams { bool autoCaptureOnStartup = false; }; +struct EditorNativeWindowRuntimeSurface { + void* nativeWindowHandle = nullptr; + std::uint32_t widthPixels = 0u; + std::uint32_t heightPixels = 0u; + float dpiScale = 1.0f; + + bool IsValid() const { + return nativeWindowHandle != nullptr && widthPixels > 0u && heightPixels > 0u; + } +}; + +struct EditorNativeWindowFrameSnapshot { + std::uint32_t widthPixels = 0u; + std::uint32_t heightPixels = 0u; + float widthDips = 0.0f; + float heightDips = 0.0f; + float dpiScale = 1.0f; + ::XCEngine::UI::UIRect workspaceBounds = {}; + std::vector<::XCEngine::UI::UIInputEvent> inputEvents = {}; + std::optional cursorScreenPoint = {}; + bool useDetachedTitleBarTabStrip = false; + + bool IsValid() const { + return widthPixels > 0u && heightPixels > 0u && + widthDips > 0.0f && heightDips > 0.0f; + } +}; + +struct EditorNativeWindowFrameCommands { + bool applyShellRuntimePointerCapture = false; + bool applyCurrentCursor = false; +}; + +struct EditorNativeWindowMetrics { + float dpiScale = 1.0f; +}; + class EditorWindowNativePeer; class EditorHostWindow { @@ -121,27 +158,17 @@ public: virtual ~EditorWindowNativePeer() = default; virtual bool HasLiveHostWindow() const = 0; - virtual void* GetNativeWindowHandle() const = 0; - virtual bool QueryCurrentClientPixelSize( - std::uint32_t& outWidth, - std::uint32_t& outHeight) const = 0; - virtual bool ResolveRenderClientPixelSize( - std::uint32_t& outWidth, - std::uint32_t& outHeight) const = 0; - virtual float GetDpiScale() const = 0; - virtual float PixelsToDips(float pixels) const = 0; - virtual ::XCEngine::UI::UIRect ResolveWorkspaceBounds( + virtual EditorNativeWindowMetrics CaptureMetrics() const = 0; + virtual bool CaptureRuntimeSurface( const EditorHostWindow& window, - float clientWidthDips, - float clientHeightDips) const = 0; - virtual bool ShouldUseDetachedTitleBarTabStrip( - const EditorHostWindow& window) const = 0; - virtual std::vector<::XCEngine::UI::UIInputEvent> TakePendingInputEvents() = 0; - virtual std::optional QueryCursorScreenPoint() const = 0; - virtual void SyncShellCapturedPointerButtonsFromSystemState( - const UIEditorShellInteractionState& shellState) = 0; - virtual void ApplyShellRuntimePointerCapture(const EditorHostWindow& window) = 0; - virtual bool ApplyCurrentCursor() const = 0; + EditorNativeWindowRuntimeSurface& outSurface) = 0; + virtual bool CaptureFrameSnapshot( + const EditorHostWindow& window, + const UIEditorShellInteractionState& shellState, + EditorNativeWindowFrameSnapshot& outSnapshot) = 0; + virtual void ApplyFrameCommands( + const EditorHostWindow& window, + const EditorNativeWindowFrameCommands& commands) = 0; virtual void AppendChrome( const EditorHostWindow& window, ::XCEngine::UI::UIDrawList& drawList,