diff --git a/host/psxrecomp_codegen_host.c b/host/psxrecomp_codegen_host.c index 78c41f07..5aa70ce5 100644 --- a/host/psxrecomp_codegen_host.c +++ b/host/psxrecomp_codegen_host.c @@ -38,6 +38,9 @@ static int g_ready; static int g_relaunch_is_helper; /* Wizard BIOS pick (survives cwd-relative bios.cfg misses on Windows). */ static char g_wizard_bios[1100]; +/* Set when host_persist_setup receives an explicit bios_path (including "" + * for OpenBIOS). Distinguishes intentional OpenBIOS clear from "unset". */ +static int g_wizard_bios_explicit; static const char* cfg_or(const char* v, const char* d) { return (v && v[0]) ? v : d; @@ -884,6 +887,7 @@ static int host_persist_setup(void* ctx, const char* rom_path, char abs_bios[1100]; (void)ctx; if (bios_path) { + g_wizard_bios_explicit = 1; if (bios_path[0] && absolutize_existing_file(abs_bios, sizeof(abs_bios), bios_path)) { snprintf(g_wizard_bios, sizeof(g_wizard_bios), "%s", abs_bios); @@ -941,6 +945,22 @@ static void persist_relaunch_sidecars(const char* near_exe, if (g_wizard_bios[0] && absolutize_existing_file(abs_bios, sizeof(abs_bios), g_wizard_bios)) { snprintf(bios_line, sizeof(bios_line), "%s", abs_bios); + } else if (g_wizard_bios_explicit) { + /* Intentional OpenBIOS: clear sidecars — do not revive a stale + * bios.cfg (e.g. root-owned file that remove() could not delete). */ + write_sidecar_near_exe(near_exe, "bios.cfg", ""); + write_line_file("bios.cfg", ""); + if (g_project_root[0] && + join_path(project_sidecar, sizeof(project_sidecar), g_project_root, + "bios.cfg")) + write_line_file(project_sidecar, ""); + if (g_build_dir[0]) { + char build_exe[1200]; + if (join_path(build_exe, sizeof(build_exe), g_build_dir, + g_exe_basename)) + write_sidecar_near_exe(build_exe, "bios.cfg", ""); + } + return; } else { char line[1100]; line[0] = '\0'; diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index aecbc6c8..77a16cba 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -1042,6 +1042,8 @@ function(psxrecomp_add_runtime_target target) # can still use -DPSX_ENABLE_VULKAN=OFF to produce the inert stub explicitly. option(PSX_ENABLE_VULKAN "Build the Vulkan renderer backend when SDK tools are available" ON) if(PSX_ENABLE_VULKAN) + # $VULKAN_SDK first; else find_path. Unset before find_path — an empty + # normal _vk_inc makes find_path a no-op on modern CMake (Homebrew miss). set(_vk_inc "") if(DEFINED ENV{VULKAN_SDK}) if(EXISTS "$ENV{VULKAN_SDK}/Include/vulkan/vulkan.h") @@ -1051,6 +1053,8 @@ function(psxrecomp_add_runtime_target target) endif() endif() if(NOT _vk_inc) + unset(_vk_inc CACHE) + unset(_vk_inc) find_path(_vk_inc vulkan/vulkan.h) endif() find_program(GLSLC_EXE NAMES glslc diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 687cc293..67b0d066 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -95,6 +95,7 @@ extern "C" void psx_game_codegen_relaunch_or_exit(const char* disc_path); #include #include #include +#include #include #ifdef _WIN32 @@ -2557,33 +2558,57 @@ struct ControllerSource { }; struct PsxButtonMap { - uint16_t bit; + uint16_t bit; /* 0 = stick-direction slot (no digital bit alone) */ const char* ini_name; std::vector sources; + /* When set, a pressed stick-direction source also contributes this d-pad + * bit in digital mode (ls_* -> Up/Down/Left/Right). */ + uint16_t fold_bit = 0; }; static int controller_device_index = 0; /* Default ~10% of SDL axis range (32767). Overridden per-player via settings. */ static int controller_deadzone = 3277; static constexpr int kDefaultDeadzoneRaw = 3277; -static std::array controller_map = {{ - { PAD_UP, "up", {} }, - { PAD_DOWN, "down", {} }, - { PAD_LEFT, "left", {} }, - { PAD_RIGHT, "right", {} }, - { PAD_CROSS, "cross", {} }, - { PAD_CIRCLE, "circle", {} }, - { PAD_SQUARE, "square", {} }, - { PAD_TRIANGLE, "triangle", {} }, - { PAD_L1, "l1", {} }, - { PAD_R1, "r1", {} }, - { PAD_L2, "l2", {} }, - { PAD_R2, "r2", {} }, - { PAD_L3, "l3", {} }, - { PAD_R3, "r3", {} }, - { PAD_START, "start", {} }, - { PAD_SELECT, "select", {} }, +static constexpr int kControllerMapN = 24; +using ControllerMap = std::array; +static ControllerMap controller_map = {{ + { PAD_UP, "up", {}, 0 }, + { PAD_DOWN, "down", {}, 0 }, + { PAD_LEFT, "left", {}, 0 }, + { PAD_RIGHT, "right", {}, 0 }, + { PAD_CROSS, "cross", {}, 0 }, + { PAD_CIRCLE, "circle", {}, 0 }, + { PAD_SQUARE, "square", {}, 0 }, + { PAD_TRIANGLE, "triangle", {}, 0 }, + { PAD_L1, "l1", {}, 0 }, + { PAD_R1, "r1", {}, 0 }, + { PAD_L2, "l2", {}, 0 }, + { PAD_R2, "r2", {}, 0 }, + { PAD_L3, "l3", {}, 0 }, + { PAD_R3, "r3", {}, 0 }, + { PAD_START, "start", {}, 0 }, + { PAD_SELECT, "select", {}, 0 }, + /* Stick directions (analog axes by default; fold onto d-pad digitally). */ + { 0, "ls_up", {}, PAD_UP }, + { 0, "ls_down", {}, PAD_DOWN }, + { 0, "ls_left", {}, PAD_LEFT }, + { 0, "ls_right", {}, PAD_RIGHT }, + { 0, "rs_up", {}, 0 }, + { 0, "rs_down", {}, 0 }, + { 0, "rs_left", {}, 0 }, + { 0, "rs_right", {}, 0 }, }}; +/* Per-GUID overrides from input.ini [mapping.]. Absent GUID => global. */ +static std::unordered_map controller_maps_by_guid; + +static const ControllerMap& controller_map_for(const PlayerInput& p) { + if (p.guid[0]) { + auto it = controller_maps_by_guid.find(p.guid); + if (it != controller_maps_by_guid.end()) return it->second; + } + return controller_map; +} static std::string trim_copy(const std::string& s) { size_t first = 0; @@ -2776,34 +2801,49 @@ static std::vector parse_source_list(const std::string& value) return sources; } -static void set_default_controller_mapping(void) { - for (auto& entry : controller_map) entry.sources.clear(); - - auto set_sources = [](const char* name, const char* sources) { - for (auto& entry : controller_map) { - if (std::strcmp(entry.ini_name, name) == 0) { - entry.sources = parse_source_list(sources); - return; - } +static void apply_sources_to_map(ControllerMap& map, const char* name, + const char* sources) { + for (auto& entry : map) { + if (std::strcmp(entry.ini_name, name) == 0) { + entry.sources = parse_source_list(sources); + return; } - }; + } +} - set_sources("up", "dpup,lefty-"); - set_sources("down", "dpdown,lefty+"); - set_sources("left", "dpleft,leftx-"); - set_sources("right", "dpright,leftx+"); - set_sources("cross", "a"); - set_sources("circle", "b"); - set_sources("square", "x"); - set_sources("triangle", "y"); - set_sources("l1", "leftshoulder"); - set_sources("r1", "rightshoulder"); - set_sources("l2", "lefttrigger"); - set_sources("r2", "righttrigger"); - set_sources("l3", "leftstick"); - set_sources("r3", "rightstick"); - set_sources("start", "start"); - set_sources("select", "back"); +static void set_default_controller_mapping_into(ControllerMap& map) { + for (auto& entry : map) entry.sources.clear(); + /* D-pad and sticks are separate (launcher Gamepad Bindings layout). Digital + * mode still folds ls_* onto d-pad bits via fold_bit. */ + apply_sources_to_map(map, "up", "dpup"); + apply_sources_to_map(map, "down", "dpdown"); + apply_sources_to_map(map, "left", "dpleft"); + apply_sources_to_map(map, "right", "dpright"); + apply_sources_to_map(map, "cross", "a"); + apply_sources_to_map(map, "circle", "b"); + apply_sources_to_map(map, "square", "x"); + apply_sources_to_map(map, "triangle", "y"); + apply_sources_to_map(map, "l1", "leftshoulder"); + apply_sources_to_map(map, "r1", "rightshoulder"); + apply_sources_to_map(map, "l2", "lefttrigger"); + apply_sources_to_map(map, "r2", "righttrigger"); + apply_sources_to_map(map, "l3", "leftstick"); + apply_sources_to_map(map, "r3", "rightstick"); + apply_sources_to_map(map, "start", "start"); + apply_sources_to_map(map, "select", "back"); + apply_sources_to_map(map, "ls_up", "lefty-"); + apply_sources_to_map(map, "ls_down", "lefty+"); + apply_sources_to_map(map, "ls_left", "leftx-"); + apply_sources_to_map(map, "ls_right", "leftx+"); + apply_sources_to_map(map, "rs_up", "righty-"); + apply_sources_to_map(map, "rs_down", "righty+"); + apply_sources_to_map(map, "rs_left", "rightx-"); + apply_sources_to_map(map, "rs_right", "rightx+"); +} + +static void set_default_controller_mapping(void) { + set_default_controller_mapping_into(controller_map); + controller_maps_by_guid.clear(); } static std::string default_input_ini_text(void) { @@ -2812,6 +2852,7 @@ static std::string default_input_ini_text(void) { "; Sources use SDL/Xbox names: a,b,x,y,back,start,leftshoulder,rightshoulder,\n" "; lefttrigger,righttrigger,leftstick,rightstick (stick clicks -> L3/R3),\n" "; dpup,dpdown,dpleft,dpright,leftx-/leftx+/lefty-/lefty+.\n" + "; Optional per-device overrides: [mapping.].\n" "\n" "[controller]\n" "enabled = true\n" @@ -2819,10 +2860,10 @@ static std::string default_input_ini_text(void) { "deadzone = 3277\n" "\n" "[mapping]\n" - "up = dpup,lefty-\n" - "down = dpdown,lefty+\n" - "left = dpleft,leftx-\n" - "right = dpright,leftx+\n" + "up = dpup\n" + "down = dpdown\n" + "left = dpleft\n" + "right = dpright\n" "cross = a\n" "circle = b\n" "square = x\n" @@ -2834,7 +2875,15 @@ static std::string default_input_ini_text(void) { "l3 = leftstick\n" "r3 = rightstick\n" "start = start\n" - "select = back\n"; + "select = back\n" + "ls_up = lefty-\n" + "ls_down = lefty+\n" + "ls_left = leftx-\n" + "ls_right = leftx+\n" + "rs_up = righty-\n" + "rs_down = righty+\n" + "rs_left = rightx-\n" + "rs_right = rightx+\n"; } static void load_input_config(const char* argv0) { @@ -2846,15 +2895,20 @@ static void load_input_config(const char* argv0) { if (!fs::exists(config_path, ec)) { std::ofstream out(config_path, std::ios::binary); if (out) out << default_input_ini_text(); + psx_keybinds_init(argv0); return; } std::ifstream in(config_path); - if (!in) return; + if (!in) { + psx_keybinds_init(argv0); + return; + } bool controller_enabled = true; std::string section; std::string line; + ControllerMap* guid_target = nullptr; while (std::getline(in, line)) { size_t comment = line.find_first_of(";#"); if (comment != std::string::npos) line.resize(comment); @@ -2862,6 +2916,17 @@ static void load_input_config(const char* argv0) { if (line.empty()) continue; if (line.front() == '[' && line.back() == ']') { section = lower_copy(trim_copy(line.substr(1, line.size() - 2))); + guid_target = nullptr; + if (section.rfind("mapping.", 0) == 0) { + const std::string guid = section.substr(8); + if (!guid.empty()) { + /* Seed from the current global map (defaults + any + * [mapping] keys already parsed), then overlay GUID keys. */ + if (!controller_maps_by_guid.count(guid)) + controller_maps_by_guid[guid] = controller_map; + guid_target = &controller_maps_by_guid[guid]; + } + } continue; } size_t eq = line.find('='); @@ -2884,11 +2949,20 @@ static void load_input_config(const char* argv0) { break; } } + } else if (guid_target) { + for (auto& entry : *guid_target) { + if (key == entry.ini_name) { + entry.sources = parse_source_list(value); + break; + } + } } } if (!controller_enabled) { for (auto& entry : controller_map) entry.sources.clear(); + for (auto& kv : controller_maps_by_guid) + for (auto& entry : kv.second) entry.sources.clear(); } /* Configurable KEYBOARD keybinds (keybinds.ini, next to the exe) — separate @@ -2946,6 +3020,13 @@ static void open_player(PlayerInput& p, int self_slot) { if (p.handle) { SDL_Joystick* joy = SDL_GameControllerGetJoystick(p.handle); p.instance = joy ? SDL_JoystickInstanceID(joy) : -1; + /* Persist the GUID of the pad we actually opened so per-device + * [mapping.] lookups match the live hardware (including the + * first-available fallback when the saved GUID is offline). */ + { + SDL_JoystickGUID g = SDL_JoystickGetDeviceGUID(chosen); + SDL_JoystickGetGUIDString(g, p.guid, (int)sizeof(p.guid)); + } const char* name = SDL_GameControllerName(p.handle); std::fprintf(stdout, "psxrecomp runtime: opened controller for slot: %s\n", name ? name : "(unnamed)"); @@ -3083,19 +3164,23 @@ static bool source_is_stick_axis(const ControllerSource& s) { /* Build the active-low PSX button word from a controller's input.ini map. When * suppress_stick_axes is set (the pad is presenting as analog this frame), the * left/right analog-stick axes do NOT contribute button bits — see - * source_is_stick_axis above. Digital mode passes false, so the stick still - * folds onto the D-pad (its only outlet there). */ -static uint16_t controller_pad_buttons(SDL_GameController* h, bool suppress_stick_axes, + * source_is_stick_axis above. Digital mode passes false, so ls_* stick + * directions still fold onto the D-pad via fold_bit. */ +static uint16_t controller_pad_buttons(const ControllerMap& map, + SDL_GameController* h, + bool suppress_stick_axes, int deadzone_raw) { uint16_t buttons = 0xFFFF; /* all released */ if (!h) return buttons; - for (const auto& entry : controller_map) { + for (const auto& entry : map) { for (const auto& source : entry.sources) { if (suppress_stick_axes && source_is_stick_axis(source)) continue; - if (controller_source_pressed_h(h, source, deadzone_raw)) { + if (!controller_source_pressed_h(h, source, deadzone_raw)) continue; + if (entry.bit) buttons &= (uint16_t)~entry.bit; - break; - } + if (entry.fold_bit && !suppress_stick_axes) + buttons &= (uint16_t)~entry.fold_bit; + break; } } return buttons; @@ -3135,7 +3220,9 @@ static void axes_to_pad_pair(int16_t vx, int16_t vy, uint8_t* obx, uint8_t* oby, * 1..5 — selects which keybinds.ini section drives a keyboard port. */ static uint16_t pad_buttons_for(const PlayerInput& p, int player, bool suppress_stick_axes) { if (p.kind == 1) return pad_from_keyboard(player); - if (p.kind == 2) return controller_pad_buttons(p.handle, suppress_stick_axes, p.deadzone); + if (p.kind == 2) + return controller_pad_buttons(controller_map_for(p), p.handle, + suppress_stick_axes, p.deadzone); return 0xFFFF; } @@ -3163,17 +3250,83 @@ static void pad_sticks_for(const PlayerInput& p, int player, uint8_t out[4], boo return; } if (p.kind == 2 && p.handle) { - axes_to_pad_pair(SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTX), - SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTY), - &out[0], &out[1], p.deadzone); - axes_to_pad_pair(SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_RIGHTX), - SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_RIGHTY), - &out[2], &out[3], p.deadzone); + const ControllerMap& map = controller_map_for(p); + auto find_entry = [&](const char* name) -> const PsxButtonMap* { + for (const auto& e : map) + if (std::strcmp(e.ini_name, name) == 0) return &e; + return nullptr; + }; + /* Default left/right stick axis layout → smooth radial deadzone path. + * Any remapped stick direction falls back to discrete full deflection. */ + auto sticks_default_axes = [&](const char* up, const char* down, + const char* left, const char* right, + SDL_GameControllerAxis ax, + SDL_GameControllerAxis ay) -> bool { + auto is_axis = [&](const char* n, int axis_id, ControllerSource::Kind k) { + const PsxButtonMap* e = find_entry(n); + if (!e || e->sources.size() != 1) return false; + const auto& s = e->sources[0]; + return s.kind == k && s.id == axis_id; + }; + return is_axis(up, ay, ControllerSource::Kind::AxisNegative) && + is_axis(down, ay, ControllerSource::Kind::AxisPositive) && + is_axis(left, ax, ControllerSource::Kind::AxisNegative) && + is_axis(right, ax, ControllerSource::Kind::AxisPositive); + }; + auto apply_discrete_stick = [&](const char* up, const char* down, + const char* left, const char* right, + uint8_t* bx, uint8_t* by) { + *bx = *by = 0x80; + auto pressed = [&](const char* n) { + const PsxButtonMap* e = find_entry(n); + if (!e) return false; + for (const auto& s : e->sources) + if (controller_source_pressed_h(p.handle, s, p.deadzone)) + return true; + return false; + }; + if (pressed(left)) *bx = 0x00; + if (pressed(right)) *bx = 0xFF; + if (pressed(up)) *by = 0x00; + if (pressed(down)) *by = 0xFF; + }; + + if (sticks_default_axes("ls_up", "ls_down", "ls_left", "ls_right", + SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY)) { + axes_to_pad_pair(SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTX), + SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTY), + &out[0], &out[1], p.deadzone); + } else { + apply_discrete_stick("ls_up", "ls_down", "ls_left", "ls_right", + &out[0], &out[1]); + } + if (sticks_default_axes("rs_up", "rs_down", "rs_left", "rs_right", + SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY)) { + axes_to_pad_pair(SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_RIGHTX), + SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_RIGHTY), + &out[2], &out[3], p.deadzone); + } else { + apply_discrete_stick("rs_up", "rs_down", "rs_left", "rs_right", + &out[2], &out[3]); + } if (fold_dpad) { - if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) out[0] = 0x00; - if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) out[0] = 0xFF; - if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_UP)) out[1] = 0x00; - if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) out[1] = 0xFF; + /* Fold physical D-pad (mapped "up"/etc. button sources that are + * d-pad buttons) — prefer the mapped sources over hardcoded buttons + * so a remapped d-pad still folds onto the left stick. */ + auto dpad_pressed = [&](const char* n) { + const PsxButtonMap* e = find_entry(n); + if (!e) return false; + for (const auto& s : e->sources) { + if (source_is_stick_axis(s)) continue; + if (controller_source_pressed_h(p.handle, s, p.deadzone)) + return true; + } + return false; + }; + if (dpad_pressed("left")) out[0] = 0x00; + if (dpad_pressed("right")) out[0] = 0xFF; + if (dpad_pressed("up")) out[1] = 0x00; + if (dpad_pressed("down")) out[1] = 0xFF; } } } @@ -3250,7 +3403,15 @@ static uint16_t dev_all_controllers_buttons(bool suppress_stick_axes) { SDL_JoystickID inst = SDL_JoystickGetDeviceInstanceID(i); SDL_GameController* h = SDL_GameControllerFromInstanceID(inst); if (!h) h = SDL_GameControllerOpen(i); /* open once; SDL keeps it */ - if (h) btn &= controller_pad_buttons(h, suppress_stick_axes, controller_deadzone); + if (!h) continue; + PlayerInput tmp; + tmp.kind = 2; + { + SDL_JoystickGUID g = SDL_JoystickGetDeviceGUID(i); + SDL_JoystickGetGUIDString(g, tmp.guid, (int)sizeof(tmp.guid)); + } + btn &= controller_pad_buttons(controller_map_for(tmp), h, + suppress_stick_axes, controller_deadzone); } return btn; } @@ -5150,21 +5311,24 @@ namespace { int ae_bios_verify(const char* bios_path, RecompLauncherCBiosVerify* out) { if (!out) return 0; std::memset(out, 0, sizeof(*out)); - /* Empty path = use bundled OpenBIOS when this title allows it. */ + /* Empty path = use bundled OpenBIOS when this title allows it. + * Never needs_regen: switching to OpenBIOS is always a hot-swap. */ if (!bios_path || !bios_path[0]) { + out->needs_regen = 0; if (s_openbios_allowed && psx_bios_bundled()) { out->ok = 1; std::snprintf(out->detail, sizeof(out->detail), "Using bundled OpenBIOS."); return 1; } - /* Setup host: no BIOS backends linked yet — Generate & rebuild - * will emit OpenBIOS C from the redistributable openbios.bin. */ + /* Setup host: no BIOS backends linked yet — first Generate for + * game C will also emit OpenBIOS from redistributable openbios.bin. + * Selecting OpenBIOS itself still does not require a BIOS regen. */ if (s_openbios_allowed && psx_bios_registry_count == 0) { out->ok = 1; std::snprintf(out->detail, sizeof(out->detail), - "OpenBIOS will be generated on first " - "Generate & rebuild (optional: pick SCPH1001)."); + "Using OpenBIOS (emitted on first Generate & " + "rebuild with game C; optional: pick SCPH1001)."); return 1; } std::snprintf(out->detail, sizeof(out->detail),