input: add trusted offline controller sources for game plugins

Adapted from the original feature commits, retaining their author credit:
[5a1264b7](https://github.com/FractalEngineer/psxrecomp/commit/5a1264b79b69b5d75f7b8507e305b1e13a911a50)

Source scope and current-master validation: docs/internal/upstream/offline-plugin-controller-sources.md

Co-authored-by: Yves <calibratedbeats@gmail.com>
This commit is contained in:
Yves
2026-10-04 05:54:05 +08:00
parent 305b5485b9
commit cbe828fcf5
8 changed files with 148 additions and 1 deletions
@@ -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 <calibratedbeats@gmail.com>. 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 <calibratedbeats@gmail.com>` 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.
+3
View File
@@ -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
+11
View File
@@ -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
+12
View File
@@ -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
+1
View File
@@ -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
+20 -1
View File
@@ -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;
+25
View File
@@ -0,0 +1,25 @@
#include "mod_controller_source.h"
#include "sio.h"
#include <string.h>
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;
}
@@ -0,0 +1,35 @@
#include "mod_controller_source.h"
#include "sio.h"
#include <stdio.h>
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;
}