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 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-13 02:07:39 -07:00
parent 18e10693c4
commit 8eccea1612
4 changed files with 68 additions and 26 deletions
+6 -1
View File
@@ -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 <name> # the last pass writes the window
format rgba8|rgba16f # default rgba8
filter linear|nearest # how later passes sample this target
uniform <name> <f> [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:
+46 -21
View File
@@ -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;
+11 -3
View File
@@ -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);
+5 -1
View File
@@ -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);