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