#include "Application.h" #include "EditorResources.h" #include "SystemInteractionService.h" #include "EditorContext.h" #include "EditorShellRuntime.h" #include "Engine/EngineEditorServices.h" #include "EditorUtilityWindowRegistry.h" #include "EditorWorkspacePanelRegistry.h" #include "EditorWindowManager.h" #include "Assets/EditorIconServiceFactory.h" #include "Diagnostics/Win32CrashTrace.h" #include "Viewport/EditorViewportRuntimeServicesFactory.h" #include "System/Win32SystemInteractionHost.h" #include "Resources/Win32EditorResourceService.h" #include "Windowing/EditorWindow.h" #include "Windowing/EditorWindowHostConfig.h" #include "Windowing/EditorWindowHostRuntime.h" #include "Windowing/EditorWindowMessageDispatcher.h" #include "D3D12EditorWindowRenderRuntime.h" #include "EnvironmentFlags.h" #include #include #include #include #include #include #include namespace XCEngine::UI::Editor { namespace { constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost"; constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor"; constexpr DWORD kBorderlessWindowStyle = WS_POPUP | WS_THICKFRAME; constexpr int kDefaultSmokeTestDurationSeconds = 12; class UIEditorRuntimeTraceLogSink final : public ::XCEngine::Debug::ILogSink { public: void Log(const ::XCEngine::Debug::LogEntry& entry) override { if (entry.category != ::XCEngine::Debug::LogCategory::Rendering || entry.level < ::XCEngine::Debug::LogLevel::Warning) { return; } std::ostringstream stream = {}; stream << ::XCEngine::Debug::LogLevelToString(entry.level) << ' ' << entry.message.CStr(); AppendUIEditorRuntimeTrace("engine", stream.str()); } void Flush() override {} }; bool HasEditorWorkspaceMarkers(const std::filesystem::path& root) { return std::filesystem::exists(root / "CMakeLists.txt") && std::filesystem::exists(root / "editor" / "resources") && std::filesystem::exists(root / "project"); } std::filesystem::path NormalizePath(const std::filesystem::path& path) { std::error_code errorCode = {}; std::filesystem::path normalized = std::filesystem::weakly_canonical(path, errorCode); return errorCode ? path.lexically_normal() : normalized.lexically_normal(); } App::EditorRuntimePaths BuildEditorRuntimePaths( const std::filesystem::path& workspaceRoot, const std::filesystem::path& executableDirectory) { App::EditorRuntimePaths paths = {}; paths.workspaceRoot = NormalizePath(workspaceRoot); paths.executableRoot = NormalizePath(executableDirectory); const std::filesystem::path sourceTreeResourceRoot = paths.workspaceRoot / "editor" / "resources"; const std::filesystem::path packagedResourceRoot = paths.executableRoot / "resources"; paths.resourceRoot = std::filesystem::exists(sourceTreeResourceRoot) ? NormalizePath(sourceTreeResourceRoot) : NormalizePath(packagedResourceRoot); const std::filesystem::path sourceTreeProjectRoot = paths.workspaceRoot / "project"; const std::filesystem::path packagedProjectRoot = paths.executableRoot / "project"; paths.projectRoot = std::filesystem::exists(sourceTreeProjectRoot) ? NormalizePath(sourceTreeProjectRoot) : NormalizePath(packagedProjectRoot); paths.captureRoot = NormalizePath(paths.executableRoot / "captures"); return paths; } void EnableDpiAwareness() { const HMODULE user32 = GetModuleHandleW(L"user32.dll"); if (user32 != nullptr) { using SetProcessDpiAwarenessContextFn = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT); const auto setProcessDpiAwarenessContext = reinterpret_cast( GetProcAddress(user32, "SetProcessDpiAwarenessContext")); if (setProcessDpiAwarenessContext != nullptr) { if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) { return; } if (GetLastError() == ERROR_ACCESS_DENIED) { return; } if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) { return; } if (GetLastError() == ERROR_ACCESS_DENIED) { return; } } } const HMODULE shcore = LoadLibraryW(L"shcore.dll"); if (shcore != nullptr) { using SetProcessDpiAwarenessFn = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS); const auto setProcessDpiAwareness = reinterpret_cast(GetProcAddress(shcore, "SetProcessDpiAwareness")); if (setProcessDpiAwareness != nullptr) { const HRESULT hr = setProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); FreeLibrary(shcore); if (SUCCEEDED(hr) || hr == E_ACCESSDENIED) { return; } } else { FreeLibrary(shcore); } } if (user32 != nullptr) { using SetProcessDPIAwareFn = BOOL(WINAPI*)(); const auto setProcessDPIAware = reinterpret_cast(GetProcAddress(user32, "SetProcessDPIAware")); if (setProcessDPIAware != nullptr) { setProcessDPIAware(); } } } UIEditorWindowWorkspaceState BuildPrimaryWindowState( std::string_view windowId, const UIEditorWorkspaceController& workspaceController) { UIEditorWindowWorkspaceState windowState = {}; windowState.windowId = std::string(windowId); windowState.workspace = workspaceController.GetWorkspace(); windowState.session = workspaceController.GetSession(); return windowState; } } // namespace Application::Application() : m_editorContext(std::make_unique()) {} Application::~Application() = default; int RunXCEditor(HINSTANCE hInstance, int nCmdShow) { Application application; return application.Run(hInstance, nCmdShow); } } // namespace XCEngine::UI::Editor namespace XCEngine::UI::Editor { bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_hInstance = hInstance; m_resourceService = std::make_unique(m_hInstance); m_runtimePaths = ResolveRuntimePaths(m_resourceService->GetExecutableDirectory()); m_engineComposition = App::CreateEngineEditorComposition(); EnableDpiAwareness(); const std::filesystem::path logRoot = m_resourceService->GetExecutableDirectory() / "logs"; InitializeUIEditorRuntimeTrace(logRoot); auto runtimeTraceLogSink = std::make_unique(); m_runtimeTraceLogSink = runtimeTraceLogSink.get(); ::XCEngine::Debug::Logger::Get().AddSink(std::move(runtimeTraceLogSink)); SetUnhandledExceptionFilter(&Application::HandleUnhandledException); AppendUIEditorRuntimeTrace("app", "initialize begin"); if (m_engineComposition == nullptr) { AppendUIEditorRuntimeTrace("app", "engine services initialization failed"); return false; } if (!m_editorContext->Initialize( m_runtimePaths, m_engineComposition->GetSceneBackendFactory())) { AppendUIEditorRuntimeTrace( "app", "editor context initialization failed: " + m_editorContext->GetValidationMessage()); return false; } if (!RegisterWindowClass()) { return false; } m_systemInteractionHost = std::make_unique(); m_editorContext->SetSystemInteractionHost(m_systemInteractionHost.get()); m_windowSystem = std::make_unique( m_editorContext->GetShellAsset().panelRegistry); App::EditorWindowHostConfig hostConfig = {}; hostConfig.hInstance = m_hInstance; hostConfig.windowClassName = kWindowClassName; hostConfig.windowStyle = kBorderlessWindowStyle; hostConfig.primaryWindowTitle = kWindowTitle; hostConfig.windowUserData = this; m_windowHostRuntime = std::make_unique( hostConfig, m_runtimePaths); m_renderRuntimeFactory = std::make_unique(); App::EditorWorkspaceShellRuntimeFactory workspaceShellRuntimeFactory = [this]() { return App::CreateEditorWorkspaceShellRuntime( App::CreateEditorWorkspacePanelRuntimeSet( m_editorContext->GetSession(), m_editorContext->GetCommandFocusService(), m_editorContext->GetProjectRuntime(), m_editorContext->GetSceneRuntime(), m_editorContext->GetColorPickerToolState(), m_systemInteractionHost.get(), [this](App::EditorUtilityWindowKind kind) { m_editorContext->RequestOpenUtilityWindow(kind); }, [this](const std::filesystem::path& scenePath) { return m_editorContext->RequestOpenSceneAsset(scenePath); }), App::CreateEditorIconService(), App::CreateEditorViewportRuntimeServices()); }; m_windowManager = std::make_unique( *m_editorContext, m_engineComposition->GetSceneViewportBridge(), m_engineComposition->GetGameViewportBridge(), m_engineComposition->GetShaderProvider(), *m_windowSystem, *m_renderRuntimeFactory, *m_resourceService, *m_windowHostRuntime, std::move(workspaceShellRuntimeFactory), [this](App::EditorUtilityWindowKind kind) { return App::CreateEditorUtilityWindowPanel( kind, m_editorContext->GetColorPickerToolState(), m_editorContext->GetSceneRuntime()); }); m_editorContext->SetExitRequestHandler([this]() { if (m_windowManager == nullptr) { return; } m_windowManager->RequestPrimaryWindowClose(); }); m_editorContext->SetReadyStatus(); App::EditorWindowCreateParams createParams = {}; createParams.windowId = "main"; createParams.title = kWindowTitle; createParams.category = App::EditorWindowCategory::Workspace; createParams.showCommand = nCmdShow; createParams.primary = true; createParams.autoCaptureOnStartup = App::IsEnvironmentFlagEnabled("XCUI_AUTO_CAPTURE_ON_STARTUP"); m_smokeTestEnabled = App::IsEnvironmentFlagEnabled("XCUIEDITOR_SMOKE_TEST"); m_smokeTestCloseRequested = false; m_smokeTestRenderedFrameCount = 0; m_smokeTestFrameLimit = m_smokeTestEnabled ? App::TryGetEnvironmentInt("XCUIEDITOR_SMOKE_TEST_FRAME_LIMIT").value_or(0) : 0; if (m_smokeTestFrameLimit < 0) { m_smokeTestFrameLimit = 0; } int smokeTestDurationSeconds = m_smokeTestEnabled ? App::TryGetEnvironmentInt("XCUIEDITOR_SMOKE_TEST_DURATION_SECONDS") .value_or(kDefaultSmokeTestDurationSeconds) : 0; if (smokeTestDurationSeconds < 0) { smokeTestDurationSeconds = 0; } m_smokeTestDuration = std::chrono::seconds(smokeTestDurationSeconds); m_smokeTestStartTime = {}; UIEditorWorkspaceController primaryWorkspaceController = m_editorContext->BuildWorkspaceController(); const UIEditorWindowWorkspaceState primaryWindowState = BuildPrimaryWindowState(createParams.windowId, primaryWorkspaceController); std::string windowSystemError = {}; if (!m_windowSystem->BootstrapPrimaryWindow( createParams.windowId, primaryWindowState, windowSystemError)) { AppendUIEditorRuntimeTrace( "app", "window system bootstrap failed: " + windowSystemError); return false; } if (m_windowManager->CreateWorkspaceWindow( primaryWindowState, createParams) == nullptr) { AppendUIEditorRuntimeTrace("app", "primary window creation failed"); return false; } AppendUIEditorRuntimeTrace("app", "initialize end"); if (m_smokeTestEnabled) { m_smokeTestStartTime = std::chrono::steady_clock::now(); AppendUIEditorRuntimeTrace( "smoke", "enabled durationSeconds=" + std::to_string(smokeTestDurationSeconds) + " frameLimit=" + std::to_string(m_smokeTestFrameLimit)); } return true; } void Application::Shutdown() { AppendUIEditorRuntimeTrace("app", "shutdown begin"); if (m_windowManager != nullptr) { m_windowManager->Shutdown(); m_windowManager.reset(); } m_renderRuntimeFactory.reset(); m_windowHostRuntime.reset(); m_windowSystem.reset(); m_resourceService.reset(); if (m_editorContext != nullptr) { m_editorContext->SetSystemInteractionHost(nullptr); } m_systemInteractionHost.reset(); if (m_engineComposition != nullptr) { m_engineComposition->GetLifecycle().Shutdown(); m_engineComposition.reset(); } if (m_windowClassAtom != 0 && m_hInstance != nullptr) { UnregisterClassW(kWindowClassName, m_hInstance); m_windowClassAtom = 0; } m_smokeTestStartTime = {}; m_smokeTestDuration = std::chrono::milliseconds::zero(); m_smokeTestFrameLimit = 0; m_smokeTestRenderedFrameCount = 0; m_smokeTestEnabled = false; m_smokeTestCloseRequested = false; AppendUIEditorRuntimeTrace("app", "shutdown end"); if (m_runtimeTraceLogSink != nullptr) { ::XCEngine::Debug::Logger::Get().RemoveSink(m_runtimeTraceLogSink); m_runtimeTraceLogSink = nullptr; } ShutdownUIEditorRuntimeTrace(); } App::EditorRuntimePaths Application::ResolveRuntimePaths( const std::filesystem::path& executableDirectory) { std::filesystem::path current = NormalizePath(executableDirectory); while (!current.empty()) { if (HasEditorWorkspaceMarkers(current)) { return BuildEditorRuntimePaths(current, executableDirectory); } const std::filesystem::path parent = current.parent_path(); if (parent == current) { break; } current = parent; } return BuildEditorRuntimePaths(executableDirectory, executableDirectory); } LONG WINAPI Application::HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo) { App::AppendWin32UnhandledExceptionCrashTrace(exceptionInfo); return EXCEPTION_EXECUTE_HANDLER; } } // namespace XCEngine::UI::Editor namespace XCEngine::UI::Editor { int Application::Run(HINSTANCE hInstance, int nCmdShow) { if (!Initialize(hInstance, nCmdShow)) { Shutdown(); return 1; } constexpr int kMaxMessagesPerTick = 64; MSG message = {}; while (true) { int processedMessageCount = 0; while (processedMessageCount < kMaxMessagesPerTick && PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { if (message.message == WM_QUIT) { Shutdown(); return static_cast(message.wParam); } TranslateMessage(&message); DispatchMessageW(&message); ++processedMessageCount; } if (m_windowManager != nullptr) { if (m_engineComposition != nullptr) { m_engineComposition->GetLifecycle().UpdateAsyncLoads(); } m_windowManager->DestroyClosedWindows(); if (!m_windowManager->HasWindows()) { break; } m_windowManager->RenderAllWindows(); if (m_smokeTestEnabled && !m_smokeTestCloseRequested) { ++m_smokeTestRenderedFrameCount; const bool reachedFrameLimit = m_smokeTestFrameLimit > 0 && m_smokeTestRenderedFrameCount >= m_smokeTestFrameLimit; const bool reachedDuration = m_smokeTestDuration.count() > 0 && m_smokeTestStartTime != std::chrono::steady_clock::time_point{} && (std::chrono::steady_clock::now() - m_smokeTestStartTime) >= m_smokeTestDuration; if (reachedFrameLimit || reachedDuration) { AppendUIEditorRuntimeTrace( "smoke", "auto-exit requested after duration/frame limit"); m_smokeTestCloseRequested = true; if (!m_windowManager->RequestPrimaryWindowClose()) { PostQuitMessage(0); } } } } else { break; } } Shutdown(); return 0; } } // namespace XCEngine::UI::Editor namespace XCEngine::UI::Editor { namespace { void TryEnableNonClientDpiScaling(HWND hwnd) { if (hwnd == nullptr) { return; } const HMODULE user32 = GetModuleHandleW(L"user32.dll"); if (user32 == nullptr) { return; } using EnableNonClientDpiScalingFn = BOOL(WINAPI*)(HWND); const auto enableNonClientDpiScaling = reinterpret_cast( GetProcAddress(user32, "EnableNonClientDpiScaling")); if (enableNonClientDpiScaling != nullptr) { enableNonClientDpiScaling(hwnd); } } Application* GetApplicationFromWindowUserData(HWND hwnd) { return reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); } } // namespace bool Application::RegisterWindowClass() { WNDCLASSEXW windowClass = {}; windowClass.cbSize = sizeof(windowClass); windowClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; windowClass.lpfnWndProc = &Application::WndProc; windowClass.cbClsExtra = 0; windowClass.cbWndExtra = 0; windowClass.hInstance = m_hInstance; windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); windowClass.hbrBackground = nullptr; windowClass.lpszMenuName = nullptr; windowClass.hIcon = static_cast( LoadImageW( m_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE)); windowClass.hIconSm = static_cast( LoadImageW( m_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON_SMALL), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR)); windowClass.lpszClassName = kWindowClassName; m_windowClassAtom = RegisterClassExW(&windowClass); if (m_windowClassAtom == 0) { AppendUIEditorRuntimeTrace("app", "window class registration failed"); return false; } return true; } LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message == WM_NCCREATE) { TryEnableNonClientDpiScaling(hwnd); const auto* createStruct = reinterpret_cast(lParam); Application* application = createStruct != nullptr ? reinterpret_cast(createStruct->lpCreateParams) : nullptr; SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(application)); if (application != nullptr && application->m_windowHostRuntime != nullptr) { application->m_windowHostRuntime->HandlePendingNativeWindowCreated(hwnd); } return TRUE; } Application* application = GetApplicationFromWindowUserData(hwnd); LRESULT dispatcherResult = 0; if (application != nullptr && application->m_windowHostRuntime != nullptr && application->m_windowManager != nullptr) { if (App::EditorWindow* const window = application->m_windowHostRuntime->FindWindow(hwnd); window != nullptr && App::EditorWindowMessageDispatcher::TryDispatch( hwnd, *application->m_windowManager, *window, message, wParam, lParam, dispatcherResult)) { return dispatcherResult; } } return DefWindowProcW(hwnd, message, wParam, lParam); } } // namespace XCEngine::UI::Editor