#include "common/common.h" #include "common/dateTime.h" #include "common/debug.h" #include "common/file.h" #include "common/magicEnum.h" #include "common/platform/sysDbg.h" #include "common/stringUtils.h" #include "common/threads.h" #include "common/virtualMemory.h" #include "emulator.h" #include "kytyGitVersion.h" #include #include #include using namespace Common; using namespace Emulator; static std::string GetBuildString() { Date date = Date::FromMacros(std::string(__DATE__)); #if KYTY_BUILD == KYTY_BUILD_DEBUG std::string type = "Debug"; #elif KYTY_BUILD == KYTY_BUILD_RELEASE std::string type = "Release"; #else std::string type = "????"; #endif std::string compiler = Debug::GetCompiler() + "-" + Debug::GetLinker() + "-" + Debug::GetBitness(); std::string str = fmt::format("{}, {}, ver = {}, git = {}, date = {}", type.c_str(), compiler.c_str(), KYTY_VERSION, KYTY_GIT_VERSION, date.ToString().c_str()); return str; } static void PrintUsage() { ::printf("%s\n", GetBuildString().c_str()); ::printf("kyty_emulator --game [options]\n\n"); ::printf("Options:\n"); ::printf(" --game Game directory or ELF to load.\n"); ::printf( " --game-patch Validated patch plan to apply before entry.\n"); ::printf(" --screen-width Window width. Default: 1280.\n"); ::printf(" --screen-height Window height. Default: 720.\n"); ::printf( " --user-name Local user name (1-16 bytes). Default: Kyty.\n"); ::printf(" --present-mode Fifo, Mailbox, or Immediate. Default: Fifo.\n"); ::printf(" --fullscreen Run in borderless desktop fullscreen.\n"); ::printf(" --vblank-frequency Virtual vblank frequency. Default: 60.\n"); ::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n"); ::printf(" --vulkan-validation Enable Vulkan validation.\n"); ::printf(" --gpu-assisted-validation Bounds-check shader accesses on the GPU.\n" " Implies --vulkan-validation; very slow.\n"); ::printf(" --shader-validation Enable shader validation.\n"); ::printf(" --shader-optimization-type None, Size, or Performance.\n"); ::printf(" --shader-log-direction Silent, Console, or File.\n"); ::printf(" --shader-log-folder Shader log output folder.\n"); ::printf(" --command-buffer-dump Enable command buffer dumps.\n"); ::printf(" --command-buffer-dump-folder Command buffer dump folder.\n"); ::printf(" --graphics-debug-dump Enable graphics debug dumps.\n"); ::printf(" --printf-direction Silent, Console, or File.\n"); ::printf(" --printf-output-file Guest printf output file.\n"); ::printf(" --profiler-direction None or Network.\n"); ::printf(" --spirv-debug-printf Enable SPIR-V debug printf.\n"); ::printf( " --readback-linear-images Read back writable linear images on submit.\n"); ::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n"); #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS ::printf(" --redzone Protect the guest SysV red zone.\n"); #endif ::printf(" --keymap DualSense mapping; may be repeated.\n"); ::printf(" --rd Enable RenderDoc capture.\n"); } static bool NextArg(int argc, char* argv[], int& index, std::string& out) { if (index + 1 >= argc) { return false; } index++; out = argv[index]; return true; } static bool ParseBool(const std::string& value, bool& out) { if (Common::EqualNoCase(value, "true") || value == "1" || Common::EqualNoCase(value, "yes") || Common::EqualNoCase(value, "on")) { out = true; return true; } if (Common::EqualNoCase(value, "false") || value == "0" || Common::EqualNoCase(value, "no") || Common::EqualNoCase(value, "off")) { out = false; return true; } return false; } template static bool ParseEnum(const std::string& value, E& out) { auto enum_value = magic_enum::enum_cast(value.c_str()); if (!enum_value.has_value()) { return false; } out = enum_value.value(); return true; } static bool ParseConsoleLanguage(const std::string& value, uint32_t& out) { uint32_t language = 0; auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), language); if (error != std::errc {} || end != value.data() + value.size() || language > Config::MAX_CONSOLE_LANGUAGE) { return false; } out = language; return true; } static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_help) { show_help = false; for (int i = 1; i < argc; i++) { std::string arg = std::string(argv[i]); std::string value; if (arg == "--help" || arg == "-h") { show_help = true; continue; } if (arg == "--rd") { options.config.renderdoc_enabled = true; continue; } if (arg == "--fullscreen") { options.config.fullscreen_enabled = true; continue; } if (arg == "--playgo-hack") { options.config.playgo_hack_enabled = true; continue; } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS if (arg == "--redzone") { options.config.red_zone_protection_enabled = true; continue; } #endif if (!Common::StartsWith(arg, "--")) { ::printf("game input must be provided with --game\n"); return false; } if (!NextArg(argc, argv, i, value)) { ::printf("missing value for %s\n", arg.c_str()); return false; } if (arg == "--game") { if (!options.app0_dir.empty()) { ::printf("--game can only be specified once\n"); return false; } value = Common::FixFilenameSlash(value); if (Common::File::IsDirectoryExisting(value)) { options.app0_dir = value; options.elf = "/app0/eboot.bin"; } else if (Common::File::IsFileExisting(value)) { options.app0_dir = Common::DirectoryWithoutFilename(value); if (options.app0_dir.empty()) { options.app0_dir = "."; } options.elf = "/app0/" + Common::FilenameWithoutDirectory(value); } else { ::printf("--game must point to an existing directory or ELF: %s\n", value.c_str()); return false; } } else if (arg == "--game-patch") { if (!options.game_patch.empty()) { ::printf("--game-patch can only be specified once\n"); return false; } value = Common::FixFilenameSlash(value); if (!Common::File::IsFileExisting(value)) { ::printf("--game-patch must point to an existing file: %s\n", value.c_str()); return false; } options.game_patch = value; } else if (arg == "--screen-width") { options.config.screen_width = static_cast(Common::ToInt32(value)); } else if (arg == "--screen-height") { options.config.screen_height = static_cast(Common::ToInt32(value)); } else if (arg == "--user-name") { if (value.empty() || value.size() > Config::MAX_USER_NAME_LENGTH) { ::printf("invalid user name: must contain 1-%zu bytes\n", Config::MAX_USER_NAME_LENGTH); return false; } options.config.user_name = value; } else if (arg == "--present-mode") { if (!ParseEnum(value, options.config.present_mode)) { ::printf("invalid present mode: %s\n", value.c_str()); return false; } } else if (arg == "--vblank-frequency") { const int32_t vblank_frequency = Common::ToInt32(value); options.config.vblank_frequency = static_cast(vblank_frequency < 0 ? 0 : vblank_frequency); } else if (arg == "--console-language") { if (!ParseConsoleLanguage(value, options.config.console_language)) { ::printf("invalid console language: %s\n", value.c_str()); return false; } } else if (arg == "--vulkan-validation") { if (!ParseBool(value, options.config.vulkan_validation_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--gpu-assisted-validation") { if (!ParseBool(value, options.config.gpu_assisted_validation_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--shader-validation") { if (!ParseBool(value, options.config.shader_validation_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--shader-optimization-type") { if (!ParseEnum(value, options.config.shader_optimization_type)) { ::printf("invalid shader optimization type: %s\n", value.c_str()); return false; } } else if (arg == "--shader-log-direction") { if (!ParseEnum(value, options.config.shader_log_direction)) { ::printf("invalid shader log direction: %s\n", value.c_str()); return false; } } else if (arg == "--shader-log-folder") { options.config.shader_log_folder = value; } else if (arg == "--command-buffer-dump") { if (!ParseBool(value, options.config.command_buffer_dump_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--command-buffer-dump-folder") { options.config.command_buffer_dump_folder = value; } else if (arg == "--graphics-debug-dump") { if (!ParseBool(value, options.config.graphics_debug_dump_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--printf-direction") { if (!ParseEnum(value, options.config.printf_direction)) { ::printf("invalid printf direction: %s\n", value.c_str()); return false; } } else if (arg == "--printf-output-file") { options.config.printf_output_file = value; } else if (arg == "--profiler-direction") { if (!ParseEnum(value, options.config.profiler_direction)) { ::printf("invalid profiler direction: %s\n", value.c_str()); return false; } } else if (arg == "--spirv-debug-printf") { if (!ParseBool(value, options.config.spirv_debug_printf_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--readback-linear-images") { if (!ParseBool(value, options.config.readback_linear_images)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--keymap") { const auto split = value.find('='); if (split == std::string::npos || split == 0 || split + 1 == value.size()) { ::printf("invalid keymap: %s\n", value.c_str()); return false; } options.config.keymap.push_back(value); } else { ::printf("unknown option: %s\n", arg.c_str()); return false; } } if (options.config.gpu_assisted_validation_enabled) { options.config.vulkan_validation_enabled = true; } return show_help || (!options.app0_dir.empty() && !options.elf.empty()); } int main(int argc, char* argv[]) { VirtualMemory::Init(); InitializeThreads(); RunOptions options; bool show_help = false; if (argc < 2) { PrintUsage(); return 0; } if (!ParseArgs(argc, argv, options, show_help)) { PrintUsage(); return 1; } if (show_help) { PrintUsage(); return 0; } Run(options); return 0; }