Compare commits

...

8 Commits

Author SHA1 Message Date
Stenzek
9e87d1afed VulkanDevice: Destroy context on zero refs with SDL window type
If it's not destroyed, it prevents OpenGL contexts from being created
without any errors...
2026-02-16 22:07:25 +10:00
Stenzek
801b90146a CMake: Ensure resources directory exists before rcc
Fixes spurious build failures on low core count machines.
2026-02-16 21:47:58 +10:00
Stenzek
85332addd5 GameDB: Add sort names for NFS/NASCAR games 2026-02-16 21:28:33 +10:00
Stenzek
499ca01a1e GameDB: Add NeGconRumble to a bunch of supported games 2026-02-16 21:28:33 +10:00
Stenzek
c201d7f4ff FullscreenUI: Apply fullyscreen resolution change immediately 2026-02-16 21:28:33 +10:00
Stenzek
d777983d74 FullscreenUI: Fix mode list not populating on default adapter 2026-02-16 21:28:33 +10:00
Stenzek
e0175b55db GPUDevice: Enumerate fullscreen modes if using SDL 2026-02-16 21:28:32 +10:00
Stenzek
445428c978 Mini: Set fullscreen display mode on "windows" on kmsdrm
Allow the user to choose the display mode.
2026-02-16 21:28:32 +10:00
16 changed files with 291 additions and 32 deletions

View File

@@ -3627,6 +3627,7 @@ SLPS-01689:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Atlus Co"
developer: "Atlus Co"
@@ -29014,6 +29015,7 @@ SLES-00477:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Sony"
developer: "Codemasters"
@@ -29043,6 +29045,7 @@ SLES-01204:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Sony"
developer: "Codemasters"
@@ -29072,6 +29075,7 @@ SCUS-94474:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Sony"
developer: "Codemasters"
@@ -29092,6 +29096,7 @@ SLPS-91477:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Spike"
developer: "Codemasters"
@@ -29112,6 +29117,7 @@ SLPS-03274:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Spike"
developer: "Codemasters"
@@ -29136,6 +29142,7 @@ SLES-02605:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Codemasters"
developer: "Codemasters"
@@ -29164,6 +29171,7 @@ SLUS-01222:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
settings:
displayActiveStartOffset: 64
displayActiveEndOffset: 68
@@ -29189,6 +29197,7 @@ SLPS-91431:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Spike"
developer: "Codemasters"
@@ -70340,6 +70349,7 @@ SCES-00984:
- AnalogJoystick
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Sony"
developer: "Polyphony Digital"
@@ -70385,6 +70395,7 @@ SCPS-10045:
- AnalogJoystick
- DigitalController
- NeGcon
- NeGconRumble
codes:
- SCPS-10045
- SCPS-45149
@@ -70415,6 +70426,7 @@ SCUS-94194:
- AnalogJoystick
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Sony"
developer: "Polyphony Digital"
@@ -113003,6 +113015,7 @@ SLES-02191:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "Stormfront Studios"
@@ -113024,6 +113037,7 @@ SLUS-00962:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "Stormfront Studios"
@@ -113045,6 +113059,7 @@ SLUS-01263:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "Black Box Games"
@@ -113061,6 +113076,7 @@ SLUS-01263:
linkCable: false
SLES-00905:
name: "NASCAR 98"
sortName: "NASCAR 1998 (Europe)"
saveName: "NASCAR 98 (Europe)"
controllers:
- AnalogController
@@ -113083,9 +113099,11 @@ SLES-00905:
linkCable: false
SLES-00765:
name: "NASCAR 98"
sortName: "NASCAR 1998 (France)"
saveName: "NASCAR 98 (France)"
SLES-00880:
name: "NASCAR 98"
sortName: "NASCAR 1998 (Germany)"
saveName: "NASCAR 98 (Germany)"
controllers:
- AnalogController
@@ -113109,6 +113127,7 @@ SLES-00880:
SLPS-01295:
name: "NASCAR 98"
localizedName: "ナスカー98"
sortName: "NASCAR 1998 (Japan)"
saveName: "NASCAR 98 (Japan)"
controllers:
- AnalogController
@@ -113131,6 +113150,7 @@ SLPS-01295:
linkCable: false
SLUS-00521:
name: "NASCAR 98"
sortName: "NASCAR 1998 (USA)"
saveName: "NASCAR 98 (USA)"
controllers:
- AnalogController
@@ -113153,6 +113173,7 @@ SLUS-00521:
linkCable: false
SLUS-00647:
name: "NASCAR 98 Collector's Edition"
sortName: "NASCAR 1998 Collector's Edition (USA)"
saveName: "NASCAR 98 Collector's Edition (USA)"
controllers:
- AnalogController
@@ -113175,11 +113196,13 @@ SLUS-00647:
linkCable: false
SLES-01447:
name: "NASCAR 99"
sortName: "NASCAR 1999 (Europe)"
saveName: "NASCAR 99 (Europe)"
controllers:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "Stormfront Studios"
@@ -113196,11 +113219,13 @@ SLES-01447:
linkCable: false
SLES-01452:
name: "NASCAR 99"
sortName: "NASCAR 1999 (France)"
saveName: "NASCAR 99 (France)"
controllers:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "Stormfront Studios"
@@ -113217,11 +113242,13 @@ SLES-01452:
linkCable: false
SLES-01453:
name: "NASCAR 99"
sortName: "NASCAR 1999 (Germany)"
saveName: "NASCAR 99 (Germany)"
controllers:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "Stormfront Studios"
@@ -113238,11 +113265,13 @@ SLES-01453:
linkCable: false
SLUS-00740:
name: "NASCAR 99"
sortName: "NASCAR 1999 (USA)"
saveName: "NASCAR 99 (USA)"
controllers:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "Stormfront Studios"
@@ -113259,11 +113288,13 @@ SLUS-00740:
linkCable: false
SLUS-00883:
name: "NASCAR 99 Legacy"
sortName: "NASCAR 1999 Legacy (USA)"
saveName: "NASCAR 99 Legacy (USA)"
controllers:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "Stormfront Studios"
@@ -118447,11 +118478,13 @@ SLUS-00764:
linkCable: false
SLES-01876:
name: "Need for Speed - High Stakes"
sortName: "Need for Speed 4 - High Stakes (Australia)"
saveName: "Need for Speed - High Stakes (Australia)"
controllers:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "EA Canada"
@@ -118468,6 +118501,7 @@ SLES-01876:
linkCable: false
SLUS-00826:
name: "Need for Speed - High Stakes"
sortName: "Need for Speed 4 - High Stakes (USA)"
saveName: "Need for Speed - High Stakes (USA)"
compatibility:
rating: NoIssues
@@ -118476,6 +118510,7 @@ SLUS-00826:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Electronic Arts"
developer: "EA Canada"
@@ -118492,6 +118527,7 @@ SLUS-00826:
linkCable: false
SLES-02689:
name: "Need for Speed - Porsche 2000"
sortName: "Need for Speed 5 - Porsche 2000 (Europe) (En,De,Sv)"
saveName: "Need for Speed - Porsche 2000 (Europe) (En,De,Sv)"
controllers:
- AnalogController
@@ -118516,6 +118552,7 @@ SLES-02689:
linkCable: false
SLES-02700:
name: "Need for Speed - Porsche 2000"
sortName: "Need for Speed 5 - Porsche 2000 (Europe) (Fr,Es,It)"
saveName: "Need for Speed - Porsche 2000 (Europe) (Fr,Es,It)"
controllers:
- AnalogController
@@ -118540,6 +118577,7 @@ SLES-02700:
linkCable: false
SLUS-01104:
name: "Need for Speed - Porsche Unleashed"
sortName: "Need for Speed 5 - Porsche Unleashed (USA)"
saveName: "Need for Speed - Porsche Unleashed (USA)"
compatibility:
rating: NoIssues
@@ -118564,6 +118602,7 @@ SLUS-01104:
linkCable: false
SLES-01790:
name: "Need for Speed - Road Challenge"
sortName: "Need for Speed 4 - Road Challenge (Europe) (En,Es,It)"
saveName: "Need for Speed - Road Challenge (Europe) (En,Es,It)"
controllers:
- AnalogController
@@ -118587,6 +118626,7 @@ SLES-01790:
linkCable: false
SLES-01788:
name: "Need for Speed - Road Challenge"
sortName: "Need for Speed 4 - Road Challenge (Europe) (En,Sv)"
saveName: "Need for Speed - Road Challenge (Europe) (En,Sv)"
controllers:
- AnalogController
@@ -118609,6 +118649,7 @@ SLES-01788:
linkCable: false
SLES-01789:
name: "Need for Speed - Road Challenge"
sortName: "Need for Speed 4 - Road Challenge (Europe) (Fr,De)"
saveName: "Need for Speed - Road Challenge (Europe) (Fr,De)"
controllers:
- AnalogController
@@ -118679,6 +118720,7 @@ SLUS-01003:
linkCable: false
SLES-00658:
name: "Need for Speed II"
sortName: "Need for Speed 2 (Europe) (En,Fr,De,Es,It,Sv)"
saveName: "Need for Speed II (Europe) (En,Fr,De,Es,It,Sv)"
compatibility:
rating: NoIssues
@@ -118709,6 +118751,7 @@ SLES-00658:
linkCable: false
SLUS-00276:
name: "Need for Speed II"
sortName: "Need for Speed 2 (USA)"
saveName: "Need for Speed II (USA)"
compatibility:
rating: NoIssues
@@ -118739,6 +118782,7 @@ SLUS-00276:
linkCable: false
SLES-01154:
name: "Need for Speed III - Hot Pursuit"
sortName: "Need for Speed 3 - Hot Pursuit (Europe) (En,Fr,De,Es,It,Sv)"
saveName: "Need for Speed III - Hot Pursuit (Europe) (En,Fr,De,Es,It,Sv)"
controllers:
- AnalogController
@@ -118765,6 +118809,7 @@ SLES-01154:
linkCable: false
SLUS-00620:
name: "Need for Speed III - Hot Pursuit"
sortName: "Need for Speed 3 - Hot Pursuit (USA)"
saveName: "Need for Speed III - Hot Pursuit (USA)"
compatibility:
rating: NoIssues
@@ -139298,6 +139343,7 @@ SLPS-01601:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
codes:
- SLPS-01601
- SLPS-02258
@@ -139329,6 +139375,7 @@ SLPS-02679:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Prism Arts"
developer: "Prism Arts"
@@ -144362,6 +144409,7 @@ SLPS-02388:
linkCable: false
SLES-00223:
name: "Road & Track Presents - The Need for Speed"
sortName: "Need for Speed 1 (Europe) (En,De)"
saveName: "Road & Track Presents - The Need for Speed (Europe) (En,De)"
compatibility:
rating: NoIssues
@@ -144386,6 +144434,7 @@ SLES-00223:
linkCable: true
SLUS-00204:
name: "Road & Track Presents - The Need for Speed"
sortName: "Need for Speed 1 (USA)"
saveName: "Road & Track Presents - The Need for Speed (USA)"
compatibility:
rating: NoIssues
@@ -153665,6 +153714,7 @@ SLPM-86344:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Taito"
developer: "Taito"
@@ -173565,6 +173615,7 @@ SLUS-00996:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Codemasters"
developer: "Codemasters"
@@ -173588,6 +173639,7 @@ SLES-01542:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Codemasters"
developer: "Codemasters"
@@ -173611,6 +173663,7 @@ SLES-01547:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Codemasters"
developer: "Codemasters"
@@ -173633,6 +173686,7 @@ SLUS-00611:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "3DO"
developer: "Codemasters"
@@ -173654,6 +173708,7 @@ SLES-00376:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Codemasters"
developer: "Codemasters"
@@ -173676,6 +173731,7 @@ SLPS-01410:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
metadata:
publisher: "Upstar (Lay-Up)"
developer: "Codemasters"
@@ -173697,6 +173753,7 @@ SLES-02572:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
libcrypt: true
metadata:
publisher: "Codemasters"
@@ -173721,6 +173778,7 @@ SLES-02573:
- AnalogController
- DigitalController
- NeGcon
- NeGconRumble
libcrypt: true
metadata:
publisher: "Codemasters"

View File

@@ -3816,6 +3816,9 @@ void FullscreenUI::DrawGraphicsSettingsPage()
options.emplace_back(FSUI_STR("Default"), current_adapter.has_value() && current_adapter->empty());
for (const GPUDevice::AdapterInfo& adapter : s_settings_locals.graphics_adapter_list_cache)
{
if (adapter.name.empty())
continue;
const bool checked = (current_adapter.has_value() && current_adapter.value() == adapter.name);
options.emplace_back(adapter.name, checked);
}
@@ -4017,7 +4020,7 @@ void FullscreenUI::DrawGraphicsSettingsPage()
FSUI_VSTR("Use Global Setting")))
{
const GPUDevice::AdapterInfo* selected_adapter = nullptr;
if (current_adapter.has_value())
if (current_adapter.has_value() && !current_adapter->empty())
{
for (const GPUDevice::AdapterInfo& ai : s_settings_locals.graphics_adapter_list_cache)
{
@@ -4067,7 +4070,10 @@ void FullscreenUI::DrawGraphicsSettingsPage()
else
bsi->SetStringValue("GPU", "FullscreenMode", value);
SetSettingsChanged(bsi);
ShowToast(OSDMessageType::Info, std::string(), FSUI_STR("Resolution change will be applied after restarting."));
Host::RunOnCoreThread([]() {
if (VideoThread::IsFullscreen())
VideoThread::RecreateRenderWindow();
});
};
OpenChoiceDialog(FSUI_ICONVSTR(ICON_FA_TV, "Fullscreen Resolution"), false, std::move(options),
std::move(callback));

View File

@@ -611,7 +611,6 @@ TRANSLATE_NOOP("FullscreenUI", "Reset Settings");
TRANSLATE_NOOP("FullscreenUI", "Resets all configuration to defaults (including bindings).");
TRANSLATE_NOOP("FullscreenUI", "Resets all settings to the defaults.");
TRANSLATE_NOOP("FullscreenUI", "Resets memory card directory to default (user directory).");
TRANSLATE_NOOP("FullscreenUI", "Resolution change will be applied after restarting.");
TRANSLATE_NOOP("FullscreenUI", "Restart Game");
TRANSLATE_NOOP("FullscreenUI", "Restore Defaults");
TRANSLATE_NOOP("FullscreenUI", "Resume Game");

View File

@@ -64,10 +64,9 @@ namespace MiniHost {
/// Use two async worker threads, should be enough for most tasks.
static constexpr u32 NUM_ASYNC_WORKER_THREADS = 2;
// static constexpr u32 DEFAULT_WINDOW_WIDTH = 1280;
// static constexpr u32 DEFAULT_WINDOW_HEIGHT = 720;
static constexpr u32 DEFAULT_WINDOW_WIDTH = 1920;
static constexpr u32 DEFAULT_WINDOW_HEIGHT = 1080;
static constexpr float DEFAULT_WINDOW_REFRESH_RATE = 60.0f;
static constexpr auto CORE_THREAD_POLL_INTERVAL =
std::chrono::milliseconds(8); // how often we'll poll controllers when paused
@@ -111,6 +110,7 @@ struct SDLHostState
float sdl_window_scale = 0.0f;
float sdl_window_refresh_rate = 0.0f;
WindowInfoPrerotation force_prerotation = WindowInfoPrerotation::Identity;
bool use_window_fullscreen_mode = false;
Threading::Thread core_thread;
Threading::Thread video_thread;
@@ -161,6 +161,23 @@ bool MiniHost::EarlyProcessStartup()
return false;
}
s_state.func_event_id = SDL_RegisterEvents(1);
if (s_state.func_event_id == static_cast<u32>(-1))
{
Host::ReportFatalError("Error", TinyString::from_format("SDL_RegisterEvents() failed: {}", SDL_GetError()));
return false;
}
#ifdef __linux__
// If we're running without a windowing system, force fullscreen.
if (const char* video_driver = SDL_GetCurrentVideoDriver(); video_driver && std::strcmp(video_driver, "kmsdrm") == 0)
{
fprintf(stderr, "kmsdrm video driver detected, using fullscreen display mode for windows.\n");
s_state.use_window_fullscreen_mode = true;
s_state.start_fullscreen_ui_fullscreen = true;
}
#endif
#if !__has_include("scmversion/tag.h")
//
// To those distributing their own builds or packages of DuckStation, and seeing this message:
@@ -358,7 +375,19 @@ std::optional<WindowInfo> MiniHost::TranslateSDLWindowInfo(SDL_Window* win, Erro
int window_px_width = 1, window_px_height = 1;
SDL_GetWindowSize(win, &window_width, &window_height);
SDL_GetWindowSizeInPixels(win, &window_px_width, &window_px_height);
s_state.sdl_window_scale = SDL_GetWindowDisplayScale(win);
// For kmsdrm, assume 720p is 100% scale. Jank, but no other way to do it outside of querying the display size..
if (s_state.use_window_fullscreen_mode)
{
s_state.sdl_window_scale = std::clamp(std::min(window_px_width / 1280.0f, window_px_height / 720.0f), 1.0f, 4.0f);
INFO_LOG("Using window fullscreen display mode, setting scale to {}", s_state.sdl_window_scale);
}
else
{
s_state.sdl_window_scale = SDL_GetWindowDisplayScale(win);
INFO_LOG("Using window scale of {} (window size {}x{}, pixel size {}x{})", s_state.sdl_window_scale, window_width,
window_height, window_px_width, window_px_height);
}
const SDL_DisplayMode* dispmode = nullptr;
@@ -494,24 +523,61 @@ std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool f
else if (render_api == RenderAPI::Vulkan)
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, true);
if (fullscreen)
{
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);
}
std::optional<SDL_DisplayMode> window_fullscreen_mode;
if (s32 window_x, window_y, window_width, window_height;
MiniHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height))
if (!s_state.use_window_fullscreen_mode)
{
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, window_x);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, window_y);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, window_width);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, window_height);
if (fullscreen)
{
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);
}
if (s32 window_x, window_y, window_width, window_height;
MiniHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height))
{
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, window_x);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, window_y);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, window_width);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, window_height);
}
else
{
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, DEFAULT_WINDOW_WIDTH);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, DEFAULT_WINDOW_HEIGHT);
}
}
else
{
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, DEFAULT_WINDOW_WIDTH);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, DEFAULT_WINDOW_HEIGHT);
std::optional<GPUDevice::ExclusiveFullscreenMode> fullscreen_mode =
GPUDevice::ExclusiveFullscreenMode::Parse(Core::GetTinyStringSettingValue("GPU", "FullscreenMode"));
if (!fullscreen_mode.has_value())
{
INFO_LOG("Using default fullscreen mode ({}x{})", DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT);
fullscreen_mode =
GPUDevice::ExclusiveFullscreenMode{DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_REFRESH_RATE};
}
window_fullscreen_mode.emplace();
if (SDL_GetClosestFullscreenDisplayMode(SDL_GetPrimaryDisplay(), fullscreen_mode->width, fullscreen_mode->height,
fullscreen_mode->refresh_rate, true, &window_fullscreen_mode.value()))
{
INFO_LOG("Requesting window fullscreen display mode {}x{} @ {} hz (closest match: {}x{} @ {} hz)",
fullscreen_mode->width, fullscreen_mode->height, fullscreen_mode->refresh_rate,
window_fullscreen_mode->w, window_fullscreen_mode->h, window_fullscreen_mode->refresh_rate);
// For GL we need to set the window size to the display mode. Annoyingly can't set the refresh rate...
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, window_fullscreen_mode->w);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, window_fullscreen_mode->h);
}
else
{
ERROR_LOG("SDL_GetClosestFullscreenDisplayMode() failed: {}", SDL_GetError());
window_fullscreen_mode.reset();
}
// Force fullscreen.
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);
}
s_state.sdl_window = SDL_CreateWindowWithProperties(props);
@@ -519,6 +585,12 @@ std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool f
if (s_state.sdl_window)
{
if (window_fullscreen_mode.has_value() &&
!SDL_SetWindowFullscreenMode(s_state.sdl_window, &window_fullscreen_mode.value()))
{
ERROR_LOG("SDL_SetWindowFullscreenMode() failed: {}", SDL_GetError());
}
wi = TranslateSDLWindowInfo(s_state.sdl_window, error);
if (!wi.has_value())
{
@@ -1698,13 +1770,6 @@ int main(int argc, char* argv[])
return EXIT_FAILURE;
}
s_state.func_event_id = SDL_RegisterEvents(1);
if (s_state.func_event_id == static_cast<u32>(-1))
{
Host::ReportFatalError("Error", TinyString::from_format("SDL_RegisterEvents() failed: {}", SDL_GetError()));
return EXIT_FAILURE;
}
if (!EarlyProcessStartup())
return EXIT_FAILURE;

View File

@@ -299,6 +299,10 @@ if(NOT APPLE)
set(RCC_FILE "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/resources/duckstation-qt.rcc")
qt_add_binary_resources(duckstation-qt-rcc resources/duckstation-qt.qrc DESTINATION ${RCC_FILE} OPTIONS -no-compress)
add_dependencies(duckstation-qt duckstation-qt-rcc)
# Need to ensure the resources directory exists, it might not. Happens when low CPU count and parallel builds.
add_custom_target(duckstation-qt-rcc-mkdir COMMAND ${CMAKE_COMMAND} -E create_directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/resources")
add_dependencies(duckstation-qt-rcc duckstation-qt-rcc-mkdir)
else()
set(RCC_FILE "${CMAKE_CURRENT_BINARY_DIR}/duckstation-qt.rcc")
qt_add_binary_resources(duckstation-qt-rcc resources/duckstation-qt.qrc DESTINATION ${RCC_FILE} OPTIONS -no-compress)

View File

@@ -683,6 +683,9 @@ void GraphicsSettingsWidget::populateGPUAdaptersAndResolutions(RenderAPI render_
const std::string current_adapter_name = m_dialog->getEffectiveStringValue("GPU", "Adapter", "");
for (const GPUDevice::AdapterInfo& adapter : m_adapters)
{
if (adapter.name.empty())
continue;
const QString qadaptername = QString::fromStdString(adapter.name);
m_ui.adapter->addItem(qadaptername, QVariant(qadaptername));
if (adapter.name == current_adapter_name)

View File

@@ -200,6 +200,7 @@ if(NOT ANDROID)
sdl_audio_stream.cpp
sdl_input_source.cpp
sdl_input_source.h
sdl_video_helpers.h
)
if(ENABLE_OPENGL)
target_sources(util PRIVATE

View File

@@ -37,6 +37,7 @@ LOG_CHANNEL(GPUDevice);
#endif
#ifdef ENABLE_OPENGL
#include "opengl_context.h"
#include "opengl_device.h"
#endif
@@ -394,8 +395,7 @@ std::optional<GPUDevice::AdapterInfoList> GPUDevice::GetAdapterListForAPI(Render
#ifdef ENABLE_OPENGL
case RenderAPI::OpenGL:
case RenderAPI::OpenGLES:
// No way of querying.
ret = AdapterInfoList();
ret = OpenGLContext::GetAdapterList(window_type, error);
break;
#endif

View File

@@ -210,3 +210,13 @@ std::unique_ptr<OpenGLContext> OpenGLContext::Create(WindowInfo& wi, SurfaceHand
return context;
}
GPUDevice::AdapterInfoList OpenGLContext::GetAdapterList(WindowInfoType window_type, Error* error)
{
#ifdef ENABLE_SDL
if (window_type == WindowInfoType::SDL)
return OpenGLContextSDL::GetAdapterList(window_type, error);
#endif
return {};
}

View File

@@ -3,6 +3,7 @@
#pragma once
#include "gpu_device.h"
#include "window_info.h"
#include "common/types.h"
@@ -52,6 +53,8 @@ public:
static std::unique_ptr<OpenGLContext> Create(WindowInfo& wi, SurfaceHandle* surface, bool prefer_gles_context,
Error* error);
static GPUDevice::AdapterInfoList GetAdapterList(WindowInfoType window_type, Error* error);
protected:
Version m_version = {};
};

View File

@@ -1,16 +1,17 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "opengl_context_sdl.h"
#include "gpu_texture.h"
#include "opengl_loader.h"
#include "sdl_video_helpers.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/log.h"
#include "common/scoped_guard.h"
#include "SDL3/SDL.h"
#include <SDL3/SDL.h>
LOG_CHANNEL(GPUDevice);
@@ -35,6 +36,26 @@ std::unique_ptr<OpenGLContext> OpenGLContextSDL::Create(WindowInfo& wi, SurfaceH
return context;
}
GPUDevice::AdapterInfoList OpenGLContextSDL::GetAdapterList(WindowInfoType window_type, Error* error)
{
std::vector<GPUDevice::ExclusiveFullscreenMode> fullscreen_modes = SDLVideoHelpers::GetFullscreenModeList();
if (fullscreen_modes.empty())
{
// no point adding anything if no modes
return {};
}
// Set some reasonable defaults, since we don't know this until we actually create the context.
GPUDevice::AdapterInfoList ret;
GPUDevice::AdapterInfo& ai = ret.emplace_back();
ai.driver_type = GPUDriverType::Unknown;
ai.max_multisamples = 8;
ai.supports_sample_shading = true;
ai.max_texture_size = 16384;
ai.fullscreen_modes = std::move(fullscreen_modes);
return ret;
}
bool OpenGLContextSDL::Initialize(WindowInfo& wi, SurfaceHandle* surface, std::span<const Version> versions_to_try,
bool share_context, Error* error)
{

View File

@@ -19,6 +19,7 @@ public:
static std::unique_ptr<OpenGLContext> Create(WindowInfo& wi, SurfaceHandle* surface,
std::span<const Version> versions_to_try, Error* error);
static GPUDevice::AdapterInfoList GetAdapterList(WindowInfoType window_type, Error* error);
void* GetProcAddress(const char* name) override;
SurfaceHandle CreateSurface(WindowInfo& wi, Error* error = nullptr) override;

View File

@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "gpu_device.h"
#include "common/error.h"
#include "common/log.h"
#include <SDL3/SDL.h>
#include <algorithm>
namespace SDLVideoHelpers {
inline std::vector<GPUDevice::ExclusiveFullscreenMode> GetFullscreenModeList()
{
int display_count = 0;
const SDL_DisplayID* const displays = SDL_GetDisplays(&display_count);
if (display_count <= 0)
{
GENERIC_LOG(Log::Channel::SDL, Log::Level::Error, Log::Color::Default, "SDL_GetDisplays() returned no displays: {}",
SDL_GetError());
return {};
}
std::vector<GPUDevice::ExclusiveFullscreenMode> modes;
for (int i = 0; i < display_count; i++)
{
int dm_count = 0;
const SDL_DisplayMode* const* const dms = SDL_GetFullscreenDisplayModes(displays[i], &dm_count);
if (dm_count <= 0)
{
GENERIC_LOG(Log::Channel::SDL, Log::Level::Error, Log::Color::Default,
"SDL_GetFullscreenDisplayModes() returned no modes for display {}: {}", displays[i], SDL_GetError());
continue;
}
modes.reserve(modes.size() + static_cast<size_t>(dm_count));
for (int j = 0; j < dm_count; j++)
{
const SDL_DisplayMode* const dm = dms[j];
const GPUDevice::ExclusiveFullscreenMode mode{static_cast<u32>(dm->w), static_cast<u32>(dm->h), dm->refresh_rate};
if (std::ranges::find(modes, mode) == modes.end())
modes.push_back(mode);
}
}
return modes;
}
} // namespace SDLVideoHelpers

View File

@@ -92,6 +92,7 @@
<ClInclude Include="postprocessing_shader_fx.h" />
<ClInclude Include="postprocessing_shader_glsl.h" />
<ClInclude Include="postprocessing_shader_slang.h" />
<ClInclude Include="sdl_video_helpers.h" />
<ClInclude Include="sdl_input_source.h" />
<ClInclude Include="shadergen.h" />
<ClInclude Include="shiftjis.h" />

View File

@@ -82,6 +82,7 @@
<ClInclude Include="core_audio_stream.h" />
<ClInclude Include="vulkan_headers.h" />
<ClInclude Include="gpu_types.h" />
<ClInclude Include="sdl_video_helpers.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="state_wrapper.cpp" />

View File

@@ -16,6 +16,7 @@
#include "common/log.h"
#ifdef ENABLE_SDL
#include "sdl_video_helpers.h"
#include <SDL3/SDL_vulkan.h>
#endif
@@ -58,6 +59,8 @@ static void LockedDestroyVulkanInstance();
static bool SelectInstanceExtensions(VulkanDevice::ExtensionList* extension_list, WindowInfoType wtype,
bool debug_instance, Error* error);
static std::vector<GPUDevice::ExclusiveFullscreenMode> EnumerateFullscreenModes(WindowInfoType wtype);
VKAPI_ATTR static VkBool32 VKAPI_CALL DebugMessengerCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity,
VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
@@ -597,6 +600,12 @@ void VulkanLoader::LockedReleaseVulkanInstance()
// We specifically keep the instance around even after releasing it.
// Both AMD on Windows and Mesa leak a few tens of megabytes for every instance...
DEV_LOG("Released Vulkan instance, reference count {}", s_locals.reference_count);
#ifdef ENABLE_SDL
// SDL Vulkan kinda breaks OpenGL contexts if the instance isn't destroyed...
if (s_locals.window_type == WindowInfoType::SDL && s_locals.reference_count == 0)
LockedDestroyVulkanInstance();
#endif
}
void VulkanLoader::LockedDestroyVulkanInstance()
@@ -735,6 +744,16 @@ VulkanLoader::GPUList VulkanLoader::EnumerateGPUs(Error* error)
return gpus;
}
std::vector<GPUDevice::ExclusiveFullscreenMode> VulkanLoader::EnumerateFullscreenModes(WindowInfoType wtype)
{
#ifdef ENABLE_SDL
if (wtype == WindowInfoType::SDL)
return SDLVideoHelpers::GetFullscreenModeList();
#endif
return {};
}
bool VulkanLoader::IsSuitableDefaultRenderer(WindowInfoType window_type)
{
#ifdef __ANDROID__
@@ -834,6 +853,7 @@ GPUDriverType VulkanLoader::GuessDriverType(const VkPhysicalDeviceProperties& de
std::optional<GPUDevice::AdapterInfoList> VulkanLoader::GetAdapterList(WindowInfoType window_type, Error* error)
{
std::optional<GPUDevice::AdapterInfoList> ret;
std::vector<GPUDevice::ExclusiveFullscreenMode> fullscreen_modes;
GPUList gpus;
{
const std::lock_guard lock(s_locals.mutex);
@@ -842,6 +862,7 @@ std::optional<GPUDevice::AdapterInfoList> VulkanLoader::GetAdapterList(WindowInf
if (s_locals.instance != VK_NULL_HANDLE)
{
gpus = EnumerateGPUs(error);
fullscreen_modes = EnumerateFullscreenModes(window_type);
}
else
{
@@ -851,6 +872,7 @@ std::optional<GPUDevice::AdapterInfoList> VulkanLoader::GetAdapterList(WindowInf
return ret;
gpus = EnumerateGPUs(error);
fullscreen_modes = EnumerateFullscreenModes(window_type);
LockedReleaseVulkanInstance();
}
@@ -858,8 +880,18 @@ std::optional<GPUDevice::AdapterInfoList> VulkanLoader::GetAdapterList(WindowInf
ret.emplace();
ret->reserve(gpus.size());
for (auto& [physical_device, adapter_info] : gpus)
ret->push_back(std::move(adapter_info));
for (size_t i = 0; i < gpus.size(); i++)
{
GPUDevice::AdapterInfo& ai = gpus[i].second;
// splat fullscreen modes across gpus
if (i == (gpus.size() - 1))
ai.fullscreen_modes = std::move(fullscreen_modes);
else
ai.fullscreen_modes = fullscreen_modes;
ret->push_back(std::move(ai));
}
return ret;
}