graphics: runtime backend selection for dual-backend builds

This commit is contained in:
mchughalex 2026-07-19 19:40:18 +01:00
parent fbd4e06a34
commit 9f91f90d60
7 changed files with 143 additions and 31 deletions

View file

@ -28,10 +28,11 @@ endif()
# Graphics backend selection # Graphics backend selection
# D3D12: Windows only, ON by default. # 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) if(WIN32)
option(REXGLUE_USE_D3D12 "Enable D3D12 graphics backend" ON) 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() else()
set(REXGLUE_USE_D3D12 OFF) set(REXGLUE_USE_D3D12 OFF)
option(REXGLUE_USE_VULKAN "Enable Vulkan graphics backend" ON) option(REXGLUE_USE_VULKAN "Enable Vulkan graphics backend" ON)

View file

@ -96,6 +96,7 @@ class SimpleSettingsDialog final : public ImGuiDialog {
// Staged setting values (committed by SaveVideo / SaveProfile). // Staged setting values (committed by SaveVideo / SaveProfile).
GraphicsDeviceList device_list_; GraphicsDeviceList device_list_;
int graphics_api_index_ = 0;
int device_index_ = 0; int device_index_ = 0;
int resolution_scale_index_ = 0; int resolution_scale_index_ = 0;
int frame_cap_index_ = 0; int frame_cap_index_ = 0;

View file

@ -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") REXCVAR_DEFINE_BOOL(occlusion_query_enable, true, "GPU", "Enable host occlusion query handling")
.lifecycle(rex::cvar::Lifecycle::kHotReload); .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", REXCVAR_DEFINE_STRING(readback_resolve, "none", "GPU",
"Controls CPU readback of render-to-texture resolve results.\n" "Controls CPU readback of render-to-texture resolve results.\n"
" none: Disable readback (default)\n" " none: Disable readback (default)\n"

View file

@ -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") "the breakdown in the FPS overlay and a periodic profile log")
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_ultrawide_skip_shadow_targets, true, "Skate 3", // Defined in graphics/command_processor.cpp, shared with the Vulkan backend.
"Do not apply Skate 3 Hor+ NDC correction while rendering likely shadow maps") REXCVAR_DECLARE(bool, skate3_ultrawide_skip_shadow_targets);
.lifecycle(rex::cvar::Lifecycle::kHotReload); REXCVAR_DECLARE(int32_t, skate3_ultrawide_shadow_skip_mode);
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_BOOL(skate3_ultrawide_fake_occlusion_queries, false, "Skate 3", REXCVAR_DEFINE_BOOL(skate3_ultrawide_fake_occlusion_queries, false, "Skate 3",
"Return positive visibility query results while Skate 3 Hor+ ultrawide is " "Return positive visibility query results while Skate 3 Hor+ ultrawide is "
"active so side-of-frame assets remain resident") "active so side-of-frame assets remain resident")

View file

@ -170,19 +170,9 @@ REXCVAR_DEFINE_INT32(vulkan_gpu_clock_log_interval_frames, 120, "GPU/Vulkan",
.range(0, 36000) .range(0, 36000)
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
// Skate 3 ultrawide cvars. These mirror the Direct3D 12 backend's definitions // The shared Skate 3 Hor+ classifier cvars (skate3_ultrawide_skip_shadow_targets,
// so the Hor+ classifier behaves identically on Vulkan; the D3D12 translation // skate3_ultrawide_shadow_skip_mode) live in graphics/command_processor.cpp so
// unit is not compiled on this platform, so without these the queries below // every backend sees one registration; the queries below resolve them by name.
// 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);
REXCVAR_DEFINE_BOOL(skate3_ultrawide_skip_screen_space_targets, false, "Skate 3", REXCVAR_DEFINE_BOOL(skate3_ultrawide_skip_screen_space_targets, false, "Skate 3",
"Exclude screen-space/orthographic (pre-divided XY) passes from Skate 3 Hor+ " "Exclude screen-space/orthographic (pre-divided XY) passes from Skate 3 Hor+ "
"NDC correction. Off by default so the Vulkan default classifier matches the " "NDC correction. Off by default so the Vulkan default classifier matches the "

View file

@ -222,6 +222,10 @@ std::vector<std::string_view> GetSimpleSettingsCvars() {
} else if (device_cvar == "vulkan_device" && HasCvar("vulkan_device")) { } else if (device_cvar == "vulkan_device" && HasCvar("vulkan_device")) {
cvars.push_back("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")) { if (HasCvar("skate3_ultrawide")) {
cvars.push_back("skate3_ultrawide"); cvars.push_back("skate3_ultrawide");
} }
@ -289,7 +293,11 @@ int ResolutionIndexFromCvar() {
} }
bool HasHostFrameCapCvars() { 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 // 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"); 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<std::string>("gpu_backend");
if (value == "vulkan") {
return 1;
}
if (value == "d3d12") {
return 0;
}
return GetGraphicsDeviceList().cvar_name == "vulkan_device" ? 1 : 0;
}
bool TearingFromCvar() { bool TearingFromCvar() {
if (HasCvar("d3d12_allow_variable_refresh_rate_and_tearing")) { if (HasCvar("d3d12_allow_variable_refresh_rate_and_tearing")) {
return rex::cvar::Query<bool>("d3d12_allow_variable_refresh_rate_and_tearing"); return rex::cvar::Query<bool>("d3d12_allow_variable_refresh_rate_and_tearing");
@ -960,6 +988,7 @@ void SimpleSettingsDialog::Show() {
void SimpleSettingsDialog::LoadSettingsFromCvars() { void SimpleSettingsDialog::LoadSettingsFromCvars() {
device_list_ = GetGraphicsDeviceList(); device_list_ = GetGraphicsDeviceList();
graphics_api_index_ = HasGraphicsApiChoice() ? GraphicsApiIndexFromCvar() : 0;
device_index_ = DeviceIndexFromCvar(device_list_); device_index_ = DeviceIndexFromCvar(device_list_);
resolution_scale_index_ = ResolutionIndexFromCvar(); resolution_scale_index_ = ResolutionIndexFromCvar();
frame_cap_index_ = FrameCapIndexFromCvar(); frame_cap_index_ = FrameCapIndexFromCvar();
@ -1007,7 +1036,8 @@ void SimpleSettingsDialog::LoadSettingsFromCvars() {
} }
bool SimpleSettingsDialog::HasSettingsChanges() const { 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() || resolution_scale_index_ != ResolutionIndexFromCvar() ||
frame_cap_index_ != FrameCapIndexFromCvar() || frame_cap_index_ != FrameCapIndexFromCvar() ||
(HasCvar("skate3_ultrawide") && (HasCvar("skate3_ultrawide") &&
@ -1082,6 +1112,12 @@ void SimpleSettingsDialog::SaveVideo() {
aspect_ratio_index_ = aspect_ratio_index_ =
std::clamp(aspect_ratio_index_, 0, static_cast<int>(kAspectRatioLabels.size()) - 1); std::clamp(aspect_ratio_index_, 0, static_cast<int>(kAspectRatioLabels.size()) - 1);
field_of_view_ = std::clamp(field_of_view_, 40.0f, 120.0f); 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)) { if (!device_list_.device_names.empty() && HasCvar(device_list_.cvar_name)) {
device_index_ = std::clamp(device_index_, 0, device_index_ = std::clamp(device_index_, 0,
static_cast<int>(device_list_.device_names.size()) - 1); static_cast<int>(device_list_.device_names.size()) - 1);
@ -1207,6 +1243,22 @@ void SimpleSettingsDialog::BuildRows(std::vector<RowSpec>& rows, int category) {
switch (category) { switch (category) {
case 0: { // Video case 0: { // Video
header("Display"); 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)) { if (!device_list_.device_names.empty() && HasCvar(device_list_.cvar_name)) {
RowSpec row; RowSpec row;
row.kind = RowSpec::kEnum; row.kind = RowSpec::kEnum;

View file

@ -59,6 +59,7 @@
#include <string_view> #include <string_view>
#include <thread> #include <thread>
#include <utility> #include <utility>
#include <vector>
namespace rex { namespace rex {
@ -209,6 +210,13 @@ void StartForcedExitWatchdog(const char* reason) {
REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, true, "UI/Advanced", REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, true, "UI/Advanced",
"Enable the developer cvar browser on F4"); "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") REXCVAR_DEFINE_BOOL(show_fps_counter, false, "UI", "Show the guest FPS counter overlay")
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
@ -505,23 +513,76 @@ bool ReXApp::ConstructRuntime(const PathConfig& paths) {
} }
bool ReXApp::SetupPresentation() { 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<rex::system::IGraphicsSystem> (*make)();
};
std::vector<GraphicsCandidate> candidates;
#if REX_HAS_D3D12 #if REX_HAS_D3D12
config_.graphics = REX_GRAPHICS_BACKEND(rex::graphics::d3d12::D3D12GraphicsSystem); candidates.push_back({"d3d12", "Direct3D 12", []() -> std::unique_ptr<rex::system::IGraphicsSystem> {
#elif REX_HAS_VULKAN return std::make_unique<rex::graphics::d3d12::D3D12GraphicsSystem>();
config_.graphics = REX_GRAPHICS_BACKEND(rex::graphics::vulkan::VulkanGraphicsSystem); }});
#endif #endif
#if REX_HAS_VULKAN
candidates.push_back({"vulkan", "Vulkan", []() -> std::unique_ptr<rex::system::IGraphicsSystem> {
return std::make_unique<rex::graphics::vulkan::VulkanGraphicsSystem>();
}});
#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_.audio_factory = REX_AUDIO_BACKEND(rex::audio::sdl::SDLAudioSystem);
config_.input_factory = REX_INPUT_BACKEND(rex::input::CreateDefaultInputSystem); config_.input_factory = REX_INPUT_BACKEND(rex::input::CreateDefaultInputSystem);
config_.kernel_init = rex::kernel::InitializeKernel; config_.kernel_init = rex::kernel::InitializeKernel;
rex::system::IGraphicsSystem* selected_graphics = config_.graphics.get();
OnPreSetup(config_); 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) { if (config_.graphics) {
for (;;) {
X_STATUS status = config_.graphics->SetupPresentation(&app_context()); X_STATUS status = config_.graphics->SetupPresentation(&app_context());
if (XFAILED(status)) { 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); REXLOG_ERROR("Graphics presentation setup failed: {:08X}", status);
if (!stock_graphics || candidate_index + 1 >= candidates.size()) {
return false; return false;
} }
++candidate_index;
REXLOG_WARN("Trying the {} graphics backend instead", candidates[candidate_index].name);
config_.graphics = candidates[candidate_index].make();
}
} }
// Create window // Create window