mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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++) {
|
||||
|
||||
@@ -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 : "");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user