#include "common/archive.h" #include "common/common.h" #include "common/dateTime.h" #include "common/debug.h" #include "common/file.h" #include "common/stringUtils.h" #include "common/threads.h" #include "common/virtualMemory.h" #include "emulator.h" #include "kytyGitVersion.h" #include #include #include #include #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(); 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, ELF, or ZArchive to load.\n"); ::printf(" --game-patch ETAHen cheat file.\n"); ::printf(" --addcont-dir Add-ons (DLC) folder (default: none).\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(" --user-id Local user ID. Default: %d.\n", Config::DEFAULT_USER_ID); ::printf(" --mic Capture from this microphone; omit for silence.\n"); ::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n"); ::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 50.\n"); ::printf(" --controller-vibration <0-100> DualSense vibration intensity. Default: 100.\n"); ::printf(" --keyboard-player <0-4> Keyboard player; 0 shares player 1 with the gamepad. Default: 0.\n"); ::printf( " --present-mode Fifo, Mailbox, or Immediate. Default: Mailbox.\n"); ::printf( " --gpu Vulkan physical device index. Default: auto.\n"); ::printf(" --fullscreen Run in borderless desktop fullscreen.\n"); ::printf(" --hdr Enable HDR10 output when supported.\n"); ::printf( " --hide-cursor Hide the cursor after 2 s idle. Default: off.\n"); ::printf(" --no-splash Disable the startup splash screen.\n"); ::printf(" --vr Enable the virtual VR headset.\n"); ::printf(" --amd-cpu Apply AMD CPU instruction patches.\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(" --network-connected Use the host local network (default: false).\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(" --tessellation Draw tessellation patches; skipped by default.\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(" --profile Enable the Tracy profiler.\n"); ::printf(" --spirv-debug-printf Enable SPIR-V debug printf.\n"); ::printf( " --readback-linear-images Read back writable linear images on submit.\n"); ::printf( " --sync-raw-image-buffers Synchronize raw reads of GPU images. Default: false.\n"); ::printf( " --trophy-notifications Show trophy unlock toasts and play their sound.\n"); ::printf( " --trophy-duration Trophy toast duration, 5-10. Default: 6.\n"); ::printf(" --perf-overlay Show the performance panel (F2 or Cmd+P).\n"); ::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n"); ::printf( " --skip-notice-screen Skip startup logos and notices in supported games.\n" " Default: false.\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 ParseUint32(const std::string& value, uint32_t& out) { uint32_t number = 0; auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number); if (error != std::errc {} || end != value.data() + value.size()) { return false; } out = number; return true; } static bool ParseControllerColor(const std::string& value, Config::ControllerColor& out) { if (value.size() != 7 || value[0] != '#') { return false; } uint32_t rgb = 0; auto [end, error] = std::from_chars(value.data() + 1, value.data() + value.size(), rgb, 16); if (error != std::errc {} || end != value.data() + value.size()) { return false; } out = {static_cast(rgb >> 16), static_cast(rgb >> 8), static_cast(rgb)}; return true; } static bool ParseInt32(const std::string& value, int32_t& out) { int32_t number = 0; auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number); if (error != std::errc {} || end != value.data() + value.size()) { return false; } out = number; return true; } static bool ParseUserId(const std::string& value, int32_t& out) { int32_t user_id = 0; auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), user_id); if (error != std::errc {} || end != value.data() + value.size() || !Config::IsConfiguredUserIdValid(user_id)) { return false; } out = user_id; 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 == "--hdr") { options.config.hdr_enabled = true; continue; } if (arg == "--hide-cursor") { options.config.hide_cursor_enabled = true; continue; } if (arg == "--no-splash") { options.config.splash_enabled = false; continue; } if (arg == "--vr") { options.config.vr_enabled = true; continue; } if (arg == "--amd-cpu") { options.config.amd_cpu_enabled = true; continue; } if (arg == "--playgo-hack") { options.config.playgo_hack_enabled = true; continue; } if (arg == "--tessellation") { options.config.tessellation_enabled = true; continue; } if (arg == "--profile") { options.config.profiler_enabled = true; continue; } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS if (arg == "--redzone") { options.config.red_zone_protection_enabled = true; continue; } #endif if (!arg.starts_with("--")) { ::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); const auto path = Common::PathFromUtf8(value); if (Common::File::IsDirectoryExisting(path)) { options.app0_dir = path; options.elf = "/app0/eboot.bin"; } else if (Common::IsSupportedArchive(path) && Common::File::IsFileExisting(path)) { const auto root = Common::MakeArchivePath(path); if (!Common::File::IsFileExisting(root / "eboot.bin")) { ::printf("Archive does not contain eboot.bin: %s\n", value.c_str()); return false; } options.app0_dir = root; options.elf = "/app0/eboot.bin"; } else if (Common::File::IsFileExisting(path)) { options.app0_dir = path.parent_path(); if (options.app0_dir.empty()) { options.app0_dir = "."; } options.elf = std::filesystem::path("/app0") / path.filename(); } else { ::printf("--game must point to an existing directory, ELF, or archive: %s\n", value.c_str()); return false; } } else if (arg == "--guest-exec") { options.elf = Common::PathFromUtf8(Common::FixFilenameSlash(value)); options.config.guest_arguments.assign(argv + i + 1, argv + argc); break; } 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); const auto path = Common::PathFromUtf8(value); if (!Common::File::IsFileExisting(path)) { ::printf("--game-patch must point to an existing file: %s\n", value.c_str()); return false; } options.game_patch = path; } else if (arg == "--addcont-dir") { if (!options.addcont_dir.empty()) { ::printf("--addcont-dir can only be specified once\n"); return false; } options.addcont_dir = Common::PathFromUtf8(Common::FixFilenameSlash(value)); } else if (arg == "--screen-width") { if (!ParseUint32(value, options.config.screen_width) || options.config.screen_width == 0) { ::printf("invalid screen width: %s\n", value.c_str()); return false; } } else if (arg == "--screen-height") { if (!ParseUint32(value, options.config.screen_height) || options.config.screen_height == 0) { ::printf("invalid screen height: %s\n", value.c_str()); return false; } } 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 == "--user-id") { if (!ParseUserId(value, options.config.user_id)) { ::printf("invalid user ID: %s\n", value.c_str()); return false; } } else if (arg == "--mic") { options.config.audio_input_device = value; } else if (arg == "--controller-color") { Config::ControllerColor color {}; if (!ParseControllerColor(value, color)) { ::printf("invalid controller color (expected #RRGGBB): %s\n", value.c_str()); return false; } options.config.controller_color = color; } else if (arg == "--controller-volume") { if (!ParseUint32(value, options.config.controller_speaker_volume) || options.config.controller_speaker_volume > 100) { ::printf("invalid controller volume: %s\n", value.c_str()); return false; } } else if (arg == "--controller-vibration") { if (!ParseUint32(value, options.config.controller_vibration_intensity) || options.config.controller_vibration_intensity > 100) { ::printf("invalid controller vibration intensity: %s\n", value.c_str()); return false; } } else if (arg == "--keyboard-player") { if (!ParseUint32(value, options.config.keyboard_player) || options.config.keyboard_player > static_cast(Config::MAX_PLAYERS)) { ::printf("invalid keyboard player: %s\n", value.c_str()); return false; } } 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 == "--gpu") { if (!ParseInt32(value, options.config.gpu_index)) { ::printf("invalid gpu index: %s\n", value.c_str()); return false; } } else if (arg == "--vblank-frequency") { if (!ParseUint32(value, options.config.vblank_frequency)) { ::printf("invalid vblank frequency: %s\n", value.c_str()); return false; } } else if (arg == "--trophy-duration") { if (!ParseUint32(value, options.config.trophy_duration_seconds)) { ::printf("invalid trophy duration: %s\n", value.c_str()); return false; } } 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 == "--network-connected") { if (!ParseBool(value, options.config.network_connected)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), 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 = Common::PathFromUtf8(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 = Common::PathFromUtf8(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 = Common::PathFromUtf8(value); } 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 == "--sync-raw-image-buffers") { if (!ParseBool(value, options.config.sync_raw_image_buffers)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--trophy-notifications") { if (!ParseBool(value, options.config.trophy_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--skip-notice-screen") { if (!ParseBool(value, options.config.skip_notice_screen)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--perf-overlay") { if (!ParseBool(value, options.config.perf_overlay_enabled)) { ::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()); } static 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; } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS int wmain(int argc, wchar_t* argv[]) { std::vector utf8_args; utf8_args.reserve(static_cast(argc)); for (int index = 0; index < argc; index++) { const std::wstring_view wide(argv[index]); const std::u16string utf16(wide.begin(), wide.end()); utf8_args.push_back(Common::Utf16ToUtf8(utf16)); } std::vector utf8_argv; utf8_argv.reserve(utf8_args.size()); for (auto& argument: utf8_args) { utf8_argv.push_back(argument.data()); } return Main(argc, utf8_argv.data()); } #else int main(int argc, char* argv[]) { return Main(argc, argv); } #endif