From 3ad83c910bd0dc132a66a82599af40e311501a38 Mon Sep 17 00:00:00 2001 From: yacineolivier-maker <234833416+yacineolivier-maker@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:58:45 +0200 Subject: [PATCH 1/4] Post effects: depth and history inputs, u_time / u_proj, live uniforms For title-specific lighting presets ([video] post_shader): - `input depth`: view depth sz (GTE screen z) of the presented frame, R32F at window size, rebuilt from the surfaces' PGXP depth; 0 where nothing 3D drew. Only built while a loaded chain reads it. - `input history`: the pass's own target from the last real frame (trails, temporal filters); generated frames do not update it. - `u_time` (seconds since the chain loaded) and `u_proj` (latest GTE projection: 240/H, screen centre in uv, aspect) in every pass. - Live uniforms: psx_host_set_post_uniform(name, x, y, z, w) sets a preset `uniform` from game state (e.g. a light source projected to the screen); the preset value applies until the first call. - Debug: TCP {"cmd":"post_shader"} reports the loaded preset, its parse status and the live values; {"cmd":"post_shader","path":"..."} reloads a preset (or loads another) without restarting, for shader work. docs/POST_EFFECTS.md documents the inputs and the uniforms. Used by a lighting preset for R4 (sun and tunnel light shafts, lamp halos, speed streaks, paint reflections on the player's car from live car pose), about 0.7 ms of GPU at 1080p on an RTX 3060. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Rkyma8rbyPiccs9n2bkyG6 --- docs/POST_EFFECTS.md | 13 ++ runtime/include/gpu_gl_renderer.h | 2 + runtime/include/host_overlay.h | 7 + runtime/include/postfx_chain.h | 10 +- runtime/src/debug_server.c | 12 ++ runtime/src/gl_postfx.inc | 224 ++++++++++++++++++++++++++++-- runtime/src/gte.cpp | 5 + runtime/src/postfx_chain.c | 4 +- 8 files changed, 263 insertions(+), 14 deletions(-) diff --git a/docs/POST_EFFECTS.md b/docs/POST_EFFECTS.md index 33d7def6..22421d69 100644 --- a/docs/POST_EFFECTS.md +++ b/docs/POST_EFFECTS.md @@ -92,6 +92,19 @@ Bloom uses the mask at both ends: only lit pixels feed its bright pass, and only lit pixels take the glow back. HUD pixels are therefore never changed, even next to a lamp. +## Depth, history, time and live uniforms + +- `input depth`: view depth `sz` (GTE screen z) of the presented frame, R32F at window + size, rebuilt from the surfaces' PGXP depth (`zn = 1 - 512/(sz + 256)`); 0 where nothing 3D drew. + Only built while a loaded chain reads it. +- `input history`: this pass's own target from the last real frame (trails, temporal + filters). Generated frames do not update it. +- `u_time` (seconds since the chain loaded) and `u_proj` (latest GTE projection: `240/H`, screen + centre in uv, aspect) are declared in every pass. +- Live uniforms: a title sets a preset `uniform` from game state with + `psx_host_set_post_uniform(name, x, y, z, w)` (`host_overlay.h`), e.g. a light source projected + to the screen; the preset value is the default until the first call. + ## Tests - `postfx_chain_test`: the format, built-in presets, errors and file import. diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 45e79835..cfa24c22 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -199,6 +199,8 @@ float gl_renderer_get_bloom(void); * compile is logged and skipped. */ void gl_renderer_set_post_shader(const char *path); int gl_renderer_post_shader_loaded(void); +/* Debug: path, last load outcome and live uniforms of the user chain (JSON members). */ +int gl_renderer_post_shader_debug(char *out, int cap); typedef struct { float strength, threshold; uint64_t passes, us_n; diff --git a/runtime/include/host_overlay.h b/runtime/include/host_overlay.h index 9064e382..b736165b 100644 --- a/runtime/include/host_overlay.h +++ b/runtime/include/host_overlay.h @@ -56,6 +56,13 @@ void psx_host_overlay_draw(int width, int height); /* renderer side */ void psx_host_set_hud_draw_cb(PsxHostOverlayDrawFn fn, void *ctx); void psx_host_hud_draw(int width, int height); /* renderer side */ +/* ---- Post-effect live uniforms ------------------------------------------ + * Sets the value of a `uniform ...` line of the loaded post-effect chain + * ([video] post_shader, postfx_chain.h) from game state, e.g. a light source + * projected to the screen. Any thread; effective at the next present; the + * preset default applies until the first call. Up to 16 names. */ +void psx_host_set_post_uniform(const char *name, float x, float y, float z, float w); + /* ---- P3: input sink ----------------------------------------------------- * The sink sees every host SDL event first (main thread), in the normal event * drain and in the host pause loop. Returning non-zero consumes the event. diff --git a/runtime/include/postfx_chain.h b/runtime/include/postfx_chain.h index b19759a3..0c87a321 100644 --- a/runtime/include/postfx_chain.h +++ b/runtime/include/postfx_chain.h @@ -9,7 +9,10 @@ * pfx 1 # format version (required first) * pass # starts a pass; the last pass writes the window * shader # fragment body (see below) - * input > + * input > + * # depth: PGXP view depth sz (GTE screen z, R32F; + * # 0 where nothing 3D drew); history: this pass's + * # own target from the last real frame (not last pass) * # source_down: the image scaled to this pass's * # target (one bilinear blit, no full-size copy) * scale # target size, fraction of the game rect (default 1) @@ -27,6 +30,7 @@ * uniform vec2 u_rcp; // 1 / this pass's target size in px * uniform vec2 u_size; // this pass's target size in px * uniform ivec2 u_org; // window origin of the game rect (gl_FragCoord - u_org = px in it) + * uniform float u_time; // seconds since the chain loaded (real and generated frames) * plus one `uniform sampler2D ;` per input. v_uv is 0..1 over the * game rect, bottom-up like the window. */ #ifndef PSX_POSTFX_CHAIN_H @@ -38,7 +42,8 @@ extern "C" { enum { PFX_MAX_PASSES = 16, PFX_MAX_INPUTS = 4, PFX_MAX_UNIFORMS = 8, PFX_NAME = 32 }; /* Input sources; a value >= 0 is an earlier pass's index. */ -enum { PFX_IN_SOURCE = -1, PFX_IN_MASK = -2, PFX_IN_SOURCE_DOWN = -3 }; +enum { PFX_IN_SOURCE = -1, PFX_IN_MASK = -2, PFX_IN_SOURCE_DOWN = -3, PFX_IN_DEPTH = -4, + PFX_IN_HISTORY = -5 }; enum { PFX_BLEND_REPLACE = 0, PFX_BLEND_SCREEN = 1 }; enum { PFX_FMT_RGBA8 = 0, PFX_FMT_RGBA16F = 1 }; @@ -69,6 +74,7 @@ typedef struct { char name[PFX_NAME]; int npasses; int uses_mask; /* some pass reads `mask` */ + int uses_depth; /* some pass reads `depth` */ PfxPass pass[PFX_MAX_PASSES]; } PfxChain; diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 553bcc1b..09a5096a 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8778,6 +8778,17 @@ static void handle_bloom(int id, const char *json) bi.gen_us_mean, (unsigned long long)bi.reused); } +/* post_shader: {"cmd":"post_shader"} reads the user chain (path, load outcome, live + * uniforms); "path" sets it, "" turns it off, the same path reloads it (shader edits). */ +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); + send_fmt("{\"id\":%d,\"ok\":true,\"loaded\":%d,%s}", id, gl_renderer_post_shader_loaded(), info); +} + static void handle_post_aa(int id, const char *json) { int mode = json_get_int(json, "mode", -1); @@ -16347,6 +16358,7 @@ static const CmdEntry s_commands[] = { { "frame_perf", handle_frame_perf }, { "batch_sites", handle_batch_sites }, { "post_aa", handle_post_aa }, + { "post_shader", handle_post_shader }, { "fmv_chroma", handle_fmv_chroma }, { "texture_lod", handle_texture_lod }, { "accurate_blending", handle_accurate_blending }, diff --git a/runtime/src/gl_postfx.inc b/runtime/src/gl_postfx.inc index 2b632835..d01e14d3 100644 --- a/runtime/src/gl_postfx.inc +++ b/runtime/src/gl_postfx.inc @@ -36,6 +36,7 @@ typedef struct { GLint fixed[PFX_LOC_FIXED]; GLint in[PFX_MAX_INPUTS]; GLint u[PFX_MAX_UNIFORMS]; + GLint time, proj; } PfxLocs; typedef struct { PfxChain chain; @@ -46,6 +47,8 @@ typedef struct { GLuint dtex[PFX_MAX_PASSES], dfbo[PFX_MAX_PASSES]; /* source_down copies */ int dw[PFX_MAX_PASSES], dh[PFX_MAX_PASSES]; int tw[PFX_MAX_PASSES], th[PFX_MAX_PASSES]; + GLuint htex[PFX_MAX_PASSES], hfbo[PFX_MAX_PASSES]; /* `history` copies */ + int hw[PFX_MAX_PASSES], hh[PFX_MAX_PASSES], hist[PFX_MAX_PASSES]; } PfxStage; enum { PFX_STAGE_BLOOM, PFX_STAGE_USER, PFX_STAGE_AA, PFX_STAGE_HQ, PFX_NSTAGES }; static PfxStage s_pfx[PFX_NSTAGES]; @@ -78,6 +81,49 @@ static GLint s_mmap_uUv = -1, s_mmap_uS = -1, s_mmap_uSrc = -1, s_mmap_uDst = - * depth-stencil blit is needed (one costs the CPU milliseconds on macOS). */ typedef struct { GLuint rb, tex, fbo; int w, h; } PfxSurfMask; static PfxSurfMask s_smask[2]; +/* `depth`: each surface's depth-stencil renderbuffer is blitted into a depth + * texture of its size (dtex/dfbo), then mapped to the window as view depth sz + * (GTE screen z; PGXP writes zn = 1 - 512/(sz + 256)) in an R32F texture. */ +typedef struct { GLuint rb, dtex, dfbo; int w, h; } PfxSurfDepth; +static PfxSurfDepth s_sdepth[2]; +static GLuint s_depth_tex = 0, s_depth_fbo = 0, s_depth_map_prog = 0; +static GLint s_dmap_uUv = -1, s_dmap_uS = -1, s_dmap_uSrc = -1, s_dmap_uDst = -1; +static int s_depth_w = 0, s_depth_h = 0, s_depth_ok = 0; +static int s_pfx_depth_on = 0; /* a loaded chain reads `depth` */ +static uint64_t s_pfx_t0 = 0; /* u_time origin */ +/* Live values for preset `uniform` lines, set by the title from game state + * (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; +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; + 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); + s_pfx_nlive++; + } + if (i < s_pfx_nlive) { + s_pfx_live[i].v[0] = x; s_pfx_live[i].v[1] = y; s_pfx_live[i].v[2] = z; s_pfx_live[i].v[3] = w; + } + SDL_UnlockSpinlock(&s_pfx_live_lock); +} +/* Last load outcome of the user chain, for the debug server. */ +static char s_pfx_user_status[192] = ""; +static int pfx_live_get(const char *name, float out[4]) { + int found = 0; + SDL_LockSpinlock(&s_pfx_live_lock); + for (int i = 0; i < s_pfx_nlive; i++) + if (!strcmp(s_pfx_live[i].name, name)) { memcpy(out, s_pfx_live[i].v, sizeof s_pfx_live[i].v); found = 1; break; } + 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) { @@ -102,7 +148,10 @@ static void pfx_stage_unload(PfxStage *st) { if (st->tex[i]) glDeleteTextures(1, &st->tex[i]); if (st->dfbo[i]) p_glDeleteFramebuffers(1, &st->dfbo[i]); if (st->dtex[i]) glDeleteTextures(1, &st->dtex[i]); + if (st->hfbo[i]) p_glDeleteFramebuffers(1, &st->hfbo[i]); + if (st->htex[i]) glDeleteTextures(1, &st->htex[i]); } + st->htex[i] = st->hfbo[i] = 0; st->hw[i] = st->hh[i] = 0; st->hist[i] = 0; st->prog[i] = st->tex[i] = st->fbo[i] = 0; st->tw[i] = st->th[i] = 0; st->dtex[i] = st->dfbo[i] = 0; st->dw[i] = st->dh[i] = 0; } @@ -118,6 +167,9 @@ static void pfx_mask_wanted_update(void) { if (i == PFX_STAGE_BLOOM) want = 1; /* the bloom preset reads it */ else if (s_pfx[i].loaded && s_pfx[i].chain.uses_mask) want = 1; } + s_pfx_depth_on = 0; + for (int i = 0; i < PFX_NSTAGES; i++) + if (pfx_stage_on(i) && s_pfx[i].loaded && s_pfx[i].chain.uses_depth) s_pfx_depth_on = 1; if (want != s_bcls_on) { s_bcls_on = want; pgxp_render_wanted_update(); } } @@ -135,26 +187,41 @@ static int pfx_stage_load(int i) { if (!ok) { fprintf(stdout, "psxrecomp: post effect '%s' not loaded: %s\n", i == PFX_STAGE_USER ? s_pfx_user_path : "built-in", err); + if (i == PFX_STAGE_USER) snprintf(s_pfx_user_status, sizeof s_pfx_user_status, "not loaded: %s", err); st->failed = 1; return 0; } st->loaded = 1; - if (i == PFX_STAGE_USER) + for (int p = 0; p < st->chain.npasses; p++) + for (int k = 0; k < st->chain.pass[p].ninputs; k++) + if (st->chain.pass[p].in[k].from == PFX_IN_HISTORY) st->hist[p] = 1; + if (i == PFX_STAGE_USER) { fprintf(stdout, "psxrecomp: post shader '%s' (%d passes%s)\n", s_pfx_user_path, st->chain.npasses, st->chain.uses_mask ? ", mask" : ""); + snprintf(s_pfx_user_status, sizeof s_pfx_user_status, "parsed: %d passes", st->chain.npasses); + } return 1; } static uint64_t s_pfx_builds = 0; /* program builds (tests) */ static GLuint pfx_build_pass(const PfxPass *ps) { s_pfx_builds++; - size_t n = strlen(ps->shader) + 512 + (size_t)ps->ninputs * 64; + size_t n = strlen(ps->shader) + 512 + (size_t)ps->ninputs * 64 + (size_t)ps->nuniforms * 64; char *src = (char *)malloc(n); if (!src) return 0; int k = snprintf(src, n, "#version 330\nin vec2 v_uv; out vec4 o;\n" - "uniform vec2 u_rcp; uniform vec2 u_size; uniform ivec2 u_org;\n"); + "uniform vec2 u_rcp; uniform vec2 u_size; uniform ivec2 u_org;\n" + "uniform float u_time; uniform vec4 u_proj;\n"); for (int i = 0; i < ps->ninputs; i++) k += snprintf(src + k, n - (size_t)k, "uniform sampler2D %s;\n", ps->in[i].sampler); + /* The preset's `uniform` lines are declared here unless the body declares + * them itself (the built-in shaders do). */ + static const char *const kTypes[] = { "float", "vec2", "vec3", "vec4" }; + for (int i = 0; i < ps->nuniforms && (size_t)k + 96 < n; i++) { + char decl[PFX_NAME + 16]; + snprintf(decl, sizeof decl, "%s %s;", kTypes[ps->u[i].count - 1], ps->u[i].name); + if (!strstr(ps->shader, decl)) k += snprintf(src + k, n - (size_t)k, "uniform %s\n", decl); + } snprintf(src + k, n - (size_t)k, "%s", ps->shader); GLuint prog = build_program(PRESENT_VS, src); free(src); @@ -188,7 +255,7 @@ static void pfx_rect_px(GLuint tex, float tex_w, float tex_h, int x, int y, int static void postfx_note_center(GLuint fbo, GLuint tex, float tex_w, float tex_h, int x, int y, int w, int h, int off, int wide_w) { s_mask_ctr_valid = 0; - if (!s_bcls_on || !fbo || !tex || wide_w <= 0) return; + if ((!s_bcls_on && !s_pfx_depth_on) || !fbo || !tex || wide_w <= 0) return; s_mask_ctr_rb = pfx_surface_rb(fbo); if (!s_mask_ctr_rb) return; pfx_rect_px(tex, tex_w, tex_h, x, y, w, h, s_mask_ctr, &s_mask_ctr_tw, &s_mask_ctr_th); @@ -204,7 +271,7 @@ static void postfx_note_source(GLuint fbo, GLuint tex, float tex_w, float tex_h, int x, int y, int w, int h) { s_mask_src_valid = 0; s_mask_ctr_valid = 0; - if (!s_bcls_on || !fbo || !tex || tex_w <= 0.0f || tex_h <= 0.0f) return; + if ((!s_bcls_on && !s_pfx_depth_on) || !fbo || !tex || tex_w <= 0.0f || tex_h <= 0.0f) return; s_mask_src_rb = pfx_surface_rb(fbo); if (!s_mask_src_rb) return; pfx_rect_px(tex, tex_w, tex_h, x, y, w, h, s_mask_src, &s_mask_src_tw, &s_mask_src_th); @@ -217,6 +284,7 @@ static void pfx_tex_alloc(GLuint *t, int w, int h, int fmt, int nearest) { glBindTexture(GL_TEXTURE_2D, *t); if (fmt == PFX_FMT_RGBA16F) glTexImage2D(GL_TEXTURE_2D, 0, 0x881A /*RGBA16F*/, w, h, 0, GL_RGBA, GL_FLOAT, NULL); else if (fmt == -1) glTexImage2D(GL_TEXTURE_2D, 0, 0x8229 /*R8*/, w, h, 0, 0x1903 /*RED*/, GL_UNSIGNED_BYTE, NULL); + else if (fmt == -2) glTexImage2D(GL_TEXTURE_2D, 0, 0x822E /*R32F*/, w, h, 0, 0x1903 /*RED*/, GL_FLOAT, NULL); else glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); @@ -309,6 +377,81 @@ static int pfx_mask_build(int fresh, int lw, int lh) { return s_mask_ok; } +/* Slot k: surface rb (tw x th) blitted into its depth texture. */ +static int pfx_surf_depth(int k, GLuint fbo, GLuint rb, int tw, int th) { + PfxSurfDepth *d = &s_sdepth[k]; + if (d->rb != rb || d->w != tw || d->h != th) { + if (!d->dtex) glGenTextures(1, &d->dtex); + glBindTexture(GL_TEXTURE_2D, d->dtex); + glTexImage2D(GL_TEXTURE_2D, 0, PSXGL_DEPTH24_STENCIL8, tw, th, 0, + 0x84F9 /*DEPTH_STENCIL*/, 0x84FA /*UNSIGNED_INT_24_8*/, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + if (!d->dfbo) p_glGenFramebuffers(1, &d->dfbo); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, d->dfbo); + p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, d->dtex, 0); + d->rb = rb; d->w = tw; d->h = th; + } + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, d->dfbo); + p_glBlitFramebuffer(0, 0, tw, th, 0, 0, tw, th, GL_DEPTH_BUFFER_BIT, GL_NEAREST); + return 1; +} +static void pfx_depth_map(int k, const int r[4], int x0, int x1, int lh) { + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_depth_fbo); + glViewport(x0, 0, x1 - x0, lh); + p_glUseProgram(s_depth_map_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_sdepth[k].dtex); + p_glUniform1i(s_dmap_uS, 0); + p_glUniform4i(s_dmap_uSrc, r[0], r[1], r[2], r[3]); + p_glUniform4f(s_dmap_uDst, (float)x0, (float)(x1 - x0), (float)lh, 0.0f); + post_fullscreen(s_depth_map_prog, s_dmap_uUv); + glBindTexture(GL_TEXTURE_2D, 0); +} +/* Window-sized view depth from the presented surface (fresh) or the last. */ +static int pfx_depth_build(int fresh, int lw, int lh) { + if (!s_depth_map_prog) { + s_depth_map_prog = build_program(PRESENT_VS, + "#version 330\nin vec2 v_uv; out vec4 o; uniform sampler2D u_s;\n" + "uniform ivec4 u_src; uniform vec4 u_dst;\n" + "void main(){\n" + " vec2 f = gl_FragCoord.xy;\n" + " int sx = u_src.x + int(floor((f.x - u_dst.x) * float(u_src.z - u_src.x) / u_dst.y));\n" + " int sy = u_src.y + int(floor((u_dst.z - f.y) * float(u_src.w - u_src.y) / u_dst.z));\n" + " float d = texelFetch(u_s, ivec2(sx, sy), 0).r;\n" + " /* zn = 2d - 1 = 1 - 512/(sz + 256); d = 0.5 is the zn = 0 of 2D draws */\n" + " float sz = (d >= 0.999999 || abs(d - 0.5) < 1e-6) ? 0.0 : 256.0 / max(1.0 - d, 1e-6) - 256.0;\n" + " o = vec4(sz);\n" + "}\n"); + if (!s_depth_map_prog) return 0; + s_dmap_uUv = p_glGetUniformLocation(s_depth_map_prog, "u_uv_rect"); + s_dmap_uS = p_glGetUniformLocation(s_depth_map_prog, "u_s"); + s_dmap_uSrc = p_glGetUniformLocation(s_depth_map_prog, "u_src"); + s_dmap_uDst = p_glGetUniformLocation(s_depth_map_prog, "u_dst"); + } + if (s_depth_w != lw || s_depth_h != lh) { + pfx_tex_alloc(&s_depth_tex, lw, lh, -2, 1); + pfx_fbo_attach(&s_depth_fbo, s_depth_tex); + s_depth_w = lw; s_depth_h = lh; s_depth_ok = 0; + } + if (fresh && s_mask_src_valid) { + pfx_surf_depth(0, s_mask_src_fbo, s_mask_src_rb, s_mask_src_tw, s_mask_src_th); + if (s_mask_ctr_valid) + pfx_surf_depth(1, s_mask_ctr_fbo, s_mask_ctr_rb, s_mask_ctr_tw, s_mask_ctr_th); + pfx_depth_map(0, s_mask_src, 0, lw, lh); + if (s_mask_ctr_valid) { + const int c0 = (int)lrintf(s_mask_ctr_x0 * (float)lw), c1 = (int)lrintf(s_mask_ctr_x1 * (float)lw); + if (c1 > c0) pfx_depth_map(1, s_mask_ctr, c0, c1, lh); + } + s_depth_ok = 1; + } + return s_depth_ok; +} + /* Run one stage's chain over the window rect. 0 when it could not run. * generated: a Smooth motion in-between frame; passes marked `generated * reuse` keep the target the last real frame left (when it is there at this @@ -330,12 +473,16 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in L->fixed[PFX_LOC_SIZE] = p_glGetUniformLocation(pg, "u_size"); L->fixed[PFX_LOC_ORG] = p_glGetUniformLocation(pg, "u_org"); L->fixed[PFX_LOC_UV] = p_glGetUniformLocation(pg, "u_uv_rect"); + L->time = p_glGetUniformLocation(pg, "u_time"); + L->proj = p_glGetUniformLocation(pg, "u_proj"); for (int k = 0; k < ps->ninputs; k++) L->in[k] = p_glGetUniformLocation(pg, ps->in[k].sampler); for (int k = 0; k < ps->nuniforms; k++) L->u[k] = p_glGetUniformLocation(pg, ps->u[k].name); } if (!st->prog[i]) { fprintf(stdout, "psxrecomp: post effect '%s' pass '%s' did not compile; effect off\n", c->name, ps->name); + if (st == &s_pfx[PFX_STAGE_USER]) + snprintf(s_pfx_user_status, sizeof s_pfx_user_status, "pass '%s' did not compile", ps->name); st->failed = 1; return 0; } @@ -381,7 +528,9 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in const int from = ps->in[k].from; const GLuint t = from == PFX_IN_SOURCE ? s_post_copy_tex : from == PFX_IN_SOURCE_DOWN ? st->dtex[i] - : from == PFX_IN_MASK ? s_mask_tex : st->tex[from]; + : from == PFX_IN_MASK ? s_mask_tex + : from == PFX_IN_DEPTH ? s_depth_tex + : from == PFX_IN_HISTORY ? st->htex[i] : st->tex[from]; p_glActiveTexture(PSXGL_TEXTURE0 + (GLenum)k); glBindTexture(GL_TEXTURE_2D, t); p_glUniform1i(L->in[k], k); @@ -390,15 +539,30 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in p_glUniform2f(L->fixed[PFX_LOC_RCP], 1.0f / (float)w, 1.0f / (float)h); p_glUniform2f(L->fixed[PFX_LOC_SIZE], (float)w, (float)h); p_glUniform2i(L->fixed[PFX_LOC_ORG], last ? lx : 0, last ? ly : 0); + if (!s_pfx_t0) s_pfx_t0 = SDL_GetPerformanceCounter(); + if (L->proj >= 0) { + /* 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; + 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); + } + if (L->time >= 0) + p_glUniform1f(L->time, (float)((double)(SDL_GetPerformanceCounter() - s_pfx_t0) / + (double)SDL_GetPerformanceFrequency())); for (int k = 0; k < ps->nuniforms; k++) { const PfxUniform *u = &ps->u[k]; const GLint loc = L->u[k]; if (loc < 0) continue; + float live[4]; + const float *v = pfx_live_get(u->name, live) ? live : u->v; switch (u->count) { - case 1: p_glUniform1f(loc, u->v[0]); break; - case 2: p_glUniform2f(loc, u->v[0], u->v[1]); break; - case 3: p_glUniform3f(loc, u->v[0], u->v[1], u->v[2]); break; - default: p_glUniform4f(loc, u->v[0], u->v[1], u->v[2], u->v[3]); break; + case 1: p_glUniform1f(loc, v[0]); break; + case 2: p_glUniform2f(loc, v[0], v[1]); break; + case 3: p_glUniform3f(loc, v[0], v[1], v[2]); break; + default: p_glUniform4f(loc, v[0], v[1], v[2], v[3]); break; } } if (ps->blend == PFX_BLEND_SCREEN) { @@ -408,6 +572,17 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in } post_fullscreen(prog, L->fixed[PFX_LOC_UV]); if (ps->blend != PFX_BLEND_REPLACE) glDisable(GL_BLEND); + if (!last && !generated && st->hist[i]) { + /* This pass's target becomes its `history` for the next real frame. */ + if (st->hw[i] != w || st->hh[i] != h) { + pfx_tex_alloc(&st->htex[i], w, h, ps->format, ps->nearest); + pfx_fbo_attach(&st->hfbo[i], st->htex[i]); + st->hw[i] = w; st->hh[i] = h; + } + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, st->fbo[i]); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, st->hfbo[i]); + p_glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST); + } } for (int k = PFX_MAX_INPUTS - 1; k >= 0; k--) { p_glActiveTexture(PSXGL_TEXTURE0 + (GLenum)k); @@ -453,7 +628,7 @@ static void postfx_present(int fresh, int with_aa, int lx, int ly, int lw, int l PfxGlState saved; pfx_state_save(&saved); glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_SCISSOR_TEST); glDisable(GL_STENCIL_TEST); - int mask_done = 0, mask_ok = 0; + int mask_done = 0, mask_ok = 0, depth_done = 0, depth_ok = 0; for (int i = 0; i < PFX_NSTAGES; i++) { if (!pfx_stage_on(i)) continue; if ((i == PFX_STAGE_AA || i == PFX_STAGE_HQ) && !with_aa) continue; @@ -463,6 +638,10 @@ static void postfx_present(int fresh, int with_aa, int lx, int ly, int lw, int l if (!mask_done) { mask_ok = pfx_mask_build(src_fresh, lw, lh); mask_done = 1; } if (!mask_ok) continue; /* no marks yet */ } + if (st->chain.uses_depth) { + if (!depth_done) { depth_ok = pfx_depth_build(src_fresh, lw, lh); depth_done = 1; } + if (!depth_ok) continue; /* no depth yet */ + } if (i == PFX_STAGE_BLOOM) { const float k = s_bloom; pfx_chain_set_uniform(&st->chain, "u_strength", 1, &k); @@ -515,6 +694,12 @@ static void postfx_release(void) { } s_mask_tex = s_mask_fbo = s_mask_prog = s_mask_map_prog = 0; memset(s_smask, 0, sizeof s_smask); + s_depth_tex = s_depth_fbo = s_depth_map_prog = 0; s_depth_w = s_depth_h = 0; s_depth_ok = 0; + memset(s_sdepth, 0, sizeof s_sdepth); + for (int i = 0; i < PFX_NSTAGES; i++) + for (int k = 0; k < PFX_MAX_PASSES; k++) { + s_pfx[i].htex[k] = s_pfx[i].hfbo[k] = 0; s_pfx[i].hw[k] = s_pfx[i].hh[k] = 0; + } s_mask_w = s_mask_h = 0; s_mask_ok = 0; s_mask_src_valid = 0; } @@ -538,6 +723,23 @@ void gl_renderer_set_post_shader(const char *path) { } pfx_mask_wanted_update(); } +/* 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); + 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]); + SDL_UnlockSpinlock(&s_pfx_live_lock); + if (k < cap - 2) k += snprintf(out + k, (size_t)(cap - k), "}"); + return k; +} + int gl_renderer_post_shader_loaded(void) { GL_RT_SYNC("post_shader"); return s_pfx[PFX_STAGE_USER].loaded && !s_pfx[PFX_STAGE_USER].failed; diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 7c9c36ce..fce93bbf 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -20,6 +20,8 @@ extern "C" int gpu_ws_precise_nclip_enabled(void); * plus native replay of one candidate) is running. */ 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; } extern "C" uint32_t g_debug_last_store_pc; /* debug_server.c */ extern "C" { int (*g_psx_projection_command)(CPUState*, uint32_t) = nullptr; } @@ -1206,6 +1208,9 @@ 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; + } int64_t sx16 = gte->OFX + xterm; int64_t sy16 = gte->OFY + (int64_t)gte->IR2 * h_div_sz; /* Render-view projection override (psx_mod_render_view, projection=1). diff --git a/runtime/src/postfx_chain.c b/runtime/src/postfx_chain.c index 6c735552..76d06f04 100644 --- a/runtime/src/postfx_chain.c +++ b/runtime/src/postfx_chain.c @@ -238,13 +238,15 @@ int pfx_chain_parse(const char *text, const char *name, PfxShaderLoader load, vo if (!strcmp(f, "source")) from = PFX_IN_SOURCE; else if (!strcmp(f, "source_down")) from = PFX_IN_SOURCE_DOWN; else if (!strcmp(f, "mask")) { from = PFX_IN_MASK; out->uses_mask = 1; } + else if (!strcmp(f, "depth")) { from = PFX_IN_DEPTH; out->uses_depth = 1; } + else if (!strcmp(f, "history")) from = PFX_IN_HISTORY; else if (!strcmp(f, "previous")) { if (self == 0) { pfx_chain_free(out); return pfx_fail(err, errlen, line, "the first pass has no previous"); } from = self - 1; } else { from = -100; for (int i = 0; i < self; i++) if (!strcmp(out->pass[i].name, f)) from = i; - if (from == -100) { pfx_chain_free(out); return pfx_fail(err, errlen, line, "unknown input '%s' (source, mask, previous or an earlier pass)", f); } + if (from == -100) { pfx_chain_free(out); return pfx_fail(err, errlen, line, "unknown input '%s' (source, mask, depth, history, previous or an earlier pass)", f); } } snprintf(cur->in[cur->ninputs].sampler, PFX_NAME, "%s", s); cur->in[cur->ninputs++].from = from; 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 2/4] 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; +} From dcde68f98fc86ceb763d8a67da32e0d501be0450 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 09:56:17 -0700 Subject: [PATCH 3/4] Keep isolated OpenGL fixtures linked with optional feature API doubles --- runtime/tests/gl_postfx_projection_fixture.h | 6 ++++++ runtime/tests/test_gl_frame_gen.c | 1 + runtime/tests/test_gl_readback_region.c | 1 + runtime/tests/test_gl_render_thread.c | 1 + runtime/tests/test_gl_scale_invariance.c | 1 + 5 files changed, 10 insertions(+) create mode 100644 runtime/tests/gl_postfx_projection_fixture.h diff --git a/runtime/tests/gl_postfx_projection_fixture.h b/runtime/tests/gl_postfx_projection_fixture.h new file mode 100644 index 00000000..7dfa91f4 --- /dev/null +++ b/runtime/tests/gl_postfx_projection_fixture.h @@ -0,0 +1,6 @@ +#pragma once +/* Renderer fixtures have no guest CPU; exercise the neutral unavailable projection. + * Atomic publication is covered independently by postfx_publication_test. */ +#include "gte_postfx_projection.h" +void gte_postfx_projection_enable(int enabled) {(void)enabled;} +int gte_postfx_projection_read(GtePostfxProjection *out) {(void)out;return 0;} diff --git a/runtime/tests/test_gl_frame_gen.c b/runtime/tests/test_gl_frame_gen.c index f3f65c3e..03821090 100644 --- a/runtime/tests/test_gl_frame_gen.c +++ b/runtime/tests/test_gl_frame_gen.c @@ -34,6 +34,7 @@ static void fixture_present(int generated); static void fixture_window(int slot); #define GL_PRESENT_THREAD_TEST_HOOK(slot) fixture_window(slot) #include "gpu_gl_renderer.c" +#include "gl_postfx_projection_fixture.h" #include "gpu_hd_texture_stubs.inc" #include "mod_texture_banks.c" #include "gl_batch_policy_lle.c" diff --git a/runtime/tests/test_gl_readback_region.c b/runtime/tests/test_gl_readback_region.c index 9a5e8350..9b245733 100644 --- a/runtime/tests/test_gl_readback_region.c +++ b/runtime/tests/test_gl_readback_region.c @@ -1,5 +1,6 @@ /* Original source-owned GL readback-coherence regression. No retail payload. */ #include "gpu_gl_renderer.c" +#include "gl_postfx_projection_fixture.h" #include "gpu_hd_texture_stubs.inc" #include "mod_texture_banks.c" #include "gl_batch_policy_lle.c" diff --git a/runtime/tests/test_gl_render_thread.c b/runtime/tests/test_gl_render_thread.c index 0d095002..d15bbe73 100644 --- a/runtime/tests/test_gl_render_thread.c +++ b/runtime/tests/test_gl_render_thread.c @@ -30,6 +30,7 @@ * surface), and with the thread on its record/acquire counters. * Original source-owned scene; no retail payload. */ #include "gpu_gl_renderer.c" +#include "gl_postfx_projection_fixture.h" #include "gpu_hd_texture_stubs.inc" #include "mod_texture_banks.c" #include "gl_batch_policy_lle.c" diff --git a/runtime/tests/test_gl_scale_invariance.c b/runtime/tests/test_gl_scale_invariance.c index e89d304c..51e6a867 100644 --- a/runtime/tests/test_gl_scale_invariance.c +++ b/runtime/tests/test_gl_scale_invariance.c @@ -81,6 +81,7 @@ * 3x as the nearest rescale of all their pixels, before any redraw. * Original source-owned scene; no retail payload. */ #include "gpu_gl_renderer.c" +#include "gl_postfx_projection_fixture.h" #include "gpu_hd_texture_stubs.inc" #include "mod_texture_banks.c" #include "gl_batch_policy_lle.c" From 81dc805e8df9d66c4960eaf1c008b0aac06d1e95 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 10:25:45 -0700 Subject: [PATCH 4/4] Preserve final-pass history and reset each shader chain clock on reload --- docs/POST_EFFECTS.md | 8 ++- runtime/src/gl_postfx.inc | 35 +++++++---- runtime/tests/test_gl_bloom.c | 79 +++++++++++++++++++++++- runtime/tests/test_gl_scale_invariance.c | 9 +++ 4 files changed, 115 insertions(+), 16 deletions(-) diff --git a/docs/POST_EFFECTS.md b/docs/POST_EFFECTS.md index 22421d69..7cc42d94 100644 --- a/docs/POST_EFFECTS.md +++ b/docs/POST_EFFECTS.md @@ -98,8 +98,8 @@ even next to a lamp. size, rebuilt from the surfaces' PGXP depth (`zn = 1 - 512/(sz + 256)`); 0 where nothing 3D drew. Only built while a loaded chain reads it. - `input history`: this pass's own target from the last real frame (trails, temporal - filters). Generated frames do not update it. -- `u_time` (seconds since the chain loaded) and `u_proj` (latest GTE projection: `240/H`, screen + filters), including the final window pass. Generated frames do not update it. History starts empty after loading or resizing. +- `u_time` (host seconds since the chain first drew, reset on reload) and `u_proj` (latest GTE projection: `240/H`, screen centre in uv, aspect) are declared in every pass. - Live uniforms: a title sets a preset `uniform` from game state with `psx_host_set_post_uniform(name, x, y, z, w)` (`host_overlay.h`), e.g. a light source projected @@ -115,3 +115,7 @@ even next to a lamp. thread and on the render thread. - `gl_post_aa_test`: FXAA on the pipeline. Its output is byte-identical to the former fixed pass. + +The projection is a coherent latest CPU value. It is not associated with a +queued frame or an individual replay view; shaders requiring an exact camera +for a delayed or stereo image need corresponding frame/view metadata. diff --git a/runtime/src/gl_postfx.inc b/runtime/src/gl_postfx.inc index b6e07e02..1e652462 100644 --- a/runtime/src/gl_postfx.inc +++ b/runtime/src/gl_postfx.inc @@ -1,5 +1,8 @@ #include "gte_postfx_projection.h" #include "postfx_debug_json.h" +#ifndef PFX_NOW_TICKS +#define PFX_NOW_TICKS() SDL_GetPerformanceCounter() +#endif /* ---- 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 @@ -43,6 +46,7 @@ typedef struct { typedef struct { PfxChain chain; int loaded, failed; + uint64_t started_at; GLuint prog[PFX_MAX_PASSES]; PfxLocs loc[PFX_MAX_PASSES]; GLuint tex[PFX_MAX_PASSES], fbo[PFX_MAX_PASSES]; @@ -92,7 +96,6 @@ static GLuint s_depth_tex = 0, s_depth_fbo = 0, s_depth_map_prog = 0; static GLint s_dmap_uUv = -1, s_dmap_uS = -1, s_dmap_uSrc = -1, s_dmap_uDst = -1; static int s_depth_w = 0, s_depth_h = 0, s_depth_ok = 0; static int s_pfx_depth_on = 0; /* a loaded chain reads `depth` */ -static uint64_t s_pfx_t0 = 0; /* u_time origin */ /* Live values for preset `uniform` lines, set by the title from game state * (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. */ @@ -160,6 +163,7 @@ static void pfx_stage_unload(PfxStage *st) { } pfx_chain_free(&st->chain); st->loaded = st->failed = 0; + st->started_at = 0; } /* Which stages read the mask decides whether draws keep it. */ @@ -465,6 +469,7 @@ static int pfx_depth_build(int fresh, int lw, int lh) { static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, int lh) { PfxChain *c = &st->chain; int copied = 0; + if (!st->started_at) st->started_at = PFX_NOW_TICKS(); for (int i = 0; i < c->npasses; i++) { const PfxPass *ps = &c->pass[i]; const int last = i == c->npasses - 1; @@ -523,6 +528,17 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in p_glBlitFramebuffer(lx, ly, lx + lw, ly + lh, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR); } } + /* Allocate/reset before sampling, including the final window pass. + * A resize starts an empty history rather than sampling the old size. */ + if (st->hist[i] && (st->hw[i] != w || st->hh[i] != h)) { + pfx_tex_alloc(&st->htex[i], w, h, ps->format, ps->nearest); + pfx_fbo_attach(&st->hfbo[i], st->htex[i]); + st->hw[i] = w; st->hh[i] = h; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, st->hfbo[i]); + GLfloat clear[4]; glGetFloatv(GL_COLOR_CLEAR_VALUE, clear); + glClearColor(0,0,0,0); glClear(GL_COLOR_BUFFER_BIT); + glClearColor(clear[0],clear[1],clear[2],clear[3]); + } if (last) { p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); glViewport(lx, ly, lw, lh); } else { p_glBindFramebuffer(PSXGL_FRAMEBUFFER, st->fbo[i]); glViewport(0, 0, w, h); } const GLuint prog = st->prog[i]; @@ -543,7 +559,6 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in p_glUniform2f(L->fixed[PFX_LOC_RCP], 1.0f / (float)w, 1.0f / (float)h); p_glUniform2f(L->fixed[PFX_LOC_SIZE], (float)w, (float)h); p_glUniform2i(L->fixed[PFX_LOC_ORG], last ? lx : 0, last ? ly : 0); - if (!s_pfx_t0) s_pfx_t0 = SDL_GetPerformanceCounter(); if (L->proj >= 0) { /* 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). */ @@ -556,7 +571,7 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in 1.0f - (ofy > 0.0f ? ofy : 120.0f) / 240.0f, aspect); } if (L->time >= 0) - p_glUniform1f(L->time, (float)((double)(SDL_GetPerformanceCounter() - s_pfx_t0) / + p_glUniform1f(L->time, (float)((double)(PFX_NOW_TICKS() - st->started_at) / (double)SDL_GetPerformanceFrequency())); for (int k = 0; k < ps->nuniforms; k++) { const PfxUniform *u = &ps->u[k]; @@ -578,16 +593,12 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in } post_fullscreen(prog, L->fixed[PFX_LOC_UV]); if (ps->blend != PFX_BLEND_REPLACE) glDisable(GL_BLEND); - if (!last && !generated && st->hist[i]) { - /* This pass's target becomes its `history` for the next real frame. */ - if (st->hw[i] != w || st->hh[i] != h) { - pfx_tex_alloc(&st->htex[i], w, h, ps->format, ps->nearest); - pfx_fbo_attach(&st->hfbo[i], st->htex[i]); - st->hw[i] = w; st->hh[i] = h; - } - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, st->fbo[i]); + if (!generated && st->hist[i]) { + /* Capture the final window target as well as intermediate FBOs. */ + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, last ? 0 : st->fbo[i]); p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, st->hfbo[i]); - p_glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST); + const int x = last ? lx : 0, y = last ? ly : 0; + p_glBlitFramebuffer(x, y, x+w, y+h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST); } } for (int k = PFX_MAX_INPUTS - 1; k >= 0; k--) { diff --git a/runtime/tests/test_gl_bloom.c b/runtime/tests/test_gl_bloom.c index 1ca26cc4..9ae618fb 100644 --- a/runtime/tests/test_gl_bloom.c +++ b/runtime/tests/test_gl_bloom.c @@ -11,10 +11,17 @@ * bottom-up) so a run against another tree can be compared byte for byte * (BLOOM_FIXTURE_NO_BLOOM builds it without the bloom API). */ #define GL_PRESENT_TEST_HOOK(gen) bloom_capture_hook(gen) +#include +static uint64_t pfx_fixture_clock(void); +#define PFX_NOW_TICKS() pfx_fixture_clock() static void bloom_capture_hook(int gen); #define main scale_fixture_main #include "test_gl_scale_invariance.c" #undef main +static uint64_t s_pfx_test_clock; +static uint64_t pfx_fixture_clock(void) { + return s_pfx_test_clock ? s_pfx_test_clock : SDL_GetPerformanceCounter(); +} void gpu_hd_textures_begin_upload(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;} void gpu_hd_textures_track_upload(int x,int y,int w,int h,const uint16_t* p){(void)x;(void)y;(void)w;(void)h;(void)p;} void gpu_hd_textures_invalidate(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;} @@ -54,6 +61,7 @@ static void bloom_capture_hook(int gen) { } enum { SC_2D, SC_ADD, SC_3D, SC_HUD_OVER_3D, SC_PARTIAL, SC_N }; +enum { SC_DEPTH_ONLY = 99 }; static const char *k_scene[SC_N] = { "2d", "additive", "3d", "hud-over-3d", "partial" }; /* SC_PARTIAL's grey HUD panels (native x, y, w, h): one beside the lamp, one * covering part of it. */ @@ -77,8 +85,10 @@ static uint8_t *frame(int scene, float bloom, uint64_t *vram_hash) { glb_set_mask_bits(0, 0); glb_set_semi_transparency(0, 0); glb_fill_rect(0, 0, FRAME_W, FRAME_H, 0x0000); /* A dark 3D ground everywhere: lit (may take a halo), below the threshold. */ - tri3d(0, 0, FRAME_W, 0, 0, FRAME_H, 0x0842); - tri3d(FRAME_W, 0, FRAME_W, FRAME_H, 0, FRAME_H, 0x0842); + if (scene != SC_DEPTH_ONLY) { + tri3d(0, 0, FRAME_W, 0, 0, FRAME_H, 0x0842); + tri3d(FRAME_W, 0, FRAME_W, FRAME_H, 0, FRAME_H, 0x0842); + } switch (scene) { case SC_2D: /* a white HUD panel */ glb_draw_flat_rect(120, 90, 80, 60, 0x7fff); @@ -88,6 +98,7 @@ static uint8_t *frame(int scene, float bloom, uint64_t *vram_hash) { glb_draw_flat_triangle(120, 150, 200, 150, 160, 80, 0x7fff); glb_set_semi_transparency(0, 0); break; + case SC_DEPTH_ONLY: case SC_3D: /* a lit 3D polygon */ tri3d(120, 150, 200, 150, 160, 80, 0x7fff); break; @@ -234,6 +245,7 @@ int main(int argc, char **argv) { fputs("void main(){ o = this is not glsl; }\n", f); fclose(f); } + gl_renderer_render_thread_stop(); for (int pass = 0; pass < 2; pass++) { /* 0: this thread, 1: render thread */ uint64_t h = 0; gl_renderer_set_post_shader("pfx_invert.pfx"); @@ -261,6 +273,69 @@ int main(int argc, char **argv) { check(lit > s_cap_w * s_cap_h / 2, "mask input: the 3D ground and lamp are lit"); free(m); + /* A history input also works on the final window pass. The blue + * channel observes the shader clock, so reload must reset its origin. */ + FILE *history_file=fopen("pfx_history.glsl","w"); + check(history_file!=NULL,"open history shader"); + if(history_file) { + fputs("void main(){ float r=texelFetch(u_prev,ivec2(gl_FragCoord.xy)-u_org,0).r;" + "o=vec4(r*0.5+u_add,0.0,clamp(u_time/4.0,0.0,1.0),1.0); }\n",history_file); + fclose(history_file); + } + history_file=fopen("pfx_history.pfx","w"); + if(history_file) { + fputs("pfx 1\npass history\n shader pfx_history.glsl\n input u_prev history\n uniform u_add 0.25\n",history_file); + fclose(history_file); + } + psx_host_set_post_uniform("u_add",0.25f,0,0,0); + s_pfx_test_clock=100*SDL_GetPerformanceFrequency(); + gl_renderer_set_post_shader("pfx_history.pfx"); + uint8_t *hist=frame(SC_PARTIAL,0,&h); + size_t center=((size_t)(s_cap_h/2)*s_cap_w+s_cap_w/2)*3; + check(abs(hist[center]-64)<=1 && hist[center+2]<=1,"first history frame is cleared and starts at time zero"); + free(hist); + s_pfx_test_clock=102*SDL_GetPerformanceFrequency(); + hist=frame(SC_PARTIAL,0,&h); + check(abs(hist[center]-96)<=1 && abs(hist[center+2]-128)<=1,"final pass samples the previous real image and its own elapsed time"); + free(hist); + if(!s_rth_on) { + for(int repeat=0;repeat<2;++repeat) { + postfx_present(0,0,0,0,s_cap_w,s_cap_h); + bloom_capture_hook(1); + check(abs(s_cap[center]-112)<=1,"generated presents do not advance history"); + } + } + psx_host_set_post_uniform("u_add",0.125f,0,0,0); + hist=frame(SC_PARTIAL,0,&h); + check(abs(hist[center]-80)<=1,"live uniform update reaches the actual shader"); + free(hist); + s_pfx_test_clock=104*SDL_GetPerformanceFrequency(); + gl_renderer_set_post_shader("pfx_history.pfx"); + hist=frame(SC_PARTIAL,0,&h); + check(abs(hist[center]-32)<=1 && hist[center+2]<=1,"reload clears history and resets shader time while retaining live values"); + free(hist); + s_pfx_test_clock=0; + + FILE *depth_file=fopen("pfx_depth.glsl","w"); + if(depth_file) { + fputs("void main(){ float d=texelFetch(u_depth,ivec2(gl_FragCoord.xy)-u_org,0).r;" + "o=vec4(clamp(d/4000.0,0.0,1.0),0.0,0.0,1.0); }\n",depth_file); + fclose(depth_file); + } + depth_file=fopen("pfx_depth.pfx","w"); + if(depth_file) { + fputs("pfx 1\npass depth\n shader pfx_depth.glsl\n input u_depth depth\n",depth_file); + fclose(depth_file); + } + gl_renderer_set_pgxp_depth(1); + gl_renderer_set_post_shader("pfx_depth.pfx"); + uint8_t *depth=frame(SC_DEPTH_ONLY,0,&h); + size_t outside=((size_t)(s_cap_h-1-20)*s_cap_w+20)*3; + check(abs(depth[center]-128)<=3,"depth input reconstructs the triangle's GTE screen depth"); + check(depth[outside]<=1,"depth input is zero where no 3D primitive drew"); + free(depth); + gl_renderer_set_pgxp_depth(0); + gl_renderer_set_post_shader("pfx_screen.pfx"); check(gl_renderer_post_shader_loaded(), "source_down + blend screen chain loads"); uint8_t *sc = frame(SC_PARTIAL, 0.0f, &h); diff --git a/runtime/tests/test_gl_scale_invariance.c b/runtime/tests/test_gl_scale_invariance.c index 51e6a867..4c53b8e9 100644 --- a/runtime/tests/test_gl_scale_invariance.c +++ b/runtime/tests/test_gl_scale_invariance.c @@ -103,6 +103,15 @@ int psx_netplay_active(void){return 0;} GrBackend gr_backend(void){return GR_BACKEND_OPENGL;} void gr_refresh_backend(void){} #endif +/* These renderer-only fixtures have no frontend telemetry/menu consumer. */ +void psx_video_telemetry_compose(int r,int s,int sw,int sh,int dw,int dh,int vw,int vh) { + (void)r;(void)s;(void)sw;(void)sh;(void)dw;(void)dh;(void)vw;(void)vh; +} +void psx_video_telemetry_present(int result) {(void)result;} +int psx_host_menu_overlay_image(const uint32_t **p,int *w,int *h) { + *p=NULL;*w=*h=0;return 0; +} + int gpu_display_is_depth24(void){return 0;} void gpu_get_display_info(GpuDisplayInfo *out){memset(out,0,sizeof(*out));out->width=320;out->height=240;} int psx_ws_prim_in_backdrop(void){return 0;}