rexglue-skate3/src/ui/rex_app.cpp
mchughalex b845372efa
Some checks failed
Build rexglue-sdk linux-arm64 / build (push) Failing after 46s
Build rexglue-sdk linux-amd64 / build (push) Failing after 2s
Build rexglue-sdk win-amd64 / build (push) Failing after 2s
Lint & Format Check / Format Check (push) Has been cancelled
add product version metadata
2026-06-04 19:56:26 +02:00

534 lines
16 KiB
C++

/**
* @file ui/rex_app.cpp
* @brief ReXApp implementation — compiled as part of the consumer executable
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#include <rex/rex_app.h>
#include <rex/chrono/clock.h>
#include <rex/cvar.h>
#include <rex/ui/flags.h>
#include <rex/kernel/crt/heap.h>
#include <rex/filesystem.h>
#include <rex/logging/sink.h>
#include <rex/logging.h>
#include <rex/platform.h>
#include <rex/ui/overlay/console_overlay.h>
#include <rex/ui/overlay/debug_overlay.h>
#include <rex/ui/overlay/settings_overlay.h>
#include <rex/graphics/graphics_system.h>
#if REX_HAS_VULKAN
#include <rex/graphics/vulkan/graphics_system.h>
#endif
#if REX_HAS_D3D12
#include <rex/graphics/d3d12/graphics_system.h>
#endif
#include <rex/audio/audio_system.h>
#include <rex/audio/sdl/sdl_audio_system.h>
#include <rex/input/input_system.h>
#include <rex/kernel/init.h>
#include <rex/system.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xthread.h>
#include <rex/ui/graphics_provider.h>
#include <rex/ui/keybinds.h>
#include <rex/version.h>
#include <fmt/format.h>
#include <imgui.h>
#include <toml++/toml.hpp>
#include <algorithm>
#include <filesystem>
#include <optional>
#include <string_view>
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<std::string> {
if (!has_config_table) {
return std::nullopt;
}
if (auto value = config_table[key].value<std::string>()) {
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::LogCaptureSink>();
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<rex::Runtime>(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<rex::input::InputSystem*>(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<rex::input::InputSystem*>(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<rex::graphics::GraphicsSystem*>(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<rex::ui::ImGuiDrawer>(
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<ui::DebugOverlayDialog>(
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<ui::ConsoleDialog>(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<ui::SettingsDialog>(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<rex::graphics::GraphicsSystem*>(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<void(PathConfig)> 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