Frame generation: in-between frames from an in-between camera

Pairing triangles of consecutive draw lists by key and draw order paired
the wrong ones whenever the game reordered, culled or switched LODs (R4:
a grandstand of one repeated texture turned into a staircase; on a
synthetic version 225 of 226 pairs were wrong). No primitive is paired by
draw order any more.

gte.cpp notes each RTPS/RTPT output (only while generation is on) with an
identity (issuing ra + model-space vertex), its camera-space position and
H, indexed by screen word; gpu.c records each polygon vertex's source
ahead of the triangle (RTH_FG_SRC, never drawn). Per view the camera
motion between the two frames is fitted from the paired camera-space
vertices (RANSAC + least squares); one real frame is redrawn from the
in-between camera: world vertices re-projected, vertices moving against
the world (cars) lerped in camera space, CPU-built vertices moved with
their triangle, HUD/2D unchanged. Moving forward the older frame is
redrawn; uncovered areas show the newer real frame. A verdict rejects
the generated frame (the real one shows) without projections, with few
pairs/inliers, or with a motion no camera makes. Real frames are
unchanged (gl_frame_gen_test).

PSX_PRESENT_SHOT_BURST=N captures the next N composed frames, real and
generated, for checking in-between frames against their neighbours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-06 19:28:44 -07:00
parent 89ae676068
commit 3de80c2f20
10 changed files with 1184 additions and 252 deletions
+45 -19
View File
@@ -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
`<path>_NN_{r|g}_f<flip>.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
+4
View File
@@ -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);
+85 -25
View File
@@ -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 <stddef.h>
#include <stdint.h>
@@ -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)
+6
View File
@@ -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);
+457 -96
View File
@@ -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,
+12
View File
@@ -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) {
+267 -27
View File
@@ -111,6 +111,7 @@
#include <string.h>
#include <stdatomic.h>
#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 <path>_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 ------------------------------------------------ */
+100
View File
@@ -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(&gte, 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(&gte, cmd, cpu->gpr[31]);
#ifndef PSX_NO_DEBUG_TOOLS
if (func == 0x01 || func == 0x30) gte_rtp_record(&gte, cmd);
+163 -78
View File
@@ -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);
+45 -7
View File
@@ -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")) {