From e4e2aa7a7d56c2a4d829b789bb8b63266ba7873a Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 28 Jul 2026 00:20:37 -0700 Subject: [PATCH] runtime: add per-game radial stick response --- recompiler/src/config_loader.cpp | 8 ++++ recompiler/src/config_loader.h | 6 +++ runtime/include/psx_stick.h | 26 ++++++++++++ runtime/runtime.cmake | 1 + runtime/src/main.cpp | 33 +++------------ runtime/src/psx_stick.c | 73 ++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 26 deletions(-) create mode 100644 runtime/include/psx_stick.h create mode 100644 runtime/src/psx_stick.c diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index f264b13e..a83c6f0b 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -608,6 +608,14 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& rt.deadzone = static_cast(n); rt.has_deadzone = true; } + if (ct.contains("anti_deadzone")) { + const auto n = toml::find(ct, "anti_deadzone"); + if (n < 0 || n > 32767) + throw std::runtime_error(fmt::format( + "[controller] anti_deadzone out of range (0..32767): {}", n)); + rt.anti_deadzone = static_cast(n); + rt.has_anti_deadzone = true; + } } return rt; diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 4df1363f..51d5d864 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -447,6 +447,12 @@ struct RuntimeConfig { bool has_deadzone = false; int deadzone = 0; + // anti_deadzone: minimum radial analog output after leaving deadzone, in + // raw SDL axis units (0..32767). This is a game-owned response setting used + // to compensate a title's own internal stick deadzone. Absent => 0. + bool has_anti_deadzone = false; + int anti_deadzone = 0; + }; // One entry from [[recompiler.bios_vectors]]. diff --git a/runtime/include/psx_stick.h b/runtime/include/psx_stick.h new file mode 100644 index 00000000..6349dfb8 --- /dev/null +++ b/runtime/include/psx_stick.h @@ -0,0 +1,26 @@ +#ifndef PSX_STICK_H +#define PSX_STICK_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Convert one SDL stick vector to the two unsigned DualShock axis bytes. + * + * deadzone and anti_deadzone use SDL's 0..32767 radial magnitude scale. + * Values inside deadzone are centred. Outside it, magnitude is remapped from + * anti_deadzone to full travel while direction is preserved. anti_deadzone=0 + * is an ordinary rescaled radial deadzone. + */ +void psx_stick_to_dualshock(int16_t x, int16_t y, + int deadzone, int anti_deadzone, + uint8_t *out_x, uint8_t *out_y); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index ba901aed..ed534df6 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -111,6 +111,7 @@ endif() set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/main.cpp + ${PSXRECOMP_ROOT}/runtime/src/psx_stick.c ${PSXRECOMP_ROOT}/runtime/src/memory.c ${PSXRECOMP_ROOT}/runtime/src/gpu.c ${PSXRECOMP_ROOT}/runtime/src/ws_ui_group.c diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 6cc0b88e..aca710f7 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -66,6 +66,7 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "iso_reader.h" /* text-image guard: extract the boot EXE from the disc */ #include "launcher_device.h" /* recomp-ui controller-source round-trip */ #include "psx_keybinds.h" /* configurable keyboard->DualShock keybinds (keybinds.ini) */ +#include "psx_stick.h" /* radial SDL-stick -> DualShock response transform */ #if defined(RECOMP_LAUNCHER) #include "recomp_launcher.h" /* shared recomp-ui Dear ImGui launcher */ #include "launcher_profile.h" /* per-system variant profile (theme/caps bundle) */ @@ -2103,6 +2104,7 @@ struct PsxButtonMap { static int controller_device_index = 0; static int controller_deadzone = 12000; +static int controller_anti_deadzone = 0; static std::array controller_map = {{ { PAD_UP, "up", {} }, { PAD_DOWN, "down", {} }, @@ -2608,33 +2610,10 @@ static uint16_t controller_pad_buttons(SDL_GameController* h, bool suppress_stic return buttons; } -/* Radial deadzone: process a stick's X and Y together so the dead region is a - * small CIRCLE, not a per-axis square. Travel within controller_deadzone reads - * centred (0x80/0x80); beyond it the vector MAGNITUDE is rescaled to the full - * range along the stick's true direction, so diagonals are preserved (a - * per-axis deadzone snaps diagonals toward the cardinals and feels notchy — - * bad for directional analog like Ape Escape's net swing). The magnitude is - * capped at 32767 before rescale so a full-diagonal push (raw mag ~46341) maps - * to ~0x9E/0x9E per axis — the circular gate a real DualShock stick reports, - * not 0xFF/0xFF. At dz==0 it reduces to a plain magnitude-preserving map. */ static void axes_to_pad_pair(int16_t vx, int16_t vy, uint8_t* obx, uint8_t* oby) { - const double dz = controller_deadzone; - double x = vx, y = vy; - double mag = std::sqrt(x * x + y * y); /* 0 .. ~46341 */ - if (mag <= dz || mag <= 0.0) { *obx = 0x80; *oby = 0x80; return; } - double capped = mag > 32767.0 ? 32767.0 : mag; - double range = 32767.0 - dz; - if (range < 1.0) range = 1.0; - double newmag = (capped - dz) * 32767.0 / range; /* 0 .. 32767 */ - double scale = newmag / mag; /* along true direction */ - int sx = (int)std::lround(x * scale); - int sy = (int)std::lround(y * scale); - if (sx > 32767) sx = 32767; else if (sx < -32768) sx = -32768; - if (sy > 32767) sy = 32767; else if (sy < -32768) sy = -32768; - int bx = (sx + 32768) >> 8; - int by = (sy + 32768) >> 8; - *obx = (uint8_t)(bx < 0 ? 0 : (bx > 255 ? 255 : bx)); - *oby = (uint8_t)(by < 0 ? 0 : (by > 255 ? 255 : by)); + psx_stick_to_dualshock(vx, vy, + controller_deadzone, controller_anti_deadzone, + obx, oby); } /* Buttons for a player's selected device (0xFFFF = none pressed). `player` is @@ -5154,6 +5133,8 @@ int main(int argc, char** argv) { ctrl_lock_mode = gc.runtime.controller_lock_mode; ctrl_lock_device = gc.runtime.controller_lock_device; if (gc.runtime.has_deadzone) resolved_deadzone = gc.runtime.deadzone; + if (gc.runtime.has_anti_deadzone) + controller_anti_deadzone = gc.runtime.anti_deadzone; { const char *e = std::getenv("PSX_GL_FORCE_CPU_PRESENT"); if (e && e[0] && e[0] != '0') g_gl_fbo_present = 0; } game_entry_pc = gc.entry_pc; diff --git a/runtime/src/psx_stick.c b/runtime/src/psx_stick.c new file mode 100644 index 00000000..22ef4783 --- /dev/null +++ b/runtime/src/psx_stick.c @@ -0,0 +1,73 @@ +#include "psx_stick.h" + +#include + +static int clamp_axis_setting(int value) +{ + if (value < 0) + return 0; + if (value > 32767) + return 32767; + return value; +} + +static uint8_t signed_axis_to_byte(int value) +{ + int byte = (value + 32768) >> 8; + if (byte < 0) + byte = 0; + if (byte > 255) + byte = 255; + return (uint8_t)byte; +} + +void psx_stick_to_dualshock(int16_t x, int16_t y, + int deadzone, int anti_deadzone, + uint8_t *out_x, uint8_t *out_y) +{ + const double raw_x = (double)x; + const double raw_y = (double)y; + const double magnitude = sqrt(raw_x * raw_x + raw_y * raw_y); + double source_magnitude; + double travel; + double output_magnitude; + double scale; + int scaled_x; + int scaled_y; + + deadzone = clamp_axis_setting(deadzone); + anti_deadzone = clamp_axis_setting(anti_deadzone); + + if (deadzone == 32767 || + magnitude <= (double)deadzone || magnitude <= 0.0) { + *out_x = 0x80; + *out_y = 0x80; + return; + } + + source_magnitude = magnitude > 32767.0 ? 32767.0 : magnitude; + travel = (source_magnitude - (double)deadzone) / + (32767.0 - (double)deadzone); + if (travel < 0.0) + travel = 0.0; + if (travel > 1.0) + travel = 1.0; + + output_magnitude = (double)anti_deadzone + + travel * (32767.0 - (double)anti_deadzone); + scale = output_magnitude / magnitude; + scaled_x = (int)lround(raw_x * scale); + scaled_y = (int)lround(raw_y * scale); + + if (scaled_x < -32768) + scaled_x = -32768; + if (scaled_x > 32767) + scaled_x = 32767; + if (scaled_y < -32768) + scaled_y = -32768; + if (scaled_y > 32767) + scaled_y = 32767; + + *out_x = signed_axis_to_byte(scaled_x); + *out_y = signed_axis_to_byte(scaled_y); +}