/* Frame generation on a real, hidden OpenGL context * (docs/FRAME_GENERATION.md). Run by run_gl_frame_gen.py with the render * thread on and frame generation off (argv[2] == 0) or on (1, forced: every * flip generates, whatever the surplus). A 30 Hz "game" double-buffers at * x = 0 / x = 512 and presents every VBlank (each game frame twice), paced at * 60 Hz: a fill, textured / shaded / flat triangles that move a little each * game frame, a fixed HUD rect, render-to-texture and a VRAM copy outside the * display, and every fifth frame a triangle that jumps (never matched). The * triangles carry projection sources (fixture_sources) as gpu.c records them * for GTE-projected geometry, so in-between frames follow the camera. * argv[3] == "wide" presents through a native-wide surface instead of VRAM; * argv[5] == "late" flips to each frame one VBlank after the next one has * started drawing (as R4 does). * Printed: real (digest of every distinct real presented image, in order; a * present that shows the same image again is not counted: generation replaces * those with in-between frames), presents, generated, and * with generation on the endpoint checks: the in-between image composed at * phase 1 equals the newer real frame's image, at phase 0 the older one's * (the last two game frames move without a jump, so every triangle pairs). * FG_METHOD=reprojection (env) generates by reprojection * (docs/FRAME_GENERATION.md) and checks the warp's pixels: phase 0.5 against * the redraw of the same pair (known camera pan), every pixel one of the * real frames' or the redraw's (disocclusion filled), the HUD exactly the * newer frame's; FG_PGXP=1 adds PGXP depth and checks the warp leaves no * depth behind for the cars drawn after it. In redraw (the default) the * fixture checks reprojection ran and allocated nothing, and prints the * phase 0.5 image's digest (p05=) to compare with master. * Original source-owned scene; no retail payload. */ static void fixture_present(int generated); #define GL_PRESENT_TEST_HOOK(gen) fixture_present(gen) /* argv[6] == "pt": the present thread is on; every image it puts in the * window (read back after its copy, before its swap) must be the image the * composing side presented, in the same order. */ static void fixture_window(int slot); #define GL_PRESENT_THREAD_TEST_HOOK(slot) fixture_window(slot) #include "gpu_gl_renderer.c" #include "gpu_hd_texture_stubs.inc" #include "mod_texture_banks.c" #include "gl_batch_policy_lle.c" #include "gl_batch_policy_hle.c" #include "gl_batch_policy_select.c" /* gpu_render.c (the facade) observes draws for HD texture packs: no pack here. */ int gpu_hd_textures_active(void){return 0;} int gpu_hd_textures_dump_enabled(void){return 0;} void gpu_hd_textures_observe_draw(uint16_t tp,uint16_t cx,uint16_t cy,const int l[4], uint32_t w,int semi){(void)tp;(void)cx;(void)cy;(void)l;(void)w;(void)semi;} void gpu_hd_textures_set_vram(const uint16_t* v){(void)v;} void gpu_hd_textures_track_upload(int x,int y,int w,int h,const uint16_t* words){ (void)x;(void)y;(void)w;(void)h;(void)words;} void gpu_hd_textures_invalidate(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;} 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_begin_copy(int sx,int sy,int dx,int dy,int w,int h){ (void)sx;(void)sy;(void)dx;(void)dy;(void)w;(void)h;} void gpu_hd_textures_end_copy(void){} uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;} /* mod_texture_banks.c refuses inside a netplay host-output hook (mod_runtime.cpp); none here. */ int psx_mod_host_output_scope(void){return 0;} uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;} int g_psx_vram_dirty_tracking=0; uint64_t s_frame_count=0; void gpu_vram_dirty_mark_row_impl(uint32_t y){(void)y;} void gpu_vram_dirty_mark_rect(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;} void gpu_vram_dirty_mark_all(void){} int psx_netplay_active(void){return 0;} const GpuRenderBackend *vk_backend_get(void){return NULL;} 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;} /* Guest state the backend reads per primitive. It changes with every * primitive here, so a replay that read it live (instead of the value * captured when the primitive was recorded) would draw a different image. */ static int g_prim_id; static int g_bg_full; int psx_ws_prim_in_backdrop(void){return (g_prim_id % 3) == 0;} int psx_ws_prim_is_tagged(void){return (g_prim_id % 5) == 0;} int gpu_ws_nw_flat_backdrop_enabled(void){return 0;} int gpu_ws_background_requires_full_composite(void){return g_bg_full;} int g_ws_tex_edge_pct=0; void psx_ws_dbg_gate_frame_snapshot(void){} void gpu_depth24_upload_span_reset(void){} int gpu_ws_netplay_local_viewport_width(void){return 0;} int render_pass_netplay_enabled(void){return 0;} uint64_t psx_cycle_count=0; uint32_t g_psx_vblank_cycles=564480u; /* Frontend hooks of the present path (host overlays, pacing, rings). */ int host_osd_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;} int host_osd_volume_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;} int host_osd_needs_present(void){return 0;} void host_osd_present_done(void){} int psx_rewind_overlay_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;} float psx_rewind_slide(void){return 0.0f;} int psx_savestate_menu_overlay_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;} int present_shot_take(char *out,int n){(void)out;(void)n;return 0;} void present_shot_done(int ok){(void)ok;} void psx_host_sleep_ms(uint32_t ms){(void)ms;} void psx_host_sleep_micros(unsigned us){(void)us;} /* Called on every present. On the render thread it also stands in for a slow * swap, so replay runs well behind recording and anything it read live from * the "guest" (instead of what was recorded) would be a later value. */ int psx_present_vsync_owns_cadence(void){return 0;} void latency_ring_mark(LatencyStage s){(void)s;} void gpu_timeline_note(uint8_t k,uint32_t a,uint32_t b){(void)k;(void)a;(void)b;} static uint16_t vram[1024*512]; static int checks, failures; static void check(int ok, const char *label) { checks++; if (!ok) { fprintf(stderr, "FAIL %s\n", label); failures++; } } static uint64_t fnv(const void *pp, size_t n, uint64_t h) { const uint8_t *p = (const uint8_t *)pp; for (size_t i = 0; i < n; i++) { h ^= p[i]; h *= 0x100000001b3ull; } return h; } /* Presented images: every swap (render thread or not) passes here first. */ static uint64_t real_seq = 0xcbf29ce484222325ull; static uint64_t n_real = 0, n_gen = 0; static uint32_t *img_last[2]; /* the last two real images */ static int img_w, img_h; static uint64_t read_back(uint32_t *dst) { int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); img_w = ww; img_h = wh; p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); glPixelStorei(GL_PACK_ALIGNMENT, 4); glReadPixels(0, 0, ww, wh, GL_RGBA, GL_UNSIGNED_BYTE, dst); return fnv(dst, (size_t)ww * wh * 4, 0xcbf29ce484222325ull); } #define MAX_SHOWN 4096 static int pt_mode; static uint64_t composed_d[MAX_SHOWN], window_d[MAX_SHOWN]; static int n_composed, n_window; static void fixture_window(int slot) { (void)slot; int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); uint32_t *px = (uint32_t *)malloc((size_t)ww * wh * 4); p_glBindFramebuffer_raw(PSXGL_READ_FRAMEBUFFER, 0); glReadBuffer(GL_BACK); glPixelStorei(GL_PACK_ALIGNMENT, 4); glReadPixels(0, 0, ww, wh, GL_RGBA, GL_UNSIGNED_BYTE, px); if (n_window < MAX_SHOWN) window_d[n_window] = fnv(px, (size_t)ww * wh * 4, 0xcbf29ce484222325ull); n_window++; free(px); } static void fixture_present(int generated) { if (pt_mode) { int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); uint32_t *px = (uint32_t *)malloc((size_t)ww * wh * 4); p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); glPixelStorei(GL_PACK_ALIGNMENT, 4); glReadPixels(0, 0, ww, wh, GL_RGBA, GL_UNSIGNED_BYTE, px); if (n_composed < MAX_SHOWN) composed_d[n_composed] = fnv(px, (size_t)ww * wh * 4, 0xcbf29ce484222325ull); n_composed++; free(px); } if (generated) { n_gen++; return; } int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); for (int i = 0; i < 2; i++) if (!img_last[i]) img_last[i] = (uint32_t *)malloc((size_t)ww * wh * 4); uint32_t *t = img_last[0]; img_last[0] = img_last[1]; img_last[1] = t; static uint64_t last_d = 0; uint64_t d = read_back(img_last[1]); if (d == last_d) { /* the same image shown again: not a new real frame */ uint32_t *u = img_last[0]; img_last[0] = img_last[1]; img_last[1] = u; return; } last_d = d; if (getenv("FG_DUMP")) { printf("R %llu %016llx\n", (unsigned long long)n_real, (unsigned long long)d); char fn[64]; snprintf(fn, sizeof fn, "real%02llu.rgba", (unsigned long long)n_real); FILE *f = fopen(fn, "wb"); fwrite(img_last[1], 4, (size_t)ww * wh, f); fclose(f); } real_seq = fnv(&d, sizeof d, real_seq); n_real++; } #define FRAME_W 320 #define FRAME_H 240 static void textures(void) { static uint16_t page[64*64], clut[16]; for (int i = 0; i < 64*64; i++) page[i] = (uint16_t)((i * 0x1357u) ^ (i >> 3)); for (int i = 0; i < 16; i++) clut[i] = (uint16_t)(i ? (0x0421u * (uint16_t)i) | ((i & 1) << 15) : 0); /* Outside both buffers (x 0..319, 512..831; y 0..239). */ for (int i = 0; i < 64*64; i++) vram[(i / 64) * 1024 + 896 + i % 64] = page[i]; gr_vram_transfer_in(896, 0, 64, 64, page); gr_vram_transfer_in(512, 300, 16, 1, clut); } static int wide_mode; /* FG_PGXP= (run_gl_frame_gen.py's PGXP pass): every triangle is a * PGXP 3D triangle (sub-pixel positions and per-vertex GTE SZ, as gpu.c * sends them while a PGXP renderer feature is on), packed so neighbours * overlap with later triangles farther away: the depth buffer, not painter * order, decides the overlaps, and SZ differs per vertex. "depth" turns the * depth buffer on, "color" perspective-correct Gouraud colour, "seam" seam * expansion (wide: half a native px); "none" draws the same scene without * them. The generated frame at phase 1 must still * equal the real one. */ static int g_pgxp, g_pgxp_depth, g_pgxp_color, g_pgxp_seam; static int g_pan = 1; /* FG_PAN: camera pan per game frame, px */ /* What gpu.c records for a GTE-projected triangle (gl_renderer_fg_source): * vertex identities and camera-space positions that project (H = z = 1000) * to the triangle's screen positions relative to its buffer. */ /* Structurally different: a channel off by more than 48 (sub-pixel * placement at a high scale resamples textures by a few LSB). */ static int px_far(uint32_t a, uint32_t b) { for (int c = 0; c < 24; c += 8) { int d = (int)((a >> c) & 255) - (int)((b >> c) & 255); if (d > 48 || d < -48) return 1; } return 0; } static void fixture_sources(int i, int bx, int x, int y) { static const int off[4][6] = { { 0, 0, 30, 2, 3, 32 }, { 0, 0, 28, 0, 0, 28 }, { 0, 0, 26, 4, 6, 30 }, { 0, 0, 24, 20, -4, 26 } }; const int *o = off[i % 4]; uint32_t id[3]; int32_t pc[9], h[3], xs[3], ys[3]; for (int k = 0; k < 3; k++) { id[k] = (uint32_t)(i * 3 + k + 1); xs[k] = x + o[2 * k]; ys[k] = y + o[2 * k + 1]; pc[3 * k] = xs[k] - bx; pc[3 * k + 1] = ys[k]; pc[3 * k + 2] = 1000; h[k] = 1000; } gl_renderer_fg_source(id, pc, h, xs, ys); if (g_pgxp) { gr_set_precise_triangle(1, xs[0] * 65536, ys[0] * 65536, xs[1] * 65536, ys[1] * 65536, xs[2] * 65536, ys[2] * 65536); /* Along a row each triangle is farther than all of its left neighbour * (steps well beyond the 2% tolerance, below the 4096 clear threshold). */ const float z = 2000.0f + 2500.0f * (float)(i % 8) + 100.0f * (float)(i / 8); gr_set_depth_triangle(1, z, z + 600.0f * (float)(1 + i % 3), z + 300.0f); } } /* FG_PAN > 1: a projected backdrop twice as far as the triangles (gouraud * tiles, 40 px at H = 1000, z = 2000) behind them, so the whole picture is * world the warp moves, with parallax: the near triangles move twice as far * as the backdrop and uncover it at their trailing edges. */ static void backdrop(int g, int bx) { const int cx = (g % 9) * g_pan; /* the camera, as the triangles see it */ for (int ty = 0; ty < 7; ty++) for (int tx = 0; tx < 10; tx++) { int gx[4], gy[4]; uint32_t gid[4]; uint32_t col[4]; for (int c = 0; c < 4; c++) { const int X = (tx + (c & 1)) * 80 - 80, Y = (ty + (c >> 1)) * 80 - 40; gx[c] = bx + (X + cx) / 2; /* the triangles move right with it */ gy[c] = Y / 2; gid[c] = 1000u + (uint32_t)((ty + (c >> 1)) * 16 + tx + (c & 1)); const uint32_t h = gid[c] * 2654435761u; col[c] = ((h >> 3) & 0x1f) | (((h >> 11) & 0x1f) << 5) | (((h >> 19) & 0x1f) << 10); } static const int tri[2][3] = { { 0, 1, 2 }, { 1, 3, 2 } }; for (int t = 0; t < 2; t++) { uint32_t id[3]; int32_t pc[9], h[3], xs[3], ys[3]; for (int k = 0; k < 3; k++) { const int c = tri[t][k]; id[k] = gid[c]; xs[k] = gx[c]; ys[k] = gy[c]; pc[3 * k] = (xs[k] - bx) * 2; pc[3 * k + 1] = ys[k] * 2; pc[3 * k + 2] = 2000; h[k] = 1000; } gl_renderer_fg_source(id, pc, h, xs, ys); const int a = tri[t][0], b = tri[t][1], c = tri[t][2]; gr_draw_gouraud_triangle(gx[a], gy[a], (uint16_t)col[a], gx[b], gy[b], (uint16_t)col[b], gx[c], gy[c], (uint16_t)col[c]); } } } /* One game frame drawn into buffer bx. Positions move by whole pixels with * g; every fifth frame triangle 0 jumps across the screen. */ static void game_frame_part(int g, int bx, int part) { g_prim_id = -1; gr_set_draw_area(bx, 0, bx + FRAME_W - 1, FRAME_H - 1); gr_set_draw_offset(bx, 0); if (wide_mode) gr_wide_set_target(bx); if (part == 0) { gr_fill_rect(bx, 0, FRAME_W, FRAME_H, 0x1084); if (wide_mode) gr_wide_clear(bx, 0, FRAME_H, 0x1084); if (g_pan > 1) backdrop(g, bx); } gr_set_color_modulation(128, 128, 128, 0); gr_set_semi_transparency(0, 0); for (int i = part ? 20 : 0; i < (part ? 40 : 20); i++) { /* The camera pans one pixel per game frame (every triangle moves * alike); triangle 5 is an object moving on its own. */ int x = bx + 10 + (i % 8) * (g_pgxp ? 20 : 36) + (i == 5 ? (g * 3) % 9 : (g % 9) * g_pan) - 4; int y = 12 + (i / 8) * 42 + (i == 5 ? g % 5 : 0); if (i == 0 && g % 5 == 0) { x = bx + 250 - (g * 37) % 200; y = 180; } fixture_sources(i, bx, x, y); switch (i % 4) { case 0: gr_draw_shaded_textured_triangle(x, y, 0, 0, 0x808080, x + 30, y + 2, 63, 4, 0x6090b0, x + 3, y + 32, 8, 63, 0xb09060, 512, 300, 0x000e, 0); break; case 1: gr_draw_textured_triangle(x, y, 4, 4, x + 28, y, 60, 4, x, y + 28, 4, 60, 512, 300, 0x000e); break; case 2: gr_draw_gouraud_triangle(x, y, 0x7c00, x + 26, y + 4, 0x03e0, x + 6, y + 30, 0x001f); break; default: gr_draw_flat_triangle(x, y, x + 24, y + 20, x - 4, y + 26, (uint16_t)(0x2108 * (i & 3) + 0x0421)); break; } } if (part == 0) return; gr_draw_flat_rect(bx + 8, 220, 80, 12, 0x7fff); /* HUD */ gr_draw_flat_rect(900, 300, 32, 32, (uint16_t)(0x03e0 + g)); /* render to texture */ gr_copy_rect(bx + 10, 10, 900, 400, 16, 16); /* copy out of the display */ } static void present(int bx) { if (wide_mode) (void)gl_renderer_present_wide_fbo(bx, 0, FRAME_H, 0); else gl_renderer_present_vram(bx, 0, FRAME_W, FRAME_H, 0, 0); gl_renderer_render_thread_frame_boundary(); } static _Atomic int stats_writer_done; static int stats_writer(void *unused) { (void)unused; for (unsigned i = 0; i < 50000; i++) { s_fg_generated++; fg_gen_cost_publish(1000); } atomic_store(&stats_writer_done, 1); return 0; } static void test_cost_publication(void) { SDL_Thread *writer = SDL_CreateThread(stats_writer, "fg-stats-test", NULL); check(writer != NULL, "generation stats writer started"); if (!writer) return; int coherent = 1, monotonic = 1; uint64_t last = 0; do { uint64_t gen, count, cost; gl_renderer_frame_gen_costs(&gen, &count, &cost); coherent &= cost == count * 1000 && gen >= count; monotonic &= count >= last; last = count; } while (!atomic_load(&stats_writer_done)); SDL_WaitThread(writer, NULL); uint64_t gen, real, count, cost; gl_renderer_frame_gen_counts(&gen, &real); gl_renderer_frame_gen_costs(NULL, &count, &cost); check(coherent && monotonic, "concurrent generation stats retain a coherent monotonic cost/count pair"); check(gen == 50000 && count == gen && cost == gen * 1000 && real == 0, "generation stats final totals exact"); /* Only fixture initialization resets totals, before the GL RT exists. */ atomic_store(&s_fg_generated, 0); atomic_store(&s_fg_gen_measured, 0); atomic_store(&s_fg_gen_cost_ns, 0); atomic_store(&s_fg_gen_cost_seq, 0); } int main(int argc, char **argv) { int scale = argc > 1 ? atoi(argv[1]) : 2; int fg = argc > 2 && argv[2][0] == '1'; wide_mode = argc > 3 && !strcmp(argv[3], "wide"); int frames = argc > 4 ? atoi(argv[4]) : 24; /* "late": the game flips to a frame one VBlank after it starts drawing * the next one (as R4 does), so a flip is not a list boundary. */ int late = argc > 5 && !strcmp(argv[5], "late"); pt_mode = argc > 6 && !strcmp(argv[6], "pt"); { const char *e = getenv("FG_PGXP"); g_pgxp = e && *e; g_pgxp_depth = g_pgxp && strstr(e, "depth") != NULL; g_pgxp_color = g_pgxp && strstr(e, "color") != NULL; g_pgxp_seam = g_pgxp && strstr(e, "seam") != NULL; } if (SDL_Init(SDL_INIT_VIDEO) != 0) { fprintf(stderr, "SKIP no video (%s)\n", SDL_GetError()); return 77; } SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); test_cost_publication(); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); #if defined(__APPLE__) SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); #endif SDL_Window *win = SDL_CreateWindow("Frame generation hidden test", 0, 0, 320, 240, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); if (!win) { fprintf(stderr, "SKIP no window (%s)\n", SDL_GetError()); return 77; } { SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); SDL_GLContext probe = SDL_GL_CreateContext(win); if (!probe) { fprintf(stderr, "SKIP no GL 3.3 core context (%s)\n", SDL_GetError()); return 77; } SDL_GL_MakeCurrent(win, NULL); SDL_GL_DeleteContext(probe); } gr_set_backend(GR_BACKEND_OPENGL); gr_init(vram); gr_set_scale(scale); gl_renderer_set_swap_interval(0); if (!gl_renderer_init_context(win)) { fprintf(stderr, "FAIL context\n"); return 2; } textures(); if (wide_mode) gr_wide_configure(426, 53); if (pt_mode) gl_renderer_set_present_thread(1, 3); if (g_pgxp) { gl_renderer_set_pgxp_depth(g_pgxp_depth); gl_renderer_set_pgxp_color_perspective(g_pgxp_color); gl_renderer_set_pgxp_seam(g_pgxp_seam ? 2 : 0); check(gl_renderer_pgxp_render_wanted() == (g_pgxp_depth || g_pgxp_color || g_pgxp_seam), "PGXP renderer features as asked"); } check(gl_renderer_render_thread_start(2) == 1, "render thread started"); if (pt_mode) check(gl_renderer_present_thread_active(), "present thread started"); const char *method = getenv("FG_METHOD"); const int rp = method && !strcmp(method, "reprojection"); const int pgxp = getenv("FG_RP_DEPTH") && getenv("FG_RP_DEPTH")[0] == '1'; /* reprojection pass: PGXP depth on */ if (getenv("FG_PAN")) g_pan = atoi(getenv("FG_PAN")) > 0 ? atoi(getenv("FG_PAN")) : 1; if (pgxp) gl_renderer_set_pgxp_depth(1); if (fg) { #ifdef _WIN32 _putenv("PSX_FRAME_GEN_FORCE=1"); #else setenv("PSX_FRAME_GEN_FORCE", "1", 1); #endif if (rp) gl_renderer_set_frame_generation_method(GL_FG_METHOD_REPROJECTION); check(gl_renderer_frame_generation_method() == (rp ? GL_FG_METHOD_REPROJECTION : GL_FG_METHOD_REDRAW), "generation method"); gl_renderer_set_frame_generation(1); check(gl_renderer_frame_generation() == 1, "frame generation on"); } gl_renderer_frame_gen_configure(120.0, 59.94); for (int g = 0; g < frames; g++) { const int bx = (g & 1) ? 512 : 0, prev = bx ^ 512; if (late) { game_frame_part(g, bx, 0); present(prev); /* flips to the previous frame */ SDL_Delay(16); game_frame_part(g, bx, 1); present(prev); SDL_Delay(16); continue; } game_frame_part(g, bx, 0); game_frame_part(g, bx, 1); present(bx); /* the game frame's first VBlank: a flip */ SDL_Delay(16); present(bx); /* its second: the same buffer again */ SDL_Delay(16); } SDL_Delay(60); gl_renderer_render_thread_sync("test"); /* the render thread parks; we hold the context */ if (fg) { char js[4096]; gl_renderer_frame_gen_json(js, sizeof js); printf("fg {%s}\n", js); /* Endpoints: compose (no swap) at phase 1 and 0 between the last two * game frames and compare with their real images. */ int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); uint32_t *img = (uint32_t *)malloc((size_t)ww * wh * 4); check(img_last[0] && img_last[1], "two real images kept"); check(fg_generate(1.0, 0) == 1, "compose at phase 1"); uint64_t d1 = read_back(img); uint64_t r1 = fnv(img_last[1], (size_t)ww * wh * 4, 0xcbf29ce484222325ull); if (getenv("FG_DUMP")) { FILE *f = fopen("p1.rgba", "wb"); fwrite(img, 4, (size_t)ww * wh, f); fclose(f); f = fopen("r1.rgba", "wb"); fwrite(img_last[1], 4, (size_t)ww * wh, f); fclose(f); f = fopen("r0.rgba", "wb"); fwrite(img_last[0], 4, (size_t)ww * wh, f); fclose(f); printf("dump %dx%d\n", ww, wh); } check(d1 == r1, "phase 1 equals the newer real frame"); check(fg_generate(0.0, 0) == 1, "compose at phase 0"); uint64_t d0 = read_back(img); uint64_t r0 = fnv(img_last[0], (size_t)ww * wh * 4, 0xcbf29ce484222325ull); /* Every projected triangle is where the older frame had it; what the * in-between camera uncovers (here: the background beside a moved * triangle, through its transparent texels) shows the newer frame * (frame_gen.h). So each phase-0 pixel is the older frame's, or the * newer frame's where they differ. */ if (getenv("FG_DUMP")) { FILE *f = fopen("p0.rgba", "wb"); fwrite(img, 4, (size_t)ww * wh, f); fclose(f); } long diff = 0, neither = 0; for (int i = 0; i < ww * wh; i++) if (img[i] != img_last[0][i]) { diff++; neither += px_far(img[i], img_last[0][i]) && px_far(img[i], img_last[1][i]); } printf("phase0_diff_px=%ld (neither frame %ld) of %d camera=%u object=%u\n", diff, neither, ww * wh, s_fg_fit.camera, s_fg_fit.object); (void)d0; (void)r0; check(s_fg_fit.ok && (g_pan > 1 ? s_fg_fit.camera + s_fg_fit.object >= 120 : s_fg_fit.camera + s_fg_fit.object == 120), "every projected vertex placed"); /* With the parallax backdrop (FG_PAN) edges resample: 1 %. */ check(neither * (g_pan > 1 ? 100 : 1000) < (long)ww * wh, "phase 0: the older real frame, uncovered areas the newer (<0.1%, parallax <1%)"); check(fg_generate(0.5, 0) == 1, "compose at phase 0.5"); uint64_t dm = read_back(img); if (getenv("FG_DUMP")) { FILE *f = fopen("pm.rgba", "wb"); fwrite(img, 4, (size_t)ww * wh, f); fclose(f); } check(dm != r0 && dm != r1, "phase 0.5 is neither endpoint"); printf("p05=%016llx\n", (unsigned long long)dm); if (!rp) { /* Redraw: nothing of reprojection ran or was allocated. */ check(s_rp_frames == 0 && s_rp_fallback == 0, "redraw: no reprojected frame"); check(!rp_allocated(), "redraw: reprojection allocated nothing"); check(s_fg_cost.clamp_spikes == 0, "redraw: cost estimate unclamped"); } else { check(s_rp_frames > 0, "reprojection: in-between frames reprojected"); printf("reprojected=%llu fallback=%llu\n", (unsigned long long)s_rp_frames, (unsigned long long)s_rp_fallback); /* The warp's own depth must not stay for the cars (PGXP depth * tests them LEQUAL against it): read the surface it drew into. */ if (pgxp && s_rp_last_dst) { const int dw = wide_mode ? g_wide_w * scale : FRAME_W * scale, dh = FRAME_H * scale; float *dep = (float *)malloc((size_t)dw * dh * sizeof *dep); p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_rp_last_dst); glPixelStorei(GL_PACK_ALIGNMENT, 4); glReadPixels(0, 0, dw, dh, GL_DEPTH_COMPONENT, GL_FLOAT, dep); p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); long near_count = 0; for (long i = 0; i < (long)dw * dh; i++) near_count += dep[i] < 0.9999f; printf("pgxp depth_written_px=%ld of %d\n", near_count, dw * dh); check(near_count * 20 < (long)dw * dh, "pgxp: no warp depth left for the cars (<5%)"); free(dep); } if (getenv("FG_ZDBG")) { /* the depth image's pixel kinds */ float *zz = (float *)malloc((size_t)s_rp_zw * s_rp_zh * 16); p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_rp_zfbo); glReadPixels(0, 0, s_rp_zw, s_rp_zh, GL_RGBA, GL_FLOAT, zz); long c[4] = {0}; for (long i = 0; i < (long)s_rp_zw * s_rp_zh; i++) { int k = (int)(zz[4*i+1]+0.5f); if (k>=0&&k<4) c[k]++; } printf("zdbg %dx%d kinds %ld %ld %ld %ld zn=%u views=%u ok=%d iz=%g\n", s_rp_zw, s_rp_zh, c[0], c[1], c[2], c[3], s_rp_zn, s_fg_fit.nviews, s_fg_fit.ok, zz[4*(s_rp_zw*100+100)]); free(zz); } uint32_t *ref = (uint32_t *)malloc((size_t)ww * wh * 4); s_fg_reproject = 0; /* the redraw of the same pair and fit: the reference */ check(fg_generate(0.5, 0) == 1, "redraw reference at phase 0.5"); read_back(ref); s_fg_reproject = 1; check(fg_generate(0.5, 0) == 1, "reprojected again at phase 0.5"); read_back(img); long far_count = 0, far_newer = 0, none = 0, hud = 0, hud_bad = 0; for (int i = 0; i < ww * wh; i++) { const int f = px_far(img[i], ref[i]); far_count += f; far_newer += px_far(img_last[1][i], ref[i]); none += f && px_far(img[i], img_last[0][i]) && px_far(img[i], img_last[1][i]); /* The HUD bar (near white in both real frames, where no * triangle reaches): exactly the newer frame's. */ const uint32_t a = img_last[0][i], b = img_last[1][i]; if (a == b && (b & 0xff) >= 0xf0 && ((b >> 8) & 0xff) >= 0xf0 && ((b >> 16) & 0xff) >= 0xf0) { hud++; hud_bad += img[i] != b; } } printf("reproject_vs_redraw far_px=%ld newer_vs_redraw=%ld unfilled_px=%ld hud_px=%ld " "hud_changed=%ld of %d\n", far_count, far_newer, none, hud, hud_bad, ww * wh); /* With a real pan (FG_PAN; one pixel is half a pixel at phase 0.5, * below what a warp at native resolution resolves) the warp moved * the picture: close to the redraw, much closer than the newer * frame it started from. */ if (g_pan > 1) { check(far_count * 20 < (long)ww * wh, "reprojection: phase 0.5 matches the redraw (<5% differ)"); check(far_newer * 50 > (long)ww * wh && far_count * 2 < far_newer, "reprojection: the warp follows the camera (under half the newer frame's difference)"); } check(none * 100 < (long)ww * wh, "reprojection: no holes, every pixel from a real frame or the redraw (<1%)"); check(hud > 0 && hud_bad == 0, "reprojection: HUD exactly the newer frame's"); free(ref); } /* With an HD texture pack the CPU raster is native VRAM's authority; * an in-between frame is presentation only and must not draw there. */ gl_renderer_set_hd_texture_mode(1); static uint16_t before[1024 * 512]; memcpy(before, s_vram, sizeof before); check(fg_generate(0.5, 0) == 1, "compose at phase 0.5 under HD authority"); check(memcmp(before, s_vram, sizeof before) == 0, "generated frame leaves native VRAM unchanged under HD authority"); gl_renderer_set_hd_texture_mode(0); free(img); check(n_gen > 0, "generated frames were presented"); } check(glGetError() == GL_NO_ERROR, "GL error"); if (g_pgxp_depth) { uint64_t dt = 0, dc = 0, st = 0; gl_renderer_pgxp_render_stats(&dt, &dc, &st); printf("pgxp depth_tris=%llu depth_clears=%llu seam_tris=%llu\n", (unsigned long long)dt, (unsigned long long)dc, (unsigned long long)st); check(dt > 0 && dc > 0, "PGXP depth tested the triangles"); } printf("real=%016llx\n", (unsigned long long)real_seq); printf("presents=%llu generated=%llu\n", (unsigned long long)n_real, (unsigned long long)n_gen); gl_renderer_shutdown(); /* stops the render and present threads: all shown */ if (pt_mode) { int same = n_composed == n_window && n_composed <= MAX_SHOWN && n_composed > 0; for (int i = 0; same && i < n_composed; i++) same = composed_d[i] == window_d[i]; printf("pt composed=%d shown=%d\n", n_composed, n_window); check(same, "the window shows every composed image, in order"); } printf("checks=%d failures=%d\n", checks, failures); SDL_DestroyWindow(win); SDL_Quit(); return failures ? 1 : 0; }