diff --git a/docs/FRAME_GENERATION.md b/docs/FRAME_GENERATION.md index 01891e8b..4ab4c081 100644 --- a/docs/FRAME_GENERATION.md +++ b/docs/FRAME_GENERATION.md @@ -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. diff --git a/runtime/include/frame_gen.h b/runtime/include/frame_gen.h index 5ea3eed7..8e17ec95 100644 --- a/runtime/include/frame_gen.h +++ b/runtime/include/frame_gen.h @@ -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. diff --git a/runtime/src/frame_gen.c b/runtime/src/frame_gen.c index 646f1fbd..70a7ec55 100644 --- a/runtime/src/frame_gen.c +++ b/runtime/src/frame_gen.c @@ -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++) { diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 83e65412..874df29f 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -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 : ""); }