/* Frame generation's renderer-independent half (src/frame_gen.c, * docs/FRAME_GENERATION.md): matching triangles of two frames, the * interpolation endpoints, the plan of how many in-between frames fit, and * the breaker, the generated-frame cost estimate and the guest pace. */ #include "frame_gen.h" #include #include #include #include static int checks, failures; static void check(int ok, const char *what) { checks++; if (!ok) { failures++; fprintf(stderr, "FAIL: %s\n", what); } } /* ---- 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"); } /* 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 }; 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); } /* 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 }; 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_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 }; 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_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. */ check(fg_plan(f30, 120.0, 0.010, 0.002, 0.85, 7) == 3, "30 Hz on 120 Hz: three in-between frames"); /* 60 Hz game frames on 120 Hz: one. On 60 Hz: none. */ check(fg_plan(f60, 120.0, 0.004, 0.002, 0.85, 7) == 1, "60 Hz on 120 Hz: one"); check(fg_plan(f60, 60.0, 0.004, 0.002, 0.85, 7) == 0, "60 Hz on 60 Hz: none"); check(fg_plan(f30, 60.0, 0.010, 0.002, 0.85, 7) == 1, "30 Hz on 60 Hz: one"); /* Only what fits: 33.4 ms * 0.85 = 28.4; real 20 ms leaves 8.4 -> two of 4 ms. */ check(fg_plan(f30, 120.0, 0.020, 0.004, 0.85, 7) == 2, "only what fits"); check(fg_plan(f30, 120.0, 0.030, 0.001, 0.85, 7) == 0, "no surplus: none"); /* Unknown generation cost: one, if a third of the budget is free. */ check(fg_plan(f30, 120.0, 0.010, 0.0, 0.85, 7) == 1, "unmeasured: one to measure"); /* Windows, R4 30 Hz with the display ring: 2 x 7.2 ms real leaves 14 of * 28.4 ms, under half; one is still drawn to measure the cost. */ check(fg_plan(f30, 120.0, 0.0144, 0.0, 0.85, 7) == 1, "unmeasured, real over 42 %: one to measure"); check(fg_plan(f30, 120.0, 0.020, 0.0, 0.85, 7) == 0, "unmeasured without room: none"); check(fg_plan(f30, 240.0, 0.001, 0.0001, 0.85, 2) == 2, "max_gens caps"); } static void test_breaker(void) { FgBreaker b; fg_breaker_init(&b, 3.0, 24.0, 10.0); check(fg_breaker_open(&b, 0.0), "starts allowing generation"); fg_breaker_trip(&b, 1.0, "late"); check(!fg_breaker_open(&b, 3.9) && fg_breaker_open(&b, 4.0), "a trip holds 3 s"); fg_breaker_trip(&b, 5.0, "late"); /* within 10 s of the last hold's end */ check(!fg_breaker_open(&b, 10.9) && fg_breaker_open(&b, 11.0), "a repeat doubles the hold"); fg_breaker_trip(&b, 6.0, "late"); /* already held: the hold stands */ check(fg_breaker_open(&b, 11.0), "a trip while held does not extend it"); fg_breaker_trip(&b, 40.0, "late"); /* long after: back to the base hold */ check(!fg_breaker_open(&b, 42.9) && fg_breaker_open(&b, 43.0), "a later trip starts over"); check(b.trips == 4 && b.reason && strcmp(b.reason, "late") == 0, "trips counted with a reason"); for (int i = 0; i < 10; i++) fg_breaker_trip(&b, 43.0 + 30.0 * i + (i ? 0 : 0), "x"); check(b.hold <= 24.0, "the hold is capped"); } static void test_cost(void) { FgCost c; fg_cost_init(&c, 2.0, 16.0); const double fit = 0.020; check(fg_cost_estimate(&c, 0.0, fit) == 0.0, "unknown before any sample"); /* Cold: the first frames after allocation are discarded. */ fg_cost_cold(&c, 2); fg_cost_add(&c, 0.070, fit); fg_cost_add(&c, 0.040, fit); check(c.ema == 0.0 && c.discarded == 2, "cold samples discarded"); fg_cost_add(&c, 0.006, fit); check(fabs(fg_cost_estimate(&c, 0.1, fit) - 0.006) < 1e-12, "first warm sample is the estimate"); fg_cost_add(&c, 0.016, fit); check(fabs(c.ema - 0.008) < 1e-12, "warm samples blend"); /* A spike that pushes the estimate past the fit blocks the plan... */ for (int i = 0; i < 10; i++) fg_cost_add(&c, 0.060, fit); check(fg_cost_estimate(&c, 10.0, fit) > fit, "a high estimate blocks"); check(fg_cost_estimate(&c, 11.9, fit) > fit, "... until the probe interval"); /* ...for at most probe_s: then one probe, whose sample replaces it. */ check(fg_cost_estimate(&c, 12.0, fit) == 0.0 && c.probes == 1, "stale estimate re-probed"); check(fg_cost_estimate(&c, 12.03, fit) > fit, "one probe at a time"); fg_cost_add(&c, 0.005, fit); check(fabs(fg_cost_estimate(&c, 12.1, fit) - 0.005) < 1e-12, "the probe replaces the estimate"); /* A probe that still does not fit backs off. */ for (int i = 0; i < 10; i++) fg_cost_add(&c, 0.060, fit); (void)fg_cost_estimate(&c, 20.0, fit); check(fg_cost_estimate(&c, 22.0, fit) == 0.0, "second probe"); fg_cost_add(&c, 0.050, fit); check(c.probe_s == 4.0, "a probe that does not fit doubles the wait"); check(fg_cost_estimate(&c, 25.9, fit) > fit && fg_cost_estimate(&c, 26.0, fit) == 0.0, "next probe after the doubled wait"); fg_cost_add(&c, 0.004, fit); check(c.probe_s == 2.0, "a probe that fits resets the wait"); /* A granted probe that was never drawn is given up, then re-granted. */ for (int i = 0; i < 10; i++) fg_cost_add(&c, 0.060, fit); (void)fg_cost_estimate(&c, 40.0, fit); check(fg_cost_estimate(&c, 42.0, fit) == 0.0, "probe granted"); check(fg_cost_estimate(&c, 44.0, fit) > fit, "an undrawn probe expires"); check(fg_cost_estimate(&c, 46.0, fit) == 0.0, "and is granted again"); /* Re-allocation makes the next samples cold again. */ fg_cost_cold(&c, 2); fg_cost_add(&c, 0.5, fit); fg_cost_add(&c, 0.5, fit); check(c.cold == 0 && c.ema < 0.5, "cold after re-allocation"); } static void test_pace(void) { FgPace p; memset(&p, 0, sizeof p); const double T = 1.0 / 59.94, slack = 2.0 * T; double t = 0.0; int late = 0; for (int i = 0; i < 600; i++) { /* jitter: +-6 ms around the schedule */ late += fg_pace_note(&p, t + ((i % 3) - 1) * 0.006, T, slack); t += T; } check(late == 0, "jitter is not late"); late = 0; /* one 40 ms frame made up by a burst */ late += fg_pace_note(&p, t + 0.024, T, slack); t += T; late += fg_pace_note(&p, t + 0.004, T, slack); t += T; late += fg_pace_note(&p, t, T, slack); t += T; check(late == 0, "a long frame made up is not late"); /* A real slip: 60 ms gap. */ check(fg_pace_note(&p, t + 0.045, T, slack) == 1, "a slip beyond two intervals is late"); t += 0.045 + T; check(fg_pace_note(&p, t, T, slack) == 0, "the schedule restarts after a slip"); /* Slow but steady (a 50 Hz guest told 59.94): one trip, then resync each time it slips. */ int trips = 0; for (int i = 0; i < 60; i++) { t += 0.020; trips += fg_pace_note(&p, t, T, slack); } check(trips > 0 && trips < 20, "a steadily slow guest is late now and then"); } static void test_ceiling(void) { FgCeiling c; fg_ceiling_init(&c, 7, 2.0); check(fg_ceiling_get(&c, 0.0) == 7, "no ceiling before a trip"); fg_ceiling_trip(&c, 3, 1.0); check(fg_ceiling_get(&c, 1.5) == 2, "an overload at three plans two"); fg_ceiling_trip(&c, 2, 1.6); check(fg_ceiling_get(&c, 1.7) == 1, "another lowers it again"); fg_ceiling_trip(&c, 1, 1.8); check(fg_ceiling_get(&c, 1.9) == 1, "never below one (the breaker stops generation)"); check(fg_ceiling_get(&c, 3.7) == 1 && fg_ceiling_get(&c, 3.9) == 2, "recovers one per 2 s"); check(fg_ceiling_get(&c, 7.9) == 4, "and keeps recovering"); fg_ceiling_trip(&c, 0, 8.0); check(fg_ceiling_get(&c, 8.0) == 4, "a trip with nothing planned is not an overload"); check(fg_ceiling_get(&c, 100.0) == 7, "up to the maximum"); } /* Any-rate presents: the grid fg_tick runs (fg_next_due, fg_clock_phase, * fg_plan at fg_plan_hz), simulated over 10 s of 30 Hz game frames. */ static void test_any_rate(void) { const double rates[] = { 60.0, 90.0, 100.0, 120.0, 144.0, 165.0, 240.0, 360.0 }; const double flip_s = 2.0 / 59.94; const uint64_t flip_ns = (uint64_t)(flip_s * 1e9); for (unsigned r = 0; r < sizeof rates / sizeof rates[0]; r++) { const double hz = rates[r]; const uint64_t step = (uint64_t)(fg_step_s(flip_s, hz, 1) * 1e9); const int n = fg_plan(flip_s, fg_plan_hz(flip_s, hz), 0.001, 0.0005, 0.85, 64); uint64_t due = 0, last = 0, min_gap = UINT64_MAX; unsigned presents = 0, frames = 300, gens = 0; int phase_ok = 1, shown_ok = 1; unsigned max_shown = 0; /* A whole number of intervals a game frame (59.94 Hz content on * 60 / 120 / 240 / 360 Hz): every game frame shows the same count. */ const double x = flip_s * hz; const int whole = fabs(x - floor(x + 0.5)) < 0.01; for (unsigned f = 0; f < frames; f++) { const unsigned gens_before = gens; const uint64_t t0 = (uint64_t)f * flip_ns, next = t0 + flip_ns; if (due < t0 || due > t0 + step) due = t0; double prev_t = 0.0; for (int k = 1;; ) { uint64_t now = due; if (now >= next) { now = next; } else if (k <= n) { double t = fg_clock_phase(now - t0, step, flip_ns); if (t > 0.0) { if (!(t > prev_t && t < 1.0)) phase_ok = 0; prev_t = t; k++; gens++; if (presents && now - last < min_gap) min_gap = now - last; last = now; presents++; due = fg_next_due(due, now, step); continue; } } if (presents && now - last < min_gap && now != next) min_gap = now - last; last = now; presents++; due = fg_next_due(due, now, step); break; } /* Every planned in-between frame is shown (the plan, and the * dynamic resolution budget built on it, count no dropped one). */ if (f > 0 && gens - gens_before > max_shown) max_shown = gens - gens_before; if (f > 0 && whole && gens - gens_before != (unsigned)n) shown_ok = 0; } const double secs = frames * flip_s, rate = presents / secs; char what[96]; snprintf(what, sizeof what, "%.0f Hz: %.1f presents/s, phases rise", hz, rate); check(phase_ok && rate > hz * 0.95 && rate < hz * 1.03, what); snprintf(what, sizeof what, "%.0f Hz: no two presents closer than one interval", hz); check(min_gap + 1000u >= step, what); /* The plan (and the dynamic resolution budget built on it) is the * most frames the clock shows in a game frame: none planned is * dropped every frame, none shown is unbudgeted. */ snprintf(what, sizeof what, "%.0f Hz: the clock shows up to the %d planned frames", hz, n); check(max_shown == (unsigned)n && shown_ok, what); } } /* 59.94 Hz content (two VBlanks a game frame) on 60 / 120 Hz panels: 2.002 * and 4.004 intervals. The clock shows 1 / 3 in-between frames; the plan * and the dynamic resolution share count exactly those (fg_slots). */ static void test_slots_5994(void) { const double flip_s = 2.0 / 59.94; check(fg_slots(flip_s, 60.0) == 2, "59.94 on 60 Hz: 2 slots (1 in-between frame)"); check(fg_slots(flip_s, 120.0) == 4, "59.94 on 120 Hz: 4 slots"); check(fg_slots(flip_s, 100.0) == 4, "59.94 on 100 Hz: 4 slots (3.34 intervals)"); check(fg_slots(2.0 / 60.0, 60.0) == 2, "60 on 60 Hz: 2 slots"); check(fg_plan(flip_s, fg_plan_hz(flip_s, 60.0), 0.001, 0.0005, 0.85, 64) == 1, "59.94 on 60 Hz: one in-between frame planned"); check(fg_plan(flip_s, fg_plan_hz(flip_s, 120.0), 0.001, 0.0005, 0.85, 64) == 3, "59.94 on 120 Hz: three planned"); } static void test_hud_lerp(void) { FgPrimList L = {0}, O = {0}; FgPrim p; memset(&p, 0, sizeof p); p.key = 7; p.view = 1; p.x[0] = 10; p.y[0] = 10; p.x[1] = 20; p.y[1] = 10; p.x[2] = 15; p.y[2] = 30; fg_prims_add(&L, &p); /* needle */ p.key = 9; fg_prims_add(&L, &p); /* a digit */ FgPrim q = L.v[0]; q.x[2] = 25; fg_prims_add(&O, &q); /* needle tip moved 10 px */ q = L.v[1]; q.key = 10; fg_prims_add(&O, &q); /* the digit changed */ float x[6], y[6]; for (int i = 0; i < 6; i++) { x[i] = L.v[i / 3].x[i % 3]; y[i] = L.v[i / 3].y[i % 3]; } fg_hud_lerp(&L, &O, 0.5, x, y, 24.0f); check(x[2] == 20.0f && x[0] == 10.0f, "HUD needle halfway between the frames"); check(x[5] == 15.0f, "a changed digit stays as drawn"); fg_prims_free(&L); fg_prims_free(&O); /* The match window is 8 2D triangles of the other frame, the 8th * included: a needle found there still moves, one at the 9th does not. */ for (int at = 8; at <= 9; at++) { FgPrimList A = {0}, B = {0}; memset(&p, 0, sizeof p); p.key = 7; p.view = 1; p.x[0] = 10; p.y[0] = 10; p.x[1] = 20; p.y[1] = 10; p.x[2] = 15; p.y[2] = 30; fg_prims_add(&A, &p); for (int i = 1; i < at; i++) { FgPrim o = p; o.key = 100 + i; fg_prims_add(&B, &o); } FgPrim m = p; m.x[2] = 25; fg_prims_add(&B, &m); float ax[3] = { 10, 20, 15 }, ay[3] = { 10, 10, 30 }; fg_hud_lerp(&A, &B, 0.5, ax, ay, 24.0f); check(at == 8 ? ax[2] == 20.0f : ax[2] == 15.0f, at == 8 ? "a match at the 8th 2D triangle moves" : "a match at the 9th stays"); fg_prims_free(&A); fg_prims_free(&B); } } static void test_view_affine(void) { FgCamFit f; memset(&f, 0, sizeof f); f.nviews = 1; const double a = 0.1; /* 0.2 rad about y */ f.v[0].q[0] = cos(a); f.v[0].q[2] = sin(a); f.v[0].t[0] = 30; f.v[0].t[1] = -5; f.v[0].t[2] = 400; float A[9], b[3]; check(fg_view_affine(&f, 0, 1.0, A, b), "affine"); check(fabsf(A[0] - 1) < 1e-5f && fabsf(A[4] - 1) < 1e-5f && fabsf(A[8] - 1) < 1e-5f && fabsf(A[1]) < 1e-5f && fabsf(b[0]) < 1e-3f && fabsf(b[2]) < 1e-3f, "t = 1: the newer camera itself"); /* t = 0 maps a newer point back to the older frame: P_old = M^T (P - T). */ fg_view_affine(&f, 0, 0.0, A, b); const float P[3] = { 100, 20, 1000 }; const float o0 = A[0] * P[0] + A[1] * P[1] + A[2] * P[2] + b[0]; const float o2 = A[6] * P[0] + A[7] * P[1] + A[8] * P[2] + b[2]; /* M rotates about y by 0.2: M^T (P-T) */ const double c = cos(2 * a), sn = sin(2 * a), dx = P[0] - 30, dz = P[2] - 400; check(fabs(o0 - (c * dx - sn * dz)) < 1e-2 && fabs(o2 - (sn * dx + c * dz)) < 1e-2, "t = 0: the older frame's position"); } /* A 320x240 main view and a 64x48 inset with the same valid camera motion. * The inset must still interpolate in redraw; only opt-in reprojection * freezes it. Zero-initialized fits also keep the default redraw policy. */ static void test_small_view_policy(void) { FgPrim p[2] = {0}; FgCamFit fit = {0}; FgVert verts[6] = {0}; FgCamParams cp; fg_cam_defaults(&cp); check(!cp.freeze_small_views, "redraw defaults do not freeze small views"); fit.ok = 1; fit.nviews = 2; for (int view = 0; view < 2; view++) { const float h = view ? 48.0f : 240.0f; const float cx = view ? 32.0f : 160.0f, cy = view ? 24.0f : 120.0f; fit.v[view].ok = 1; fit.v[view].q[0] = 1.0; fit.v[view].t[0] = 20.0; fit.v[view].area[2] = view ? 64.0f : 320.0f; fit.v[view].area[3] = h; for (int k = 0; k < 3; k++) { p[view].p[k][0] = k == 1 ? 20.0f : 0.0f; p[view].p[k][1] = k == 2 ? 20.0f : 0.0f; p[view].p[k][2] = 200.0f; p[view].h[k] = h; p[view].x[k] = cx + h * p[view].p[k][0] / 200.0f; p[view].y[k] = cy + h * p[view].p[k][1] / 200.0f; verts[view * 3 + k].mode = FG_PLACE_CAMERA; verts[view * 3 + k].view = (int8_t)view; } } FgPrimList list = {p, 2, 2}; float x[6], y[6]; for (int freeze = 0; freeze < 2; freeze++) { fit.freeze_small_views = freeze; fg_cam_place(&list, &fit, verts, 0.5, x, y, NULL); check(fabsf(x[0] - p[0].x[0] + 12.0f) < 0.001f, "main view always interpolates"); check(fabsf(x[3] - p[1].x[0] + (freeze ? 0.0f : 2.4f)) < 0.001f, freeze ? "opt-in reprojection freezes the inset" : "redraw interpolates the inset"); fg_cam_place(&list, &fit, verts, 1.0, x, y, NULL); for (int i = 0; i < 6; i++) check(x[i] == p[i / 3].x[i % 3] && y[i] == p[i / 3].y[i % 3], "both policies preserve the real-frame endpoint"); } /* The fit API propagates the policy; keep_partial alone must not set it. */ FgPrimList empty = {0}; cp.keep_partial = 1; fg_cam_fit(&empty, &empty, &cp, &fit, verts); check(!fit.freeze_small_views, "partial fit does not implicitly freeze insets"); cp.freeze_small_views = 1; fg_cam_fit(&empty, &empty, &cp, &fit, verts); check(fit.freeze_small_views, "fit propagates explicit reprojection policy"); } int main(void) { test_small_view_policy(); test_view_affine(); test_hud_lerp(); test_slots_5994(); test_any_rate(); test_ceiling(); test_cost(); test_pace(); test_camera_reordered(); test_objects_hud_neighbours(); test_verdict(); test_plan(); test_breaker(); printf("frame_gen_test: checks=%d failures=%d\n", checks, failures); return failures ? 1 : 0; }