From 00de61a2e700738d96506fb9f18140f9ada8ced3 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] 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 4a27bdb2..c32d6e5d 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -196,6 +196,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 06c2e504..239fa9b9 100644 --- a/runtime/include/host_overlay.h +++ b/runtime/include/host_overlay.h @@ -46,6 +46,13 @@ void psx_host_overlay_set_draw_cb(PsxHostOverlayDrawFn fn, void *ctx); int psx_host_overlay_has_draw(void); void psx_host_overlay_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 31f1b0bd..c7d88fa3 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8757,6 +8757,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); @@ -16212,6 +16223,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 59b7adc4..422c85c1 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -19,6 +19,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; } @@ -1190,6 +1192,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;