diff --git a/docs/POST_EFFECTS.md b/docs/POST_EFFECTS.md index 22421d69..7cc42d94 100644 --- a/docs/POST_EFFECTS.md +++ b/docs/POST_EFFECTS.md @@ -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 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. diff --git a/runtime/src/gl_postfx.inc b/runtime/src/gl_postfx.inc index b6e07e02..1e652462 100644 --- a/runtime/src/gl_postfx.inc +++ b/runtime/src/gl_postfx.inc @@ -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--) { diff --git a/runtime/tests/test_gl_bloom.c b/runtime/tests/test_gl_bloom.c index 1ca26cc4..9ae618fb 100644 --- a/runtime/tests/test_gl_bloom.c +++ b/runtime/tests/test_gl_bloom.c @@ -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 +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); diff --git a/runtime/tests/test_gl_scale_invariance.c b/runtime/tests/test_gl_scale_invariance.c index 46f8bee9..fad32806 100644 --- a/runtime/tests/test_gl_scale_invariance.c +++ b/runtime/tests/test_gl_scale_invariance.c @@ -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;}