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 94e0e245..ffb1b780 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 8d5f054a..6607758f 100644 --- a/runtime/include/host_overlay.h +++ b/runtime/include/host_overlay.h @@ -54,6 +54,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 e2630733..552ad40a 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8777,6 +8777,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); @@ -16252,6 +16263,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 b8b19f9e..6c94540f 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;