From 8eccea16123b62834a195ea4dff41496aefa82af Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:07:39 -0700 Subject: [PATCH] post effects: no per-frame GL queries; CRLF presets tested - Uniform locations are looked up once per program (each lookup can be a driver round trip on a threaded GL; the executor runs per present, generated frames included). - State save queries only capability flags and the viewport. The mask pass no longer clears (it draws 0 then 1), so the clear colour is never touched; the present's window target and texture unit 0 are left bound. The GL fixture checks exactly that contract. - postfx_chain_test: a preset with CRLF line ends parses the same, and the import check writes its shader in binary mode (it failed on Windows). - docs/POST_EFFECTS.md: `generated reuse`. Co-Authored-By: Claude Opus 5.5 --- docs/POST_EFFECTS.md | 7 +++- runtime/src/gl_postfx.inc | 67 +++++++++++++++++++++---------- runtime/tests/test_gl_bloom.c | 14 +++++-- runtime/tests/test_postfx_chain.c | 6 ++- 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/docs/POST_EFFECTS.md b/docs/POST_EFFECTS.md index 4ab5ec7c..33feb68f 100644 --- a/docs/POST_EFFECTS.md +++ b/docs/POST_EFFECTS.md @@ -15,7 +15,10 @@ a fresh copy of the image: They run on whichever thread presents (the render thread included), before hold-last capture, screenshots and the OSD. Smooth motion's generated frames -run bloom and the user chain; AA stays on real frames, as before. The executor +run bloom and the user chain; AA stays on real frames, as before. Passes +marked `generated reuse` (bloom's bright and blur passes) are not run again +on generated frames: those reuse the last real frame's result and run only +the rest (bloom's masked composite, over their own picture and mask). The executor owns targets, programs, uniforms and GL state, and puts back everything it touches. @@ -30,6 +33,8 @@ pass # the last pass writes the window format rgba8|rgba16f # default rgba8 filter linear|nearest # how later passes sample this target uniform [f [f [f]]] # float .. vec4 + generated reuse|run # Smooth motion generated frames: keep this pass's + # target from the last real frame (default: run) ``` A shader file is a GLSL 330 fragment body. It is appended to this header: diff --git a/runtime/src/gl_postfx.inc b/runtime/src/gl_postfx.inc index 81359739..e2dcb995 100644 --- a/runtime/src/gl_postfx.inc +++ b/runtime/src/gl_postfx.inc @@ -29,10 +29,19 @@ * source); runtime/tests/test_postfx_chain.c builds it alone. */ #include "postfx_chain.c" +/* Uniform locations, looked up once per program (a lookup per frame is a + * driver round trip on a threaded GL). */ +enum { PFX_LOC_RCP, PFX_LOC_SIZE, PFX_LOC_ORG, PFX_LOC_UV, PFX_LOC_FIXED }; +typedef struct { + GLint fixed[PFX_LOC_FIXED]; + GLint in[PFX_MAX_INPUTS]; + GLint u[PFX_MAX_UNIFORMS]; +} PfxLocs; typedef struct { PfxChain chain; int loaded, failed; GLuint prog[PFX_MAX_PASSES]; + PfxLocs loc[PFX_MAX_PASSES]; GLuint tex[PFX_MAX_PASSES], fbo[PFX_MAX_PASSES]; int tw[PFX_MAX_PASSES], th[PFX_MAX_PASSES]; } PfxStage; @@ -54,6 +63,7 @@ static GLuint s_mask_src_fbo = 0; static int s_mask_src[4]; /* x0,y0,x1,y1 in source FBO px (y0 = top) */ static int s_mask_src_valid = 0, s_mask_ok = 0; static GLuint s_mask_tex = 0, s_mask_rb = 0, s_mask_fbo = 0, s_mask_prog = 0; +static GLint s_mask_uUv = -1, s_mask_uV = -1; static int s_mask_w = 0, s_mask_h = 0; static int pfx_stage_on(int i) { @@ -171,8 +181,10 @@ static void pfx_fbo_attach(GLuint *f, GLuint tex) { static int pfx_mask_build(int fresh, int lw, int lh) { if (!s_mask_prog) { s_mask_prog = build_program(PRESENT_VS, - "#version 330\nin vec2 v_uv; out vec4 o;\nvoid main(){ o = vec4(1.0); }\n"); + "#version 330\nin vec2 v_uv; out vec4 o; uniform float u_v;\nvoid main(){ o = vec4(u_v); }\n"); if (!s_mask_prog) return 0; + s_mask_uUv = p_glGetUniformLocation(s_mask_prog, "u_uv_rect"); + s_mask_uV = p_glGetUniformLocation(s_mask_prog, "u_v"); } if (s_mask_w != lw || s_mask_h != lh) { pfx_tex_alloc(&s_mask_tex, lw, lh, -1, 1); @@ -192,11 +204,16 @@ static int pfx_mask_build(int fresh, int lw, int lh) { 0, lh, lw, 0, GL_STENCIL_BUFFER_BIT, GL_NEAREST); p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_mask_fbo); glViewport(0, 0, lw, lh); - glClear(GL_COLOR_BUFFER_BIT); + /* 0 everywhere, then 1 where the marks are (no clear: the clear + * colour is caller state this pass would have to query back). */ + p_glUseProgram(s_mask_prog); + p_glUniform1f(s_mask_uV, 0.0f); + post_fullscreen(s_mask_prog, s_mask_uUv); glEnable(GL_STENCIL_TEST); glStencilFunc(GL_EQUAL, 0x02, 0x02); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); - post_fullscreen(s_mask_prog, p_glGetUniformLocation(s_mask_prog, "u_uv_rect")); + p_glUniform1f(s_mask_uV, 1.0f); + post_fullscreen(s_mask_prog, s_mask_uUv); glDisable(GL_STENCIL_TEST); s_mask_ok = 1; } @@ -216,6 +233,16 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in const int last = i == c->npasses - 1; if (!st->prog[i]) { st->prog[i] = pfx_build_pass(ps); + if (st->prog[i]) { + PfxLocs *L = &st->loc[i]; + const GLuint pg = st->prog[i]; + L->fixed[PFX_LOC_RCP] = p_glGetUniformLocation(pg, "u_rcp"); + 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"); + 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); @@ -244,6 +271,7 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in glViewport(lx, ly, lw, lh); } const GLuint prog = st->prog[i]; + const PfxLocs *L = &st->loc[i]; p_glUseProgram(prog); for (int k = 0; k < ps->ninputs; k++) { const int from = ps->in[k].from; @@ -251,15 +279,15 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in : from == PFX_IN_MASK ? s_mask_tex : st->tex[from]; p_glActiveTexture(PSXGL_TEXTURE0 + (GLenum)k); glBindTexture(GL_TEXTURE_2D, t); - p_glUniform1i(p_glGetUniformLocation(prog, ps->in[k].sampler), k); + p_glUniform1i(L->in[k], k); } p_glActiveTexture(PSXGL_TEXTURE0); - p_glUniform2f(p_glGetUniformLocation(prog, "u_rcp"), 1.0f / (float)w, 1.0f / (float)h); - p_glUniform2f(p_glGetUniformLocation(prog, "u_size"), (float)w, (float)h); - p_glUniform2i(p_glGetUniformLocation(prog, "u_org"), last ? lx : 0, last ? ly : 0); + 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); for (int k = 0; k < ps->nuniforms; k++) { const PfxUniform *u = &ps->u[k]; - const GLint loc = p_glGetUniformLocation(prog, u->name); + const GLint loc = L->u[k]; if (loc < 0) continue; switch (u->count) { case 1: p_glUniform1f(loc, u->v[0]); break; @@ -268,7 +296,7 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in default: p_glUniform4f(loc, u->v[0], u->v[1], u->v[2], u->v[3]); break; } } - post_fullscreen(prog, p_glGetUniformLocation(prog, "u_uv_rect")); + post_fullscreen(prog, L->fixed[PFX_LOC_UV]); } for (int k = PFX_MAX_INPUTS - 1; k >= 0; k--) { p_glActiveTexture(PSXGL_TEXTURE0 + (GLenum)k); @@ -280,31 +308,29 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in } /* The GL state the executor changes, put back afterwards so whatever runs - * next (the OSD, the next frame's draws) sees what it left. */ + * next (the OSD, the next frame's draws) sees what it left. It queries + * little (capability flags and the viewport; every query can be a driver + * round trip on a threaded GL, per present, generated frames included) and + * never touches the clear colour. It runs inside a present, which draws to + * the window, so both framebuffer bindings go back to the window (0) and + * texture unit 0 is left active. */ typedef struct { GLboolean blend, depth, scissor, stencil; - GLfloat clear[4]; - GLint viewport[4], fbo, rfbo, active; + GLint viewport[4]; } PfxGlState; static void pfx_state_save(PfxGlState *st) { st->blend = glIsEnabled(GL_BLEND); st->depth = glIsEnabled(GL_DEPTH_TEST); st->scissor = glIsEnabled(GL_SCISSOR_TEST); st->stencil = glIsEnabled(GL_STENCIL_TEST); - glGetFloatv(GL_COLOR_CLEAR_VALUE, st->clear); glGetIntegerv(GL_VIEWPORT, st->viewport); - glGetIntegerv(0x8CA6 /*DRAW_FRAMEBUFFER_BINDING*/, &st->fbo); - glGetIntegerv(0x8CAA /*READ_FRAMEBUFFER_BINDING*/, &st->rfbo); - glGetIntegerv(0x84E0 /*ACTIVE_TEXTURE*/, &st->active); } static void pfx_state_restore(const PfxGlState *st) { if (st->blend) glEnable(GL_BLEND); else glDisable(GL_BLEND); if (st->depth) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); if (st->scissor) glEnable(GL_SCISSOR_TEST); else glDisable(GL_SCISSOR_TEST); if (st->stencil) glEnable(GL_STENCIL_TEST); else glDisable(GL_STENCIL_TEST); - glClearColor(st->clear[0], st->clear[1], st->clear[2], st->clear[3]); glViewport(st->viewport[0], st->viewport[1], st->viewport[2], st->viewport[3]); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, (GLuint)st->fbo); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, (GLuint)st->rfbo); - p_glActiveTexture((GLenum)st->active); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + p_glActiveTexture(PSXGL_TEXTURE0); } /* fresh: the noted source is this present's (0: reuse the last mask). @@ -316,7 +342,6 @@ 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); - glClearColor(0.f, 0.f, 0.f, 0.f); int mask_done = 0, mask_ok = 0; for (int i = 0; i < PFX_NSTAGES; i++) { if (!pfx_stage_on(i)) continue; diff --git a/runtime/tests/test_gl_bloom.c b/runtime/tests/test_gl_bloom.c index ea0226b4..a9a62d55 100644 --- a/runtime/tests/test_gl_bloom.c +++ b/runtime/tests/test_gl_bloom.c @@ -267,7 +267,8 @@ int main(int argc, char **argv) { } } gl_renderer_render_thread_stop(); - /* The executor leaves the GL state as it found it, both ways round, with + /* The executor leaves the GL state as it found it (capabilities both ways + * round, the viewport, the clear colour; the window target bound), with * every stage on (bloom, an imported chain reading the mask, FXAA). */ { uint64_t h = 0; @@ -281,13 +282,20 @@ int main(int argc, char **argv) { else { glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_SCISSOR_TEST); glDisable(GL_STENCIL_TEST); } glClearColor(0.25f, 0.5f, 0.75f, on ? 1.0f : 0.0f); glViewport(3, 5, 101 + on, 77); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, on ? s_hr_fbo : 0); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); /* a present draws to the window */ + p_glActiveTexture(PSXGL_TEXTURE0); pfx_state_save(&want); + GLfloat cc0[4], cc1[4]; glGetFloatv(GL_COLOR_CLEAR_VALUE, cc0); const uint64_t n0 = s_bloom_passes, a0 = s_paa_passes; postfx_present(0, 1, 0, 0, ww, wh); pfx_state_save(&got); + glGetFloatv(GL_COLOR_CLEAR_VALUE, cc1); + GLint dfb = -1, rfb = -1, act = -1; + glGetIntegerv(0x8CA6, &dfb); glGetIntegerv(0x8CAA, &rfb); glGetIntegerv(0x84E0, &act); check(s_bloom_passes == n0 + 1 && s_paa_passes == a0 + 1, "state test ran bloom and AA"); - check(memcmp(&want, &got, sizeof want) == 0, "the pipeline leaves GL state unchanged"); + check(memcmp(&want, &got, sizeof want) == 0, "the pipeline restores caps and viewport"); + check(!memcmp(cc0, cc1, sizeof cc0), "the pipeline never touches the clear colour"); + check(dfb == 0 && rfb == 0 && act == (GLint)PSXGL_TEXTURE0, "the pipeline leaves the window target bound"); } p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_SCISSOR_TEST); glDisable(GL_STENCIL_TEST); diff --git a/runtime/tests/test_postfx_chain.c b/runtime/tests/test_postfx_chain.c index e0db8ed9..5cbc5c9f 100644 --- a/runtime/tests/test_postfx_chain.c +++ b/runtime/tests/test_postfx_chain.c @@ -65,12 +65,16 @@ int main(void) { } /* File import: shaders resolve next to the preset. */ - FILE *f = fopen("pfx_chain_test.glsl", "w"); fputs("void main(){ o = vec4(1.0); }\n", f); fclose(f); + FILE *f = fopen("pfx_chain_test.glsl", "wb"); fputs("void main(){ o = vec4(1.0); }\n", f); fclose(f); f = fopen("pfx_chain_test.pfx", "w"); fputs("pfx 1\npass p\n shader pfx_chain_test.glsl\n input t source\n", f); fclose(f); check(pfx_chain_load_file("pfx_chain_test.pfx", &c, err, sizeof err) && !strcmp(c.pass[0].shader, "void main(){ o = vec4(1.0); }\n"), "file preset and shader import"); pfx_chain_free(&c); check(!pfx_chain_load_file("pfx_missing.pfx", &c, err, sizeof err) && strstr(err, "cannot open"), "missing preset"); + /* A preset written with CRLF line ends (Windows editors) parses the same. */ + check(parse("pfx 1\r\npass a\r\n shader builtin:blur\r\n input t source\r\n scale 0.5\r\n", &c, err) && + c.pass[0].in[0].from == PFX_IN_SOURCE, "CRLF preset"); + pfx_chain_free(&c); remove("pfx_chain_test.glsl"); remove("pfx_chain_test.pfx"); printf("checks=%d failures=%d\n", checks, failures);