mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge feat/render-pass-kit (motion kit, frame driver)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,125 @@ back to `HOLD` when the status is `READY` again. A backend mode that cannot
|
||||
host passes (for example a renderer path whose VRAM is not one texture) should
|
||||
report `BACKEND` from `gl_renderer_pass_unavailable()`.
|
||||
|
||||
## Flip modes
|
||||
|
||||
`psx_mod_set_render_pass_flip()` says how the game flips relative to the pass
|
||||
point (default `PENDING`, reset at every session start):
|
||||
|
||||
- `PENDING`: frame N is drawn and waits; the next flip shows the pass rect.
|
||||
This is the PsyQ `VSync(0)`-then-`PutDispEnv` loop, and also any game that
|
||||
flips only after the next frame is submitted (Vigilante 8: 2nd Offense
|
||||
flips from a VBlank callback armed by the next submit). Phase 0 is
|
||||
re-captured when the flip is seen, so a game that adds a last layer to the
|
||||
shown buffer after the pass point (V8:2 draws its HUD at the next submit)
|
||||
shows its own final image at phase 0.
|
||||
- `SHOWN`: the game flips each frame as soon as it is drawn, so at the pass
|
||||
point frame N is already on screen. The rect is that on-screen rect; frame
|
||||
N and its in-betweens are shown from the next flip, one game frame later
|
||||
than stock (the latency of any interpolator with one frame of history).
|
||||
|
||||
Decide by decoding the game's DISPENV / DRAWENV tables and where it calls
|
||||
`PutDispEnv` and `DrawOTag`, not by assumption: in V8:2 `DISPENV[i]` shows
|
||||
the buffer `DRAWENV[1-i]` draws.
|
||||
|
||||
## Replaying a span of frame code
|
||||
|
||||
Re-implementing a game's per-frame draw call sequence in a plugin copies one
|
||||
path through it (HUD, messages, pause menu, replay mode each branch
|
||||
differently) and drifts from it. Instead, a plugin records the registers at
|
||||
the start of the game's draw section during the real frame (an instruction
|
||||
hook, `mod_instruction_sites`) and, inside a pass, replays that section:
|
||||
|
||||
```c
|
||||
int psx_mod_run_guest_span(struct CPUState *cpu, uint32_t start_pc, uint32_t stop_pc);
|
||||
```
|
||||
|
||||
Every PC in `[start_pc, stop_pc)` is interpreted from its RAM bytes, even
|
||||
where a compiled body or a compiled resume point (the return address of a
|
||||
call) begins, so the span may start in the middle of a compiled function.
|
||||
Calls the span makes leave the range as plain transfers and run with the
|
||||
full dispatch trampoline back to their return address (compiled code is CPS:
|
||||
a callee's own calls and returns go through the trampoline). A call may be
|
||||
the span's last instruction. Anything else that leaves the range ends the
|
||||
span with failure. Only inside a pass: the sandbox rolls a failed or runaway
|
||||
span back, and the watchdog landing closes an open span. The span is a
|
||||
replay of the game's own instructions, never a substitute for code the
|
||||
recompiler could not translate; it stays out of capture, alias seeding and
|
||||
per-PC entry statistics, and fires instruction hooks as compiled code does.
|
||||
`render_pass_stats` counts `spans` / `span_failures` and keeps the last
|
||||
failure (`span_fail`: reason, exit PC, `$ra`, post-call PC).
|
||||
|
||||
## Blending game state (render_pass_motion.h)
|
||||
|
||||
What makes a pass an in-between image is the state the draw code reads,
|
||||
placed part of the way between two logic ticks. `render_pass_motion.h` holds
|
||||
that for any title:
|
||||
|
||||
```c
|
||||
psx_motion_begin(set, tick); /* each frame, capture point */
|
||||
psx_motion_track(set, PSX_MOTION_MATRIX, obj + 0x20, psx_motion_identity(type, fn, mesh));
|
||||
psx_motion_prepare(set, &limits, &stats); /* pair with the previous frame */
|
||||
/* in each pass */
|
||||
psx_motion_apply(set, alpha); /* write blends to guest RAM */
|
||||
psx_motion_blend_at(set, kind, addr, alpha, &out); /* a value a hook hands on */
|
||||
```
|
||||
|
||||
Kinds: PsyQ `MATRIX` (slerped rotation, linear translation), its translation
|
||||
only (billboards), `VECTOR`, `SVECTOR`, 12-bit `ANGLES` (shorter arc) and a
|
||||
`SCALAR`. Values pair by address and identity, so a heap block reused by
|
||||
another object never blends between the two. `limits.move_per_tick` (game
|
||||
units) and `limits.turn_radians` separate motion from placement: a value
|
||||
past them was respawned, teleported or cut to and keeps its current value.
|
||||
Measure the game's real motion first; V8:2's world spans about 2^27 units
|
||||
per axis and moves up to 2^16 per frame.
|
||||
|
||||
## Per-frame driver
|
||||
|
||||
```c
|
||||
int psx_mod_activate_render_pass_rate(const char *pkg, const char *feature,
|
||||
const char *option, uint32_t flip_mode);
|
||||
uint32_t psx_mod_render_pass_frame(struct CPUState *cpu,
|
||||
const PSXModRenderPassFrame *frame,
|
||||
PSXModRenderPassFn fn, void *user);
|
||||
```
|
||||
|
||||
The first reads a choice option (`display` or a rate) and selects the FLIP
|
||||
source, HOLD blend, the flip mode and the rate. The second plans one game
|
||||
frame, runs `fn` at each phase into the frame's rect, and switches the
|
||||
presenter to a crossfade while passes are unavailable for a lasting reason
|
||||
(back to HOLD when they return).
|
||||
|
||||
## Porting a title
|
||||
|
||||
1. Find the main loop's draw section: the range from camera setup through
|
||||
OT build and HUD to the submit call, and every call in it.
|
||||
2. Decode DISPENV/DRAWENV and the submit/flip path: which buffer the next
|
||||
flip shows at the pass point, and in what order the game draws into it
|
||||
(immediate `DrawOTag`, VBlank callback). Pick the flip mode.
|
||||
3. Find the pass point (a function entry after the section's OT build, before
|
||||
submit) and the logic tick counter.
|
||||
4. Find what the draw code reads that moves: the camera input (often the
|
||||
argument of a set-camera function: track it with `blend_at` from an entry
|
||||
hook), object transforms and their identity, particles.
|
||||
5. Measure motion per frame with histograms (mod counters) before setting
|
||||
limits.
|
||||
6. Plugin: instruction hook at the section start (registers), capture at the
|
||||
pass point, `psx_mod_render_pass_frame` with a pass function that applies
|
||||
the blend, replays the span and submits as the screen gets it.
|
||||
7. Verify in `--hidden-window` (below): `render_pass_stats` passes with no
|
||||
span failures, `render_pass_dump` images between their neighbours,
|
||||
`PSX_RENDER_PASS_VERIFY=1` with `verify_mismatch` 0.
|
||||
|
||||
`Vigilante82PSXRecomp/src/mods/v82_frame_rate_plugin.cpp` is the worked
|
||||
example.
|
||||
|
||||
## Testing without a window
|
||||
|
||||
`--hidden-window` runs the full windowed runtime (presenter, interpolation,
|
||||
render passes) in a window that is never shown and never opens a modal or a
|
||||
file picker. `--headless` skips the presenter, so plans there are always
|
||||
empty. Drive it over TCP; set `SDL_AUDIODRIVER=dummy` to keep it silent.
|
||||
|
||||
## What a pass may and may not do
|
||||
|
||||
While `fn` runs (`g_psx_render_pass_active`):
|
||||
|
||||
@@ -811,6 +811,18 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME render_pass_plan_test COMMAND render_pass_plan_test)
|
||||
|
||||
# Motion kit and per-frame driver for render-pass plugins: blending,
|
||||
# identity matching, limits and the crossfade fallback, on mock RAM.
|
||||
add_executable(render_pass_motion_test
|
||||
tests/test_render_pass_motion.c
|
||||
src/render_pass_motion.c
|
||||
src/render_pass_frame.c)
|
||||
target_include_directories(render_pass_motion_test PRIVATE include)
|
||||
if(NOT MSVC)
|
||||
target_link_libraries(render_pass_motion_test PRIVATE m)
|
||||
endif()
|
||||
add_test(NAME render_pass_motion_test COMMAND render_pass_motion_test)
|
||||
|
||||
# Render-pass time freeze against mock devices (psx_cycles.c only).
|
||||
add_executable(render_pass_freeze_test
|
||||
tests/test_render_pass_freeze.c
|
||||
|
||||
@@ -419,6 +419,32 @@ enum {
|
||||
};
|
||||
int psx_mod_set_render_pass_flip(uint32_t mode);
|
||||
|
||||
/*
|
||||
* The per-frame plumbing every frame-rate plugin repeats (render_pass_frame.c).
|
||||
* Blending the game state itself: render_pass_motion.h.
|
||||
*
|
||||
* Activation: read a choice option holding "display" (follow the measured
|
||||
* display refresh) or a frame rate, and select the FLIP source, HOLD blend,
|
||||
* `flip_mode` and that rate. Returns 0 if any setting was refused.
|
||||
*/
|
||||
int psx_mod_activate_render_pass_rate(const char* package, const char* feature,
|
||||
const char* option, uint32_t flip_mode);
|
||||
typedef struct PSXModRenderPassFrame {
|
||||
uint32_t struct_size; /* sizeof(PSXModRenderPassFrame) */
|
||||
uint32_t period_vblanks; /* VBlanks this game frame stays on screen */
|
||||
uint32_t shown_after_vblanks; /* VBlank presents before it is shown */
|
||||
uint16_t x, y, w, h; /* VRAM rect the passes draw */
|
||||
} PSXModRenderPassFrame;
|
||||
/*
|
||||
* Plan this game frame's passes and run `fn` at each phase. While passes are
|
||||
* unavailable for a lasting reason (BACKEND, DISABLED) the presenter is
|
||||
* switched to a motion-adaptive crossfade, and back to HOLD once they return.
|
||||
* Returns how many passes produced an image.
|
||||
*/
|
||||
uint32_t psx_mod_render_pass_frame(struct CPUState* cpu,
|
||||
const PSXModRenderPassFrame* frame,
|
||||
PSXModRenderPassFn fn, void* user);
|
||||
|
||||
/*
|
||||
* Replay part of an already-loaded guest function inside a render pass:
|
||||
* run from start_pc with the CPU state given until control reaches stop_pc.
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#ifndef PSX_RENDER_PASS_MOTION_H
|
||||
#define PSX_RENDER_PASS_MOTION_H
|
||||
/*
|
||||
* Motion kit for render-pass plugins (docs/RENDER_PASSES.md, "Blending game
|
||||
* state"). A pass redraws the scene with the game's own code between two
|
||||
* logic ticks; what makes the image an in-between one is the game state the
|
||||
* draw code reads, placed part of the way from the previous tick to the
|
||||
* current one. This kit owns that part for any title:
|
||||
*
|
||||
* per game frame, at the plugin's capture point
|
||||
* psx_motion_begin(set, tick) the game's logic tick counter
|
||||
* psx_motion_track(set, kind, addr, identity) for each value the draw
|
||||
* code reads (object matrices,
|
||||
* positions, angles, the camera)
|
||||
* psx_motion_prepare(set, &limits, &stats) pair with the previous frame
|
||||
* in each pass
|
||||
* psx_motion_apply(set, alpha) write the blends into guest RAM
|
||||
* psx_motion_blend_at(set, addr, ...) or blend one value a hook hands on
|
||||
*
|
||||
* Values are matched by address AND identity: game objects are heap blocks,
|
||||
* and a freed address reused by another object must never blend between the
|
||||
* two. A value that moved further than the limits allow in the ticks between
|
||||
* the frames was placed (respawn, teleport, camera cut), not moved; it keeps
|
||||
* its current value, as do values without history. Nothing is written
|
||||
* outside psx_motion_apply, which a pass's sandbox rolls back.
|
||||
*
|
||||
* Guest RAM is read and written through psx_mod_read_* / psx_mod_write_*.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* What lives at a tracked address (guest RAM layout). */
|
||||
enum {
|
||||
/* PsyQ MATRIX: 3x3 s16 rotation in 4.12 at +0x00..+0x11, s32 t[3] at
|
||||
* +0x14. Rotation slerps when both rows are orthonormal, otherwise it
|
||||
* blends elementwise; translation blends linearly. */
|
||||
PSX_MOTION_MATRIX = 1,
|
||||
/* Only the translation of a MATRIX (+0x14..+0x1F): for matrices whose
|
||||
* rotation the draw code rebuilds itself (billboards). */
|
||||
PSX_MOTION_MATRIX_TRANSLATION = 2,
|
||||
PSX_MOTION_VECTOR = 3, /* s32 x, y, z */
|
||||
PSX_MOTION_SVECTOR = 4, /* s16 x, y, z */
|
||||
/* s16 x, y, z angles, 4096 per turn: each blends along the shorter arc. */
|
||||
PSX_MOTION_ANGLES = 5,
|
||||
PSX_MOTION_SCALAR = 6 /* s32 */
|
||||
};
|
||||
|
||||
typedef struct PSXMotionLimits {
|
||||
/* Distance one value may move per logic tick and still blend
|
||||
* (translation, VECTOR, SVECTOR, SCALAR, in the game's units). */
|
||||
double move_per_tick;
|
||||
/* Rotation between the two frames beyond which a value snaps
|
||||
* (MATRIX rotation, ANGLES). 0 means pi (any turn up to a half turn). */
|
||||
double turn_radians;
|
||||
} PSXMotionLimits;
|
||||
|
||||
typedef struct PSXMotionStats {
|
||||
uint32_t tracked; /* values captured this frame */
|
||||
uint32_t blended; /* ... that will blend */
|
||||
uint32_t placed; /* ... that moved or turned past the limits */
|
||||
uint32_t unmatched; /* ... new, or a different identity at the address */
|
||||
uint32_t ticks; /* logic ticks between the two frames (0 = no history) */
|
||||
} PSXMotionStats;
|
||||
|
||||
typedef struct PSXMotionMatrix {
|
||||
int16_t r[9];
|
||||
int32_t t[3];
|
||||
} PSXMotionMatrix;
|
||||
|
||||
typedef struct PSXMotionSet PSXMotionSet;
|
||||
|
||||
PSXMotionSet* psx_motion_set_create(uint32_t capacity);
|
||||
void psx_motion_set_destroy(PSXMotionSet* set);
|
||||
|
||||
/* Start this frame's capture. `tick` is the game's logic tick counter at
|
||||
* this frame; the previous capture becomes the history. */
|
||||
void psx_motion_begin(PSXMotionSet* set, uint32_t tick);
|
||||
/* Record the value of `kind` at `addr` as the draw code will read it.
|
||||
* `identity` names what lives there (psx_motion_identity of its type,
|
||||
* handler, model...). One value per (addr, kind) per frame; a repeat
|
||||
* replaces the earlier one. Returns 0 when the set is full. */
|
||||
int psx_motion_track(PSXMotionSet* set, uint32_t kind, uint32_t addr,
|
||||
uint32_t identity);
|
||||
/* Pair this capture with the previous one. Returns how many values will
|
||||
* blend. History counts only when it is the capture just before this one
|
||||
* with 1..8 ticks between them; otherwise nothing blends. */
|
||||
uint32_t psx_motion_prepare(PSXMotionSet* set, const PSXMotionLimits* limits,
|
||||
PSXMotionStats* stats);
|
||||
/* Write every prepared value at t (0 = previous frame, 1 = this frame). */
|
||||
void psx_motion_apply(const PSXMotionSet* set, double t);
|
||||
/* Blend one prepared value at t into `out` without writing guest RAM:
|
||||
* a PSXMotionMatrix for the MATRIX kinds, int32_t[3] for VECTOR / SVECTOR /
|
||||
* ANGLES, int32_t for SCALAR. Returns 0 when that value does not blend this
|
||||
* frame (out then holds its current value) or was not tracked (out
|
||||
* untouched, returns -1). */
|
||||
int psx_motion_blend_at(const PSXMotionSet* set, uint32_t kind, uint32_t addr,
|
||||
double t, void* out);
|
||||
/* Forget all history (scene change, load, a frame the plugin skipped). */
|
||||
void psx_motion_invalidate(PSXMotionSet* set);
|
||||
|
||||
/* Mix up to three words into an identity tag. */
|
||||
uint32_t psx_motion_identity(uint32_t a, uint32_t b, uint32_t c);
|
||||
|
||||
/* The math, for plugins that blend values the set does not hold. Returns 0
|
||||
* when the rotation turned past turn_radians (out = b). */
|
||||
int psx_motion_blend_matrix(const PSXMotionMatrix* a, const PSXMotionMatrix* b,
|
||||
double t, double turn_radians,
|
||||
PSXMotionMatrix* out);
|
||||
void psx_motion_read_matrix(uint32_t addr, PSXMotionMatrix* out);
|
||||
void psx_motion_write_matrix(uint32_t addr, const PSXMotionMatrix* m,
|
||||
int rotation);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -353,6 +353,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/frame_interpolation.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/render_pass.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/render_pass_plan.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/render_pass_motion.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/render_pass_frame.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/host_time.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_fiber.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/sio.c
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/* Per-frame driver and activation for render-pass frame-rate plugins: the
|
||||
* parts every title repeats around psx_mod_render_pass_plan / _pass. */
|
||||
#include "mod_plugins.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define FRAME_MAX_PHASES 16u
|
||||
|
||||
static int s_crossfade; /* passes lastingly unavailable: presenter crossfades */
|
||||
|
||||
int psx_mod_activate_render_pass_rate(const char* package, const char* feature,
|
||||
const char* option, uint32_t flip_mode) {
|
||||
char value[16];
|
||||
unsigned long fps = 0; /* 0 follows the measured display refresh */
|
||||
if (package && feature && option &&
|
||||
psx_mod_option_value(package, feature, option, value, sizeof value) &&
|
||||
strcmp(value, "display") != 0) {
|
||||
char* end = value;
|
||||
const unsigned long parsed = strtoul(value, &end, 10);
|
||||
if (end != value && *end == '\0') fps = parsed;
|
||||
}
|
||||
s_crossfade = 0;
|
||||
if (!psx_mod_set_frame_interpolation_source(PSX_MOD_FRAME_SOURCE_FLIP) ||
|
||||
!psx_mod_set_render_pass_flip(flip_mode) ||
|
||||
!psx_mod_set_frame_interpolation_blend(PSX_MOD_FRAME_INTERPOLATION_HOLD))
|
||||
return 0;
|
||||
return psx_mod_set_frame_interpolation((uint32_t)fps);
|
||||
}
|
||||
|
||||
uint32_t psx_mod_render_pass_frame(struct CPUState* cpu,
|
||||
const PSXModRenderPassFrame* frame,
|
||||
PSXModRenderPassFn fn, void* user) {
|
||||
uint32_t phases[FRAME_MAX_PHASES], n, kept = 0;
|
||||
const uint32_t status = psx_mod_render_pass_status();
|
||||
const int lasting = status == PSX_MOD_RENDER_PASS_BACKEND ||
|
||||
status == PSX_MOD_RENDER_PASS_DISABLED;
|
||||
PSXModRenderPass pass;
|
||||
if (lasting != s_crossfade) {
|
||||
s_crossfade = lasting;
|
||||
psx_mod_set_frame_interpolation_blend(
|
||||
lasting ? PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE
|
||||
: PSX_MOD_FRAME_INTERPOLATION_HOLD);
|
||||
}
|
||||
if (!cpu || !frame || !fn || frame->struct_size < sizeof *frame ||
|
||||
frame->period_vblanks == 0 || frame->w == 0 || frame->h == 0)
|
||||
return 0;
|
||||
n = psx_mod_render_pass_plan(frame->period_vblanks, frame->shown_after_vblanks,
|
||||
phases, FRAME_MAX_PHASES);
|
||||
memset(&pass, 0, sizeof pass);
|
||||
pass.struct_size = sizeof pass;
|
||||
pass.x = frame->x; pass.y = frame->y; pass.w = frame->w; pass.h = frame->h;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
pass.alpha_q16 = phases[i];
|
||||
if (psx_mod_render_pass(cpu, &pass, fn, user)) kept++;
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
/* Motion kit for render-pass plugins: capture, pair and blend the game state
|
||||
* a pass redraws from. See include/render_pass_motion.h. */
|
||||
#include "render_pass_motion.h"
|
||||
#include "mod_plugins.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MOTION_MAX_TICKS 8u
|
||||
#define MOTION_PI 3.14159265358979323846
|
||||
|
||||
typedef struct MotionEntry {
|
||||
uint32_t addr, identity, seq;
|
||||
uint8_t kind;
|
||||
int32_t v[12]; /* MATRIX: r[0..8], t at [9..11]; others from v[0] */
|
||||
} MotionEntry;
|
||||
|
||||
typedef struct MotionPair { uint32_t prev, cur; } MotionPair;
|
||||
|
||||
struct PSXMotionSet {
|
||||
uint32_t cap;
|
||||
MotionEntry* cap_buf[2];
|
||||
uint32_t n[2];
|
||||
uint32_t tick[2];
|
||||
int began[2]; /* a capture was made into this buffer */
|
||||
int sorted[2];
|
||||
int cur; /* buffer the current capture writes */
|
||||
int history; /* the other buffer holds the capture just before */
|
||||
MotionPair* pairs;
|
||||
uint32_t npairs;
|
||||
uint32_t ticks;
|
||||
};
|
||||
|
||||
uint32_t psx_motion_identity(uint32_t a, uint32_t b, uint32_t c) {
|
||||
uint32_t h = 2166136261u;
|
||||
const uint32_t w[3] = {a, b, c};
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int k = 0; k < 4; k++) h = (h ^ ((w[i] >> (8 * k)) & 0xFFu)) * 16777619u;
|
||||
return h;
|
||||
}
|
||||
|
||||
PSXMotionSet* psx_motion_set_create(uint32_t capacity) {
|
||||
PSXMotionSet* s;
|
||||
if (capacity == 0) return NULL;
|
||||
s = (PSXMotionSet*)calloc(1, sizeof *s);
|
||||
if (!s) return NULL;
|
||||
s->cap = capacity;
|
||||
s->cap_buf[0] = (MotionEntry*)calloc(capacity, sizeof(MotionEntry));
|
||||
s->cap_buf[1] = (MotionEntry*)calloc(capacity, sizeof(MotionEntry));
|
||||
s->pairs = (MotionPair*)calloc(capacity, sizeof(MotionPair));
|
||||
if (!s->cap_buf[0] || !s->cap_buf[1] || !s->pairs) {
|
||||
psx_motion_set_destroy(s);
|
||||
return NULL;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void psx_motion_set_destroy(PSXMotionSet* s) {
|
||||
if (!s) return;
|
||||
free(s->cap_buf[0]);
|
||||
free(s->cap_buf[1]);
|
||||
free(s->pairs);
|
||||
free(s);
|
||||
}
|
||||
|
||||
void psx_motion_invalidate(PSXMotionSet* s) {
|
||||
if (!s) return;
|
||||
s->history = 0;
|
||||
s->began[0] = s->began[1] = 0;
|
||||
s->n[0] = s->n[1] = 0;
|
||||
s->npairs = 0;
|
||||
s->ticks = 0;
|
||||
}
|
||||
|
||||
void psx_motion_begin(PSXMotionSet* s, uint32_t tick) {
|
||||
if (!s) return;
|
||||
s->history = s->began[s->cur];
|
||||
s->cur ^= 1;
|
||||
s->n[s->cur] = 0;
|
||||
s->tick[s->cur] = tick;
|
||||
s->began[s->cur] = 1;
|
||||
s->sorted[s->cur] = 0;
|
||||
s->npairs = 0;
|
||||
s->ticks = 0;
|
||||
}
|
||||
|
||||
/* ---- guest RAM --------------------------------------------------------- */
|
||||
|
||||
void psx_motion_read_matrix(uint32_t addr, PSXMotionMatrix* m) {
|
||||
for (int i = 0; i < 9; i++) m->r[i] = (int16_t)psx_mod_read_half(addr + 2u * (uint32_t)i);
|
||||
for (int i = 0; i < 3; i++) m->t[i] = (int32_t)psx_mod_read_word(addr + 0x14u + 4u * (uint32_t)i);
|
||||
}
|
||||
|
||||
void psx_motion_write_matrix(uint32_t addr, const PSXMotionMatrix* m, int rotation) {
|
||||
if (rotation)
|
||||
for (int i = 0; i < 9; i++) psx_mod_write_half(addr + 2u * (uint32_t)i, (uint16_t)m->r[i]);
|
||||
for (int i = 0; i < 3; i++) psx_mod_write_word(addr + 0x14u + 4u * (uint32_t)i, (uint32_t)m->t[i]);
|
||||
}
|
||||
|
||||
static void read_entry(MotionEntry* e) {
|
||||
switch (e->kind) {
|
||||
case PSX_MOTION_MATRIX:
|
||||
case PSX_MOTION_MATRIX_TRANSLATION: {
|
||||
PSXMotionMatrix m;
|
||||
psx_motion_read_matrix(e->addr, &m);
|
||||
for (int i = 0; i < 9; i++) e->v[i] = m.r[i];
|
||||
for (int i = 0; i < 3; i++) e->v[9 + i] = m.t[i];
|
||||
break;
|
||||
}
|
||||
case PSX_MOTION_VECTOR:
|
||||
for (int i = 0; i < 3; i++) e->v[i] = (int32_t)psx_mod_read_word(e->addr + 4u * (uint32_t)i);
|
||||
break;
|
||||
case PSX_MOTION_SVECTOR:
|
||||
case PSX_MOTION_ANGLES:
|
||||
for (int i = 0; i < 3; i++) e->v[i] = (int16_t)psx_mod_read_half(e->addr + 2u * (uint32_t)i);
|
||||
break;
|
||||
case PSX_MOTION_SCALAR:
|
||||
e->v[0] = (int32_t)psx_mod_read_word(e->addr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int psx_motion_track(PSXMotionSet* s, uint32_t kind, uint32_t addr, uint32_t identity) {
|
||||
MotionEntry* e;
|
||||
if (!s || kind < PSX_MOTION_MATRIX || kind > PSX_MOTION_SCALAR) return 0;
|
||||
if (!s->began[s->cur] || s->n[s->cur] >= s->cap) return 0;
|
||||
e = &s->cap_buf[s->cur][s->n[s->cur]];
|
||||
memset(e, 0, sizeof *e);
|
||||
e->addr = addr;
|
||||
e->kind = (uint8_t)kind;
|
||||
e->identity = identity;
|
||||
e->seq = s->n[s->cur]++;
|
||||
s->sorted[s->cur] = 0;
|
||||
read_entry(e);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- math ---------------------------------------------------------------- */
|
||||
|
||||
static int orthonormal(const int16_t* r) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double len = 0;
|
||||
for (int j = 0; j < 3; j++) len += (double)r[3 * i + j] * r[3 * i + j];
|
||||
if (fabs(sqrt(len) - 4096.0) > 4096.0 * 0.04) return 0;
|
||||
for (int k = i + 1; k < 3; k++) {
|
||||
double dot = 0;
|
||||
for (int j = 0; j < 3; j++) dot += (double)r[3 * i + j] * r[3 * k + j];
|
||||
if (fabs(dot) > 4096.0 * 4096.0 * 0.04) return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void to_quat(const int16_t* r, double q[4]) {
|
||||
double a[9], n;
|
||||
for (int i = 0; i < 9; i++) a[i] = r[i] / 4096.0;
|
||||
const double tr = a[0] + a[4] + a[8];
|
||||
if (tr > 0) {
|
||||
double s = sqrt(tr + 1.0) * 2;
|
||||
q[0] = 0.25 * s; q[1] = (a[7] - a[5]) / s; q[2] = (a[2] - a[6]) / s; q[3] = (a[3] - a[1]) / s;
|
||||
} else if (a[0] > a[4] && a[0] > a[8]) {
|
||||
double s = sqrt(1.0 + a[0] - a[4] - a[8]) * 2;
|
||||
q[0] = (a[7] - a[5]) / s; q[1] = 0.25 * s; q[2] = (a[1] + a[3]) / s; q[3] = (a[2] + a[6]) / s;
|
||||
} else if (a[4] > a[8]) {
|
||||
double s = sqrt(1.0 + a[4] - a[0] - a[8]) * 2;
|
||||
q[0] = (a[2] - a[6]) / s; q[1] = (a[1] + a[3]) / s; q[2] = 0.25 * s; q[3] = (a[5] + a[7]) / s;
|
||||
} else {
|
||||
double s = sqrt(1.0 + a[8] - a[0] - a[4]) * 2;
|
||||
q[0] = (a[3] - a[1]) / s; q[1] = (a[2] + a[6]) / s; q[2] = (a[5] + a[7]) / s; q[3] = 0.25 * s;
|
||||
}
|
||||
n = sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
|
||||
for (int i = 0; i < 4; i++) q[i] /= n;
|
||||
}
|
||||
|
||||
static int16_t fixed12(double v) {
|
||||
double x = v * 4096.0;
|
||||
if (x > 32767.0) x = 32767.0;
|
||||
if (x < -32768.0) x = -32768.0;
|
||||
return (int16_t)lround(x);
|
||||
}
|
||||
|
||||
static double turn_limit(double turn) { return turn > 0 ? turn : MOTION_PI; }
|
||||
|
||||
/* Angle between two orientations, or -1 when either is not a rotation. */
|
||||
static double rotation_angle(const int16_t* a, const int16_t* b) {
|
||||
double p[4], q[4], d;
|
||||
if (!orthonormal(a) || !orthonormal(b)) return -1;
|
||||
to_quat(a, p);
|
||||
to_quat(b, q);
|
||||
d = fabs(p[0] * q[0] + p[1] * q[1] + p[2] * q[2] + p[3] * q[3]);
|
||||
if (d > 1.0) d = 1.0;
|
||||
return 2.0 * acos(d);
|
||||
}
|
||||
|
||||
static int blend_rotation(const int16_t* a, const int16_t* b, double t, double turn, int16_t* out) {
|
||||
if (orthonormal(a) && orthonormal(b)) {
|
||||
double p[4], q[4], d, angle, wa, wb, r[4];
|
||||
to_quat(a, p);
|
||||
to_quat(b, q);
|
||||
d = p[0] * q[0] + p[1] * q[1] + p[2] * q[2] + p[3] * q[3];
|
||||
if (d < 0) { for (int i = 0; i < 4; i++) q[i] = -q[i]; d = -d; }
|
||||
if (d > 1.0) d = 1.0;
|
||||
angle = acos(d);
|
||||
if (2.0 * angle > turn_limit(turn)) { memcpy(out, b, 9 * sizeof *out); return 0; }
|
||||
wa = 1 - t; wb = t;
|
||||
if (angle > 1e-6) { wa = sin((1 - t) * angle) / sin(angle); wb = sin(t * angle) / sin(angle); }
|
||||
for (int i = 0; i < 4; i++) r[i] = wa * p[i] + wb * q[i];
|
||||
{
|
||||
const double w = r[0], x = r[1], y = r[2], z = r[3];
|
||||
const double m[9] = {1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w),
|
||||
2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w),
|
||||
2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)};
|
||||
for (int i = 0; i < 9; i++) out[i] = fixed12(m[i]);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
/* Scaled or sheared: elementwise, refusing jumps no motion produces. */
|
||||
for (int i = 0; i < 9; i++)
|
||||
if (abs((int)a[i] - (int)b[i]) > 4096) { memcpy(out, b, 9 * sizeof *out); return 0; }
|
||||
for (int i = 0; i < 9; i++) out[i] = (int16_t)lround(a[i] + (b[i] - a[i]) * t);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int psx_motion_blend_matrix(const PSXMotionMatrix* a, const PSXMotionMatrix* b,
|
||||
double t, double turn, PSXMotionMatrix* out) {
|
||||
const int ok = blend_rotation(a->r, b->r, t, turn, out->r);
|
||||
for (int i = 0; i < 3; i++)
|
||||
out->t[i] = ok ? (int32_t)llround(a->t[i] + ((double)b->t[i] - a->t[i]) * t) : b->t[i];
|
||||
return ok;
|
||||
}
|
||||
|
||||
static double dist3(const int32_t* a, const int32_t* b) {
|
||||
double s = 0;
|
||||
for (int i = 0; i < 3; i++) { const double d = (double)b[i] - a[i]; s += d * d; }
|
||||
return sqrt(s);
|
||||
}
|
||||
|
||||
static int32_t arc12(int32_t from, int32_t to) { /* shortest signed delta */
|
||||
int32_t d = (to - from) & 4095;
|
||||
return d > 2048 ? d - 4096 : d; /* an exact half turn goes forward */
|
||||
}
|
||||
|
||||
/* ---- pairing ------------------------------------------------------------- */
|
||||
|
||||
static int entry_cmp(const void* x, const void* y) {
|
||||
const MotionEntry* a = (const MotionEntry*)x;
|
||||
const MotionEntry* b = (const MotionEntry*)y;
|
||||
if (a->addr != b->addr) return a->addr < b->addr ? -1 : 1;
|
||||
if (a->kind != b->kind) return a->kind < b->kind ? -1 : 1;
|
||||
return a->seq < b->seq ? -1 : a->seq > b->seq;
|
||||
}
|
||||
|
||||
/* Sort by (addr, kind); of repeats the last tracked wins. */
|
||||
static void sort_capture(PSXMotionSet* s, int b) {
|
||||
MotionEntry* e = s->cap_buf[b];
|
||||
uint32_t n = s->n[b], w = 0;
|
||||
if (s->sorted[b]) return;
|
||||
qsort(e, n, sizeof *e, entry_cmp);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
if (w && e[w - 1].addr == e[i].addr && e[w - 1].kind == e[i].kind) e[w - 1] = e[i];
|
||||
else e[w++] = e[i];
|
||||
}
|
||||
s->n[b] = w;
|
||||
s->sorted[b] = 1;
|
||||
}
|
||||
|
||||
/* 1 when the pair moved within the limits over `ticks`. */
|
||||
static int within_limits(const MotionEntry* a, const MotionEntry* b,
|
||||
const PSXMotionLimits* lim, uint32_t ticks) {
|
||||
const double move = lim->move_per_tick > 0 ? lim->move_per_tick * ticks : HUGE_VAL;
|
||||
switch (a->kind) {
|
||||
case PSX_MOTION_MATRIX: {
|
||||
double ang;
|
||||
int16_t ra[9], rb[9];
|
||||
if (dist3(&a->v[9], &b->v[9]) > move) return 0;
|
||||
for (int i = 0; i < 9; i++) { ra[i] = (int16_t)a->v[i]; rb[i] = (int16_t)b->v[i]; }
|
||||
ang = rotation_angle(ra, rb);
|
||||
if (ang >= 0) return ang <= turn_limit(lim->turn_radians);
|
||||
for (int i = 0; i < 9; i++) if (abs(a->v[i] - b->v[i]) > 4096) return 0;
|
||||
return 1;
|
||||
}
|
||||
case PSX_MOTION_MATRIX_TRANSLATION:
|
||||
return dist3(&a->v[9], &b->v[9]) <= move;
|
||||
case PSX_MOTION_VECTOR:
|
||||
case PSX_MOTION_SVECTOR:
|
||||
return dist3(a->v, b->v) <= move;
|
||||
case PSX_MOTION_ANGLES: {
|
||||
const double lim12 = turn_limit(lim->turn_radians) * 4096.0 / (2.0 * MOTION_PI);
|
||||
for (int i = 0; i < 3; i++) if (fabs((double)arc12(a->v[i], b->v[i])) > lim12) return 0;
|
||||
return 1;
|
||||
}
|
||||
case PSX_MOTION_SCALAR:
|
||||
return fabs((double)b->v[0] - a->v[0]) <= move;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t psx_motion_prepare(PSXMotionSet* s, const PSXMotionLimits* lim,
|
||||
PSXMotionStats* stats) {
|
||||
PSXMotionStats st;
|
||||
const PSXMotionLimits none = {0, 0};
|
||||
const int c = s ? s->cur : 0, h = c ^ 1;
|
||||
memset(&st, 0, sizeof st);
|
||||
if (!lim) lim = &none;
|
||||
if (!s || !s->began[c]) { if (stats) *stats = st; return 0; }
|
||||
sort_capture(s, c);
|
||||
st.tracked = s->n[c];
|
||||
s->npairs = 0;
|
||||
s->ticks = 0;
|
||||
if (s->history && s->began[h]) {
|
||||
const uint32_t ticks = s->tick[c] - s->tick[h];
|
||||
if (ticks >= 1 && ticks <= MOTION_MAX_TICKS) s->ticks = ticks;
|
||||
}
|
||||
if (!s->ticks) {
|
||||
st.unmatched = st.tracked;
|
||||
} else {
|
||||
const MotionEntry* pe = s->cap_buf[h];
|
||||
const MotionEntry* ce = s->cap_buf[c];
|
||||
uint32_t i = 0;
|
||||
sort_capture(s, h);
|
||||
for (uint32_t j = 0; j < s->n[c]; j++) {
|
||||
while (i < s->n[h] && (pe[i].addr < ce[j].addr ||
|
||||
(pe[i].addr == ce[j].addr && pe[i].kind < ce[j].kind)))
|
||||
i++;
|
||||
if (i >= s->n[h] || pe[i].addr != ce[j].addr || pe[i].kind != ce[j].kind ||
|
||||
pe[i].identity != ce[j].identity) {
|
||||
st.unmatched++;
|
||||
} else if (!within_limits(&pe[i], &ce[j], lim, s->ticks)) {
|
||||
st.placed++;
|
||||
} else {
|
||||
s->pairs[s->npairs].prev = i;
|
||||
s->pairs[s->npairs].cur = j;
|
||||
s->npairs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
st.blended = s->npairs;
|
||||
st.ticks = s->ticks;
|
||||
if (stats) *stats = st;
|
||||
return s->npairs;
|
||||
}
|
||||
|
||||
/* Blend one pair into `v` (entry layout). */
|
||||
static void blend_entry(const MotionEntry* a, const MotionEntry* b, double t, int32_t* v) {
|
||||
switch (a->kind) {
|
||||
case PSX_MOTION_MATRIX: {
|
||||
int16_t ra[9], rb[9], ro[9];
|
||||
for (int i = 0; i < 9; i++) { ra[i] = (int16_t)a->v[i]; rb[i] = (int16_t)b->v[i]; }
|
||||
blend_rotation(ra, rb, t, MOTION_PI * 2, ro); /* limits were applied at prepare */
|
||||
for (int i = 0; i < 9; i++) v[i] = ro[i];
|
||||
for (int i = 9; i < 12; i++) v[i] = (int32_t)llround(a->v[i] + ((double)b->v[i] - a->v[i]) * t);
|
||||
break;
|
||||
}
|
||||
case PSX_MOTION_MATRIX_TRANSLATION:
|
||||
for (int i = 0; i < 9; i++) v[i] = b->v[i];
|
||||
for (int i = 9; i < 12; i++) v[i] = (int32_t)llround(a->v[i] + ((double)b->v[i] - a->v[i]) * t);
|
||||
break;
|
||||
case PSX_MOTION_ANGLES:
|
||||
for (int i = 0; i < 3; i++)
|
||||
v[i] = (int32_t)lround(a->v[i] + arc12(a->v[i], b->v[i]) * t);
|
||||
break;
|
||||
default:
|
||||
for (int i = 0; i < 3; i++) v[i] = (int32_t)llround(a->v[i] + ((double)b->v[i] - a->v[i]) * t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void write_entry(uint8_t kind, uint32_t addr, const int32_t* v) {
|
||||
switch (kind) {
|
||||
case PSX_MOTION_MATRIX:
|
||||
case PSX_MOTION_MATRIX_TRANSLATION: {
|
||||
PSXMotionMatrix m;
|
||||
for (int i = 0; i < 9; i++) m.r[i] = (int16_t)v[i];
|
||||
for (int i = 0; i < 3; i++) m.t[i] = v[9 + i];
|
||||
psx_motion_write_matrix(addr, &m, kind == PSX_MOTION_MATRIX);
|
||||
break;
|
||||
}
|
||||
case PSX_MOTION_VECTOR:
|
||||
for (int i = 0; i < 3; i++) psx_mod_write_word(addr + 4u * (uint32_t)i, (uint32_t)v[i]);
|
||||
break;
|
||||
case PSX_MOTION_SVECTOR:
|
||||
for (int i = 0; i < 3; i++) psx_mod_write_half(addr + 2u * (uint32_t)i, (uint16_t)v[i]);
|
||||
break;
|
||||
case PSX_MOTION_ANGLES:
|
||||
for (int i = 0; i < 3; i++) psx_mod_write_half(addr + 2u * (uint32_t)i, (uint16_t)(v[i] & 4095));
|
||||
break;
|
||||
case PSX_MOTION_SCALAR:
|
||||
psx_mod_write_word(addr, (uint32_t)v[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void psx_motion_apply(const PSXMotionSet* s, double t) {
|
||||
if (!s || !s->npairs) return;
|
||||
const MotionEntry* pe = s->cap_buf[s->cur ^ 1];
|
||||
const MotionEntry* ce = s->cap_buf[s->cur];
|
||||
for (uint32_t k = 0; k < s->npairs; k++) {
|
||||
int32_t v[12];
|
||||
const MotionEntry* a = &pe[s->pairs[k].prev];
|
||||
const MotionEntry* b = &ce[s->pairs[k].cur];
|
||||
blend_entry(a, b, t, v);
|
||||
write_entry(b->kind, b->addr, v);
|
||||
}
|
||||
}
|
||||
|
||||
static void export_value(uint8_t kind, const int32_t* v, void* out) {
|
||||
if (kind == PSX_MOTION_MATRIX || kind == PSX_MOTION_MATRIX_TRANSLATION) {
|
||||
PSXMotionMatrix* m = (PSXMotionMatrix*)out;
|
||||
for (int i = 0; i < 9; i++) m->r[i] = (int16_t)v[i];
|
||||
for (int i = 0; i < 3; i++) m->t[i] = v[9 + i];
|
||||
} else if (kind == PSX_MOTION_SCALAR) {
|
||||
*(int32_t*)out = v[0];
|
||||
} else {
|
||||
memcpy(out, v, 3 * sizeof(int32_t));
|
||||
}
|
||||
}
|
||||
|
||||
int psx_motion_blend_at(const PSXMotionSet* s, uint32_t kind, uint32_t addr,
|
||||
double t, void* out) {
|
||||
const MotionEntry* ce;
|
||||
uint32_t lo = 0, hi;
|
||||
if (!s || !out || !s->began[s->cur]) return -1;
|
||||
ce = s->cap_buf[s->cur];
|
||||
hi = s->n[s->cur];
|
||||
if (!s->sorted[s->cur]) {
|
||||
for (uint32_t j = hi; j-- > 0;) /* unprepared: newest wins */
|
||||
if (ce[j].addr == addr && ce[j].kind == kind) { export_value(ce[j].kind, ce[j].v, out); return 0; }
|
||||
return -1;
|
||||
}
|
||||
while (lo < hi) {
|
||||
const uint32_t mid = (lo + hi) / 2;
|
||||
if (ce[mid].addr < addr || (ce[mid].addr == addr && ce[mid].kind < kind)) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
if (lo >= s->n[s->cur] || ce[lo].addr != addr || ce[lo].kind != kind) return -1;
|
||||
for (uint32_t k = 0; k < s->npairs; k++) {
|
||||
if (s->pairs[k].cur == lo) {
|
||||
int32_t v[12];
|
||||
blend_entry(&s->cap_buf[s->cur ^ 1][s->pairs[k].prev], &ce[lo], t, v);
|
||||
export_value(ce[lo].kind, v, out);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
export_value(ce[lo].kind, ce[lo].v, out);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/* 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 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_frame_driver();
|
||||
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user