784 lines
25 KiB
C++
784 lines
25 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/fps_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>
|
|
|
|
#if REX_PLATFORM_LINUX
|
|
#include <gnu/libc-version.h>
|
|
#include <sys/utsname.h>
|
|
#endif
|
|
|
|
#include <fmt/format.h>
|
|
#include <imgui.h>
|
|
#include <toml++/toml.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <optional>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <utility>
|
|
|
|
namespace rex {
|
|
|
|
namespace {
|
|
|
|
constexpr bool kBlockShaderStorageStartup =
|
|
#if REX_PLATFORM_WIN32
|
|
true;
|
|
#else
|
|
false;
|
|
#endif
|
|
|
|
#if REX_PLATFORM_LINUX
|
|
std::string Trim(std::string value) {
|
|
const auto first = value.find_first_not_of(" \t\r\n");
|
|
if (first == std::string::npos) {
|
|
return "";
|
|
}
|
|
const auto last = value.find_last_not_of(" \t\r\n");
|
|
return value.substr(first, last - first + 1);
|
|
}
|
|
|
|
std::string UnquoteOsReleaseValue(std::string value) {
|
|
value = Trim(std::move(value));
|
|
if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
|
|
std::string unquoted;
|
|
unquoted.reserve(value.size() - 2);
|
|
bool escaped = false;
|
|
for (size_t i = 1; i + 1 < value.size(); ++i) {
|
|
const char ch = value[i];
|
|
if (escaped) {
|
|
unquoted.push_back(ch);
|
|
escaped = false;
|
|
} else if (ch == '\\') {
|
|
escaped = true;
|
|
} else {
|
|
unquoted.push_back(ch);
|
|
}
|
|
}
|
|
return unquoted;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
std::optional<std::string> ReadOsReleaseValue(std::string_view key) {
|
|
std::ifstream file("/etc/os-release");
|
|
std::string line;
|
|
while (std::getline(file, line)) {
|
|
const auto equals = line.find('=');
|
|
if (equals == std::string::npos) {
|
|
continue;
|
|
}
|
|
if (std::string_view(line.data(), equals) == key) {
|
|
return UnquoteOsReleaseValue(line.substr(equals + 1));
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::string TruncateForLog(std::string value) {
|
|
constexpr size_t kMaxLength = 512;
|
|
if (value.size() <= kMaxLength) {
|
|
return value;
|
|
}
|
|
value.resize(kMaxLength);
|
|
value += "...";
|
|
return value;
|
|
}
|
|
|
|
void LogEnvIfSet(const char* name) {
|
|
const char* value = std::getenv(name);
|
|
if (value && *value) {
|
|
REXLOG_INFO(" {}={}", name, TruncateForLog(value));
|
|
}
|
|
}
|
|
|
|
void LogLinuxRuntimeDiagnostics() {
|
|
REXLOG_INFO("Linux runtime diagnostics:");
|
|
|
|
const auto pretty_name = ReadOsReleaseValue("PRETTY_NAME");
|
|
const auto id = ReadOsReleaseValue("ID");
|
|
const auto version_id = ReadOsReleaseValue("VERSION_ID");
|
|
if (pretty_name) {
|
|
REXLOG_INFO(" OS: {}", *pretty_name);
|
|
}
|
|
if (id || version_id) {
|
|
REXLOG_INFO(" OS ID: {} {}", id.value_or("unknown"), version_id.value_or(""));
|
|
}
|
|
|
|
utsname uts = {};
|
|
if (uname(&uts) == 0) {
|
|
REXLOG_INFO(" Kernel: {} {} {}", uts.sysname, uts.release, uts.machine);
|
|
}
|
|
REXLOG_INFO(" glibc: {}", gnu_get_libc_version());
|
|
|
|
#if defined(__clang__)
|
|
REXLOG_INFO(" Compiler: clang {}", __clang_version__);
|
|
#elif defined(__GNUC__)
|
|
REXLOG_INFO(" Compiler: GCC {}.{}.{}", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
|
|
#endif
|
|
|
|
REXLOG_INFO("Linux session environment:");
|
|
LogEnvIfSet("XDG_SESSION_TYPE");
|
|
LogEnvIfSet("XDG_CURRENT_DESKTOP");
|
|
LogEnvIfSet("DESKTOP_SESSION");
|
|
LogEnvIfSet("DISPLAY");
|
|
LogEnvIfSet("WAYLAND_DISPLAY");
|
|
LogEnvIfSet("GDK_BACKEND");
|
|
LogEnvIfSet("SDL_VIDEODRIVER");
|
|
LogEnvIfSet("LD_LIBRARY_PATH");
|
|
|
|
REXLOG_INFO("Steam runtime environment:");
|
|
LogEnvIfSet("SteamAppId");
|
|
LogEnvIfSet("SteamGameId");
|
|
LogEnvIfSet("STEAM_COMPAT_APP_ID");
|
|
LogEnvIfSet("STEAM_COMPAT_CLIENT_INSTALL_PATH");
|
|
LogEnvIfSet("STEAM_COMPAT_DATA_PATH");
|
|
LogEnvIfSet("STEAM_RUNTIME");
|
|
LogEnvIfSet("STEAM_RUNTIME_LIBRARY_PATH");
|
|
LogEnvIfSet("STEAM_RUNTIME_HEAVY");
|
|
LogEnvIfSet("PRESSURE_VESSEL_RUNTIME");
|
|
LogEnvIfSet("PRESSURE_VESSEL_APP_ID");
|
|
LogEnvIfSet("container");
|
|
|
|
REXLOG_INFO("Steam Deck / gamescope environment:");
|
|
LogEnvIfSet("SteamDeck");
|
|
LogEnvIfSet("STEAMOS");
|
|
LogEnvIfSet("GAMESCOPE_WAYLAND_DISPLAY");
|
|
LogEnvIfSet("GAMESCOPE_EXTERNAL_OVERLAY");
|
|
LogEnvIfSet("ENABLE_GAMESCOPE_WSI");
|
|
LogEnvIfSet("MESA_VK_WSI_PRESENT_MODE");
|
|
LogEnvIfSet("RADV_PERFTEST");
|
|
LogEnvIfSet("VK_ICD_FILENAMES");
|
|
LogEnvIfSet("VK_DRIVER_FILES");
|
|
}
|
|
|
|
void StartForcedExitWatchdog(const char* reason) {
|
|
std::thread([reason]() {
|
|
std::this_thread::sleep_for(std::chrono::seconds(10));
|
|
REXLOG_WARN("{} watchdog exiting process after shutdown timeout", reason);
|
|
std::_Exit(EXIT_SUCCESS);
|
|
}).detach();
|
|
}
|
|
#endif
|
|
|
|
} // namespace
|
|
|
|
REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, true, "UI/Advanced",
|
|
"Enable the developer cvar browser on F4");
|
|
|
|
REXCVAR_DEFINE_BOOL(show_fps_counter, false, "UI", "Show the guest FPS counter overlay")
|
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
|
|
|
// --- 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 REX_PLATFORM_MAC
|
|
// Let the platform event loop run once before runtime setup. SDL creates the
|
|
// Cocoa window synchronously, but the app may not visibly activate if guest
|
|
// initialization starts before the first event-loop pass.
|
|
app_context().CallInUIThreadDeferred([this, paths = std::move(*paths)]() mutable {
|
|
if (shutting_down_.load(std::memory_order_acquire))
|
|
return;
|
|
if (!ConstructRuntime(paths)) {
|
|
app_context().QuitFromUIThread();
|
|
return;
|
|
}
|
|
LaunchModule();
|
|
});
|
|
#else
|
|
if (!ConstructRuntime(*paths))
|
|
return false;
|
|
LaunchModule();
|
|
#endif
|
|
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());
|
|
#if REX_PLATFORM_LINUX
|
|
LogLinuxRuntimeDiagnostics();
|
|
#endif
|
|
|
|
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 (!imgui_drawer_->HasDialogs()) {
|
|
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());
|
|
if (!frame_stats_provider_) {
|
|
frame_stats_provider_ = [presenter]() {
|
|
ui::Presenter::GuestFrameStats stats = presenter->GetGuestFrameStats();
|
|
return ui::FrameStats{stats.frame_time_ms, stats.fps, stats.frame_count};
|
|
};
|
|
}
|
|
auto update_guest_frame_stats_enabled = [this, presenter]() {
|
|
presenter->SetGuestFrameStatsEnabled(fps_overlay_ != nullptr || debug_overlay_ != nullptr);
|
|
};
|
|
rex::ui::RegisterBind("bind_fps_counter", "F2", "Toggle FPS counter", [this, presenter] {
|
|
if (fps_overlay_) {
|
|
fps_overlay_.reset();
|
|
} else {
|
|
fps_overlay_ =
|
|
std::make_unique<ui::FpsOverlayDialog>(imgui_drawer_.get(), presenter);
|
|
}
|
|
REXCVAR_SET(show_fps_counter, fps_overlay_ != nullptr);
|
|
presenter->SetGuestFrameStatsEnabled(fps_overlay_ != nullptr || debug_overlay_ != nullptr);
|
|
if (!config_path_.empty()) {
|
|
rex::cvar::SaveConfigValues(config_path_, {"show_fps_counter"});
|
|
}
|
|
});
|
|
if (REXCVAR_GET(show_fps_counter)) {
|
|
fps_overlay_ = std::make_unique<ui::FpsOverlayDialog>(imgui_drawer_.get(), presenter);
|
|
}
|
|
// The settings menu (and console/config reload) write show_fps_counter
|
|
// through cvar::SetFlagByName - without a change callback the value
|
|
// only stuck on disk and the overlay object never followed (the menu
|
|
// toggle looked dead). Add/RemoveDialog are safe mid-draw: the drawer
|
|
// iterates by index and adjusts it on removal. The F2 bind above
|
|
// already syncs the overlay itself before setting the cvar, so this
|
|
// callback no-ops for that path.
|
|
rex::cvar::RegisterChangeCallback(
|
|
"show_fps_counter", [this, presenter](std::string_view, std::string_view) {
|
|
const bool want = REXCVAR_GET(show_fps_counter);
|
|
if (want == (fps_overlay_ != nullptr)) {
|
|
return;
|
|
}
|
|
if (want) {
|
|
fps_overlay_ =
|
|
std::make_unique<ui::FpsOverlayDialog>(imgui_drawer_.get(), presenter);
|
|
} else {
|
|
fps_overlay_.reset();
|
|
}
|
|
presenter->SetGuestFrameStatsEnabled(fps_overlay_ != nullptr ||
|
|
debug_overlay_ != nullptr);
|
|
});
|
|
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());
|
|
}
|
|
auto* graphics_system = static_cast<rex::graphics::GraphicsSystem*>(config_.graphics.get());
|
|
if (graphics_system && graphics_system->presenter()) {
|
|
graphics_system->presenter()->SetGuestFrameStatsEnabled(
|
|
fps_overlay_ != nullptr || debug_overlay_ != nullptr);
|
|
}
|
|
});
|
|
update_guest_frame_stats_enabled();
|
|
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;
|
|
}
|
|
#if REX_PLATFORM_MAC
|
|
main_thread_ = main_thread;
|
|
#endif
|
|
|
|
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...");
|
|
#if REX_PLATFORM_LINUX
|
|
StartForcedExitWatchdog("Linux window close");
|
|
#endif
|
|
shutting_down_.store(true, std::memory_order_release);
|
|
#if REX_PLATFORM_MAC
|
|
if (main_thread_ && main_thread_->is_running()) {
|
|
main_thread_->Terminate(0);
|
|
}
|
|
#endif
|
|
if (runtime_ && runtime_->kernel_state()) {
|
|
runtime_->kernel_state()->TerminateTitle();
|
|
}
|
|
app_context().QuitFromUIThread();
|
|
}
|
|
|
|
void ReXApp::OnDestroy() {
|
|
// Notify subclass before cleanup
|
|
OnShutdown();
|
|
|
|
#if REX_PLATFORM_MAC
|
|
shutting_down_.store(true, std::memory_order_release);
|
|
if (main_thread_ && main_thread_->is_running()) {
|
|
main_thread_->Terminate(0);
|
|
}
|
|
#endif
|
|
|
|
// Unregister overlay keybinds before destroying dialogs
|
|
rex::ui::UnregisterBind("bind_fps_counter");
|
|
rex::ui::UnregisterBind("bind_debug_overlay");
|
|
rex::ui::UnregisterBind("bind_console");
|
|
rex::ui::UnregisterBind("bind_settings");
|
|
// The show_fps_counter callback captures `this` - drop it before teardown.
|
|
rex::cvar::UnregisterChangeCallbacks("show_fps_counter");
|
|
|
|
// ImGui cleanup (reverse of setup)
|
|
fps_overlay_.reset();
|
|
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 REX_PLATFORM_MAC
|
|
main_thread_ = nullptr;
|
|
#endif
|
|
// Input drivers may still be registered as window listeners. Detach them
|
|
// before destroying the SDL window so their backend state is released in
|
|
// listener order.
|
|
if (runtime_ && runtime_->input_system()) {
|
|
static_cast<rex::input::InputSystem*>(runtime_->input_system())->Shutdown();
|
|
}
|
|
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
|