Files
psxrecomp/runtime/tests/test_gl_frame_gen.c

712 lines
37 KiB
C

/* 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 "gl_postfx_projection_fixture.h"
#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;
/* Host menu overlay and video telemetry (psx_host_menu.c, video_telemetry.cpp):
* none here. */
int psx_host_menu_overlay_image(const uint32_t **pixels, int *w, int *h){(void)pixels;(void)w;(void)h;return 0;}
void psx_video_telemetry_present(int result){(void)result;}
void psx_video_telemetry_compose(int renderer, int scale, int source_w, int source_h,
int drawable_w, int drawable_h, int viewport_w, int viewport_h){
(void)renderer;(void)scale;(void)source_w;(void)source_h;(void)drawable_w;(void)drawable_h;
(void)viewport_w;(void)viewport_h;}
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=<features> (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]);
}
}
}
/* FG_OCCLUDE=1 (reprojection, on the FG_PAN = 6 scene): after the near
* triangles (z = 1000) and over the backdrop (z = 2000), a far quad
* (z = 3000, moving with the same camera) is drawn last, as a game's sort or
* Max Detail's far sections can leave far geometry. The reprojection depth
* image must keep the nearest world depth there: painter order put the far
* quad's depth over the near triangles, and the warp moved them as distant
* scenery (objects through walls, doubled signs). */
static int g_occlude;
static void occlude_quad(int g, int bx) {
const int T = (g % 9) * g_pan; /* the triangles' camera: T units at z = 1000 */
const int qx = bx + 60 + T / 3, qy = 40, qw = 120, qh = 100, z = 3000;
const int xs4[4] = { qx, qx + qw, qx, qx + qw }, ys4[4] = { qy, qy, qy + qh, qy + qh };
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] = 3000u + (uint32_t)c; xs[k] = xs4[c]; ys[k] = ys4[c];
pc[3 * k] = (xs[k] - bx - T / 3) * 3 + T; pc[3 * k + 1] = ys[k] * 3; pc[3 * k + 2] = z; h[k] = 1000;
}
gl_renderer_fg_source(id, pc, h, xs, ys);
gr_draw_flat_triangle(xs[0], ys[0], xs[1], ys[1], xs[2], ys[2], 0x7c1f);
}
}
/* 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;
if (g_occlude) occlude_quad(g, bx);
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;
}
g_occlude = getenv("FG_OCCLUDE") && getenv("FG_OCCLUDE")[0] == '1';
if (g_occlude) g_pan = 6;
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");
if (g_occlude) {
/* The reprojection depth image under the wall must hold the
* wall's depth (the nearest world surface), whatever the draw
* order: painter order left the far quad's depth there, and the
* warp moved the wall's pixels as distant scenery. */
check(fg_generate(0.5, 0) == 1, "occlusion: generated");
check(s_rp_ztex && s_rp_zw > 0 && s_rp_zh > 0, "occlusion: depth image built");
float *z = (float *)malloc((size_t)s_rp_zw * s_rp_zh * 4 * sizeof(float));
glBindTexture(GL_TEXTURE_2D, s_rp_ztex);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_FLOAT, z);
glBindTexture(GL_TEXTURE_2D, 0);
long near_px = 0, far_px = 0;
/* The quad's box at the newer frame (g = 23: T = 30, +10 px). */
for (int y = 45; y < 135; y++)
for (int x = 75; x < 185; x++) {
const int zx = x * RP_ZS, zy = y * RP_ZS;
if (zx >= s_rp_zw || zy >= s_rp_zh) continue;
const float *t = &z[((size_t)zy * s_rp_zw + zx) * 4];
if (t[1] != 1.0f || t[0] <= 0.0f) continue; /* world pixels only */
const float Z = 1.0f / t[0];
near_px += Z < 1500.0f; far_px += Z > 2500.0f;
}
free(z);
printf("occlusion depth_image near=%ld far=%ld reprojected=%llu\n", near_px, far_px,
(unsigned long long)s_rp_frames);
check(near_px > 100, "occlusion: the near triangles are in the depth image");
check(far_px == 0, "occlusion: the far quad drawn last leaves no depth over nearer world (nearest wins)");
free(img);
printf(failures ? "FAIL\n" : "PASS\n");
return failures ? 1 : 0;
}
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%)");
/* Edge disocclusion (FG_PAN > 1: near triangles over a backdrop
* with parallax): what the in-between camera uncovers at a near
* object's trailing edge comes from the older frame's own warp
* (two-frame reprojection), not the newer frame's pixels
* un-warped. The single-frame warp left 0.5 % here. */
if (g_pan > 1) {
check(s_rp_older_frames > 0, "reprojection: the older frame was warped too");
check(none * 300 < (long)ww * wh, "reprojection: uncovered edges filled by the older frame's warp (<0.33%)");
}
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);
/* A large unprojected triangle after projected geometry must reject
* reprojection, while preserving the pre-existing redraw route. */
{
FgList *b = &s_fg_l[s_fg_pb];
FgPrim *unplaced = NULL;
int seen = 0;
double view_area = 0;
for (uint32_t vi = 0; vi < s_fg_fit.nviews; ++vi) {
const float *a = s_fg_fit.v[vi].area;
double area = (double)(a[2]-a[0])*(a[3]-a[1]);
if (area > view_area) view_area = area;
}
for (uint32_t i = 0; i < b->n; ++i) {
int pi = b->rec2prim[i];
if (pi < 0) continue;
FgPrim *p = &b->prims.v[pi];
if (!(p->vid[0] || p->vid[1] || p->vid[2])) continue;
double area = 0.5*fabs((double)(p->x[1]-p->x[0])*(p->y[2]-p->y[0])-
(double)(p->x[2]-p->x[0])*(p->y[1]-p->y[0]));
if (seen) { unplaced = p; break; }
seen = 1;
}
check(unplaced != NULL && view_area > 0, "method scope: large geometry witness");
if (unplaced) {
FgPrim saved = *unplaced;
int saved_method = s_fg_reproject;
const float *a = s_fg_fit.v[0].area;
unplaced->x[0] = a[0]; unplaced->x[1] = a[2]; unplaced->x[2] = a[0];
unplaced->y[0] = a[1]; unplaced->y[1] = a[1]; unplaced->y[2] = a[3];
memset(unplaced->vid, 0, sizeof(unplaced->vid));
memset(unplaced->pz, 0, sizeof(unplaced->pz));
check(fg_unplaced_share(b) > fg_max_unplaced(), "method scope: unplaced geometry exceeds threshold");
s_fg_reproject = 1;
check(fg_generate(0.5, 0) == 0, "method scope: reprojection refuses unplaceable geometry");
s_fg_reproject = 0;
check(fg_generate(0.5, 0) == 1, "method scope: redraw keeps its original route");
*unplaced = saved;
s_fg_reproject = saved_method;
}
}
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;
}