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