diff --git a/docs/FRAME_GENERATION.md b/docs/FRAME_GENERATION.md index 898dd6a0..01891e8b 100644 --- a/docs/FRAME_GENERATION.md +++ b/docs/FRAME_GENERATION.md @@ -30,19 +30,34 @@ has measured time to spare. game frame; a present of the same buffer is the same frame again. R4 updates every other VBlank, so generation interpolates between flips (30 Hz game frames), not between VBlanks. -- **Matching** (`frame_gen.c`). Triangles are keyed by op, texture page, - CLUT, texture coordinates and the draw area they were clipped to - (relative to their buffer); positions are relative to the buffer. Each - newer triangle pairs, in draw order, with the nearest older one of the same - key whose three vertices moved at most 96 native px and alike (within - 24 px of each other). R4 races pair 87-97 % of their triangles. -- **Drawing.** An in-between frame redraws the newer list into separate - surfaces (a copy of the displayed buffer's hr rect and wide rows) with each - matched triangle at `lerp(older, newer, t)`, sub-pixel via the precise - triangle path; unmatched triangles, rects, lines, fills (2D, HUD) draw as - the newer frame has them. The raw texture mirror is not packed from the - generated surfaces; all bookkeeping the draws touch is restored, so the - real stream is unaffected. +- **Sources.** While generation is on, every RTPS/RTPT output is noted + (gte.cpp, `gte_fg_source_*`) with an identity (the issuing function `ra` + and the model-space vertex), the camera-space position the GTE divided and + H, indexed by the packed screen word. gpu.c looks each polygon vertex up by + its packet word and records the sources ahead of the triangle + (`RTH_FG_SRC`, never drawn). CPU-built vertices have none. Guest-visible + GTE results are untouched. +- **Camera** (`frame_gen.c`, `fg_cam_fit`). No primitive is paired by draw + order. Per view (draw area), vertices of the two frames with the same + identity are paired in camera space (instances: the clearly nearest) and a + rigid motion is fitted (RANSAC + least squares, inliers within 1 screen px + at their depth). The static world agrees with it; paired vertices that + disagree are objects (cars, the followed car). Verdict: no projections, a + view with few pairs or under half inliers, or a turn/travel no camera makes + between two frames rejects the generated frame (the real frame shows). +- **Drawing** (`fg_cam_place`). One real frame is redrawn from an + in-between camera: world vertices re-projected through the fraction of the + fitted motion, object vertices lerped in camera space, CPU-built vertices + of placed triangles moved with their neighbours, everything else (HUD, 2D, + sprites, the mirror's own view if unprojected) unchanged. Moving forward + the older frame is redrawn (seen from further on it spreads past the + screen edges); otherwise the newer. Drawing starts on the newer real + frame's image and skips the clears before the first draw, and strips along + a view's edges the picture moved away from are copied from that image, so + what the in-between camera uncovers shows the newer frame instead of a + hole. Triangles crossing the camera plane are dropped. The raw texture + mirror is not packed from the generated surfaces; all bookkeeping the draws + touch is restored, so the real stream is unaffected. - **Schedule.** With `n` in-between frames per game frame, at the flip the real frame is composed and kept (its buffer may be drawn over before it is shown), frames `t = k/(n+1)` are presented `flip/(n+1)` apart, then the @@ -73,7 +88,9 @@ has measured time to spare. ## Limits -- Only triangles move; sprites, lines and fills are the newer frame's. +- Only GTE-projected triangles move; sprites, lines and fills are the + redrawn frame's. Areas uncovered by the in-between camera show the newer + frame (thin slivers at depth discontinuities are possible). - A texture the frame drew earlier into its own displayed buffer is sampled as the raw mirror holds it when the in-between frame is drawn. - Single-buffered games never flip, so nothing is generated. @@ -83,17 +100,26 @@ has measured time to spare. `{"cmd":"frame_gen"}` (not a sync point): generated / real presents, flips, dups, plan (`last_n`, `slots`), costs (`real_ms`, `gen_ms`, `swap_ms`), last -match counts, breaker, last hold, total swaps. +camera fit (`views`, `pairs`, `inliers`, `place_*`, `cam_angle_deg`, +`cam_shift`), verdict (`verdict_ok`, `rejected`, `reject_why`), breaker, +ceiling, last hold, total swaps. `PSX_PRESENT_SHOT_GENERATED=1` (diagnostic): a staged `present_shot` is taken at the next generated frame instead of the next present, to inspect -in-between images in game. +in-between images in game. `PSX_PRESENT_SHOT_BURST=N`: a staged +`present_shot` takes the next N composed frames (written after the burst as +`_NN_{r|g}_f.png`, in composition order: a real frame precedes +the in-between frames that lead up to it). ## Tests -- `frame_gen_test`: matching (moved scenes, keys, move and deformation - limits, duplicates, pair-once), interpolation endpoints, the plan, the - breaker. +- `frame_gen_test`: the camera fit on a synthetic grandstand of one + repeated texture, redrawn in another order with a row culled and one added + (the case the old draw-order matcher paired wrongly: 225 of 226 pairs) - + phase 0 / 0.5 / 1 land where the older / halfway / newer camera sees each + vertex; a followed car, a HUD triangle, a CPU-built vertex; the verdict + (no projections, a cut, an impossible turn, inconsistent motion); the plan, + breaker, cost, pace, ceiling. - `render_thread_test`: the tick hook runs on the render thread with the context, never early, and wakes an idle thread. - `gl_frame_gen_test` (real GL, `gpu;opengl;hardware`): a double-buffered diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 31b44d6b..cf75c591 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -221,6 +221,10 @@ extern void gte_precision_tracking_set(int enabled); * unchanged — this is a visual-only enhancement, default off. */ extern void gte_geometry_correction_set(int enabled); extern int gte_geometry_correction_enabled(void); +/* Frame generation's vertex sources (gte.cpp): 0 = unknown. */ +extern void gte_fg_source_set(int enabled); +extern int gte_fg_source_enabled(void); +extern int gte_fg_source_lookup(uint32_t packed, uint32_t *id, int32_t p[3], int32_t *hdist); /* Corrected vertices consumed since the last gte_geometry_correction_set — * the "is this actually doing anything on this title" counter. */ extern uint32_t gte_geometry_correction_hits(void); diff --git a/runtime/include/frame_gen.h b/runtime/include/frame_gen.h index a24a18c6..5ea3eed7 100644 --- a/runtime/include/frame_gen.h +++ b/runtime/include/frame_gen.h @@ -8,14 +8,38 @@ * The GL backend (gpu_gl_renderer.c) owns the record lists themselves, * drawing and presenting; nothing here touches GL or guest state. * - * A primitive is a triangle of one frame's draw list with a match key (the - * draw op, texture page, CLUT, texture coordinates, the draw area it was - * clipped to) and its three screen positions in native pixels relative to - * that frame's displayed buffer. Matching pairs each primitive of the newer - * frame with one of the older frame's, in draw order: the same key, every - * vertex moved at most max_move, and the three vertices moved alike (a - * rigid-ish motion, at most max_deform apart). A primitive with no partner - * is drawn where the newer frame has it. + * A primitive is a triangle of one frame's draw list with a key (the draw + * op, texture page, CLUT, texture coordinates, the draw area it was clipped + * to), a view (its draw area: split-screen views have their own cameras), its + * three screen positions in native pixels relative to that frame's displayed + * buffer and, per vertex, the GTE projection that produced it when one did + * (gte_fg_source_lookup): an identity (the projecting function and the + * model-space vertex), the camera-space position the GTE divided and the + * projection distance H. + * + * In-between frames are the NEWER frame redrawn from an in-between camera; + * no primitive of the older frame is drawn and none is paired by draw order. + * Camera motion: per view, vertices of both frames with the same identity + * are paired in camera space and a rigid motion old -> new is fitted to them + * (RANSAC, then least squares on the inliers: the static world agrees, moving + * objects are outliers). A vertex is placed at phase t by + * - camera: the fitted motion's fraction t applied to the older camera, + * i.e. P_t = D^t D^-1 P_new (the static world, and anything new); + * - object: a paired vertex that moved against the world (a car, the + * player's own car under a chase camera) lerps its camera-space position; + * - neighbours: a vertex without a projection (CPU-built), or one new to + * a moving object, in a triangle with placed vertices moves by their + * mean screen motion, per position; + * - unchanged: everything else (HUD, 2D, sprites). + * Each is re-projected (H * x / z) and moved by the difference from its own + * newer projection, so draw offsets and sub-pixel positions stay the game's. + * What the in-between camera uncovers shows the newer frame: the caller + * starts from its image and skips the clears before the first draw, and the + * strips along a view's edges the picture moved away from (fg_cam_place's + * margins) are the newer frame's. + * The fit's verdict rejects the whole generated frame (the real frame shows) + * when a view with projections has too few pairs, too few inliers, or a + * motion no camera makes between two frames. */ #include #include @@ -26,8 +50,13 @@ extern "C" { typedef struct FgPrim { uint32_t rec; /* index of its record in the owner's list */ - uint32_t key; /* match key (fg_hash) */ + uint32_t key; /* fg_hash of what it draws */ + uint32_t view; /* fg_hash of its draw area */ + float area[4]; /* that draw area (x1, y1, x2, y2), relative like x/y */ + uint32_t vid[3]; /* projection identities, 0 = no projection */ float x[3], y[3]; /* native px, relative to the list's display origin */ + float p[3][3]; /* camera-space positions (GTE units) */ + float h[3]; /* projection distance */ } FgPrim; typedef struct FgPrimList { @@ -44,25 +73,56 @@ int fg_prims_add(FgPrimList *l, const FgPrim *p); uint32_t fg_hash(uint32_t h, const int32_t *w, int n); #define FG_HASH_INIT 2166136261u -typedef struct FgMatchParams { - float max_move; /* native px a vertex may move between the frames */ - float max_deform; /* native px the vertices' moves may differ by */ - int window; /* same-key candidates considered per primitive */ -} FgMatchParams; -void fg_match_defaults(FgMatchParams *p); +typedef struct FgCamParams { + int iters; /* RANSAC samples per view */ + float tol_px; /* inlier: |D a - b| within this many screen px at b's depth */ + float min_inliers; /* share of pairs the camera must explain */ + uint32_t min_pairs; /* pairs a view with projections needs */ + float max_angle; /* radians the camera may turn between frames */ + float max_shift; /* camera travel, as a share of the median depth */ + float max_obj; /* object pairing: |b - a| < max_obj * |b.z| */ +} FgCamParams; +void fg_cam_defaults(FgCamParams *p); -typedef struct FgMatchStats { - uint32_t prims, matched, moved, unmatched; -} FgMatchStats; +#define FG_MAX_VIEWS 4 +typedef struct FgView { + uint32_t view, sources, pairs, inliers; + int ok; + float area[4]; /* the view's draw area */ + double q[4], t[3]; /* old -> new: rotation quaternion (w,x,y,z), translation */ + const char *why; +} FgView; -/* For every primitive of `newer`, the index of its partner in `older` or -1 - * (match[newer->n]). Each older primitive pairs at most once. Returns the - * matched count. */ -uint32_t fg_match(const FgPrimList *older, const FgPrimList *newer, - const FgMatchParams *p, int32_t *match, FgMatchStats *st); +typedef struct FgCamFit { + int ok; + const char *why; + uint32_t nviews, prims, camera, object, neighbour, unchanged; + FgView v[FG_MAX_VIEWS]; +} FgCamFit; -/* Positions at phase t between a (t = 0) and b (t = 1). */ -void fg_lerp(const FgPrim *a, const FgPrim *b, double t, float x[3], float y[3]); +/* How each newer vertex is placed (fg_cam_fit fills it, newer->n * 3). */ +typedef struct FgVert { + uint8_t mode; /* FG_PLACE_* */ + int8_t view; /* index into FgCamFit.v, -1 */ + uint8_t paired; /* the older frame has this vertex */ + float a[3]; /* FG_PLACE_OBJECT: the older camera-space position */ +} FgVert; +enum { FG_PLACE_UNCHANGED = 0, FG_PLACE_CAMERA, FG_PLACE_OBJECT, FG_PLACE_NEIGHBOUR }; + +/* Fits the camera motion older -> newer per view and decides each newer + * vertex's placement; returns fit->ok (the verdict). */ +int fg_cam_fit(const FgPrimList *older, const FgPrimList *newer, const FgCamParams *p, + FgCamFit *fit, FgVert *verts); + +/* Screen positions of every newer vertex at phase t (0 = the older frame's + * camera, 1 = the newer frame exactly): x/y[newer->n * 3], NaN for a vertex + * behind the in-between camera (its triangle is not drawn). margin (may be + * NULL) receives, per view, how far the picture moved in from each edge of + * its draw area (left, top, right, bottom, px): the newer frame drew nothing + * beyond its edges, so that strip has no in-between picture and the caller + * shows the newer frame there. */ +void fg_cam_place(const FgPrimList *newer, const FgCamFit *fit, const FgVert *verts, + double t, float *x, float *y, float margin[][4]); /* How many in-between frames to draw per game frame. * flip_s the game frame's interval (time between flips) diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index ebc6edb2..0ce21b8c 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -334,6 +334,12 @@ void gl_renderer_dynres_stats(GlDynresStats *out); * (not a breaker trip), e.g. while dynamic resolution is over budget. * json: the debug server's {"cmd":"frame_gen"} fields (no sync point). */ void gl_renderer_set_frame_generation(int on); +/* The sources of the next triangle's vertices (gte_fg_source_lookup: id, + * camera-space x/y/z, projection distance H; id 0 = none) and the integer + * positions it will be drawn at; recorded for frame generation only, never + * drawn. */ +void gl_renderer_fg_source(const uint32_t id[3], const int32_t pc[9], const int32_t h[3], + const int32_t x[3], const int32_t y[3]); int gl_renderer_frame_generation(void); void gl_renderer_frame_gen_configure(double refresh_hz, double guest_hz); void gl_renderer_frame_gen_hold(const char *reason, double secs); diff --git a/runtime/src/frame_gen.c b/runtime/src/frame_gen.c index 05d4d8b0..646f1fbd 100644 --- a/runtime/src/frame_gen.c +++ b/runtime/src/frame_gen.c @@ -37,119 +37,480 @@ uint32_t fg_hash(uint32_t h, const int32_t *w, int n) { return h; } -void fg_match_defaults(FgMatchParams *p) { - p->max_move = 96.0f; - p->max_deform = 24.0f; - p->window = 16; +void fg_cam_defaults(FgCamParams *p) { + p->iters = 64; + p->tol_px = 1.0f; + p->min_inliers = 0.5f; + p->min_pairs = 24; + p->max_angle = 0.35f; + p->max_shift = 0.5f; + p->max_obj = 0.25f; } -/* Open-addressing table from key to the first older primitive with it; the - * older primitives with one key are chained in draw order. */ -typedef struct { - uint32_t *keys; - int32_t *head; - uint32_t mask; -} KeyTab; +/* ---- rigid motion ---- */ +typedef struct { double q[4], t[3]; } Rigid; -static int tab_init(KeyTab *t, uint32_t n) { - uint32_t cap = 64; - while (cap < n * 2u) cap <<= 1; - t->keys = (uint32_t *)malloc((size_t)cap * sizeof *t->keys); - t->head = (int32_t *)malloc((size_t)cap * sizeof *t->head); - t->mask = cap - 1; - if (!t->keys || !t->head) return 0; - for (uint32_t i = 0; i < cap; i++) t->head[i] = -1; +static void q_to_m(const double q[4], double m[9]) { + const double w = q[0], x = q[1], y = q[2], z = q[3]; + m[0] = 1 - 2 * (y * y + z * z); m[1] = 2 * (x * y - w * z); m[2] = 2 * (x * z + w * y); + m[3] = 2 * (x * y + w * z); m[4] = 1 - 2 * (x * x + z * z); m[5] = 2 * (y * z - w * x); + m[6] = 2 * (x * z - w * y); m[7] = 2 * (y * z + w * x); m[8] = 1 - 2 * (x * x + y * y); +} + +static void m_mul_v(const double m[9], const double v[3], double o[3]) { + for (int i = 0; i < 3; i++) o[i] = m[3 * i] * v[0] + m[3 * i + 1] * v[1] + m[3 * i + 2] * v[2]; +} + +/* Least-squares rigid motion a -> b (Horn's quaternion method; the largest + * eigenvector by Jacobi rotations). */ +static int fit_rigid(const float (*a)[3], const float (*b)[3], const uint32_t *idx, uint32_t n, Rigid *r) { + if (n < 3) return 0; + double ca[3] = { 0, 0, 0 }, cb[3] = { 0, 0, 0 }; + for (uint32_t i = 0; i < n; i++) + for (int k = 0; k < 3; k++) { ca[k] += a[idx[i]][k]; cb[k] += b[idx[i]][k]; } + for (int k = 0; k < 3; k++) { ca[k] /= n; cb[k] /= n; } + double S[3][3] = { { 0 } }; + for (uint32_t i = 0; i < n; i++) + for (int u = 0; u < 3; u++) + for (int v = 0; v < 3; v++) + S[u][v] += (a[idx[i]][u] - ca[u]) * (b[idx[i]][v] - cb[v]); + const double N[4][4] = { + { S[0][0] + S[1][1] + S[2][2], S[1][2] - S[2][1], S[2][0] - S[0][2], S[0][1] - S[1][0] }, + { S[1][2] - S[2][1], S[0][0] - S[1][1] - S[2][2], S[0][1] + S[1][0], S[2][0] + S[0][2] }, + { S[2][0] - S[0][2], S[0][1] + S[1][0], -S[0][0] + S[1][1] - S[2][2], S[1][2] + S[2][1] }, + { S[0][1] - S[1][0], S[2][0] + S[0][2], S[1][2] + S[2][1], -S[0][0] - S[1][1] + S[2][2] } }; + /* Cyclic Jacobi: the eigenvector of the largest eigenvalue, exactly. */ + double A[4][4], V[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) { A[i][j] = N[i][j]; V[i][j] = i == j; } + for (int sweep = 0; sweep < 50; sweep++) { + double off = 0; + for (int i = 0; i < 4; i++) for (int j = i + 1; j < 4; j++) off += A[i][j] * A[i][j]; + if (off < 1e-30) break; + for (int pp = 0; pp < 4; pp++) + for (int qq = pp + 1; qq < 4; qq++) { + if (fabs(A[pp][qq]) < 1e-300) continue; + const double th = (A[qq][qq] - A[pp][pp]) / (2.0 * A[pp][qq]); + const double tt = (th >= 0 ? 1.0 : -1.0) / (fabs(th) + sqrt(th * th + 1.0)); + const double c = 1.0 / sqrt(tt * tt + 1.0), sn = tt * c; + for (int k = 0; k < 4; k++) { + const double akp = A[k][pp], akq = A[k][qq]; + A[k][pp] = c * akp - sn * akq; A[k][qq] = sn * akp + c * akq; + } + for (int k = 0; k < 4; k++) { + const double apk = A[pp][k], aqk = A[qq][k]; + A[pp][k] = c * apk - sn * aqk; A[qq][k] = sn * apk + c * aqk; + } + for (int k = 0; k < 4; k++) { + const double vkp = V[k][pp], vkq = V[k][qq]; + V[k][pp] = c * vkp - sn * vkq; V[k][qq] = sn * vkp + c * vkq; + } + } + } + int best = 0; + for (int i = 1; i < 4; i++) if (A[i][i] > A[best][best]) best = i; + double q[4], len = 0; + for (int i = 0; i < 4; i++) { q[i] = V[i][best]; len += q[i] * q[i]; } + len = sqrt(len); + if (!(len > 0)) return 0; + for (int i = 0; i < 4; i++) q[i] /= len; + if (q[0] < 0) for (int i = 0; i < 4; i++) q[i] = -q[i]; + memcpy(r->q, q, sizeof q); + double m[9], rc[3]; + q_to_m(q, m); + m_mul_v(m, ca, rc); + for (int k = 0; k < 3; k++) r->t[k] = cb[k] - rc[k]; return 1; } -static int32_t *tab_slot(KeyTab *t, uint32_t key) { - uint32_t i = (key * 2654435761u) & t->mask; - while (t->head[i] >= 0 && t->keys[i] != key) i = (i + 1) & t->mask; - t->keys[i] = key; - return &t->head[i]; +static int is_inlier(const double m[9], const double t[3], const float a[3], const float b[3], + float h, const FgCamParams *p) { + const double av[3] = { a[0], a[1], a[2] }; + double o[3]; + m_mul_v(m, av, o); + double e = 0; + for (int k = 0; k < 3; k++) { const double d = o[k] + t[k] - b[k]; e += d * d; } + /* In screen pixels at the point's depth. */ + const double tol = p->tol_px * fmax(fabs(b[2]), 1.0) / fmax(h, 1.0); + return e < tol * tol; } -uint32_t fg_match(const FgPrimList *older, const FgPrimList *newer, - const FgMatchParams *p, int32_t *match, FgMatchStats *st) { - FgMatchStats s; - memset(&s, 0, sizeof s); - s.prims = newer->n; - for (uint32_t i = 0; i < newer->n; i++) match[i] = -1; - if (!older->n || !newer->n) { - s.unmatched = newer->n; - if (st) *st = s; - return 0; +/* ---- pairing ---- */ +typedef struct { uint32_t id; float p[3], h; } Src; + +static int src_cmp(const void *x, const void *y) { + const Src *a = (const Src *)x, *b = (const Src *)y; + if (a->id != b->id) return a->id < b->id ? -1 : 1; + for (int k = 0; k < 3; k++) if (a->p[k] != b->p[k]) return a->p[k] < b->p[k] ? -1 : 1; + return 0; +} + +/* The distinct sources of one view, sorted. */ +static uint32_t gather(const FgPrimList *l, uint32_t view, Src *out) { + uint32_t n = 0; + for (uint32_t j = 0; j < l->n; j++) { + const FgPrim *pr = &l->v[j]; + if (pr->view != view) continue; + for (int k = 0; k < 3; k++) { + if (!pr->vid[k]) continue; + out[n].id = pr->vid[k]; + memcpy(out[n].p, pr->p[k], sizeof out[n].p); + out[n].h = pr->h[k]; + n++; + } } - KeyTab t; - int32_t *next = (int32_t *)malloc((size_t)older->n * sizeof *next); - uint8_t *used = (uint8_t *)calloc(older->n, 1); - if (!tab_init(&t, older->n) || !next || !used) { - free(t.keys); free(t.head); free(next); free(used); - s.unmatched = newer->n; - if (st) *st = s; - return 0; + qsort(out, n, sizeof *out, src_cmp); + uint32_t w = 0; + for (uint32_t i = 0; i < n; i++) + if (!w || src_cmp(&out[w - 1], &out[i]) != 0) out[w++] = out[i]; + return w; +} + +static uint32_t lower(const Src *s, uint32_t n, uint32_t id) { + uint32_t lo = 0, hi = n; + while (lo < hi) { const uint32_t m = (lo + hi) / 2; if (s[m].id < id) lo = m + 1; else hi = m; } + return lo; +} + +static float dist3(const float a[3], const float b[3]) { + const float dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2]; + return sqrtf(dx * dx + dy * dy + dz * dz); +} + +static double median_z(const Src *s, uint32_t n) { + if (!n) return 0; + /* A coarse median: the depths in 64 buckets. */ + double lo = 1e30, hi = -1e30; + for (uint32_t i = 0; i < n; i++) { if (s[i].p[2] < lo) lo = s[i].p[2]; if (s[i].p[2] > hi) hi = s[i].p[2]; } + if (hi <= lo) return lo; + uint32_t b[64] = { 0 }; + for (uint32_t i = 0; i < n; i++) { + int k = (int)((s[i].p[2] - lo) / (hi - lo) * 63.999); + b[k]++; } - /* Chains in draw order: insert from the back. */ - for (uint32_t i = older->n; i-- > 0;) { - int32_t *h = tab_slot(&t, older->v[i].key); - next[i] = *h; - *h = (int32_t)i; + uint32_t c = 0; + for (int k = 0; k < 64; k++) { c += b[k]; if (c * 2 >= n) return lo + (hi - lo) * (k + 0.5) / 64.0; } + return hi; +} + +static int fit_view(const FgPrimList *older, const FgPrimList *newer, const FgCamParams *p, + FgView *v, Src **pa_out, Src **pb_out, uint8_t **in_out, uint32_t *np_out, + Src **nb_out, uint32_t *nnb_out) { + Src *sa = (Src *)malloc(((size_t)older->n * 3 + 1) * sizeof *sa); + Src *sb = (Src *)malloc(((size_t)newer->n * 3 + 1) * sizeof *sb); + if (!sa || !sb) { free(sa); free(sb); v->why = "out of memory"; return 0; } + const uint32_t na = gather(older, v->view, sa), nb = gather(newer, v->view, sb); + v->sources = nb; + /* Pairs: the same identity; among several (instances of one mesh), the + * nearest, when clearly the nearest. */ + float (*a)[3] = (float (*)[3])malloc(((size_t)nb + 1) * sizeof *a); + float (*b)[3] = (float (*)[3])malloc(((size_t)nb + 1) * sizeof *b); + float *hb = (float *)malloc(((size_t)nb + 1) * sizeof *hb); + uint32_t *bi = (uint32_t *)malloc(((size_t)nb + 1) * sizeof *bi); + uint32_t np = 0; + for (uint32_t i = 0; a && b && bi && hb && i < nb; i++) { + uint32_t j = lower(sa, na, sb[i].id); + float best = -1, second = -1; + uint32_t bj = 0; + for (; j < na && sa[j].id == sb[i].id; j++) { + const float d = dist3(sa[j].p, sb[i].p); + if (best < 0 || d < best) { second = best; best = d; bj = j; } + else if (second < 0 || d < second) second = d; + } + if (best < 0) continue; + if (second >= 0 && best > 0.5f * second) continue; + memcpy(a[np], sa[bj].p, sizeof a[np]); + memcpy(b[np], sb[i].p, sizeof b[np]); + hb[np] = sb[i].h; + bi[np++] = i; } - const float mm = p->max_move * p->max_move; - const float md = p->max_deform * p->max_deform; + v->pairs = np; + uint8_t *in = (uint8_t *)calloc((size_t)np + 1, 1); + uint32_t *idx = (uint32_t *)malloc(((size_t)np + 1) * sizeof *idx); + int ok = 0; + if (!a || !b || !bi || !hb || !in || !idx) { v->why = "out of memory"; goto done; } + if (np < p->min_pairs) { v->why = "few pairs"; goto done; } + { + Rigid best_r; + uint32_t best_n = 0, seed = 0x9E3779B9u ^ np; + double m[9]; + for (int it = 0; it < p->iters; it++) { + uint32_t s3[3]; + for (int k = 0; k < 3; k++) { seed = seed * 1664525u + 1013904223u; s3[k] = (seed >> 8) % np; } + if (s3[0] == s3[1] || s3[1] == s3[2] || s3[0] == s3[2]) continue; + Rigid r; + if (!fit_rigid((const float (*)[3])a, (const float (*)[3])b, s3, 3, &r)) continue; + q_to_m(r.q, m); + uint32_t c = 0; + for (uint32_t i = 0; i < np; i++) c += (uint32_t)is_inlier(m, r.t, a[i], b[i], hb[i], p); + if (c > best_n) { best_n = c; best_r = r; } + } + if (best_n < 3) { v->why = "no consistent camera"; goto done; } + /* Refine on the inliers, twice. */ + for (int pass = 0; pass < 2; pass++) { + q_to_m(best_r.q, m); + uint32_t c = 0; + for (uint32_t i = 0; i < np; i++) + if ((in[i] = (uint8_t)is_inlier(m, best_r.t, a[i], b[i], hb[i], p))) idx[c++] = i; + Rigid r; + if (c >= 3 && fit_rigid((const float (*)[3])a, (const float (*)[3])b, idx, c, &r)) best_r = r; + } + q_to_m(best_r.q, m); + uint32_t c = 0; + for (uint32_t i = 0; i < np; i++) c += (in[i] = (uint8_t)is_inlier(m, best_r.t, a[i], b[i], hb[i], p)); + v->inliers = c; + memcpy(v->q, best_r.q, sizeof v->q); + memcpy(v->t, best_r.t, sizeof v->t); + const double ang = 2.0 * acos(fmin(1.0, fabs(best_r.q[0]))); + const double mz = median_z(sb, nb); + const double sh = sqrt(best_r.t[0] * best_r.t[0] + best_r.t[1] * best_r.t[1] + best_r.t[2] * best_r.t[2]); + if ((float)c < p->min_inliers * (float)np || c < p->min_pairs) v->why = "few inliers"; + else if (ang > p->max_angle) v->why = "camera turned too far"; + else if (mz > 0 && sh > p->max_shift * mz) v->why = "camera moved too far"; + else ok = 1; + } +done: + v->ok = ok; + free(idx); + /* Pairs out (for the object placement), newer sources kept. */ + if (pa_out) { + Src *pa = (Src *)malloc(((size_t)np + 1) * sizeof *pa); + if (pa && a) for (uint32_t i = 0; i < np; i++) { pa[i] = sb[bi[i]]; memcpy(pa[i].p, a[i], sizeof pa[i].p); } + *pa_out = pa; + } + if (pb_out) { + Src *pb = (Src *)malloc(((size_t)np + 1) * sizeof *pb); + if (pb && bi) for (uint32_t i = 0; i < np; i++) pb[i] = sb[bi[i]]; + *pb_out = pb; + } + *in_out = in; *np_out = np; + *nb_out = sb; *nnb_out = nb; + free(sa); free(a); free(b); free(bi); free(hb); + return ok; +} + +int fg_cam_fit(const FgPrimList *older, const FgPrimList *newer, const FgCamParams *p, + FgCamFit *fit, FgVert *verts) { + memset(fit, 0, sizeof *fit); + fit->prims = newer->n; + for (uint32_t j = 0; j < newer->n * 3u; j++) { + verts[j].mode = FG_PLACE_UNCHANGED; verts[j].view = -1; verts[j].paired = 0; + } + /* The views with projections, most first. */ + uint32_t vk[FG_MAX_VIEWS], vc[FG_MAX_VIEWS], nv = 0; for (uint32_t j = 0; j < newer->n; j++) { - const FgPrim *b = &newer->v[j]; - int32_t *h = tab_slot(&t, b->key); - /* Drop used entries at the chain head so later lookups stay short. */ - while (*h >= 0 && used[*h]) *h = next[*h]; - int32_t best = -1; - float best_d = 0.0f; - int seen = 0; - for (int32_t i = *h; i >= 0 && seen < p->window; i = next[i]) { - if (used[i]) continue; - seen++; - const FgPrim *a = &older->v[i]; - float dx[3], dy[3], d = 0.0f; - int ok = 1; - for (int k = 0; k < 3 && ok; k++) { - dx[k] = b->x[k] - a->x[k]; - dy[k] = b->y[k] - a->y[k]; - float d2 = dx[k] * dx[k] + dy[k] * dy[k]; - if (d2 > mm) ok = 0; - d += d2; - } - for (int k = 1; k < 3 && ok; k++) { - float ex = dx[k] - dx[0], ey = dy[k] - dy[0]; - if (ex * ex + ey * ey > md) ok = 0; - } - if (!ok) continue; - if (best < 0 || d < best_d) { - best = i; - best_d = d; - if (d == 0.0f) break; /* did not move: nothing closer */ - } - } - if (best >= 0) { - used[best] = 1; - match[j] = best; - s.matched++; - if (best_d > 0.0f) s.moved++; - } else { - s.unmatched++; + const FgPrim *pr = &newer->v[j]; + if (!pr->vid[0] && !pr->vid[1] && !pr->vid[2]) continue; + uint32_t i = 0; + while (i < nv && vk[i] != pr->view) i++; + if (i == nv) { + if (nv == FG_MAX_VIEWS) { fit->why = "too many views"; return 0; } + vk[nv] = pr->view; vc[nv++] = 0; } + vc[i]++; } - free(t.keys); free(t.head); free(next); free(used); - if (st) *st = s; - return s.matched; + if (!nv) { fit->why = "no projections"; return 0; } + fit->nviews = nv; + int all = 1; + for (uint32_t vi = 0; vi < nv; vi++) { + FgView *v = &fit->v[vi]; + v->view = vk[vi]; + for (uint32_t j = 0; j < newer->n; j++) + if (newer->v[j].view == v->view) { memcpy(v->area, newer->v[j].area, sizeof v->area); break; } + Src *pa = NULL, *pb = NULL, *sb = NULL; + uint8_t *in = NULL; + uint32_t np = 0, nb = 0; + const int ok = fit_view(older, newer, p, v, &pa, &pb, &in, &np, &sb, &nb); + /* A view of a few projections (a 3D icon, a stray lookup) without a + * camera stays as it is; a real view without one rejects the frame. */ + if (!ok && vc[vi] >= p->min_pairs) { all = 0; if (!fit->why) fit->why = v->why; } + if (ok && pa && pb && in) { + /* Pairs are in newer-source order (pb sorted): look vertices up. */ + for (uint32_t j = 0; j < newer->n; j++) { + const FgPrim *pr = &newer->v[j]; + if (pr->view != v->view) continue; + for (int k = 0; k < 3; k++) { + if (!pr->vid[k]) continue; + FgVert *fv = &verts[3 * j + k]; + fv->view = (int8_t)vi; + fv->mode = FG_PLACE_CAMERA; + Src key; key.id = pr->vid[k]; memcpy(key.p, pr->p[k], sizeof key.p); + uint32_t lo = 0, hi = np; + while (lo < hi) { const uint32_t m = (lo + hi) / 2; if (src_cmp(&pb[m], &key) < 0) lo = m + 1; else hi = m; } + fv->paired = lo < np && src_cmp(&pb[lo], &key) == 0; + if (fv->paired && !in[lo] && + dist3(pa[lo].p, pr->p[k]) < p->max_obj * fabsf(pr->p[k][2]) + 1.0f) { + fv->mode = FG_PLACE_OBJECT; + memcpy(fv->a, pa[lo].p, sizeof fv->a); + } + } + } + } + free(pa); free(pb); free(in); free(sb); + } + fit->ok = all; + if (!all) { + for (uint32_t j = 0; j < newer->n * 3u; j++) verts[j].mode = FG_PLACE_UNCHANGED; + return 0; + } + /* An unpaired vertex of a moving object (a camera-attached sky dome + * whose vertex was culled last frame) moves with the object, not with + * the world. */ + for (uint32_t j = 0; j < newer->n; j++) { + int obj = 0; + for (int k = 0; k < 3; k++) obj |= verts[3 * j + k].mode == FG_PLACE_OBJECT; + if (!obj) continue; + for (int k = 0; k < 3; k++) + if (verts[3 * j + k].mode == FG_PLACE_CAMERA && !verts[3 * j + k].paired) + verts[3 * j + k].mode = FG_PLACE_NEIGHBOUR; + } + /* Vertices without a projection in triangles with placed ones. */ + for (uint32_t j = 0; j < newer->n; j++) { + int placed = 0; + for (int k = 0; k < 3; k++) placed += verts[3 * j + k].mode != FG_PLACE_UNCHANGED; + if (!placed || placed == 3) continue; + int8_t vw = -1; + for (int k = 0; k < 3; k++) if (verts[3 * j + k].view >= 0) vw = verts[3 * j + k].view; + for (int k = 0; k < 3; k++) + if (verts[3 * j + k].mode == FG_PLACE_UNCHANGED && !newer->v[j].vid[k]) { + verts[3 * j + k].mode = FG_PLACE_NEIGHBOUR; + verts[3 * j + k].view = vw; + } + } + for (uint32_t j = 0; j < newer->n * 3u; j++) + switch (verts[j].mode) { + case FG_PLACE_CAMERA: fit->camera++; break; + case FG_PLACE_OBJECT: fit->object++; break; + case FG_PLACE_NEIGHBOUR: fit->neighbour++; break; + default: fit->unchanged++; break; + } + return 1; } -void fg_lerp(const FgPrim *a, const FgPrim *b, double t, float x[3], float y[3]) { - for (int k = 0; k < 3; k++) { - if (t >= 1.0) { x[k] = b->x[k]; y[k] = b->y[k]; continue; } - if (t <= 0.0) { x[k] = a->x[k]; y[k] = a->y[k]; continue; } - x[k] = (float)((double)a->x[k] + ((double)b->x[k] - (double)a->x[k]) * t); - y[k] = (float)((double)a->y[k] + ((double)b->y[k] - (double)a->y[k]) * t); +/* Projection of a camera-space point (0 when behind the near plane). */ +static int proj(const double v[3], double h, double *sx, double *sy) { + if (v[2] < 1.0) return 0; + *sx = h * v[0] / v[2]; + *sy = h * v[1] / v[2]; + return 1; +} + +typedef struct { int32_t qx, qy; float dx, dy; uint32_t n; } NAcc; + +void fg_cam_place(const FgPrimList *newer, const FgCamFit *fit, const FgVert *verts, + double t, float *x, float *y, float margin[][4]) { + const uint32_t n = newer->n; + if (margin) memset(margin, 0, sizeof(float) * 4 * FG_MAX_VIEWS); + for (uint32_t j = 0; j < n; j++) + for (int k = 0; k < 3; k++) { x[3 * j + k] = newer->v[j].x[k]; y[3 * j + k] = newer->v[j].y[k]; } + if (!fit->ok || t >= 1.0) return; + if (t < 0.0) t = 0.0; + /* Per view: P_t = R_t (R^T (P - T)) + t T, R_t = the rotation's fraction t. */ + double M[FG_MAX_VIEWS][9], Rt[FG_MAX_VIEWS][9], T[FG_MAX_VIEWS][3], Tt[FG_MAX_VIEWS][3]; + for (uint32_t vi = 0; vi < fit->nviews; vi++) { + const FgView *v = &fit->v[vi]; + q_to_m(v->q, M[vi]); + const double w = fmin(1.0, fabs(v->q[0])); + const double ang = 2.0 * acos(w), s = sqrt(fmax(0.0, 1.0 - w * w)); + double qt[4] = { 1, 0, 0, 0 }; + if (s > 1e-9) { + const double sg = v->q[0] < 0 ? -1.0 : 1.0; + qt[0] = cos(0.5 * ang * t); + for (int k = 0; k < 3; k++) qt[k + 1] = sg * v->q[k + 1] / s * sin(0.5 * ang * t); + } + q_to_m(qt, Rt[vi]); + for (int k = 0; k < 3; k++) { T[vi][k] = v->t[k]; Tt[vi][k] = v->t[k] * t; } } + float *dxs = (float *)malloc((size_t)n * 3 * sizeof *dxs); + float *dys = (float *)malloc((size_t)n * 3 * sizeof *dys); + if (!dxs || !dys) { free(dxs); free(dys); return; } + for (uint32_t j = 0; j < n; j++) { + const FgPrim *pr = &newer->v[j]; + for (int k = 0; k < 3; k++) { + const uint32_t i = 3 * j + k; + dxs[i] = dys[i] = 0.0f; + const FgVert *fv = &verts[i]; + if (fv->mode != FG_PLACE_CAMERA && fv->mode != FG_PLACE_OBJECT) continue; + const double pn[3] = { pr->p[k][0], pr->p[k][1], pr->p[k][2] }; + double pt[3]; + if (fv->mode == FG_PLACE_OBJECT) { + for (int c = 0; c < 3; c++) pt[c] = fv->a[c] + (pn[c] - fv->a[c]) * t; + } else { + const int vi = fv->view; + double d[3], o[3]; + for (int c = 0; c < 3; c++) d[c] = pn[c] - T[vi][c]; + /* R^T d */ + for (int c = 0; c < 3; c++) o[c] = M[vi][c] * d[0] + M[vi][3 + c] * d[1] + M[vi][6 + c] * d[2]; + m_mul_v(Rt[vi], o, pt); + for (int c = 0; c < 3; c++) pt[c] += Tt[vi][c]; + } + double ax, ay, bx, by; + if (!proj(pn, pr->h[k], &bx, &by)) continue; + if (!proj(pt, pr->h[k], &ax, &ay)) { dxs[i] = dys[i] = NAN; continue; } /* behind the camera at t */ + dxs[i] = (float)(ax - bx); dys[i] = (float)(ay - by); + } + } + /* Neighbours: the mean motion of the placed vertices of the triangles + * that share the position (1/16 px). */ + uint32_t nn = 0; + for (uint32_t i = 0; i < n * 3u; i++) nn += verts[i].mode == FG_PLACE_NEIGHBOUR; + if (nn) { + uint32_t cap = 64; + while (cap < nn * 2u) cap <<= 1; + NAcc *tab = (NAcc *)calloc(cap, sizeof *tab); + if (tab) { + for (uint32_t j = 0; j < n; j++) { + float sx = 0, sy = 0; int c = 0; + for (int k = 0; k < 3; k++) { + const uint8_t m = verts[3 * j + k].mode; + if ((m == FG_PLACE_CAMERA || m == FG_PLACE_OBJECT) && !isnan(dxs[3 * j + k])) { + sx += dxs[3 * j + k]; sy += dys[3 * j + k]; c++; + } + } + if (!c) continue; + for (int k = 0; k < 3; k++) { + if (verts[3 * j + k].mode != FG_PLACE_NEIGHBOUR) continue; + const int32_t qx = (int32_t)lrintf(newer->v[j].x[k] * 16.0f), qy = (int32_t)lrintf(newer->v[j].y[k] * 16.0f); + uint32_t h = ((uint32_t)qx * 73856093u ^ (uint32_t)qy * 19349663u) & (cap - 1); + while (tab[h].n && (tab[h].qx != qx || tab[h].qy != qy)) h = (h + 1) & (cap - 1); + tab[h].qx = qx; tab[h].qy = qy; + tab[h].dx += sx / c; tab[h].dy += sy / c; tab[h].n++; + } + } + for (uint32_t j = 0; j < n; j++) + for (int k = 0; k < 3; k++) { + const uint32_t i = 3 * j + k; + if (verts[i].mode != FG_PLACE_NEIGHBOUR) continue; + const int32_t qx = (int32_t)lrintf(newer->v[j].x[k] * 16.0f), qy = (int32_t)lrintf(newer->v[j].y[k] * 16.0f); + uint32_t h = ((uint32_t)qx * 73856093u ^ (uint32_t)qy * 19349663u) & (cap - 1); + while (tab[h].n && (tab[h].qx != qx || tab[h].qy != qy)) h = (h + 1) & (cap - 1); + if (tab[h].n) { dxs[i] = tab[h].dx / tab[h].n; dys[i] = tab[h].dy / tab[h].n; } + } + free(tab); + } + } + /* Motion to 1/8 px: the fit's rounding error must not move an edge off + * a pixel boundary (a whole-pixel move stays whole). */ + for (uint32_t i = 0; i < n * 3u; i++) { + x[i] += roundf(dxs[i] * 8.0f) / 8.0f; + y[i] += roundf(dys[i] * 8.0f) / 8.0f; + } + /* Margins: vertices on or past a view edge that moved inward. */ + if (margin) + for (uint32_t j = 0; j < n; j++) + for (int k = 0; k < 3; k++) { + const uint32_t i = 3 * j + k; + const int vi = verts[i].view; + if (vi < 0 || verts[i].mode == FG_PLACE_UNCHANGED || isnan(dxs[i])) continue; + const float *ar = fit->v[vi].area, bx = newer->v[j].x[k], by = newer->v[j].y[k]; + /* Geometry that reached past an edge now ends this far inside it. */ + float m; + if (bx <= ar[0] + 1.0f && (m = bx + dxs[i] - ar[0]) > margin[vi][0]) margin[vi][0] = m; + if (by <= ar[1] + 1.0f && (m = by + dys[i] - ar[1]) > margin[vi][1]) margin[vi][1] = m; + if (bx >= ar[2] - 1.0f && (m = ar[2] - (bx + dxs[i])) > margin[vi][2]) margin[vi][2] = m; + if (by >= ar[3] - 1.0f && (m = ar[3] - (by + dys[i])) > margin[vi][3]) margin[vi][3] = m; + } + free(dxs); free(dys); } int fg_plan(double flip_s, double refresh_hz, double real_cost_s, diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 523599d4..38f2c0cf 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -4470,6 +4470,18 @@ static void prepare_precise_triangle(int i0, int i1, int i2, ws_native_wide_active() && psx_gpu_triangle_oversize(raw_x,raw_y,0,1,2)); } + if (gr_backend() == GR_BACKEND_OPENGL && + gl_renderer_frame_generation() != gte_fg_source_enabled()) + gte_fg_source_set(gl_renderer_frame_generation()); /* from the next projection */ + if (gr_backend() == GR_BACKEND_OPENGL && gte_fg_source_enabled()) { + /* Frame generation re-projects vertices from their camera-space source. */ + uint32_t id[3] = { 0, 0, 0 }; + int32_t pc[9] = { 0 }, hd[3] = { 0, 0, 0 }; + const int ix[3] = { i0, i1, i2 }; + for (int k = 0; k < 3; k++) + if (!gte_fg_source_lookup(gp0_cmd_buf[ix[k]], &id[k], &pc[3 * k], &hd[k])) id[k] = 0; + gl_renderer_fg_source(id, pc, hd, vx, vy); + } gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f); const int geometry = gte_geometry_correction_enabled(); if (!geometry && !s_native_wide_projection_correction) { diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 97f460e2..83e65412 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -111,6 +111,7 @@ #include #include #include "png_write.h" /* png_write_rgb — present_shot readback */ +static uint64_t s_fg_flips; /* frame generation (below): game frames seen */ #ifndef GL_BGRA #define GL_BGRA 0x80E1 @@ -446,7 +447,7 @@ enum { RTH_WIDE_DISABLE, RTH_WIDE_CLEAR, RTH_WIDE_CLEAR_MARGINS, RTH_PROJ_TRI, RTH_WIDE_RECOVERY, RTH_INTERP_SUSPENDED, RTH_PRESENT_VRAM, RTH_PRESENT_WIDE, RTH_STATE, RTH_PEEK, RTH_RING_CAPTURE, RTH_DYN_STEP, - RTH_FRAME + RTH_FRAME, RTH_FG_SRC }; static int rth_record_mode(void); /* Render-thread frame cost (dynamic resolution; defined with GL_RT_BACKEND). */ @@ -8505,7 +8506,70 @@ static void gl_swap_with_osd(void) { const char *e = getenv("PSX_PRESENT_SHOT_GENERATED"); gen_only = (e && *e && *e != '0') ? 1 : 0; } - if ((!gen_only || s_fg_presenting) && + /* PSX_PRESENT_SHOT_BURST=N (diagnostic): a staged present_shot takes + * the next N composed frames, kept in memory and written when the + * burst ends as _NN_r.png (real) / _NN_g.png (generated), in + * composition order: with frame generation a real frame is composed + * before the in-between frames that lead up to it. */ + static int burst = -1, b_i = 0, b_w = 0, b_h = 0; + static uint8_t **b_px = NULL; static char *b_kind = NULL; static uint64_t b_flip[240]; + static char b_path[512]; + if (burst < 0) { + const char *e = getenv("PSX_PRESENT_SHOT_BURST"); + burst = e ? atoi(e) : 0; + if (burst < 0) burst = 0; + if (burst > 240) burst = 240; + if (burst) { + b_px = (uint8_t **)calloc((size_t)burst, sizeof *b_px); + b_kind = (char *)calloc((size_t)burst, 1); + if (!b_px || !b_kind) burst = 0; + } + } + if (burst && b_i > 0) { + int ww = 0, wh = 0; + SDL_GL_GetDrawableSize(s_win, &ww, &wh); + if (ww == b_w && wh == b_h && (b_px[b_i] = (uint8_t *)malloc((size_t)ww * wh * 3))) { + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, ww, wh, GL_RGB, GL_UNSIGNED_BYTE, b_px[b_i]); + b_kind[b_i] = s_fg_presenting ? 'g' : 'r'; + b_flip[b_i] = s_fg_flips; + b_i++; + } else { + b_i = burst; /* resized: end it */ + } + if (b_i >= burst) { + int ok = 1; + uint8_t *flip = (uint8_t *)malloc((size_t)b_w * b_h * 3); + for (int i = 0; i < burst; i++) { + if (!b_px[i] || !flip) { ok = 0; continue; } + for (int y = 0; y < b_h; y++) + memcpy(flip + (size_t)y * b_w * 3, b_px[i] + (size_t)(b_h - 1 - y) * b_w * 3, + (size_t)b_w * 3); + char fn[600]; + snprintf(fn, sizeof fn, "%s_%02d_%c_f%llu.png", b_path, i, b_kind[i], + (unsigned long long)b_flip[i]); + FILE *pf = fopen(fn, "wb"); + if (!pf || !png_write_rgb(pf, flip, (uint32_t)b_w, (uint32_t)b_h)) ok = 0; + if (pf) fclose(pf); + free(b_px[i]); b_px[i] = NULL; + } + free(flip); + b_i = 0; + present_shot_done(ok); + } + } else if (burst && present_shot_take(shot_path, (int)sizeof(shot_path))) { + SDL_GL_GetDrawableSize(s_win, &b_w, &b_h); + snprintf(b_path, sizeof b_path, "%s", shot_path); + if (b_w > 0 && b_h > 0 && (b_px[0] = (uint8_t *)malloc((size_t)b_w * b_h * 3))) { + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, b_w, b_h, GL_RGB, GL_UNSIGNED_BYTE, b_px[0]); + b_kind[0] = s_fg_presenting ? 'g' : 'r'; + b_flip[0] = s_fg_flips; + b_i = 1; + } else { + present_shot_done(0); + } + } else if (!burst && (!gen_only || s_fg_presenting) && present_shot_take(shot_path, (int)sizeof(shot_path))) { int ww = 0, wh = 0; int wrote = 0; @@ -10013,7 +10077,7 @@ typedef struct { int wide_w, wide_off, scale, linear, force43; } FgList; /* Raw triangle captured while recording a list (absolute coords). */ -typedef struct { uint32_t rec, key0; float x[3], y[3]; int area[4]; } FgRaw; +typedef struct { uint32_t rec, key0, vid[3]; float x[3], y[3], p[3][3], h[3]; int area[4]; } FgRaw; static int s_fg_on = 0; /* configured (any thread reads) */ static int s_fg_force = 0; /* PSX_FRAME_GEN_FORCE: tests */ @@ -10033,8 +10097,24 @@ static int s_fg_have_last = 0, s_fg_last_dx = 0, s_fg_last_dy = 0; * start the next frame, so a flip is not a list boundary). */ static int s_fg_buf[4][4], s_fg_nbuf = 0, s_fg_cur_buf = -1; static int s_fg_vblanks = 0, s_fg_flip_vb = 1; /* presents per flip */ -static int32_t *s_fg_match = NULL; static uint32_t s_fg_match_cap = 0; -static FgMatchStats s_fg_mst; +/* The camera fit of the pair a schedule draws from and each newer vertex's + * placement (fg_cam_fit), positions at the current phase, and the pending + * vertex sources of the next triangle (RTH_FG_SRC: ids, integer x/y, + * camera-space x/y/z, H). */ +static FgVert *s_fg_verts = NULL; static uint32_t s_fg_verts_cap = 0; +static float *s_fg_px = NULL, *s_fg_py = NULL; static uint32_t s_fg_pos_cap = 0; +static FgCamFit s_fg_fit; +static float s_fg_margin[FG_MAX_VIEWS][4]; /* fg_cam_place, per generated frame */ +static int s_fg_src_older = 0; /* the older frame is redrawn (moving forward) */ +static uint32_t fg_main_view(const FgCamFit *f) { + uint32_t m = 0; + for (uint32_t i = 1; i < f->nviews; i++) if (f->v[i].sources > f->v[m].sources) m = i; + return m; +} +static int s_fg_src_valid = 0; static int32_t s_fg_src[21]; +static uint32_t s_fg_src_seen = 0; +static uint64_t s_fg_rejected = 0; /* flips the verdict refused */ +static const char *s_fg_reject_why = NULL; static FgBreaker s_fg_brk; static int s_fg_brk_init = 0; static uint64_t s_fg_bp_seen = 0, s_fg_bp_ns_seen = 0; @@ -10281,6 +10361,12 @@ static void fg_rotate(void) { /* Called for every replayed record (gl_rth_exec), before it executes. */ static void fg_capture(const RtCmd *c, const void *payload) { + if (c->op == RTH_FG_SRC) { + memcpy(s_fg_src, payload, sizeof s_fg_src); + s_fg_src_valid = 1; + s_fg_src_seen++; + return; + } if (!fg_op_kept(c->op)) return; /* Which buffer a draw goes to: a fill's rect, else the draw area. */ int tb = -1; @@ -10319,6 +10405,20 @@ static void fg_capture(const RtCmd *c, const void *payload) { FgRaw raw; if (fg_tri_geom(c->op, payload, s_fg_pc_valid ? s_fg_pc : NULL, raw.x, raw.y, &raw.key0)) { raw.rec = (uint32_t)r; + /* The identities belong to this triangle only if it is drawn where + * they were looked up (a culled triangle leaves its record behind). */ + for (int k = 0; k < 3; k++) raw.vid[k] = 0; + if (s_fg_src_valid) { + int at = 1; + for (int k = 0; k < 3; k++) + if (fabsf(raw.x[k] - (float)s_fg_src[3 + 2 * k]) > 1.5f || + fabsf(raw.y[k] - (float)s_fg_src[4 + 2 * k]) > 1.5f) at = 0; + if (at) for (int k = 0; k < 3; k++) { + raw.vid[k] = (uint32_t)s_fg_src[k]; + for (int c = 0; c < 3; c++) raw.p[k][c] = (float)s_fg_src[9 + 3 * k + c]; + raw.h[k] = (float)s_fg_src[18 + k]; + } + } raw.area[0] = s_area_x1; raw.area[1] = s_area_y1; raw.area[2] = s_area_x2; raw.area[3] = s_area_y2; (void)fg_raw_add(&raw); @@ -10327,6 +10427,7 @@ static void fg_capture(const RtCmd *c, const void *payload) { case RTH_FLAT_TRI: case RTH_GOURAUD_TRI: case RTH_TEX_TRI: case RTH_SHADED_TEX_TRI: case RTH_PROJ_TRI: s_fg_pc_valid = 0; + s_fg_src_valid = 0; break; default: break; } @@ -10357,6 +10458,13 @@ static void fg_list_close(FgList *l, const int disp[4]) { const int32_t aw[4] = { r->area[0] - disp[0], r->area[1] - disp[1], r->area[2] - disp[0], r->area[3] - disp[1] }; p.key = fg_hash(r->key0, aw, 4); + p.view = fg_hash(FG_HASH_INIT, aw, 4); + for (int k = 0; k < 4; k++) p.area[k] = (float)aw[k]; + for (int k = 0; k < 3; k++) { + p.vid[k] = r->vid[k]; + memcpy(p.p[k], r->p[k], sizeof p.p[k]); + p.h[k] = r->h[k]; + } for (int k = 0; k < 3; k++) { p.x[k] = r->x[k] - (float)disp[0]; p.y[k] = r->y[k] - (float)disp[1]; } if (!fg_prims_add(&l->prims, &p)) { l->valid = 0; return; } l->rec2prim[r->rec] = (int32_t)(l->prims.n - 1); @@ -10369,6 +10477,7 @@ static void fg_list_open(void) { fg_state_capture(&l->start); s_fg_nraw = 0; s_fg_pc_valid = 0; + s_fg_src_valid = 0; } static void fg_invalidate(void) { @@ -10439,6 +10548,14 @@ static void fg_surfaces_free(void) { * (most of a generated frame at high levels). The stencil is the mask-bit * mirror of alpha; the caller marks it stale and it is rebuilt from the * copied alpha only if a generated draw checks the mask. */ +static void fg_blit2(GLuint src, GLuint dst, int sx, int sy, int dx, int dy, int w, int h) { + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, dst); + glDisable(GL_SCISSOR_TEST); + p_glBlitFramebuffer(sx, sy, sx + w, sy + h, dx, dy, dx + w, dy + h, GL_COLOR_BUFFER_BIT, GL_NEAREST); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); +} static void fg_blit(GLuint src, GLuint dst, int x, int y, int w, int h) { p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src); p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, dst); @@ -10505,10 +10622,28 @@ static int fg_rect_meets(int x, int y, int w, int h, const int *disp) { /* Redraw list b at phase t (0 = a's positions, 1 = b's) into the surfaces * currently bound as hr / the displayed wide surface. */ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wide) { + (void)a; /* in-between frames redraw the newer frame only */ const int *disp = b->disp; + /* Every vertex's position at this phase (the newer frame from an + * in-between camera, frame_gen.h). */ + int placed = 0; + memset(s_fg_margin, 0, sizeof s_fg_margin); + if (s_fg_verts && s_fg_verts_cap >= b->prims.n) { + if (s_fg_pos_cap < b->prims.n) { + float *nx = (float *)realloc(s_fg_px, (size_t)b->prims.n * 3 * sizeof *nx); + if (nx) s_fg_px = nx; + float *ny = (float *)realloc(s_fg_py, (size_t)b->prims.n * 3 * sizeof *ny); + if (ny) s_fg_py = ny; + if (nx && ny) s_fg_pos_cap = b->prims.n; + } + if (s_fg_pos_cap >= b->prims.n) { + fg_cam_place(&b->prims, &s_fg_fit, s_fg_verts, t, s_fg_px, s_fg_py, s_fg_margin); + placed = 1; + } + } const float ddx = (float)(b->disp[0]), ddy = (float)(b->disp[1]); fg_state_apply(&b->start, (b->start.wide_on && b->start.wide_base == disp[0]) ? gen_wide : 0); - int pc_pending = 0; int32_t pc[7]; + int pc_pending = 0, drawn = 0; int32_t pc[7]; for (uint32_t r = 0; r < b->n; r++) { FgRec h; memcpy(&h, b->buf + b->off[r], sizeof h); @@ -10549,24 +10684,34 @@ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wid glb_wide_clear_margins(v[0], v[1], v[2], (uint16_t)v[3], v[4]); break; case RTH_FILL: - if (fg_rect_meets(v[0], v[1], v[2], v[3], disp)) + /* The clears before anything is drawn are not redrawn: the + * surface starts as the newer real frame, so what the in-between + * camera uncovers (the road below the receding near edge) shows + * that frame's pixels instead of the clear colour. Fills after + * drawing (panels, letterbox bands) are part of the picture. */ + if (drawn && fg_rect_meets(v[0], v[1], v[2], v[3], disp)) glb_fill_rect(v[0], v[1], v[2], v[3], (uint16_t)v[4]); break; case RTH_FLAT_RECT: + drawn |= in; if (in) glb_draw_flat_rect(v[0], v[1], v[2], v[3], (uint16_t)v[4]); break; case RTH_TEX_RECT: + drawn |= in; if (in) glb_draw_textured_rect(v[0], v[1], v[2], v[3], v[4], v[5], (uint16_t)v[6], (uint16_t)v[7], (uint16_t)v[8]); break; case RTH_TEX_RECT_SCALED: + drawn |= in; if (in) glb_draw_textured_rect_scaled(v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], (uint16_t)v[8], (uint16_t)v[9], (uint16_t)v[10]); break; case RTH_LINE: + drawn |= in; if (in) glb_draw_line(v[0], v[1], v[2], v[3], (uint16_t)v[4]); break; case RTH_SHADED_LINE: + drawn |= in; if (in) glb_draw_shaded_line(v[0], v[1], (uint16_t)v[2], v[3], v[4], (uint16_t)v[5]); break; case RTH_FLAT_TRI: case RTH_GOURAUD_TRI: case RTH_TEX_TRI: @@ -10576,15 +10721,23 @@ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wid pc_pending = 0; break; } - const int32_t ai = s_fg_match ? s_fg_match[pi] : -1; const FgPrim *pb = &b->prims.v[pi]; float x[3], y[3]; - if (ai >= 0 && t < 1.0) { - fg_lerp(&a->prims.v[ai], pb, t, x, y); + if (placed) { + int gone = 0; + for (int k = 0; k < 3; k++) { + x[k] = s_fg_px[3 * pi + k]; y[k] = s_fg_py[3 * pi + k]; + gone |= isnan(x[k]); + } + if (gone) { /* crossed the camera plane: not in this frame */ + pc_pending = 0; + break; + } } else { for (int k = 0; k < 3; k++) { x[k] = pb->x[k]; y[k] = pb->y[k]; } } for (int k = 0; k < 3; k++) { x[k] += ddx; y[k] += ddy; } + drawn = 1; fg_draw_tri_at(h.op, v, pl, x, y); precise_consumed(); pc_pending = 0; @@ -10668,8 +10821,16 @@ static int fg_generate(double t, int swap) { /* The displayed buffer as the real frame has it, then swap the surfaces. */ const int dy = b->disp[1] < 0 ? 0 : b->disp[1]; const int dh = b->disp[1] + b->disp[3] > VRAM_H ? VRAM_H - dy : b->disp[1] + b->disp[3] - dy; - fg_blit(s_hr_fbo, s_fg_hr_fbo, b->disp[0] * S, dy * S, b->disp[2] * S, dh * S); - if (iw >= 0) fg_blit(s_wide_fbo[iw], s_fg_w_fbo, 0, dy * S, g_wide_w * S, dh * S); + /* The redrawn frame (the older one when moving forward) is drawn where + * its own buffer is, over the newer real frame's image: what the + * in-between camera uncovers shows that. */ + const FgList *L = s_fg_src_older ? a : b; + const double tl = s_fg_src_older ? 1.0 - t : t; + const int ly = L->disp[1] < 0 ? 0 : L->disp[1]; + const int lh = L->disp[1] + L->disp[3] > VRAM_H ? VRAM_H - ly : L->disp[1] + L->disp[3] - ly; + if (L->disp[0] != b->disp[0] || lh != dh || L->disp[2] != b->disp[2]) return 0; + fg_blit2(s_hr_fbo, s_fg_hr_fbo, b->disp[0] * S, dy * S, L->disp[0] * S, ly * S, b->disp[2] * S, dh * S); + if (iw >= 0) fg_blit2(s_wide_fbo[iw], s_fg_w_fbo, 0, dy * S, 0, ly * S, g_wide_w * S, dh * S); GLuint t0 = s_hr_tex, f0 = s_hr_fbo, r0 = s_hr_rb; s_hr_tex = s_fg_hr_tex; s_hr_fbo = s_fg_hr_fbo; s_hr_rb = s_fg_hr_rb; GLuint wt = 0, wf = 0, wr = 0; int was = 0; @@ -10680,11 +10841,43 @@ static int fg_generate(double t, int swap) { } /* The copied stencil is stale: rebuilt from alpha if a draw needs it. */ s_stencil_valid = 0; - rect_add(&s_stencil_stale, b->disp[0], dy, b->disp[0] + b->disp[2] - 1, dy + dh - 1); - if (iw >= 0) wst_add(iw, 0, dy, g_wide_w, dy + dh); + rect_add(&s_stencil_stale, L->disp[0], ly, L->disp[0] + L->disp[2] - 1, ly + lh - 1); + if (iw >= 0) wst_add(iw, 0, ly, g_wide_w, ly + lh); s_fg_drawing = 1; - fg_replay(a, b, t, iw >= 0 ? s_fg_w_fbo : 0); - fg_compose(b, iw >= 0 ? s_fg_w_fbo : 0, iw >= 0 ? s_fg_w_tex : 0, b->linear, b->force43); + fg_replay(NULL, L, tl, iw >= 0 ? s_fg_w_fbo : 0); + flush_line_batch(); + flush_flat_batch(); + flush_tex_batch(); + /* The strips along a view's edges the picture moved away from: the + * newer frame drew nothing beyond the edge, so they show it. On the + * native-wide surface native x maps to x + wide_off. */ + { + const int ox = iw >= 0 ? L->wide_off : 0, sw = iw >= 0 ? g_wide_w : L->disp[2]; + const GLuint src = iw >= 0 ? wf : f0, dst = iw >= 0 ? s_fg_w_fbo : s_fg_hr_fbo; + const int bx0 = iw >= 0 ? 0 : L->disp[0], sdy = dy - ly; /* source rows: the newer frame's */ + for (uint32_t vi = 0; vi < s_fg_fit.nviews; vi++) { + const float *ar = s_fg_fit.v[vi].area, *m = s_fg_margin[vi]; + int x1 = (int)floorf(ar[0]) + ox, x2 = (int)ceilf(ar[2]) + 1 + ox; + int y1 = L->disp[1] + (int)ar[1], y2 = L->disp[1] + (int)ar[3] + 1; + if (x1 < 0) x1 = 0; + if (x2 > sw) x2 = sw; + if (y1 < ly) y1 = ly; + if (y2 > ly + lh) y2 = ly + lh; + if (x2 <= x1 || y2 <= y1) continue; + const int w = x2 - x1, h = y2 - y1; + int ml = m[0] > 0 ? (int)ceilf(m[0]) + 1 : 0, mt = m[1] > 0 ? (int)ceilf(m[1]) + 1 : 0; + int mr = m[2] > 0 ? (int)ceilf(m[2]) + 1 : 0, mb = m[3] > 0 ? (int)ceilf(m[3]) + 1 : 0; + if (ml > w) ml = w; + if (mr > w) mr = w; + if (mt > h) mt = h; + if (mb > h) mb = h; + if (ml) fg_blit2(src, dst, (bx0 + x1) * S, (y1 + sdy) * S, (bx0 + x1) * S, y1 * S, ml * S, h * S); + if (mr) fg_blit2(src, dst, (bx0 + x2 - mr) * S, (y1 + sdy) * S, (bx0 + x2 - mr) * S, y1 * S, mr * S, h * S); + if (mt) fg_blit2(src, dst, (bx0 + x1) * S, (y1 + sdy) * S, (bx0 + x1) * S, y1 * S, w * S, mt * S); + if (mb) fg_blit2(src, dst, (bx0 + x1) * S, (y2 - mb + sdy) * S, (bx0 + x1) * S, (y2 - mb) * S, w * S, mb * S); + } + } + fg_compose(L, iw >= 0 ? s_fg_w_fbo : 0, iw >= 0 ? s_fg_w_tex : 0, b->linear, b->force43); s_fg_drawing = 0; if (swap) { s_fg_presenting = 1; @@ -10995,15 +11188,45 @@ static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stal if (n <= 0) return 0; /* Match the two frames' triangles. */ const uint64_t m0 = rt_now_ns(); - if (s_fg_match_cap < B->prims.n) { - int32_t *nm = (int32_t *)realloc(s_fg_match, (size_t)B->prims.n * sizeof *nm); - if (!nm) return 0; - s_fg_match = nm; s_fg_match_cap = B->prims.n; + /* Which frame is redrawn: the one the in-between camera sees more of. + * Moving forward, the older frame seen from further on spreads past + * the screen edges (nothing missing); the newer frame seen from further + * back would shrink away from them. Otherwise the newer frame. */ + const uint32_t nmax = A->prims.n > B->prims.n ? A->prims.n : B->prims.n; + if (s_fg_verts_cap < nmax) { + FgVert *nv = (FgVert *)realloc(s_fg_verts, (size_t)nmax * 3 * sizeof *nv); + if (!nv) return 0; + s_fg_verts = nv; s_fg_verts_cap = nmax; } - FgMatchParams mp; - fg_match_defaults(&mp); - fg_match(&A->prims, &B->prims, &mp, s_fg_match, &s_fg_mst); + FgCamParams cp; + fg_cam_defaults(&cp); + FgList *src = A; + if (!fg_cam_fit(&B->prims, &A->prims, &cp, &s_fg_fit, s_fg_verts) || + s_fg_fit.v[fg_main_view(&s_fg_fit)].t[2] <= 0.0) { + src = B; + fg_cam_fit(&A->prims, &B->prims, &cp, &s_fg_fit, s_fg_verts); + } + s_fg_src_older = src == A; + /* A full-width view on the native-wide surface reaches its edges, not + * the native draw area's. */ + if (src->wide) + for (uint32_t vi = 0; vi < s_fg_fit.nviews; vi++) { + float *ar = s_fg_fit.v[vi].area; + if (ar[0] <= 0.0f && ar[2] >= (float)(src->disp[2] - 1)) { + ar[0] -= (float)src->wide_off; + ar[2] += (float)src->wide_off; + } + } s_fg_last_match_ms = (double)(rt_now_ns() - m0) * 1e-6; + /* The verdict: a frame whose correspondence is not trustworthy is not + * generated; the real frame shows at its own time. A forced run (the + * fixture's synthetic scenes carry no GTE identities) skips it. */ + if (!s_fg_fit.ok && !s_fg_force) { + s_fg_rejected++; + s_fg_reject_why = s_fg_fit.why; + s_fg_last_n = 0; + return 0; + } /* The real frame waits for the generated ones before it. */ if (s_fg_real_cap < bytes) { uint8_t *nb = (uint8_t *)realloc(s_fg_real_p, bytes); @@ -11043,6 +11266,14 @@ void gl_renderer_set_frame_generation(int on) { } int gl_renderer_frame_generation(void) { return s_fg_on; } +void gl_renderer_fg_source(const uint32_t id[3], const int32_t pc[9], const int32_t h[3], + const int32_t x[3], const int32_t y[3]) { + if (!s_fg_on || !rth_record_mode()) return; + RTH_REC(RTH_FG_SRC, 0, (int32_t)id[0], (int32_t)id[1], (int32_t)id[2], + x[0], y[0], x[1], y[1], x[2], y[2], + pc[0], pc[1], pc[2], pc[3], pc[4], pc[5], pc[6], pc[7], pc[8], h[0], h[1], h[2]); +} + void gl_renderer_frame_gen_configure(double refresh_hz, double guest_hz) { atomic_store(&s_fg_refresh_hz, refresh_hz); if (guest_hz > 1.0) atomic_store(&s_fg_guest_hz, guest_hz); @@ -11079,12 +11310,15 @@ int gl_renderer_frame_gen_json(char *out, int cap) { "\"last_n\":%d,\"slots\":%d,\"flip_vblanks\":%d,\"refresh_hz\":%.1f," "\"real_ms\":%.3f,\"gen_ms\":%.3f,\"gen_cpu_ms\":%.3f,\"gen_gpu_ms\":%.3f,\"swap_ms\":%.3f," "\"match_ms\":%.3f," - "\"prims\":%u,\"matched\":%u,\"moved\":%u,\"unmatched\":%u," + "\"prims\":%u,\"views\":%u,\"pairs\":%u,\"inliers\":%u,\"src_triangles\":%u," "\"breaker_s\":%.2f,\"trips\":%u,\"last_trip\":\"%s\",\"held_s\":%.2f," "\"last_hold\":\"%s\",\"swaps\":%llu,\"gen_samples\":%u,\"gen_cold\":%u," "\"gen_probes\":%u,\"ignored_late\":%llu,\"ignored_bp\":%llu," "\"real_cpu_ms\":%.3f,\"real_gpu_ms\":%.3f,\"ceiling\":%d," - "\"trips_late\":%u,\"trips_backed_up\":%u,\"trips_behind\":%u", + "\"trips_late\":%u,\"trips_backed_up\":%u,\"trips_behind\":%u," + "\"place_camera\":%u,\"place_object\":%u,\"place_neighbour\":%u," + "\"place_unchanged\":%u,\"cam_angle_deg\":%.3f,\"cam_shift\":%.1f," + "\"verdict_ok\":%d,\"rejected\":%llu,\"reject_why\":\"%s\"", s_fg_on, s_fg_on && s_rth_on && open, s_fg_force, (unsigned long long)s_fg_generated, (unsigned long long)s_fg_real_presents, (unsigned long long)s_fg_flips, (unsigned long long)s_fg_dups, @@ -11093,7 +11327,7 @@ int gl_renderer_frame_gen_json(char *out, int cap) { s_fg_real_ema * 1e3, fg_cost()->ema * 1e3, s_fg_gen_cpu_ms, s_fg_gen_gpu_ms, s_fg_swap_ema * 1e3, s_fg_last_match_ms, - s_fg_mst.prims, s_fg_mst.matched, s_fg_mst.moved, s_fg_mst.unmatched, + s_fg_fit.prims, s_fg_fit.nviews, s_fg_fit.v[0].pairs, s_fg_fit.v[0].inliers, s_fg_src_seen, open ? 0.0 : s_fg_brk.until - now, s_fg_brk.trips, s_fg_brk.reason ? s_fg_brk.reason : "", (double)(atomic_load(&s_fg_hold_until) > rt_now_ns() @@ -11102,7 +11336,13 @@ int gl_renderer_frame_gen_json(char *out, int cap) { fg_cost()->samples, fg_cost()->discarded, fg_cost()->probes, (unsigned long long)s_fg_ignored_late, (unsigned long long)s_fg_ignored_bp, s_fg_real_ema * 1e3, s_fg_real_gpu_ema * 1e3, fg_ceil()->cap, - s_fg_trip_late, s_fg_trip_bp, s_fg_trip_behind); + s_fg_trip_late, s_fg_trip_bp, s_fg_trip_behind, + s_fg_fit.camera, s_fg_fit.object, s_fg_fit.neighbour, s_fg_fit.unchanged, + 2.0 * acos(fmin(1.0, fabs(s_fg_fit.v[0].q[0]))) * 57.29578, + sqrt(s_fg_fit.v[0].t[0] * s_fg_fit.v[0].t[0] + s_fg_fit.v[0].t[1] * s_fg_fit.v[0].t[1] + + s_fg_fit.v[0].t[2] * s_fg_fit.v[0].t[2]), + s_fg_fit.ok, + (unsigned long long)s_fg_rejected, s_fg_reject_why ? s_fg_reject_why : ""); } /* ---- the recording vtable ------------------------------------------------ */ diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 6b2d4634..0f8f099a 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -401,6 +401,104 @@ extern "C" int gte_geometry_correction_lookup_probe(uint32_t packed, return gte_geometry_correction_lookup_impl(packed, x16, y16, 0); } +/* Frame generation's vertex sources ([video] frame_generation, + * docs/FRAME_GENERATION.md). Every RTPS/RTPT output position is noted with + * what produced it: an identity (the game function that issued the + * projection, ra, and the model-space input vertex VX/VY/VZ - the same in + * consecutive frames for the same piece of geometry, whatever the camera did + * and in whatever order the game draws), the camera-space position RT*V+TR + * the GTE divided, and the projection distance H. The GPU side looks a packet + * vertex up by its packed screen word; frame generation estimates the camera + * motion from these positions and re-projects from an in-between camera. + * Direct-indexed by screen position like the geometry cache, pointing into a + * ring of the recent projections; a position two different vertices projected + * to in one frame is ambiguous. Entries a few frames old are not believed. + * Allocated only while frame generation is on: the guest-visible GTE results + * are untouched either way. */ +struct FgSrcEntry { uint32_t seq; uint32_t gen; }; /* gen: frame << 1 | ambiguous */ +struct FgSrcRec { uint32_t seq, id; int32_t p[3]; int32_t h; }; +#define FGSRC_RING (1u << 17) +static FgSrcEntry *s_fgsrc = nullptr; +static FgSrcRec *s_fgsrc_ring = nullptr; +static uint32_t s_fgsrc_seq = 0; +#define FGSRC_MAX_AGE 4u +extern "C" { extern uint64_t s_frame_count; } +static int32_t gte_h_scaled(const GTEState* gte); + +extern "C" void gte_fg_source_set(int enabled) { + if (enabled && !s_fgsrc) { + s_fgsrc = (FgSrcEntry *)std::calloc(GEOM_CACHE_SIZE, sizeof(FgSrcEntry)); + s_fgsrc_ring = (FgSrcRec *)std::calloc(FGSRC_RING, sizeof(FgSrcRec)); + if (!s_fgsrc || !s_fgsrc_ring) enabled = 0; + } + if (!enabled) { + std::free(s_fgsrc); s_fgsrc = nullptr; + std::free(s_fgsrc_ring); s_fgsrc_ring = nullptr; + } +} + +extern "C" int gte_fg_source_enabled(void) { return s_fgsrc != nullptr; } + +static inline uint32_t fgsrc_frame(void) { return (uint32_t)(s_frame_count + 1u) & 0x7FFFFFFFu; } + +static void fgsrc_note(const GTEState *g, int32_t packed, uint32_t ra, const int16_t v[3]) { + const int64_t slot = geom_slot((uint32_t)packed); + if (slot < 0) return; + uint32_t h = 2166136261u; + const uint32_t w[4] = { ra, (uint16_t)v[0], (uint16_t)v[1], (uint16_t)v[2] }; + for (int i = 0; i < 4; i++) + for (int k = 0; k < 4; k++) h = (h ^ ((w[i] >> (8 * k)) & 0xFFu)) * 16777619u; + if (!h) h = 1; + const uint32_t seq = ++s_fgsrc_seq ? s_fgsrc_seq : ++s_fgsrc_seq; + FgSrcRec &r = s_fgsrc_ring[seq & (FGSRC_RING - 1u)]; + r.seq = seq; + r.id = h; + for (int i = 0; i < 3; i++) { + int64_t m = (int64_t)g->TR[i] * 4096; + for (int k = 0; k < 3; k++) m += (int64_t)g->RT[i][k] * v[k]; + r.p[i] = (int32_t)(m >> 12); + } + r.h = gte_h_scaled(g); + FgSrcEntry &e = s_fgsrc[slot]; + const uint32_t f = fgsrc_frame(); + if ((e.gen >> 1) == f && e.seq && s_fgsrc_ring[e.seq & (FGSRC_RING - 1u)].seq == e.seq && + s_fgsrc_ring[e.seq & (FGSRC_RING - 1u)].id != h) { + e.gen |= 1u; /* two vertices, one pixel */ + return; + } + if ((e.gen >> 1) != f) e.gen = f << 1; + e.seq = seq; +} + +static void fgsrc_record(const GTEState *g, uint32_t cmd, uint32_t ra) { + ra &= 0x1FFFFFFFu; + if ((cmd & 0x3F) == 0x30) { + fgsrc_note(g, g->SXY[0], ra, g->V0); + fgsrc_note(g, g->SXY[1], ra, g->V1); + fgsrc_note(g, g->SXY[2], ra, g->V2); + } else { + fgsrc_note(g, g->SXY[2], ra, g->V0); + } +} + +/* The source of the vertex last projected to a packed screen word: 0 when + * none, ambiguous or stale; else 1 with its identity, camera-space position + * and projection distance. */ +extern "C" int gte_fg_source_lookup(uint32_t packed, uint32_t *id, int32_t p[3], int32_t *hdist) { + if (!s_fgsrc) return 0; + const int64_t slot = geom_slot(packed); + if (slot < 0) return 0; + const FgSrcEntry &e = s_fgsrc[slot]; + if (!e.seq || (e.gen & 1u)) return 0; + if (((fgsrc_frame() - (e.gen >> 1)) & 0x7FFFFFFFu) > FGSRC_MAX_AGE) return 0; + const FgSrcRec &r = s_fgsrc_ring[e.seq & (FGSRC_RING - 1u)]; + if (r.seq != e.seq) return 0; + *id = r.id; + p[0] = r.p[0]; p[1] = r.p[1]; p[2] = r.p[2]; + *hdist = r.h; + return 1; +} + /* Render-pass checkpoint for the gte.cpp side of precision tracking: the * position cache entries a pass writes are journaled and put back, and the * speculative bracket depth is restored (a watchdog abort can leave one open). @@ -2255,6 +2353,8 @@ extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) { #endif gte_run_command(>e, cmd); pgxp_gte_op_end(func); /* IR / MAC scalars the op overwrote */ + if (s_fgsrc && !s_gte_replay_sandbox && (func == 0x01 || func == 0x30)) + fgsrc_record(>e, cmd, cpu->gpr[31]); #ifndef PSX_NO_DEBUG_TOOLS if (func == 0x01 || func == 0x30) gte_rtp_record(>e, cmd); diff --git a/runtime/tests/test_frame_gen.c b/runtime/tests/test_frame_gen.c index eed76216..6767ceae 100644 --- a/runtime/tests/test_frame_gen.c +++ b/runtime/tests/test_frame_gen.c @@ -15,96 +15,182 @@ static void check(int ok, const char *what) { if (!ok) { failures++; fprintf(stderr, "FAIL: %s\n", what); } } -static FgPrim tri(uint32_t key, float x, float y, float s) { - FgPrim p; - memset(&p, 0, sizeof p); - p.key = key; - p.x[0] = x; p.y[0] = y; - p.x[1] = x + s; p.y[1] = y; - p.x[2] = x; p.y[2] = y + s; - return p; +/* ---- a synthetic 3D scene seen by a moving camera ---- + * World points on a grandstand facade (one repeated texture: every triangle + * has the same key), a camera at eye E with yaw Y (world -> camera: rotate by + * -Y about the vertical axis, after subtracting E), projection H / z. */ +#define GW 16 +#define GH 6 +#define HH 300.0 +typedef struct { double eye[3], yaw; } Cam; + +static void to_cam(const Cam *c, const double w[3], double o[3]) { + const double d[3] = { w[0] - c->eye[0], w[1] - c->eye[1], w[2] - c->eye[2] }; + const double cs = cos(c->yaw), sn = sin(c->yaw); + o[0] = cs * d[0] - sn * d[2]; + o[1] = d[1]; + o[2] = sn * d[0] + cs * d[2]; +} + +static void wpt(int r, int col, double w[3]) { + w[0] = -400.0 + col * 50.0; w[1] = -100.0 + r * 40.0; w[2] = 2000.0 + col * 30.0; +} + +static uint32_t vid_of(int r, int col) { return (uint32_t)(r * (GW + 1) + col) + 1; } + +static void vtx(FgPrim *p, int k, const Cam *c, int r, int col, uint32_t id) { + double w[3], q[3]; + wpt(r, col, w); + to_cam(c, w, q); + p->vid[k] = id; + for (int i = 0; i < 3; i++) p->p[k][i] = (float)q[i]; + p->h[k] = (float)HH; + p->x[k] = (float)(160.0 + HH * q[0] / q[2]); + p->y[k] = (float)(120.0 + HH * q[1] / q[2]); } static void add(FgPrimList *l, FgPrim p) { p.rec = l->n; check(fg_prims_add(l, &p), "add"); } -static void test_identical_and_moved(void) { +/* Rows r0..r1-1, bottom first when `reverse`. */ +static void stand(FgPrimList *l, const Cam *c, int r0, int r1, int reverse) { + for (int ri = r0; ri < r1; ri++) { + const int r = reverse ? r1 - 1 - (ri - r0) : ri; + for (int col = 0; col < GW; col++) { + FgPrim p; + memset(&p, 0, sizeof p); + p.key = 7; p.view = 1; + vtx(&p, 0, c, r, col, vid_of(r, col)); + vtx(&p, 1, c, r, col + 1, vid_of(r, col + 1)); + vtx(&p, 2, c, r + 1, col, vid_of(r + 1, col)); + add(l, p); + vtx(&p, 0, c, r, col + 1, vid_of(r, col + 1)); + vtx(&p, 1, c, r + 1, col + 1, vid_of(r + 1, col + 1)); + vtx(&p, 2, c, r + 1, col, vid_of(r + 1, col)); + add(l, p); + } + } +} + +/* Worst screen distance between where a newer vertex is placed at phase t + * and where camera c shows its world point. */ +static float place_error(const FgPrimList *b, const float *x, const float *y, const Cam *c) { + float worst = 0.0f; + for (uint32_t j = 0; j < b->n; j++) + for (int k = 0; k < 3; k++) { + const uint32_t id = b->v[j].vid[k] - 1; + double w[3], q[3]; + wpt((int)(id / (GW + 1)), (int)(id % (GW + 1)), w); + to_cam(c, w, q); + const float ex = x[3 * j + k] - (float)(160.0 + HH * q[0] / q[2]); + const float ey = y[3 * j + k] - (float)(120.0 + HH * q[1] / q[2]); + const float e = sqrtf(ex * ex + ey * ey); + if (e > worst) worst = e; + } + return worst; +} + +/* The grandstand regression: the camera pans and travels, the newer frame + * draws the rows in another order with the top row culled and a new row in. + * In-between frames are the newer frame from an in-between camera: at phase + * 0 every vertex lands where the older camera saw it, at 0.5 where the + * halfway camera does. */ +static void test_camera_reordered(void) { + const Cam co = { { 0, 0, 0 }, 0.00 }, cn = { { 30, 0, 60 }, 0.04 }, cm = { { 15, 0, 30 }, 0.02 }; FgPrimList a = { 0 }, b = { 0 }; - for (int i = 0; i < 200; i++) add(&a, tri((uint32_t)(i % 7) + 1, (float)(i % 20) * 16.0f, (float)(i / 20) * 16.0f, 12.0f)); - for (int i = 0; i < 200; i++) add(&b, tri((uint32_t)(i % 7) + 1, (float)(i % 20) * 16.0f + 3.0f, (float)(i / 20) * 16.0f - 2.0f, 12.0f)); - FgMatchParams mp; - fg_match_defaults(&mp); - int32_t *m = (int32_t *)malloc(sizeof(int32_t) * b.n); - FgMatchStats st; - uint32_t got = fg_match(&a, &b, &mp, m, &st); - check(got == 200 && st.matched == 200 && st.unmatched == 0, "a moved scene matches every triangle"); - int pairs_ok = 1; - for (uint32_t i = 0; i < b.n; i++) if (m[i] != (int32_t)i) pairs_ok = 0; - check(pairs_ok, "each triangle pairs with itself (draw order, nearest)"); - /* Same positions: zero moves. */ - got = fg_match(&a, &a, &mp, m, &st); - check(got == 200 && st.moved == 0, "an unchanged scene matches without motion"); - free(m); + stand(&a, &co, 0, GH - 1, 0); + stand(&b, &cn, 1, GH, 1); + FgCamParams cp; + fg_cam_defaults(&cp); + FgVert *v = (FgVert *)malloc(sizeof(FgVert) * b.n * 3); + float *x = (float *)malloc(sizeof(float) * b.n * 3), *y = (float *)malloc(sizeof(float) * b.n * 3); + FgCamFit fit; + check(fg_cam_fit(&a, &b, &cp, &fit, v) == 1 && fit.ok, "the reordered stand gets a camera"); + check(fit.camera == b.n * 3 && fit.v[0].inliers == fit.v[0].pairs, "every vertex placed by the camera"); + fg_cam_place(&b, &fit, v, 0.0, x, y, NULL); + check(place_error(&b, x, y, &co) < 0.1f, "phase 0: where the older camera saw them (new row too)"); + fg_cam_place(&b, &fit, v, 0.5, x, y, NULL); + check(place_error(&b, x, y, &cm) < 0.5f, "phase 0.5: where the halfway camera sees them"); + fg_cam_place(&b, &fit, v, 1.0, x, y, NULL); + check(place_error(&b, x, y, &cn) < 1e-3f, "phase 1: the newer frame exactly"); + free(v); free(x); free(y); fg_prims_free(&a); fg_prims_free(&b); } -static void test_keys_limits_and_duplicates(void) { +/* A car the camera follows (the same camera-space position in both frames) + * stays where it is; a HUD triangle (no projection) is not moved; a vertex + * without a projection in a world triangle moves with its neighbours. */ +static void test_objects_hud_neighbours(void) { + const Cam co = { { 0, 0, 0 }, 0.0 }, cn = { { 0, 0, 80 }, 0.03 }; FgPrimList a = { 0 }, b = { 0 }; - FgMatchParams mp; - fg_match_defaults(&mp); - /* 0: different key never pairs. 1: moved past max_move. 2: deformed. - * 3/4: two copies of one key; the newer ones swapped in draw order but - * each still nearest its own. */ - add(&a, tri(10, 0, 0, 10)); - add(&a, tri(11, 0, 0, 10)); - add(&a, tri(12, 0, 0, 10)); - add(&a, tri(13, 100, 100, 10)); - add(&a, tri(13, 200, 100, 10)); - add(&b, tri(99, 0, 0, 10)); - add(&b, tri(11, mp.max_move + 10.0f, 0, 10)); - FgPrim d = tri(12, 0, 0, 10); - d.x[1] += mp.max_deform + 8.0f; /* one vertex moves alone */ - add(&b, d); - add(&b, tri(13, 204, 101, 10)); - add(&b, tri(13, 103, 99, 10)); - int32_t m[5]; - FgMatchStats st; - fg_match(&a, &b, &mp, m, &st); - check(m[0] == -1, "a different key does not pair"); - check(m[1] == -1, "a move past max_move does not pair (placed, not moved)"); - check(m[2] == -1, "a triangle whose vertices moved apart does not pair"); - check(m[3] == 4 && m[4] == 3, "duplicate keys pair by distance"); - check(st.matched == 2 && st.unmatched == 3, "match stats"); + stand(&a, &co, 0, GH, 0); + stand(&b, &cn, 0, GH, 0); + FgPrim car; + memset(&car, 0, sizeof car); + car.key = 9; car.view = 1; + const float cpos[3][3] = { { -50, 60, 600 }, { 50, 60, 600 }, { 0, 20, 650 } }; + for (int k = 0; k < 3; k++) { + car.vid[k] = 900000u + (uint32_t)k; + memcpy(car.p[k], cpos[k], sizeof car.p[k]); + car.h[k] = (float)HH; + car.x[k] = (float)(160.0 + HH * cpos[k][0] / cpos[k][2]); + car.y[k] = (float)(120.0 + HH * cpos[k][1] / cpos[k][2]); + } + add(&a, car); add(&b, car); + FgPrim hud; + memset(&hud, 0, sizeof hud); + hud.key = 3; hud.view = 2; + hud.x[0] = 10; hud.y[0] = 10; hud.x[1] = 40; hud.y[1] = 10; hud.x[2] = 10; hud.y[2] = 30; + add(&b, hud); + FgPrim cpu = b.v[0]; /* a CPU-built vertex in a world triangle */ + cpu.vid[2] = 0; + add(&b, cpu); + FgCamParams cp; + fg_cam_defaults(&cp); + FgVert *v = (FgVert *)malloc(sizeof(FgVert) * b.n * 3); + float *x = (float *)malloc(sizeof(float) * b.n * 3), *y = (float *)malloc(sizeof(float) * b.n * 3); + FgCamFit fit; + check(fg_cam_fit(&a, &b, &cp, &fit, v) && fit.object == 3, "the followed car is an object"); + fg_cam_place(&b, &fit, v, 0.0, x, y, NULL); + const uint32_t ic = 3 * (b.n - 3), ih = 3 * (b.n - 2), iu = 3 * (b.n - 1); + check(fabsf(x[ic] - car.x[0]) < 1e-3f && fabsf(y[ic + 2] - car.y[2]) < 1e-3f, "the followed car stays put"); + check(x[ih] == hud.x[0] && y[ih + 2] == hud.y[2], "the HUD is not moved"); + check(v[iu + 2].mode == FG_PLACE_NEIGHBOUR && fabsf(x[iu + 2] - b.v[b.n - 1].x[2]) > 0.5f, + "a CPU-built vertex moves with its triangle"); + free(v); free(x); free(y); fg_prims_free(&a); fg_prims_free(&b); } -static void test_each_older_pairs_once(void) { +static void test_verdict(void) { + const Cam co = { { 0, 0, 0 }, 0.0 }, cn = { { 0, 0, 40 }, 0.02 }, far = { { 0, 0, 0 }, 0.9 }; + FgCamParams cp; + fg_cam_defaults(&cp); FgPrimList a = { 0 }, b = { 0 }; - add(&a, tri(5, 0, 0, 10)); - add(&b, tri(5, 0, 0, 10)); - add(&b, tri(5, 1, 0, 10)); - FgMatchParams mp; - fg_match_defaults(&mp); - int32_t m[2]; - fg_match(&a, &b, &mp, m, NULL); - check(m[0] == 0 && m[1] == -1, "an older triangle pairs at most once"); - /* Empty older list: nothing pairs. */ - FgPrimList e = { 0 }; - fg_match(&e, &b, &mp, m, NULL); - check(m[0] == -1 && m[1] == -1, "no history: nothing pairs"); + FgVert v[3 * GW * 2 * GH + 64]; + FgCamFit fit; + /* No projections (a 2D screen). */ + for (int i = 0; i < 40; i++) { + FgPrim p; memset(&p, 0, sizeof p); p.key = 1; p.x[1] = (float)i; add(&a, p); add(&b, p); + } + check(!fg_cam_fit(&a, &b, &cp, &fit, v) && fit.why && strstr(fit.why, "no projections"), "no projections: rejected"); + fg_prims_reset(&a); fg_prims_reset(&b); + /* A cut: other geometry entirely. */ + stand(&a, &co, 0, GH, 0); + stand(&b, &cn, 0, GH, 0); + for (uint32_t j = 0; j < b.n; j++) for (int k = 0; k < 3; k++) b.v[j].vid[k] += 100000u; + check(!fg_cam_fit(&a, &b, &cp, &fit, v) && fit.why && strstr(fit.why, "few pairs"), "a cut: rejected"); + fg_prims_reset(&b); + /* A turn no camera makes between two frames. */ + stand(&b, &far, 0, GH, 0); + check(!fg_cam_fit(&a, &b, &cp, &fit, v), "an implausible turn: rejected"); + fg_prims_reset(&b); + /* Half the identities moved independently (wrong lookups): no consistent camera. */ + stand(&b, &cn, 0, GH, 0); + for (uint32_t j = 0; j < b.n; j += 2) + for (int k = 0; k < 3; k++) { b.v[j].p[k][0] += 300.0f * (float)((j * 7 + k) % 5); b.v[j].p[k][2] += 150.0f; } + check(!fg_cam_fit(&a, &b, &cp, &fit, v), "inconsistent motion: rejected"); fg_prims_free(&a); fg_prims_free(&b); } -static void test_lerp(void) { - FgPrim a = tri(1, 10, 20, 8), b = tri(1, 30, 10, 8); - float x[3], y[3]; - fg_lerp(&a, &b, 0.0, x, y); - check(x[0] == a.x[0] && y[2] == a.y[2], "phase 0 is the older frame exactly"); - fg_lerp(&a, &b, 1.0, x, y); - check(x[1] == b.x[1] && y[1] == b.y[1], "phase 1 is the newer frame exactly"); - fg_lerp(&a, &b, 0.25, x, y); - check(fabsf(x[0] - 15.0f) < 1e-5f && fabsf(y[0] - 17.5f) < 1e-5f, "phase 0.25"); -} - static void test_plan(void) { const double f30 = 2.0 / 59.94, f60 = 1.0 / 59.94; /* 30 Hz game frames on a 120 Hz panel: 4 slots, 3 in-between. */ @@ -231,10 +317,9 @@ int main(void) { test_ceiling(); test_cost(); test_pace(); - test_identical_and_moved(); - test_keys_limits_and_duplicates(); - test_each_older_pairs_once(); - test_lerp(); + test_camera_reordered(); + test_objects_hud_neighbours(); + test_verdict(); test_plan(); test_breaker(); printf("frame_gen_test: checks=%d failures=%d\n", checks, failures); diff --git a/runtime/tests/test_gl_frame_gen.c b/runtime/tests/test_gl_frame_gen.c index 459d5d58..1f45cd7b 100644 --- a/runtime/tests/test_gl_frame_gen.c +++ b/runtime/tests/test_gl_frame_gen.c @@ -5,7 +5,9 @@ * 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). + * 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). @@ -173,6 +175,29 @@ static void textures(void) { } static int wide_mode; +/* 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); +} + /* 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) { @@ -187,9 +212,12 @@ static void game_frame_part(int g, int bx, int part) { 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++) { - int x = bx + 10 + (i % 8) * 36 + ((i * 3 + g * (1 + i % 3)) % 9) - 4; - int y = 12 + (i / 8) * 42 + (g * (i % 2 ? 1 : -1)) % 5; + /* 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) * 36 + (i == 5 ? (g * 3) % 9 : g % 9) - 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; @@ -306,10 +334,20 @@ int main(int argc, char **argv) { 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); - long diff = 0; - for (int i = 0; i < ww * wh; i++) if (img[i] != img_last[0][i]) diff++; - printf("phase0_diff_px=%ld matched=%u/%u\n", diff, s_fg_mst.matched, s_fg_mst.prims); - check(d0 == r0, "phase 0 equals the older real frame"); + /* 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 && s_fg_fit.camera + s_fg_fit.object == 120, "every projected vertex placed"); + check(neither * 1000 < (long)ww * wh, "phase 0: the older real frame, uncovered areas the newer (<0.1%)"); check(fg_generate(0.5, 0) == 1, "compose at phase 0.5"); uint64_t dm = read_back(img); if (getenv("FG_DUMP")) {