Frame generation: close cracks, skip frames the camera passes through

Corner and tunnel runs showed light cracks along seams and near the view
edge in generated frames:
- weld: every placed vertex at one screen position of a view moves alike
  (the same corner arrives from several projections);
- a vertex without a projection on an edge between placed vertices moves
  as that point of the edge (T-junctions from the game's polygon splits);
- a vertex the in-between camera passes is projected at a clamped depth,
  and such a game frame is not generated (no near-plane clipping: tunnel
  ceilings, overhead bridges);
- debug: "clamped", "guessed" (CPU-built vertices placed by a triangle
  mean over a >1 px motion spread).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-06 19:44:34 -07:00
parent 3de80c2f20
commit 83bc766d13
4 changed files with 151 additions and 11 deletions
+10 -2
View File
@@ -55,7 +55,13 @@ has measured time to spare.
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
hole. Every placed vertex at one screen position moves alike (a weld: the same
corner reaches the GPU from several projections), and a vertex without a
projection lying on an edge between placed vertices moves as that point of
the edge (closes the T-junctions the game's polygon splits leave). A game
frame whose in-between camera would pass through geometry (a vertex behind
it: tunnel ceilings, overhead bridges) is not generated: without near-plane
clipping its image would be wrong. 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
@@ -90,7 +96,9 @@ has measured time to spare.
- 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).
frame. Vertices without a projection that are neither on a placed edge nor
shared take their triangle's mean motion (`guessed` counts those over a
1 px spread); near R4's tunnel walls this still opens thin cracks.
- 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.
+6 -3
View File
@@ -97,6 +97,9 @@ typedef struct FgCamFit {
int ok;
const char *why;
uint32_t nviews, prims, camera, object, neighbour, unchanged;
uint32_t clamped; /* fg_cam_place: vertices the in-between camera passes (frame rejected) */
uint32_t guessed; /* fg_cam_place: vertices without a projection placed by a triangle
* mean over a motion that varies by more than 1 px */
FgView v[FG_MAX_VIEWS];
} FgCamFit;
@@ -115,13 +118,13 @@ int fg_cam_fit(const FgPrimList *older, const FgPrimList *newer, const FgCamPara
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
* camera, 1 = the newer frame exactly): x/y[newer->n * 3]; a vertex the
* in-between camera is passing is projected at a clamped depth. 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,
void fg_cam_place(const FgPrimList *newer, FgCamFit *fit, const FgVert *verts,
double t, float *x, float *y, float margin[][4]);
/* How many in-between frames to draw per game frame.
+119 -4
View File
@@ -397,7 +397,7 @@ static int proj(const double v[3], double h, double *sx, double *sy) {
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,
void fg_cam_place(const FgPrimList *newer, 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);
@@ -421,6 +421,7 @@ void fg_cam_place(const FgPrimList *newer, const FgCamFit *fit, const FgVert *ve
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; }
}
uint32_t clamped = 0, guessed = 0;
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; }
@@ -446,10 +447,79 @@ void fg_cam_place(const FgPrimList *newer, const FgCamFit *fit, const FgVert *ve
}
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 */
/* Near geometry the in-between camera is passing: projected at a
* clamped depth. A function of the vertex alone, so triangles
* sharing it stay closed (no cracks onto what is behind). */
const double zmin = fmax(16.0, 0.25 * pn[2]);
if (pt[2] < zmin) { pt[2] = zmin; clamped++; }
if (!proj(pt, pr->h[k], &ax, &ay)) continue;
dxs[i] = (float)(ax - bx); dys[i] = (float)(ay - by);
}
}
/* A vertex without a projection lying on an edge between two placed
* vertices (the game split a polygon there: near-plane clipping,
* subdivision) moves as that point of the edge does, so the T-junction
* stays closed. */
uint8_t *done = (uint8_t *)calloc((size_t)n * 3, 1);
{
uint32_t ne = 0;
for (uint32_t i = 0; i < n * 3u; i++) ne += verts[i].mode == FG_PLACE_NEIGHBOUR;
if (ne && done) {
enum { G = 16 }; /* grid cell, native px */
int32_t gx0 = INT32_MAX, gy0 = INT32_MAX, gx1 = INT32_MIN, gy1 = INT32_MIN;
for (uint32_t i = 0; i < n * 3u; i++) {
const int32_t cx = (int32_t)floorf(newer->v[i / 3].x[i % 3] / G), cy = (int32_t)floorf(newer->v[i / 3].y[i % 3] / G);
if (cx < gx0) gx0 = cx; if (cx > gx1) gx1 = cx; if (cy < gy0) gy0 = cy; if (cy > gy1) gy1 = cy;
}
if (gx0 < -64) gx0 = -64; if (gy0 < -64) gy0 = -64; if (gx1 > 128) gx1 = 128; if (gy1 > 128) gy1 = 128;
const int32_t gw = gx1 - gx0 + 1, gh = gy1 - gy0 + 1;
/* Cells hold neighbour vertices; edges look them up. */
int32_t *head = gw > 0 && gh > 0 ? (int32_t *)malloc((size_t)gw * gh * sizeof *head) : NULL;
int32_t *nxt = (int32_t *)malloc((size_t)n * 3 * sizeof *nxt);
float *best = (float *)malloc((size_t)n * 3 * sizeof *best);
if (head && nxt && best) {
for (int32_t c = 0; c < gw * gh; c++) head[c] = -1;
for (uint32_t i = 0; i < n * 3u; i++) {
best[i] = 0.75f;
if (verts[i].mode != FG_PLACE_NEIGHBOUR) continue;
const int32_t cx = (int32_t)floorf(newer->v[i / 3].x[i % 3] / G) - gx0;
const int32_t cy = (int32_t)floorf(newer->v[i / 3].y[i % 3] / G) - gy0;
if (cx < 0 || cy < 0 || cx >= gw || cy >= gh) continue;
nxt[i] = head[cy * gw + cx]; head[cy * gw + cx] = (int32_t)i;
}
for (uint32_t j = 0; j < n; j++) {
const FgPrim *pr = &newer->v[j];
for (int e = 0; e < 3; e++) {
const uint32_t i0 = 3 * j + e, i1 = 3 * j + (e + 1) % 3;
const uint8_t m0 = verts[i0].mode, m1 = verts[i1].mode;
if ((m0 != FG_PLACE_CAMERA && m0 != FG_PLACE_OBJECT) ||
(m1 != FG_PLACE_CAMERA && m1 != FG_PLACE_OBJECT)) continue;
const float ax = pr->x[e], ay = pr->y[e], bx = pr->x[(e + 1) % 3], by = pr->y[(e + 1) % 3];
const float ex = bx - ax, ey = by - ay, l2 = ex * ex + ey * ey;
if (l2 < 1.0f) continue;
int32_t cx0 = (int32_t)floorf(fminf(ax, bx) / G) - gx0, cx1 = (int32_t)floorf(fmaxf(ax, bx) / G) - gx0;
int32_t cy0 = (int32_t)floorf(fminf(ay, by) / G) - gy0, cy1 = (int32_t)floorf(fmaxf(ay, by) / G) - gy0;
if (cx0 < 0) cx0 = 0; if (cy0 < 0) cy0 = 0; if (cx1 >= gw) cx1 = gw - 1; if (cy1 >= gh) cy1 = gh - 1;
if ((int64_t)(cx1 - cx0 + 1) * (cy1 - cy0 + 1) > 4096) continue;
for (int32_t cy = cy0; cy <= cy1; cy++)
for (int32_t cx = cx0; cx <= cx1; cx++)
for (int32_t v = head[cy * gw + cx]; v >= 0; v = nxt[v]) {
const float px = newer->v[v / 3].x[v % 3], py = newer->v[v / 3].y[v % 3];
const float u = ((px - ax) * ex + (py - ay) * ey) / l2;
if (u <= 0.0f || u >= 1.0f) continue;
const float dx = ax + u * ex - px, dy = ay + u * ey - py, d = sqrtf(dx * dx + dy * dy);
if (d >= best[v]) continue;
best[v] = d;
dxs[v] = dxs[i0] + (dxs[i1] - dxs[i0]) * u;
dys[v] = dys[i0] + (dys[i1] - dys[i0]) * u;
done[v] = 1;
}
}
}
}
free(head); free(nxt); free(best);
}
}
/* Neighbours: the mean motion of the placed vertices of the triangles
* that share the position (1/16 px). */
uint32_t nn = 0;
@@ -468,8 +538,19 @@ void fg_cam_place(const FgPrimList *newer, const FgCamFit *fit, const FgVert *ve
}
}
if (!c) continue;
/* The mean stands in for a motion that varies across the
* triangle with depth; where it varies by more than a pixel
* the guess opens cracks (near walls split by the game). */
float spread = 0;
for (int k = 0; k < 3; k++) {
if (verts[3 * j + k].mode != FG_PLACE_NEIGHBOUR) continue;
const uint8_t m = verts[3 * j + k].mode;
if (m != FG_PLACE_CAMERA && m != FG_PLACE_OBJECT) continue;
const float ex = dxs[3 * j + k] - sx / c, ey = dys[3 * j + k] - sy / c;
if (ex * ex + ey * ey > spread) spread = ex * ex + ey * ey;
}
for (int k = 0; k < 3; k++) {
if (verts[3 * j + k].mode != FG_PLACE_NEIGHBOUR || (done && done[3 * j + k])) continue;
if (spread > 1.0f) guessed++;
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);
@@ -480,7 +561,7 @@ void fg_cam_place(const FgPrimList *newer, const FgCamFit *fit, const FgVert *ve
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;
if (verts[i].mode != FG_PLACE_NEIGHBOUR || (done && done[i])) 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);
@@ -489,6 +570,40 @@ void fg_cam_place(const FgPrimList *newer, const FgCamFit *fit, const FgVert *ve
free(tab);
}
}
/* Weld: every placed vertex at one screen position (1/16 px) of one view
* moves alike. The same corner reaches the GPU from several projections
* (another function, another model vertex, an object and the world); if
* they moved apart, the shared edge would crack open onto whatever was
* drawn behind it (sky through a tunnel ceiling). */
{
uint32_t cap = 64;
while (cap < n * 6u) cap <<= 1;
NAcc *tab = (NAcc *)calloc(cap, sizeof *tab);
uint32_t *slot = (uint32_t *)malloc((size_t)n * 3 * sizeof *slot);
if (tab && slot) {
for (uint32_t i = 0; i < n * 3u; i++) {
slot[i] = UINT32_MAX;
if (verts[i].mode == FG_PLACE_UNCHANGED) continue;
const FgPrim *pr = &newer->v[i / 3];
const int32_t qx = (int32_t)lrintf(pr->x[i % 3] * 16.0f) ^ (verts[i].view << 24);
const int32_t qy = (int32_t)lrintf(pr->y[i % 3] * 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 += dxs[i]; tab[h].dy += dys[i]; tab[h].n++;
slot[i] = h;
}
for (uint32_t i = 0; i < n * 3u; i++)
if (slot[i] != UINT32_MAX) {
dxs[i] = tab[slot[i]].dx / tab[slot[i]].n;
dys[i] = tab[slot[i]].dy / tab[slot[i]].n;
}
}
free(tab); free(slot);
}
free(done);
fit->clamped = clamped;
fit->guessed = guessed;
/* 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++) {
+16 -2
View File
@@ -11221,6 +11221,20 @@ static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stal
/* 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. */
/* Geometry the in-between camera passes through (a tunnel ceiling, a
* bridge overhead) has no correct in-between image without clipping at
* the near plane: such a game frame shows its real frames. */
if (s_fg_fit.ok && s_fg_pos_cap < src->prims.n) {
float *nx = (float *)realloc(s_fg_px, (size_t)src->prims.n * 3 * sizeof *nx);
if (nx) s_fg_px = nx;
float *ny = (float *)realloc(s_fg_py, (size_t)src->prims.n * 3 * sizeof *ny);
if (ny) s_fg_py = ny;
if (nx && ny) s_fg_pos_cap = src->prims.n;
}
if (s_fg_fit.ok && s_fg_pos_cap >= src->prims.n) { /* the phase furthest from the redrawn frame */
fg_cam_place(&src->prims, &s_fg_fit, s_fg_verts, 1.0 / (double)(n + 1), s_fg_px, s_fg_py, NULL);
if (s_fg_fit.clamped) { s_fg_fit.ok = 0; s_fg_fit.why = "camera passes through geometry"; }
}
if (!s_fg_fit.ok && !s_fg_force) {
s_fg_rejected++;
s_fg_reject_why = s_fg_fit.why;
@@ -11318,7 +11332,7 @@ int gl_renderer_frame_gen_json(char *out, int cap) {
"\"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\"",
"\"clamped\":%u,\"guessed\":%u,\"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,
@@ -11341,7 +11355,7 @@ int gl_renderer_frame_gen_json(char *out, int cap) {
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,
s_fg_fit.clamped, s_fg_fit.guessed, s_fg_fit.ok,
(unsigned long long)s_fg_rejected, s_fg_reject_why ? s_fg_reject_why : "");
}