Files
psxrecomp/runtime/tests/test_render_pass_motion.c
T

238 lines
11 KiB
C

/* render_pass_motion.c and render_pass_frame.c against a mock guest RAM and
* mock render-pass / mod APIs. */
#include "mod_plugins.h"
#include "render_pass_motion.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
static int failures;
#define CHECK(cond, msg) do { if (!(cond)) { failures++; printf("FAIL: %s\n", msg); } } while (0)
/* ---- mock guest RAM ------------------------------------------------------ */
static uint8_t s_ram[0x10000];
static uint32_t off(uint32_t a) { return a & 0xFFFFu; }
uint8_t psx_mod_read_byte(uint32_t a) { return s_ram[off(a)]; }
uint16_t psx_mod_read_half(uint32_t a) { uint16_t v; memcpy(&v, s_ram + off(a), 2); return v; }
uint32_t psx_mod_read_word(uint32_t a) { uint32_t v; memcpy(&v, s_ram + off(a), 4); return v; }
void psx_mod_write_byte(uint32_t a, uint8_t v) { s_ram[off(a)] = v; }
void psx_mod_write_half(uint32_t a, uint16_t v) { memcpy(s_ram + off(a), &v, 2); }
void psx_mod_write_word(uint32_t a, uint32_t v) { memcpy(s_ram + off(a), &v, 4); }
/* ---- mock mod / pass APIs (render_pass_frame.c) --------------------------- */
static char s_option[16] = "display";
static uint32_t s_fps = 99, s_source = 99, s_blend = 99, s_flip = 99, s_blend_calls;
static uint32_t s_status, s_phases, s_period, s_shown, s_passes_run;
int psx_mod_option_value(const char* p, const char* f, const char* o, char* out, uint32_t n) {
(void)p; (void)f; (void)o;
snprintf(out, n, "%s", s_option);
return 1;
}
int psx_mod_set_frame_interpolation(uint32_t fps) { s_fps = fps; return 1; }
int psx_mod_set_frame_interpolation_source(uint32_t s) { s_source = s; return 1; }
int psx_mod_set_frame_interpolation_blend(uint32_t b) { s_blend = b; s_blend_calls++; return 1; }
int psx_mod_set_render_pass_flip(uint32_t m) { s_flip = m; return 1; }
uint32_t psx_mod_render_pass_status(void) { return s_status; }
uint32_t psx_mod_render_pass_plan(uint32_t period, uint32_t shown, uint32_t* a, uint32_t max) {
s_period = period; s_shown = shown;
if (s_status != PSX_MOD_RENDER_PASS_READY) return 0;
for (uint32_t i = 0; i < s_phases && i < max; i++) a[i] = (i + 1) * 65536u / (s_phases + 1);
return s_phases < max ? s_phases : max;
}
int psx_mod_render_pass(struct CPUState* cpu, const PSXModRenderPass* p,
PSXModRenderPassFn fn, void* user) {
s_passes_run++;
return fn(cpu, user, p->alpha_q16);
}
/* ---- helpers --------------------------------------------------------------- */
static void put_matrix(uint32_t a, double yaw, int32_t x, int32_t y, int32_t z) {
PSXMotionMatrix m;
const double c = cos(yaw), s = sin(yaw);
const double r[9] = {c, 0, s, 0, 1, 0, -s, 0, c};
for (int i = 0; i < 9; i++) m.r[i] = (int16_t)lround(r[i] * 4096.0);
m.t[0] = x; m.t[1] = y; m.t[2] = z;
psx_motion_write_matrix(a, &m, 1);
}
static double yaw_of(uint32_t a) {
PSXMotionMatrix m;
psx_motion_read_matrix(a, &m);
return atan2(m.r[2] / 4096.0, m.r[0] / 4096.0);
}
static void test_matrix_slerp(void) {
PSXMotionSet* s = psx_motion_set_create(16);
PSXMotionLimits lim = {1000.0, 0};
PSXMotionStats st;
PSXMotionMatrix m;
put_matrix(0x100, 0.0, 0, 0, 0);
psx_motion_begin(s, 10);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x100, 7);
CHECK(psx_motion_prepare(s, &lim, &st) == 0 && st.ticks == 0, "first frame has no history");
put_matrix(0x100, 1.2, 800, 0, -400);
psx_motion_begin(s, 12);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x100, 7);
CHECK(psx_motion_prepare(s, &lim, &st) == 1 && st.ticks == 2 && st.blended == 1,
"the same object two ticks later blends");
psx_motion_apply(s, 0.5);
psx_motion_read_matrix(0x100, &m);
CHECK(fabs(yaw_of(0x100) - 0.6) < 0.002, "rotation slerps to the half-way angle");
CHECK(m.t[0] == 400 && m.t[2] == -200, "translation blends linearly");
{
double len = 0;
for (int j = 0; j < 3; j++) len += (double)m.r[j] * m.r[j];
CHECK(fabs(sqrt(len) - 4096.0) < 3.0, "the blended rotation stays a rotation");
}
CHECK(psx_motion_blend_at(s, PSX_MOTION_MATRIX, 0x100, 1.0, &m) == 1 &&
m.t[0] == 800, "blend_at at t=1 is the current value");
psx_motion_set_destroy(s);
}
static void test_identity_and_limits(void) {
PSXMotionSet* s = psx_motion_set_create(16);
PSXMotionLimits lim = {1000.0, 1.0};
PSXMotionStats st;
int32_t v[3];
put_matrix(0x200, 0.0, 0, 0, 0); /* moves within limits */
put_matrix(0x300, 0.0, 0, 0, 0); /* reused by another object */
put_matrix(0x400, 0.0, 0, 0, 0); /* teleports */
put_matrix(0x500, 0.0, 0, 0, 0); /* spins past the turn limit */
psx_motion_begin(s, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x200, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x300, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x400, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x500, 1);
psx_motion_prepare(s, &lim, &st);
put_matrix(0x200, 0.0, 1500, 0, 0);
put_matrix(0x300, 0.0, 10, 0, 0);
put_matrix(0x400, 0.0, 5000, 0, 0);
put_matrix(0x500, 2.0, 0, 0, 0);
psx_motion_begin(s, 3); /* two ticks: 2000 units allowed */
psx_motion_track(s, PSX_MOTION_MATRIX, 0x200, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x300, 2);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x400, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x500, 1);
psx_motion_track(s, PSX_MOTION_VECTOR, 0x600, 1); /* new this frame */
CHECK(psx_motion_prepare(s, &lim, &st) == 1, "only the moving object blends");
CHECK(st.placed == 2 && st.unmatched == 2 && st.tracked == 5,
"teleport and spin are placed; reuse and new are unmatched");
psx_motion_apply(s, 0.5);
CHECK((int32_t)psx_mod_read_word(0x300 + 0x14) == 10, "a reused address keeps its new object");
CHECK((int32_t)psx_mod_read_word(0x400 + 0x14) == 5000, "a teleport is drawn where it landed");
CHECK((int32_t)psx_mod_read_word(0x200 + 0x14) == 750, "the mover is half-way");
CHECK(psx_motion_blend_at(s, PSX_MOTION_VECTOR, 0x600, 0.5, v) == 0, "no history: current value");
CHECK(psx_motion_blend_at(s, PSX_MOTION_VECTOR, 0x700, 0.5, v) == -1, "untracked: refused");
/* A gap (a frame not captured) or too many ticks: nothing blends. */
psx_motion_begin(s, 20);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x200, 1);
CHECK(psx_motion_prepare(s, &lim, &st) == 0 && st.ticks == 0, "17 ticks apart is not one motion");
psx_motion_invalidate(s);
psx_motion_begin(s, 21);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x200, 1);
CHECK(psx_motion_prepare(s, &lim, &st) == 0, "after invalidate there is no history");
psx_motion_set_destroy(s);
}
static void test_angles_vectors_scalars(void) {
PSXMotionSet* s = psx_motion_set_create(16);
PSXMotionLimits lim = {100.0, 0};
PSXMotionStats st;
psx_mod_write_half(0x800, 4000); psx_mod_write_half(0x802, 100); psx_mod_write_half(0x804, 0);
psx_mod_write_half(0x810, (uint16_t)-50); psx_mod_write_half(0x812, 0); psx_mod_write_half(0x814, 0);
psx_mod_write_word(0x820, 1000);
psx_motion_begin(s, 5);
psx_motion_track(s, PSX_MOTION_ANGLES, 0x800, 1);
psx_motion_track(s, PSX_MOTION_SVECTOR, 0x810, 1);
psx_motion_track(s, PSX_MOTION_SCALAR, 0x820, 1);
psx_motion_prepare(s, &lim, &st);
psx_mod_write_half(0x800, 96); psx_mod_write_half(0x802, 300); psx_mod_write_half(0x804, 2048);
psx_mod_write_half(0x810, 50);
psx_mod_write_word(0x820, 1100);
psx_motion_begin(s, 6);
psx_motion_track(s, PSX_MOTION_ANGLES, 0x800, 1);
psx_motion_track(s, PSX_MOTION_SVECTOR, 0x810, 1);
psx_motion_track(s, PSX_MOTION_SCALAR, 0x820, 1);
CHECK(psx_motion_prepare(s, &lim, &st) == 3, "angles, svector and scalar blend");
psx_motion_apply(s, 0.5);
CHECK(psx_mod_read_half(0x800) == 0, "4000 -> 96 crosses 4096 the short way (0)");
CHECK(psx_mod_read_half(0x802) == 200, "100 -> 300 is 200");
CHECK(psx_mod_read_half(0x804) == 1024, "0 -> 2048 half turn blends to 1024");
CHECK((int16_t)psx_mod_read_half(0x810) == 0, "signed svector -50 -> 50 is 0");
CHECK(psx_mod_read_word(0x820) == 1050, "scalar blends");
psx_motion_set_destroy(s);
}
static void test_rotation_only(void) {
PSXMotionSet* s = psx_motion_set_create(4);
PSXMotionLimits lim = {100.0, 0};
PSXMotionStats st;
/* A bare rotation (as in a pose record) with other data right after it. */
put_matrix(0xA00, 0.0, 0, 0, 0);
psx_mod_write_word(0xA00 + 0x12, 0xDEADBEEFu);
psx_motion_begin(s, 1);
psx_motion_track(s, PSX_MOTION_ROTATION, 0xA00, 3);
psx_motion_prepare(s, &lim, &st);
put_matrix(0xA00, 1.0, 0, 0, 0);
psx_mod_write_word(0xA00 + 0x12, 0xDEADBEEFu);
psx_motion_begin(s, 2);
psx_motion_track(s, PSX_MOTION_ROTATION, 0xA00, 3);
CHECK(psx_motion_prepare(s, &lim, &st) == 1, "a bare rotation blends");
psx_motion_apply(s, 0.5);
CHECK(fabs(yaw_of(0xA00) - 0.5) < 0.002, "rotation-only slerps");
CHECK(psx_mod_read_word(0xA00 + 0x12) == 0xDEADBEEFu, "nothing past the 18 bytes is written");
psx_motion_set_destroy(s);
}
static int s_fn_calls;
static double s_fn_last;
static int frame_fn(struct CPUState* cpu, void* user, uint32_t alpha) {
(void)cpu; (void)user;
s_fn_calls++;
s_fn_last = alpha / 65536.0;
return 1;
}
static void test_frame_driver(void) {
PSXModRenderPassFrame f;
char cpu_storage[4096];
struct CPUState* cpu = (struct CPUState*)cpu_storage;
strcpy(s_option, "165");
CHECK(psx_mod_activate_render_pass_rate("p", "f", "rate", PSX_MOD_RENDER_PASS_FLIP_PENDING) &&
s_fps == 165 && s_source == PSX_MOD_FRAME_SOURCE_FLIP &&
s_blend == PSX_MOD_FRAME_INTERPOLATION_HOLD &&
s_flip == PSX_MOD_RENDER_PASS_FLIP_PENDING, "activation selects rate, FLIP, HOLD, flip mode");
strcpy(s_option, "display");
psx_mod_activate_render_pass_rate("p", "f", "rate", PSX_MOD_RENDER_PASS_FLIP_SHOWN);
CHECK(s_fps == 0 && s_flip == PSX_MOD_RENDER_PASS_FLIP_SHOWN, "display refresh is rate 0");
memset(&f, 0, sizeof f);
f.struct_size = sizeof f;
f.period_vblanks = 2; f.w = 320; f.h = 240;
s_status = PSX_MOD_RENDER_PASS_READY;
s_phases = 3;
s_blend_calls = 0;
CHECK(psx_mod_render_pass_frame(cpu, &f, frame_fn, NULL) == 3 && s_fn_calls == 3 &&
s_period == 2 && s_fn_last > 0.7, "every planned phase runs");
CHECK(s_blend_calls == 0, "READY keeps HOLD without re-setting it");
s_status = PSX_MOD_RENDER_PASS_BACKEND;
CHECK(psx_mod_render_pass_frame(cpu, &f, frame_fn, NULL) == 0 &&
s_blend == PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE, "lasting refusal crossfades");
s_status = PSX_MOD_RENDER_PASS_SESSION;
psx_mod_render_pass_frame(cpu, &f, frame_fn, NULL);
CHECK(s_blend == PSX_MOD_FRAME_INTERPOLATION_HOLD, "a transient reason returns to HOLD");
f.struct_size = 4;
CHECK(psx_mod_render_pass_frame(cpu, &f, frame_fn, NULL) == 0, "a short struct is refused");
}
int main(void) {
test_matrix_slerp();
test_identity_and_limits();
test_angles_vectors_scalars();
test_rotation_only();
test_frame_driver();
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}