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