mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Publish coherent postfx projection and escape bounded diagnostic JSON
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user