mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
712 lines
37 KiB
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;
|
|
}
|