Frame generation: shared corners are one vertex; no motion rounding

The remaining tunnel cracks came from vertex sources, not from missing
depth: 46 % of R4 lookups were marked ambiguous because the same 3D point
is projected several times per frame (corners shared by separate meshes,
other functions), so those vertices fell back to a triangle-mean guess.
- The same point projected again is one vertex (the smaller identity, so
  the choice does not follow draw order).
- Up to two different points on one pixel are kept; gpu.c picks the one
  projected by the same function as the triangle's other vertices, else
  the one nearest their depth.
- Per-vertex motion is no longer rounded to 1/8 px: at high internal
  scales the rounding opened hairlines along long T-junction seams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-06 19:56:55 -07:00
parent ba66fa6cac
commit 7b741b10f4
5 changed files with 66 additions and 30 deletions
+6 -2
View File
@@ -33,7 +33,11 @@ has measured time to spare.
- **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
H, indexed by the packed screen word. The same point projected again in a
frame (a corner shared by separate meshes, another function) is one vertex
(the smaller identity); up to two different points on one pixel are kept,
and gpu.c picks the one projected by the same function as the triangle's
other vertices, else the one nearest their depth. 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.
@@ -98,7 +102,7 @@ has measured time to spare.
redrawn frame's. Areas uncovered by the in-between camera show the newer
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.
1 px spread; 0 in R4 races once shared corners resolve).
- 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.
+2 -1
View File
@@ -224,7 +224,8 @@ 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);
typedef struct GteFgSrc { uint32_t id, ra; int32_t p[3], h; } GteFgSrc;
extern int gte_fg_source_lookup(uint32_t packed, GteFgSrc out[2]);
/* 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);
+1 -6
View File
@@ -604,12 +604,7 @@ void fg_cam_place(const FgPrimList *newer, FgCamFit *fit, const FgVert *verts,
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++) {
x[i] += roundf(dxs[i] * 8.0f) / 8.0f;
y[i] += roundf(dys[i] * 8.0f) / 8.0f;
}
for (uint32_t i = 0; i < n * 3u; i++) { x[i] += dxs[i]; y[i] += dys[i]; }
/* Margins: vertices on or past a view edge that moved inward. */
if (margin)
for (uint32_t j = 0; j < n; j++)
+23 -2
View File
@@ -4478,8 +4478,29 @@ static void prepare_precise_triangle(int i0, int i1, int i2,
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;
GteFgSrc c[3][2];
int nc[3];
for (int k = 0; k < 3; k++) nc[k] = gte_fg_source_lookup(gp0_cmd_buf[ix[k]], c[k]);
/* Two points on one pixel: the one projected by the same function as
* a vertex with one source, else the one nearest its depth. */
for (int k = 0; k < 3; k++) {
int pick = nc[k] == 1 ? 0 : -1;
if (nc[k] == 2) {
for (int m = 0; m < 3 && pick < 0; m++)
if (m != k && nc[m] == 1)
for (int q = 0; q < 2; q++)
if (c[k][q].ra == c[m][0].ra) { pick = q; break; }
if (pick < 0)
for (int m = 0; m < 3 && pick < 0; m++)
if (m != k && nc[m] == 1) {
const int32_t z = c[m][0].p[2];
pick = abs(c[k][0].p[2] - z) <= abs(c[k][1].p[2] - z) ? 0 : 1;
}
}
if (pick < 0) continue;
id[k] = c[k][pick].id; hd[k] = c[k][pick].h;
for (int q = 0; q < 3; q++) pc[3 * k + q] = c[k][pick].p[q];
}
gl_renderer_fg_source(id, pc, hd, vx, vy);
}
gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f);
+34 -19
View File
@@ -415,8 +415,8 @@ extern "C" int gte_geometry_correction_lookup_probe(uint32_t packed,
* 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; };
struct FgSrcEntry { uint32_t seq, seq2; uint32_t gen; }; /* gen: frame << 1 | ambiguous; seq2: a second point */
struct FgSrcRec { uint32_t seq, id, ra; int32_t p[3]; int32_t h; };
#define FGSRC_RING (1u << 17)
static FgSrcEntry *s_fgsrc = nullptr;
static FgSrcRec *s_fgsrc_ring = nullptr;
@@ -453,6 +453,7 @@ static void fgsrc_note(const GTEState *g, int32_t packed, uint32_t ra, const int
FgSrcRec &r = s_fgsrc_ring[seq & (FGSRC_RING - 1u)];
r.seq = seq;
r.id = h;
r.ra = ra;
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];
@@ -461,13 +462,22 @@ static void fgsrc_note(const GTEState *g, int32_t packed, uint32_t ra, const int
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;
if ((e.gen >> 1) != f) { e.gen = f << 1; e.seq = seq; e.seq2 = 0; return; }
/* Earlier this frame: the same point projected again (another function,
* another model vertex: shared corners of separate meshes) is one vertex,
* keeping the smaller identity so the choice does not depend on draw
* order. Up to two different points on one pixel are kept (the GPU side
* picks by the triangle's other vertices); a third is ambiguous. */
auto same = [&](uint32_t sq) {
const FgSrcRec &o = s_fgsrc_ring[sq & (FGSRC_RING - 1u)];
return o.seq == sq && std::abs(o.p[0] - r.p[0]) <= 2 && std::abs(o.p[1] - r.p[1]) <= 2 &&
std::abs(o.p[2] - r.p[2]) <= 2;
};
auto keep = [&](uint32_t &sq) { if (s_fgsrc_ring[sq & (FGSRC_RING - 1u)].id > h) sq = seq; };
if (same(e.seq)) { keep(e.seq); return; }
if (!e.seq2) { e.seq2 = seq; return; }
if (same(e.seq2)) { keep(e.seq2); return; }
e.gen |= 1u;
}
static void fgsrc_record(const GTEState *g, uint32_t cmd, uint32_t ra) {
@@ -481,22 +491,27 @@ static void fgsrc_record(const GTEState *g, uint32_t cmd, uint32_t ra) {
}
}
/* 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) {
/* The sources of the vertices last projected to a packed screen word this
* frame (or a few frames ago): 0 when none, ambiguous or stale, else 1 or 2
* (two different points on that pixel). */
extern "C" int gte_fg_source_lookup(uint32_t packed, GteFgSrc out[2]) {
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;
int n = 0;
const uint32_t sq[2] = { e.seq, e.seq2 };
for (int i = 0; i < 2; i++) {
if (!sq[i]) continue;
const FgSrcRec &r = s_fgsrc_ring[sq[i] & (FGSRC_RING - 1u)];
if (r.seq != sq[i]) continue;
out[n].id = r.id; out[n].ra = r.ra; out[n].h = r.h;
for (int k = 0; k < 3; k++) out[n].p[k] = r.p[k];
n++;
}
return n;
}
/* Render-pass checkpoint for the gte.cpp side of precision tracking: the