Publish coherent postfx projection and escape bounded diagnostic JSON

This commit is contained in:
Matthew Stanley
2026-10-24 09:26:47 -07:00
parent 3ad83c910b
commit e706b9d84f
8 changed files with 156 additions and 20 deletions
+3
View File
@@ -23,6 +23,9 @@ psxrecomp_add_runtime_target(psx-runtime
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_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)
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <stdint.h>
#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
@@ -0,0 +1,31 @@
#pragma once
#include "gte_postfx_projection.h"
#include <atomic>
/* 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<uint32_t> control{0}, version{0}, published{0};
std::atomic<int32_t> 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;
}
};
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <stdio.h>
#include <string.h>
#include <math.h>
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<count;++i) {
if(i)pfx_json_raw(&w,",");
pfx_json_quote(&w,live[i].name);pfx_json_raw(&w,":[");
for(int j=0;j<4;++j) {
if(j)pfx_json_raw(&w,",");
if(!isfinite(live[i].v[j])){w.ok=0;break;}
char number[48];snprintf(number,sizeof number,"%.9g",(double)live[i].v[j]);pfx_json_raw(&w,number);
}
pfx_json_raw(&w,"]");
}
pfx_json_raw(&w,"}");
if(!w.ok){out[0]=0;return 0;}return (int)w.used;
}
+3 -1
View File
@@ -8785,7 +8785,9 @@ static void handle_post_shader(int id, const char *json)
char path[512];
if (json_get_str(json, "path", path, sizeof path)) gl_renderer_set_post_shader(path);
char info[1536];
gl_renderer_post_shader_debug(info, (int)sizeof info);
if (!gl_renderer_post_shader_debug(info, (int)sizeof info)) {
send_err(id, "post shader diagnostics exceed response capacity"); return;
}
send_fmt("{\"id\":%d,\"ok\":true,\"loaded\":%d,%s}", id, gl_renderer_post_shader_loaded(), info);
}
+17 -17
View File
@@ -1,3 +1,5 @@
#include "gte_postfx_projection.h"
#include "postfx_debug_json.h"
/* ---- Post-effect pipeline (docs/POST_EFFECTS.md) ---------------------------
* One executor for every post effect on the composed game image: chains of
* full-screen fragment passes (postfx_chain.h). Three stages, in order, each
@@ -95,17 +97,18 @@ static uint64_t s_pfx_t0 = 0; /* u_time origin */
* (psx_host_set_post_uniform, host_overlay.h): a live value replaces the
* preset default of the same name in every pass that declares it. */
enum { PFX_LIVE_MAX = 16 };
typedef struct { char name[PFX_NAME]; float v[4]; } PfxLive;
typedef PfxDebugLive PfxLive;
static PfxLive s_pfx_live[PFX_LIVE_MAX];
static int s_pfx_nlive = 0;
static SDL_SpinLock s_pfx_live_lock = 0;
void psx_host_set_post_uniform(const char *name, float x, float y, float z, float w) {
if (!name || !name[0] || strlen(name) >= 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) {
+7 -2
View File
@@ -1,5 +1,6 @@
#include <cmath>
#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;
+40
View File
@@ -0,0 +1,40 @@
#include "gte_projection_publication.hpp"
#include "postfx_debug_json.h"
#include <atomic>
#include <cstdio>
#include <cstring>
#include <limits>
#include <thread>
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<bool> 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<float>::infinity();
check(!pfx_debug_json(json,sizeof json,"","",&live,1)&&!json[0],"nonfinite values fail without invalid JSON");
return failures?1:0;
}