Review integration: final-pass history, chain clock and GPU regressions

This commit is contained in:
Matthew Stanley
2026-10-24 10:25:47 -07:00
4 changed files with 115 additions and 16 deletions
+6 -2
View File
@@ -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 <sampler> 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.
+23 -12
View File
@@ -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--) {
+77 -2
View File
@@ -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 <stdint.h>
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);
+9
View File
@@ -105,6 +105,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;}