graphics: runtime backend selection for dual-backend builds
This commit is contained in:
parent
fbd4e06a34
commit
9f91f90d60
7 changed files with 143 additions and 31 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 "
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,22 +513,75 @@ 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) {
|
||||||
X_STATUS status = config_.graphics->SetupPresentation(&app_context());
|
for (;;) {
|
||||||
if (XFAILED(status)) {
|
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);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue