Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/duckstation-mini/mini_host.cpp
4802 views
1
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "scmversion/scmversion.h"
5
6
#include "core/achievements.h"
7
#include "core/bus.h"
8
#include "core/controller.h"
9
#include "core/fullscreen_ui.h"
10
#include "core/game_list.h"
11
#include "core/gpu.h"
12
#include "core/gpu_backend.h"
13
#include "core/gpu_thread.h"
14
#include "core/host.h"
15
#include "core/imgui_overlays.h"
16
#include "core/settings.h"
17
#include "core/system.h"
18
#include "core/system_private.h"
19
20
#include "util/gpu_device.h"
21
#include "util/imgui_fullscreen.h"
22
#include "util/imgui_manager.h"
23
#include "util/ini_settings_interface.h"
24
#include "util/input_manager.h"
25
#include "util/platform_misc.h"
26
#include "util/sdl_input_source.h"
27
28
#include "imgui.h"
29
#include "imgui_internal.h"
30
#include "imgui_stdlib.h"
31
32
#include "common/assert.h"
33
#include "common/crash_handler.h"
34
#include "common/error.h"
35
#include "common/file_system.h"
36
#include "common/log.h"
37
#include "common/path.h"
38
#include "common/string_util.h"
39
#include "common/threading.h"
40
#include "common/time_helpers.h"
41
42
#include "IconsEmoji.h"
43
#include "fmt/format.h"
44
45
#include <SDL3/SDL.h>
46
#include <cinttypes>
47
#include <cmath>
48
#include <condition_variable>
49
#include <csignal>
50
#include <ctime>
51
#include <thread>
52
53
LOG_CHANNEL(Host);
54
55
namespace MiniHost {
56
57
/// Use two async worker threads, should be enough for most tasks.
58
static constexpr u32 NUM_ASYNC_WORKER_THREADS = 2;
59
60
// static constexpr u32 DEFAULT_WINDOW_WIDTH = 1280;
61
// static constexpr u32 DEFAULT_WINDOW_HEIGHT = 720;
62
static constexpr u32 DEFAULT_WINDOW_WIDTH = 1920;
63
static constexpr u32 DEFAULT_WINDOW_HEIGHT = 1080;
64
65
static constexpr u32 SETTINGS_VERSION = 3;
66
static constexpr auto CPU_THREAD_POLL_INTERVAL =
67
std::chrono::milliseconds(8); // how often we'll poll controllers when paused
68
69
static bool ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[],
70
std::optional<SystemBootParameters>& autoboot);
71
static void PrintCommandLineVersion();
72
static void PrintCommandLineHelp(const char* progname);
73
static bool InitializeConfig();
74
static void InitializeEarlyConsole();
75
static void HookSignals();
76
static void SetAppRoot();
77
static void SetResourcesDirectory();
78
static bool SetDataDirectory();
79
static bool SetCriticalFolders();
80
static void SetDefaultSettings(SettingsInterface& si, bool system, bool controller);
81
static std::string GetResourcePath(std::string_view name, bool allow_override);
82
static bool PerformEarlyHardwareChecks();
83
static bool EarlyProcessStartup();
84
static void WarnAboutInterface();
85
static void StartCPUThread();
86
static void StopCPUThread();
87
static void ProcessCPUThreadEvents(bool block);
88
static void ProcessCPUThreadPlatformMessages();
89
static void CPUThreadEntryPoint();
90
static void CPUThreadMainLoop();
91
static void GPUThreadEntryPoint();
92
static void UIThreadMainLoop();
93
static void ProcessSDLEvent(const SDL_Event* ev);
94
static std::string GetWindowTitle(const std::string& game_title);
95
static std::optional<WindowInfo> TranslateSDLWindowInfo(SDL_Window* win, Error* error);
96
static bool GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height);
97
static void SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height);
98
99
struct SDLHostState
100
{
101
// UI thread state
102
ALIGN_TO_CACHE_LINE INISettingsInterface base_settings_interface;
103
bool batch_mode = false;
104
bool start_fullscreen_ui_fullscreen = false;
105
bool was_paused_by_focus_loss = false;
106
bool ui_thread_running = false;
107
108
u32 func_event_id = 0;
109
110
SDL_Window* sdl_window = nullptr;
111
float sdl_window_scale = 0.0f;
112
WindowInfo::PreRotation force_prerotation = WindowInfo::PreRotation::Identity;
113
std::atomic_bool fullscreen{false};
114
115
Threading::Thread cpu_thread;
116
Threading::Thread gpu_thread;
117
Threading::KernelSemaphore platform_window_updated;
118
119
std::mutex state_mutex;
120
FullscreenUI::BackgroundProgressCallback* game_list_refresh_progress = nullptr;
121
122
// CPU thread state.
123
ALIGN_TO_CACHE_LINE std::atomic_bool cpu_thread_running{false};
124
std::mutex cpu_thread_events_mutex;
125
std::condition_variable cpu_thread_event_done;
126
std::condition_variable cpu_thread_event_posted;
127
std::deque<std::pair<std::function<void()>, bool>> cpu_thread_events;
128
u32 blocking_cpu_events_pending = 0;
129
};
130
131
static SDLHostState s_state;
132
} // namespace MiniHost
133
134
//////////////////////////////////////////////////////////////////////////
135
// Initialization/Shutdown
136
//////////////////////////////////////////////////////////////////////////
137
138
bool MiniHost::PerformEarlyHardwareChecks()
139
{
140
Error error;
141
const bool okay = System::PerformEarlyHardwareChecks(&error);
142
if (okay && !error.IsValid()) [[likely]]
143
return true;
144
145
if (okay)
146
Host::ReportErrorAsync("Hardware Check Warning", error.GetDescription());
147
else
148
Host::ReportFatalError("Hardware Check Failed", error.GetDescription());
149
150
return okay;
151
}
152
153
bool MiniHost::EarlyProcessStartup()
154
{
155
Error error;
156
if (!System::ProcessStartup(&error)) [[unlikely]]
157
{
158
Host::ReportFatalError("Process Startup Failed", error.GetDescription());
159
return false;
160
}
161
162
#if !__has_include("scmversion/tag.h")
163
//
164
// To those distributing their own builds or packages of DuckStation, and seeing this message:
165
//
166
// DuckStation is licensed under the CC-BY-NC-ND-4.0 license.
167
//
168
// This means that you do NOT have permission to re-distribute your own modified builds of DuckStation.
169
// Modifying DuckStation for personal use is fine, but you cannot distribute builds with your changes.
170
// As per the CC-BY-NC-ND conditions, you can re-distribute the official builds from https://www.duckstation.org/ and
171
// https://github.com/stenzek/duckstation, so long as they are left intact, without modification. I welcome and
172
// appreciate any pull requests made to the official repository at https://github.com/stenzek/duckstation.
173
//
174
// I made the decision to switch to a no-derivatives license because of numerous "forks" that were created purely for
175
// generating money for the person who knocked it off, and always died, leaving the community with multiple builds to
176
// choose from, most of which were out of date and broken, and endless confusion. Other forks copy/pasted upstream
177
// changes without attribution, violating copyright.
178
//
179
// Thanks, and I hope you understand.
180
//
181
182
const char* message = ICON_EMOJI_WARNING "WARNING! You are not using an official release! " ICON_EMOJI_WARNING "\n\n"
183
"DuckStation is licensed under the terms of CC-BY-NC-ND-4.0,\n"
184
"which does not allow modified builds to be distributed.\n\n"
185
"This build is NOT OFFICIAL and may be broken and/or malicious.\n\n"
186
"You should download an official build from https://www.duckstation.org/.";
187
188
Host::AddKeyedOSDWarning("OfficialReleaseWarning", message, Host::OSD_CRITICAL_ERROR_DURATION);
189
#endif
190
191
return true;
192
}
193
194
bool MiniHost::SetCriticalFolders()
195
{
196
SetAppRoot();
197
SetResourcesDirectory();
198
if (!SetDataDirectory())
199
return false;
200
201
// logging of directories in case something goes wrong super early
202
DEV_LOG("AppRoot Directory: {}", EmuFolders::AppRoot);
203
DEV_LOG("DataRoot Directory: {}", EmuFolders::DataRoot);
204
DEV_LOG("Resources Directory: {}", EmuFolders::Resources);
205
206
// Write crash dumps to the data directory, since that'll be accessible for certain.
207
CrashHandler::SetWriteDirectory(EmuFolders::DataRoot);
208
209
// the resources directory should exist, bail out if not
210
if (!FileSystem::DirectoryExists(EmuFolders::Resources.c_str()))
211
{
212
Host::ReportFatalError("Error", "Resources directory is missing, your installation is incomplete.");
213
return false;
214
}
215
216
return true;
217
}
218
219
void MiniHost::SetAppRoot()
220
{
221
const std::string program_path = FileSystem::GetProgramPath();
222
INFO_LOG("Program Path: {}", program_path);
223
224
EmuFolders::AppRoot = Path::Canonicalize(Path::GetDirectory(program_path));
225
}
226
227
void MiniHost::SetResourcesDirectory()
228
{
229
#ifndef __APPLE__
230
// On Windows/Linux, these are in the binary directory.
231
EmuFolders::Resources = Path::Combine(EmuFolders::AppRoot, "resources");
232
#else
233
// On macOS, this is in the bundle resources directory.
234
EmuFolders::Resources = Path::Canonicalize(Path::Combine(EmuFolders::AppRoot, "../Resources"));
235
#endif
236
}
237
238
bool MiniHost::SetDataDirectory()
239
{
240
EmuFolders::DataRoot = Host::Internal::ComputeDataDirectory();
241
242
// make sure it exists
243
if (!EmuFolders::DataRoot.empty() && !FileSystem::DirectoryExists(EmuFolders::DataRoot.c_str()))
244
{
245
// we're in trouble if we fail to create this directory... but try to hobble on with portable
246
Error error;
247
if (!FileSystem::EnsureDirectoryExists(EmuFolders::DataRoot.c_str(), false, &error))
248
{
249
Host::ReportFatalError("Error",
250
TinyString::from_format("Failed to create data directory: {}", error.GetDescription()));
251
return false;
252
}
253
}
254
255
// couldn't determine the data directory? fallback to portable.
256
if (EmuFolders::DataRoot.empty())
257
EmuFolders::DataRoot = EmuFolders::AppRoot;
258
259
return true;
260
}
261
262
bool MiniHost::InitializeConfig()
263
{
264
if (!SetCriticalFolders())
265
return false;
266
267
std::string settings_path = Path::Combine(EmuFolders::DataRoot, "settings.ini");
268
const bool settings_exists = FileSystem::FileExists(settings_path.c_str());
269
INFO_LOG("Loading config from {}.", settings_path);
270
s_state.base_settings_interface.SetPath(std::move(settings_path));
271
Host::Internal::SetBaseSettingsLayer(&s_state.base_settings_interface);
272
273
u32 settings_version;
274
if (!settings_exists || !s_state.base_settings_interface.Load() ||
275
!s_state.base_settings_interface.GetUIntValue("Main", "SettingsVersion", &settings_version) ||
276
settings_version != SETTINGS_VERSION)
277
{
278
if (s_state.base_settings_interface.ContainsValue("Main", "SettingsVersion"))
279
{
280
// NOTE: No point translating this, because there's no config loaded, so no language loaded.
281
Host::ReportErrorAsync("Error", fmt::format("Settings version {} does not match expected version {}, resetting.",
282
settings_version, SETTINGS_VERSION));
283
}
284
285
s_state.base_settings_interface.SetUIntValue("Main", "SettingsVersion", SETTINGS_VERSION);
286
SetDefaultSettings(s_state.base_settings_interface, true, true);
287
288
// Make sure we can actually save the config, and the user doesn't have some permission issue.
289
Error error;
290
if (!s_state.base_settings_interface.Save(&error))
291
{
292
Host::ReportFatalError(
293
"Error",
294
fmt::format(
295
"Failed to save configuration to\n\n{}\n\nThe error was: {}\n\nPlease ensure this directory is writable. You "
296
"can also try portable mode by creating portable.txt in the same directory you installed DuckStation into.",
297
s_state.base_settings_interface.GetPath(), error.GetDescription()));
298
return false;
299
}
300
}
301
302
EmuFolders::LoadConfig(s_state.base_settings_interface);
303
EmuFolders::EnsureFoldersExist();
304
305
// We need to create the console window early, otherwise it appears in front of the main window.
306
if (!Log::IsConsoleOutputEnabled() && s_state.base_settings_interface.GetBoolValue("Logging", "LogToConsole", false))
307
Log::SetConsoleOutputParams(true, s_state.base_settings_interface.GetBoolValue("Logging", "LogTimestamps", true));
308
309
return true;
310
}
311
312
void MiniHost::SetDefaultSettings(SettingsInterface& si, bool system, bool controller)
313
{
314
if (system)
315
{
316
System::SetDefaultSettings(si);
317
EmuFolders::SetDefaults();
318
EmuFolders::Save(si);
319
}
320
321
if (controller)
322
{
323
InputManager::SetDefaultSourceConfig(si);
324
Settings::SetDefaultControllerConfig(si);
325
Settings::SetDefaultHotkeyConfig(si);
326
}
327
}
328
329
void Host::ReportDebuggerMessage(std::string_view message)
330
{
331
ERROR_LOG("ReportDebuggerMessage(): {}", message);
332
}
333
334
std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()
335
{
336
return {};
337
}
338
339
const char* Host::GetLanguageName(std::string_view language_code)
340
{
341
return "";
342
}
343
344
bool Host::ChangeLanguage(const char* new_language)
345
{
346
return false;
347
}
348
349
void Host::AddFixedInputBindings(const SettingsInterface& si)
350
{
351
}
352
353
void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)
354
{
355
Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier),
356
fmt::format("Input device {0} ({1}) connected.", device_name, identifier), 10.0f);
357
}
358
359
void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)
360
{
361
Host::AddKeyedOSDMessage(fmt::format("InputDeviceConnected-{}", identifier),
362
fmt::format("Input device {} disconnected.", identifier), 10.0f);
363
}
364
365
s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,
366
std::string_view disambiguation, char* tbuf, size_t tbuf_space)
367
{
368
if (msg.size() > tbuf_space)
369
return -1;
370
else if (msg.empty())
371
return 0;
372
373
std::memcpy(tbuf, msg.data(), msg.size());
374
return static_cast<s32>(msg.size());
375
}
376
377
std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)
378
{
379
TinyString count_str = TinyString::from_format("{}", count);
380
381
std::string ret(msg);
382
for (;;)
383
{
384
std::string::size_type pos = ret.find("%n");
385
if (pos == std::string::npos)
386
break;
387
388
ret.replace(pos, pos + 2, count_str.view());
389
}
390
391
return ret;
392
}
393
394
SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,
395
int count)
396
{
397
SmallString ret(msg);
398
ret.replace("%n", TinyString::from_format("{}", count));
399
return ret;
400
}
401
402
std::string MiniHost::GetResourcePath(std::string_view filename, bool allow_override)
403
{
404
return allow_override ? EmuFolders::GetOverridableResourcePath(filename) :
405
Path::Combine(EmuFolders::Resources, filename);
406
}
407
408
bool Host::ResourceFileExists(std::string_view filename, bool allow_override)
409
{
410
const std::string path = MiniHost::GetResourcePath(filename, allow_override);
411
return FileSystem::FileExists(path.c_str());
412
}
413
414
std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)
415
{
416
const std::string path = MiniHost::GetResourcePath(filename, allow_override);
417
return FileSystem::ReadBinaryFile(path.c_str(), error);
418
}
419
420
std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)
421
{
422
const std::string path = MiniHost::GetResourcePath(filename, allow_override);
423
return FileSystem::ReadFileToString(path.c_str(), error);
424
}
425
426
std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)
427
{
428
const std::string path = MiniHost::GetResourcePath(filename, allow_override);
429
FILESYSTEM_STAT_DATA sd;
430
if (!FileSystem::StatFile(path.c_str(), &sd))
431
{
432
ERROR_LOG("Failed to stat resource file '{}'", filename);
433
return std::nullopt;
434
}
435
436
return sd.ModificationTime;
437
}
438
439
void Host::LoadSettings(const SettingsInterface& si, std::unique_lock<std::mutex>& lock)
440
{
441
}
442
443
void Host::CheckForSettingsChanges(const Settings& old_settings)
444
{
445
}
446
447
void Host::CommitBaseSettingChanges()
448
{
449
auto lock = Host::GetSettingsLock();
450
Error error;
451
if (!MiniHost::s_state.base_settings_interface.Save(&error))
452
ERROR_LOG("Failed to save settings: {}", error.GetDescription());
453
}
454
455
std::optional<WindowInfo> MiniHost::TranslateSDLWindowInfo(SDL_Window* win, Error* error)
456
{
457
if (!win)
458
{
459
Error::SetStringView(error, "Window handle is null.");
460
return std::nullopt;
461
}
462
463
const SDL_WindowFlags window_flags = SDL_GetWindowFlags(win);
464
int window_width = 1, window_height = 1;
465
int window_px_width = 1, window_px_height = 1;
466
SDL_GetWindowSize(win, &window_width, &window_height);
467
SDL_GetWindowSizeInPixels(win, &window_px_width, &window_px_height);
468
s_state.sdl_window_scale = SDL_GetWindowDisplayScale(win);
469
470
const SDL_DisplayMode* dispmode = nullptr;
471
472
if (window_flags & SDL_WINDOW_FULLSCREEN)
473
{
474
if (!(dispmode = SDL_GetWindowFullscreenMode(win)))
475
ERROR_LOG("SDL_GetWindowFullscreenMode() failed: {}", SDL_GetError());
476
}
477
478
if (const SDL_DisplayID display_id = SDL_GetDisplayForWindow(win); display_id != 0)
479
{
480
if (!(window_flags & SDL_WINDOW_FULLSCREEN))
481
{
482
if (!(dispmode = SDL_GetDesktopDisplayMode(display_id)))
483
ERROR_LOG("SDL_GetDesktopDisplayMode() failed: {}", SDL_GetError());
484
}
485
}
486
487
WindowInfo wi;
488
wi.surface_width = static_cast<u16>(window_px_width);
489
wi.surface_height = static_cast<u16>(window_px_height);
490
wi.surface_scale = s_state.sdl_window_scale;
491
wi.surface_prerotation = s_state.force_prerotation;
492
493
// set display refresh rate if available
494
if (dispmode && dispmode->refresh_rate > 0.0f)
495
{
496
INFO_LOG("Display mode refresh rate: {} hz", dispmode->refresh_rate);
497
wi.surface_refresh_rate = dispmode->refresh_rate;
498
}
499
500
// SDL's opengl window flag tends to make a mess of pixel formats...
501
if (!(SDL_GetWindowFlags(win) & (SDL_WINDOW_OPENGL | SDL_WINDOW_VULKAN)))
502
{
503
const SDL_PropertiesID props = SDL_GetWindowProperties(win);
504
if (props == 0)
505
{
506
Error::SetStringFmt(error, "SDL_GetWindowProperties() failed: {}", SDL_GetError());
507
return std::nullopt;
508
}
509
510
#if defined(SDL_PLATFORM_WINDOWS)
511
wi.type = WindowInfo::Type::Win32;
512
wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
513
if (!wi.window_handle)
514
{
515
Error::SetStringView(error, "SDL_PROP_WINDOW_WIN32_HWND_POINTER not found.");
516
return std::nullopt;
517
}
518
#elif defined(SDL_PLATFORM_MACOS)
519
wi.type = WindowInfo::Type::MacOS;
520
wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);
521
if (!wi.window_handle)
522
{
523
Error::SetStringView(error, "SDL_PROP_WINDOW_COCOA_WINDOW_POINTER not found.");
524
return std::nullopt;
525
}
526
#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_FREEBSD)
527
const std::string_view video_driver = SDL_GetCurrentVideoDriver();
528
if (video_driver == "x11")
529
{
530
wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);
531
wi.window_handle = reinterpret_cast<void*>(
532
static_cast<intptr_t>(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0)));
533
if (!wi.display_connection)
534
{
535
Error::SetStringView(error, "SDL_PROP_WINDOW_X11_DISPLAY_POINTER not found.");
536
return std::nullopt;
537
}
538
else if (!wi.window_handle)
539
{
540
Error::SetStringView(error, "SDL_PROP_WINDOW_X11_WINDOW_NUMBER not found.");
541
return std::nullopt;
542
}
543
}
544
else if (video_driver == "wayland")
545
{
546
wi.display_connection = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);
547
wi.window_handle = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);
548
if (!wi.display_connection)
549
{
550
Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER not found.");
551
return std::nullopt;
552
}
553
else if (!wi.window_handle)
554
{
555
Error::SetStringView(error, "SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER not found.");
556
return std::nullopt;
557
}
558
}
559
else
560
{
561
Error::SetStringFmt(error, "Video driver {} not supported.", video_driver);
562
return std::nullopt;
563
}
564
#else
565
#error Unsupported platform.
566
#endif
567
}
568
else
569
{
570
// nothing handled, fall back to SDL abstraction
571
wi.type = WindowInfo::Type::SDL;
572
wi.window_handle = win;
573
}
574
575
return wi;
576
}
577
578
std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,
579
Error* error)
580
{
581
using namespace MiniHost;
582
583
std::optional<WindowInfo> wi;
584
585
Host::RunOnUIThread([render_api, fullscreen, error, &wi]() {
586
const std::string window_title = GetWindowTitle(System::GetGameTitle());
587
const SDL_PropertiesID props = SDL_CreateProperties();
588
SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_title.c_str());
589
590
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
591
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN, true);
592
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
593
594
if (render_api == RenderAPI::OpenGL || render_api == RenderAPI::OpenGLES)
595
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
596
else if (render_api == RenderAPI::Vulkan)
597
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, true);
598
599
if (fullscreen)
600
{
601
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
602
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);
603
}
604
605
if (s32 window_x, window_y, window_width, window_height;
606
MiniHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height))
607
{
608
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, window_x);
609
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, window_y);
610
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, window_width);
611
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, window_height);
612
}
613
else
614
{
615
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, DEFAULT_WINDOW_WIDTH);
616
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, DEFAULT_WINDOW_HEIGHT);
617
}
618
619
s_state.sdl_window = SDL_CreateWindowWithProperties(props);
620
SDL_DestroyProperties(props);
621
622
if (s_state.sdl_window)
623
{
624
wi = TranslateSDLWindowInfo(s_state.sdl_window, error);
625
if (wi.has_value())
626
{
627
s_state.fullscreen.store(fullscreen, std::memory_order_release);
628
}
629
else
630
{
631
SDL_DestroyWindow(s_state.sdl_window);
632
s_state.sdl_window = nullptr;
633
}
634
}
635
else
636
{
637
Error::SetStringFmt(error, "SDL_CreateWindow() failed: {}", SDL_GetError());
638
}
639
640
s_state.platform_window_updated.Post();
641
});
642
643
s_state.platform_window_updated.Wait();
644
645
// reload input sources, since it might use the window handle
646
{
647
auto lock = Host::GetSettingsLock();
648
InputManager::ReloadSources(*Host::GetSettingsInterface(), lock);
649
}
650
651
return wi;
652
}
653
654
void Host::ReleaseRenderWindow()
655
{
656
using namespace MiniHost;
657
658
if (!s_state.sdl_window)
659
return;
660
661
Host::RunOnUIThread([]() {
662
if (!s_state.fullscreen.load(std::memory_order_acquire))
663
{
664
int window_x = SDL_WINDOWPOS_UNDEFINED, window_y = SDL_WINDOWPOS_UNDEFINED;
665
int window_width = DEFAULT_WINDOW_WIDTH, window_height = DEFAULT_WINDOW_HEIGHT;
666
SDL_GetWindowPosition(s_state.sdl_window, &window_x, &window_y);
667
SDL_GetWindowSize(s_state.sdl_window, &window_width, &window_height);
668
MiniHost::SavePlatformWindowGeometry(window_x, window_y, window_width, window_height);
669
}
670
else
671
{
672
s_state.fullscreen.store(false, std::memory_order_release);
673
}
674
675
SDL_DestroyWindow(s_state.sdl_window);
676
s_state.sdl_window = nullptr;
677
678
s_state.platform_window_updated.Post();
679
});
680
681
s_state.platform_window_updated.Wait();
682
}
683
684
bool Host::IsFullscreen()
685
{
686
using namespace MiniHost;
687
688
return s_state.fullscreen.load(std::memory_order_acquire);
689
}
690
691
void Host::SetFullscreen(bool enabled)
692
{
693
using namespace MiniHost;
694
695
if (!s_state.sdl_window || s_state.fullscreen.load(std::memory_order_acquire) == enabled)
696
return;
697
698
if (!SDL_SetWindowFullscreen(s_state.sdl_window, enabled))
699
{
700
ERROR_LOG("SDL_SetWindowFullscreen() failed: {}", SDL_GetError());
701
return;
702
}
703
704
s_state.fullscreen.store(enabled, std::memory_order_release);
705
}
706
707
void Host::BeginTextInput()
708
{
709
using namespace MiniHost;
710
711
SDL_StartTextInput(s_state.sdl_window);
712
}
713
714
void Host::EndTextInput()
715
{
716
// we want to keep getting text events, SDL_StopTextInput() apparently inhibits that
717
}
718
719
bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,
720
std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,
721
AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)
722
{
723
// not here, but could be...
724
Error::SetStringView(error, "Not supported.");
725
return false;
726
}
727
728
void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */,
729
s32* pos_y /* = nullptr */, u32* width /* = nullptr */,
730
u32* height /* = nullptr */)
731
{
732
// noop
733
}
734
735
bool MiniHost::GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height)
736
{
737
const auto lock = Host::GetSettingsLock();
738
739
bool result = s_state.base_settings_interface.GetIntValue("UI", "MainWindowX", x);
740
result = result && s_state.base_settings_interface.GetIntValue("UI", "MainWindowY", y);
741
result = result && s_state.base_settings_interface.GetIntValue("UI", "MainWindowWidth", width);
742
result = result && s_state.base_settings_interface.GetIntValue("UI", "MainWindowHeight", height);
743
return result;
744
}
745
746
void MiniHost::SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height)
747
{
748
if (Host::IsFullscreen())
749
return;
750
751
const auto lock = Host::GetSettingsLock();
752
s_state.base_settings_interface.SetIntValue("UI", "MainWindowX", x);
753
s_state.base_settings_interface.SetIntValue("UI", "MainWindowY", y);
754
s_state.base_settings_interface.SetIntValue("UI", "MainWindowWidth", width);
755
s_state.base_settings_interface.SetIntValue("UI", "MainWindowHeight", height);
756
}
757
758
void MiniHost::UIThreadMainLoop()
759
{
760
while (s_state.ui_thread_running)
761
{
762
SDL_Event ev;
763
if (!SDL_WaitEvent(&ev))
764
continue;
765
766
ProcessSDLEvent(&ev);
767
}
768
}
769
770
void MiniHost::ProcessSDLEvent(const SDL_Event* ev)
771
{
772
switch (ev->type)
773
{
774
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
775
{
776
Host::RunOnCPUThread(
777
[window_width = ev->window.data1, window_height = ev->window.data2, window_scale = s_state.sdl_window_scale]() {
778
GPUThread::ResizeDisplayWindow(window_width, window_height, window_scale);
779
});
780
}
781
break;
782
783
case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
784
case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
785
{
786
const float new_scale = SDL_GetWindowDisplayScale(s_state.sdl_window);
787
if (new_scale != s_state.sdl_window_scale)
788
{
789
s_state.sdl_window_scale = new_scale;
790
791
int window_width = 1, window_height = 1;
792
SDL_GetWindowSizeInPixels(s_state.sdl_window, &window_width, &window_height);
793
Host::RunOnCPUThread([window_width, window_height, window_scale = s_state.sdl_window_scale]() {
794
GPUThread::ResizeDisplayWindow(window_width, window_height, window_scale);
795
});
796
}
797
}
798
break;
799
800
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
801
{
802
Host::RunOnCPUThread([]() { Host::RequestExitApplication(false); });
803
}
804
break;
805
806
case SDL_EVENT_WINDOW_FOCUS_GAINED:
807
{
808
Host::RunOnCPUThread([]() {
809
if (!System::IsValid() || !s_state.was_paused_by_focus_loss)
810
return;
811
812
System::PauseSystem(false);
813
s_state.was_paused_by_focus_loss = false;
814
});
815
}
816
break;
817
818
case SDL_EVENT_WINDOW_FOCUS_LOST:
819
{
820
Host::RunOnCPUThread([]() {
821
if (!System::IsRunning() || !g_settings.pause_on_focus_loss)
822
return;
823
824
s_state.was_paused_by_focus_loss = true;
825
System::PauseSystem(true);
826
});
827
}
828
break;
829
830
case SDL_EVENT_KEY_DOWN:
831
case SDL_EVENT_KEY_UP:
832
{
833
if (const std::optional<u32> key = InputManager::ConvertHostNativeKeyCodeToKeyCode(ev->key.raw))
834
{
835
Host::RunOnCPUThread([key_code = key.value(), pressed = (ev->type == SDL_EVENT_KEY_DOWN)]() {
836
InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key_code), pressed ? 1.0f : 0.0f,
837
GenericInputBinding::Unknown);
838
});
839
}
840
}
841
break;
842
843
case SDL_EVENT_TEXT_INPUT:
844
{
845
if (ImGuiManager::WantsTextInput())
846
Host::RunOnCPUThread([text = std::string(ev->text.text)]() { ImGuiManager::AddTextInput(std::move(text)); });
847
}
848
break;
849
850
case SDL_EVENT_MOUSE_MOTION:
851
{
852
Host::RunOnCPUThread([x = static_cast<float>(ev->motion.x), y = static_cast<float>(ev->motion.y)]() {
853
InputManager::UpdatePointerAbsolutePosition(0, x, y);
854
ImGuiManager::UpdateMousePosition(x, y);
855
});
856
}
857
break;
858
859
case SDL_EVENT_MOUSE_BUTTON_DOWN:
860
case SDL_EVENT_MOUSE_BUTTON_UP:
861
{
862
if (ev->button.button > 0)
863
{
864
// swap middle/right because sdl orders them differently
865
const u8 button = (ev->button.button == 3) ? 1 : ((ev->button.button == 2) ? 2 : (ev->button.button - 1));
866
Host::RunOnCPUThread([button, pressed = (ev->type == SDL_EVENT_MOUSE_BUTTON_DOWN)]() {
867
InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button), pressed ? 1.0f : 0.0f,
868
GenericInputBinding::Unknown);
869
});
870
}
871
}
872
break;
873
874
case SDL_EVENT_MOUSE_WHEEL:
875
{
876
Host::RunOnCPUThread([x = ev->wheel.x, y = ev->wheel.y]() {
877
if (x != 0.0f)
878
InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelX, x);
879
if (y != 0.0f)
880
InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelY, y);
881
});
882
}
883
break;
884
885
case SDL_EVENT_QUIT:
886
{
887
Host::RunOnCPUThread([]() { Host::RequestExitApplication(false); });
888
}
889
break;
890
891
default:
892
{
893
if (ev->type == s_state.func_event_id)
894
{
895
std::function<void()>* pfunc = reinterpret_cast<std::function<void()>*>(ev->user.data1);
896
if (pfunc)
897
{
898
(*pfunc)();
899
delete pfunc;
900
}
901
}
902
else if (SDLInputSource::IsHandledInputEvent(ev))
903
{
904
Host::RunOnCPUThread([event_copy = *ev]() {
905
SDLInputSource* is =
906
static_cast<SDLInputSource*>(InputManager::GetInputSourceInterface(InputSourceType::SDL));
907
if (is)
908
is->ProcessSDLEvent(&event_copy);
909
});
910
}
911
}
912
break;
913
}
914
}
915
916
void MiniHost::ProcessCPUThreadPlatformMessages()
917
{
918
// This is lame. On Win32, we need to pump messages, even though *we* don't have any windows
919
// on the CPU thread, because SDL creates a hidden window for raw input for some game controllers.
920
// If we don't do this, we don't get any controller events.
921
#ifdef _WIN32
922
MSG msg;
923
while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE))
924
{
925
TranslateMessage(&msg);
926
DispatchMessageW(&msg);
927
}
928
#endif
929
}
930
931
void MiniHost::ProcessCPUThreadEvents(bool block)
932
{
933
std::unique_lock lock(s_state.cpu_thread_events_mutex);
934
935
for (;;)
936
{
937
if (s_state.cpu_thread_events.empty())
938
{
939
if (!block || !s_state.cpu_thread_running.load(std::memory_order_acquire))
940
return;
941
942
// we still need to keep polling the controllers when we're paused
943
do
944
{
945
ProcessCPUThreadPlatformMessages();
946
InputManager::PollSources();
947
} while (!s_state.cpu_thread_event_posted.wait_for(lock, CPU_THREAD_POLL_INTERVAL,
948
[]() { return !s_state.cpu_thread_events.empty(); }));
949
}
950
951
// return after processing all events if we had one
952
block = false;
953
954
auto event = std::move(s_state.cpu_thread_events.front());
955
s_state.cpu_thread_events.pop_front();
956
lock.unlock();
957
event.first();
958
lock.lock();
959
960
if (event.second)
961
{
962
s_state.blocking_cpu_events_pending--;
963
s_state.cpu_thread_event_done.notify_one();
964
}
965
}
966
}
967
968
void MiniHost::StartCPUThread()
969
{
970
s_state.cpu_thread_running.store(true, std::memory_order_release);
971
s_state.cpu_thread.Start(CPUThreadEntryPoint);
972
}
973
974
void MiniHost::StopCPUThread()
975
{
976
if (!s_state.cpu_thread.Joinable())
977
return;
978
979
{
980
std::unique_lock lock(s_state.cpu_thread_events_mutex);
981
s_state.cpu_thread_running.store(false, std::memory_order_release);
982
s_state.cpu_thread_event_posted.notify_one();
983
}
984
985
s_state.cpu_thread.Join();
986
}
987
988
void MiniHost::CPUThreadEntryPoint()
989
{
990
Threading::SetNameOfCurrentThread("CPU Thread");
991
992
// input source setup must happen on emu thread
993
Error error;
994
if (!System::CPUThreadInitialize(&error, NUM_ASYNC_WORKER_THREADS))
995
{
996
Host::ReportFatalError("CPU Thread Initialization Failed", error.GetDescription());
997
return;
998
}
999
1000
// start up GPU thread
1001
s_state.gpu_thread.Start(&GPUThreadEntryPoint);
1002
1003
// start the fullscreen UI and get it going
1004
if (GPUThread::StartFullscreenUI(s_state.start_fullscreen_ui_fullscreen, &error))
1005
{
1006
WarnAboutInterface();
1007
1008
// kick a game list refresh if we're not in batch mode
1009
if (!s_state.batch_mode)
1010
Host::RefreshGameListAsync(false);
1011
1012
CPUThreadMainLoop();
1013
1014
Host::CancelGameListRefresh();
1015
}
1016
else
1017
{
1018
Host::ReportFatalError("Error", fmt::format("Failed to start fullscreen UI: {}", error.GetDescription()));
1019
}
1020
1021
// finish any events off (e.g. shutdown system with save)
1022
ProcessCPUThreadEvents(false);
1023
1024
if (System::IsValid())
1025
System::ShutdownSystem(false);
1026
1027
GPUThread::StopFullscreenUI();
1028
GPUThread::Internal::RequestShutdown();
1029
s_state.gpu_thread.Join();
1030
1031
System::CPUThreadShutdown();
1032
1033
// Tell the UI thread to shut down.
1034
Host::RunOnUIThread([]() { s_state.ui_thread_running = false; });
1035
}
1036
1037
void MiniHost::CPUThreadMainLoop()
1038
{
1039
while (s_state.cpu_thread_running.load(std::memory_order_acquire))
1040
{
1041
if (System::IsRunning())
1042
{
1043
System::Execute();
1044
continue;
1045
}
1046
else if (!GPUThread::IsUsingThread() && GPUThread::IsRunningIdle())
1047
{
1048
ProcessCPUThreadEvents(false);
1049
if (!GPUThread::IsUsingThread() && GPUThread::IsRunningIdle())
1050
GPUThread::Internal::DoRunIdle();
1051
}
1052
1053
ProcessCPUThreadEvents(true);
1054
}
1055
}
1056
1057
void MiniHost::GPUThreadEntryPoint()
1058
{
1059
Threading::SetNameOfCurrentThread("GPU Thread");
1060
GPUThread::Internal::GPUThreadEntryPoint();
1061
}
1062
1063
void Host::OnSystemStarting()
1064
{
1065
MiniHost::s_state.was_paused_by_focus_loss = false;
1066
}
1067
1068
void Host::OnSystemStarted()
1069
{
1070
}
1071
1072
void Host::OnSystemPaused()
1073
{
1074
}
1075
1076
void Host::OnSystemResumed()
1077
{
1078
}
1079
1080
void Host::OnSystemStopping()
1081
{
1082
}
1083
1084
void Host::OnSystemDestroyed()
1085
{
1086
}
1087
1088
void Host::OnSystemAbnormalShutdown(const std::string_view reason)
1089
{
1090
GPUThread::RunOnThread([reason = std::string(reason)]() {
1091
ImGuiFullscreen::OpenInfoMessageDialog(
1092
"Abnormal System Shutdown", fmt::format("Unfortunately, the virtual machine has abnormally shut down and cannot "
1093
"be recovered. More information about the error is below:\n\n{}",
1094
reason));
1095
});
1096
}
1097
1098
void Host::OnGPUThreadRunIdleChanged(bool is_active)
1099
{
1100
}
1101
1102
void Host::FrameDoneOnGPUThread(GPUBackend* gpu_backend, u32 frame_number)
1103
{
1104
}
1105
1106
void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)
1107
{
1108
// noop
1109
}
1110
1111
void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)
1112
{
1113
// noop
1114
}
1115
1116
void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)
1117
{
1118
// noop
1119
}
1120
1121
void Host::OnAchievementsRefreshed()
1122
{
1123
// noop
1124
}
1125
1126
void Host::OnAchievementsActiveChanged(bool active)
1127
{
1128
// noop
1129
}
1130
1131
void Host::OnAchievementsHardcoreModeChanged(bool enabled)
1132
{
1133
// noop
1134
}
1135
1136
void Host::OnAchievementsAllProgressRefreshed()
1137
{
1138
// noop
1139
}
1140
1141
#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
1142
1143
void Host::OnRAIntegrationMenuChanged()
1144
{
1145
// noop
1146
}
1147
1148
#endif
1149
1150
void Host::SetMouseMode(bool relative, bool hide_cursor)
1151
{
1152
// noop
1153
}
1154
1155
void Host::OnMediaCaptureStarted()
1156
{
1157
// noop
1158
}
1159
1160
void Host::OnMediaCaptureStopped()
1161
{
1162
// noop
1163
}
1164
1165
void Host::PumpMessagesOnCPUThread()
1166
{
1167
MiniHost::ProcessCPUThreadEvents(false);
1168
}
1169
1170
std::string MiniHost::GetWindowTitle(const std::string& game_title)
1171
{
1172
#if defined(_DEBUGFAST)
1173
static constexpr std::string_view suffix = " [DebugFast]";
1174
#elif defined(_DEBUG)
1175
static constexpr std::string_view suffix = " [Debug]";
1176
#else
1177
static constexpr std::string_view suffix = std::string_view();
1178
#endif
1179
1180
if (System::IsShutdown() || game_title.empty())
1181
return fmt::format("DuckStation {}{}", g_scm_version_str, suffix);
1182
else
1183
return fmt::format("{}{}", game_title, suffix);
1184
}
1185
1186
void MiniHost::WarnAboutInterface()
1187
{
1188
const char* message = "This is the \"mini\" interface for DuckStation, and is missing many features.\n"
1189
" We recommend using the Qt interface instead, which you can download\n"
1190
" from https://www.duckstation.org/.";
1191
Host::AddIconOSDWarning("MiniWarning", ICON_EMOJI_WARNING, message, Host::OSD_INFO_DURATION);
1192
}
1193
1194
void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,
1195
const std::string& game_name, GameHash game_hash)
1196
{
1197
using namespace MiniHost;
1198
1199
VERBOSE_LOG("Host::OnGameChanged(\"{}\", \"{}\", \"{}\")", disc_path, game_serial, game_name);
1200
if (s_state.sdl_window)
1201
SDL_SetWindowTitle(s_state.sdl_window, GetWindowTitle(game_name).c_str());
1202
}
1203
1204
void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)
1205
{
1206
//
1207
}
1208
1209
void Host::RunOnCPUThread(std::function<void()> function, bool block /* = false */)
1210
{
1211
using namespace MiniHost;
1212
1213
std::unique_lock lock(s_state.cpu_thread_events_mutex);
1214
s_state.cpu_thread_events.emplace_back(std::move(function), block);
1215
s_state.blocking_cpu_events_pending += BoolToUInt32(block);
1216
s_state.cpu_thread_event_posted.notify_one();
1217
if (block)
1218
s_state.cpu_thread_event_done.wait(lock, []() { return s_state.blocking_cpu_events_pending == 0; });
1219
}
1220
1221
void Host::RunOnUIThread(std::function<void()> function, bool block /* = false */)
1222
{
1223
using namespace MiniHost;
1224
1225
std::function<void()>* pfunc = new std::function<void()>(std::move(function));
1226
1227
SDL_Event ev;
1228
ev.user = {};
1229
ev.type = s_state.func_event_id;
1230
ev.user.data1 = pfunc;
1231
SDL_PushEvent(&ev);
1232
}
1233
1234
void Host::RefreshGameListAsync(bool invalidate_cache)
1235
{
1236
using namespace MiniHost;
1237
1238
std::unique_lock lock(s_state.state_mutex);
1239
1240
while (s_state.game_list_refresh_progress)
1241
{
1242
lock.unlock();
1243
CancelGameListRefresh();
1244
lock.lock();
1245
}
1246
1247
s_state.game_list_refresh_progress = new FullscreenUI::BackgroundProgressCallback("glrefresh");
1248
System::QueueAsyncTask([invalidate_cache]() {
1249
GameList::Refresh(invalidate_cache, false, s_state.game_list_refresh_progress);
1250
1251
std::unique_lock lock(s_state.state_mutex);
1252
delete s_state.game_list_refresh_progress;
1253
s_state.game_list_refresh_progress = nullptr;
1254
});
1255
}
1256
1257
void Host::CancelGameListRefresh()
1258
{
1259
using namespace MiniHost;
1260
1261
{
1262
std::unique_lock lock(s_state.state_mutex);
1263
if (!s_state.game_list_refresh_progress)
1264
return;
1265
1266
s_state.game_list_refresh_progress->SetCancelled();
1267
}
1268
1269
System::WaitForAllAsyncTasks();
1270
}
1271
1272
void Host::OnGameListEntriesChanged(std::span<const u32> changed_indices)
1273
{
1274
// constantly re-querying, don't need to do anything
1275
}
1276
1277
std::optional<WindowInfo> Host::GetTopLevelWindowInfo()
1278
{
1279
return MiniHost::TranslateSDLWindowInfo(MiniHost::s_state.sdl_window, nullptr);
1280
}
1281
1282
void Host::RequestResetSettings(bool system, bool controller)
1283
{
1284
using namespace MiniHost;
1285
1286
auto lock = Host::GetSettingsLock();
1287
{
1288
SettingsInterface& si = s_state.base_settings_interface;
1289
1290
if (system)
1291
{
1292
System::SetDefaultSettings(si);
1293
EmuFolders::SetDefaults();
1294
EmuFolders::Save(si);
1295
}
1296
1297
if (controller)
1298
{
1299
InputManager::SetDefaultSourceConfig(si);
1300
Settings::SetDefaultControllerConfig(si);
1301
Settings::SetDefaultHotkeyConfig(si);
1302
}
1303
}
1304
1305
System::ApplySettings(false);
1306
}
1307
1308
void Host::RequestExitApplication(bool allow_confirm)
1309
{
1310
Host::RunOnCPUThread([]() {
1311
System::ShutdownSystem(g_settings.save_state_on_exit);
1312
1313
// clear the running flag, this'll break out of the main CPU loop once the VM is shutdown.
1314
MiniHost::s_state.cpu_thread_running.store(false, std::memory_order_release);
1315
});
1316
}
1317
1318
void Host::RequestExitBigPicture()
1319
{
1320
// sorry dude
1321
}
1322
1323
void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)
1324
{
1325
// TODO: Confirm
1326
if (System::IsValid())
1327
{
1328
Host::RunOnCPUThread([save_state]() { System::ShutdownSystem(save_state); });
1329
}
1330
}
1331
1332
void Host::ReportFatalError(std::string_view title, std::string_view message)
1333
{
1334
// Depending on the platform, this may not be available.
1335
std::fputs(SmallString::from_format("Fatal error: {}: {}\n", title, message).c_str(), stderr);
1336
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr);
1337
}
1338
1339
void Host::ReportErrorAsync(std::string_view title, std::string_view message)
1340
{
1341
std::fputs(SmallString::from_format("Error: {}: {}\n", title, message).c_str(), stderr);
1342
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, TinyString(title).c_str(), SmallString(message).c_str(), nullptr);
1343
}
1344
1345
void Host::RequestResizeHostDisplay(s32 width, s32 height)
1346
{
1347
using namespace MiniHost;
1348
1349
if (!s_state.sdl_window || s_state.fullscreen.load(std::memory_order_acquire))
1350
return;
1351
1352
SDL_SetWindowSize(s_state.sdl_window, width, height);
1353
}
1354
1355
void Host::OpenURL(std::string_view url)
1356
{
1357
if (!SDL_OpenURL(SmallString(url).c_str()))
1358
ERROR_LOG("SDL_OpenURL({}) failed: {}", url, SDL_GetError());
1359
}
1360
1361
std::string Host::GetClipboardText()
1362
{
1363
std::string ret;
1364
1365
char* text = SDL_GetClipboardText();
1366
if (text)
1367
{
1368
ret = text;
1369
SDL_free(text);
1370
}
1371
1372
return ret;
1373
}
1374
1375
bool Host::CopyTextToClipboard(std::string_view text)
1376
{
1377
if (!SDL_SetClipboardText(SmallString(text).c_str()))
1378
{
1379
ERROR_LOG("SDL_SetClipboardText({}) failed: {}", text, SDL_GetError());
1380
return false;
1381
}
1382
1383
return true;
1384
}
1385
1386
std::string Host::FormatNumber(NumberFormatType type, s64 value)
1387
{
1388
std::string ret;
1389
1390
if (type >= NumberFormatType::ShortDate && type <= NumberFormatType::LongDateTime)
1391
{
1392
const char* format;
1393
switch (type)
1394
{
1395
case NumberFormatType::ShortDate:
1396
format = "%x";
1397
break;
1398
1399
case NumberFormatType::LongDate:
1400
format = "%A %B %e %Y";
1401
break;
1402
1403
case NumberFormatType::ShortTime:
1404
case NumberFormatType::LongTime:
1405
format = "%X";
1406
break;
1407
1408
case NumberFormatType::ShortDateTime:
1409
format = "%X %x";
1410
break;
1411
1412
case NumberFormatType::LongDateTime:
1413
format = "%c";
1414
break;
1415
1416
DefaultCaseIsUnreachable();
1417
}
1418
1419
ret.resize(128);
1420
1421
if (const std::optional<std::tm> ltime = Common::LocalTime(static_cast<std::time_t>(value)))
1422
ret.resize(std::strftime(ret.data(), ret.size(), format, &ltime.value()));
1423
else
1424
ret = "Invalid";
1425
}
1426
else
1427
{
1428
ret = fmt::format("{}", value);
1429
}
1430
1431
return ret;
1432
}
1433
1434
std::string Host::FormatNumber(NumberFormatType type, double value)
1435
{
1436
return fmt::format("{}", value);
1437
}
1438
1439
bool Host::ConfirmMessage(std::string_view title, std::string_view message)
1440
{
1441
const SmallString title_copy(title);
1442
const SmallString message_copy(message);
1443
1444
static constexpr SDL_MessageBoxButtonData bd[2] = {
1445
{SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, "Yes"},
1446
{SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 2, "No"},
1447
};
1448
const SDL_MessageBoxData md = {SDL_MESSAGEBOX_INFORMATION,
1449
nullptr,
1450
title_copy.c_str(),
1451
message_copy.c_str(),
1452
static_cast<int>(std::size(bd)),
1453
bd,
1454
nullptr};
1455
1456
int buttonid = -1;
1457
SDL_ShowMessageBox(&md, &buttonid);
1458
return (buttonid == 1);
1459
}
1460
1461
void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,
1462
std::string_view yes_text /* = std::string_view() */,
1463
std::string_view no_text /* = std::string_view() */)
1464
{
1465
Host::RunOnCPUThread([title = std::string(title), message = std::string(message), callback = std::move(callback),
1466
yes_text = std::string(yes_text), no_text = std::move(no_text)]() mutable {
1467
// in case we haven't started yet...
1468
if (!FullscreenUI::IsInitialized())
1469
{
1470
callback(false);
1471
return;
1472
}
1473
1474
// Pause system while dialog is up.
1475
const bool needs_pause = System::IsValid() && !System::IsPaused();
1476
if (needs_pause)
1477
System::PauseSystem(true);
1478
1479
GPUThread::RunOnThread([title = std::string(title), message = std::string(message), callback = std::move(callback),
1480
yes_text = std::string(yes_text), no_text = std::string(no_text), needs_pause]() mutable {
1481
if (!FullscreenUI::Initialize())
1482
{
1483
callback(false);
1484
1485
if (needs_pause)
1486
{
1487
Host::RunOnCPUThread([]() {
1488
if (System::IsValid())
1489
System::PauseSystem(false);
1490
});
1491
}
1492
1493
return;
1494
}
1495
1496
// Need to reset run idle state _again_ after displaying.
1497
auto final_callback = [callback = std::move(callback)](bool result) {
1498
FullscreenUI::UpdateRunIdleState();
1499
callback(result);
1500
};
1501
1502
ImGuiFullscreen::OpenConfirmMessageDialog(std::move(title), std::move(message), std::move(final_callback),
1503
fmt::format(ICON_FA_CHECK " {}", yes_text),
1504
fmt::format(ICON_FA_XMARK " {}", no_text));
1505
FullscreenUI::UpdateRunIdleState();
1506
});
1507
});
1508
}
1509
1510
void Host::OpenHostFileSelectorAsync(std::string_view title, bool select_directory, FileSelectorCallback callback,
1511
FileSelectorFilters filters /* = FileSelectorFilters() */,
1512
std::string_view initial_directory /* = std::string_view() */)
1513
{
1514
// TODO: Use SDL FileDialog API
1515
callback(std::string());
1516
}
1517
1518
const char* Host::GetDefaultFullscreenUITheme()
1519
{
1520
return "";
1521
}
1522
1523
bool Host::ShouldPreferHostFileSelector()
1524
{
1525
return false;
1526
}
1527
1528
BEGIN_HOTKEY_LIST(g_host_hotkeys)
1529
END_HOTKEY_LIST()
1530
1531
static void SignalHandler(int signal)
1532
{
1533
// First try the normal (graceful) shutdown/exit.
1534
static bool graceful_shutdown_attempted = false;
1535
if (!graceful_shutdown_attempted)
1536
{
1537
std::fprintf(stderr, "Received CTRL+C, attempting graceful shutdown. Press CTRL+C again to force.\n");
1538
graceful_shutdown_attempted = true;
1539
Host::RequestExitApplication(false);
1540
return;
1541
}
1542
1543
std::signal(signal, SIG_DFL);
1544
1545
// MacOS is missing std::quick_exit() despite it being C++11...
1546
#ifndef __APPLE__
1547
std::quick_exit(1);
1548
#else
1549
_Exit(1);
1550
#endif
1551
}
1552
1553
void MiniHost::HookSignals()
1554
{
1555
std::signal(SIGINT, SignalHandler);
1556
std::signal(SIGTERM, SignalHandler);
1557
1558
#ifndef _WIN32
1559
// Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.
1560
struct sigaction sa_chld = {};
1561
sigemptyset(&sa_chld.sa_mask);
1562
sa_chld.sa_handler = SIG_IGN;
1563
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;
1564
sigaction(SIGCHLD, &sa_chld, nullptr);
1565
#endif
1566
}
1567
1568
void MiniHost::InitializeEarlyConsole()
1569
{
1570
const bool was_console_enabled = Log::IsConsoleOutputEnabled();
1571
if (!was_console_enabled)
1572
Log::SetConsoleOutputParams(true);
1573
}
1574
1575
void MiniHost::PrintCommandLineVersion()
1576
{
1577
InitializeEarlyConsole();
1578
1579
std::fprintf(stderr, "DuckStation Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);
1580
std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");
1581
std::fprintf(stderr, "\n");
1582
}
1583
1584
void MiniHost::PrintCommandLineHelp(const char* progname)
1585
{
1586
InitializeEarlyConsole();
1587
1588
PrintCommandLineVersion();
1589
std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);
1590
std::fprintf(stderr, "\n");
1591
std::fprintf(stderr, " -help: Displays this information and exits.\n");
1592
std::fprintf(stderr, " -version: Displays version information and exits.\n");
1593
std::fprintf(stderr, " -batch: Enables batch mode (exits after powering off).\n");
1594
std::fprintf(stderr, " -fastboot: Force fast boot for provided filename.\n");
1595
std::fprintf(stderr, " -slowboot: Force slow boot for provided filename.\n");
1596
std::fprintf(stderr, " -bios: Boot into the BIOS shell.\n");
1597
std::fprintf(stderr, " -resume: Load resume save state. If a boot filename is provided,\n"
1598
" that game's resume state will be loaded, otherwise the most\n"
1599
" recent resume save state will be loaded.\n");
1600
std::fprintf(stderr, " -state <index>: Loads specified save state by index. If a boot\n"
1601
" filename is provided, a per-game state will be loaded, otherwise\n"
1602
" a global state will be loaded.\n");
1603
std::fprintf(stderr, " -statefile <filename>: Loads state from the specified filename.\n"
1604
" No boot filename is required with this option.\n");
1605
std::fprintf(stderr, " -exe <filename>: Boot the specified exe instead of loading from disc.\n");
1606
std::fprintf(stderr, " -fullscreen: Enters fullscreen mode immediately after starting.\n");
1607
std::fprintf(stderr, " -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n");
1608
std::fprintf(stderr, " -earlyconsole: Creates console as early as possible, for logging.\n");
1609
std::fprintf(stderr, " -prerotation <degrees>: Prerotates output by 90/180/270 degrees.\n");
1610
std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n"
1611
" parameters make up the filename. Use when the filename contains\n"
1612
" spaces or starts with a dash.\n");
1613
std::fprintf(stderr, "\n");
1614
}
1615
1616
std::optional<SystemBootParameters>& AutoBoot(std::optional<SystemBootParameters>& autoboot)
1617
{
1618
if (!autoboot)
1619
autoboot.emplace();
1620
1621
return autoboot;
1622
}
1623
1624
bool MiniHost::ParseCommandLineParametersAndInitializeConfig(int argc, char* argv[],
1625
std::optional<SystemBootParameters>& autoboot)
1626
{
1627
std::optional<s32> state_index;
1628
bool starting_bios = false;
1629
bool no_more_args = false;
1630
1631
for (int i = 1; i < argc; i++)
1632
{
1633
if (!no_more_args)
1634
{
1635
#define CHECK_ARG(str) (std::strcmp(argv[i], (str)) == 0)
1636
#define CHECK_ARG_PARAM(str) (std::strcmp(argv[i], (str)) == 0 && ((i + 1) < argc))
1637
1638
if (CHECK_ARG("-help"))
1639
{
1640
PrintCommandLineHelp(argv[0]);
1641
return false;
1642
}
1643
else if (CHECK_ARG("-version"))
1644
{
1645
PrintCommandLineVersion();
1646
return false;
1647
}
1648
else if (CHECK_ARG("-batch"))
1649
{
1650
INFO_LOG("Command Line: Using batch mode.");
1651
s_state.batch_mode = true;
1652
continue;
1653
}
1654
else if (CHECK_ARG("-bios"))
1655
{
1656
INFO_LOG("Command Line: Starting BIOS.");
1657
AutoBoot(autoboot);
1658
starting_bios = true;
1659
continue;
1660
}
1661
else if (CHECK_ARG("-fastboot"))
1662
{
1663
INFO_LOG("Command Line: Forcing fast boot.");
1664
AutoBoot(autoboot)->override_fast_boot = true;
1665
continue;
1666
}
1667
else if (CHECK_ARG("-slowboot"))
1668
{
1669
INFO_LOG("Command Line: Forcing slow boot.");
1670
AutoBoot(autoboot)->override_fast_boot = false;
1671
continue;
1672
}
1673
else if (CHECK_ARG("-resume"))
1674
{
1675
state_index = -1;
1676
INFO_LOG("Command Line: Loading resume state.");
1677
continue;
1678
}
1679
else if (CHECK_ARG_PARAM("-state"))
1680
{
1681
state_index = StringUtil::FromChars<s32>(argv[++i]);
1682
if (!state_index.has_value())
1683
{
1684
ERROR_LOG("Invalid state index");
1685
return false;
1686
}
1687
1688
INFO_LOG("Command Line: Loading state index: {}", state_index.value());
1689
continue;
1690
}
1691
else if (CHECK_ARG_PARAM("-statefile"))
1692
{
1693
AutoBoot(autoboot)->save_state = argv[++i];
1694
INFO_LOG("Command Line: Loading state file: '{}'", autoboot->save_state);
1695
continue;
1696
}
1697
else if (CHECK_ARG_PARAM("-exe"))
1698
{
1699
AutoBoot(autoboot)->override_exe = argv[++i];
1700
INFO_LOG("Command Line: Overriding EXE file: '{}'", autoboot->override_exe);
1701
continue;
1702
}
1703
else if (CHECK_ARG("-fullscreen"))
1704
{
1705
INFO_LOG("Command Line: Using fullscreen.");
1706
AutoBoot(autoboot)->override_fullscreen = true;
1707
s_state.start_fullscreen_ui_fullscreen = true;
1708
continue;
1709
}
1710
else if (CHECK_ARG("-nofullscreen"))
1711
{
1712
INFO_LOG("Command Line: Not using fullscreen.");
1713
AutoBoot(autoboot)->override_fullscreen = false;
1714
continue;
1715
}
1716
else if (CHECK_ARG("-earlyconsole"))
1717
{
1718
InitializeEarlyConsole();
1719
continue;
1720
}
1721
else if (CHECK_ARG_PARAM("-prerotation"))
1722
{
1723
const char* prerotation_str = argv[++i];
1724
if (std::strcmp(prerotation_str, "0") == 0 || StringUtil::EqualNoCase(prerotation_str, "identity"))
1725
{
1726
INFO_LOG("Command Line: Forcing surface pre-rotation to identity.");
1727
s_state.force_prerotation = WindowInfo::PreRotation::Identity;
1728
}
1729
else if (std::strcmp(prerotation_str, "90") == 0)
1730
{
1731
INFO_LOG("Command Line: Forcing surface pre-rotation to 90 degrees clockwise.");
1732
s_state.force_prerotation = WindowInfo::PreRotation::Rotate90Clockwise;
1733
}
1734
else if (std::strcmp(prerotation_str, "180") == 0)
1735
{
1736
INFO_LOG("Command Line: Forcing surface pre-rotation to 180 degrees clockwise.");
1737
s_state.force_prerotation = WindowInfo::PreRotation::Rotate180Clockwise;
1738
}
1739
else if (std::strcmp(prerotation_str, "270") == 0)
1740
{
1741
INFO_LOG("Command Line: Forcing surface pre-rotation to 270 degrees clockwise.");
1742
s_state.force_prerotation = WindowInfo::PreRotation::Rotate270Clockwise;
1743
}
1744
else
1745
{
1746
ERROR_LOG("Invalid prerotation value: {}", prerotation_str);
1747
return false;
1748
}
1749
1750
continue;
1751
}
1752
else if (CHECK_ARG("--"))
1753
{
1754
no_more_args = true;
1755
continue;
1756
}
1757
else if (argv[i][0] == '-')
1758
{
1759
Host::ReportFatalError("Error", fmt::format("Unknown parameter: {}", argv[i]));
1760
return false;
1761
}
1762
1763
#undef CHECK_ARG
1764
#undef CHECK_ARG_PARAM
1765
}
1766
1767
if (autoboot && !autoboot->path.empty())
1768
autoboot->path += ' ';
1769
AutoBoot(autoboot)->path += argv[i];
1770
}
1771
1772
// To do anything useful, we need the config initialized.
1773
if (!InitializeConfig())
1774
{
1775
// NOTE: No point translating this, because no config means the language won't be loaded anyway.
1776
Host::ReportFatalError("Error", "Failed to initialize config.");
1777
return EXIT_FAILURE;
1778
}
1779
1780
// Check the file we're starting actually exists.
1781
1782
if (autoboot && !autoboot->path.empty() && !FileSystem::FileExists(autoboot->path.c_str()))
1783
{
1784
Host::ReportFatalError("Error", fmt::format("File '{}' does not exist.", autoboot->path));
1785
return false;
1786
}
1787
1788
if (state_index.has_value())
1789
{
1790
AutoBoot(autoboot);
1791
1792
if (autoboot->path.empty())
1793
{
1794
// loading global state, -1 means resume the last game
1795
if (state_index.value() < 0)
1796
autoboot->save_state = System::GetMostRecentResumeSaveStatePath();
1797
else
1798
autoboot->save_state = System::GetGlobalSaveStatePath(state_index.value());
1799
}
1800
else
1801
{
1802
// loading game state
1803
const std::string game_serial(GameDatabase::GetSerialForPath(autoboot->path.c_str()));
1804
autoboot->save_state = System::GetGameSaveStatePath(game_serial, state_index.value());
1805
}
1806
1807
if (autoboot->save_state.empty() || !FileSystem::FileExists(autoboot->save_state.c_str()))
1808
{
1809
Host::ReportFatalError("Error", "The specified save state does not exist.");
1810
return false;
1811
}
1812
}
1813
1814
// check autoboot parameters, if we set something like fullscreen without a bios
1815
// or disc, we don't want to actually start.
1816
if (autoboot && autoboot->path.empty() && autoboot->save_state.empty() && !starting_bios)
1817
autoboot.reset();
1818
1819
// if we don't have autoboot, we definitely don't want batch mode (because that'll skip
1820
// scanning the game list).
1821
if (s_state.batch_mode)
1822
{
1823
if (!autoboot)
1824
{
1825
Host::ReportFatalError("Error", "Cannot use batch mode, because no boot filename was specified.");
1826
return false;
1827
}
1828
1829
// if using batch mode, immediately refresh the game list so the data is available
1830
GameList::Refresh(false, true);
1831
}
1832
1833
return true;
1834
}
1835
1836
#include <SDL3/SDL_main.h>
1837
1838
int main(int argc, char* argv[])
1839
{
1840
using namespace MiniHost;
1841
1842
CrashHandler::Install(&Bus::CleanupMemoryMap);
1843
1844
if (!PerformEarlyHardwareChecks())
1845
return EXIT_FAILURE;
1846
1847
if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS))
1848
{
1849
Host::ReportFatalError("Error", TinyString::from_format("SDL_InitSubSystem() failed: {}", SDL_GetError()));
1850
return EXIT_FAILURE;
1851
}
1852
1853
s_state.func_event_id = SDL_RegisterEvents(1);
1854
if (s_state.func_event_id == static_cast<u32>(-1))
1855
{
1856
Host::ReportFatalError("Error", TinyString::from_format("SDL_RegisterEvents() failed: {}", SDL_GetError()));
1857
return EXIT_FAILURE;
1858
}
1859
1860
if (!EarlyProcessStartup())
1861
return EXIT_FAILURE;
1862
1863
std::optional<SystemBootParameters> autoboot;
1864
if (!ParseCommandLineParametersAndInitializeConfig(argc, argv, autoboot))
1865
return EXIT_FAILURE;
1866
1867
// the rest of initialization happens on the CPU thread.
1868
HookSignals();
1869
1870
// prevent input source polling on CPU thread...
1871
SDLInputSource::ALLOW_EVENT_POLLING = false;
1872
s_state.ui_thread_running = true;
1873
StartCPUThread();
1874
1875
// process autoboot early, that way we can set the fullscreen flag
1876
if (autoboot)
1877
{
1878
s_state.start_fullscreen_ui_fullscreen =
1879
s_state.start_fullscreen_ui_fullscreen || autoboot->override_fullscreen.value_or(false);
1880
Host::RunOnCPUThread([params = std::move(autoboot.value())]() mutable {
1881
Error error;
1882
if (!System::BootSystem(std::move(params), &error))
1883
Host::ReportErrorAsync("Failed to boot system", error.GetDescription());
1884
});
1885
}
1886
1887
UIThreadMainLoop();
1888
1889
StopCPUThread();
1890
1891
System::ProcessShutdown();
1892
1893
// Ensure log is flushed.
1894
Log::SetFileOutputParams(false, nullptr);
1895
1896
if (s_state.base_settings_interface.IsDirty())
1897
s_state.base_settings_interface.Save();
1898
1899
SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
1900
1901
return EXIT_SUCCESS;
1902
}
1903
1904