416 lines
14 KiB
C++
416 lines
14 KiB
C++
#include "Application.h"
|
|
|
|
#include "EditorCompositionRoot.h"
|
|
#include "EditorResources.h"
|
|
#include "Diagnostics/Win32CrashTrace.h"
|
|
#include "Resources/Win32EditorResourceService.h"
|
|
#include "Product/Support/EnvironmentFlags.h"
|
|
|
|
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
|
#include <XCEngine/Debug/Logger.h>
|
|
|
|
#include <shellscalingapi.h>
|
|
#include <sstream>
|
|
#include <utility>
|
|
|
|
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<SetProcessDpiAwarenessContextFn>(
|
|
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<SetProcessDpiAwarenessFn>(
|
|
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<SetProcessDPIAwareFn>(GetProcAddress(user32, "SetProcessDPIAware"));
|
|
if (setProcessDPIAware != nullptr) {
|
|
setProcessDPIAware();
|
|
}
|
|
}
|
|
}
|
|
|
|
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<EnableNonClientDpiScalingFn>(
|
|
GetProcAddress(user32, "EnableNonClientDpiScaling"));
|
|
if (enableNonClientDpiScaling != nullptr) {
|
|
enableNonClientDpiScaling(hwnd);
|
|
}
|
|
}
|
|
|
|
Application* GetApplicationFromWindowUserData(HWND hwnd) {
|
|
return reinterpret_cast<Application*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Application::Application() = default;
|
|
|
|
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;
|
|
EnableDpiAwareness();
|
|
|
|
auto resourceService = std::make_unique<Host::Win32EditorResourceService>(m_hInstance);
|
|
const App::EditorRuntimePaths runtimePaths =
|
|
ResolveRuntimePaths(resourceService->GetExecutableDirectory());
|
|
const std::filesystem::path logRoot = resourceService->GetExecutableDirectory() / "logs";
|
|
InitializeUIEditorRuntimeTrace(logRoot);
|
|
auto runtimeTraceLogSink = std::make_unique<UIEditorRuntimeTraceLogSink>();
|
|
m_runtimeTraceLogSink = runtimeTraceLogSink.get();
|
|
::XCEngine::Debug::Logger::Get().AddSink(std::move(runtimeTraceLogSink));
|
|
SetUnhandledExceptionFilter(&Application::HandleUnhandledException);
|
|
AppendUIEditorRuntimeTrace("app", "initialize begin");
|
|
|
|
if (!RegisterWindowClass()) {
|
|
return false;
|
|
}
|
|
|
|
m_compositionRoot = std::make_unique<App::EditorCompositionRoot>();
|
|
App::EditorCompositionRootInitializeParams compositionParams = {};
|
|
compositionParams.hInstance = m_hInstance;
|
|
compositionParams.showCommand = nCmdShow;
|
|
compositionParams.windowClassName = kWindowClassName;
|
|
compositionParams.windowStyle = kBorderlessWindowStyle;
|
|
compositionParams.primaryWindowTitle = kWindowTitle;
|
|
compositionParams.windowUserData = this;
|
|
compositionParams.runtimePaths = runtimePaths;
|
|
compositionParams.resourceService = std::move(resourceService);
|
|
if (!m_compositionRoot->Initialize(std::move(compositionParams))) {
|
|
AppendUIEditorRuntimeTrace(
|
|
"app",
|
|
"composition root initialization failed: " +
|
|
m_compositionRoot->GetValidationMessage());
|
|
return false;
|
|
}
|
|
|
|
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 = {};
|
|
|
|
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_compositionRoot != nullptr) {
|
|
m_compositionRoot->Shutdown();
|
|
m_compositionRoot.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;
|
|
}
|
|
|
|
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<int>(message.wParam);
|
|
}
|
|
|
|
TranslateMessage(&message);
|
|
DispatchMessageW(&message);
|
|
++processedMessageCount;
|
|
}
|
|
|
|
if (m_compositionRoot == nullptr) {
|
|
break;
|
|
}
|
|
|
|
if (!m_compositionRoot->TickFrame()) {
|
|
break;
|
|
}
|
|
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_compositionRoot->RequestPrimaryWindowClose()) {
|
|
PostQuitMessage(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Shutdown();
|
|
return 0;
|
|
}
|
|
|
|
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<HICON>(
|
|
LoadImageW(
|
|
m_hInstance,
|
|
MAKEINTRESOURCEW(IDI_APP_ICON),
|
|
IMAGE_ICON,
|
|
0,
|
|
0,
|
|
LR_DEFAULTSIZE));
|
|
windowClass.hIconSm = static_cast<HICON>(
|
|
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<const CREATESTRUCTW*>(lParam);
|
|
Application* application =
|
|
createStruct != nullptr
|
|
? reinterpret_cast<Application*>(createStruct->lpCreateParams)
|
|
: nullptr;
|
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(application));
|
|
if (application != nullptr && application->m_compositionRoot != nullptr) {
|
|
application->m_compositionRoot->HandlePendingNativeWindowCreated(hwnd);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
Application* application = GetApplicationFromWindowUserData(hwnd);
|
|
LRESULT dispatcherResult = 0;
|
|
if (application != nullptr &&
|
|
application->m_compositionRoot != nullptr &&
|
|
application->m_compositionRoot->TryDispatchWindowMessage(
|
|
hwnd,
|
|
message,
|
|
wParam,
|
|
lParam,
|
|
dispatcherResult)) {
|
|
return dispatcherResult;
|
|
}
|
|
|
|
return DefWindowProcW(hwnd, message, wParam, lParam);
|
|
}
|
|
|
|
} // namespace XCEngine::UI::Editor
|