From dfd29f76e268fd8298a849ea30c654841b732f8c Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 14:30:44 -0700 Subject: [PATCH] Require retained-bank OpenGL policy for trusted co-op sessions --- docs/NETPLAY.md | 11 ++++++++++ overlay_toolchain/include/mod_netplay.h | 8 +++++++ runtime/CMakeLists.txt | 1 + runtime/include/mod_netplay.h | 8 +++++++ runtime/src/gpu_gl_renderer.c | 9 +++++++- runtime/src/main.cpp | 11 ++++++++++ runtime/src/mod_netplay.c | 9 +++++++- runtime/tests/gpu_hd_texture_stubs.inc | 4 ++++ runtime/tests/test_gl_readback_region.c | 23 +++++++++++++++++++++ runtime/tests/test_mod_netplay.c | 12 ++++++++++- runtime/tests/test_mod_runtime.cpp | 2 +- runtime/tests/test_savestate_title_policy.c | 2 +- 12 files changed, 95 insertions(+), 5 deletions(-) diff --git a/docs/NETPLAY.md b/docs/NETPLAY.md index 5c5de4d7..e3b858ba 100644 --- a/docs/NETPLAY.md +++ b/docs/NETPLAY.md @@ -39,6 +39,17 @@ An optional read-only callback contributes canonical host-owned gameplay state to the existing core CRC; digest logs report this as `mod=`. The callback must exclude pointers, padding, rendering caches and local-only input state. +`requires_retained_texture_banks` opts a trusted profile into retained GL +textures during dual-raster netplay. It requires a working OpenGL backend; +software/Vulkan selections and an unavailable bank implementation are rejected +before guest boot. Software at 1x remains the authoritative VRAM/GPUREAD/CRC +source, independent of GL presentation resolution/filter preferences. Retained +bank pixels replace textures only on the GL image; the software authority still +samples ordinary VRAM for those commands. This policy follows the X6 co-op +renderer behavior and does not provide software retained-bank rendering or +prove visual equivalence to authoritative VRAM. Validate gameplay state and +GL presentation separately. The flag is part of the session content identity. + For an offline mode whose host state is not serialized, a trusted plugin can call `psx_mod_set_savestate_blocked(1)` and `psx_mod_set_rewind_blocked(1)` during activation. These policies reset before each new session; save blocking also diff --git a/overlay_toolchain/include/mod_netplay.h b/overlay_toolchain/include/mod_netplay.h index 25756b80..b079a882 100644 --- a/overlay_toolchain/include/mod_netplay.h +++ b/overlay_toolchain/include/mod_netplay.h @@ -25,6 +25,10 @@ typedef struct PSXModNetplayProfile { unsigned fixed_aspect_mask; /* bit 0=4:3, 1=16:9, 2=21:9; zero=no policy */ const char *widescreen_plugin_id; /* Optional forced renderer for wide. */ PSXModNetplayStateDigest state_digest; + /* Require GL retained texture banks. Netplay retains software @1x VRAM + * authority, while GL may use immutable banks for presentation. Software + * and unsupported GL launches are refused before guest boot. */ + int requires_retained_texture_banks; } PSXModNetplayProfile; int psx_mod_register_netplay_profile(const PSXModNetplayProfile *profile); @@ -43,6 +47,10 @@ int psx_mod_netplay_set_aspect(int index); int psx_mod_netplay_aspect(void); int psx_mod_netplay_has_state_digest(void); uint32_t psx_mod_netplay_state_digest(void); +int psx_mod_netplay_requires_texture_banks(void); +/* Runtime renderer index: 0=software, 1=OpenGL, 2=Vulkan. No profile leaves + * existing renderer policy unchanged; call only for a netplay launch. */ +int psx_mod_netplay_renderer_allowed(int renderer); #ifdef __cplusplus } diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 01f4d164..a3aebafb 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -28,6 +28,7 @@ if(BUILD_TESTING) target_include_directories(mod_netplay_test PRIVATE include) add_test(NAME mod_netplay_test COMMAND mod_netplay_test) add_test(NAME mod_netplay_wide_test COMMAND mod_netplay_test --wide) + add_test(NAME mod_netplay_optional_bank_test COMMAND mod_netplay_test --no-banks) add_executable(savestate_title_policy_test tests/test_savestate_title_policy.c src/mod_netplay.c src/psx_sha256.c) target_include_directories(savestate_title_policy_test PRIVATE include) diff --git a/runtime/include/mod_netplay.h b/runtime/include/mod_netplay.h index 25756b80..b079a882 100644 --- a/runtime/include/mod_netplay.h +++ b/runtime/include/mod_netplay.h @@ -25,6 +25,10 @@ typedef struct PSXModNetplayProfile { unsigned fixed_aspect_mask; /* bit 0=4:3, 1=16:9, 2=21:9; zero=no policy */ const char *widescreen_plugin_id; /* Optional forced renderer for wide. */ PSXModNetplayStateDigest state_digest; + /* Require GL retained texture banks. Netplay retains software @1x VRAM + * authority, while GL may use immutable banks for presentation. Software + * and unsupported GL launches are refused before guest boot. */ + int requires_retained_texture_banks; } PSXModNetplayProfile; int psx_mod_register_netplay_profile(const PSXModNetplayProfile *profile); @@ -43,6 +47,10 @@ int psx_mod_netplay_set_aspect(int index); int psx_mod_netplay_aspect(void); int psx_mod_netplay_has_state_digest(void); uint32_t psx_mod_netplay_state_digest(void); +int psx_mod_netplay_requires_texture_banks(void); +/* Runtime renderer index: 0=software, 1=OpenGL, 2=Vulkan. No profile leaves + * existing renderer policy unchanged; call only for a netplay launch. */ +int psx_mod_netplay_renderer_allowed(int renderer); #ifdef __cplusplus } diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 9a138cbe..a313e92e 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -80,6 +80,7 @@ #include "gpu_gl_renderer.h" #include "gpu_hd_textures.h" #include "mod_texture_banks.h" +#include "mod_netplay.h" #include "frame_interpolation.h" #include "render_pass_plan.h" #include "render_pass.h" @@ -5218,7 +5219,13 @@ static int init_gpu_raster(void) { return 1; } -int gl_renderer_texture_banks_supported(void) { return s_raster_ok && !s_cpu_auth_dual && !s_hd_native_authority; } +int gl_renderer_texture_banks_supported(void) { + /* A trusted title's bank pixels affect GL presentation only. Keep the + * existing software @1x VRAM authority during its delay-sync session. */ + return s_raster_ok && !s_hd_native_authority && + (!s_cpu_auth_dual || (psx_mod_netplay_is_active() && + psx_mod_netplay_requires_texture_banks())); +} int gl_renderer_fit_wide_aspect(int disp_w, int *num, int *den) { /* No sync point: pure arithmetic on limits that change only while the diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 324fad11..b2a2311a 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -18047,6 +18047,11 @@ session_reboot: } memory_init(bios_path_str.c_str()); if (g_headless_opengl) g_video_renderer = 1; + if (net_cfg.enabled && !psx_mod_netplay_renderer_allowed(g_video_renderer)) { + std::fprintf(stderr, "psxrecomp: this title's netplay profile requires OpenGL " + "retained textures; select the OpenGL renderer\n"); + return 1; + } #ifndef PSX_HAVE_VULKAN /* Vulkan was not compiled in (PSX_ENABLE_VULKAN=OFF or no SDK found). * Refuse a vulkan request from ANY source (config / CLI / launcher seed) @@ -18884,6 +18889,12 @@ session_reboot: log_present_cadence(); } + if (net_cfg.enabled && psx_mod_netplay_requires_texture_banks() && + !psx_mod_texture_banks_supported()) { + std::fprintf(stderr, "psxrecomp: this title's netplay profile requires " + "OpenGL retained texture banks; the backend is unavailable\n"); + return 1; + } /* Register vblank presentation callback. */ gpu_set_vblank_callback(sdl_vblank_present); diff --git a/runtime/src/mod_netplay.c b/runtime/src/mod_netplay.c index db466460..3fa675d0 100644 --- a/runtime/src/mod_netplay.c +++ b/runtime/src/mod_netplay.c @@ -53,7 +53,7 @@ int psx_mod_netplay_content_identity(const char *execution_content, char out[65] char normalized[65]; size_t n = strlen(input), i; psx_sha256_ctx ctx; - uint8_t bytes[32], policy[5]; + uint8_t bytes[32], policy[6]; static const char domain[] = "psx-trusted-netplay-profile-v1"; static const char hex[] = "0123456789abcdef"; const char *renderer; @@ -75,6 +75,7 @@ int psx_mod_netplay_content_identity(const char *execution_content, char out[65] policy[2] = title_profile->rollback_supported != 0; policy[3] = title_profile->savestates_supported != 0; policy[4] = title_profile->skip_bios_intro != 0; + policy[5] = title_profile->requires_retained_texture_banks != 0; psx_sha256_init(&ctx); psx_sha256_update(&ctx, (const uint8_t*)domain, sizeof(domain)); psx_sha256_update(&ctx, (const uint8_t*)normalized, n + 1); @@ -105,3 +106,9 @@ int psx_mod_netplay_has_state_digest(void) { uint32_t psx_mod_netplay_state_digest(void) { return psx_mod_netplay_has_state_digest() ? title_profile->state_digest() : 0u; } +int psx_mod_netplay_requires_texture_banks(void) { + return title_profile && title_profile->requires_retained_texture_banks != 0; +} +int psx_mod_netplay_renderer_allowed(int renderer) { + return !psx_mod_netplay_requires_texture_banks() || renderer == 1; +} diff --git a/runtime/tests/gpu_hd_texture_stubs.inc b/runtime/tests/gpu_hd_texture_stubs.inc index ef44dac4..0ea77cf0 100644 --- a/runtime/tests/gpu_hd_texture_stubs.inc +++ b/runtime/tests/gpu_hd_texture_stubs.inc @@ -1,5 +1,9 @@ /* Existing GL fixtures deliberately run without any external pack. The HD * fixture links the real session adapter instead. */ +/* These renderer fixtures also run without a trusted title profile. */ +static int test_title_profile_active, test_title_profile_banks; +int psx_mod_netplay_is_active(void) { return test_title_profile_active; } +int psx_mod_netplay_requires_texture_banks(void) { return test_title_profile_banks; } #ifndef PSX_TEST_HD_TEXTURE_PACK int gpu_hd_textures_acquire_draw(uint16_t tp, uint16_t cx, uint16_t cy, const int limits[4], uint32_t window, int semi, GpuHdTextureImage* image) { diff --git a/runtime/tests/test_gl_readback_region.c b/runtime/tests/test_gl_readback_region.c index e4da089b..4c47cba0 100644 --- a/runtime/tests/test_gl_readback_region.c +++ b/runtime/tests/test_gl_readback_region.c @@ -526,6 +526,29 @@ int main(int argc,char **argv){ for(test_full_composite=0;test_full_composite<2;test_full_composite++){verify_wide_overlay_band();verify_wide_overlay_order();} test_full_composite=0; verify_presentation_capture(); + /* A trusted retained-bank profile permits GL presentation in delay-sync, + * while the software authority still samples the original guest texture. */ + gl_renderer_sync_cpu(); + gl_renderer_set_cpu_auth_dual(1); + check(!gl_renderer_texture_banks_supported(),"dual banks refused without profile"); + test_title_profile_banks=1; + check(!gl_renderer_texture_banks_supported(),"dual banks refused for inactive profile"); + test_title_profile_active=1; + check(gl_renderer_texture_banks_supported(),"trusted dual retained-bank profile allowed"); + test_title_profile_banks=0; + check(!gl_renderer_texture_banks_supported(),"active profile must explicitly request banks"); + test_title_profile_banks=1; + s_hd_native_authority=1; + check(!gl_renderer_texture_banks_supported(),"HD authority still refuses retained banks"); + s_hd_native_authority=0; + glb_set_draw_area(0,0,1023,511); + glb_vram_write(0,0,0x7c00); + check(gl_renderer_select_texture_bank(7),"trusted dual bank selected"); + glb_draw_shaded_textured_triangle(100,300,0,0,0x808080,132,300,0,0,0x808080,100,332,0,0,0x808080,0,0,0x100,1); + check(gl_renderer_select_texture_bank(0),"reset trusted dual bank"); + check(glb_vram_read(102,302)==0x7c00,"dual software authority samples ordinary VRAM"); + test_title_profile_active=test_title_profile_banks=0; + gl_renderer_set_cpu_auth_dual(0); printf("checks=%d failures=%d\n",checks,failures); gl_renderer_shutdown();SDL_DestroyWindow(win);SDL_Quit();return failures?1:0; } diff --git a/runtime/tests/test_mod_netplay.c b/runtime/tests/test_mod_netplay.c index 7a6b499d..e9a0e1d8 100644 --- a/runtime/tests/test_mod_netplay.c +++ b/runtime/tests/test_mod_netplay.c @@ -48,9 +48,10 @@ int main(int argc, char** argv) { NetplayCoreParts baseline, live, changed; PSXModNetplayProfile invalid, duplicate; static PSXModNetplayProfile profile = { - "test.simulation", "test-coop-delay-v1", 0, 0, 1, 1u, NULL, gameplay_digest + "test.simulation", "test-coop-delay-v1", 0, 0, 1, 1u, NULL, gameplay_digest, 1 }; const int wide = argc > 1 && strcmp(argv[1], "--wide") == 0; + const int no_banks = argc > 1 && strcmp(argv[1], "--no-banks") == 0; uint32_t original_session, profile_session; char original_identity[65], current_identity[65]; const char* fingerprint = @@ -60,6 +61,9 @@ int main(int argc, char** argv) { netplay_core_digest_parts(&cpu, &baseline); assert(baseline.mod == 0); assert(psx_mod_netplay_rollback_supported()); + assert(!psx_mod_netplay_requires_texture_banks()); + assert(psx_mod_netplay_renderer_allowed(0) && psx_mod_netplay_renderer_allowed(1) && + psx_mod_netplay_renderer_allowed(2)); assert(psx_mod_netplay_savestates_supported()); assert(strcmp(psx_mod_netplay_version("dev"), "dev") == 0); assert(psx_mod_netplay_session_id(123) == 123); @@ -81,11 +85,17 @@ int main(int argc, char** argv) { invalid.fixed_aspect_mask = 8u; assert(!psx_mod_register_netplay_profile(&invalid)); if (wide) profile.fixed_aspect_mask = 6u; + if (no_banks) profile.requires_retained_texture_banks = 0; assert(psx_mod_register_netplay_profile(&profile)); assert(psx_mod_register_netplay_profile(&profile)); duplicate = profile; assert(!psx_mod_register_netplay_profile(&duplicate)); assert(!psx_mod_netplay_rollback_supported()); + assert(psx_mod_netplay_requires_texture_banks() == !no_banks); + assert(psx_mod_netplay_renderer_allowed(1)); + assert(psx_mod_netplay_renderer_allowed(0) == no_banks); + assert(psx_mod_netplay_renderer_allowed(2) == no_banks); + assert(psx_mod_netplay_renderer_allowed(-1) == no_banks); assert(psx_mod_netplay_savestates_supported()); assert(strcmp(psx_mod_netplay_version("override"), "test-coop-delay-v1") == 0); assert(psx_mod_netplay_aspect() == (wide ? 1 : 0)); diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index ba382927..5f42a97d 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -1755,7 +1755,7 @@ int main(int argc, char** argv) { static const PSXModNetplayProfile profile = { missing_profile ? "runtime.missing-simulation" : "runtime.test-vblank", "runtime-coop-delay-v1", 0, 0, 1, 3u, - "runtime.missing-renderer", nullptr + "runtime.missing-renderer", nullptr, 0 }; check(psx_mod_register_netplay_profile(&profile), "trusted profile registration"); check(PSXRecompV4::mod_runtime_initialize(root, "SLUS-RUNTIME", 0x80002000, {}, &error), diff --git a/runtime/tests/test_savestate_title_policy.c b/runtime/tests/test_savestate_title_policy.c index 696938b5..5e7bc8df 100644 --- a/runtime/tests/test_savestate_title_policy.c +++ b/runtime/tests/test_savestate_title_policy.c @@ -57,7 +57,7 @@ int main(void) { CPUState cpu; const uint8_t blob = 1; static const PSXModNetplayProfile profile = { - "test.simulation", "test-no-snapshots-v1", 0, 0, 0, 1u, NULL, NULL + "test.simulation", "test-no-snapshots-v1", 0, 0, 0, 1u, NULL, NULL, 0 }; memset(&cpu, 0, sizeof(cpu)); s_configured = 1; /* No configure/filesystem needed to test request staging. */