mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Smooth motion: budget dynres for the in-between frames, time them from the game frame's arrival, keep small views put, move HUD needles
- Dynamic resolution budgets the real frames with the share of the game frame Smooth motion leaves them (gl_renderer_frame_gen_real_share), so the scale drops until the in-between frames fit the display refresh. - In-between frames are timed from the game frame's arrival, not from the end of its composition; the last one per game frame is no longer dropped by a quarter-interval margin. - Views under 15 % of the largest (a rear-view mirror) stay as the real frame drew them: a wrong mirror camera fit threw its lane marks across the screen as a dashed line. - HUD: 2D triangles both frames draw with the same key that moved a little (tachometer needle) are interpolated; changed digits stay as drawn. - frame_gen JSON: end_ahead / end_bp / end_phase counters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -145,6 +145,9 @@ void fg_cam_place(const FgPrimList *newer, FgCamFit *fit, const FgVert *verts,
|
||||
* 0 when it would land on the next real frame. */
|
||||
double fg_step_s(double flip_s, double refresh_hz, int n);
|
||||
double fg_clock_phase(uint64_t since_real_ns, uint64_t step_ns, uint64_t flip_ns);
|
||||
/* HUD motion between two frames' 2D triangles (frame_gen.c). */
|
||||
void fg_hud_lerp(const FgPrimList *L, const FgPrimList *O, double u, float *x, float *y,
|
||||
float max_px);
|
||||
/* The refresh fg_plan sees: rounded up to whole slots per game frame. */
|
||||
double fg_plan_hz(double flip_s, double refresh_hz);
|
||||
/* The next present time on the global grid after presenting at `now`. */
|
||||
|
||||
@@ -342,6 +342,7 @@ void gl_renderer_fg_source(const uint32_t id[3], const int32_t pc[9], const int3
|
||||
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);
|
||||
double gl_renderer_frame_gen_real_share(void);
|
||||
void gl_renderer_frame_gen_hold(const char *reason, double secs);
|
||||
int gl_renderer_frame_gen_json(char *out, int cap);
|
||||
/* Host time the renderer spent, as running totals in performance-counter
|
||||
|
||||
+61
-2
@@ -604,6 +604,24 @@ void fg_cam_place(const FgPrimList *newer, FgCamFit *fit, const FgVert *verts,
|
||||
free(done);
|
||||
fit->clamped = clamped;
|
||||
fit->guessed = guessed;
|
||||
/* Small views (a rear-view mirror inside the main view) stay as the
|
||||
* real frame drew them: their camera is fitted from a few dozen
|
||||
* vertices, and a wrong fit threw their lane marks across the screen as
|
||||
* a dashed line. At this size their 30 Hz motion does not show. */
|
||||
{
|
||||
float big = 0.0f;
|
||||
for (uint32_t vi = 0; vi < fit->nviews; vi++) {
|
||||
const float *ar = fit->v[vi].area;
|
||||
const float a = (ar[2] - ar[0]) * (ar[3] - ar[1]);
|
||||
if (a > big) big = a;
|
||||
}
|
||||
for (uint32_t i = 0; i < n * 3u; i++) {
|
||||
const int vi = verts[i].view;
|
||||
if (vi < 0 || (uint32_t)vi >= fit->nviews) continue;
|
||||
const float *ar = fit->v[vi].area;
|
||||
if ((ar[2] - ar[0]) * (ar[3] - ar[1]) < 0.15f * big) { dxs[i] = 0.0f; dys[i] = 0.0f; }
|
||||
}
|
||||
}
|
||||
for (uint32_t i = 0; i < n * 3u; i++) { x[i] += dxs[i]; y[i] += dys[i]; }
|
||||
/* Margins: vertices on or past a view edge that moved inward. */
|
||||
if (margin)
|
||||
@@ -661,8 +679,12 @@ uint64_t fg_next_due(uint64_t due, uint64_t now, uint64_t step_ns) {
|
||||
double fg_clock_phase(uint64_t since_real_ns, uint64_t step_ns, uint64_t flip_ns) {
|
||||
if (flip_ns == 0u) return 0.0;
|
||||
double t = ((double)since_real_ns + (double)step_ns) / (double)flip_ns;
|
||||
/* Within a quarter interval of the next real frame it is the real one. */
|
||||
if (t >= 1.0 - 0.25 * (double)step_ns / (double)flip_ns) return 0.0;
|
||||
/* At (or past) the next real frame's time it is the real one. The
|
||||
* phase is where the camera is when this frame is seen, one interval
|
||||
* from now, so the last in-between frame of a game frame lands close
|
||||
* to 1 (a quarter-interval margin here dropped it at every refresh). */
|
||||
(void)step_ns;
|
||||
if (t >= 0.985) return 0.0;
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -774,3 +796,40 @@ int fg_ceiling_get(FgCeiling *c, double now) {
|
||||
}
|
||||
return c->cap;
|
||||
}
|
||||
|
||||
/* HUD motion: 2D triangles (no GTE projection: gauges, needles) of the
|
||||
* redrawn list L that the other frame O draws too (same key: op, texture,
|
||||
* colour, UVs, in the same order among the 2D triangles) and that moved a
|
||||
* little (at most max_px per corner: a tachometer needle, a sliding panel)
|
||||
* are placed at fraction u of the way from L to O. Anything that changed
|
||||
* what it draws (digits) or jumped stays as L drew it. */
|
||||
void fg_hud_lerp(const FgPrimList *L, const FgPrimList *O, double u, float *x, float *y,
|
||||
float max_px) {
|
||||
if (!L || !O || u <= 0.0 || u >= 1.0) return;
|
||||
uint32_t o = 0;
|
||||
for (uint32_t j = 0; j < L->n; j++) {
|
||||
const FgPrim *a = &L->v[j];
|
||||
if (a->vid[0] || a->vid[1] || a->vid[2]) continue;
|
||||
uint32_t m = o, seen = 0;
|
||||
for (; m < O->n && seen < 8u; m++) {
|
||||
const FgPrim *b = &O->v[m];
|
||||
if (b->vid[0] || b->vid[1] || b->vid[2]) continue;
|
||||
seen++;
|
||||
if (b->key == a->key && b->view == a->view) break;
|
||||
}
|
||||
if (m >= O->n || seen >= 8u) continue;
|
||||
const FgPrim *b = &O->v[m];
|
||||
o = m + 1;
|
||||
int ok = 1, moved = 0;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
const float dx = b->x[k] - a->x[k], dy = b->y[k] - a->y[k];
|
||||
if (fabsf(dx) > max_px || fabsf(dy) > max_px) ok = 0;
|
||||
if (dx != 0.0f || dy != 0.0f) moved = 1;
|
||||
}
|
||||
if (!ok || !moved) continue;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
x[3 * j + k] = a->x[k] + (b->x[k] - a->x[k]) * (float)u;
|
||||
y[3 * j + k] = a->y[k] + (b->y[k] - a->y[k]) * (float)u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10220,7 +10220,8 @@ static uint64_t s_fg_q_cpu[4];
|
||||
static uint64_t s_fg_generated = 0, s_fg_real_presents = 0, s_fg_flips = 0,
|
||||
s_fg_flushed = 0, s_fg_skipped_plan = 0, s_fg_dups = 0;
|
||||
static int s_fg_last_n = 0, s_fg_last_slots = 0;
|
||||
static const char *s_fg_noplan_why = ""; /* why the last flip planned nothing */
|
||||
static const char *s_fg_noplan_why = "";
|
||||
static uint64_t s_fg_end_ahead = 0, s_fg_end_bp = 0, s_fg_end_phase = 0; /* why a game frame's in-betweens stopped early */ /* why the last flip planned nothing */
|
||||
static double s_fg_last_match_ms = 0.0;
|
||||
|
||||
static double fg_now_s(void) { return (double)rt_now_ns() * 1e-9; }
|
||||
@@ -10692,7 +10693,7 @@ 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 */
|
||||
/* b: the redrawn frame; a: the other one (its HUD motion only). */
|
||||
const int *disp = b->disp;
|
||||
/* Every vertex's position at this phase (the newer frame from an
|
||||
* in-between camera, frame_gen.h). */
|
||||
@@ -10708,6 +10709,7 @@ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wid
|
||||
}
|
||||
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);
|
||||
if (a) fg_hud_lerp(&b->prims, &a->prims, 1.0 - t, s_fg_px, s_fg_py, 24.0f);
|
||||
placed = 1;
|
||||
}
|
||||
}
|
||||
@@ -10914,7 +10916,7 @@ static int fg_generate(double t, int swap) {
|
||||
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(NULL, L, tl, iw >= 0 ? s_fg_w_fbo : 0);
|
||||
fg_replay(s_fg_src_older ? b : a, L, tl, iw >= 0 ? s_fg_w_fbo : 0);
|
||||
flush_line_batch();
|
||||
flush_flat_batch();
|
||||
flush_tex_batch();
|
||||
@@ -11134,6 +11136,7 @@ static uint64_t fg_tick(void *user, uint64_t now) {
|
||||
/* Backpressure itself is judged at the flip, by how long the guest
|
||||
* waited (fg_on_present); here it only shows the real frame now. */
|
||||
if (bp == s_fg_bp_seen && ahead >= 2 && fg_recently_generated()) fg_overload();
|
||||
if (s_fg_k <= s_fg_n) { if (bp != s_fg_bp_seen) s_fg_end_bp++; else s_fg_end_ahead++; }
|
||||
s_fg_bp_seen = bp;
|
||||
fg_flush();
|
||||
return 0;
|
||||
@@ -11154,6 +11157,7 @@ static uint64_t fg_tick(void *user, uint64_t now) {
|
||||
return s_fg_due;
|
||||
}
|
||||
s_fg_flushed++;
|
||||
s_fg_end_phase++;
|
||||
}
|
||||
fg_present_real();
|
||||
return 0;
|
||||
@@ -11162,6 +11166,12 @@ static uint64_t fg_tick(void *user, uint64_t now) {
|
||||
/* A present record replayed with frame generation on. Returns 1 when it was
|
||||
* handled (scheduled or dropped), 0 to present it as usual. */
|
||||
static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stale) {
|
||||
/* The game frame's arrival: its in-between frames are timed from here,
|
||||
* not from when its composition finished, because the next game frame
|
||||
* arrives one game frame after this one whatever the composition cost
|
||||
* (timed from the end, the last in-between frame of every game frame
|
||||
* collided with the next one and was dropped). */
|
||||
const uint64_t arrive_ns = rt_now_ns();
|
||||
RthOvHdr hd;
|
||||
memcpy(&hd, p, sizeof hd);
|
||||
const int32_t *a = hd.args;
|
||||
@@ -11360,7 +11370,7 @@ static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stal
|
||||
s_fg_capture_real = 0;
|
||||
s_fg_pa = s_fg_older; s_fg_pb = s_fg_newer;
|
||||
s_fg_n = n; s_fg_k = 1;
|
||||
s_fg_t0 = rt_now_ns();
|
||||
s_fg_t0 = arrive_ns;
|
||||
s_fg_flip_ns = (uint64_t)(flip_s * 1e9);
|
||||
s_fg_step_ns = (uint64_t)(fg_step_s(flip_s, rhz, n) * 1e9);
|
||||
if (s_fg_due < s_fg_t0 || s_fg_due > s_fg_t0 + s_fg_step_ns) s_fg_due = s_fg_t0;
|
||||
@@ -11394,6 +11404,26 @@ void gl_renderer_fg_source(const uint32_t id[3], const int32_t pc[9], const int3
|
||||
pc[0], pc[1], pc[2], pc[3], pc[4], pc[5], pc[6], pc[7], pc[8], h[0], h[1], h[2]);
|
||||
}
|
||||
|
||||
/* The share of a game frame the real frame may use so Smooth motion still
|
||||
* fills every display interval: (0.85 flip - slots-1 generated frames and
|
||||
* their swaps) / flip, never below 0.35. 1 while generation is off or its
|
||||
* cost is unknown. Dynamic resolution budgets the real frames with it, so
|
||||
* the scale drops until the in-between frames fit (they draw at the same
|
||||
* scale, so they get cheaper too). */
|
||||
double gl_renderer_frame_gen_real_share(void) {
|
||||
if (!s_fg_on || !rth_record_mode()) return 1.0;
|
||||
const double ghz = atomic_load(&s_fg_guest_hz), rhz = atomic_load(&s_fg_refresh_hz);
|
||||
const double flip_s = (double)s_fg_flip_vb / (ghz > 1.0 ? ghz : 59.94);
|
||||
if (flip_s <= 0.0 || rhz <= 0.0 || s_fg_flip_vb < 2) return 1.0;
|
||||
const int slots = (int)ceil(flip_s * rhz - 1e-6);
|
||||
const double g = s_fg_cost.ema > 0.0 ? s_fg_cost.ema + s_fg_swap_ema : 0.0;
|
||||
if (slots < 2 || g <= 0.0) return 1.0;
|
||||
double share = (0.85 * flip_s - (double)(slots - 1) * g) / flip_s;
|
||||
if (share < 0.35) share = 0.35;
|
||||
if (share > 1.0) share = 1.0;
|
||||
return share;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -11438,7 +11468,7 @@ int gl_renderer_frame_gen_json(char *out, int cap) {
|
||||
"\"trips_late\":%u,\"trips_backed_up\":%u,\"trips_behind\":%u,"
|
||||
"\"place_camera\":%u,\"place_object\":%u,\"place_neighbour\":%u,"
|
||||
"\"place_unchanged\":%u,\"cam_angle_deg\":%.3f,\"cam_shift\":%.1f,"
|
||||
"\"clamped\":%u,\"guessed\":%u,\"verdict_ok\":%d,\"rejected\":%llu,\"reject_why\":\"%s\",\"noplan_why\":\"%s\"",
|
||||
"\"clamped\":%u,\"guessed\":%u,\"verdict_ok\":%d,\"rejected\":%llu,\"reject_why\":\"%s\",\"noplan_why\":\"%s\",\"end_ahead\":%llu,\"end_bp\":%llu,\"end_phase\":%llu",
|
||||
s_fg_on, s_fg_on && s_rth_on && !s_hd_native_authority && 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,
|
||||
@@ -11463,7 +11493,9 @@ int gl_renderer_frame_gen_json(char *out, int cap) {
|
||||
s_fg_fit.v[0].t[2] * s_fg_fit.v[0].t[2]),
|
||||
s_fg_fit.clamped, s_fg_fit.guessed, s_fg_fit.ok,
|
||||
(unsigned long long)s_fg_rejected, s_fg_reject_why ? s_fg_reject_why : "",
|
||||
s_fg_noplan_why ? s_fg_noplan_why : "");
|
||||
s_fg_noplan_why ? s_fg_noplan_why : "",
|
||||
(unsigned long long)s_fg_end_ahead, (unsigned long long)s_fg_end_bp,
|
||||
(unsigned long long)s_fg_end_phase);
|
||||
}
|
||||
|
||||
/* ---- the recording vtable ------------------------------------------------ */
|
||||
|
||||
@@ -9421,7 +9421,9 @@ static void dynres_tick_rt(double now_s, double wall, double period, int held,
|
||||
g_dynres.rt_acc_gpu_frames += co.gpu_frames - c0.gpu_frames;
|
||||
g_dynres.last_costs = co;
|
||||
g_dynres.last_bp_ns = bp_ns;
|
||||
DynrtSample smp{ period, wall, frames, cost, bp, held };
|
||||
/* Smooth motion's in-between frames share the game frame with it. */
|
||||
const double share = gl_renderer_frame_gen_real_share();
|
||||
DynrtSample smp{ period * share, wall, frames, cost, bp, held };
|
||||
const int prev_level = c.level;
|
||||
const int level = dynrt_sample(&c, now_s, &smp);
|
||||
/* Frame generation only spends surplus: not while the real frames are
|
||||
|
||||
@@ -360,7 +360,25 @@ static void test_any_rate(void) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_hud_lerp();
|
||||
test_any_rate();
|
||||
test_ceiling();
|
||||
test_cost();
|
||||
|
||||
Reference in New Issue
Block a user