diff --git a/docs/internal/upstream/offline-plugin-controller-sources.md b/docs/internal/upstream/offline-plugin-controller-sources.md new file mode 100644 index 00000000..c71129dd --- /dev/null +++ b/docs/internal/upstream/offline-plugin-controller-sources.md @@ -0,0 +1,41 @@ +# Offline plugin controller sources: source and validation + +Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on +`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`. +Exact source commits: + +- [5a1264b7](https://github.com/FractalEngineer/psxrecomp/commit/5a1264b79b69b5d75f7b8507e305b1e13a911a50) + +Original author: Yves . The feature commit retains +that exact human author identity. The focused port consolidates the selected +source hunks and corrections. Its commit also cites these sources and carries +the original human contributor's `Co-authored-by: Yves ` trailer. + +Adapted: Only trusted controller-source registration, validation/neutralization and the existing offline host-input boundary. + +Deliberately excluded: OpenXR Touch action implementation, game bindings/deadzones/aiming and guest SIO replacements. + +The feature was reconstructed on upstream master +`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs, +renderer/input policy and build integration. Validation after reconstruction: +Absent/invalid/declined/analog/digital source, detach and session-reset executable checks. + +The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442). +Game-specific integration remains in that repository. Consuming this change +requires a deliberate framework pin update in the game repo and a rebuild. +This is a host/runtime/tool change; it requires no BIOS or game C regeneration. + +Historical alpha acceptance used the older framework checkpoint above. It is +separate from current-master branch verification. Fresh game, renderer and +headset checks, where applicable, are recorded in the PR validation notes; +uncertain hardware/runtime behavior must not be inferred from a clean link. + +## Current-master integration validation + +Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled. + +Release controller-source checks pass for absent, invalid, declined, analog/digital, detach and session-reset samples; they remain active with NDEBUG enabled. Integrated flat input reaches gameplay. Hardware controller mappings and live headset focus/reconnect behavior remain outside this test coverage. + +Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested. + +The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 74cb0cc4..ab92f530 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -364,6 +364,9 @@ if(BUILD_TESTING) target_include_directories(projection_scale_config_test PRIVATE include ../recompiler/lib/toml11) add_test(NAME projection_scale_config_test COMMAND projection_scale_config_test) + add_executable(mod_controller_source_test tests/test_mod_controller_source.c src/mod_controller_source.c) + target_include_directories(mod_controller_source_test PRIVATE include) + add_test(NAME mod_controller_source_test COMMAND mod_controller_source_test) add_executable(gte_register_access_test tests/test_gte_register_access.cpp diff --git a/runtime/include/mod_controller_source.h b/runtime/include/mod_controller_source.h new file mode 100644 index 00000000..f14465d7 --- /dev/null +++ b/runtime/include/mod_controller_source.h @@ -0,0 +1,11 @@ +#pragma once +#include "mod_plugins.h" +#ifdef __cplusplus +extern "C" { +#endif +void mod_controller_source_reset(void); +int mod_controller_source_sample(uint32_t player, PSXModControllerState *state); +int mod_controller_source_present(uint32_t player); +#ifdef __cplusplus +} +#endif diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 8ea89578..de09ada8 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -499,6 +499,18 @@ int psx_mod_set_controller_presentation_policy( uint32_t initial_mode, int config_capable); +/* Trusted offline source owns a player's pad at normal input sampling. A + * declined/invalid sample delivers neutral, not the previous held input. + * Existing TCP overrides take priority; netplay/resim and eye redraws never + * invoke the source. The runtime keeps coherent SIO type requests/recording. + * Pass NULL to detach. Local keyboard/pad buttons remain merged for menus; + * the source owns sticks and type. No source leaves faithful defaults intact. */ +typedef struct PSXModControllerState { + uint32_t struct_size, buttons, lx, ly, rx, ry, analog; +} PSXModControllerState; +typedef int (*PSXModControllerSource)(PSXModControllerState *state); +int psx_mod_set_controller_source(uint32_t player, PSXModControllerSource source); + /* * Register a C plugin before main() on the compilers supported by the runtime. * The registry itself uses function-local initialization, so constructor order diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 1be8be05..c21bcd84 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -352,6 +352,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/gpu_render.c ${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c ${PSXRECOMP_ROOT}/runtime/src/vr_pose_math.c + ${PSXRECOMP_ROOT}/runtime/src/mod_controller_source.c ${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c ${PSXRECOMP_ROOT}/runtime/src/dma_gpu_ll.c ${PSXRECOMP_ROOT}/runtime/src/dma.c diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 55ac9088..b5b0f2a5 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -8,6 +8,7 @@ #include "cpu_state.h" #include "projection_scale.hpp" #include "projection_scale_config.hpp" +#include "mod_controller_source.h" #include "window_size.h" /* default game-window size */ #include "internal_resolution.h" /* Settings -> Display -> Internal resolution */ #include "psx_scheduler.h" /* psx_scheduler_run — deterministic TCB scheduler */ @@ -6242,7 +6243,24 @@ static void sample_pad_into_sio(int override) { psx_start_consumer_enabled() ? psx_start_consumer_offline_frame() : 0u; for (int s = 0; s < n; s++) { PsxNetPad pad; - if (!capture_pad_slot(s, &pad)) continue; /* no device in this port */ + PSXModControllerState source; + if (mod_controller_source_sample((uint32_t)s, &source)) { + /* Sample physical buttons separately; its axes/type cannot steer + * the source's virtual pad. Guards release every source channel. */ + PsxNetPad local; + const int have_local = capture_pad_slot(s, &local); + pad.buttons = (uint16_t)source.buttons; + if (have_local) pad.buttons &= local.buttons; + pad.lx = (uint8_t)source.lx; pad.ly = (uint8_t)source.ly; + pad.rx = (uint8_t)source.rx; pad.ry = (uint8_t)source.ry; + pad.analog = (uint8_t)source.analog; pad.connected = 1; + if (savestate_input_guard_active()) { + pad.buttons = 0xffff; + pad.lx = pad.ly = pad.rx = pad.ry = 128; + } + sio_set_pad_connected(s, 1); + sio_set_pad_config_capable(s, 1); + } else if (!capture_pad_slot(s, &pad)) continue; /* no device in this port */ /* Push sticks every frame; request the pad type (digital/analog) through * the coherent channel so a policy switch is applied only at an idle, * non-config bus boundary (never mid-poll / mid-handshake). This is the @@ -15782,6 +15800,7 @@ int main(int argc, char** argv) { * choices first so disabling a package cannot leave its prior state * latched across a soft return. */ g_mod_controller_mode_override.fill(-1); + mod_controller_source_reset(); for (auto& policy : g_mod_controller_policy) policy = ModControllerPresentationPolicy{}; g_mod_load_wall_multiplier = -1; diff --git a/runtime/src/mod_controller_source.c b/runtime/src/mod_controller_source.c new file mode 100644 index 00000000..0be0cda9 --- /dev/null +++ b/runtime/src/mod_controller_source.c @@ -0,0 +1,25 @@ +#include "mod_controller_source.h" +#include "sio.h" +#include +static PSXModControllerSource s_source[PSX_MAX_PLAYERS]; +static uint8_t s_release[PSX_MAX_PLAYERS]; +int psx_mod_set_controller_source(uint32_t player, PSXModControllerSource source) { + if (player >= PSX_MAX_PLAYERS) return 0; + if(s_source[player] && !source)s_release[player]=1; + s_source[player] = source; return 1; +} +void mod_controller_source_reset(void) { memset(s_source, 0, sizeof s_source); memset(s_release,0,sizeof s_release); } +int mod_controller_source_present(uint32_t player) { + return player < PSX_MAX_PLAYERS && s_source[player] != NULL; +} +int mod_controller_source_sample(uint32_t player, PSXModControllerState *state) { + if (!state || player>=PSX_MAX_PLAYERS || (!s_source[player] && !s_release[player])) return 0; + PSXModControllerState neutral = {sizeof neutral, 0xffff, 128,128,128,128,1}; + *state = neutral; + if(s_release[player]) { s_release[player]=0; return 1; } + if (!s_source[player](state) || state->struct_size != sizeof *state || + state->buttons > 0xffff || state->lx > 255 || state->ly > 255 || + state->rx > 255 || state->ry > 255 || state->analog > 1) *state = neutral; + if (!state->analog) state->lx = state->ly = state->rx = state->ry = 128; + return 1; +} diff --git a/runtime/tests/test_mod_controller_source.c b/runtime/tests/test_mod_controller_source.c new file mode 100644 index 00000000..9ec39acb --- /dev/null +++ b/runtime/tests/test_mod_controller_source.c @@ -0,0 +1,35 @@ +#include "mod_controller_source.h" +#include "sio.h" +#include +static int mode; +static int source(PSXModControllerState *p) { + p->buttons=0xfeff;p->lx=200;p->ly=5; + if(mode==1)return 0; + if(mode==2)p->rx=256; + if(mode==3)p->analog=0; + return 1; +} +/* Evaluate side effects and failures even when NDEBUG is defined. */ +#define CHECK(expr) do { \ + if (!(expr)) { \ + fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); \ + return 1; \ + } \ +} while (0) +int main(void) { + PSXModControllerState p; + CHECK(!psx_mod_set_controller_source(PSX_MAX_PLAYERS,source)); + CHECK(!mod_controller_source_sample(0,&p)); + CHECK(psx_mod_set_controller_source(0,source)); + CHECK(mod_controller_source_sample(0,&p) && p.lx==200 && p.ly==5); + mode=1;CHECK(mod_controller_source_sample(0,&p) && p.buttons==0xffff && p.lx==128); + mode=2;CHECK(mod_controller_source_sample(0,&p) && p.buttons==0xffff && p.rx==128); + mode=3;CHECK(mod_controller_source_sample(0,&p) && !p.analog && p.lx==128 && p.ly==128); + CHECK(psx_mod_set_controller_source(0,0)); + CHECK(mod_controller_source_sample(0,&p) && p.buttons==0xffff && p.lx==128); + CHECK(!mod_controller_source_sample(0,&p)); + CHECK(psx_mod_set_controller_source(0,source)); + mod_controller_source_reset(); + CHECK(!mod_controller_source_present(0) && !mod_controller_source_sample(0,&p)); + return 0; +}