Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ psxrecomp_add_runtime_target(psx-runtime
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.

if(BUILD_TESTING)
add_executable(digital_pad_snapshot_test tests/test_digital_pad_snapshot.c)
target_include_directories(digital_pad_snapshot_test PRIVATE include)
if(MSVC)
target_compile_options(digital_pad_snapshot_test PRIVATE /UNDEBUG)
else()
target_compile_options(digital_pad_snapshot_test PRIVATE -UNDEBUG)
endif()
add_test(NAME digital_pad_snapshot_test COMMAND digital_pad_snapshot_test)
add_executable(gpu_fmv_hd_test tests/test_gpu_fmv_hd.cpp src/gpu_fmv_hd.c)
target_include_directories(gpu_fmv_hd_test PRIVATE include ${PSX_SDL_INCLUDE_DIRS})
target_compile_definitions(gpu_fmv_hd_test PRIVATE SDL_MAIN_HANDLED PSX_FMV_HD_TESTING=1
Expand Down
51 changes: 51 additions & 0 deletions runtime/include/digital_pad_snapshot.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#ifndef PSX_DIGITAL_PAD_SNAPSHOT_H
#define PSX_DIGITAL_PAD_SNAPSHOT_H
#include <stdint.h>
#include <string.h>
#include "mod_plugins.h"

/* Host-only observations. Never serialized with the emulated SIO machine. */
typedef struct PSXResolvedDigitalPad {
uint32_t held, generation, press[16];
} PSXResolvedDigitalPad;
static inline void digital_pad_history_reset(PSXResolvedDigitalPad *s,uint16_t active_low) {
uint32_t generation=s->generation+1u;
memset(s,0,sizeof *s);s->generation=generation?generation:1u;
s->held=(uint16_t)~active_low;
}
static inline void digital_pad_history_record(PSXResolvedDigitalPad *s,uint16_t active_low) {
uint32_t held=(uint16_t)~active_low,pressed=held & ~s->held;
for(unsigned i=0;i<16;++i)if(pressed & (1u<<i))++s->press[i];
s->held=held;
}
static inline void digital_pad_reader_reset(PSXModDigitalPadReader *r) {
memset(r,0,sizeof *r);r->struct_size=sizeof *r;
}
static inline int digital_pad_reader_read(PSXModDigitalPadReader *r,const PSXResolvedDigitalPad *s,
uint64_t frame,uint16_t legacy,uint32_t *out) {
if(!r || !s || !out || r->struct_size!=sizeof *r)return 0;
uint32_t held=0;
if(!r->valid || r->generation!=s->generation || frame<r->frame || frame-r->frame>1)goto prime;
if(frame==r->frame) {*out=r->held;return 1;}
for(unsigned i=0;i<16;++i) {
uint32_t delta=s->press[i]-r->last_press[i];
if(delta>4 || r->pending[i]>4-delta)goto prime;
}
for(unsigned i=0;i<16;++i) {
const uint32_t bit=1u<<i;
r->pending[i]+=s->press[i]-r->last_press[i];r->last_press[i]=s->press[i];
if(r->held & bit) {
/* A second real press implies an intervening release. Deliver
* that release first, not a duplicate held/press frame. */
if((s->held & bit) && !r->pending[i])held|=bit;
} else if(r->pending[i]) {
held|=bit;--r->pending[i];
} else if(s->held & bit)held|=bit;
}
r->held=held;r->frame=frame;*out=held;return 1;
prime:
digital_pad_reader_reset(r);r->generation=s->generation;r->frame=frame;
r->held=legacy;r->valid=1;memcpy(r->last_press,s->press,sizeof r->last_press);
return 0;
}
#endif
21 changes: 21 additions & 0 deletions runtime/include/mod_plugins.h
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,27 @@ typedef struct PSXModControllerState {
} PSXModControllerState;
typedef int (*PSXModControllerSource)(PSXModControllerState *state);
int psx_mod_set_controller_source(uint32_t player, PSXModControllerSource source);

/* Optional low-latency digital reader. Reads already resolved local SIO input;
* never polls SDL, writes a packet, changes controller routing or runs guest
* input/rumble code. Main thread, standalone offline digital P1 only. Other
* controller policies/sources, UI/input guards, resim and multitap decline.
* Caller initializes only struct_size (other fields zero), keeps one reader
* per input-consumer and does not persist/copy it into a savestate. Repeated
* reads within a guest frame are idempotent. Captured short taps are latched
* until the next original-rate read, with release/press separation; a skipped
* frame, device/session generation change or excessive backlog re-primes from
* legacy_buttons rather than replaying old actions. Return 0: keep the full
* original read result. Return 1: buttons receives active-high 16-bit buttons.
* This does not increase guest input/simulation frequency. */
typedef struct PSXModDigitalPadReader {
uint32_t struct_size, generation, held, valid;
uint64_t frame;
uint32_t last_press[16], pending[16]; /* private reader state */
} PSXModDigitalPadReader;
int psx_mod_read_digital_pad(uint32_t player, PSXModDigitalPadReader *reader,
uint32_t legacy_buttons, uint32_t *buttons);

/* Presence of an attached offline source (default connected). A diagnostic
* source may explicitly emulate an unplugged pad without falling back to a
* physical controller assigned to that port. Cleared on detach/session reset. */
Expand Down
2 changes: 2 additions & 0 deletions runtime/include/sio.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ void sio_get_pad_jogcon_motor(int slot, uint8_t *command, uint8_t *strength);
* pad 0 .. PSX_MAX_PLAYERS-1. */
uint16_t sio_get_pad_buttons(void);
uint16_t sio_get_pad_buttons_slot(int slot);
struct PSXResolvedDigitalPad;
int sio_get_resolved_digital_pad(int slot, struct PSXResolvedDigitalPad *out);

/* Side-effect-FREE SIO register peeks for the debug/observability path ONLY.
* NEVER use these for the guest bus — the guest must call sio_read() (which keeps
Expand Down
22 changes: 22 additions & 0 deletions runtime/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ extern "C" uint32_t g_guest_last_store_addr, g_guest_last_store_val;
#include "psx_stick.h" /* radial SDL-stick -> DualShock response transform */
#include "psx_trigger.h" /* continuous SDL trigger -> 0..255 magnitude */
#include "mod_pad_transform.h"
#include "digital_pad_snapshot.h"
#include "psx_hotkey_pad.h" /* host shortcut bindings, direct claims */
#include "psx_stick_fold.h" /* default-map digital stick fold (host layer) */
#include "psx_controller_type.h" /* mapped wheel-name classification */
Expand Down Expand Up @@ -9132,6 +9133,27 @@ static bool local_mouse_live(int override_word) {
return pad_ext_live(&g) != 0;
}

extern "C" int psx_mod_read_digital_pad(uint32_t player,PSXModDigitalPadReader *reader,
uint32_t legacy_buttons,uint32_t *buttons) {
if(!reader || reader->struct_size!=sizeof *reader || !buttons)return 0;
#ifndef PSX_NO_DEBUG_TOOLS
if(debug_server_get_input_override()>=0) {
digital_pad_reader_reset(reader);return 0;
}
#endif
if(player!=0 || g_offline_pad_count!=1 || !local_mouse_live(-1) ||
psx_local_mouse_installed() || sio_get_multitap() ||
mod_controller_source_present(player) || mod_pad_transform_initial_type(player)>=0 ||
g_mod_controller_policy[player].callback || g_mod_controller_mode_override[player]>=0) {
digital_pad_reader_reset(reader);return 0;
}
PSXResolvedDigitalPad sample{};
if(!sio_get_resolved_digital_pad((int)player,&sample)) {
digital_pad_reader_reset(reader);return 0;
}
return digital_pad_reader_read(reader,&sample,s_frame_count,(uint16_t)legacy_buttons,buttons);
}

static NetplayVblankEpilogue sdl_vblank_present_body(void) {
NetplayVblankEpilogue ep{};
/* Guest quantum for this vblank is complete. Drop top-level-resume armed
Expand Down
25 changes: 23 additions & 2 deletions runtime/src/sio.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
*/

#include "sio.h"
#include "digital_pad_snapshot.h"
#include "psx_cycle_freeze.h"
#include "psx_bss.h"
#include "memcard.h"
#include "debug_server.h"
Expand Down Expand Up @@ -46,6 +48,7 @@ static void sio_debug_poll_maybe(void) {
/* Pad state: 0=pressed, 1=released (PS1 convention). Indexed by LOGICAL pad
* 0 .. PSX_MAX_PLAYERS-1 (not physical SIO slot). */
static uint16_t pad_buttons[PSX_MAX_PLAYERS] = { PSX_PAD_INIT(0xFFFF) };
static PSXResolvedDigitalPad pad_input_history[PSX_MAX_PLAYERS];

/* Per-logical-pad type + analog stick state. Values are SIO_PAD_*; sticks are
* 0..255, 0x80 centred. JogCon uses left-X as a signed wheel position; NeGcon
Expand Down Expand Up @@ -864,6 +867,7 @@ void sio_init(void) {
for (int i = 0; i < PSX_MAX_PLAYERS; i++) {
pad_analog_device[i] = SIO_PAD_DUALSHOCK;
pad_buttons[i] = 0xFFFF;
digital_pad_history_reset(&pad_input_history[i],pad_buttons[i]);
pad_analog[i] = 0;
pad_stick[i][0] = pad_stick[i][1] = pad_stick[i][2] = pad_stick[i][3] = 0x80;
pad_in_config[i] = 0;
Expand Down Expand Up @@ -1059,6 +1063,8 @@ void sio_netplay_canonicalize_session_pads(int slot_count)

void sio_set_pad_connected(int slot, int connected) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (!!connected != !!(pad_connected & (1u<<slot)))
digital_pad_history_reset(&pad_input_history[slot],pad_buttons[slot]);
if (connected) {
pad_connected |= (uint8_t)(1u << slot);
} else {
Expand Down Expand Up @@ -1106,11 +1112,14 @@ void sio_get_pad_rumble(int slot, uint8_t *small, uint8_t *large) {
}

void sio_set_pad_state(uint16_t buttons) {
pad_buttons[0] = buttons;
sio_set_pad_state_slot(0,buttons);
}

void sio_set_pad_state_slot(int slot, uint16_t buttons) {
if (slot >= 0 && slot < PSX_MAX_PLAYERS) pad_buttons[slot] = buttons;
if (slot >= 0 && slot < PSX_MAX_PLAYERS) {
digital_pad_history_record(&pad_input_history[slot],buttons);
pad_buttons[slot] = buttons;
}
}

/* Entering or leaving NeGcon swaps the controller on the port; it is not the
Expand All @@ -1127,6 +1136,7 @@ static int pad_type_is_device_swap(uint8_t from, uint8_t to) {
static void pad_commit_type(int slot, uint8_t type) {
const uint8_t from = pad_analog[slot];
if (from != type) {
digital_pad_history_reset(&pad_input_history[slot],pad_buttons[slot]);
pad_jogcon_last_steering[slot] = 0;
pad_jogcon_motor_command[slot] = 0;
pad_jogcon_motor_strength[slot] = 0;
Expand Down Expand Up @@ -1219,6 +1229,12 @@ uint16_t sio_get_pad_buttons(void) {
return pad_buttons[0];
}

int sio_get_resolved_digital_pad(int slot,PSXResolvedDigitalPad *out) {
if(!out || slot<0 || slot>=PSX_MAX_PLAYERS || !sio_get_pad_connected(slot) ||
sio_get_pad_analog(slot)!=SIO_PAD_DIGITAL)return 0;
*out=pad_input_history[slot];return 1;
}

uint16_t sio_get_pad_buttons_slot(int slot) {
return (slot >= 0 && slot < PSX_MAX_PLAYERS) ? pad_buttons[slot] : 0xFFFF;
}
Expand Down Expand Up @@ -3084,6 +3100,11 @@ void sio_snapshot_section_ends(uint32_t out[5]) {
}

static int sio_snap_parse(PstR *r) {
/* A real state load invalidates input history; restoring a render-only
* checkpoint must not discard host presses captured by normal polling. */
if (!g_psx_render_pass_active)
for(int slot=0;slot<PSX_MAX_PLAYERS;++slot)
digital_pad_history_reset(&pad_input_history[slot],pad_buttons[slot]);
uint32_t u;
int32_t i;
int16_t tr;
Expand Down
48 changes: 48 additions & 0 deletions runtime/tests/test_digital_pad_snapshot.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include <assert.h>
#include <stdio.h>
#include "digital_pad_snapshot.h"
int main(void) {
PSXResolvedDigitalPad s={0};PSXModDigitalPadReader r={sizeof r};uint32_t out=0;
digital_pad_history_reset(&s,0xffff);assert(!digital_pad_reader_read(&r,&s,0,0,&out));
/* A complete short tap between original game reads is not lost. */
digital_pad_history_record(&s,0xffdf);digital_pad_history_record(&s,0xffff);
assert(digital_pad_reader_read(&r,&s,1,0,&out) && out==0x20);
assert(digital_pad_reader_read(&r,&s,1,0,&out) && out==0x20); /* same frame */
assert(digital_pad_reader_read(&r,&s,2,0x20,&out) && !out); /* delayed packet cannot replay it */
assert(digital_pad_reader_read(&r,&s,3,0,&out) && !out);
/* Held input is never turned into repeated presses; release is prompt. */
digital_pad_history_record(&s,0xffbf);
assert(digital_pad_reader_read(&r,&s,4,0,&out) && out==0x40);
for(unsigned f=5;f<10;++f) {
digital_pad_history_record(&s,0xffbf);
assert(digital_pad_reader_read(&r,&s,f,0x40,&out) && out==0x40);
}
digital_pad_history_record(&s,0xffff);
assert(digital_pad_reader_read(&r,&s,10,0x40,&out) && !out);
/* Two captured taps are separated into two presses and two releases. */
for(int i=0;i<2;++i) {digital_pad_history_record(&s,0x7fff);digital_pad_history_record(&s,0xffff);}
for(unsigned f=11;f<15;++f) {
assert(digital_pad_reader_read(&r,&s,f,0,&out));assert(out==((f&1)?0x8000:0));
}
/* Scene/load gaps and device changes never replay accumulated actions. */
digital_pad_history_record(&s,0xffdf);digital_pad_history_record(&s,0xffff);
assert(!digital_pad_reader_read(&r,&s,20,0,&out));
assert(digital_pad_reader_read(&r,&s,21,0,&out) && !out);
digital_pad_history_reset(&s,0xffff);assert(!digital_pad_reader_read(&r,&s,22,0,&out));
/* All 16 button bits, including sign bit, remain independent. */
for(unsigned bit=0;bit<16;++bit) {
digital_pad_history_record(&s,(uint16_t)~(1u<<bit));
assert(digital_pad_reader_read(&r,&s,23+2*bit,0,&out) && out==(1u<<bit));
digital_pad_history_record(&s,0xffff);
assert(digital_pad_reader_read(&r,&s,24+2*bit,0,&out) && !out);
}
/* Counter wrap and unsupported burst backlog fail safely without overflow. */
digital_pad_reader_reset(&r);s.press[0]=0xffffffffu;
assert(!digital_pad_reader_read(&r,&s,100,0,&out));
digital_pad_history_record(&s,0xfffe);digital_pad_history_record(&s,0xffff);
assert(digital_pad_reader_read(&r,&s,101,0,&out) && out==1);
digital_pad_reader_reset(&r);assert(!digital_pad_reader_read(&r,&s,200,0,&out));
s.press[0]+=10;assert(!digital_pad_reader_read(&r,&s,201,0,&out));
assert(digital_pad_reader_read(&r,&s,202,0,&out) && !out);
puts("PASS digital history: short taps, edges, repeat reads, rapid taps, gaps, reset, wrap and bounded backlog");
}
5 changes: 5 additions & 0 deletions runtime/tests/test_psx_pad_apply.c
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,8 @@ int main(void) {
fprintf(stderr, "psx_pad_apply: passed\n");
return 0;
}

int g_psx_render_pass_active;

/* Match the current SIO flush-status query in this no-file fixture. */
int memcard_last_flush_result(int card) { (void)card;return 0; }
6 changes: 6 additions & 0 deletions runtime/tests/test_sio_card_irq_integrity.c
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,9 @@ int main(int argc, char **argv) {
printf("%d/%d checks passed\n", checks - failures, checks);
return failures ? 1 : 0;
}

/* Standalone device test: not executing a render-only checkpoint. */
int g_psx_render_pass_active;

/* Match the current SIO flush-status query in this no-file fixture. */
int memcard_last_flush_result(int card) { (void)card;return 0; }
3 changes: 3 additions & 0 deletions runtime/tests/test_sio_card_protocol.c
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,6 @@ int main(int argc, char **argv) {
g_checks, g_failures);
return g_failures > 0 ? 1 : 0;
}

/* Standalone device test: not executing a render-only checkpoint. */
int g_psx_render_pass_active;
39 changes: 39 additions & 0 deletions runtime/tests/test_sio_digital_command.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* Plain digital pads shift their ID during every command byte.
* Synthetic bus transactions over the real sio.c; no BIOS/disc/core fixture. */
#include "sio.h"
#include "digital_pad_snapshot.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -94,7 +95,39 @@ static void setup(unsigned slot) {
sio_set_pad_config_capable(slot, 0);
sio_set_pad_state_slot(slot, 0xABCD);
}
extern int g_psx_render_pass_active;
static void test_host_history(void) {
setup(0);
sio_set_pad_state_slot(0,0xffff);
PSXResolvedDigitalPad a,b;
expect("digital snapshot available",1,sio_get_resolved_digital_pad(0,&a));
sio_set_pad_state_slot(0,0xffdf);sio_set_pad_state_slot(0,0xffff);
expect("released snapshot available",1,sio_get_resolved_digital_pad(0,&b));
expect("short tap counted",a.press[5]+1,b.press[5]);
expect("short tap released",0,b.held);
uint32_t size=sio_snapshot_bytes();uint8_t *bytes=malloc(size);
if(!bytes)exit(1);
sio_snapshot_write(bytes);
a=b;g_psx_render_pass_active=1;
expect("render checkpoint restore",1,sio_snapshot_read(bytes,size));
g_psx_render_pass_active=0;
expect("checkpoint history available",1,sio_get_resolved_digital_pad(0,&b));
expect("checkpoint preserves history",a.generation,b.generation);
expect("checkpoint preserves press",a.press[5],b.press[5]);
expect("real state restore",1,sio_snapshot_read(bytes,size));free(bytes);
expect("state-load history available",1,sio_get_resolved_digital_pad(0,&b));
expect("state load invalidates reader",1,a.generation!=b.generation);
sio_set_pad_connected(0,0);
expect("disconnected has no snapshot",0,sio_get_resolved_digital_pad(0,&b));
sio_set_pad_connected(0,1);
expect("reconnected snapshot available",1,sio_get_resolved_digital_pad(0,&b));
expect("reconnect advances generation",1,a.generation!=b.generation);
sio_set_pad_type(0,SIO_PAD_DUALSHOCK,128,128,128,128);
expect("analog has no digital snapshot",0,sio_get_resolved_digital_pad(0,&b));
}

int main(void) {
test_host_history();
for (unsigned slot = 0; slot < 2; ++slot) {
for (unsigned cmd = 0; cmd < 256; ++cmd) {
setup(slot);
Expand Down Expand Up @@ -155,3 +188,9 @@ int main(void) {
printf("digital command: %u checks, %u failures\n", checks, failures);
return failures ? 1 : 0;
}

/* Standalone device test: not executing a render-only checkpoint. */
int g_psx_render_pass_active;

/* Match the current SIO flush-status query in this no-file fixture. */
int memcard_last_flush_result(int card) { (void)card;return 0; }
6 changes: 6 additions & 0 deletions runtime/tests/test_sio_dualshock_rumble.c
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,9 @@ int main(void) {
fprintf(stderr, "DualShock rumble protocol: passed\n");
return 0;
}

/* Standalone device test: not executing a render-only checkpoint. */
int g_psx_render_pass_active;

/* Match the current SIO flush-status query in this no-file fixture. */
int memcard_last_flush_result(int card) { (void)card;return 0; }
6 changes: 6 additions & 0 deletions runtime/tests/test_sio_jogcon_protocol.c
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,9 @@ int main(void) {
fprintf(stderr, "JogCon SIO protocol: passed\n");
return 0;
}

/* Standalone device test: not executing a render-only checkpoint. */
int g_psx_render_pass_active;

/* Match the current SIO flush-status query in this no-file fixture. */
int memcard_last_flush_result(int card) { (void)card;return 0; }
6 changes: 6 additions & 0 deletions runtime/tests/test_sio_negcon_protocol.c
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,9 @@ int main(void) {
fprintf(stderr, "NeGcon SIO protocol (players=%d): passed\n", PSX_MAX_PLAYERS);
return 0;
}

/* Standalone device test: not executing a render-only checkpoint. */
int g_psx_render_pass_active;

/* Match the current SIO flush-status query in this no-file fixture. */
int memcard_last_flush_result(int card) { (void)card;return 0; }
3 changes: 3 additions & 0 deletions runtime/tests/test_sio_spu_istat_ownership.c
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,6 @@ int main(void) {
printf("ALL PASS (%d checks)\n", g_checks);
return 0;
}

/* Standalone device test: not executing a render-only checkpoint. */
int g_psx_render_pass_active;
Loading