mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
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:
+45
-19
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -10011,7 +10075,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 */
|
||||
@@ -10031,8 +10095,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;
|
||||
@@ -10279,6 +10359,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;
|
||||
@@ -10317,6 +10403,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);
|
||||
@@ -10325,6 +10425,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;
|
||||
}
|
||||
@@ -10355,6 +10456,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);
|
||||
@@ -10367,6 +10475,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) {
|
||||
@@ -10437,6 +10546,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);
|
||||
@@ -10503,10 +10620,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);
|
||||
@@ -10547,24 +10682,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:
|
||||
@@ -10574,15 +10719,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;
|
||||
@@ -10666,8 +10819,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;
|
||||
@@ -10678,11 +10839,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;
|
||||
@@ -10993,15 +11186,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);
|
||||
@@ -11041,6 +11264,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);
|
||||
@@ -11077,12 +11308,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,
|
||||
@@ -11091,7 +11325,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()
|
||||
@@ -11100,7 +11334,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 ------------------------------------------------ */
|
||||
|
||||
@@ -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(>e, 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(>e, cmd, cpu->gpr[31]);
|
||||
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
if (func == 0x01 || func == 0x30) gte_rtp_record(>e, cmd);
|
||||
|
||||
+163
-78
@@ -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);
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
Reference in New Issue
Block a user