From 9f91f90d60a3861302ac4d1e4294a0cd770368c8 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:40:18 +0100 Subject: [PATCH] graphics: runtime backend selection for dual-backend builds --- CMakeLists.txt | 5 +- .../rex/ui/overlay/simple_settings_overlay.h | 1 + src/graphics/command_processor.cpp | 12 +++ src/graphics/d3d12/command_processor.cpp | 11 +-- src/graphics/vulkan/command_processor.cpp | 16 +--- src/ui/overlay/simple_settings_overlay.cpp | 56 +++++++++++++- src/ui/rex_app.cpp | 73 +++++++++++++++++-- 7 files changed, 143 insertions(+), 31 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a8fbf62..aafa95c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,10 +28,11 @@ endif() # Graphics backend selection # D3D12: Windows only, ON by default. -# Vulkan: Cross-platform, ON by default on Linux, OFF by default on Windows. +# Vulkan: Cross-platform, ON by default everywhere. Windows builds both +# backends by default; the active one is chosen at runtime (gpu_backend cvar). if(WIN32) option(REXGLUE_USE_D3D12 "Enable D3D12 graphics backend" ON) - option(REXGLUE_USE_VULKAN "Enable Vulkan graphics backend" OFF) + option(REXGLUE_USE_VULKAN "Enable Vulkan graphics backend" ON) else() set(REXGLUE_USE_D3D12 OFF) option(REXGLUE_USE_VULKAN "Enable Vulkan graphics backend" ON) diff --git a/include/rex/ui/overlay/simple_settings_overlay.h b/include/rex/ui/overlay/simple_settings_overlay.h index d0ef863..2192585 100644 --- a/include/rex/ui/overlay/simple_settings_overlay.h +++ b/include/rex/ui/overlay/simple_settings_overlay.h @@ -96,6 +96,7 @@ class SimpleSettingsDialog final : public ImGuiDialog { // Staged setting values (committed by SaveVideo / SaveProfile). GraphicsDeviceList device_list_; + int graphics_api_index_ = 0; int device_index_ = 0; int resolution_scale_index_ = 0; int frame_cap_index_ = 0; diff --git a/src/graphics/command_processor.cpp b/src/graphics/command_processor.cpp index 8c40a62..183b5b6 100644 --- a/src/graphics/command_processor.cpp +++ b/src/graphics/command_processor.cpp @@ -61,6 +61,18 @@ REXCVAR_DEFINE_INT32(scaled_resolve_texture_loads_from_shared_memory_max_pixels, REXCVAR_DEFINE_BOOL(occlusion_query_enable, true, "GPU", "Enable host occlusion query handling") .lifecycle(rex::cvar::Lifecycle::kHotReload); +// Skate 3 Hor+ ultrawide classifier controls shared by every graphics backend. +// Defined here (not per backend) so builds with several backends compiled in +// register them exactly once. +REXCVAR_DEFINE_BOOL(skate3_ultrawide_skip_shadow_targets, true, "Skate 3", + "Do not apply Skate 3 Hor+ NDC correction while rendering likely shadow maps") + .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_INT32(skate3_ultrawide_shadow_skip_mode, 2, "Skate 3", + "Shadow pass skip mode: 1 = square atlases, 2 = plus large offscreen, " + "3 = plus all depth-only") + .range(1, 3) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + REXCVAR_DEFINE_STRING(readback_resolve, "none", "GPU", "Controls CPU readback of render-to-texture resolve results.\n" " none: Disable readback (default)\n" diff --git a/src/graphics/d3d12/command_processor.cpp b/src/graphics/d3d12/command_processor.cpp index aef9c03..44995da 100644 --- a/src/graphics/d3d12/command_processor.cpp +++ b/src/graphics/d3d12/command_processor.cpp @@ -58,14 +58,9 @@ REXCVAR_DEFINE_BOOL(d3d12_gpu_timestamp_buckets, false, "GPU/D3D12", "the breakdown in the FPS overlay and a periodic profile log") .lifecycle(rex::cvar::Lifecycle::kHotReload); -REXCVAR_DEFINE_BOOL(skate3_ultrawide_skip_shadow_targets, true, "Skate 3", - "Do not apply Skate 3 Hor+ NDC correction while rendering likely shadow maps") - .lifecycle(rex::cvar::Lifecycle::kHotReload); -REXCVAR_DEFINE_INT32(skate3_ultrawide_shadow_skip_mode, 2, "Skate 3", - "Shadow pass skip mode: 1 = square atlases, 2 = plus large offscreen, " - "3 = plus all depth-only") - .range(1, 3) - .lifecycle(rex::cvar::Lifecycle::kHotReload); +// Defined in graphics/command_processor.cpp, shared with the Vulkan backend. +REXCVAR_DECLARE(bool, skate3_ultrawide_skip_shadow_targets); +REXCVAR_DECLARE(int32_t, skate3_ultrawide_shadow_skip_mode); REXCVAR_DEFINE_BOOL(skate3_ultrawide_fake_occlusion_queries, false, "Skate 3", "Return positive visibility query results while Skate 3 Hor+ ultrawide is " "active so side-of-frame assets remain resident") diff --git a/src/graphics/vulkan/command_processor.cpp b/src/graphics/vulkan/command_processor.cpp index d326281..f308fd0 100644 --- a/src/graphics/vulkan/command_processor.cpp +++ b/src/graphics/vulkan/command_processor.cpp @@ -170,19 +170,9 @@ REXCVAR_DEFINE_INT32(vulkan_gpu_clock_log_interval_frames, 120, "GPU/Vulkan", .range(0, 36000) .lifecycle(rex::cvar::Lifecycle::kHotReload); -// Skate 3 ultrawide cvars. These mirror the Direct3D 12 backend's definitions -// so the Hor+ classifier behaves identically on Vulkan; the D3D12 translation -// unit is not compiled on this platform, so without these the queries below -// would resolve to an unregistered flag (empty string -> false/0) and silently -// disable the shadow-map skip. -REXCVAR_DEFINE_BOOL(skate3_ultrawide_skip_shadow_targets, true, "Skate 3", - "Do not apply Skate 3 Hor+ NDC correction while rendering likely shadow maps") - .lifecycle(rex::cvar::Lifecycle::kHotReload); -REXCVAR_DEFINE_INT32(skate3_ultrawide_shadow_skip_mode, 2, "Skate 3", - "Shadow pass skip mode: 1 = square atlases, 2 = plus large offscreen, " - "3 = plus all depth-only") - .range(1, 3) - .lifecycle(rex::cvar::Lifecycle::kHotReload); +// The shared Skate 3 Hor+ classifier cvars (skate3_ultrawide_skip_shadow_targets, +// skate3_ultrawide_shadow_skip_mode) live in graphics/command_processor.cpp so +// every backend sees one registration; the queries below resolve them by name. REXCVAR_DEFINE_BOOL(skate3_ultrawide_skip_screen_space_targets, false, "Skate 3", "Exclude screen-space/orthographic (pre-divided XY) passes from Skate 3 Hor+ " "NDC correction. Off by default so the Vulkan default classifier matches the " diff --git a/src/ui/overlay/simple_settings_overlay.cpp b/src/ui/overlay/simple_settings_overlay.cpp index 9e97033..0d790d4 100644 --- a/src/ui/overlay/simple_settings_overlay.cpp +++ b/src/ui/overlay/simple_settings_overlay.cpp @@ -222,6 +222,10 @@ std::vector GetSimpleSettingsCvars() { } else if (device_cvar == "vulkan_device" && HasCvar("vulkan_device")) { cvars.push_back("vulkan_device"); } + // Persist the graphics API choice only on builds where it can be chosen. + if (HasCvar("gpu_backend") && HasCvar("d3d12_adapter") && HasCvar("vulkan_device")) { + cvars.push_back("gpu_backend"); + } if (HasCvar("skate3_ultrawide")) { cvars.push_back("skate3_ultrawide"); } @@ -289,7 +293,11 @@ int ResolutionIndexFromCvar() { } bool HasHostFrameCapCvars() { - return HasCvar("d3d12_present_frame_limiter") && HasCvar("d3d12_present_frame_limiter_fps"); + // The host present limiter is implemented by the D3D12 presenter; on builds + // with both backends compiled in its cvars exist even while Vulkan renders, + // so gate on the backend actually running. + return HasCvar("d3d12_present_frame_limiter") && HasCvar("d3d12_present_frame_limiter_fps") && + GetGraphicsDeviceList().cvar_name != "vulkan_device"; } // The guest-side cap paces the game's render loop at the swap boundary, so @@ -563,6 +571,26 @@ void SetBoolCvar(std::string_view name, bool value) { rex::cvar::SetFlagByName(name, value ? "true" : "false"); } +// The Graphics API row only exists on builds with both backends compiled in; +// each backend's UI provider registers its device cvar, so their joint +// presence identifies such a build. +bool HasGraphicsApiChoice() { + return HasCvar("gpu_backend") && HasCvar("d3d12_adapter") && HasCvar("vulkan_device"); +} + +// 0 = Direct3D 12, 1 = Vulkan. "auto" reads as the backend actually running, +// so the row reflects reality even after a startup fallback. +int GraphicsApiIndexFromCvar() { + const std::string value = rex::cvar::Query("gpu_backend"); + if (value == "vulkan") { + return 1; + } + if (value == "d3d12") { + return 0; + } + return GetGraphicsDeviceList().cvar_name == "vulkan_device" ? 1 : 0; +} + bool TearingFromCvar() { if (HasCvar("d3d12_allow_variable_refresh_rate_and_tearing")) { return rex::cvar::Query("d3d12_allow_variable_refresh_rate_and_tearing"); @@ -960,6 +988,7 @@ void SimpleSettingsDialog::Show() { void SimpleSettingsDialog::LoadSettingsFromCvars() { device_list_ = GetGraphicsDeviceList(); + graphics_api_index_ = HasGraphicsApiChoice() ? GraphicsApiIndexFromCvar() : 0; device_index_ = DeviceIndexFromCvar(device_list_); resolution_scale_index_ = ResolutionIndexFromCvar(); frame_cap_index_ = FrameCapIndexFromCvar(); @@ -1007,7 +1036,8 @@ void SimpleSettingsDialog::LoadSettingsFromCvars() { } bool SimpleSettingsDialog::HasSettingsChanges() const { - return device_index_ != DeviceIndexFromCvar(device_list_) || + return (HasGraphicsApiChoice() && graphics_api_index_ != GraphicsApiIndexFromCvar()) || + device_index_ != DeviceIndexFromCvar(device_list_) || resolution_scale_index_ != ResolutionIndexFromCvar() || frame_cap_index_ != FrameCapIndexFromCvar() || (HasCvar("skate3_ultrawide") && @@ -1082,6 +1112,12 @@ void SimpleSettingsDialog::SaveVideo() { aspect_ratio_index_ = std::clamp(aspect_ratio_index_, 0, static_cast(kAspectRatioLabels.size()) - 1); field_of_view_ = std::clamp(field_of_view_, 40.0f, 120.0f); + graphics_api_index_ = std::clamp(graphics_api_index_, 0, 1); + // Written only when the selection changed, so an untouched row keeps the + // cvar on "auto". + if (HasGraphicsApiChoice() && graphics_api_index_ != GraphicsApiIndexFromCvar()) { + rex::cvar::SetFlagByName("gpu_backend", graphics_api_index_ == 1 ? "vulkan" : "d3d12"); + } if (!device_list_.device_names.empty() && HasCvar(device_list_.cvar_name)) { device_index_ = std::clamp(device_index_, 0, static_cast(device_list_.device_names.size()) - 1); @@ -1207,6 +1243,22 @@ void SimpleSettingsDialog::BuildRows(std::vector& rows, int category) { switch (category) { case 0: { // Video header("Display"); + if (HasGraphicsApiChoice()) { + RowSpec row; + row.kind = RowSpec::kEnum; + row.label = "Graphics API"; + row.desc = + "Which graphics API the game renders through. Both perform " + "similarly - try the other one if you run into driver issues. " + "Applied with Apply & Restart."; + row.options = {"DirectX 12", "Vulkan"}; + row.index = &graphics_api_index_; + row.reset = [this] { + // The build default ("auto") prefers DirectX 12 on Windows. + graphics_api_index_ = 0; + }; + rows.push_back(std::move(row)); + } if (!device_list_.device_names.empty() && HasCvar(device_list_.cvar_name)) { RowSpec row; row.kind = RowSpec::kEnum; diff --git a/src/ui/rex_app.cpp b/src/ui/rex_app.cpp index 5d6849e..00adbf9 100644 --- a/src/ui/rex_app.cpp +++ b/src/ui/rex_app.cpp @@ -59,6 +59,7 @@ #include #include #include +#include namespace rex { @@ -209,6 +210,13 @@ void StartForcedExitWatchdog(const char* reason) { REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, true, "UI/Advanced", "Enable the developer cvar browser on F4"); +REXCVAR_DEFINE_STRING(gpu_backend, "auto", "GPU", + "Graphics API used for rendering (auto, d3d12, vulkan). Auto prefers " + "Direct3D 12 when available. Applied at startup; when the selected API " + "fails to initialize, any other compiled-in backend is tried.") + .allowed({"auto", "d3d12", "vulkan"}) + .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); + REXCVAR_DEFINE_BOOL(show_fps_counter, false, "UI", "Show the guest FPS counter overlay") .lifecycle(rex::cvar::Lifecycle::kHotReload); @@ -505,22 +513,75 @@ bool ReXApp::ConstructRuntime(const PathConfig& paths) { } bool ReXApp::SetupPresentation() { + // Candidate graphics backends in preference order. The gpu_backend cvar + // (already loaded from the config/settings files by SetupEnvironment) + // reorders the list at runtime; "auto" keeps Direct3D 12 first when it is + // compiled in. + struct GraphicsCandidate { + const char* id; // gpu_backend cvar value + const char* name; // log-facing name + std::unique_ptr (*make)(); + }; + std::vector candidates; #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); + candidates.push_back({"d3d12", "Direct3D 12", []() -> std::unique_ptr { + return std::make_unique(); + }}); #endif +#if REX_HAS_VULKAN + candidates.push_back({"vulkan", "Vulkan", []() -> std::unique_ptr { + return std::make_unique(); + }}); +#endif + const std::string requested = REXCVAR_GET(gpu_backend); + if (requested != "auto" && !candidates.empty()) { + bool available = false; + for (size_t i = 0; i < candidates.size(); ++i) { + if (requested == candidates[i].id) { + GraphicsCandidate wanted = candidates[i]; + candidates.erase(candidates.begin() + i); + candidates.insert(candidates.begin(), wanted); + available = true; + break; + } + } + if (!available) { + REXLOG_WARN("gpu_backend={} is not available in this build; using {}", requested, + candidates[0].id); + } + } + + size_t candidate_index = 0; + if (!candidates.empty()) { + config_.graphics = candidates[candidate_index].make(); + } 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; + rex::system::IGraphicsSystem* selected_graphics = config_.graphics.get(); OnPreSetup(config_); + // A consumer may substitute its own graphics system in OnPreSetup; the + // candidate fallback below only applies to the stock selection. + const bool stock_graphics = config_.graphics.get() == selected_graphics; if (config_.graphics) { - X_STATUS status = config_.graphics->SetupPresentation(&app_context()); - if (XFAILED(status)) { + for (;;) { + X_STATUS status = config_.graphics->SetupPresentation(&app_context()); + if (!XFAILED(status)) { + if (stock_graphics && !candidates.empty()) { + REXLOG_INFO("Graphics backend: {} (gpu_backend={})", + candidates[candidate_index].name, requested); + } + break; + } REXLOG_ERROR("Graphics presentation setup failed: {:08X}", status); - return false; + if (!stock_graphics || candidate_index + 1 >= candidates.size()) { + return false; + } + ++candidate_index; + REXLOG_WARN("Trying the {} graphics backend instead", candidates[candidate_index].name); + config_.graphics = candidates[candidate_index].make(); } }