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