/** * @file ui/rex_app.cpp * @brief ReXApp implementation — compiled as part of the consumer executable * * @copyright Copyright (c) 2026 Tom Clay * All rights reserved. * * @license BSD 3-Clause License * See LICENSE file in the project root for full license text. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #if REX_HAS_VULKAN #include #endif #if REX_HAS_D3D12 #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace rex { namespace { constexpr bool kBlockShaderStorageStartup = #if REX_PLATFORM_WIN32 true; #else false; #endif } // namespace REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, true, "UI/Advanced", "Enable the developer cvar browser on F4"); // --- ReXApp --- ReXApp::~ReXApp() = default; ReXApp::ReXApp(ui::WindowedAppContext& ctx, std::string_view name, PPCImageInfo ppc_info, std::string_view usage) : WindowedApp(ctx, name, usage), ppc_info_(ppc_info) {} std::string_view ReXApp::GetBuildTitle() const { return REXGLUE_BUILD_TITLE; } std::string_view ReXApp::GetBuildStamp() const { return REXGLUE_BUILD_STAMP; } std::string ReXApp::GetWindowTitle() const { return std::string(GetName()) + " " + std::string(GetBuildTitle()); } bool ReXApp::OnInitialize() { if (!SetupEnvironment()) return false; if (!SetupPresentation()) return false; auto paths = OnFinalizePaths(resolved_defaults_, MakeResumeCallback()); if (!paths) { // Async: consumer will invoke resume when ready. OnInitialize returns // true so the event loop keeps pumping (wizard dialogs render). return true; } if (!ConstructRuntime(*paths)) return false; LaunchModule(); return true; } bool ReXApp::SetupEnvironment() { auto exe_dir = rex::filesystem::GetExecutableFolder(); auto config_path = exe_dir / (std::string(GetName()) + ".toml"); // Load config before resolving cvar-backed paths such as game_data_root. if (std::filesystem::exists(config_path)) rex::cvar::LoadConfig(config_path); toml::table config_table; bool has_config_table = false; if (std::filesystem::exists(config_path)) { try { config_table = toml::parse_file(config_path.string()); has_config_table = true; } catch (const toml::parse_error&) { has_config_table = false; } } auto config_string = [&](std::string_view key) -> std::optional { if (!has_config_table) { return std::nullopt; } if (auto value = config_table[key].value()) { return *value; } return std::nullopt; }; std::filesystem::path game_dir; if (auto config_value = config_string("game_data_root")) { game_dir = *config_value; } std::string game_data_cvar = REXCVAR_GET(game_data_root); if (!game_data_cvar.empty()) { game_dir = game_data_cvar; } // User data: cvar override, or platform user directory std::filesystem::path user_dir; if (auto config_value = config_string("user_data_root")) { user_dir = *config_value; } std::string user_data_cvar = REXCVAR_GET(user_data_root); if (!user_data_cvar.empty()) { user_dir = user_data_cvar; } if (user_dir.empty()) { user_dir = rex::filesystem::GetUserFolder() / GetName(); } // Update data: cvar override, or empty (opt-in) std::filesystem::path update_dir; if (auto config_value = config_string("update_data_root")) { update_dir = *config_value; } std::string update_data_cvar = REXCVAR_GET(update_data_root); if (!update_data_cvar.empty()) { update_dir = update_data_cvar; } // Cache: cvar override, or user_dir/cache std::filesystem::path cache_dir; if (auto config_value = config_string("cache_path")) { cache_dir = *config_value; } std::string cache_path_cvar = REXCVAR_GET(cache_path); if (!cache_path_cvar.empty()) { cache_dir = cache_path_cvar; } if (cache_dir.empty()) { cache_dir = user_dir / "cache"; } PathConfig path_config{game_dir, user_dir, update_dir, cache_dir, config_path}; OnConfigurePaths(path_config); game_data_root_ = path_config.game_data_root; user_data_root_ = path_config.user_data_root; update_data_root_ = path_config.update_data_root; cache_root_ = path_config.cache_root; config_path_ = path_config.config_path; resolved_defaults_ = std::move(path_config); // Late-phase logging std::string log_file_cvar = REXCVAR_GET(log_file); std::string log_level_str = REXCVAR_GET(log_level); if (REXCVAR_GET(log_verbose) && log_level_str == "info") log_level_str = "trace"; auto category_levels = rex::ParseCategoryLevelsFromConfig(config_path_); auto log_config = rex::BuildLogConfig(log_file_cvar.empty() ? nullptr : log_file_cvar.c_str(), log_level_str, category_levels); if (log_file_cvar.empty()) { log_config.app_name = std::string(GetName()); log_config.log_dir = (exe_dir / "logs").string(); } rex::InitLogging(log_config); rex::RegisterLogLevelCallback(); log_sink_ = std::make_shared(); rex::AddSink(log_sink_); OnPostInitLogging(); if (std::filesystem::exists(config_path_)) REXLOG_INFO("Loaded config: {}", config_path_.filename().string()); REXLOG_INFO("{} starting", GetName()); if (!game_data_root_.empty()) { REXLOG_INFO(" Game directory: {}", game_data_root_.string()); } if (!user_data_root_.empty()) { REXLOG_INFO(" User data: {}", user_data_root_.string()); } if (!update_data_root_.empty()) { REXLOG_INFO(" Update data: {}", update_data_root_.string()); } REXLOG_INFO(" Cache root: {}", cache_root_.string()); return true; } bool ReXApp::ConstructRuntime(const PathConfig& paths) { if (paths.game_data_root.empty()) { auto msg = std::string("--game_data_root was not provided."); REXLOG_ERROR("{}", msg); rex::ShowSimpleMessageBox(rex::SimpleMessageBoxType::Error, msg); return false; } if (!std::filesystem::is_directory(paths.game_data_root)) { auto msg = fmt::format("--game_data_root does not exist: {}", paths.game_data_root.string()); REXLOG_ERROR("{}", msg); rex::ShowSimpleMessageBox(rex::SimpleMessageBoxType::Error, msg); return false; } runtime_ = std::make_unique(paths.game_data_root, paths.user_data_root, paths.update_data_root, paths.cache_root); runtime_->set_app_context(&app_context()); config_.config_path = paths.config_path; // Recompiled guest code is linked into the host executable, while the kernel // imports live in rexruntime. Keep the exe-side clock state in sync with // Runtime::Setup so mftb and KeQueryPerformanceFrequency agree. rex::chrono::Clock::set_guest_tick_frequency(50000000); rex::chrono::Clock::set_guest_system_time_base(rex::chrono::Clock::QueryHostSystemTime()); rex::chrono::Clock::set_guest_time_scalar(1.0); auto guest_tick_ratio = rex::chrono::Clock::guest_tick_ratio(); REXLOG_INFO("Host guest clock initialized: frequency={} ratio={}/{}", rex::chrono::Clock::guest_tick_frequency(), guest_tick_ratio.first, guest_tick_ratio.second); // Window and ImGui drawer already exist from SetupPresentation; publish them // to the runtime before Setup so hooks and native rendering see them. if (window_) { runtime_->set_display_window(window_.get()); } if (imgui_drawer_) { runtime_->set_imgui_drawer(imgui_drawer_.get()); } auto status = runtime_->Setup(ppc_info_, std::move(config_)); if (XFAILED(status)) { REXLOG_ERROR("Runtime setup failed: {:08X}", status); return false; } if (window_ && runtime_->input_system()) { static_cast(runtime_->input_system())->AttachWindow(window_.get()); } if (ppc_info_.register_modules) { ppc_info_.register_modules(runtime_->kernel_state()); } if (imgui_drawer_) { auto* input_sys = static_cast(runtime_->input_system()); if (input_sys) { input_sys->SetActiveCallback([this]() { if (!debug_overlay_ && !console_overlay_ && !settings_overlay_) return true; const auto& io = imgui_drawer_->GetIO(); return !io.WantCaptureMouse && !io.WantCaptureKeyboard; }); } } std::string xex_image = "game:\\default.xex"; OnLoadXexImage(xex_image); // Mirrors the game:\ / d:\ -> game_data_root mapping in Runtime::SetupVfs. { constexpr std::string_view kGameDevice = "game:\\"; constexpr std::string_view kDDevice = "d:\\"; std::string_view tail = xex_image; if (tail.starts_with(kGameDevice)) { tail.remove_prefix(kGameDevice.size()); } else if (tail.starts_with(kDDevice)) { tail.remove_prefix(kDDevice.size()); } std::string host_tail{tail}; std::replace(host_tail.begin(), host_tail.end(), '\\', '/'); auto xex_host = paths.game_data_root / host_tail; if (!std::filesystem::is_regular_file(xex_host)) { auto msg = fmt::format("Entrypoint XEX not found: {}", xex_host.string()); REXLOG_ERROR("{}", msg); rex::ShowSimpleMessageBox(rex::SimpleMessageBoxType::Error, msg); return false; } } status = runtime_->LoadXexImage(xex_image); if (XFAILED(status)) { auto msg = fmt::format("Failed to load XEX ({}): {:08X}", xex_image, status); REXLOG_ERROR("{}", msg); rex::ShowSimpleMessageBox(rex::SimpleMessageBoxType::Error, msg); return false; } OnPostLoadXexImage(); if (ppc_info_.rexcrt_heap) { if (!rex::kernel::crt::InitHeap(REXCVAR_GET(rexcrt_heap_size_mb), runtime_->memory())) { REXLOG_ERROR("Failed to initialize rexcrt heap"); return false; } } OnPostSetup(); return true; } bool ReXApp::SetupPresentation() { #if REX_HAS_D3D12 config_.graphics = REX_GRAPHICS_BACKEND(rex::graphics::d3d12::D3D12GraphicsSystem); #elif REX_HAS_VULKAN config_.graphics = REX_GRAPHICS_BACKEND(rex::graphics::vulkan::VulkanGraphicsSystem); #endif config_.audio_factory = REX_AUDIO_BACKEND(rex::audio::sdl::SDLAudioSystem); config_.input_factory = REX_INPUT_BACKEND(rex::input::CreateDefaultInputSystem); config_.kernel_init = rex::kernel::InitializeKernel; OnPreSetup(config_); if (config_.graphics) { X_STATUS status = config_.graphics->SetupPresentation(&app_context()); if (XFAILED(status)) { REXLOG_ERROR("Graphics presentation setup failed: {:08X}", status); return false; } } // Create window window_ = rex::ui::Window::Create(app_context(), GetName(), 1280, 720); if (!window_) { REXLOG_ERROR("Failed to create window"); return false; } window_->SetTitle(GetWindowTitle()); window_->AddListener(this); window_->AddInputListener(this, 0); if (REXCVAR_GET(fullscreen)) { window_->SetFullscreen(true); } window_->Open(); auto* graphics_system = static_cast(config_.graphics.get()); if (graphics_system && graphics_system->presenter()) { auto* presenter = graphics_system->presenter(); auto* provider = graphics_system->provider(); if (provider) { immediate_drawer_ = provider->CreateImmediateDrawer(); if (immediate_drawer_) { immediate_drawer_->SetPresenter(presenter); imgui_drawer_ = std::make_unique( window_.get(), 64, [this](ImFontAtlas* atlas) { OnConfigureFonts(atlas); }); imgui_drawer_->SetPresenterAndImmediateDrawer(presenter, immediate_drawer_.get()); rex::ui::RegisterBind("bind_debug_overlay", "F3", "Toggle debug overlay", [this] { if (debug_overlay_) { debug_overlay_.reset(); } else { debug_overlay_ = std::make_unique( imgui_drawer_.get(), frame_stats_provider_, GetBuildStamp()); } }); rex::ui::RegisterBind("bind_console", "Backtick", "Toggle console overlay", [this] { if (console_overlay_) { console_overlay_.reset(); } else { console_overlay_ = std::make_unique(imgui_drawer_.get(), log_sink_); } }); rex::ui::RegisterBind("bind_settings", "F4", "Toggle settings overlay", [this] { if (!REXCVAR_GET(advanced_settings_overlay_enabled)) { return; } if (settings_overlay_) { settings_overlay_.reset(); } else { settings_overlay_ = std::make_unique(imgui_drawer_.get(), config_path_); } }); OnCreateDialogs(imgui_drawer_.get()); } } window_->SetPresenter(presenter); } return true; } void ReXApp::LaunchModule() { app_context().CallInUIThreadDeferred([this]() { OnPreLaunchModule(); auto main_thread = runtime_->PrepareModuleLaunch(); if (!main_thread) { REXLOG_ERROR("Failed to launch module"); app_context().QuitFromUIThread(); return; } auto* graphics_system = static_cast(runtime_->graphics_system()); if (graphics_system && !runtime_->cache_root().empty()) { uint32_t title_id = runtime_->kernel_state()->title_id(); if (title_id != 0) { REXLOG_INFO("Initializing shader storage for title {:08X}...", title_id); graphics_system->InitializeShaderStorage(runtime_->cache_root(), title_id, kBlockShaderStorageStartup); } } OnPostLaunchModule(main_thread.get()); main_thread->Resume(); module_thread_ = std::thread([this, main_thread = std::move(main_thread)]() mutable { main_thread->Wait(0, 0, 0, nullptr); OnGuestThreadExit(main_thread.get()); REXLOG_INFO("Execution complete"); if (!shutting_down_.load(std::memory_order_acquire)) { app_context().CallInUIThread([this]() { app_context().QuitFromUIThread(); }); } }); }); } std::function ReXApp::MakeResumeCallback() { return [this](PathConfig paths) { if (shutting_down_.load(std::memory_order_acquire)) return; if (!ConstructRuntime(std::move(paths))) { app_context().QuitFromUIThread(); return; } LaunchModule(); }; } void ReXApp::OnKeyDown(ui::KeyEvent& e) { rex::ui::ProcessKeyEvent(e); } void ReXApp::OnClosing(ui::UIEvent& e) { (void)e; REXLOG_INFO("Window closing, shutting down..."); shutting_down_.store(true, std::memory_order_release); if (runtime_ && runtime_->kernel_state()) { runtime_->kernel_state()->TerminateTitle(); } app_context().QuitFromUIThread(); } void ReXApp::OnDestroy() { // Notify subclass before cleanup OnShutdown(); // Unregister overlay keybinds before destroying dialogs rex::ui::UnregisterBind("bind_debug_overlay"); rex::ui::UnregisterBind("bind_console"); rex::ui::UnregisterBind("bind_settings"); // ImGui cleanup (reverse of setup) settings_overlay_.reset(); console_overlay_.reset(); debug_overlay_.reset(); if (imgui_drawer_) { imgui_drawer_->SetPresenterAndImmediateDrawer(nullptr, nullptr); imgui_drawer_.reset(); } if (immediate_drawer_) { immediate_drawer_->SetPresenter(nullptr); immediate_drawer_.reset(); } if (runtime_) { runtime_->set_display_window(nullptr); runtime_->set_imgui_drawer(nullptr); } // Window/runtime cleanup if (window_) { window_->SetPresenter(nullptr); } if (module_thread_.joinable()) { module_thread_.join(); } if (window_) { window_->RemoveInputListener(this); window_->RemoveListener(this); } window_.reset(); runtime_.reset(); } void ReXApp::SetGuestFrameStats(ui::DebugOverlayDialog::FrameStatsProvider provider) { frame_stats_provider_ = provider; if (debug_overlay_) { debug_overlay_->SetStatsProvider(provider); } } } // namespace rex