Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/duckstation-regtest/regtest_host.cpp
7408 views
1
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "core/achievements.h"
5
#include "core/bus.h"
6
#include "core/controller.h"
7
#include "core/core_private.h"
8
#include "core/cpu_core.h"
9
#include "core/fullscreenui.h"
10
#include "core/fullscreenui_widgets.h"
11
#include "core/game_list.h"
12
#include "core/gpu.h"
13
#include "core/gpu_backend.h"
14
#include "core/host.h"
15
#include "core/spu.h"
16
#include "core/system.h"
17
#include "core/system_private.h"
18
#include "core/video_presenter.h"
19
#include "core/video_thread.h"
20
21
#include "scmversion/scmversion.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/translation.h"
28
29
#include "common/assert.h"
30
#include "common/crash_handler.h"
31
#include "common/error.h"
32
#include "common/file_system.h"
33
#include "common/log.h"
34
#include "common/path.h"
35
#include "common/sha256_digest.h"
36
#include "common/string_util.h"
37
#include "common/task_queue.h"
38
#include "common/threading.h"
39
#include "common/time_helpers.h"
40
#include "common/timer.h"
41
42
#include "fmt/format.h"
43
44
#include <csignal>
45
#include <cstdio>
46
#include <ctime>
47
48
LOG_CHANNEL(Host);
49
50
namespace RegTestHost {
51
52
static bool ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot);
53
static void PrintCommandLineVersion();
54
static void PrintCommandLineHelp(const char* progname);
55
static bool InitializeFoldersAndConfig(Error* error);
56
static void InitializeEarlyConsole();
57
static void HookSignals();
58
static bool SetNewDataRoot(const std::string& filename);
59
static void DumpSystemStateHashes();
60
static std::string GetFrameDumpPath(u32 frame);
61
static void ProcessCoreThreadEvents();
62
static void VideoThreadEntryPoint();
63
64
struct RegTestHostState
65
{
66
ALIGN_TO_CACHE_LINE std::mutex core_thread_events_mutex;
67
std::condition_variable core_thread_event_done;
68
std::deque<std::pair<std::function<void()>, bool>> cpu_thread_events;
69
u32 blocking_cpu_events_pending = 0;
70
};
71
72
static RegTestHostState s_state;
73
ALIGN_TO_CACHE_LINE static TaskQueue s_async_task_queue;
74
75
} // namespace RegTestHost
76
77
static Threading::Thread s_video_thread;
78
79
static u32 s_frames_to_run = 60 * 60;
80
static u32 s_frames_remaining = 0;
81
static u32 s_frame_dump_interval = 0;
82
static std::string s_dump_base_directory;
83
84
bool RegTestHost::InitializeFoldersAndConfig(Error* error)
85
{
86
if (!Core::SetCriticalFolders("resources", error))
87
return false;
88
89
if (!Core::InitializeBaseSettingsLayer({}, error))
90
return false;
91
92
// default settings for runner
93
const auto lock = Core::GetSettingsLock();
94
SettingsInterface& si = *Core::GetBaseSettingsLayer();
95
si.SetStringValue("GPU", "Renderer", Settings::GetRendererName(GPURenderer::Software));
96
si.SetBoolValue("GPU", "DisableShaderCache", true);
97
si.SetStringValue("Pad1", "Type", Controller::GetControllerInfo(ControllerType::AnalogController).name);
98
si.SetStringValue("Pad2", "Type", Controller::GetControllerInfo(ControllerType::None).name);
99
si.SetStringValue("MemoryCards", "Card1Type", Settings::GetMemoryCardTypeName(MemoryCardType::NonPersistent));
100
si.SetStringValue("MemoryCards", "Card2Type", Settings::GetMemoryCardTypeName(MemoryCardType::None));
101
si.SetStringValue("ControllerPorts", "MultitapMode", Settings::GetMultitapModeName(MultitapMode::Disabled));
102
si.SetStringValue("Audio", "Backend", AudioStream::GetBackendName(AudioBackend::Null));
103
si.SetBoolValue("Logging", "LogToConsole", false);
104
si.SetBoolValue("Logging", "LogToFile", false);
105
si.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(Log::Level::Info));
106
si.SetBoolValue("Main", "ApplyGameSettings", false); // don't want game settings interfering
107
si.SetBoolValue("BIOS", "PatchFastBoot", true); // no point validating the bios intro..
108
si.SetFloatValue("Main", "EmulationSpeed", 0.0f);
109
110
// disable all sources
111
for (u32 i = 0; i < static_cast<u32>(InputSourceType::Count); i++)
112
si.SetBoolValue("InputSources", InputManager::InputSourceToString(static_cast<InputSourceType>(i)), false);
113
114
return true;
115
}
116
117
void Host::ReportFatalError(std::string_view title, std::string_view message)
118
{
119
ERROR_LOG("ReportFatalError: {}", message);
120
abort();
121
}
122
123
void Host::ReportErrorAsync(std::string_view title, std::string_view message)
124
{
125
if (!title.empty() && !message.empty())
126
ERROR_LOG("ReportErrorAsync: {}: {}", title, message);
127
else if (!message.empty())
128
ERROR_LOG("ReportErrorAsync: {}", message);
129
}
130
131
void Host::ReportStatusMessage(std::string_view message)
132
{
133
INFO_LOG("ReportStatusMessage: {}", message);
134
}
135
136
void Host::ConfirmMessageAsync(std::string_view title, std::string_view message, ConfirmMessageAsyncCallback callback,
137
std::string_view yes_text, std::string_view no_text)
138
{
139
if (!title.empty() && !message.empty())
140
ERROR_LOG("ConfirmMessage: {}: {}", title, message);
141
else if (!message.empty())
142
ERROR_LOG("ConfirmMessage: {}", message);
143
144
callback(true);
145
}
146
147
void Host::ReportDebuggerEvent(CPU::DebuggerEvent event, std::string_view message)
148
{
149
ERROR_LOG("ReportDebuggerEvent: {}", message);
150
}
151
152
std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()
153
{
154
return {};
155
}
156
157
const char* Host::GetLanguageName(std::string_view language_code)
158
{
159
return "";
160
}
161
162
bool Host::ChangeLanguage(const char* new_language)
163
{
164
return false;
165
}
166
167
s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,
168
std::string_view disambiguation, char* tbuf, size_t tbuf_space)
169
{
170
if (msg.size() > tbuf_space)
171
return -1;
172
else if (msg.empty())
173
return 0;
174
175
std::memcpy(tbuf, msg.data(), msg.size());
176
return static_cast<s32>(msg.size());
177
}
178
179
std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)
180
{
181
TinyString count_str = TinyString::from_format("{}", count);
182
183
std::string ret(msg);
184
for (;;)
185
{
186
std::string::size_type pos = ret.find("%n");
187
if (pos == std::string::npos)
188
break;
189
190
ret.replace(pos, pos + 2, count_str.view());
191
}
192
193
return ret;
194
}
195
196
SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,
197
int count)
198
{
199
SmallString ret(msg);
200
ret.replace("%n", TinyString::from_format("{}", count));
201
return ret;
202
}
203
204
void Host::LoadSettings(const SettingsInterface& si, std::unique_lock<std::mutex>& lock)
205
{
206
}
207
208
void Host::CheckForSettingsChanges(const Settings& old_settings)
209
{
210
}
211
212
void Host::CommitBaseSettingChanges()
213
{
214
// noop, in memory
215
}
216
217
bool Host::ResourceFileExists(std::string_view filename, bool allow_override)
218
{
219
const std::string path(Path::Combine(EmuFolders::Resources, filename));
220
return FileSystem::FileExists(path.c_str());
221
}
222
223
std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)
224
{
225
const std::string path(Path::Combine(EmuFolders::Resources, filename));
226
return FileSystem::ReadBinaryFile(path.c_str(), error);
227
}
228
229
std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)
230
{
231
const std::string path(Path::Combine(EmuFolders::Resources, filename));
232
return FileSystem::ReadFileToString(path.c_str(), error);
233
}
234
235
std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)
236
{
237
const std::string path(Path::Combine(EmuFolders::Resources, filename));
238
FILESYSTEM_STAT_DATA sd;
239
if (!FileSystem::StatFile(path.c_str(), &sd))
240
{
241
ERROR_LOG("Failed to stat resource file '{}'", filename);
242
return std::nullopt;
243
}
244
245
return sd.ModificationTime;
246
}
247
248
void Host::OnSystemStarting()
249
{
250
//
251
}
252
253
void Host::OnSystemStarted()
254
{
255
//
256
}
257
258
void Host::OnSystemStopping()
259
{
260
//
261
}
262
263
void Host::OnSystemDestroyed()
264
{
265
//
266
}
267
268
void Host::OnSystemPaused()
269
{
270
//
271
}
272
273
void Host::OnSystemResumed()
274
{
275
//
276
}
277
278
void Host::OnSystemAbnormalShutdown(const std::string_view reason)
279
{
280
// Already logged in core.
281
}
282
283
void Host::OnVideoThreadRunIdleChanged(bool is_active)
284
{
285
//
286
}
287
288
bool Host::SetScreensaverInhibit(bool inhibit, Error* error)
289
{
290
Error::SetStringView(error, "Not implemented");
291
return false;
292
}
293
294
void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)
295
{
296
//
297
}
298
299
void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,
300
const std::string& game_name, GameHash hash)
301
{
302
INFO_LOG("Disc Path: {}", disc_path);
303
INFO_LOG("Game Serial: {}", game_serial);
304
INFO_LOG("Game Name: {}", game_name);
305
}
306
307
void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)
308
{
309
//
310
}
311
312
void Host::OnMediaCaptureStarted()
313
{
314
//
315
}
316
317
void Host::OnMediaCaptureStopped()
318
{
319
//
320
}
321
322
void Host::PumpMessagesOnCoreThread()
323
{
324
RegTestHost::ProcessCoreThreadEvents();
325
326
s_frames_remaining--;
327
if (s_frames_remaining == 0)
328
{
329
RegTestHost::DumpSystemStateHashes();
330
System::ShutdownSystem(false);
331
}
332
}
333
334
void Host::RunOnCoreThread(std::function<void()> function, bool block /* = false */)
335
{
336
using namespace RegTestHost;
337
338
std::unique_lock lock(s_state.core_thread_events_mutex);
339
s_state.cpu_thread_events.emplace_back(std::move(function), block);
340
s_state.blocking_cpu_events_pending += BoolToUInt32(block);
341
if (block)
342
s_state.core_thread_event_done.wait(lock, []() { return s_state.blocking_cpu_events_pending == 0; });
343
}
344
345
void RegTestHost::ProcessCoreThreadEvents()
346
{
347
std::unique_lock lock(s_state.core_thread_events_mutex);
348
349
for (;;)
350
{
351
if (s_state.cpu_thread_events.empty())
352
break;
353
354
auto event = std::move(s_state.cpu_thread_events.front());
355
s_state.cpu_thread_events.pop_front();
356
lock.unlock();
357
event.first();
358
lock.lock();
359
360
if (event.second)
361
{
362
s_state.blocking_cpu_events_pending--;
363
s_state.core_thread_event_done.notify_one();
364
}
365
}
366
}
367
368
void Host::RunOnUIThread(std::function<void()> function, bool block /* = false */)
369
{
370
RunOnCoreThread(std::move(function), block);
371
}
372
373
void Host::QueueAsyncTask(std::function<void()> function)
374
{
375
RegTestHost::s_async_task_queue.SubmitTask(std::move(function));
376
}
377
378
void Host::WaitForAllAsyncTasks()
379
{
380
RegTestHost::s_async_task_queue.WaitForAll();
381
}
382
383
void Host::RequestResizeHostDisplay(s32 width, s32 height)
384
{
385
//
386
}
387
388
void Host::SetDefaultSettings(SettingsInterface& si)
389
{
390
//
391
}
392
393
void Host::OnSettingsResetToDefault(bool host, bool system, bool controller)
394
{
395
//
396
}
397
398
void Host::RequestExitApplication(bool save_state_if_running)
399
{
400
//
401
}
402
403
void Host::RequestExitBigPicture()
404
{
405
//
406
}
407
408
void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)
409
{
410
//
411
}
412
413
std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,
414
Error* error)
415
{
416
return WindowInfo();
417
}
418
419
WindowInfoType Host::GetRenderWindowInfoType()
420
{
421
return WindowInfoType::Surfaceless;
422
}
423
424
void Host::ReleaseRenderWindow()
425
{
426
//
427
}
428
429
bool Host::CanChangeFullscreenMode(bool new_fullscreen_state)
430
{
431
return false;
432
}
433
434
void Host::BeginTextInput()
435
{
436
//
437
}
438
439
void Host::EndTextInput()
440
{
441
//
442
}
443
444
bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,
445
std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,
446
AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)
447
{
448
return false;
449
}
450
451
void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x /* = nullptr */,
452
s32* pos_y /* = nullptr */, u32* width /* = nullptr */,
453
u32* height /* = nullptr */)
454
{
455
}
456
457
void Host::FrameDoneOnVideoThread(GPUBackend* gpu_backend, u32 frame_number)
458
{
459
if (s_frame_dump_interval == 0 || (frame_number % s_frame_dump_interval) != 0 || !VideoPresenter::HasDisplayTexture())
460
return;
461
462
// Need to take a copy of the display texture.
463
GPUTexture* const read_texture = VideoPresenter::GetDisplayTexture();
464
const GSVector4i read_rect = VideoPresenter::GetDisplayTextureRect();
465
const u32 read_x = static_cast<u32>(read_rect.x);
466
const u32 read_y = static_cast<u32>(read_rect.y);
467
const u32 read_width = static_cast<u32>(read_rect.width());
468
const u32 read_height = static_cast<u32>(read_rect.height());
469
const ImageFormat read_format = GPUTexture::GetImageFormatForTextureFormat(read_texture->GetFormat());
470
if (read_format == ImageFormat::None)
471
return;
472
473
Image image(read_width, read_height, read_format);
474
std::unique_ptr<GPUDownloadTexture> dltex;
475
if (g_gpu_device->GetFeatures().memory_import)
476
{
477
dltex = g_gpu_device->CreateDownloadTexture(read_width, read_height, read_texture->GetFormat(), image.GetPixels(),
478
image.GetStorageSize(), image.GetPitch());
479
}
480
if (!dltex)
481
{
482
if (!(dltex = g_gpu_device->CreateDownloadTexture(read_width, read_height, read_texture->GetFormat())))
483
{
484
ERROR_LOG("Failed to create {}x{} {} download texture", read_width, read_height,
485
GPUTexture::GetFormatName(read_texture->GetFormat()));
486
return;
487
}
488
}
489
490
dltex->CopyFromTexture(0, 0, read_texture, read_x, read_y, read_width, read_height, 0, 0, !dltex->IsImported());
491
if (!dltex->ReadTexels(0, 0, read_width, read_height, image.GetPixels(), image.GetPitch()))
492
{
493
ERROR_LOG("Failed to read {}x{} download texture", read_width, read_height);
494
gpu_backend->RestoreDeviceContext();
495
return;
496
}
497
498
// no more GPU calls
499
gpu_backend->RestoreDeviceContext();
500
501
Error error;
502
const std::string path = RegTestHost::GetFrameDumpPath(frame_number);
503
auto fp = FileSystem::OpenManagedCFile(path.c_str(), "wb", &error);
504
if (!fp)
505
{
506
ERROR_LOG("Can't open file '{}': {}", Path::GetFileName(path), error.GetDescription());
507
return;
508
}
509
510
Host::QueueAsyncTask([path = std::move(path), fp = fp.release(), image = std::move(image)]() mutable {
511
Error error;
512
513
if (image.GetFormat() != ImageFormat::RGBA8)
514
{
515
std::optional<Image> convert_image = image.ConvertToRGBA8(&error);
516
if (!convert_image.has_value())
517
{
518
ERROR_LOG("Failed to convert {} screenshot to RGBA8: {}", Image::GetFormatName(image.GetFormat()),
519
error.GetDescription());
520
image.Invalidate();
521
}
522
else
523
{
524
image = std::move(convert_image.value());
525
}
526
}
527
528
bool result = false;
529
if (image.IsValid())
530
{
531
image.SetAllPixelsOpaque();
532
533
result = image.SaveToFile(path.c_str(), fp, Image::DEFAULT_SAVE_QUALITY, &error);
534
if (!result)
535
ERROR_LOG("Failed to save screenshot to '{}': '{}'", Path::GetFileName(path), error.GetDescription());
536
}
537
538
std::fclose(fp);
539
return result;
540
});
541
}
542
543
void Host::OpenURL(std::string_view url)
544
{
545
//
546
}
547
548
std::string Host::GetClipboardText()
549
{
550
return std::string();
551
}
552
553
bool Host::CopyTextToClipboard(std::string_view text)
554
{
555
return false;
556
}
557
558
std::string Host::FormatNumber(NumberFormatType type, s64 value)
559
{
560
std::string ret;
561
562
if (type >= NumberFormatType::ShortDate && type <= NumberFormatType::LongDateTime)
563
{
564
const char* format;
565
switch (type)
566
{
567
case NumberFormatType::ShortDate:
568
format = "%x";
569
break;
570
571
case NumberFormatType::LongDate:
572
format = "%A %B %e %Y";
573
break;
574
575
case NumberFormatType::ShortTime:
576
case NumberFormatType::LongTime:
577
format = "%X";
578
break;
579
580
case NumberFormatType::ShortDateTime:
581
format = "%X %x";
582
break;
583
584
case NumberFormatType::LongDateTime:
585
format = "%c";
586
break;
587
588
DefaultCaseIsUnreachable();
589
}
590
591
ret.resize(128);
592
593
if (const std::optional<std::tm> ltime = Common::LocalTime(static_cast<std::time_t>(value)))
594
ret.resize(std::strftime(ret.data(), ret.size(), format, &ltime.value()));
595
else
596
ret = "Invalid";
597
}
598
else
599
{
600
ret = fmt::format("{}", value);
601
}
602
603
return ret;
604
}
605
606
std::string Host::FormatNumber(NumberFormatType type, double value)
607
{
608
return fmt::format("{}", value);
609
}
610
611
void Host::SetMouseMode(bool relative, bool hide_cursor)
612
{
613
//
614
}
615
616
void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)
617
{
618
// noop
619
}
620
621
void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)
622
{
623
// noop
624
}
625
626
void Host::OnAchievementsActiveChanged(bool active)
627
{
628
// noop
629
}
630
631
void Host::OnAchievementsHardcoreModeChanged(bool enabled)
632
{
633
// noop
634
}
635
636
#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
637
638
void Host::OnRAIntegrationMenuChanged()
639
{
640
// noop
641
}
642
643
#endif
644
645
const char* Host::GetDefaultFullscreenUITheme()
646
{
647
return "";
648
}
649
650
void Host::AddFixedInputBindings(const SettingsInterface& si)
651
{
652
// noop
653
}
654
655
void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)
656
{
657
// noop
658
}
659
660
void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)
661
{
662
// noop
663
}
664
665
std::optional<WindowInfo> Host::GetTopLevelWindowInfo()
666
{
667
return std::nullopt;
668
}
669
670
void Host::RefreshGameListAsync(bool invalidate_cache)
671
{
672
// noop
673
}
674
675
void Host::CancelGameListRefresh()
676
{
677
// noop
678
}
679
680
void Host::OnGameListEntriesChanged(std::span<const u32> changed_indices)
681
{
682
// noop
683
}
684
685
static void SignalHandler(int signal)
686
{
687
std::signal(signal, SIG_DFL);
688
689
// MacOS is missing std::quick_exit() despite it being C++11...
690
#ifndef __APPLE__
691
std::quick_exit(1);
692
#else
693
_Exit(1);
694
#endif
695
}
696
697
void RegTestHost::HookSignals()
698
{
699
std::signal(SIGINT, SignalHandler);
700
std::signal(SIGTERM, SignalHandler);
701
702
#ifndef _WIN32
703
// Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.
704
struct sigaction sa_chld = {};
705
sigemptyset(&sa_chld.sa_mask);
706
sa_chld.sa_handler = SIG_IGN;
707
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;
708
sigaction(SIGCHLD, &sa_chld, nullptr);
709
#endif
710
}
711
712
void RegTestHost::VideoThreadEntryPoint()
713
{
714
Threading::SetNameOfCurrentThread("Video Thread");
715
VideoThread::Internal::VideoThreadEntryPoint();
716
}
717
718
void RegTestHost::DumpSystemStateHashes()
719
{
720
Error error;
721
722
// don't save full state on gpu dump, it's not going to be complete...
723
if (!System::IsReplayingGPUDump())
724
{
725
DynamicHeapArray<u8> state_data(System::GetMaxSaveStateSize(g_settings.cpu_enable_8mb_ram));
726
size_t state_data_size;
727
if (!System::SaveStateDataToBuffer(state_data, &state_data_size, &error))
728
{
729
ERROR_LOG("Failed to save system state: {}", error.GetDescription());
730
return;
731
}
732
733
INFO_LOG("Save State Hash: {}",
734
SHA256Digest::DigestToString(SHA256Digest::GetDigest(state_data.cspan(0, state_data_size))));
735
INFO_LOG("RAM Hash: {}",
736
SHA256Digest::DigestToString(SHA256Digest::GetDigest(std::span<const u8>(Bus::g_ram, Bus::g_ram_size))));
737
INFO_LOG("SPU RAM Hash: {}", SHA256Digest::DigestToString(SHA256Digest::GetDigest(SPU::GetRAM())));
738
}
739
740
INFO_LOG("VRAM Hash: {}", SHA256Digest::DigestToString(SHA256Digest::GetDigest(
741
std::span<const u8>(reinterpret_cast<const u8*>(g_vram), VRAM_SIZE))));
742
}
743
744
void RegTestHost::InitializeEarlyConsole()
745
{
746
const bool was_console_enabled = Log::IsConsoleOutputEnabled();
747
if (!was_console_enabled)
748
{
749
Log::SetConsoleOutputParams(true);
750
Log::SetLogLevel(Log::Level::Info);
751
}
752
}
753
754
void RegTestHost::PrintCommandLineVersion()
755
{
756
InitializeEarlyConsole();
757
std::fprintf(stderr, "DuckStation Regression Test Runner Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);
758
std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");
759
std::fprintf(stderr, "\n");
760
}
761
762
void RegTestHost::PrintCommandLineHelp(const char* progname)
763
{
764
InitializeEarlyConsole();
765
PrintCommandLineVersion();
766
std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);
767
std::fprintf(stderr, "\n");
768
std::fprintf(stderr, " -help: Displays this information and exits.\n");
769
std::fprintf(stderr, " -version: Displays version information and exits.\n");
770
std::fprintf(stderr, " -dumpdir: Set frame dump base directory (will be dumped to basedir/gametitle).\n");
771
std::fprintf(stderr, " -dumpinterval: Dumps every N frames.\n");
772
std::fprintf(stderr, " -frames: Sets the number of frames to execute.\n");
773
std::fprintf(stderr, " -log <level>: Sets the log level. Defaults to verbose.\n");
774
std::fprintf(stderr, " -console: Enables console logging output.\n");
775
std::fprintf(stderr, " -pgxp: Enables PGXP.\n");
776
std::fprintf(stderr, " -pgxp-cpu: Forces PGXP CPU mode.\n");
777
std::fprintf(stderr, " -renderer <renderer>: Sets the graphics renderer. Default to software.\n");
778
std::fprintf(stderr, " -upscale <multiplier>: Enables upscaled rendering at the specified multiplier.\n");
779
std::fprintf(stderr, " --: Signals that no more arguments will follow and the remaining\n"
780
" parameters make up the filename. Use when the filename contains\n"
781
" spaces or starts with a dash.\n");
782
std::fprintf(stderr, "\n");
783
}
784
785
static std::optional<SystemBootParameters>& AutoBoot(std::optional<SystemBootParameters>& autoboot)
786
{
787
if (!autoboot)
788
autoboot.emplace();
789
790
return autoboot;
791
}
792
793
bool RegTestHost::ParseCommandLineParameters(int argc, char* argv[], std::optional<SystemBootParameters>& autoboot)
794
{
795
bool no_more_args = false;
796
for (int i = 1; i < argc; i++)
797
{
798
if (!no_more_args)
799
{
800
#define CHECK_ARG(str) !std::strcmp(argv[i], str)
801
#define CHECK_ARG_PARAM(str) (!std::strcmp(argv[i], str) && ((i + 1) < argc))
802
803
if (CHECK_ARG("-help"))
804
{
805
PrintCommandLineHelp(argv[0]);
806
return false;
807
}
808
else if (CHECK_ARG("-version"))
809
{
810
PrintCommandLineVersion();
811
return false;
812
}
813
else if (CHECK_ARG_PARAM("-dumpdir"))
814
{
815
s_dump_base_directory = argv[++i];
816
if (s_dump_base_directory.empty())
817
{
818
ERROR_LOG("Invalid dump directory specified.");
819
return false;
820
}
821
822
continue;
823
}
824
else if (CHECK_ARG_PARAM("-dumpinterval"))
825
{
826
s_frame_dump_interval = StringUtil::FromChars<u32>(argv[++i]).value_or(0);
827
if (s_frame_dump_interval <= 0)
828
{
829
ERROR_LOG("Invalid dump interval specified: {}", argv[i]);
830
return false;
831
}
832
833
continue;
834
}
835
else if (CHECK_ARG_PARAM("-frames"))
836
{
837
s_frames_to_run = StringUtil::FromChars<u32>(argv[++i]).value_or(0);
838
if (s_frames_to_run == 0)
839
{
840
ERROR_LOG("Invalid frame count specified: {}", argv[i]);
841
return false;
842
}
843
844
continue;
845
}
846
else if (CHECK_ARG_PARAM("-log"))
847
{
848
std::optional<Log::Level> level = Settings::ParseLogLevelName(argv[++i]);
849
if (!level.has_value())
850
{
851
ERROR_LOG("Invalid log level specified.");
852
return false;
853
}
854
855
Log::SetLogLevel(level.value());
856
Core::SetBaseStringSettingValue("Logging", "LogLevel", Settings::GetLogLevelName(level.value()));
857
continue;
858
}
859
else if (CHECK_ARG("-console"))
860
{
861
Log::SetConsoleOutputParams(true);
862
Core::SetBaseBoolSettingValue("Logging", "LogToConsole", true);
863
continue;
864
}
865
else if (CHECK_ARG_PARAM("-renderer"))
866
{
867
std::optional<GPURenderer> renderer = Settings::ParseRendererName(argv[++i]);
868
if (!renderer.has_value())
869
{
870
ERROR_LOG("Invalid renderer specified.");
871
return false;
872
}
873
874
Core::SetBaseStringSettingValue("GPU", "Renderer", Settings::GetRendererName(renderer.value()));
875
continue;
876
}
877
else if (CHECK_ARG_PARAM("-upscale"))
878
{
879
const u32 upscale = StringUtil::FromChars<u32>(argv[++i]).value_or(0);
880
if (upscale == 0)
881
{
882
ERROR_LOG("Invalid upscale value.");
883
return false;
884
}
885
886
INFO_LOG("Setting upscale to {}.", upscale);
887
Core::SetBaseIntSettingValue("GPU", "ResolutionScale", static_cast<s32>(upscale));
888
continue;
889
}
890
else if (CHECK_ARG_PARAM("-cpu"))
891
{
892
const std::optional<CPUExecutionMode> cpu = Settings::ParseCPUExecutionMode(argv[++i]);
893
if (!cpu.has_value())
894
{
895
ERROR_LOG("Invalid CPU execution mode.");
896
return false;
897
}
898
899
INFO_LOG("Setting CPU execution mode to {}.", Settings::GetCPUExecutionModeName(cpu.value()));
900
Core::SetBaseStringSettingValue("CPU", "ExecutionMode", Settings::GetCPUExecutionModeName(cpu.value()));
901
continue;
902
}
903
else if (CHECK_ARG("-pgxp"))
904
{
905
INFO_LOG("Enabling PGXP.");
906
Core::SetBaseBoolSettingValue("GPU", "PGXPEnable", true);
907
continue;
908
}
909
else if (CHECK_ARG("-pgxp-cpu"))
910
{
911
INFO_LOG("Enabling PGXP CPU mode.");
912
Core::SetBaseBoolSettingValue("GPU", "PGXPEnable", true);
913
Core::SetBaseBoolSettingValue("GPU", "PGXPCPU", true);
914
continue;
915
}
916
else if (CHECK_ARG("--"))
917
{
918
no_more_args = true;
919
continue;
920
}
921
else if (argv[i][0] == '-')
922
{
923
ERROR_LOG("Unknown parameter: '{}'", argv[i]);
924
return false;
925
}
926
927
#undef CHECK_ARG
928
#undef CHECK_ARG_PARAM
929
}
930
931
if (autoboot && !autoboot->path.empty())
932
autoboot->path += ' ';
933
AutoBoot(autoboot)->path += argv[i];
934
}
935
936
return true;
937
}
938
939
bool RegTestHost::SetNewDataRoot(const std::string& filename)
940
{
941
if (!s_dump_base_directory.empty())
942
{
943
std::string game_subdir = Path::SanitizeFileName(Path::GetFileTitle(filename));
944
INFO_LOG("Writing to subdirectory '{}'", game_subdir);
945
946
std::string dump_directory = Path::Combine(s_dump_base_directory, game_subdir);
947
if (!FileSystem::DirectoryExists(dump_directory.c_str()))
948
{
949
INFO_LOG("Creating directory '{}'...", dump_directory);
950
if (!FileSystem::CreateDirectory(dump_directory.c_str(), false))
951
Panic("Failed to create dump directory.");
952
}
953
954
// Switch to file logging.
955
INFO_LOG("Dumping frames to '{}'...", dump_directory);
956
957
const auto lock = Core::GetSettingsLock();
958
EmuFolders::DataRoot = std::move(dump_directory);
959
SettingsInterface& si = *Core::GetBaseSettingsLayer();
960
si.SetBoolValue("Logging", "LogToFile", true);
961
si.SetStringValue("Logging", "LogLevel", Settings::GetLogLevelName(Log::Level::Dev));
962
Settings::UpdateLogConfig(si);
963
}
964
965
return true;
966
}
967
968
std::string RegTestHost::GetFrameDumpPath(u32 frame)
969
{
970
return Path::Combine(EmuFolders::DataRoot, fmt::format("frame_{:05d}.png", frame));
971
}
972
973
int main(int argc, char* argv[])
974
{
975
CrashHandler::Install(&Bus::CleanupMemoryMap);
976
977
Error error;
978
if (!System::PerformEarlyHardwareChecks(&error) || !System::ProcessStartup(&error))
979
{
980
std::fprintf(stderr, "ERROR: ProcessStartup() failed: %s\n", error.GetDescription().c_str());
981
return EXIT_FAILURE;
982
}
983
984
if (!RegTestHost::InitializeFoldersAndConfig(&error))
985
{
986
std::fprintf(stderr, "ERROR: Failed to initialize config: %s\n", error.GetDescription().c_str());
987
return EXIT_FAILURE;
988
}
989
990
std::optional<SystemBootParameters> autoboot;
991
if (!RegTestHost::ParseCommandLineParameters(argc, argv, autoboot))
992
return EXIT_FAILURE;
993
994
if (!autoboot || autoboot->path.empty())
995
{
996
ERROR_LOG("No boot path specified.");
997
return EXIT_FAILURE;
998
}
999
1000
if (!RegTestHost::SetNewDataRoot(autoboot->path))
1001
return EXIT_FAILURE;
1002
1003
if (!System::CoreThreadInitialize(&error))
1004
{
1005
ERROR_LOG("CoreThreadInitialize() failed: {}", error.GetDescription());
1006
return EXIT_FAILURE;
1007
}
1008
1009
// Only one async worker, keep the CPU usage down so we can parallelize execution of regtest itself.
1010
RegTestHost::s_async_task_queue.SetWorkerCount(1);
1011
1012
RegTestHost::HookSignals();
1013
s_video_thread.Start(&RegTestHost::VideoThreadEntryPoint);
1014
1015
int result = -1;
1016
INFO_LOG("Trying to boot '{}'...", autoboot->path);
1017
if (!System::BootSystem(std::move(autoboot.value()), &error))
1018
{
1019
ERROR_LOG("Failed to boot system: {}", error.GetDescription());
1020
goto cleanup;
1021
}
1022
1023
if (System::IsReplayingGPUDump() && !s_dump_base_directory.empty())
1024
{
1025
INFO_LOG("Replaying GPU dump, dumping all frames.");
1026
s_frame_dump_interval = 1;
1027
s_frames_to_run = static_cast<u32>(System::GetGPUDumpFrameCount());
1028
}
1029
1030
if (s_frame_dump_interval > 0)
1031
{
1032
if (s_dump_base_directory.empty())
1033
{
1034
ERROR_LOG("Dump directory not specified.");
1035
goto cleanup;
1036
}
1037
1038
INFO_LOG("Dumping every {}th frame to '{}'.", s_frame_dump_interval, s_dump_base_directory);
1039
}
1040
1041
INFO_LOG("Running for {} frames...", s_frames_to_run);
1042
s_frames_remaining = s_frames_to_run;
1043
1044
{
1045
const Timer::Value start_time = Timer::GetCurrentValue();
1046
1047
System::Execute();
1048
1049
const Timer::Value elapsed_time = Timer::GetCurrentValue() - start_time;
1050
const double elapsed_time_ms = Timer::ConvertValueToMilliseconds(elapsed_time);
1051
INFO_LOG("Total execution time: {:.2f}ms, average frame time {:.2f}ms, {:.2f} FPS", elapsed_time_ms,
1052
elapsed_time_ms / static_cast<double>(s_frames_to_run),
1053
static_cast<double>(s_frames_to_run) / elapsed_time_ms * 1000.0);
1054
}
1055
1056
INFO_LOG("Exiting with success.");
1057
result = 0;
1058
1059
cleanup:
1060
if (s_video_thread.Joinable())
1061
{
1062
VideoThread::Internal::RequestShutdown();
1063
s_video_thread.Join();
1064
}
1065
1066
RegTestHost::s_async_task_queue.SetWorkerCount(0);
1067
1068
RegTestHost::ProcessCoreThreadEvents();
1069
System::CoreThreadShutdown();
1070
System::ProcessShutdown();
1071
return result;
1072
}
1073
1074