runtime: add per-game radial stick response

This commit is contained in:
Matthew Stanley
2026-07-28 00:20:37 -07:00
parent 64097606fb
commit e4e2aa7a7d
6 changed files with 121 additions and 26 deletions
+8
View File
@@ -608,6 +608,14 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
rt.deadzone = static_cast<int>(n);
rt.has_deadzone = true;
}
if (ct.contains("anti_deadzone")) {
const auto n = toml::find<int64_t>(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<int>(n);
rt.has_anti_deadzone = true;
}
}
return rt;
+6
View File
@@ -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]].
+26
View File
@@ -0,0 +1,26 @@
#ifndef PSX_STICK_H
#define PSX_STICK_H
#include <stdint.h>
#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
+1
View File
@@ -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
+7 -26
View File
@@ -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<PsxButtonMap, 16> 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;
+73
View File
@@ -0,0 +1,73 @@
#include "psx_stick.h"
#include <math.h>
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);
}