mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Smooth motion: plan the frames the clock shows; vrr in settings.toml; atomics
- settings.toml parses [video] vsync = "vrr" and a launcher save writes it back as "vrr" (was dropped / saved as "on"); every mode round-trips in video_enhancement_settings_test. - fg_slots counts the display intervals the clock (fg_clock_phase) fills, with the same 0.985 phase limit. fg_plan_hz and the dynamic resolution share use it: 59.94 Hz content on a 60 Hz panel (2.002 intervals) plans and budgets one in-between frame, as shown, not two. - The real share read on the emulation thread uses atomics: s_fg_flip_vb, s_fg_swap_ema and a published copy of the generated-frame cost estimate. - fg_hud_lerp accepts a match at the 8th 2D triangle of the window. - docs/FRAME_GENERATION.md: the plan's slots and the vrr mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,8 +77,10 @@ has measured time to spare.
|
||||
that lead to it: up to `n/(n+1)` of a game frame later than without
|
||||
generation (R4 at 30 Hz on 120 Hz: 25 ms at n = 3, 17 ms at n = 1).
|
||||
Extrapolation (no delay, guessed motion) is not offered.
|
||||
- **Plan.** `n = fg_plan(...)`: slots = round(flip interval × refresh)
|
||||
(R4 on 120 Hz: 4), limited to what fits in 85 % of the interval after the
|
||||
- **Plan.** `n = fg_plan(...)`: slots = `fg_slots`, the display intervals
|
||||
the clock actually fills (in-between frames at phase k / (flip × refresh)
|
||||
below 0.985, plus the real one: R4 on 120 Hz: 4; 59.94 Hz content on a
|
||||
60 Hz panel, 2.002 intervals: 2, one in-between frame), limited to what fits in 85 % of the interval after the
|
||||
real frames' render-thread CPU time and one swap, at the measured cost of a
|
||||
generated frame (CPU, GPU via `GL_TIME_ELAPSED`, plus a swap). On macOS a
|
||||
real frame's `TIME_ELAPSED` span reads close to the whole interval (it
|
||||
@@ -96,6 +98,15 @@ has measured time to spare.
|
||||
- **Sync points** show the waiting real frame and restart the lists at the
|
||||
next flip.
|
||||
|
||||
- **Variable refresh.** `[video] vsync = "vrr"` (game.toml, or the
|
||||
player's settings.toml, which a launcher save writes back as `"vrr"`):
|
||||
for G-Sync / FreeSync / ProMotion panels. Each frame is presented when it
|
||||
is ready (swap interval 0); Smooth motion targets the panel's maximum
|
||||
refresh at the current size and never exceeds it. The other modes are
|
||||
`"on"` (default), `"immediate"` (`"off"`) and `"adaptive"`.
|
||||
`PSX_HOST_REFRESH_HZ` / `PSX_HOST_REFRESH_MAX_HZ` simulate a display in
|
||||
headless tests.
|
||||
|
||||
## Limits
|
||||
|
||||
- Only GTE-projected triangles move; sprites, lines and fills are the
|
||||
|
||||
@@ -1 +1 @@
|
||||
9fb6c5d611491719d009c97f5703ae028cf91bd91f1499db0011c7f56f952cfa
|
||||
c377987c91eadb52bd563bf8acf162bf5af5249cb7337d2404cdbd6171d6f9f5
|
||||
|
||||
@@ -2893,6 +2893,7 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
if (m == "on" || m == "vsync") { s.vsync = 1; s.has_vsync = true; }
|
||||
else if (m == "off" || m == "immediate"){ s.vsync = 0; s.has_vsync = true; }
|
||||
else if (m == "adaptive") { s.vsync = -1; s.has_vsync = true; }
|
||||
else if (m == "vrr") { s.vsync = 2; s.has_vsync = true; }
|
||||
});
|
||||
if (v.contains("frame_interpolation")) try_get([&]{
|
||||
s.frame_interpolation = toml::find<bool>(v, "frame_interpolation");
|
||||
@@ -3252,7 +3253,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
if (s.has_frame_generation)
|
||||
f << "frame_generation = " << (s.frame_generation ? "true" : "false") << "\n";
|
||||
if (s.has_vsync)
|
||||
f << "vsync = \"" << (s.vsync == 0 ? "immediate" : s.vsync < 0 ? "adaptive" : "on") << "\"\n";
|
||||
f << "vsync = \"" << (s.vsync == 0 ? "immediate" : s.vsync < 0 ? "adaptive"
|
||||
: s.vsync == 2 ? "vrr" : "on") << "\"\n";
|
||||
if (s.has_frame_interpolation)
|
||||
f << "frame_interpolation = " << (s.frame_interpolation ? "true" : "false") << "\n";
|
||||
if (s.has_frame_interpolation_fps)
|
||||
|
||||
@@ -354,6 +354,25 @@ static void test_pipeline_user_settings() {
|
||||
check(back.has_frame_generation && back.frame_generation, "frame_generation round-trips");
|
||||
fs::remove(p);
|
||||
|
||||
/* [video] vsync: every mode parses from settings.toml and a launcher
|
||||
* save writes back the same mode (vrr = 2 included). */
|
||||
const struct { const char *text; int value; } vs[] = {
|
||||
{ "on", 1 }, { "immediate", 0 }, { "adaptive", -1 }, { "vrr", 2 } };
|
||||
for (const auto &m : vs) {
|
||||
p = write_temp("psxrecomp_vsync_read.toml",
|
||||
std::string("[video]\nvsync = \"") + m.text + "\"\n");
|
||||
auto vr = PSXRecompV4::load_user_settings(p);
|
||||
check(vr.has_vsync && vr.vsync == m.value, "settings vsync parses every mode");
|
||||
fs::remove(p);
|
||||
PSXRecompV4::UserSettings vo;
|
||||
vo.has_vsync = true; vo.vsync = m.value;
|
||||
p = fs::temp_directory_path() / "psxrecomp_vsync_rt.toml";
|
||||
check(PSXRecompV4::save_user_settings(p, vo), "save_user_settings writes vsync");
|
||||
auto vb = PSXRecompV4::load_user_settings(p);
|
||||
check(vb.has_vsync && vb.vsync == m.value, "settings vsync round-trips every mode");
|
||||
fs::remove(p);
|
||||
}
|
||||
|
||||
PSXRecompV4::UserSettings none;
|
||||
p = fs::temp_directory_path() / "psxrecomp_pipe_none.toml";
|
||||
check(PSXRecompV4::save_user_settings(p, none), "save_user_settings writes defaults");
|
||||
|
||||
@@ -148,7 +148,11 @@ 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. */
|
||||
/* fg_slots: display intervals a game frame `flip_s` long fills on a
|
||||
* `refresh_hz` display as the clock (fg_clock_phase) schedules it: the
|
||||
* in-between frames it shows plus the real one. fg_plan_hz: the refresh
|
||||
* fg_plan sees, so its slots are exactly fg_slots. */
|
||||
int fg_slots(double flip_s, double refresh_hz);
|
||||
double fg_plan_hz(double flip_s, double refresh_hz);
|
||||
/* The next present time on the global grid after presenting at `now`. */
|
||||
uint64_t fg_next_due(uint64_t due, uint64_t now, uint64_t step_ns);
|
||||
|
||||
+25
-8
@@ -661,12 +661,25 @@ double fg_step_s(double flip_s, double refresh_hz, int n) {
|
||||
return flip_s / (double)(n + 1);
|
||||
}
|
||||
|
||||
/* The last phase fg_clock_phase still generates; at or past it the next
|
||||
* real frame is due. Shared with fg_slots so the plan (and the dynamic
|
||||
* resolution budget) counts exactly the frames the clock shows. */
|
||||
#define FG_PHASE_LAST 0.985
|
||||
|
||||
int fg_slots(double flip_s, double refresh_hz) {
|
||||
if (flip_s <= 0.0 || refresh_hz <= 0.0) return 0;
|
||||
/* The clock generates at phase k / (flip_s * refresh_hz), k = 1, 2, ...
|
||||
* while below FG_PHASE_LAST: 59.94 Hz content on 60 Hz (2.002
|
||||
* intervals) shows one, at 100 Hz (3.34) three, on 120 Hz (4.004) three. */
|
||||
const double step = 1.0 / (flip_s * refresh_hz);
|
||||
int k = 1;
|
||||
while (k < 64 && (double)k * step < FG_PHASE_LAST) k++;
|
||||
return k; /* the shown in-between frames + the real one */
|
||||
}
|
||||
|
||||
double fg_plan_hz(double flip_s, double refresh_hz) {
|
||||
if (flip_s <= 0.0 || refresh_hz <= 0.0) return refresh_hz;
|
||||
/* Room for every display interval a game frame can hold (3.34 at
|
||||
* 100 Hz -> 4 slots); the clock drops the one that does not fit. */
|
||||
double slots = ceil(flip_s * refresh_hz - 1e-6);
|
||||
return slots / flip_s;
|
||||
return (double)fg_slots(flip_s, refresh_hz) / flip_s;
|
||||
}
|
||||
|
||||
uint64_t fg_next_due(uint64_t due, uint64_t now, uint64_t step_ns) {
|
||||
@@ -684,7 +697,7 @@ double fg_clock_phase(uint64_t since_real_ns, uint64_t step_ns, uint64_t flip_ns
|
||||
* 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;
|
||||
if (t >= FG_PHASE_LAST) return 0.0;
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -803,6 +816,7 @@ int fg_ceiling_get(FgCeiling *c, double now) {
|
||||
* 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. */
|
||||
#define FG_HUD_WINDOW 8u
|
||||
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;
|
||||
@@ -810,14 +824,17 @@ void fg_hud_lerp(const FgPrimList *L, const FgPrimList *O, double u, float *x, f
|
||||
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;
|
||||
/* The match is searched among the next FG_HUD_WINDOW 2D triangles of
|
||||
* O (a match on the last of them counts). */
|
||||
uint32_t m = o, seen = 0;
|
||||
for (; m < O->n && seen < 8u; m++) {
|
||||
int found = 0;
|
||||
for (; m < O->n && seen < FG_HUD_WINDOW; 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 (b->key == a->key && b->view == a->view) { found = 1; break; }
|
||||
}
|
||||
if (m >= O->n || seen >= 8u) continue;
|
||||
if (!found) continue;
|
||||
const FgPrim *b = &O->v[m];
|
||||
o = m + 1;
|
||||
int ok = 1, moved = 0;
|
||||
|
||||
@@ -10158,7 +10158,10 @@ static int s_fg_have_last = 0, s_fg_last_dx = 0, s_fg_last_dy = 0;
|
||||
* when drawing moves to another buffer (games flip at a VBlank after they
|
||||
* 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 int s_fg_vblanks = 0;
|
||||
/* Presents per flip. Written by the render thread, read by the emulation
|
||||
* thread (gl_renderer_frame_gen_real_share): atomic. */
|
||||
static _Atomic int s_fg_flip_vb = 1;
|
||||
/* 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,
|
||||
@@ -10241,10 +10244,14 @@ static int fg_recently_generated(void) {
|
||||
/* Render thread: every swap's wall time. A swap waits for the compositor
|
||||
* (and on a busy WindowServer for its round trip); each generated frame adds
|
||||
* one, so the plan counts it. */
|
||||
static double s_fg_swap_ema = 0.0;
|
||||
/* Written by the render thread, read by the emulation thread
|
||||
* (gl_renderer_frame_gen_real_share): atomic, as is the published copy of
|
||||
* the generated-frame cost estimate (s_fg_cost.ema, render thread only). */
|
||||
static _Atomic double s_fg_swap_ema = 0.0;
|
||||
static _Atomic double s_fg_cost_ema_pub = 0.0;
|
||||
static void fg_note_swap(uint64_t ns) {
|
||||
const double c = (double)ns * 1e-9;
|
||||
s_fg_swap_ema = s_fg_swap_ema > 0.0 ? s_fg_swap_ema * 0.9 + c * 0.1 : c;
|
||||
const double c = (double)ns * 1e-9, e = atomic_load(&s_fg_swap_ema);
|
||||
atomic_store(&s_fg_swap_ema, e > 0.0 ? e * 0.9 + c * 0.1 : c);
|
||||
}
|
||||
|
||||
/* A real VBlank frame's render-thread cost, for the plan: its CPU time. Its
|
||||
@@ -11017,6 +11024,7 @@ static void fg_gen_cost_poll(void) {
|
||||
p_glGetQueryObjectui64v(s_fg_q[i], GL_QUERY_RESULT, &ns);
|
||||
double c = (double)(ns > s_fg_q_cpu[i] ? ns : s_fg_q_cpu[i]) * 1e-9;
|
||||
fg_cost_add(fg_cost(), c, s_fg_fit_s);
|
||||
atomic_store(&s_fg_cost_ema_pub, fg_cost()->ema);
|
||||
s_fg_gen_gpu_ms = (double)ns * 1e-6;
|
||||
s_fg_gen_cpu_ms = (double)s_fg_q_cpu[i] * 1e-6;
|
||||
s_fg_qt++;
|
||||
@@ -11220,8 +11228,10 @@ static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stal
|
||||
}
|
||||
if (s_fg_pending) fg_flush();
|
||||
s_fg_flips++;
|
||||
s_fg_flip_vb = s_fg_have_last ? s_fg_vblanks : 1;
|
||||
if (s_fg_flip_vb > 4) s_fg_flip_vb = 4;
|
||||
{
|
||||
const int vb = s_fg_have_last ? s_fg_vblanks : 1;
|
||||
atomic_store(&s_fg_flip_vb, vb > 4 ? 4 : vb);
|
||||
}
|
||||
s_fg_vblanks = 0;
|
||||
s_fg_have_last = 1; s_fg_last_dx = disp[0]; s_fg_last_dy = disp[1];
|
||||
/* A game that flips right after drawing: the capturing list is this
|
||||
@@ -11269,7 +11279,7 @@ static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stal
|
||||
FG_BRK_INIT();
|
||||
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);
|
||||
const int slots = (int)floor(flip_s * rhz + 0.5);
|
||||
const int slots = fg_slots(flip_s, rhz); /* as the clock shows them */
|
||||
s_fg_last_slots = slots;
|
||||
/* What one generated frame may cost (its swap included) to fit. */
|
||||
const double real_s = s_fg_real_ema * (double)s_fg_flip_vb + s_fg_swap_ema;
|
||||
@@ -11293,6 +11303,7 @@ static int fg_on_present(uint16_t op, const uint8_t *p, uint32_t bytes, int stal
|
||||
if (s_fg_force) n = slots > 1 ? slots - 1 : 1;
|
||||
else if (fg_breaker_open(&s_fg_brk, now) && !stale && !held) {
|
||||
const double est = fg_cost_estimate(fg_cost(), now, s_fg_fit_s);
|
||||
atomic_store(&s_fg_cost_ema_pub, fg_cost()->ema);
|
||||
n = fg_plan(flip_s, fg_plan_hz(flip_s, rhz), real_s, est > 0.0 ? est + s_fg_swap_ema : 0.0, 0.85,
|
||||
fg_ceiling_get(fg_ceil(), now));
|
||||
}
|
||||
@@ -11413,10 +11424,14 @@ void gl_renderer_fg_source(const uint32_t id[3], const int32_t pc[9], const int3
|
||||
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;
|
||||
const int vb = atomic_load(&s_fg_flip_vb);
|
||||
const double flip_s = (double)vb / (ghz > 1.0 ? ghz : 59.94);
|
||||
if (flip_s <= 0.0 || rhz <= 0.0 || vb < 2) return 1.0;
|
||||
/* The frames the clock actually shows (fg_slots): 59.94 Hz content on a
|
||||
* 60 Hz panel is 2.002 intervals, one in-between frame, not two. */
|
||||
const int slots = fg_slots(flip_s, rhz);
|
||||
const double ema = atomic_load(&s_fg_cost_ema_pub);
|
||||
const double g = ema > 0.0 ? ema + atomic_load(&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;
|
||||
|
||||
@@ -325,8 +325,14 @@ static void test_any_rate(void) {
|
||||
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;
|
||||
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;
|
||||
@@ -349,6 +355,10 @@ static void test_any_rate(void) {
|
||||
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];
|
||||
@@ -356,10 +366,29 @@ static void test_any_rate(void) {
|
||||
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);
|
||||
(void)gens;
|
||||
/* 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);
|
||||
@@ -375,10 +404,28 @@ static void test_hud_lerp(void) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_hud_lerp();
|
||||
test_slots_5994();
|
||||
test_any_rate();
|
||||
test_ceiling();
|
||||
test_cost();
|
||||
|
||||
Reference in New Issue
Block a user