From 184258cc292bd6b07c381353142b8a26384d1c40 Mon Sep 17 00:00:00 2001 From: Dagen Brock Date: Sun, 5 Jul 2026 23:38:51 -0500 Subject: [PATCH 1/2] Add Auto joystick mode: use gamepad when connected, else keypad The default joystick type was Keypad, but gamepad buttons still leaked through to $C061/$C062 while the analog stick did nothing -- a connected gamepad appeared half-broken until the user manually selected Native Joystick 1 in the config menu. Add joystick type 4 "Auto (Gamepad or Keypad)" and make it the default. Auto resolves at sample time via paddle_effective_joystick_type(): native joystick 1 when a device is connected, keypad otherwise. Nothing is stored, so hotplug switches modes live and config.kegs is untouched; explicitly saved types (0-3) keep their exact old meaning. Also gate the gamepad button merge on the resolved type being native, so an explicit Keypad/Mouse selection now fully ignores the gamepad instead of accepting its buttons but not its axes. Keyboard Open/Closed-Apple is unaffected (ORed separately from ADB state). The keypad-key swallowing in adb.c uses the resolved type too, so the numeric keypad types numbers again while a gamepad is driving the joystick. Co-Authored-By: Claude Fable 5 --- README.md | 3 +++ gsplus/src/adb.c | 2 +- gsplus/src/config.c | 5 +++-- gsplus/src/paddles.c | 33 ++++++++++++++++++++++++++++----- gsplus/src/protos_base.h | 1 + 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ba5d185..d3b2c4f 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,9 @@ Some features that are a part of the SDL3 build: fullscreen. - **Screenshot capture** - F10 writes the framebuffer to disk. - **Gamepad support** - cross-platform SDL_Gamepad mapped to the IIgs joystick. + A connected gamepad is used automatically (with hotplug); without one, the + numeric keypad emulates the joystick. Override in the config menu (F4) under + Joystick Configuration. - **Terminal debugger** - a REPL for the built-in 65816 debugger from the controlling terminal. - **Drag-and-drop disk loading** - drop a disk image on the window and GSplus diff --git a/gsplus/src/adb.c b/gsplus/src/adb.c index a43a5b3..fb5407e 100644 --- a/gsplus/src/adb.c +++ b/gsplus/src/adb.c @@ -2126,7 +2126,7 @@ adb_physical_key_update(Kimage *kimage_ptr, int raw_a2code, word32 unicode_c, if(ascii > 0x30 && ascii <= 0x39) { g_keypad_key_is_down[ascii - 0x30] = !is_up; } - if(g_joystick_type == 0) { + if(paddle_effective_joystick_type() == 0) { /* If Joystick type is keypad, then do not let these */ /* keypress pass on further, except for cmd/opt */ if(ascii == 0x30) { diff --git a/gsplus/src/config.c b/gsplus/src/config.c index 3d0ab74..95cc6cf 100644 --- a/gsplus/src/config.c +++ b/gsplus/src/config.c @@ -250,8 +250,9 @@ Cfg_menu g_cfg_newslot7_menu[] = { Cfg_menu g_cfg_joystick_menu[] = { { "Joystick Configuration", g_cfg_joystick_menu, 0, 0, CFGTYPE_MENU }, -{ "Joystick Emulation,0,Keypad Joystick,1,Mouse Joystick,2,Native Joystick 1," - "3,Native Joystick 2", KNMP(g_joystick_type), CFGTYPE_INT }, +{ "Joystick Emulation,4,Auto (Gamepad or Keypad),0,Keypad Joystick," + "1,Mouse Joystick,2,Native Joystick 1,3,Native Joystick 2", + KNMP(g_joystick_type), CFGTYPE_INT }, { "Joystick Scale X,0x100,Standard,0x119,+10%,0x133,+20%," "0x150,+30%,0xb0,-30%,0xcd,-20%,0xe7,-10%", KNMP(g_joystick_scale_factor_x), CFGTYPE_INT }, diff --git a/gsplus/src/paddles.c b/gsplus/src/paddles.c index 66c90f0..88aedae 100644 --- a/gsplus/src/paddles.c +++ b/gsplus/src/paddles.c @@ -21,7 +21,7 @@ int g_joystick_scale_factor_y = 0x100; int g_joystick_trim_amount_x = 0; int g_joystick_trim_amount_y = 0; -int g_joystick_type = 0; /* 0 = Keypad Joystick */ +int g_joystick_type = 4; /* 4 = Auto (gamepad if present, else keypad) */ int g_joystick_native_type1 = -1; int g_joystick_native_type2 = -1; int g_joystick_native_type = -1; @@ -35,17 +35,33 @@ dword64 g_paddle_dfcyc[4] = { 0, 0, 0, 0 }; /* g_paddle_dfcyc are the dfcyc the paddle goes to 0 */ +int +paddle_effective_joystick_type() +{ + /* Auto (type 4) is resolved every time it's sampled, never stored: + * use the native joystick/gamepad if one is connected, else the + * keypad. g_joystick_native_type1 tracks hotplug, so connecting + * or removing a gamepad mid-game switches modes automatically. */ + if(g_joystick_type == 4) { + return (g_joystick_native_type1 >= 0) ? 2 : 0; + } + return g_joystick_type; +} + void paddle_fixup_joystick_type() { + int type; + /* If g_joystick_type points to an illegal value, change it */ - if(g_joystick_type == 2) { + type = paddle_effective_joystick_type(); + if(type == 2) { g_joystick_native_type = g_joystick_native_type1; if(g_joystick_native_type1 < 0) { g_joystick_type = 0; } } - if(g_joystick_type == 3) { + if(type == 3) { g_joystick_native_type = g_joystick_native_type2; if(g_joystick_native_type2 < 0) { g_joystick_type = 0; @@ -62,7 +78,7 @@ paddle_trigger(dword64 dfcyc) /* Determine what all the paddle values are right now */ paddle_fixup_joystick_type(); - switch(g_joystick_type) { + switch(paddle_effective_joystick_type()) { case 0: /* Keypad Joystick */ paddle_trigger_keypad(dfcyc); break; @@ -183,5 +199,12 @@ void paddle_update_buttons() { paddle_fixup_joystick_type(); - joystick_update_buttons(); + if(paddle_effective_joystick_type() >= 2) { + joystick_update_buttons(); + } else { + /* In keypad/mouse modes a connected gamepad must not drive + * the buttons either (half-working input is confusing); + * also clears bits latched before a mode switch */ + g_paddle_buttons &= ~3; + } } diff --git a/gsplus/src/protos_base.h b/gsplus/src/protos_base.h index d5e589c..0246328 100644 --- a/gsplus/src/protos_base.h +++ b/gsplus/src/protos_base.h @@ -483,6 +483,7 @@ int read_vid_counters(int loc, dword64 dfcyc); /* paddles.c */ +int paddle_effective_joystick_type(void); void paddle_fixup_joystick_type(void); void paddle_trigger(dword64 dfcyc); void paddle_trigger_mouse(dword64 dfcyc); From d8c8dd1fa838dcfefef0537a3c693605d1fa5c06 Mon Sep 17 00:00:00 2001 From: Dagen Brock Date: Mon, 6 Jul 2026 00:10:53 -0500 Subject: [PATCH 2/2] Remap circular gamepad travel to the square IIgs joystick range A modern analog stick's gate is circular: full diagonal deflection only reports ~71% on each axis, so games expecting the square travel of real Apple joysticks (paddle 0/255 at the corners) could never reach them. Add paddle_map_circle_to_square(): a radial circle-to-square remap that stretches each sample along its own direction -- identity on the pure axes, growing to sqrt(2) at the diagonals -- so the circular rim lands exactly on the square perimeter. Includes an 8% radial deadzone (rescaled so travel is continuous and full deflection is unaffected) and a final clamp for pads that overshoot the unit circle. Applied centrally in paddle_update_trigger_dcycs() when the effective joystick type is native, on a copy of the X/Y pair: some backends keep g_paddle_val[] between samples, so remapping in place would compound the stretch. Composes with the existing Scale/Trim/swap/invert options, which apply downstream as before. New config var g_joystick_square_range, exposed in the joystick menu as "Gamepad Range: Square (Full Corners) / Circular (Raw)", default Square. Link libm on Linux for sqrt(). Co-Authored-By: Claude Fable 5 --- README.md | 6 +++-- gsplus/src/CMakeLists.txt | 5 ++++ gsplus/src/config.c | 3 +++ gsplus/src/paddles.c | 53 +++++++++++++++++++++++++++++++++++++++ gsplus/src/protos_base.h | 1 + gsplus/src/vars_x86linux | 2 +- 6 files changed, 67 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d3b2c4f..f98fb79 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,10 @@ Some features that are a part of the SDL3 build: - **Screenshot capture** - F10 writes the framebuffer to disk. - **Gamepad support** - cross-platform SDL_Gamepad mapped to the IIgs joystick. A connected gamepad is used automatically (with hotplug); without one, the - numeric keypad emulates the joystick. Override in the config menu (F4) under - Joystick Configuration. + numeric keypad emulates the joystick. The stick's circular travel is remapped + to the square range Apple joysticks have, so games can reach the corners. + Override either behavior in the config menu (F4) under Joystick + Configuration. - **Terminal debugger** - a REPL for the built-in 65816 debugger from the controlling terminal. - **Drag-and-drop disk loading** - drop a disk image on the window and GSplus diff --git a/gsplus/src/CMakeLists.txt b/gsplus/src/CMakeLists.txt index ebb0cea..d7ff1ea 100644 --- a/gsplus/src/CMakeLists.txt +++ b/gsplus/src/CMakeLists.txt @@ -266,6 +266,11 @@ add_executable(gsplus-sdl ) target_link_libraries(gsplus-sdl PRIVATE gsplus_core_sdl SDL3::SDL3) +if(UNIX AND NOT APPLE) + # paddles.c uses sqrt(); glibc keeps it in libm + target_link_libraries(gsplus-sdl PRIVATE m) +endif() + if(APPLE) # macOS-only: retune SDL's default menu bar so ⌘ combos pass through to the # emulated IIgs and GSplus quits on ⌥⌘Q. The Objective-C source needs Cocoa. diff --git a/gsplus/src/config.c b/gsplus/src/config.c index 95cc6cf..dfe1943 100644 --- a/gsplus/src/config.c +++ b/gsplus/src/config.c @@ -57,6 +57,7 @@ extern word32 g_mem_size_exp; extern int g_video_line_update_interval; extern int g_user_halt_bad; extern int g_joystick_type; +extern int g_joystick_square_range; extern int g_joystick_scale_factor_x; extern int g_joystick_scale_factor_y; extern int g_joystick_trim_amount_x; @@ -253,6 +254,8 @@ Cfg_menu g_cfg_joystick_menu[] = { { "Joystick Emulation,4,Auto (Gamepad or Keypad),0,Keypad Joystick," "1,Mouse Joystick,2,Native Joystick 1,3,Native Joystick 2", KNMP(g_joystick_type), CFGTYPE_INT }, +{ "Gamepad Range,1,Square (Full Corners),0,Circular (Raw)", + KNMP(g_joystick_square_range), CFGTYPE_INT }, { "Joystick Scale X,0x100,Standard,0x119,+10%,0x133,+20%," "0x150,+30%,0xb0,-30%,0xcd,-20%,0xe7,-10%", KNMP(g_joystick_scale_factor_x), CFGTYPE_INT }, diff --git a/gsplus/src/paddles.c b/gsplus/src/paddles.c index 88aedae..0f52989 100644 --- a/gsplus/src/paddles.c +++ b/gsplus/src/paddles.c @@ -9,6 +9,7 @@ /**********************************************************************/ #include "defc.h" +#include extern int g_mouse_raw_x; /* from adb.c */ extern int g_mouse_raw_y; @@ -22,6 +23,7 @@ int g_joystick_trim_amount_x = 0; int g_joystick_trim_amount_y = 0; int g_joystick_type = 4; /* 4 = Auto (gamepad if present, else keypad) */ +int g_joystick_square_range = 1; /* remap circular stick gate to square */ int g_joystick_native_type1 = -1; int g_joystick_native_type2 = -1; int g_joystick_native_type = -1; @@ -35,6 +37,45 @@ dword64 g_paddle_dfcyc[4] = { 0, 0, 0, 0 }; /* g_paddle_dfcyc are the dfcyc the paddle goes to 0 */ +#define STICK_DEADZONE 0.08 /* radial, as a fraction of full deflection */ + +void +paddle_map_circle_to_square(int *xptr, int *yptr) +{ + double x, y, r, m, scale; + + /* A modern gamepad stick travels in a circle, so a full diagonal + * only reaches ~71% on each axis and games expecting the square + * range of Apple joysticks never see the corners. Stretch each + * sample along its own direction -- identity on the axes, up to + * sqrt(2) at the diagonals -- so the circular rim lands exactly on + * the square's perimeter. */ + x = *xptr / 32767.0; + y = *yptr / 32767.0; + r = sqrt((x * x) + (y * y)); + if(r < STICK_DEADZONE) { + /* Radial deadzone: sticks do not rest at exactly 0, and the + * stretch below would amplify the drift */ + *xptr = 0; + *yptr = 0; + return; + } + /* Rescale magnitude so travel is continuous at the deadzone edge */ + scale = (r - STICK_DEADZONE) / (1.0 - STICK_DEADZONE); + x = (x / r) * scale; + y = (y / r) * scale; + m = (fabs(x) > fabs(y)) ? fabs(x) : fabs(y); + if(m > 0.0) { + x = x * (scale / m); + y = y * (scale / m); + } + /* Clamp: some pads overshoot the unit circle near the diagonals */ + x = (x > 1.0) ? 1.0 : ((x < -1.0) ? -1.0 : x); + y = (y > 1.0) ? 1.0 : ((y < -1.0) ? -1.0 : y); + *xptr = (int)(x * 32767.0); + *yptr = (int)(y * 32767.0); +} + int paddle_effective_joystick_type() { @@ -137,15 +178,27 @@ void paddle_update_trigger_dcycs(dword64 dfcyc) { dword64 trig_dfcyc; + int xy_val[2]; int val, paddle_num, scale, trim; int i; + xy_val[0] = g_paddle_val[0]; + xy_val[1] = g_paddle_val[1]; + if(g_joystick_square_range && + (paddle_effective_joystick_type() >= 2)) { + /* Work on a copy: some backends keep g_paddle_val[] between + * samples, so remapping in place would compound */ + paddle_map_circle_to_square(&xy_val[0], &xy_val[1]); + } for(i = 0; i < 4; i++) { paddle_num = i; if(g_swap_paddles) { paddle_num = i ^ 1; } val = g_paddle_val[paddle_num]; + if(paddle_num < 2) { + val = xy_val[paddle_num]; + } if(g_invert_paddles) { val = -val; } diff --git a/gsplus/src/protos_base.h b/gsplus/src/protos_base.h index 0246328..306a813 100644 --- a/gsplus/src/protos_base.h +++ b/gsplus/src/protos_base.h @@ -483,6 +483,7 @@ int read_vid_counters(int loc, dword64 dfcyc); /* paddles.c */ +void paddle_map_circle_to_square(int *xptr, int *yptr); int paddle_effective_joystick_type(void); void paddle_fixup_joystick_type(void); void paddle_trigger(dword64 dfcyc); diff --git a/gsplus/src/vars_x86linux b/gsplus/src/vars_x86linux index ac985a5..e66c85c 100644 --- a/gsplus/src/vars_x86linux +++ b/gsplus/src/vars_x86linux @@ -3,7 +3,7 @@ OBJECTS1 = pulseaudio_driver.o xdriver.o CCOPTS = -O2 -Wall -fomit-frame-pointer -DPULSE_AUDIO NAME = gsplus LD = $(CC) -EXTRA_LIBS = -lXext -lpulse +EXTRA_LIBS = -lXext -lpulse -lm EXTRA_SPECIALS = XOPTS = -I/usr/X11R6/include