Require retained-bank OpenGL policy for trusted co-op sessions

This commit is contained in:
Matthew Stanley
2026-10-08 14:30:44 -07:00
parent 1e33761d2a
commit dfd29f76e2
12 changed files with 95 additions and 5 deletions
+11
View File
@@ -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
+8
View File
@@ -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
}
+1
View File
@@ -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)
+8
View File
@@ -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
}
+8 -1
View File
@@ -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
+11
View File
@@ -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);
+8 -1
View File
@@ -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;
}
+4
View File
@@ -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) {
+23
View File
@@ -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;
}
+11 -1
View File
@@ -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));
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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. */