From e706b9d84fd0f716515bd075e4c02e9f420e7b82 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 09:26:47 -0700 Subject: [PATCH] Publish coherent postfx projection and escape bounded diagnostic JSON --- runtime/CMakeLists.txt | 3 ++ runtime/include/gte_postfx_projection.h | 12 ++++++ .../include/gte_projection_publication.hpp | 31 +++++++++++++ runtime/include/postfx_debug_json.h | 43 +++++++++++++++++++ runtime/src/debug_server.c | 4 +- runtime/src/gl_postfx.inc | 34 +++++++-------- runtime/src/gte.cpp | 9 +++- runtime/tests/test_postfx_publication.cpp | 40 +++++++++++++++++ 8 files changed, 156 insertions(+), 20 deletions(-) create mode 100644 runtime/include/gte_postfx_projection.h create mode 100644 runtime/include/gte_projection_publication.hpp create mode 100644 runtime/include/postfx_debug_json.h create mode 100644 runtime/tests/test_postfx_publication.cpp diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 94028664..6e2d73f6 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -23,6 +23,9 @@ psxrecomp_add_runtime_target(psx-runtime # (docs/ENHANCEMENTS.md G1.10). Titles get their _pgxp the same way. if(BUILD_TESTING) + add_executable(postfx_publication_test tests/test_postfx_publication.cpp) + target_include_directories(postfx_publication_test PRIVATE include) + add_test(NAME postfx_publication_test COMMAND postfx_publication_test) # Windows UTF-8 text boundary (issue #371): the helpers legacy Windows # relies on, and — with the shipped manifest — a UTF-8 process code page. if(WIN32) diff --git a/runtime/include/gte_postfx_projection.h b/runtime/include/gte_postfx_projection.h new file mode 100644 index 00000000..1ff42d9f --- /dev/null +++ b/runtime/include/gte_postfx_projection.h @@ -0,0 +1,12 @@ +#pragma once +#include +#ifdef __cplusplus +extern "C" { +#endif +typedef struct GtePostfxProjection { int32_t h, ofx, ofy; } GtePostfxProjection; +/* Coherent latest CPU projection, not a frame/view identity. One CPU producer. */ +void gte_postfx_projection_enable(int enabled); +int gte_postfx_projection_read(GtePostfxProjection *out); +#ifdef __cplusplus +} +#endif diff --git a/runtime/include/gte_projection_publication.hpp b/runtime/include/gte_projection_publication.hpp new file mode 100644 index 00000000..b51907bd --- /dev/null +++ b/runtime/include/gte_projection_publication.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "gte_postfx_projection.h" +#include +/* CPU owns cached fields; renderer owns enable(). Atomic payloads plus an SC + * version guard make concurrent reads defined and reject mixed tuples. */ +class GteProjectionPublication { + std::atomic control{0}, version{0}, published{0}; + std::atomic h{0}, x{0}, y{0}; + uint32_t seen=0; GtePostfxProjection cached{}; +public: + void enable(bool on) { + const auto c=control.load(); + if(bool(c&1)!=on) control.store(((c+2)&~1u)|uint32_t(on)); + } + void publish(int32_t ph,int32_t px,int32_t py) { + const auto c=control.load(std::memory_order_relaxed); + if(!(c&1))return; + if(c==seen && ph==cached.h && px==cached.ofx && py==cached.ofy)return; + version.fetch_add(1); h.store(ph); x.store(px); y.store(py); published.store(c); + version.fetch_add(1); seen=c; cached={ph,px,py}; + } + bool read(GtePostfxProjection& out) const { + const auto c=control.load(); if(!(c&1))return false; + for(unsigned i=0;i<3;++i) { + const auto v=version.load(); if(v&1)continue; + const auto p=published.load(); GtePostfxProjection value{h.load(),x.load(),y.load()}; + if(v==version.load() && p==c && c==control.load()) {out=value;return true;} + } + return false; + } +}; diff --git a/runtime/include/postfx_debug_json.h b/runtime/include/postfx_debug_json.h new file mode 100644 index 00000000..e63f0e28 --- /dev/null +++ b/runtime/include/postfx_debug_json.h @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include +typedef struct PfxDebugLive { char name[32]; float v[4]; } PfxDebugLive; +typedef struct PfxJsonWriter { char *out; size_t cap,used; int ok; } PfxJsonWriter; +static inline void pfx_json_raw(PfxJsonWriter *w,const char *s) { + const size_t n=strlen(s); + if(!w->ok || n>=w->cap-w->used){w->ok=0;return;} + memcpy(w->out+w->used,s,n+1);w->used+=n; +} +static inline void pfx_json_quote(PfxJsonWriter *w,const char *s) { + pfx_json_raw(w,"\""); + for(const unsigned char *p=(const unsigned char*)s;*p;++p) { + char b[7]; + if(*p=='"'||*p=='\\'){b[0]='\\';b[1]=(char)*p;b[2]=0;} + else if(*p<32)snprintf(b,sizeof b,"\\u%04x",(unsigned)*p); + else {b[0]=(char)*p;b[1]=0;} + pfx_json_raw(w,b); + } + pfx_json_raw(w,"\""); +} +/* Returns 0 with an empty buffer on insufficient space, never partial JSON. */ +static inline int pfx_debug_json(char *out,int cap,const char *path,const char *status, + const PfxDebugLive *live,int count) { + if(!out||cap<=0)return 0; + out[0]=0;PfxJsonWriter w={out,(size_t)cap,0,1}; + pfx_json_raw(&w,"\"path\":");pfx_json_quote(&w,path); + pfx_json_raw(&w,",\"status\":");pfx_json_quote(&w,status); + pfx_json_raw(&w,",\"uniforms\":{"); + for(int i=0;i= PFX_NAME) return; + if (!name || !name[0] || strlen(name) >= sizeof s_pfx_live[0].name || + !isfinite(x) || !isfinite(y) || !isfinite(z) || !isfinite(w)) return; SDL_LockSpinlock(&s_pfx_live_lock); int i = 0; while (i < s_pfx_nlive && strcmp(s_pfx_live[i].name, name)) i++; if (i == s_pfx_nlive && s_pfx_nlive < PFX_LIVE_MAX) { - snprintf(s_pfx_live[i].name, PFX_NAME, "%s", name); + snprintf(s_pfx_live[i].name, sizeof s_pfx_live[i].name, "%s", name); s_pfx_nlive++; } if (i < s_pfx_nlive) { @@ -123,7 +126,7 @@ static int pfx_live_get(const char *name, float out[4]) { SDL_UnlockSpinlock(&s_pfx_live_lock); return found; } -extern volatile int32_t g_gte_last_h, g_gte_last_ofx, g_gte_last_ofy; /* gte.cpp */ + static int s_mask_w = 0, s_mask_h = 0; static int pfx_stage_on(int i) { @@ -161,6 +164,7 @@ static void pfx_stage_unload(PfxStage *st) { /* Which stages read the mask decides whether draws keep it. */ static void pfx_mask_wanted_update(void) { + gte_postfx_projection_enable(postfx_active()); int want = 0; for (int i = 0; i < PFX_NSTAGES; i++) { if (!pfx_stage_on(i)) continue; @@ -544,8 +548,10 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in /* u_proj: x/z = (uv.x - y) * w * x, y/z = (uv.y - z) * x for a view-space * point at depth sz (240-line reference, same scale on both axes). */ const float aspect = (float)lw / (float)(lh > 0 ? lh : 1); - const float H = g_gte_last_h > 0 ? (float)g_gte_last_h : 256.0f; - const float ofx = (float)g_gte_last_ofx / 65536.0f, ofy = (float)g_gte_last_ofy / 65536.0f; + GtePostfxProjection projection = {256, 160 * 65536, 120 * 65536}; + (void)gte_postfx_projection_read(&projection); + const float H = projection.h > 0 ? (float)projection.h : 256.0f; + const float ofx = (float)projection.ofx / 65536.0f, ofy = (float)projection.ofy / 65536.0f; p_glUniform4f(L->proj, 240.0f / H, 0.5f + (ofx - 160.0f) / (240.0f * aspect), 1.0f - (ofy > 0.0f ? ofy : 120.0f) / 240.0f, aspect); } @@ -684,6 +690,7 @@ static void postfx_present(int fresh, int with_aa, int lx, int ly, int lw, int l /* Objects died with the context. */ static void postfx_release(void) { + gte_postfx_projection_enable(0); for (int i = 0; i < PFX_NSTAGES; i++) { for (int k = 0; k < PFX_MAX_PASSES; k++) { s_pfx[i].prog[k] = s_pfx[i].tex[k] = s_pfx[i].fbo[k] = 0; @@ -725,19 +732,12 @@ void gl_renderer_set_post_shader(const char *path) { } /* Debug: the user chain's last load outcome and the live uniforms, as JSON members. */ int gl_renderer_post_shader_debug(char *out, int cap) { - char chemin[sizeof s_pfx_user_path]; - snprintf(chemin, sizeof chemin, "%s", s_pfx_user_path); - for (char *q = chemin; *q; q++) if (*q == 92 || *q == 34) *q = '/'; /* JSON-safe */ - int k = snprintf(out, (size_t)cap, "\"path\":\"%s\",\"status\":\"%s\",\"uniforms\":{", - chemin, s_pfx_user_status); + GL_RT_SYNC("post_shader_debug"); + PfxLive live[PFX_LIVE_MAX]; int count; SDL_LockSpinlock(&s_pfx_live_lock); - for (int i = 0; i < s_pfx_nlive && k < cap - 96; i++) - k += snprintf(out + k, (size_t)(cap - k), "%s\"%s\":[%.4f,%.4f,%.4f,%.4f]", i ? "," : "", - s_pfx_live[i].name, s_pfx_live[i].v[0], s_pfx_live[i].v[1], s_pfx_live[i].v[2], - s_pfx_live[i].v[3]); + count = s_pfx_nlive; memcpy(live,s_pfx_live,sizeof live); SDL_UnlockSpinlock(&s_pfx_live_lock); - if (k < cap - 2) k += snprintf(out + k, (size_t)(cap - k), "}"); - return k; + return pfx_debug_json(out,cap,s_pfx_user_path,s_pfx_user_status,live,count); } int gl_renderer_post_shader_loaded(void) { diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index fce93bbf..5d60ef36 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -1,5 +1,6 @@ #include #include "gte.h" +#include "gte_projection_publication.hpp" #include "gte_view.h" #include "cpu_state.h" #include "execution_profile.h" @@ -21,7 +22,11 @@ extern "C" int gpu_ws_precise_nclip_enabled(void); extern "C" int psx_overlay_shadow_diff_active(void); extern "C" uint64_t s_frame_count; /* debug_server.c */ /* Latest projection (H, OFX, OFY in 16.16), read by the post-effect executor. */ -extern "C" { volatile int32_t g_gte_last_h = 0, g_gte_last_ofx = 0, g_gte_last_ofy = 0; } +static GteProjectionPublication s_postfx_projection; +extern "C" void gte_postfx_projection_enable(int enabled) { s_postfx_projection.enable(enabled!=0); } +extern "C" int gte_postfx_projection_read(GtePostfxProjection* out) { + return out && s_postfx_projection.read(*out); +} extern "C" uint32_t g_debug_last_store_pc; /* debug_server.c */ extern "C" { int (*g_psx_projection_command)(CPUState*, uint32_t) = nullptr; } @@ -1209,7 +1214,7 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) } dome_probe_note(gte->SZ[3]); /* locate the dome draw fn (far-vertex tally) */ if (!s_gte_replay_sandbox) { /* projection of the latest 3D draw, for post effects (u_proj) */ - g_gte_last_h = gte->H; g_gte_last_ofx = gte->OFX; g_gte_last_ofy = gte->OFY; + s_postfx_projection.publish(gte->H,gte->OFX,gte->OFY); } int64_t sx16 = gte->OFX + xterm; int64_t sy16 = gte->OFY + (int64_t)gte->IR2 * h_div_sz; diff --git a/runtime/tests/test_postfx_publication.cpp b/runtime/tests/test_postfx_publication.cpp new file mode 100644 index 00000000..5c55b20c --- /dev/null +++ b/runtime/tests/test_postfx_publication.cpp @@ -0,0 +1,40 @@ +#include "gte_projection_publication.hpp" +#include "postfx_debug_json.h" +#include +#include +#include +#include +#include +static int failures; +static void check(bool ok,const char* why) { + if(!ok){std::fprintf(stderr,"%s\n",why);++failures;} +} +int main() { + GteProjectionPublication state; GtePostfxProjection out{}; + state.publish(1,2,3); check(!state.read(out),"disabled projection is unavailable"); + state.enable(true); state.publish(1,2,3); + check(state.read(out)&&out.h==1&&out.ofx==2&&out.ofy==3,"first enabled tuple"); + state.enable(false);state.enable(true); + check(!state.read(out),"re-enabled session cannot reuse a stale tuple"); + state.publish(1,2,3);check(state.read(out),"unchanged values publish in the new epoch"); + std::atomic done{false},bad{false}; + std::thread writer([&]{for(int i=1;i<200000;++i)state.publish(i,i*2,i*3);done=true;}); + auto read=[&]{while(!done.load()){GtePostfxProjection p{}; + if(state.read(p)&&(p.ofx!=p.h*2||p.ofy!=p.h*3))bad=true;}}; + std::thread a(read),b(read);writer.join();a.join();b.join(); + check(!bad,"concurrent readers never see mixed projection fields"); + PfxDebugLive live{};std::strcpy(live.name,"u_\"x\n");live.v[0]=1.25f; + char json[512];check(pfx_debug_json(json,sizeof json,"C:\\a\"b\n","bad\tpreset",&live,1)>0,"serialize escaped diagnostics"); + check(std::strstr(json,R"("path":"C:\\a\"b\u000a")")!=nullptr,"path round trips without mutation"); + check(std::strstr(json,R"("u_\"x\u000a":[1.25,0,0,0])")!=nullptr,"uniform names are escaped"); + std::printf("{%s}\n",json); + for(int cap=0;cap<160;++cap) { + char guarded[192];std::memset(guarded,'X',sizeof guarded); + const int n=pfx_debug_json(guarded+1,cap,"C:\\a\"b\n","bad\tpreset",&live,1); + check(guarded[0]=='X'&&guarded[cap+1]=='X',"small output buffer stays within bounds"); + if(!n&&cap)check(guarded[1]==0,"failed JSON output is empty"); + } + live.v[0]=std::numeric_limits::infinity(); + check(!pfx_debug_json(json,sizeof json,"","",&live,1)&&!json[0],"nonfinite values fail without invalid JSON"); + return failures?1:0; +}