Merge pull request #424 from RetroPortingToolKit/feat/frame-rate

Add host-timed render passes for true in-between frames (opt-in)
This commit is contained in:
Matt Stanley
2026-09-29 11:22:16 -07:00
committed by GitHub
38 changed files with 4503 additions and 27 deletions
+3 -1
View File
@@ -29,9 +29,11 @@ netplay session or a later offline one never inherits them.
|---|---|
| `LINEAR` (default) | crossfade `mix(prev, cur, a)` |
| `MOTION_ADAPTIVE` | crossfade small changes, switch large changes at a = 0.5 |
| `HOLD` | no crossfade: repeat the newest frame (for titles whose plugin supplies in-between images with render passes) |
Blending never invents motion: moving objects ghost, and the image is one
source frame behind the newest one.
source frame behind the newest one. True in-between frames need the game's
own draw code: see [RENDER_PASSES.md](RENDER_PASSES.md).
## Blend source: every VBlank or real flips
+213
View File
@@ -0,0 +1,213 @@
# Render passes: true in-between frames
Frame blending ([FRAME_RATE.md](FRAME_RATE.md)) crossfades finished frames; it
cannot show objects in positions the game never drew. A **render pass** lets a
trusted game plugin redraw its scene between two logic ticks with the game's
own draw code, with objects and camera placed part of the way from one tick
to the next. The pass never changes the game: guest time is frozen while it
runs and the machine is restored bit-for-bit afterwards. Its only product is
an image the presenter shows between the game's own frames.
Default off. Nothing happens unless a plugin calls the API below.
## API (mod_plugins.h)
```c
uint32_t psx_mod_render_pass_plan(uint32_t period_vblanks,
uint32_t shown_after_vblanks,
uint32_t *alpha_q16, uint32_t max);
int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
PSXModRenderPassFn fn, void *user);
uint32_t psx_mod_render_pass_status(void);
```
Call both from an emulation-thread function-entry hook at the point where
the game has finished the logic of tick n+1 but the display still has to
flip to frame n. For a PsyQ double-buffered loop that is the entry of the
`VSync(0)` that precedes `PutDispEnv`.
Register that hook with `psx_mod_register_function_entry_plugin()` under the
plugin's manifest `[[plugin]]` id (see [MOD_PACKAGES.md](MOD_PACKAGES.md)):
it then runs only while the resolved mod plan activates the plugin, so never
while the package is disabled or in netplay. Register once, for example from
the plugin's constructor; a repeated id and address returns 0. Entry hooks
also fire for the guest functions a pass itself calls, the plugin's own
included, so a hook that plans passes must ignore entries made inside one.
1. `psx_mod_render_pass_plan(P, D, a, max)` returns the phases (Q16, in
(0, 1)) at which the presenter will show frame n: its output deadlines
during the P VBlanks the frame stays on screen, starting after D more
VBlank presents. When the host cannot afford them all, an evenly spread
subset is returned. 0 means "no passes this frame" (see Gates).
2. For each phase, `psx_mod_render_pass()` with the display rect the next
flip shows (the DISPENV rect). `fn(cpu, user, alpha)` may write guest RAM
and call guest functions with `psx_dispatch_call()`; it returns nonzero to
keep the image. The first pass after a plan also captures frame n's own
image (phase 0).
The presenter uses `PSX_MOD_FRAME_SOURCE_FLIP` to see the
flip, and a plugin that supplies passes normally selects
`PSX_MOD_FRAME_INTERPOLATION_HOLD`, so wherever no pass image applies the
newest game frame is repeated rather than crossfaded one frame late.
### When passes are unavailable
`psx_mod_render_pass_status()` says why a plan would be empty, the host-time
budget aside (an empty plan while it says `READY` was shed for time):
| Status | Meaning | Lasts |
|---|---|---|
| `READY` (0) | passes can run | |
| `NO_PRESENTER` (1) | not OpenGL, interpolation off or suspended (FMV), or not the FLIP source | until the presenter changes |
| `BACKEND` (2) | the renderer declines passes in its current mode | while that mode lasts |
| `DISABLED` (3) | switched off after repeated faults | the session |
| `SESSION` (4) | netplay, rollback, rewind, load/save replay, self-check resimulation | transient |
| `FAST_FORWARD` (5) | manual fast-forward, turbo-through-loads, FMV auto-skip, TCP turbo | transient |
| `BUSY` (6) | inside an exception, a pass or a GPU DMA walk, or no frame captured since the presenter's history restarted (a display mode change) | transient |
A plugin that relies on passes should not leave the player on stock-rate
frames while a lasting reason holds: it can switch the presenter to a
crossfade with `psx_mod_set_frame_interpolation_blend()`, which may be called
from a hook at any time and takes effect at the next present, and switch
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()`.
## What a pass may and may not do
While `fn` runs (`g_psx_render_pass_active`):
| Area | Behaviour | Where |
|---|---|---|
| Guest clock | cycles are counted (GTE and mult/div deadlines work) but no device is serviced, no VBlank or device event fires | `psx_cycles.c` freeze (`psx_cycle_freeze.h`, runtime-only: the codegen-hashed `psx_cycles.h` is untouched) |
| Interrupts | never delivered | `interrupts.c` |
| GPU DMA | linked lists and delayed completions finish synchronously | `dma.c` |
| RAM / scratchpad stores | written directly, bypassing code-page tracking, overlay watch, write traces and fingerprints; RAM addresses fold through the live geometry (2 MiB mirrored, or 8 MiB with the 8 MB RAM mod), as outside a pass | `memory.c` `render_pass_store` |
| MMIO stores | allowed: GP0, GP1 DMA mode / info, GPU and OTC DMA channels, DPCR/DICR, I_STAT/I_MASK. Dropped and counted: SPU (key-ons), CD, timers, SIO, MDEC, other DMA channels, memory control | `memory.c` |
| VRAM | only the declared rect; writes that bypass the scissor elsewhere (fills, copies, uploads, pokes: never a native-wide surface) are journaled and rolled back | `gpu_gl_renderer.c` |
| Runaway code | an 8 M guest-cycle watchdog rolls the pass back | `render_pass.c` |
After `fn` (success or not) everything is restored: CPU state with the GTE,
all of main RAM at its live size (2 MiB, or 8 MiB with the opt-in 8 MB RAM
mod; `psx_memory.h`), scratchpad, I-cache tags, I_STAT/I_MASK, timers, DMA
and GPU registers (without the widescreen side effects of a savestate load), the
VRAM rect (hr colour, mask stencil, raw 16-bit mirror, native-wide band, CPU
VRAM rows), the renderer's coherency bookkeeping, and every clock value.
A watchdog abort leaves by longjmp from inside guest code, skipping the
exits of the frames it leaves, so the host nesting those frames own is put
back from the checkpoint too: the cycle-deferral depth, the native overlay
unit depth, shard stack and cycle-flush hook, the DMA execution depth, and
the interpreter's active/phase/precise flags, resume latch and pending load
(`render_pass_abort_test`). A plugin must likewise not keep state that only
its callback's normal return resets.
After 8 faults (watchdog or refused writes) passes stay off for the session.
## Gates
The plan returns 0 in netplay, rollback resimulation, self-check, rewind,
lockstep, an exception, while a GPU DMA list is in flight, during manual
fast-forward, turbo-through-loads, FMV auto-skip and TCP turbo, on anything
but the OpenGL renderer with FLIP-source interpolation, while the presenter
is suspended (FMV), after repeated faults, and when no VBlank was presented
since the last plan (headless). `psx_mod_render_pass()` refuses under the
same conditions.
## Presentation and budget
The presenter keeps two generations of images: the frame on screen and the
one being built for the next flip. A generation becomes current when the
FLIP source sees the display flip to its rect; from that interval's start
its phases map onto host time. At each output deadline the newest image at
or before the deadline's phase is shown, crossfaded into the next one when
passes were shed. Deadlines that fall due while passes run are presented
between passes, so the frame on screen keeps moving. A frame that stays on
screen longer than the plan's period (a lagging tick: three VBlanks instead
of two) holds its newest image until the next flip, as a late stock frame
holds; it never steps back to the game's own image of the frame, which is
older (`late_presents` in `render_pass_stats`). Only a game that stops
flipping for four frame lengths expires the frame's images (`expired`), and
the presenter then shows its own newest capture.
Consecutive passes of one frame reuse the VRAM backup the first one took:
each pass's restore leaves the rect exactly as backed up, and no guest code
runs in between (the clock and guest store count are unchanged), so only the
first pass copies the rect out (`backups_reused` in `render_pass_stats`).
Passes cost host time inside the game's frame: they run on the emulation
thread. The budget per frame is `PSX_RENDER_PASS_BUDGET` percent (default 80)
of the presenter's idle time plus the presents beyond two per frame, learnt
from the previous frame, over a smoothed per-pass cost. When the budget runs
out, fewer passes are rendered, and a pass that costs more than the budget is
not planned at all. The cost is measured per presented image size: until
three passes have been measured at the current size (the first plans, and
after an aspect or internal-resolution change) a plan asks for one pass, so
an expensive size costs at most one pass per frame while it is learnt. The
three seed the average with their median, so one slow pass on a busy host
does not price passes out. A pass that creates pass textures or
framebuffers (the first frames at a size, or a slot filled for the first
time) is not a cost sample, at most eight in a row. Only passes that run
are measured, and the first passes of a race can all run in a transient (a
busy host, code run for the first time) that costs several times the steady
state; an estimate that high prices every plan out, so no pass would ever
correct it. An estimate that no pass has been measured against for 30
plans that wanted passes (about a second of a 30 Hz game) is measured again:
the warm-up restarts, with one pass per plan (`cost_rewarms` in
`render_pass_stats`). When the new median is not at least a quarter below
the old estimate, the old one was right (a size that is truly too
expensive, or a host at its limit), and the next wait doubles, up to 960
plans, so these one-pass warm-ups stay rare; a re-measure that finds the
old estimate stale keeps the wait at 30. A plugin can show a crossfade while
passes are shed (see "When passes are unavailable").
Pass images are kept at internal resolution (the size the presenter
captures). Their textures are made as the slots fill, never more slots than
two generations fit in 256 MiB, which limits passes per frame at very high
internal resolutions; a size change frees the old set.
## Verifying a title
- `render_pass_stats` (TCP): passes, shedding, faults, dropped device
stores, timing split (backup / guest code / capture / restore), presents
made from pass images, and `status`.
- `render_pass_refuse on=1` (TCP) or `PSX_RENDER_PASS_REFUSE=1`: the backend
declines passes (`BACKEND`), to test a plugin's fallback.
- `render_pass_dump path=<dir> count=<n>`: PNGs of the next n frames' images
(the game's own frame, then each pass in phase order).
- `PSX_RENDER_PASS_VERIFY=1`: hash CPU, RAM, scratchpad, I-cache, interrupt,
timer, DMA and GPU state and read back the VRAM rect before and after every
pass, and check that a pass that returned normally left the host nesting
balanced; `verify_mismatch` must stay 0.
- `PSX_RENDER_PASS_WATCHDOG=<guest cycles>`: lower the watchdog (default
8 M) below a title's pass size to drive real passes through the rollback
path; with fingerprints (below) the run must still match one without
passes.
- `frame_fingerprint reset_on_load=1` then a savestate load: the per-frame
write/MMIO/cycle fingerprints of a run with passes must equal a run
without them.
Tests (runtime ctest unless noted):
- `render_pass_plan_test`: phase planning per rate, shedding (one pass while
the cost is unknown), the cost warm-up and allocation rule, re-measuring a
stale estimate (and backing off when it stays too expensive), selection,
holding the newest image when the next flip is late, the MMIO allow-list.
- `render_pass_freeze_test`: 10^6 frozen cycles, exact clock restore,
watchdog.
- `render_pass_sandbox_test`: the store policy `memory.c` routes every pass
store through (SPU, CD, timer, other-DMA, GP1 and other device stores
dropped and counted per class, never delivered; GP0, I_STAT/I_MASK and
GPU/OTC DMA delivered); a real pass over `render_pass.c`, `psx_cycles.c`
and `timers.c` that makes those stores and runs 10^6 cycles while a timer
is armed to interrupt every 1000 (no device advances, no interrupt, RAM,
scratchpad, I_STAT/I_MASK, timers and clock restored); status gates; the
same store folding and restore with 8 MiB RAM live (unique addresses up to
0x7FFFFF, restored after a pass, and a retail pass after it folding again);
the out-of-rect VRAM journal policy and its exact rollback of the CPU VRAM
rows.
- `render_pass_abort_test`: a watchdog abort from nested frames.
- `render_pass_guards` (source guard, recompiler ctest): the choke points
above, and that the codegen-hashed headers do not carry the pass API.
The GPU half of the VRAM transaction (hr colour, stencil, raw mirror blits)
needs an OpenGL context and is checked at runtime by
`PSX_RENDER_PASS_VERIFY=1`.
+10 -3
View File
@@ -53,6 +53,10 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `screenshot_hires` | ✓ | ✓ | `path` | PNG of the **supersampled** surface (the present path the window uses), at `display × gr_scale()`. ⚠ `screenshot`/`screenshot_file` capture native 15-bit VRAM and are **blind to anything that only exists in the hi-res mirror** — geometry correction, SSAA edges, perspective UVs — so they show a clean frame while the player sees a broken one. Use this one to verify those. Falls back to the native resolve (and reports `scale: 1`) when no hi-res surface exists |
| `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) |
| `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates |
| `gl_interp` | ✓ | | — | OpenGL frame-rate presenter ([FRAME_RATE.md](FRAME_RATE.md)): enabled/suspended, host and target Hz, swaps, `source` (`vblank`/`flip`), `flip_period`, `captures` (new source frames) and `duplicates` (VBlanks that re-presented the same frame) |
| `render_pass_stats` | ✓ | | — | Render passes ([RENDER_PASSES.md](RENDER_PASSES.md)): plans, phases wanted/planned (shedding), passes, rollbacks (`nesting_repairs`: watchdog aborts whose skipped frame exits the restore undid), dropped device stores by class, `verify_mismatch` under `PSX_RENDER_PASS_VERIFY=1`, host-time split per pass, smoothed pass cost (`cost_us`; `cost_rewarms`: estimates no pass had run on for a while, measured again), presents made from pass images (`late_presents`: held past the frame's planned end because the next flip was late; `expired`: frames whose images stopped showing after several frame lengths without a flip), pass image textures allocated (`image_textures`, `image_bytes`), `status` (`psx_mod_render_pass_status`: 0 ready, 1 no presenter, 2 backend, 3 disabled, 4 session, 5 fast-forward, 6 busy), `backups_reused` (passes that reused the previous pass's VRAM backup) |
| `render_pass_dump` | ✓ | | `path`, `count` | Write the images (the game's own frame, then each pass in phase order) of the next `count` frames that get passes as `<path>/g<frame>_<index>_a<phase q16>.png` |
| `render_pass_refuse` | ✓ | | `on` | Make the OpenGL backend decline render passes (`status` 2, BACKEND), as a renderer mode without them would; tests a plugin's fallback. `PSX_RENDER_PASS_REFUSE=1` does the same from start |
| `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate |
| `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history |
| `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native |
@@ -391,9 +395,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i
## Complete command index (generated)
**316 commands registered** — 303 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
**319 commands registered** — 306 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
55 of 316 have prose above; **261 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
59 of 319 have prose above; **260 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
@@ -519,7 +523,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `get_snapshots` | ✓ | ✓ | ✓ |
| `gl_coh_ring` | ✓ | | |
| `gl_fbo_peek` | ✓ | | |
| `gl_interp` | ✓ | | |
| `gl_interp` | ✓ | | ✓ |
| `gl_present_ring` | ✓ | | |
| `gl_vram_diff` | ✓ | | |
| `gl_wide_fast` | ✓ | | |
@@ -608,6 +612,9 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `record_frame` | ✓ | | |
| `record_frame_dump` | ✓ | | |
| `record_reads_dump` | ✓ | | |
| `render_pass_dump` | ✓ | | ✓ |
| `render_pass_refuse` | ✓ | | ✓ |
| `render_pass_stats` | ✓ | | ✓ |
| `restore_trace` | ✓ | | |
| `restore_trace_clear` | ✓ | | |
| `restore_trace_window` | ✓ | | |
+1
View File
@@ -900,6 +900,7 @@ if(BUILD_TESTING)
overlay_init_guard
overlay_segment_gate
recomp_ui_root_override
render_pass_guards
pal_cadence_host_refresh
sdl3_main_single_include
spu_sample_scheduler_default
+45
View File
@@ -605,6 +605,51 @@ if(BUILD_TESTING)
add_test(NAME frame_interpolation_schedule_test
COMMAND frame_interpolation_schedule_test)
add_executable(render_pass_plan_test
tests/test_render_pass_plan.c
src/render_pass_plan.c)
target_include_directories(render_pass_plan_test PRIVATE include)
if(NOT MSVC)
target_link_libraries(render_pass_plan_test PRIVATE m)
endif()
add_test(NAME render_pass_plan_test COMMAND render_pass_plan_test)
# Render-pass time freeze against mock devices (psx_cycles.c only).
add_executable(render_pass_freeze_test
tests/test_render_pass_freeze.c
src/psx_cycles.c)
target_include_directories(render_pass_freeze_test PRIVATE include)
add_test(NAME render_pass_freeze_test COMMAND render_pass_freeze_test)
# Render-pass watchdog abort from nested guest frames: the real
# render_pass.c and psx_cycles.c against mock memory, devices and
# presenter (the frames use GCC/Clang cleanup handlers, as generated
# code does).
if(NOT MSVC)
add_executable(render_pass_abort_test
tests/test_render_pass_abort.c
src/render_pass.c
src/psx_cycles.c)
target_include_directories(render_pass_abort_test PRIVATE include)
add_test(NAME render_pass_abort_test COMMAND render_pass_abort_test)
endif()
# Render-pass sandbox: the store policy memory.c uses, a real pass
# (render_pass.c, psx_cycles.c, timers.c) that makes device stores and
# runs frozen cycles, and the out-of-rect VRAM journal.
add_executable(render_pass_sandbox_test
tests/test_render_pass_sandbox.c
src/render_pass.c
src/render_pass_plan.c
src/psx_cycles.c
src/timers.c
src/psx_video_timing.c)
target_include_directories(render_pass_sandbox_test PRIVATE include)
if(NOT MSVC)
target_link_libraries(render_pass_sandbox_test PRIVATE m)
endif()
add_test(NAME render_pass_sandbox_test COMMAND render_pass_sandbox_test)
add_executable(gpu_primitive_reject_test tests/test_gpu_primitive_reject.c)
target_include_directories(gpu_primitive_reject_test PRIVATE include)
add_test(NAME gpu_primitive_reject_test COMMAND gpu_primitive_reject_test)
+3
View File
@@ -255,6 +255,9 @@ void debug_server_freeze_dump_dirty_block_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_dirty_insn_json(FILE *f, uint32_t max_count);
void debug_server_freeze_dump_evcb_json(FILE *f, uint32_t max_count);
/* frame_fingerprint reset_on_load: called when a savestate load completes. */
void debug_server_note_savestate_loaded(void);
#ifdef __cplusplus
}
#endif
+12
View File
@@ -53,6 +53,18 @@ void dirty_ram_ld_delay_flush(CPUState* cpu);
* on the next interp step (selfcheck MotK v0=countdown vs v0=1 forks). */
void dirty_ram_ld_delay_discard(void);
/* Save / put back the deferred load-delay writeback exactly. For a landing
* that longjmps out of interpreted code and then resumes the interrupted
* code (the render-pass watchdog abort, render_pass.c): the pass's pending
* load must not reach the resumed timeline, and the interrupted one's must
* survive. */
typedef struct DirtyRamLoadDelay {
uint32_t rt, val, age;
int armed;
} DirtyRamLoadDelay;
void dirty_ram_ld_delay_save(DirtyRamLoadDelay *out);
void dirty_ram_ld_delay_restore(const DirtyRamLoadDelay *in);
/* Re-anchor host-only dirty IRQ pump ambient (entry-poll stride + 4096-insn
* gap) after snap restore. Not in boot_state — peers that drifted through FMV
* otherwise entered the post-FMV dirty wait on opposite poll phases. */
+1
View File
@@ -41,6 +41,7 @@ uint32_t dma_cycles_to_deliverable_irq(uint32_t i_mask);
uint32_t dma_get_dicr(void);
uint32_t dma_get_dpcr(void);
int dma_cdrom_transfer_active(void);
int dma_gpu_linked_list_active(void);
typedef struct DMAChannelDebugState {
uint32_t madr;
+10
View File
@@ -44,6 +44,16 @@ int frame_interpolation_schedule_begin_phase(FrameInterpolationSchedule *schedul
double source_hz, double target_hz,
double phase_lo, double phase_hi);
/* Render passes present between guest VBlanks. The next output deadline, if
* it is already due (<= now) and not later than `horizon` (host ticks), with
* stale deadlines coalesced to the newest one as begin() does. It is not
* consumed: call frame_interpolation_schedule_consume() once it is shown, so
* the next source interval simply continues after it. */
int frame_interpolation_schedule_due(FrameInterpolationSchedule *schedule,
uint64_t now, double horizon,
uint64_t *deadline);
void frame_interpolation_schedule_consume(FrameInterpolationSchedule *schedule);
uint64_t frame_interpolation_schedule_end(
const FrameInterpolationSchedule *schedule);
void frame_interpolation_schedule_reset(FrameInterpolationSchedule *schedule);
+4
View File
@@ -46,6 +46,10 @@ typedef struct {
} GpuDisplayInfo;
void gpu_get_display_info(GpuDisplayInfo* out);
/* Render-pass GPU register checkpoint (render_pass.c). One at a time. */
int gpu_pass_checkpoint_save(void);
void gpu_pass_checkpoint_restore(void);
uint64_t gpu_pass_state_hash(void);
/* Perspective-correct UV arming rate, per condition. armed/attempts is the
* real perspective coverage: attempts counts only textured triangles, so it is
+47
View File
@@ -33,12 +33,59 @@ void gl_renderer_set_swap_interval(int interval);
void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz,
double source_hz, int blend_mode);
void gl_renderer_set_interpolation_suspended(int suspended);
/* Change only the blend mode (0 linear, 1 change-adaptive, 2 hold), keeping
* the history and schedule; takes effect at the next present. */
void gl_renderer_set_interpolation_blend(int blend_mode);
/* Blend source: 0 = every guest VBlank is a source frame (default),
* 1 = only real display flips are (psx_mod_set_frame_interpolation_source). */
void gl_renderer_set_interpolation_source(int source);
void gl_renderer_interpolation_source_diag(int *source, uint32_t *flip_period,
uint64_t *captures,
uint64_t *duplicates);
/* Render passes (render_pass.c; docs/RENDER_PASSES.md). The presenter is
* ready for passes: GL raster, interpolation live with the FLIP source. */
int gl_renderer_pass_ready(void);
/* Why not: 0 ready, PSX_MOD_RENDER_PASS_NO_PRESENTER or _BACKEND
* (mod_plugins.h). A backend mode that cannot host passes reports BACKEND. */
uint32_t gl_renderer_pass_unavailable(void);
/* Debug: make the backend decline passes (PSX_RENDER_PASS_REFUSE=1 at start,
* render_pass_refuse over TCP), as a renderer mode without them would. */
void gl_renderer_pass_force_refuse(int on);
/* Phases the presenter will show for the frame the next flip displays. */
uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
uint32_t shown_after_vblanks,
uint32_t *alpha_q16, uint32_t max,
uint32_t *wanted);
/* Open the VRAM transaction for rect; open_gen first captures the game's own
* image of it as a new generation. reuse_backup: the caller guarantees that
* no guest code ran since the previous pass ended (its restore left VRAM as
* backed up), so the backup of that pass is reused when it covers the same
* rect at the same scale. Returns 0 when refused (nothing changed). */
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
uint32_t period_vblanks, int reuse_backup);
/* Capture the drawn rect at alpha_q16 (keep) and roll the rect back. */
void gl_renderer_pass_end(uint32_t alpha_q16, int keep);
uint32_t gl_renderer_pass_leaks(void);
int gl_renderer_pass_verify_vram(void);
void gl_renderer_pass_note_cost(uint64_t ticks);
/* Present output deadlines that fell due while guest code ran. */
void gl_renderer_pass_service_presents(void);
/* promotions, presents, blended presents, expired, unmatched flips, presents
* made between VBlanks, smoothed pass cost (us), images in the shown frame,
* presents past the frame's planned end (the next flip was late; the newest
* image held), stale pass-cost estimates measured again. */
void gl_renderer_pass_diag(uint64_t out[10]);
/* Out-of-rect VRAM writes journaled and rolled back (lifetime count). */
uint64_t gl_renderer_pass_journaled(void);
/* Passes that reused the previous pass's VRAM backup (lifetime count). */
uint64_t gl_renderer_pass_backups_reused(void);
/* Pass image textures allocated now (both generations) and their bytes. */
uint32_t gl_renderer_pass_image_textures(uint64_t *bytes);
/* Debug: dump the images of the next `generations` shown frames as PNGs. */
void gl_renderer_pass_dump_arm(const char *dir, int generations);
uint64_t gl_renderer_perf_ticks(void);
uint64_t gl_renderer_perf_frequency(void);
int gl_renderer_interpolation_owns_cadence(void);
void gl_renderer_interpolation_diag(int *enabled, int *suspended,
int *history_frames,
+86 -1
View File
@@ -236,8 +236,17 @@ int psx_mod_set_frame_interpolation(uint32_t frames_per_second);
*/
enum {
PSX_MOD_FRAME_INTERPOLATION_LINEAR = 0,
PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE = 1
PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE = 1,
/* No crossfade: every output frame repeats the newest game frame. For a
* plugin that supplies its own in-between images with render passes
* (below); wherever it has none, the output matches stock timing and
* nothing is shown later than the game shows it. */
PSX_MOD_FRAME_INTERPOLATION_HOLD = 2
};
/* Usually called from activation. It may also be called later from the
* emulation thread (a function-entry hook or VBlank callback), e.g. to swap
* HOLD for a crossfade while render passes are unavailable; the OpenGL
* presenter then uses the new mode from its next present. */
int psx_mod_set_frame_interpolation_blend(uint32_t blend_mode);
/*
* Choose what the OpenGL presenter treats as a new source frame. VBLANK (the
@@ -254,6 +263,82 @@ enum {
PSX_MOD_FRAME_SOURCE_FLIP = 1
};
int psx_mod_set_frame_interpolation_source(uint32_t source);
/*
* Host-timed render passes: true in-between frames for a game whose logic
* runs slower than the presentation rate. Default off: nothing happens unless
* a trusted plugin calls these. OpenGL, frame interpolation enabled with the
* FLIP source, never in netplay, rollback, rewind, fast-forward (manual,
* turbo-through-loads, FMV auto-skip) or while the presenter is suspended
* (FMV); psx_mod_render_pass_plan() returns 0 then, and
* psx_mod_render_pass_status() says why.
*
* Call both from an emulation-thread function-entry hook placed where the
* game has finished its logic for game frame N+1 but the display still has to
* flip to frame N (for a PsyQ double-buffered loop: the VSync(0) that precedes
* PutDispEnv). Each pass runs `fn`, which may call guest functions with
* psx_dispatch_call() to draw an intermediate image of the scene. While it
* runs, guest time is frozen: cycles are counted but no device advances, no
* interrupt is delivered and GPU DMA completes synchronously; SPU, CD, timer
* and other device stores are dropped (counted). Afterwards CPU state
* (including the GTE), RAM, scratchpad, I-cache tags, interrupt, timer, DMA
* and GPU registers, and the VRAM rect are exactly as before: the pass's only
* product is the image of the rect, which the presenter shows at `alpha_q16`
* of the way through frame N's time on screen (Q16, 0 = frame N's own image).
* The display rect is the one the next flip shows (its DISPENV); the pass may
* draw only inside it.
*/
/* Returns nonzero to keep the image, 0 to discard it (state is restored
* either way). */
typedef int (*PSXModRenderPassFn)(struct CPUState* cpu, void* user,
uint32_t alpha_q16);
typedef struct PSXModRenderPass {
uint32_t struct_size; /* sizeof(PSXModRenderPass) */
uint32_t alpha_q16; /* a phase returned by the plan */
uint16_t x, y, w, h; /* VRAM display rect the pass draws */
} PSXModRenderPass;
/*
* Phases (Q16, ascending, excluding 0) at which the presenter will actually
* show frame N: its output deadlines during the `period_vblanks` guest VBlanks
* the frame stays on screen, starting after `shown_after_vblanks` more
* VBlank presents (R4 at its VSync(0) entry: 1 -- the next VBlank still shows
* the previous frame). When the host cannot afford them all, an evenly spread
* subset is returned and the presenter crossfades the gaps. The first
* psx_mod_render_pass() after a plan captures frame N's own image. Returns 0
* when passes are unavailable or unaffordable.
*/
uint32_t psx_mod_render_pass_plan(uint32_t period_vblanks,
uint32_t shown_after_vblanks,
uint32_t* alpha_q16, uint32_t max);
/* Returns 1 when the pass ran and its image was queued, 0 when it was refused
* or rolled back (state is restored either way). */
int psx_mod_render_pass(struct CPUState* cpu, const PSXModRenderPass* pass,
PSXModRenderPassFn fn, void* user);
/*
* Why passes cannot run right now, the host-time budget aside (a plan that
* returns 0 while this says READY was shed for time). A plugin that relies on
* passes uses it to fall back, e.g. to a crossfade with
* psx_mod_set_frame_interpolation_blend(), while the reason lasts.
* NO_PRESENTER, BACKEND and DISABLED persist; the others are transient.
*/
enum {
PSX_MOD_RENDER_PASS_READY = 0,
/* Not OpenGL, interpolation off or suspended (FMV), or not the FLIP
* source. */
PSX_MOD_RENDER_PASS_NO_PRESENTER = 1,
/* The renderer declines passes in its current mode. */
PSX_MOD_RENDER_PASS_BACKEND = 2,
/* Switched off for this session after repeated faults. */
PSX_MOD_RENDER_PASS_DISABLED = 3,
/* Netplay, rollback, rewind, load/save replay or self-check resim. */
PSX_MOD_RENDER_PASS_SESSION = 4,
/* Fast-forward, turbo-through-loads or FMV auto-skip is running. */
PSX_MOD_RENDER_PASS_FAST_FORWARD = 5,
/* Inside an exception, a pass or a GPU DMA walk, or the presenter has not
* captured a frame since its history restarted (display mode change). */
PSX_MOD_RENDER_PASS_BUSY = 6
};
uint32_t psx_mod_render_pass_status(void);
int psx_mod_set_auto_skip_fmv(int enabled);
/*
* Draw still artwork behind the game image in OpenGL letterbox/pillarbox
+5
View File
@@ -161,6 +161,11 @@ void overlay_loader_get_ci_skip_diag(uint64_t *unit, uint64_t *supp,
uint64_t *none, uint64_t *sr,
uint64_t *deliv, uint64_t *enter);
int overlay_loader_call_unit_depth(void);
/* Native-shard nesting (active-candidate stack depth, in-progress entry),
* saved before and restored after a landing that longjmps out of a shard
* and resumes the interrupted code (render_pass.c watchdog abort). */
void overlay_loader_native_nesting(int *active_depth, uint32_t *inprogress);
void overlay_loader_set_native_nesting(int active_depth, uint32_t inprogress);
#ifdef __cplusplus
}
+49
View File
@@ -0,0 +1,49 @@
#ifndef PSX_CYCLE_FREEZE_H
#define PSX_CYCLE_FREEZE_H
/* Render-pass time freeze (render_pass.c, docs/RENDER_PASSES.md).
*
* Runtime-only: generated and overlay code never sees this header. It is kept
* out of psx_cycles.h on purpose, because psx_cycles.h is part of the
* overlay codegen hash (runtime/codegen_hash_sources.cmake) and any change to
* it invalidates every title's overlay cache and savestates. The freeze is
* enforced in psx_cycles.c's out-of-line service paths, which generated code
* already reaches through psx_cycles.h.
*
* While g_psx_render_pass_active is set, guest cycles are still counted (GTE
* and mult/div deadlines keep working) but devices are never serviced, no
* VBlank or device event fires and no interrupt is delivered; end() puts
* every clock value back exactly, so the pass consumed no guest time. A pass
* that runs longer than `watchdog_cycles` calls `overrun` (which must not
* return into the guest; render_pass.c longjmps out of the pass). */
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
extern int g_psx_render_pass_active;
typedef struct PsxCycleFreeze {
uint64_t cycle_count;
uint64_t next_service;
uint64_t fast_limit;
uint32_t batch;
uint32_t batch_limit;
uint32_t *local_acc;
uint32_t local_acc_value;
int in_device_service;
/* Generated functions bump g_psx_cyc_bb_defer on entry and drop it in a
* cleanup handler, which a longjmp out of the pass (the watchdog) skips:
* end() puts the interrupted code's depth back. */
int bb_defer;
} PsxCycleFreeze;
int psx_cycle_freeze_begin(PsxCycleFreeze *save, uint64_t watchdog_cycles,
void (*overrun)(void));
void psx_cycle_freeze_end(const PsxCycleFreeze *save);
#ifdef __cplusplus
}
#endif
#endif
+61
View File
@@ -0,0 +1,61 @@
#ifndef PSX_RENDER_PASS_H
#define PSX_RENDER_PASS_H
/* Host-timed render passes: runtime side of psx_mod_render_pass_plan() and
* psx_mod_render_pass() (mod_plugins.h). See docs/RENDER_PASSES.md. */
#include <stdint.h>
#include "psx_cycle_freeze.h" /* g_psx_render_pass_active, the time freeze */
#ifdef __cplusplus
extern "C" {
#endif
/* Stores a pass may not make (memory.c render_pass_store): they would escape
* the restore, so they are dropped and counted per device class. */
enum {
RENDER_PASS_DROP_SPU = 0,
RENDER_PASS_DROP_CD,
RENDER_PASS_DROP_TIMER,
RENDER_PASS_DROP_DMA, /* DMA channels other than GPU (2) and OTC (6) */
RENDER_PASS_DROP_GPU, /* GP1 commands other than DMA mode / info */
RENDER_PASS_DROP_OTHER, /* SIO, MDEC, memory control, mod arenas, ... */
RENDER_PASS_DROP_CLASSES
};
extern uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES];
typedef struct RenderPassStats {
uint64_t plans; /* psx_mod_render_pass_plan calls that planned >= 1 */
uint64_t planned; /* phases planned (after shedding) */
uint64_t wanted; /* phases wanted before shedding */
uint64_t refused; /* plan calls that returned 0 while enabled */
uint64_t passes; /* passes run to completion and presented */
uint64_t aborted; /* passes rolled back by a fault */
uint64_t discarded; /* passes whose plugin declined the image */
uint64_t watchdog; /* of which: guest-cycle watchdog overruns */
uint64_t vram_leaks; /* GPU writes outside the declared rect (dropped) */
uint64_t nesting_repairs; /* aborts whose skipped exits the restore undid */
uint64_t verify_checks; /* PSX_RENDER_PASS_VERIFY comparisons */
uint64_t verify_mismatch; /* ... that found a difference */
uint64_t dropped[RENDER_PASS_DROP_CLASSES];
double last_pass_ms; /* host time of the last pass (sandbox + draw) */
double avg_pass_ms; /* smoothed */
double avg_begin_ms; /* smoothed: VRAM backup + checkpoint */
double avg_guest_ms; /* smoothed: the plugin's guest draw code */
double avg_end_ms; /* smoothed: GPU flush, capture, VRAM restore */
double avg_restore_ms; /* smoothed: machine state restore */
uint64_t guest_cycles_last; /* guest cycles the last pass executed */
int disabled; /* sticky: passes disabled after repeated faults */
int watchdog_flag; /* internal: the current pass overran */
} RenderPassStats;
void render_pass_get_stats(RenderPassStats *out);
/* Clears counters and the sticky fault latch (new mod session). */
void render_pass_reset_session(void);
#ifdef __cplusplus
}
#endif
#endif
+185
View File
@@ -0,0 +1,185 @@
#ifndef PSX_RENDER_PASS_PLAN_H
#define PSX_RENDER_PASS_PLAN_H
/* Pure planning and selection math for host-timed render passes
* (render_pass.c, gpu_gl_renderer.c). No runtime state; unit-tested by
* runtime/tests/test_render_pass_plan.c.
*
* Vocabulary. A game frame F is first presented at host tick `frame_start`
* and stays on screen for `frame_length` host ticks (its flip period in guest
* VBlanks times the presenter's source period). Phase p in [0, 1) is the
* position inside that window. Phase 0 is the game's own image of F; a pass
* rendered at phase a shows the game state a of the way from F to the next
* frame. Phases are carried as Q16 (65536 = 1). */
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define RENDER_PASS_MAX_PHASES 16u
typedef struct RenderPassPlanInput {
double next_deadline; /* the presenter's next output deadline (ticks) */
double target_period; /* ticks per output frame; <= 0 = unknown */
double frame_start; /* predicted first-present tick of the frame */
double frame_length; /* ticks the frame stays on screen */
double pass_cost; /* smoothed host ticks per pass; 0 = unknown:
with a budget, one pass is planned until one
has been measured */
double budget; /* host ticks the passes may use; < 0 = unlimited */
uint32_t max; /* caller's array capacity */
} RenderPassPlanInput;
/* Fill alpha_q16[] with the ascending phases the presenter will show during
* the frame (the output deadlines that fall strictly inside it, excluding
* ones within 1/64 of phase 0, which the game's own image already covers).
* When the budget cannot pay for all of them, an evenly spread subset is
* returned; while the pass cost is unknown that subset is one phase, since
* passes run on the emulation thread. *wanted (optional) receives the
* unshed count. Returns the count (0 when nothing is wanted or affordable). */
uint32_t render_pass_plan_phases(const RenderPassPlanInput *in,
uint32_t *alpha_q16, uint32_t *wanted);
/* Pick what to show at phase p (0..1+) from a frame's rendered items.
* phases[0..n) are ascending Q16 phases, phases[0] == 0 being the game's
* image. Sets *lo and *hi (item indices) and *t (blend weight of hi, 0..1):
* inside [phases[i], phases[i+1]] the two neighbours are blended; past the
* last item it is held (nothing newer exists yet). Returns 0 when n == 0. */
int render_pass_select(const uint32_t *phases, uint32_t n, double p,
uint32_t *lo, uint32_t *hi, float *t);
/* How long a promoted generation stays on screen without the next flip, in
* frame lengths (phase units), before it expires. */
#define RENDER_PASS_GEN_HOLD_MAX 4.0
/* What a promoted generation shows at phase p: render_pass_select, except
* that it expires (returns 0) past RENDER_PASS_GEN_HOLD_MAX or for a p that
* is not a number. Phase 1 and beyond means the next flip is late (the game
* frame lasts longer than the plan assumed, e.g. a lagging tick): the newest
* image holds, as a late stock frame would, and the presenter never falls
* back to its own capture of the frame, which is older (phase 0). Only a
* game that stops flipping for several frame lengths gets that back. */
int render_pass_gen_select(const uint32_t *phases, uint32_t n, double p,
uint32_t *lo, uint32_t *hi, float *t);
/* Exponential moving average used for the per-pass host cost. */
double render_pass_ema(double current, double sample);
/* Smoothed host cost of one pass at one presented image size (the renderer
* starts a new one when the size changes). Fed one sample per pass.
*
* - `allocated`: the pass created textures or framebuffers (first use, a
* size change). That one-time cost says nothing about the next pass -- at
* a high internal resolution the first passes cost several times the
* steady state -- and a plan that sheds every pass for time never
* measures again, so such a sample is left out, at most
* RENDER_PASS_ALLOC_SKIPS times in a row (any kept sample resets that).
* - Warm-up: the first RENDER_PASS_COST_WARMUP kept samples seed the average
* with their median, so one slow pass (a busy host at the first race
* frame) cannot price passes out. Until then the estimate is 0 (unknown),
* for which a plan asks for one pass (render_pass_plan_phases).
* - Then an exponential moving average (render_pass_ema).
* - Re-measuring (render_pass_cost_note_plan): only passes that run are
* measured, so an estimate that prices every plan out is never corrected
* by one -- and the first passes of a race can all run in a transient
* (a busy host, code seen for the first time) that costs several times the
* steady state. An estimate no pass has been measured against for
* `rewarm_after` plans (RENDER_PASS_REWARM_MIN, about a second of 30 Hz
* frames) restarts the warm-up. When the new median is more than
* RENDER_PASS_REWARM_STALE of the old estimate, the old one was right -- a
* size that is truly too expensive, or a machine at its limit -- and the
* next wait doubles, up to RENDER_PASS_REWARM_MAX; a stale estimate found
* keeps the wait at the minimum. */
#define RENDER_PASS_ALLOC_SKIPS 8u
#define RENDER_PASS_COST_WARMUP 3u
#define RENDER_PASS_REWARM_MIN 30u
#define RENDER_PASS_REWARM_MAX 960u
#define RENDER_PASS_REWARM_STALE 0.75
typedef struct RenderPassCost {
double ema;
double warm[RENDER_PASS_COST_WARMUP];
unsigned kept; /* kept samples, saturating at WARMUP */
unsigned skips; /* allocating samples left out in a row */
unsigned unsampled; /* plans since the last kept sample */
unsigned rewarm_after; /* 0 = RENDER_PASS_REWARM_MIN */
double rewarm_from; /* estimate being re-measured; 0 = none */
} RenderPassCost;
void render_pass_cost_add(RenderPassCost *cost, double sample, int allocated);
/* Host ticks per pass for planning; 0 while the cost is unknown. */
double render_pass_cost_estimate(const RenderPassCost *cost);
/* Call once per plan that wanted passes at this cost's image size. Returns 1
* when the plan found the estimate stale and restarted the warm-up. */
int render_pass_cost_note_plan(RenderPassCost *cost);
/* Next frame's pass budget from the last frame: the host time the presenter
* spent idle-waiting plus the time passes used, scaled by `share` (0..1) and
* clamped to [0, frame_length]. No history (both zero) -> share of the frame. */
double render_pass_budget(double idle_ticks, double pass_ticks,
double frame_length, double share);
/* Store policy inside a pass (memory.c): -1 = the MMIO store may reach its
* device (GP0; GP1 DMA mode 0x04 / info 0x10; GPU and OTC DMA channels;
* DPCR/DICR; I_STAT/I_MASK -- all restored after the pass), otherwise the
* RENDER_PASS_DROP_* class it is dropped and counted under (render_pass.h). */
int render_pass_mmio_class(uint32_t phys, uint32_t val, uint32_t width);
/* One guest store made inside a pass (memory.c render_pass_store): RAM and
* scratchpad are written directly, bypassing every live-timeline observer;
* MMIO reaches `mmio_write` only when render_pass_mmio_class allows it; KSEG2
* (cache control), mod memory, expansion and ROM are dropped. Returns -1 when
* the store was made (or absorbed: an isolated-cache store changes nothing a
* pass restores), else the RENDER_PASS_DROP_* class it was dropped under,
* which the caller counts. */
typedef struct RenderPassStoreTarget {
uint8_t *ram; /* main RAM backing store */
uint32_t ram_size; /* LIVE geometry, a power of two: 2 MiB retail
(mirrored 4x in the 8 MiB window) or 8 MiB
(psx_ram_live_bytes()) */
uint8_t *scratchpad;
uint32_t scratchpad_size;
int isolate_cache; /* COP0 SR IsC is set */
void (*mmio_write)(uint32_t phys, uint32_t val, uint32_t width);
} RenderPassStoreTarget;
int render_pass_store_to(const RenderPassStoreTarget *t, uint32_t addr,
uint32_t val, uint32_t width);
/* VRAM journal for a pass's writes outside its display rect
* (gpu_gl_renderer.c pass_refuse_write): the CPU-side policy and the CPU VRAM
* rows. The renderer keeps the matching GPU copies per entry. */
#define RENDER_PASS_JOURNAL_MAX 16
enum {
RENDER_PASS_VRAM_ALLOW = 0, /* inside the rect, empty, or already journaled */
RENDER_PASS_VRAM_JOURNAL = 1, /* outside: back up, then allow */
RENDER_PASS_VRAM_REFUSE = 2 /* outside and cannot be journaled */
};
typedef struct RenderPassJournal {
int n;
int x[RENDER_PASS_JOURNAL_MAX], y[RENDER_PASS_JOURNAL_MAX];
int w[RENDER_PASS_JOURNAL_MAX], h[RENDER_PASS_JOURNAL_MAX];
uint16_t *rows[RENDER_PASS_JOURNAL_MAX];
size_t cap[RENDER_PASS_JOURNAL_MAX];
} RenderPassJournal;
/* Clip the write (*x, *y, *w, *h) to vram_w x vram_h and decide it against
* the pass rect (px, py, pw, ph) and the journal. can_journal = 0 refuses
* every outside write. */
int render_pass_vram_policy(const RenderPassJournal *j, int px, int py,
int pw, int ph, int vram_w, int vram_h,
int can_journal, int *x, int *y, int *w, int *h);
/* Record the CPU rows of rect (x, y, w, h) of vram (vram_w halfwords per
* row) as the next entry. Returns its index, or -1 when full / out of
* memory (nothing recorded). */
int render_pass_journal_add(RenderPassJournal *j, const uint16_t *vram,
int vram_w, int x, int y, int w, int h);
/* Put every entry's rows back, newest first, and empty the journal. */
void render_pass_journal_rollback(RenderPassJournal *j, uint16_t *vram,
int vram_w);
void render_pass_journal_free(RenderPassJournal *j);
#ifdef __cplusplus
}
#endif
#endif
+2
View File
@@ -350,6 +350,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/psx_video_timing.c
${PSXRECOMP_ROOT}/runtime/src/frame_pacing.c
${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/host_time.c
${PSXRECOMP_ROOT}/runtime/src/psx_fiber.c
${PSXRECOMP_ROOT}/runtime/src/sio.c
+109
View File
@@ -47,6 +47,8 @@
#include "card_data_writes.h"
#include "crash_trace.h"
#include "gpu_gl_renderer.h"
#include "render_pass.h"
#include "mod_plugins.h" /* psx_mod_render_pass_status */
#include "lockstep.h"
#include "guest_tty.h"
#include "frame_fingerprint.h"
@@ -4988,8 +4990,27 @@ static void handle_frame(int id, const char *json)
/* Longest entry the format below can produce: every hash at 16 hex digits and
* every counter at 20 decimal digits comes to 284 bytes with the separator. */
#define FP_JSON_ENTRY_MAX 320
/* frame_fingerprint reset_on_load=1: restart the rolling hashes and the ring
* when the next savestate load completes, so two runs that load the same state
* (for example with and without a presentation mod) compare frame by frame
* from that guest point on, independent of what happened before the load. */
static int s_fp_reset_on_load = 0;
void debug_server_note_savestate_loaded(void)
{
if (!s_fp_reset_on_load) return;
s_fp_reset_on_load = 0;
s_fp = (PsxFrameFingerprint)PSX_FRAME_FINGERPRINT_INIT;
s_fp_head = 0;
s_fp_total = 0;
}
static void handle_frame_fingerprint(int id, const char *json)
{
if (json_get_int(json, "reset_on_load", 0)) {
s_fp_reset_on_load = 1;
send_fmt("{\"id\":%d,\"ok\":true,\"armed\":1}", id);
return;
}
int count = json_get_int(json, "count", 1024);
if (count < 1) count = 1;
if (count > FP_RING_CAP) count = FP_RING_CAP;
@@ -7915,6 +7936,87 @@ static void handle_gl_interp(int id, const char *json)
(unsigned long long)duplicates);
}
/* render_pass_stats: host-timed render passes (docs/RENDER_PASSES.md).
* Counters are per mod session; `dropped` counts device stores a pass tried
* to make (SPU key-ons, CD, timers, ...), which never reach the device. */
static void handle_render_pass_stats(int id, const char *json)
{
(void)json;
RenderPassStats st;
uint64_t gd[10], image_bytes = 0;
uint32_t image_textures;
render_pass_get_stats(&st);
gl_renderer_pass_diag(gd);
image_textures = gl_renderer_pass_image_textures(&image_bytes);
send_fmt("{\"id\":%d,\"ok\":true,\"plans\":%llu,\"planned\":%llu,"
"\"wanted\":%llu,\"refused\":%llu,\"passes\":%llu,"
"\"aborted\":%llu,\"discarded\":%llu,\"watchdog\":%llu,\"vram_leaks\":%llu,"
"\"nesting_repairs\":%llu,"
"\"verify_checks\":%llu,\"verify_mismatch\":%llu,"
"\"dropped\":{\"spu\":%llu,\"cd\":%llu,\"timer\":%llu,"
"\"dma\":%llu,\"gpu\":%llu,\"other\":%llu},"
"\"last_pass_ms\":%.3f,\"avg_pass_ms\":%.3f,"
"\"avg_begin_ms\":%.3f,\"avg_guest_ms\":%.3f,"
"\"avg_end_ms\":%.3f,\"avg_restore_ms\":%.3f,"
"\"guest_cycles_last\":%llu,\"disabled\":%d,"
"\"promotions\":%llu,\"pass_presents\":%llu,"
"\"blended_presents\":%llu,\"expired\":%llu,\"late_presents\":%llu,"
"\"unmatched_flips\":%llu,\"early_presents\":%llu,"
"\"cost_us\":%llu,\"cost_rewarms\":%llu,\"frame_images\":%llu,"
"\"journaled\":%llu,"
"\"image_textures\":%u,\"image_bytes\":%llu,\"status\":%u,"
"\"backups_reused\":%llu}",
id, (unsigned long long)st.plans, (unsigned long long)st.planned,
(unsigned long long)st.wanted, (unsigned long long)st.refused,
(unsigned long long)st.passes, (unsigned long long)st.aborted,
(unsigned long long)st.discarded, (unsigned long long)st.watchdog, (unsigned long long)st.vram_leaks,
(unsigned long long)st.nesting_repairs,
(unsigned long long)st.verify_checks,
(unsigned long long)st.verify_mismatch,
(unsigned long long)st.dropped[RENDER_PASS_DROP_SPU],
(unsigned long long)st.dropped[RENDER_PASS_DROP_CD],
(unsigned long long)st.dropped[RENDER_PASS_DROP_TIMER],
(unsigned long long)st.dropped[RENDER_PASS_DROP_DMA],
(unsigned long long)st.dropped[RENDER_PASS_DROP_GPU],
(unsigned long long)st.dropped[RENDER_PASS_DROP_OTHER],
st.last_pass_ms, st.avg_pass_ms,
st.avg_begin_ms, st.avg_guest_ms, st.avg_end_ms, st.avg_restore_ms,
(unsigned long long)st.guest_cycles_last, st.disabled,
(unsigned long long)gd[0], (unsigned long long)gd[1],
(unsigned long long)gd[2], (unsigned long long)gd[3],
(unsigned long long)gd[8],
(unsigned long long)gd[4], (unsigned long long)gd[5],
(unsigned long long)gd[6], (unsigned long long)gd[9],
(unsigned long long)gd[7],
(unsigned long long)gl_renderer_pass_journaled(),
(unsigned)image_textures, (unsigned long long)image_bytes,
(unsigned)psx_mod_render_pass_status(),
(unsigned long long)gl_renderer_pass_backups_reused());
}
/* render_pass_refuse on=<0|1>: make the OpenGL backend decline render passes
* (status BACKEND), as a renderer mode without them would; for testing a
* plugin's fallback. Same as PSX_RENDER_PASS_REFUSE=1 at start. */
static void handle_render_pass_refuse(int id, const char *json)
{
int on = json_get_int(json, "on", 1);
gl_renderer_pass_force_refuse(on);
send_fmt("{\"id\":%d,\"ok\":true,\"on\":%d,\"status\":%u}", id, on ? 1 : 0,
(unsigned)psx_mod_render_pass_status());
}
/* render_pass_dump path=<dir> count=<n>: write the images (the game's own
* frame, then each render pass in phase order) of the next n frames that get
* render passes, as <dir>/g<frame>_<index>_a<phase q16>.png. */
static void handle_render_pass_dump(int id, const char *json)
{
char dir[400];
int count = json_get_int(json, "count", 1);
if (!json_get_str(json, "path", dir, sizeof dir)) { send_err(id, "missing path"); return; }
gl_renderer_pass_dump_arm(dir, count);
send_fmt("{\"id\":%d,\"ok\":true,\"count\":%d}", id, count);
}
/* gl_wide_fast on=<0|1>: native-wide centre-blit fast path. 1 (default) = skip
* the redundant centre mirror and copy the canonical 4:3 frame into the wide
* surface centre at present (fast). 0 = re-rasterize the whole wide surface
@@ -9594,6 +9696,9 @@ void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
(void)phys; (void)old_val; (void)new_val; (void)width;
return;
#endif
/* Render passes are rolled back; keep them out of the live timeline's
* fingerprints and write traces (docs/RENDER_PASSES.md). */
if (g_psx_render_pass_active) return;
if (s_fmv_quiet) {
if (psx_fp_write_eligible(phys, psx_ram_live_bytes())) psx_fp_note_quiet(&s_fp);
return;
@@ -9622,6 +9727,7 @@ void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
/* MMIO write trace — called from memory.c mmio_write32/16/8. */
void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width)
{
if (g_psx_render_pass_active) return; /* rolled back: not live */
#ifdef PSX_NO_DEBUG_TOOLS
(void)addr; (void)val; (void)width;
return;
@@ -13832,6 +13938,9 @@ static const CmdEntry s_commands[] = {
{ "frame_perf", handle_frame_perf },
{ "gl_ws_ablate", handle_gl_ws_ablate },
{ "gl_interp", handle_gl_interp },
{ "render_pass_stats", handle_render_pass_stats },
{ "render_pass_dump", handle_render_pass_dump },
{ "render_pass_refuse", handle_render_pass_refuse },
{ "gl_wide_fast", handle_gl_wide_fast },
{ "synth_recurse", handle_synth_recurse },
{ "gl_fbo_peek", handle_gl_fbo_peek },
+16
View File
@@ -349,6 +349,22 @@ void dirty_ram_ld_delay_discard(void) {
s_ld_pend_val = 0;
}
void dirty_ram_ld_delay_save(DirtyRamLoadDelay *out) {
if (!out) return;
out->rt = s_ld_pend_rt;
out->val = s_ld_pend_val;
out->age = s_ld_pend_age;
out->armed = s_ld_pend_armed;
}
void dirty_ram_ld_delay_restore(const DirtyRamLoadDelay *in) {
if (!in) return;
s_ld_pend_rt = in->rt;
s_ld_pend_val = in->val;
s_ld_pend_age = in->age;
s_ld_pend_armed = in->armed;
}
void dirty_ram_irq_ambient_resync_after_restore(void) {
/* Re-anchor host-only IRQ pump ambient at the restored timeline so both
* peers take the first post-load dirty entry poll from the same phase. */
+13 -1
View File
@@ -23,6 +23,7 @@
#include "overlay_capture.h"
#include "psx_cycles.h"
#include "psx_memory.h"
#include "psx_cycle_freeze.h"
#include "spu.h"
#include "audio_trace.h"
#include "event_ring.h"
@@ -437,7 +438,9 @@ static void cancel_async_transfer(int ch) {
static void schedule_delayed_complete(int ch, uint32_t total_words,
uint32_t cycles_per_word) {
if (total_words == 0 || cycles_per_word == 0) {
/* Render passes run in frozen guest time: a delayed completion would
* never arrive, so finish now (the pass restores DMA state after). */
if (total_words == 0 || cycles_per_word == 0 || g_psx_render_pass_active) {
complete_transfer(ch);
return;
}
@@ -968,6 +971,13 @@ static void try_execute(int ch) {
if ((channels[2].chcr & 1u) != 0u &&
((channels[2].chcr >> 9) & 3u) == 2u) {
start_async_gpu_linked_list();
/* Render pass: devices never advance in frozen time, so walk
* the whole list now. The walk is the same event-driven code,
* only with an unlimited budget; completion flags and the IRQ
* latch it raises are rolled back with the pass. */
while (g_psx_render_pass_active && gpu_linked_list.active)
dma_gpu_ll_advance(&gpu_linked_list, UINT32_MAX,
&gpu_ll_ops, NULL);
} else {
schedule_delayed_complete(2, execute_ch2_gpu(),
DMA_GPU_CYCLES_PER_WORD);
@@ -1002,6 +1012,8 @@ static void try_execute(int ch) {
/* ---- Public interface ---- */
uint32_t dma_get_dicr(void) { return dicr_read_value(dicr); }
/* A GPU linked-list walk is in flight (render passes refuse to start then). */
int dma_gpu_linked_list_active(void) { return gpu_linked_list.active != 0; }
uint32_t dma_get_dpcr(void) { return dpcr; }
int dma_cdrom_transfer_active(void) {
return cdrom_async.active &&
+22
View File
@@ -95,6 +95,28 @@ int frame_interpolation_schedule_next(FrameInterpolationSchedule *schedule,
return 1;
}
int frame_interpolation_schedule_due(FrameInterpolationSchedule *schedule,
uint64_t now, double horizon,
uint64_t *deadline) {
double now_d = (double)now;
if (!schedule || schedule->target_period <= 0.0 ||
schedule->next_present_deadline <= 0.0)
return 0;
while (schedule->next_present_deadline + schedule->target_period <= now_d &&
schedule->next_present_deadline + schedule->target_period <= horizon)
schedule->next_present_deadline += schedule->target_period;
if (schedule->next_present_deadline > now_d ||
schedule->next_present_deadline > horizon)
return 0;
if (deadline) *deadline = (uint64_t)(schedule->next_present_deadline + 0.5);
return 1;
}
void frame_interpolation_schedule_consume(FrameInterpolationSchedule *schedule) {
if (schedule && schedule->target_period > 0.0)
schedule->next_present_deadline += schedule->target_period;
}
uint64_t frame_interpolation_schedule_end(
const FrameInterpolationSchedule *schedule) {
if (!schedule || schedule->frame_end <= 0.0) return 0;
+61
View File
@@ -6299,5 +6299,66 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) {
ws_nw_sync_target();
return 1;
}
/* ---- Render-pass GPU checkpoint (render_pass.c) ---------------------------
* The same register set as a savestate (gpu_snap_emit), restored WITHOUT
* gpu_snapshot_read's side effects (widescreen scene history and HUD anchor
* tags belong to the live frame, which a pass must not reset). After the
* restore the renderer's mirrored draw state (area, offset, texture window,
* mask bits, native-wide target) is re-synced exactly as GP0(E2..E6) would. */
static uint8_t s_pass_regs[512];
static uint32_t s_pass_regs_len;
static uint32_t s_pass_poll_count;
static int32_t s_pass_doff_min, s_pass_doff_max;
static uint32_t s_pass_doff_cnt;
static GpuVerticalSplitTrace s_pass_split_this;
int gpu_pass_checkpoint_save(void) {
PstW w;
uint32_t n = gpu_snapshot_bytes();
if (n > sizeof s_pass_regs) return 0;
pst_w_init(&w, s_pass_regs, n);
if (!gpu_snap_emit(&w)) return 0;
s_pass_regs_len = n;
s_pass_poll_count = gpustat_poll_count;
s_pass_doff_min = g_doff_min_this;
s_pass_doff_max = g_doff_max_this;
s_pass_doff_cnt = g_doff_cnt_this;
s_pass_split_this = split_trace_this;
return 1;
}
void gpu_pass_checkpoint_restore(void) {
PstR r;
if (!s_pass_regs_len) return;
pst_r_init(&r, s_pass_regs, s_pass_regs_len);
(void)gpu_snap_parse(&r);
gpustat_poll_count = s_pass_poll_count;
g_doff_min_this = s_pass_doff_min;
g_doff_max_this = s_pass_doff_max;
g_doff_cnt_this = s_pass_doff_cnt;
split_trace_this = s_pass_split_this;
gr_set_texture_window(texture_window_value);
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
(int)draw_area_right, (int)draw_area_bottom);
gr_set_draw_offset(draw_offset_x, draw_offset_y);
gr_set_mask_bits((int)set_mask_bit, (int)check_mask_bit);
ws_nw_sync_target();
s_pass_regs_len = 0;
}
/* FNV-1a over the checkpointed register set (PSX_RENDER_PASS_VERIFY). */
uint64_t gpu_pass_state_hash(void) {
uint8_t buf[512];
PstW w;
uint64_t h = 1469598103934665603ULL;
uint32_t n = gpu_snapshot_bytes();
if (n > sizeof buf) return 0;
pst_w_init(&w, buf, n);
if (!gpu_snap_emit(&w)) return 0;
for (uint32_t i = 0; i < n; i++) h = (h ^ buf[i]) * 1099511628211ULL;
h = (h ^ gpustat_poll_count) * 1099511628211ULL;
return h;
}
uint16_t* gpu_get_vram_ptr(void){ return vram; }
uint32_t gpu_get_vram_bytes(void){ return (uint32_t)sizeof(vram); }
File diff suppressed because it is too large Load Diff
+6
View File
@@ -37,6 +37,7 @@
#include "event_ring.h"
#include "lockstep.h"
#include "psx_cycles.h"
#include "psx_cycle_freeze.h"
#include "psx_scheduler.h"
#include "psx_video_timing.h"
#include "spu.h"
@@ -1067,6 +1068,8 @@ void psx_interrupt_check_path_diag(uint64_t *entry, uint64_t *fast_sr,
}
int psx_interrupt_delivery_needed(const CPUState* cpu) {
/* Render passes run in frozen guest time: nothing is delivered. */
if (g_psx_render_pass_active) return 0;
if (s_defer_switch_pending) { s_need_defer++; return 1; }
if ((i_stat & i_mask) == 0) { s_skip_none++; return 0; }
@@ -1091,6 +1094,9 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) {
}
void psx_check_interrupts(CPUState* cpu) {
/* Render passes run in frozen guest time (psx_cycles.h): no delivery, and
* no check-boundary bookkeeping that the stock timeline would not do. */
if (g_psx_render_pass_active) return;
psx_cyc_batch_flush();
extern int g_ls_suppress_record;
extern int psx_netplay_active(void);
+28 -2
View File
@@ -44,6 +44,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "gpu_sw_renderer.h"
#include "gpu_render.h"
#include "gpu_gl_renderer.h"
#include "render_pass.h"
/* Declarations only: STB_IMAGE_IMPLEMENTATION lives in psx_window_icon.cpp. */
#define STBI_NO_STDIO
#include "../third_party/stb_image.h"
@@ -1017,6 +1018,9 @@ extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok)
extern "C" void psx_frontend_on_savestate_loaded(void) {
mod_runtime_on_savestate_loaded();
#ifndef PSX_NO_DEBUG_TOOLS
debug_server_note_savestate_loaded();
#endif
s_disabled_frame_presented = false;
s_force_present_after_load = true;
smooth_60_reset();
@@ -1268,6 +1272,12 @@ static int g_frame_interpolation_blend =
PSX_MOD_FRAME_INTERPOLATION_LINEAR;
static int g_frame_interpolation_blend_default =
PSX_MOD_FRAME_INTERPOLATION_LINEAR;
/* Presentation is sped up this VBlank (manual fast-forward, turbo-through-
* loads, FMV auto-skip, TCP turbo). Render passes are refused meanwhile. */
static int s_presentation_fast_forward = 0;
extern "C" int psx_presentation_fast_forward(void) {
return s_presentation_fast_forward;
}
/* Mod-owned blend source; reset_mod_owned_presentation() sets VBLANK. */
static int g_frame_interpolation_source =
PSX_MOD_FRAME_SOURCE_VBLANK;
@@ -1469,6 +1479,9 @@ static void reset_mod_owned_presentation(void) {
g_bezel_path.clear();
g_frame_interpolation_blend = g_frame_interpolation_blend_default;
g_frame_interpolation_source = PSX_MOD_FRAME_SOURCE_VBLANK;
/* Render-pass counters, the disabled-after-faults latch and any open
* plan generation belong to the session that made them. */
render_pass_reset_session();
}
/* The disc a session mounts: a disc-patching mod's private patched image when
@@ -1555,16 +1568,23 @@ extern "C" int psx_mod_set_frame_interpolation(
extern "C" int psx_mod_set_frame_interpolation_blend(
uint32_t blend_mode) {
if (blend_mode != PSX_MOD_FRAME_INTERPOLATION_LINEAR &&
blend_mode != PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE) {
blend_mode != PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE &&
blend_mode != PSX_MOD_FRAME_INTERPOLATION_HOLD) {
std::fprintf(stderr,
"psxrecomp: mod rejected invalid frame-interpolation blend %u\n",
(unsigned)blend_mode);
return 0;
}
g_frame_interpolation_blend = (int)blend_mode;
/* Live when the presenter is already configured (a later call from a
* hook); before that, session start hands it over with the rates. */
gl_renderer_set_interpolation_blend(g_frame_interpolation_blend);
std::fprintf(stdout, "psxrecomp: frame-interpolation blend = %s\n",
blend_mode == PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE
? "motion-adaptive clarity" : "linear crossfade");
? "motion-adaptive clarity"
: blend_mode == PSX_MOD_FRAME_INTERPOLATION_HOLD
? "hold (render passes supply in-between frames)"
: "linear crossfade");
return 1;
}
@@ -7078,6 +7098,10 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
}
#endif
/* Render passes (render_pass.c) stay off while presentation is sped up. */
s_presentation_fast_forward =
(turbo_loads_active || fmv_skip_active) ? 1 : 0;
if (g_headless) {
ep.skip_pace = 1;
return ep;
@@ -7090,6 +7114,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
* presentation and wall-clock pacing. */
#ifndef PSX_NO_DEBUG_TOOLS
if (debug_server_turbo_enabled()) {
s_presentation_fast_forward = 1;
ep.skip_pace = 1;
return ep;
}
@@ -7118,6 +7143,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
const int mult = manual_fast_forward_multiplier();
const int present_every = (mult < 0) ? 4 : (mult <= 4 ? 2 : 4);
manual_turbo_active = true;
s_presentation_fast_forward = 1;
if (!turbo_was_down && !g_manual_turbo_latched) {
char msg[40];
if (mult < 0)
+39
View File
@@ -26,6 +26,8 @@
#include "guest_tty.h"
#include "psx_cycles.h"
#include "psx_memory.h"
#include "render_pass.h"
#include "render_pass_plan.h"
#include "starvation_ring.h"
#include <stdint.h>
#include <stdio.h>
@@ -1703,10 +1705,45 @@ static inline void d44_note(uint32_t phys, uint32_t old, uint32_t val) {
e->frame = (uint32_t)s_frame_count;
}
/* ---- Render-pass stores (render_pass.c, docs/RENDER_PASSES.md) -------------
* A render pass runs guest draw code in frozen time and afterwards restores
* RAM, scratchpad, CPU, GPU and DMA state byte-for-byte. Its stores therefore
* bypass every observer of the live timeline (code-page tracking, overlay
* watch, write traces, card/parity hooks) and go straight to memory. MMIO is
* an allow-list: the GPU (GP0, and GP1 DMA-direction / info queries), the GPU
* and OTC DMA channels with DPCR/DICR, and I_STAT/I_MASK -- all of which the
* pass restores. Everything else (SPU key-ons, CD, timers, SIO, MDEC, memory
* control, other DMA channels) would escape the restore, so it is dropped and
* counted instead. */
uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES];
static void render_pass_mmio_write(uint32_t phys, uint32_t val,
uint32_t width) {
if (width == 4) mmio_write32(phys, val);
else if (width == 2) mmio_write16(phys, (uint16_t)val);
else mmio_write8(phys, (uint8_t)val);
}
/* The policy is render_pass_store_to (render_pass_plan.c, unit-tested by
* render_pass_sandbox_test); this only wires it to memory.c's arrays. */
static void render_pass_store(uint32_t addr, uint32_t val, uint32_t width) {
RenderPassStoreTarget t;
int cls;
t.ram = ram;
t.ram_size = psx_ram_live_bytes(); /* live geometry, as psx_ram_map_write */
t.scratchpad = scratchpad;
t.scratchpad_size = SCRATCHPAD_SIZE;
t.isolate_cache = (sr_ptr && (*sr_ptr & 0x10000u)) ? 1 : 0;
t.mmio_write = render_pass_mmio_write;
cls = render_pass_store_to(&t, addr, val, width);
if (cls >= 0) g_render_pass_dropped_writes[cls]++;
}
static void psx_write_word_raw(uint32_t addr, uint32_t val);
void psx_write_word(uint32_t addr, uint32_t val) {
extern void (*g_overlay_flush_pending_cycles)(void);
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
if (g_psx_render_pass_active) { render_pass_store(addr, val, 4); return; }
if (g_ls_mode == 2) { ls_write_hook(addr, 4, val); return; }
if (g_ds_recording) {
if (g_dma_exec_depth > 0) ds_note_dma_write();
@@ -1909,6 +1946,7 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val);
void psx_write_half(uint32_t addr, uint16_t val) {
extern void (*g_overlay_flush_pending_cycles)(void);
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
if (g_psx_render_pass_active) { render_pass_store(addr, val, 2); return; }
if (g_ls_mode == 2) { ls_write_hook(addr, 2, val); return; }
if (g_ds_recording) {
if (g_dma_exec_depth > 0) ds_note_dma_write();
@@ -2252,6 +2290,7 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val);
void psx_write_byte(uint32_t addr, uint8_t val) {
extern void (*g_overlay_flush_pending_cycles)(void);
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
if (g_psx_render_pass_active) { render_pass_store(addr, val, 1); return; }
if (g_ls_mode == 2) { ls_write_hook(addr, 1, val); return; }
if (g_ls_mode != 1 || s_ls_op_active || g_ls_suppress_record || g_dma_exec_depth > 0) { psx_write_byte_raw(addr, val); return; }
if (!psx_get_in_exception()) ls_write_hook(addr, 1, val);
+15
View File
@@ -2234,6 +2234,21 @@ void overlay_loader_get_ci_skip_diag(uint64_t *unit, uint64_t *supp,
}
int overlay_loader_call_unit_depth(void) { return g_call_unit_depth; }
/* Native-shard nesting for a landing that longjmps out of a shard and then
* resumes the interrupted code (the render-pass watchdog abort, render_pass.c):
* the active-candidate stack depth and the in-progress entry are put back by
* the dispatch frames the longjmp skips. */
void overlay_loader_native_nesting(int *active_depth, uint32_t *inprogress) {
if (active_depth) *active_depth = s_active_depth;
if (inprogress) *inprogress = s_native_inprogress;
}
void overlay_loader_set_native_nesting(int active_depth, uint32_t inprogress) {
if (active_depth >= 0 &&
active_depth <= (int)(sizeof(s_active_stack) / sizeof(s_active_stack[0])))
s_active_depth = active_depth;
s_native_inprogress = inprogress;
}
static int overlay_irq_suppressed_now(void) {
/* Differential replay (and its authoritative interpreter pass) is atomic.
* Never let a previously armed rate-limit punch a real IRQ into a shadow. */
+72
View File
@@ -1,6 +1,7 @@
/* psx_cycles.c — PSX guest CPU cycle clock. */
#include "psx_cycles.h"
#include "psx_cycle_freeze.h"
#include "cpu_state.h"
#include <stdlib.h>
#include <string.h>
@@ -28,6 +29,26 @@ uint32_t *g_psx_cyc_local_acc = NULL;
static int s_cycle_replay_active = 0;
static uint64_t s_cycle_replay_live = 0;
/* ---- render-pass freeze (see psx_cycle_freeze.h) ---- */
int g_psx_render_pass_active = 0;
static uint64_t s_freeze_start = 0;
static uint64_t s_freeze_limit = 0;
static void (*s_freeze_overrun)(void) = 0;
static void psx_cycle_freeze_tick(void) {
extern uint64_t g_psx_cycle_fast_limit;
/* Come back at most every 16K cycles for the watchdog; never service. */
psx_next_service_cycle = psx_cycle_count + 16384u;
g_psx_cycle_fast_limit = 0;
if (s_freeze_limit &&
psx_cycle_count - s_freeze_start > s_freeze_limit &&
s_freeze_overrun) {
void (*overrun)(void) = s_freeze_overrun;
s_freeze_overrun = 0;
overrun();
}
}
int psx_cycle_replay_begin(uint64_t start_cycle) {
extern int g_ls_replay_active;
if (!g_ls_replay_active || s_cycle_replay_active) return 0;
@@ -181,6 +202,7 @@ static void psx_devices_recompute_deadline(void) {
}
void psx_devices_service_to_now(void) {
if (g_psx_render_pass_active) { psx_cycle_freeze_tick(); return; }
if (s_in_device_service) return; /* device code charged cycles: absorb */
if (g_plp_cycle_diag) g_plp_svc_calls++;
g_psx_cycle_fast_limit = 0;
@@ -243,6 +265,7 @@ void psx_devices_service_to_now(void) {
* that on every GPU/CD/MDEC MMIO touch. Recompute here instead. */
void psx_devices_mmio_sync(void) {
psx_cyc_batch_flush();
if (g_psx_render_pass_active) { psx_cycle_freeze_tick(); return; }
if (s_devices_synced_cycle != psx_cycle_count) {
psx_devices_service_to_now();
} else {
@@ -304,6 +327,11 @@ void psx_advance_cycles_slow(uint32_t cycles) {
}
}
if (cycles == 0) return;
if (g_psx_render_pass_active) {
psx_cycle_count += (uint64_t)cycles;
if (psx_cycle_count >= psx_next_service_cycle) psx_cycle_freeze_tick();
return;
}
#ifdef PSX_COSIM
psx_advance_cycles_exact(cycles);
#else
@@ -792,3 +820,47 @@ void psx_gte_stall(CPUState* cpu) {
psx_advance_cycles((uint32_t)(cpu->gte_ts_done - psx_cycle_count));
}
}
int psx_cycle_freeze_begin(PsxCycleFreeze *save, uint64_t watchdog_cycles,
void (*overrun)(void)) {
extern uint64_t g_psx_cycle_fast_limit;
if (!save || g_psx_render_pass_active) return 0;
save->cycle_count = psx_cycle_count;
save->next_service = psx_next_service_cycle;
save->fast_limit = g_psx_cycle_fast_limit;
save->batch = g_psx_cyc_batch;
save->batch_limit = g_psx_cyc_batch_limit;
save->local_acc = g_psx_cyc_local_acc;
save->local_acc_value = g_psx_cyc_local_acc ? *g_psx_cyc_local_acc : 0u;
save->in_device_service = psx_in_device_service;
save->bb_defer = g_psx_cyc_bb_defer;
/* Pending deferred charges stay pending: they belong to the interrupted
* guest code and are republished, unchanged, after the pass. */
g_psx_cyc_batch = 0;
g_psx_cyc_batch_limit = 0;
g_psx_cyc_local_acc = NULL;
psx_in_device_service = 0;
s_freeze_start = psx_cycle_count;
s_freeze_limit = watchdog_cycles;
s_freeze_overrun = overrun;
g_psx_render_pass_active = 1;
psx_cycle_freeze_tick();
return 1;
}
void psx_cycle_freeze_end(const PsxCycleFreeze *save) {
extern uint64_t g_psx_cycle_fast_limit;
g_psx_render_pass_active = 0;
s_freeze_overrun = 0;
s_freeze_limit = 0;
if (!save) return;
psx_cycle_count = save->cycle_count;
psx_next_service_cycle = save->next_service;
g_psx_cycle_fast_limit = save->fast_limit;
g_psx_cyc_batch = save->batch;
g_psx_cyc_batch_limit = save->batch_limit;
g_psx_cyc_local_acc = save->local_acc;
if (save->local_acc) *save->local_acc = save->local_acc_value;
psx_in_device_service = save->in_device_service;
g_psx_cyc_bb_defer = save->bb_defer;
}
+602
View File
@@ -0,0 +1,602 @@
/* render_pass.c - host-timed render passes (psx_mod_render_pass_plan /
* psx_mod_render_pass, mod_plugins.h). See docs/RENDER_PASSES.md.
*
* A pass is a sandboxed nested guest call. The guest-visible machine is
* checkpointed, time is frozen (psx_cycle_freeze.h), the plugin draws an image of
* the scene through the game's own code, the OpenGL backend captures the
* declared display rect, and then everything is put back: CPU (with the
* GTE), RAM, scratchpad, I-cache tags, I_STAT/I_MASK, timers, DMA and GPU
* registers, and the VRAM rect. Stores that could escape that restore (SPU,
* CD, timers, other DMA channels, ...) never reach their device in the first
* place (memory.c render_pass_store).
*
* Nothing here runs unless a trusted plugin calls the API, and the plan
* refuses in netplay, rollback, rewind, fast-forward, self-check
* resimulation, or without the OpenGL presenter's flip-aware interpolation
* (psx_mod_render_pass_status says which). */
#include "render_pass.h"
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cpu_state.h"
#include "dirty_ram_interp.h"
#include "dma.h"
#include "gpu.h"
#include "gpu_gl_renderer.h"
#include "mod_plugins.h"
#include "overlay_loader.h"
#include "psx_icache.h"
#include "timers.h"
extern uint8_t *memory_get_ram_ptr(void);
extern uint8_t *memory_get_scratchpad_ptr(void);
/* Live main-RAM geometry (psx_ram_geometry.c): 2 MiB retail, or 8 MiB with
* the opt-in 8 MB RAM mod. Latched by memory_init() once per boot, so it
* cannot change inside a pass, but it can between sessions. */
extern uint32_t memory_get_ram_bytes(void);
extern uint32_t i_stat;
extern uint32_t i_mask;
extern uint32_t dma_snapshot_bytes(void);
extern void dma_snapshot_write(uint8_t *p);
extern int dma_snapshot_read(const uint8_t *p, uint32_t len);
extern uint32_t interrupts_get_cycles_since_vblank(void);
extern void interrupts_set_cycles_since_vblank(uint32_t v);
extern void psx_irq_refresh_cause_ip2(void);
extern int psx_get_in_exception(void);
extern int psx_netplay_active(void);
extern int psx_netplay_is_resimulating(void);
extern int psx_selfcheck_resim_active(void);
extern int psx_rewind_is_open(void);
extern int psx_presentation_fast_forward(void);
extern uint64_t g_guest_store_count;
extern uint64_t g_mmio_access_count;
extern uint32_t g_debug_last_store_pc;
extern uint32_t g_debug_current_func_addr;
extern int g_psx_dispatch_depth;
extern int g_psx_call_bail;
extern int g_ls_mode;
extern int g_ls_replay_active;
extern void (*g_overlay_flush_pending_cycles)(void);
/* Host nesting that runtime frames set on entry and put back on exit. */
extern int g_call_unit_depth; /* overlay_loader_call_native */
extern int g_dma_exec_depth; /* dma.c kick / async writes */
extern int g_dma_cur_ch;
extern uint32_t g_dma_cur_madr;
extern uint32_t g_dma_cur_bcr;
extern uint32_t g_dma_initiator_pc;
extern int g_dirty_interp_active; /* dirty-RAM interpreter */
extern int g_exec_phase;
extern int g_precise_mode;
extern uint32_t g_dirty_safe_resume_pc;
extern uint32_t cdrom_snapshot_bytes(void);
extern void cdrom_snapshot_write(uint8_t *p);
extern uint32_t spu_snapshot_bytes(void);
extern void spu_snapshot_write(uint8_t *p);
extern uint32_t sio_snapshot_bytes(void);
extern void sio_snapshot_write(uint8_t *p);
extern uint32_t mdec_snapshot_bytes(void);
extern void mdec_snapshot_write(uint8_t *p);
#define RP_SPAD_SIZE 1024u
/* ~8 M guest cycles is about four PS1 frames of CPU time: far beyond any
* frame's draw code, short enough that a pass stuck on a wait loop costs the
* host well under a frame before it is rolled back. */
#define RP_WATCHDOG_CYCLES 8000000u
/* Faults (watchdog, VRAM leaks) before passes stay off for the session. */
#define RP_FAULT_LIMIT 8u
/* Host-side nesting state. Generated functions, native overlay calls, DMA
* kicks, the dirty-RAM interpreter and GP0 processing each set some of this
* on entry and put it back on exit. The watchdog abort longjmps past those
* exits, so the landing puts the interrupted code's values back from the
* checkpoint, as the scheduler and exception landings reset theirs
* (traps.c psx_scheduler_run, interrupts.c psx_check_interrupts). The cycle
* deferral depth (g_psx_cyc_bb_defer) is part of PsxCycleFreeze. After a
* pass that returns normally all of it is already balanced. */
typedef struct RenderPassNesting {
int call_unit_depth;
int dma_exec_depth, dma_cur_ch;
uint32_t dma_cur_madr, dma_cur_bcr, dma_initiator_pc;
int dirty_interp_active, exec_phase, precise_mode;
uint32_t dirty_safe_resume_pc;
DirtyRamLoadDelay ld_delay;
int ov_active_depth;
uint32_t ov_inprogress;
void (*ov_flush)(void);
} RenderPassNesting;
static void nesting_save(RenderPassNesting *n) {
n->call_unit_depth = g_call_unit_depth;
n->dma_exec_depth = g_dma_exec_depth;
n->dma_cur_ch = g_dma_cur_ch;
n->dma_cur_madr = g_dma_cur_madr;
n->dma_cur_bcr = g_dma_cur_bcr;
n->dma_initiator_pc = g_dma_initiator_pc;
n->dirty_interp_active = g_dirty_interp_active;
n->exec_phase = g_exec_phase;
n->precise_mode = g_precise_mode;
n->dirty_safe_resume_pc = g_dirty_safe_resume_pc;
dirty_ram_ld_delay_save(&n->ld_delay);
overlay_loader_native_nesting(&n->ov_active_depth, &n->ov_inprogress);
n->ov_flush = g_overlay_flush_pending_cycles;
}
static void nesting_restore(const RenderPassNesting *n) {
g_call_unit_depth = n->call_unit_depth;
g_dma_exec_depth = n->dma_exec_depth;
g_dma_cur_ch = n->dma_cur_ch;
g_dma_cur_madr = n->dma_cur_madr;
g_dma_cur_bcr = n->dma_cur_bcr;
g_dma_initiator_pc = n->dma_initiator_pc;
g_dirty_interp_active = n->dirty_interp_active;
g_exec_phase = n->exec_phase;
g_precise_mode = n->precise_mode;
g_dirty_safe_resume_pc = n->dirty_safe_resume_pc;
dirty_ram_ld_delay_restore(&n->ld_delay);
overlay_loader_set_native_nesting(n->ov_active_depth, n->ov_inprogress);
g_overlay_flush_pending_cycles = n->ov_flush;
}
/* PSX_RENDER_PASS_VERIFY: a pass that returned normally must leave the
* nesting balanced; the restore would otherwise hide the imbalance. (The
* deferred load is not nesting: the interpreter retires it on exit.) */
static int nesting_balanced(const RenderPassNesting *n) {
RenderPassNesting now;
memset(&now, 0, sizeof now);
nesting_save(&now);
return now.call_unit_depth == n->call_unit_depth &&
now.dma_exec_depth == n->dma_exec_depth &&
now.dirty_interp_active == n->dirty_interp_active &&
now.exec_phase == n->exec_phase &&
now.precise_mode == n->precise_mode &&
now.ov_active_depth == n->ov_active_depth &&
now.ov_flush == n->ov_flush;
}
/* After a watchdog abort, before the restore: which exits the longjmp
* skipped (for the fault log). Returns how many values it found changed. */
static int nesting_describe(const RenderPassNesting *n, int bb_defer_ck,
char *out, size_t cap) {
RenderPassNesting now;
int count = 0;
size_t len = 0;
memset(&now, 0, sizeof now);
nesting_save(&now);
out[0] = '\0';
#define RP_NOTE(cond, ...) do { if (cond) { \
int w_ = snprintf(out + len, cap - len, "%s", count ? ", " : ""); \
if (w_ > 0 && (size_t)w_ < cap - len) len += (size_t)w_; \
w_ = snprintf(out + len, cap - len, __VA_ARGS__); \
if (w_ > 0 && (size_t)w_ < cap - len) len += (size_t)w_; \
count++; } } while (0)
RP_NOTE(g_psx_cyc_bb_defer != bb_defer_ck, "cycle deferral %+d",
g_psx_cyc_bb_defer - bb_defer_ck);
RP_NOTE(now.call_unit_depth != n->call_unit_depth, "call unit %+d",
now.call_unit_depth - n->call_unit_depth);
RP_NOTE(now.ov_active_depth != n->ov_active_depth, "shard stack %+d",
now.ov_active_depth - n->ov_active_depth);
RP_NOTE(now.ov_flush != n->ov_flush, "shard cycle hook");
RP_NOTE(now.dma_exec_depth != n->dma_exec_depth, "DMA depth %+d",
now.dma_exec_depth - n->dma_exec_depth);
RP_NOTE(now.dirty_interp_active != n->dirty_interp_active, "interpreter flag");
RP_NOTE(now.precise_mode != n->precise_mode, "precise flag");
RP_NOTE(now.exec_phase != n->exec_phase, "exec phase %d->%d",
now.exec_phase, n->exec_phase);
RP_NOTE(now.ld_delay.armed != n->ld_delay.armed, "pending load");
#undef RP_NOTE
return count;
}
typedef struct RenderPassCheckpoint {
CPUState cpu;
uint8_t spad[RP_SPAD_SIZE];
uint32_t icache[1024];
uint32_t i_stat, i_mask, csv;
uint16_t t_counter[3], t_target[3];
uint32_t t_mode[3], t_frac[3];
int32_t t_irq[3];
uint64_t store_count, mmio_count;
uint32_t last_store_pc, current_func;
int dispatch_depth, call_bail;
uint32_t dma_len;
uint32_t ram_bytes; /* live RAM geometry at the checkpoint */
RenderPassNesting nest;
} RenderPassCheckpoint;
static uint8_t *s_ram_copy; /* s_ram_copy_cap bytes */
static uint32_t s_ram_copy_cap;
static uint8_t *s_dma_copy;
static RenderPassCheckpoint s_ck;
static PsxCycleFreeze s_freeze;
static jmp_buf s_abort_jmp;
static volatile int s_abort_armed;
static int s_nesting;
static RenderPassStats s_stats;
static int s_verify = -1;
static int s_open_generation; /* next pass captures frame N's image */
static uint32_t s_plan_period = 2;
/* The last pass's restore left the machine as it found it; while no guest
* code runs (clock and store count unchanged) and the plan is the same, the
* next pass may reuse that pass's VRAM backup. */
static uint64_t s_plan_serial, s_restored_plan;
static uint64_t s_restored_cycle, s_restored_stores;
static int s_restored_valid;
/* PSX_RENDER_PASS_WATCHDOG=<guest cycles> lowers the cut-off (debug), so a
* title's rollback path can be exercised on real passes. */
static uint64_t watchdog_cycles(void) {
static uint64_t v;
if (!v) {
const char *e = getenv("PSX_RENDER_PASS_WATCHDOG");
long long n = e ? atoll(e) : 0;
v = (n >= 1000 && n < (long long)RP_WATCHDOG_CYCLES)
? (uint64_t)n : (uint64_t)RP_WATCHDOG_CYCLES;
if (v != RP_WATCHDOG_CYCLES)
fprintf(stderr, "psxrecomp: render pass watchdog lowered to %llu "
"guest cycles (PSX_RENDER_PASS_WATCHDOG)\n",
(unsigned long long)v);
}
return v;
}
static int verify_on(void) {
if (s_verify < 0) {
const char *e = getenv("PSX_RENDER_PASS_VERIFY");
s_verify = (e && e[0] && e[0] != '0') ? 1 : 0;
}
return s_verify;
}
void render_pass_get_stats(RenderPassStats *out) {
if (!out) return;
*out = s_stats;
for (int i = 0; i < RENDER_PASS_DROP_CLASSES; i++)
out->dropped[i] = g_render_pass_dropped_writes[i];
}
void render_pass_reset_session(void) {
memset(&s_stats, 0, sizeof s_stats);
memset(g_render_pass_dropped_writes, 0, sizeof g_render_pass_dropped_writes);
s_open_generation = 0;
s_restored_valid = 0;
}
/* Everything that must hold before guest code may run frozen, as a
* PSX_MOD_RENDER_PASS_* reason (READY = all of it holds). */
static uint32_t pass_status(void) {
uint32_t gl;
if (s_stats.disabled) return PSX_MOD_RENDER_PASS_DISABLED;
if (psx_netplay_active() || psx_netplay_is_resimulating() ||
psx_selfcheck_resim_active() || psx_rewind_is_open() ||
g_ls_mode || g_ls_replay_active)
return PSX_MOD_RENDER_PASS_SESSION;
if (psx_presentation_fast_forward()) return PSX_MOD_RENDER_PASS_FAST_FORWARD;
gl = gl_renderer_pass_unavailable();
if (gl != PSX_MOD_RENDER_PASS_READY) return gl;
if (s_nesting || g_psx_render_pass_active || psx_get_in_exception() ||
dma_gpu_linked_list_active())
return PSX_MOD_RENDER_PASS_BUSY;
return PSX_MOD_RENDER_PASS_READY;
}
static int passes_allowed(void) {
return pass_status() == PSX_MOD_RENDER_PASS_READY;
}
uint32_t psx_mod_render_pass_status(void) {
return pass_status();
}
uint32_t psx_mod_render_pass_plan(uint32_t period_vblanks,
uint32_t shown_after_vblanks,
uint32_t *alpha_q16, uint32_t max) {
uint32_t wanted = 0, n;
s_open_generation = 0;
if (!alpha_q16 || max == 0 || period_vblanks == 0 || period_vblanks > 8 ||
shown_after_vblanks > 8)
return 0;
if (!passes_allowed()) return 0;
n = gl_renderer_pass_plan(period_vblanks, shown_after_vblanks,
alpha_q16, max, &wanted);
s_stats.wanted += wanted;
if (!n) {
if (wanted) s_stats.refused++;
return 0;
}
s_stats.plans++;
s_stats.planned += n;
s_plan_serial++;
s_open_generation = 1;
s_plan_period = period_vblanks;
return n;
}
/* FNV-1a step over 64-bit words, then the tail bytes. Each step is a
* bijection of h, so a change confined to one word always changes the hash.
* Word steps take an eighth of the time of byte steps, which keeps
* PSX_RENDER_PASS_VERIFY affordable with 8 MiB of RAM live. */
static uint64_t fnv(uint64_t h, const void *p, size_t n) {
const uint8_t *b = (const uint8_t *)p;
size_t i = 0;
for (; i + 8u <= n; i += 8u) {
uint64_t w;
memcpy(&w, b + i, sizeof w);
h = (h ^ w) * 1099511628211ULL;
}
for (; i < n; i++) h = (h ^ b[i]) * 1099511628211ULL;
return h;
}
/* Guest-visible state hash for PSX_RENDER_PASS_VERIFY. */
static uint64_t state_hash(const CPUState *cpu) {
uint64_t h = 1469598103934665603ULL;
uint16_t tc[3], tt[3];
uint32_t tm[3], tf[3];
int32_t ti[3];
uint32_t csv = interrupts_get_cycles_since_vblank();
uint64_t cyc = psx_cycle_count;
h = fnv(h, cpu->gpr, sizeof cpu->gpr);
h = fnv(h, &cpu->hi, sizeof cpu->hi);
h = fnv(h, &cpu->lo, sizeof cpu->lo);
h = fnv(h, cpu->cop0, sizeof cpu->cop0);
h = fnv(h, cpu->gte_data, sizeof cpu->gte_data);
h = fnv(h, cpu->gte_ctrl, sizeof cpu->gte_ctrl);
h = fnv(h, memory_get_ram_ptr(), memory_get_ram_bytes());
h = fnv(h, memory_get_scratchpad_ptr(), RP_SPAD_SIZE);
h = fnv(h, g_psx_icache_tv, sizeof g_psx_icache_tv);
h = fnv(h, &i_stat, sizeof i_stat);
h = fnv(h, &i_mask, sizeof i_mask);
h = fnv(h, &csv, sizeof csv);
h = fnv(h, &cyc, sizeof cyc);
timers_get_snapshot(tc, tm, tt, ti, tf);
h = fnv(h, tc, sizeof tc); h = fnv(h, tm, sizeof tm);
h = fnv(h, tt, sizeof tt); h = fnv(h, ti, sizeof ti); h = fnv(h, tf, sizeof tf);
if (s_dma_copy) {
uint32_t n = dma_snapshot_bytes();
uint8_t *tmp = (uint8_t *)malloc(n);
if (tmp) { dma_snapshot_write(tmp); h = fnv(h, tmp, n); free(tmp); }
}
{
uint64_t g = gpu_pass_state_hash();
h = fnv(h, &g, sizeof g);
}
/* Devices a pass must never touch (their stores are dropped, their
* clocks frozen): prove it. */
{
uint32_t (*bytes[4])(void) = { cdrom_snapshot_bytes, spu_snapshot_bytes,
sio_snapshot_bytes, mdec_snapshot_bytes };
void (*write[4])(uint8_t *) = { cdrom_snapshot_write, spu_snapshot_write,
sio_snapshot_write, mdec_snapshot_write };
for (int i = 0; i < 4; i++) {
uint32_t n = bytes[i]();
uint8_t *tmp = n ? (uint8_t *)malloc(n) : NULL;
if (!tmp) continue;
write[i](tmp);
h = fnv(h, tmp, n);
free(tmp);
}
}
return h;
}
static int checkpoint_save(const CPUState *cpu) {
uint32_t dma_len = dma_snapshot_bytes();
/* The whole live RAM: a pass's stores fold through the same geometry
* (render_pass_store_to), so every byte one can reach is in here. */
uint32_t ram_bytes = memory_get_ram_bytes();
if (s_ram_copy_cap < ram_bytes) {
uint8_t *p = (uint8_t *)realloc(s_ram_copy, ram_bytes);
if (!p) return 0;
s_ram_copy = p;
s_ram_copy_cap = ram_bytes;
}
if (!s_dma_copy) {
s_dma_copy = (uint8_t *)malloc(dma_len);
if (!s_dma_copy) return 0;
}
if (!gpu_pass_checkpoint_save()) return 0;
s_ck.cpu = *cpu;
s_ck.ram_bytes = ram_bytes;
memcpy(s_ram_copy, memory_get_ram_ptr(), ram_bytes);
memcpy(s_ck.spad, memory_get_scratchpad_ptr(), RP_SPAD_SIZE);
memcpy(s_ck.icache, g_psx_icache_tv, sizeof s_ck.icache);
s_ck.i_stat = i_stat;
s_ck.i_mask = i_mask;
s_ck.csv = interrupts_get_cycles_since_vblank();
timers_get_snapshot(s_ck.t_counter, s_ck.t_mode, s_ck.t_target,
s_ck.t_irq, s_ck.t_frac);
s_ck.dma_len = dma_len;
dma_snapshot_write(s_dma_copy);
s_ck.store_count = g_guest_store_count;
s_ck.mmio_count = g_mmio_access_count;
s_ck.last_store_pc = g_debug_last_store_pc;
s_ck.current_func = g_debug_current_func_addr;
s_ck.dispatch_depth = g_psx_dispatch_depth;
s_ck.call_bail = g_psx_call_bail;
memset(&s_ck.nest, 0, sizeof s_ck.nest);
nesting_save(&s_ck.nest);
return 1;
}
static void checkpoint_restore(CPUState *cpu) {
gpu_pass_checkpoint_restore();
(void)dma_snapshot_read(s_dma_copy, s_ck.dma_len);
timers_set_snapshot(s_ck.t_counter, s_ck.t_mode, s_ck.t_target,
s_ck.t_irq, s_ck.t_frac);
interrupts_set_cycles_since_vblank(s_ck.csv);
i_stat = s_ck.i_stat;
i_mask = s_ck.i_mask;
memcpy(memory_get_ram_ptr(), s_ram_copy, s_ck.ram_bytes);
memcpy(memory_get_scratchpad_ptr(), s_ck.spad, RP_SPAD_SIZE);
memcpy(g_psx_icache_tv, s_ck.icache, sizeof s_ck.icache);
*cpu = s_ck.cpu;
psx_irq_refresh_cause_ip2();
g_guest_store_count = s_ck.store_count;
g_mmio_access_count = s_ck.mmio_count;
g_debug_last_store_pc = s_ck.last_store_pc;
g_debug_current_func_addr = s_ck.current_func;
g_psx_dispatch_depth = s_ck.dispatch_depth;
g_psx_call_bail = s_ck.call_bail;
nesting_restore(&s_ck.nest);
}
static double s_ms_per_tick = 0.0;
static double ema_ms(double cur, uint64_t ticks) {
double ms = (double)ticks * s_ms_per_tick;
return cur > 0.0 ? cur * 0.9 + ms * 0.1 : ms;
}
static void watchdog_overrun(void) {
s_stats.watchdog++;
s_stats.watchdog_flag = 1;
if (s_abort_armed) {
s_abort_armed = 0;
longjmp(s_abort_jmp, 1);
}
}
static char s_abort_detail[192];
static void note_fault(const char *what) {
s_stats.aborted++;
if (s_stats.aborted <= 4)
fprintf(stderr, "psxrecomp: render pass rolled back (%s%s%s)\n", what,
s_abort_detail[0] ? "; restored skipped exits: " : "",
s_abort_detail);
s_abort_detail[0] = '\0';
if (s_stats.watchdog + s_stats.vram_leaks >= RP_FAULT_LIMIT &&
!s_stats.disabled) {
s_stats.disabled = 1;
fprintf(stderr, "psxrecomp: render passes disabled for this session "
"after %u faults\n", (unsigned)RP_FAULT_LIMIT);
}
}
int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
PSXModRenderPassFn fn, void *user) {
uint64_t t0, t1, tb, tg, te, tr, hash_before = 0, hash_after = 0;
uint64_t cycles_before;
uint32_t leaks;
int ok = 0, open, reuse;
static uint32_t s_leaks_before;
if (!cpu || !pass || !fn || pass->struct_size < sizeof *pass ||
pass->w == 0 || pass->h == 0 || pass->alpha_q16 == 0 ||
pass->alpha_q16 >= 65536u)
return 0;
if (!passes_allowed()) return 0;
/* An overlay DLL may still hold cycles it has not published. They belong
* to the live timeline: publish them now, or the pass's first store would
* publish them into the frozen clock and the restore would drop them. */
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
t0 = gl_renderer_perf_ticks();
open = s_open_generation;
{
static double inv = 0.0;
if (inv == 0.0) inv = 1000.0 / (double)gl_renderer_perf_frequency();
s_ms_per_tick = inv;
}
reuse = !open && s_restored_valid && s_restored_plan == s_plan_serial &&
s_restored_cycle == psx_cycle_count &&
s_restored_stores == g_guest_store_count;
/* Frame N's own image is captured by the first pass after a plan. */
if (!gl_renderer_pass_begin(pass->x, pass->y, pass->w, pass->h, open,
s_plan_period, reuse))
return 0;
s_open_generation = 0;
s_leaks_before = gl_renderer_pass_leaks();
if (!checkpoint_save(cpu)) {
gl_renderer_pass_end(0, 0);
return 0;
}
if (verify_on()) hash_before = state_hash(cpu);
tb = gl_renderer_perf_ticks();
s_nesting = 1;
cycles_before = psx_cycle_count;
(void)psx_cycle_freeze_begin(&s_freeze, watchdog_cycles(),
watchdog_overrun);
if (setjmp(s_abort_jmp) == 0) {
s_abort_armed = 1;
ok = fn(cpu, user, pass->alpha_q16) ? 1 : 0;
s_abort_armed = 0;
if (!ok) s_stats.discarded++;
if (verify_on() && !nesting_balanced(&s_ck.nest)) {
s_stats.verify_mismatch++;
fprintf(stderr, "psxrecomp: RENDER PASS VERIFY mismatch "
"(host nesting not balanced after the pass)\n");
}
} else {
/* Watchdog abort from inside guest code. An overlay shard may still
* hold cycles it has not published: publish them now, into the
* frozen clock the restore discards, never into the live one. The
* nesting the longjmp skipped is put back by checkpoint_restore and
* psx_cycle_freeze_end below. */
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
if (nesting_describe(&s_ck.nest, s_freeze.bb_defer, s_abort_detail,
sizeof s_abort_detail))
s_stats.nesting_repairs++;
ok = 0;
}
s_stats.guest_cycles_last = psx_cycle_count - cycles_before;
tg = gl_renderer_perf_ticks();
leaks = gl_renderer_pass_leaks() - s_leaks_before;
if (leaks) {
s_stats.vram_leaks += leaks;
ok = 0;
}
/* Capture (when kept), then roll the VRAM rect and renderer back. */
gl_renderer_pass_end(ok ? pass->alpha_q16 : 0, ok);
te = gl_renderer_perf_ticks();
checkpoint_restore(cpu);
psx_cycle_freeze_end(&s_freeze);
s_nesting = 0;
s_restored_valid = 1;
s_restored_plan = s_plan_serial;
s_restored_cycle = psx_cycle_count;
s_restored_stores = g_guest_store_count;
tr = gl_renderer_perf_ticks();
s_stats.avg_begin_ms = ema_ms(s_stats.avg_begin_ms, tb - t0);
s_stats.avg_guest_ms = ema_ms(s_stats.avg_guest_ms, tg - tb);
s_stats.avg_end_ms = ema_ms(s_stats.avg_end_ms, te - tg);
s_stats.avg_restore_ms = ema_ms(s_stats.avg_restore_ms, tr - te);
if (verify_on()) {
hash_after = state_hash(cpu);
s_stats.verify_checks++;
if (hash_after != hash_before || !gl_renderer_pass_verify_vram()) {
s_stats.verify_mismatch++;
if (s_stats.verify_mismatch <= 8)
fprintf(stderr, "psxrecomp: RENDER PASS VERIFY mismatch "
"(state %016llx -> %016llx)\n",
(unsigned long long)hash_before,
(unsigned long long)hash_after);
}
}
t1 = gl_renderer_perf_ticks();
{
double ms = (double)(t1 - t0) * 1000.0 /
(double)gl_renderer_perf_frequency();
s_stats.last_pass_ms = ms;
s_stats.avg_pass_ms = s_stats.avg_pass_ms > 0.0
? s_stats.avg_pass_ms * 0.9 + ms * 0.1 : ms;
}
gl_renderer_pass_note_cost(t1 - t0);
if (ok) s_stats.passes++;
else if (leaks) note_fault("VRAM write outside the pass rect");
else if (s_stats.watchdog_flag) note_fault("guest-cycle watchdog");
s_stats.watchdog_flag = 0;
/* Present anything that fell due while the pass ran. */
gl_renderer_pass_service_presents();
return ok;
}
+306
View File
@@ -0,0 +1,306 @@
#include "render_pass_plan.h"
#include "render_pass.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#define Q16 65536.0
static uint32_t to_q16(double p) {
double q = floor(p * Q16 + 0.5);
if (q < 1.0) q = 1.0;
if (q > Q16 - 1.0) q = Q16 - 1.0;
return (uint32_t)q;
}
uint32_t render_pass_plan_phases(const RenderPassPlanInput *in,
uint32_t *alpha_q16, uint32_t *wanted) {
double phases[RENDER_PASS_MAX_PHASES * 4u];
uint32_t count = 0, cap, n;
if (wanted) *wanted = 0;
if (!in || !alpha_q16 || in->max == 0 || !(in->frame_length > 0.0) ||
!isfinite(in->frame_length))
return 0;
cap = sizeof phases / sizeof phases[0];
/* No live output schedule: nothing would show a pass. */
if (!(in->target_period > 0.0) || !isfinite(in->target_period) ||
!isfinite(in->next_deadline) || !isfinite(in->frame_start))
return 0;
{
/* Walk the presenter's free-running output grid through the frame. */
double end = in->frame_start + in->frame_length;
double d = in->next_deadline;
if (d <= in->frame_start) {
double steps = floor((in->frame_start - d) / in->target_period) + 1.0;
d += steps * in->target_period;
}
for (; d < end && count < cap; d += in->target_period) {
double p = (d - in->frame_start) / in->frame_length;
if (p < 1.0 / 64.0) continue;
if (p >= 1.0) break;
phases[count++] = p;
}
}
if (count == 0) return 0;
if (wanted) *wanted = count;
n = count;
if (n > in->max) n = in->max;
if (n > RENDER_PASS_MAX_PHASES) n = RENDER_PASS_MAX_PHASES;
if (in->budget >= 0.0) {
/* Cost unknown (no pass measured at this image size yet): one pass
* measures it without stalling the guest for a whole plan. */
double fit = in->pass_cost > 0.0 ? floor(in->budget / in->pass_cost)
: (in->budget > 0.0 ? 1.0 : 0.0);
if (fit < 0.0) fit = 0.0;
if (fit < (double)n) n = (uint32_t)fit;
}
if (n == 0) return 0;
if (n == count) {
for (uint32_t i = 0; i < n; i++) alpha_q16[i] = to_q16(phases[i]);
} else {
/* Evenly spread subset; the presenter blends across the gaps. */
uint32_t last = UINT32_MAX, out = 0;
for (uint32_t i = 0; i < n; i++) {
uint32_t idx = (uint32_t)floor(((double)i + 0.5) *
(double)count / (double)n);
if (idx >= count) idx = count - 1;
if (idx == last) continue;
alpha_q16[out++] = to_q16(phases[idx]);
last = idx;
}
n = out;
}
return n;
}
int render_pass_select(const uint32_t *phases, uint32_t n, double p,
uint32_t *lo, uint32_t *hi, float *t) {
uint32_t i;
double q, a, b, w;
if (!phases || n == 0) return 0;
q = p * Q16;
if (!(q > (double)phases[0])) {
*lo = *hi = 0;
*t = 0.0f;
return 1;
}
for (i = 0; i + 1 < n && (double)phases[i + 1] <= q; i++) { }
if (i + 1 >= n) {
*lo = *hi = n - 1;
*t = 0.0f;
return 1;
}
a = (double)phases[i];
b = (double)phases[i + 1];
w = b > a ? (q - a) / (b - a) : 0.0;
if (w <= 1.0 / 256.0) {
*lo = *hi = i;
*t = 0.0f;
} else if (w >= 1.0 - 1.0 / 256.0) {
*lo = *hi = i + 1;
*t = 0.0f;
} else {
*lo = i;
*hi = i + 1;
*t = (float)w;
}
return 1;
}
int render_pass_gen_select(const uint32_t *phases, uint32_t n, double p,
uint32_t *lo, uint32_t *hi, float *t) {
if (!(p <= RENDER_PASS_GEN_HOLD_MAX)) return 0;
return render_pass_select(phases, n, p, lo, hi, t);
}
double render_pass_ema(double current, double sample) {
if (!(sample >= 0.0) || !isfinite(sample)) return current;
if (!(current > 0.0)) return sample;
return current * 0.75 + sample * 0.25;
}
void render_pass_cost_add(RenderPassCost *cost, double sample, int allocated) {
if (!cost || !(sample >= 0.0) || !isfinite(sample)) return;
if (allocated && cost->skips < RENDER_PASS_ALLOC_SKIPS) {
cost->skips++;
return;
}
cost->skips = 0;
cost->unsampled = 0;
if (cost->kept < RENDER_PASS_COST_WARMUP) {
cost->warm[cost->kept++] = sample;
if (cost->kept == RENDER_PASS_COST_WARMUP) {
/* Median of the warm-up samples (insertion sort of three). */
double w[RENDER_PASS_COST_WARMUP];
memcpy(w, cost->warm, sizeof w);
for (unsigned i = 1; i < RENDER_PASS_COST_WARMUP; i++)
for (unsigned j = i; j > 0 && w[j - 1] > w[j]; j--) {
double t = w[j]; w[j] = w[j - 1]; w[j - 1] = t;
}
cost->ema = w[RENDER_PASS_COST_WARMUP / 2u];
if (cost->rewarm_from > 0.0) {
/* A re-measure: wait longer before the next one unless it
* found the old estimate stale. */
unsigned limit = cost->rewarm_after ? cost->rewarm_after
: RENDER_PASS_REWARM_MIN;
if (cost->ema <= cost->rewarm_from * RENDER_PASS_REWARM_STALE)
cost->rewarm_after = 0;
else
cost->rewarm_after = limit >= RENDER_PASS_REWARM_MAX / 2u
? RENDER_PASS_REWARM_MAX : limit * 2u;
cost->rewarm_from = 0.0;
}
}
return;
}
cost->ema = render_pass_ema(cost->ema, sample);
}
double render_pass_cost_estimate(const RenderPassCost *cost) {
if (!cost || cost->kept < RENDER_PASS_COST_WARMUP) return 0.0;
return cost->ema;
}
int render_pass_cost_note_plan(RenderPassCost *cost) {
unsigned limit;
/* Warming up: its one-pass plans are what measures the cost. */
if (!cost || cost->kept < RENDER_PASS_COST_WARMUP) return 0;
limit = cost->rewarm_after ? cost->rewarm_after : RENDER_PASS_REWARM_MIN;
if (++cost->unsampled < limit) return 0;
/* No pass has been measured against the estimate for `limit` plans:
* measure again (render_pass_cost_add sets the next wait). */
cost->rewarm_from = cost->ema;
cost->kept = 0;
cost->skips = 0;
cost->unsampled = 0;
return 1;
}
double render_pass_budget(double idle_ticks, double pass_ticks,
double frame_length, double share) {
double b;
if (!(frame_length > 0.0)) return 0.0;
if (!(share > 0.0)) return 0.0;
if (share > 1.0) share = 1.0;
if (!(idle_ticks > 0.0) && !(pass_ticks > 0.0))
return frame_length * share;
b = ((idle_ticks > 0.0 ? idle_ticks : 0.0) +
(pass_ticks > 0.0 ? pass_ticks : 0.0)) * share;
if (b > frame_length) b = frame_length;
return b;
}
int render_pass_mmio_class(uint32_t phys, uint32_t val, uint32_t width) {
if (phys == 0x1F801810u) return width == 4 ? -1 : RENDER_PASS_DROP_GPU;
if (phys == 0x1F801814u) {
uint32_t cmd = val >> 24;
return (width == 4 && (cmd == 0x04u || cmd == 0x10u))
? -1 : RENDER_PASS_DROP_GPU;
}
if (phys >= 0x1F801070u && phys <= 0x1F801077u) return -1;
if ((phys >= 0x1F8010A0u && phys <= 0x1F8010AFu) || /* ch2 GPU */
(phys >= 0x1F8010E0u && phys <= 0x1F8010EFu) || /* ch6 OTC */
(phys >= 0x1F8010F0u && phys <= 0x1F8010F7u)) /* DPCR/DICR */
return -1;
if (phys >= 0x1F801080u && phys <= 0x1F8010FFu) return RENDER_PASS_DROP_DMA;
if (phys >= 0x1F801C00u && phys <= 0x1F801FFFu) return RENDER_PASS_DROP_SPU;
if (phys >= 0x1F801800u && phys <= 0x1F801803u) return RENDER_PASS_DROP_CD;
if (phys >= 0x1F801100u && phys <= 0x1F80112Fu) return RENDER_PASS_DROP_TIMER;
return RENDER_PASS_DROP_OTHER;
}
int render_pass_store_to(const RenderPassStoreTarget *t, uint32_t addr,
uint32_t val, uint32_t width) {
uint32_t phys;
if (addr >= 0xC0000000u) return RENDER_PASS_DROP_OTHER; /* KSEG2 */
if (t->isolate_cache) return -1; /* cache-only store */
phys = addr & 0x1FFFFFFFu;
/* The 8 MiB DRAM decode window folds through the live geometry, exactly
* as psx_ram_map_write: retail mirrors 2 MiB four times, the 8 MB RAM
* map is unique. */
if (phys < 0x00800000u) phys &= t->ram_size - 1u;
if (phys < t->ram_size) {
for (uint32_t i = 0; i < width; i++)
t->ram[phys + i] = (uint8_t)(val >> (8u * i));
return -1;
}
if (phys >= 0x1F800000u && phys < 0x1F800000u + t->scratchpad_size) {
uint32_t off = phys - 0x1F800000u;
for (uint32_t i = 0; i < width && off + i < t->scratchpad_size; i++)
t->scratchpad[off + i] = (uint8_t)(val >> (8u * i));
return -1;
}
if (phys >= 0x1F801000u && phys <= 0x1F803FFFu) {
int cls = render_pass_mmio_class(phys, val, width);
if (cls >= 0) return cls;
t->mmio_write(phys, val, width);
return -1;
}
/* Mod memory, expansion, ROM, unmapped: never written by a pass (mod
* arenas are not part of the pass restore). */
return RENDER_PASS_DROP_OTHER;
}
int render_pass_vram_policy(const RenderPassJournal *j, int px, int py,
int pw, int ph, int vram_w, int vram_h,
int can_journal, int *x, int *y, int *w, int *h) {
if (*w <= 0 || *h <= 0) return RENDER_PASS_VRAM_ALLOW;
if (*x < 0) { *w += *x; *x = 0; }
if (*y < 0) { *h += *y; *y = 0; }
if (*x + *w > vram_w) *w = vram_w - *x;
if (*y + *h > vram_h) *h = vram_h - *y;
if (*w <= 0 || *h <= 0) return RENDER_PASS_VRAM_ALLOW;
if (*x >= px && *y >= py && *x + *w <= px + pw && *y + *h <= py + ph)
return RENDER_PASS_VRAM_ALLOW;
if (!can_journal) return RENDER_PASS_VRAM_REFUSE;
for (int i = 0; i < j->n; i++)
if (*x >= j->x[i] && *y >= j->y[i] && *x + *w <= j->x[i] + j->w[i] &&
*y + *h <= j->y[i] + j->h[i])
return RENDER_PASS_VRAM_ALLOW; /* already covered */
return j->n < RENDER_PASS_JOURNAL_MAX ? RENDER_PASS_VRAM_JOURNAL
: RENDER_PASS_VRAM_REFUSE;
}
int render_pass_journal_add(RenderPassJournal *j, const uint16_t *vram,
int vram_w, int x, int y, int w, int h) {
int i = j->n;
size_t need = (size_t)w * (size_t)h;
if (i >= RENDER_PASS_JOURNAL_MAX || w <= 0 || h <= 0) return -1;
if (j->cap[i] < need) {
uint16_t *p = (uint16_t *)realloc(j->rows[i], need * sizeof(uint16_t));
if (!p) return -1;
j->rows[i] = p;
j->cap[i] = need;
}
for (int row = 0; row < h; row++)
memcpy(j->rows[i] + (size_t)row * (size_t)w,
vram + (size_t)(y + row) * (size_t)vram_w + (size_t)x,
(size_t)w * sizeof(uint16_t));
j->x[i] = x; j->y[i] = y; j->w[i] = w; j->h[i] = h;
j->n = i + 1;
return i;
}
void render_pass_journal_rollback(RenderPassJournal *j, uint16_t *vram,
int vram_w) {
for (int i = j->n - 1; i >= 0; i--)
for (int row = 0; row < j->h[i]; row++)
memcpy(vram + (size_t)(j->y[i] + row) * (size_t)vram_w +
(size_t)j->x[i],
j->rows[i] + (size_t)row * (size_t)j->w[i],
(size_t)j->w[i] * sizeof(uint16_t));
j->n = 0;
}
void render_pass_journal_free(RenderPassJournal *j) {
for (int i = 0; i < RENDER_PASS_JOURNAL_MAX; i++) {
free(j->rows[i]);
j->rows[i] = NULL;
j->cap[i] = 0;
}
j->n = 0;
}
+28
View File
@@ -96,6 +96,34 @@ void gpu_ws_end_linked_list(void) {}
void gpu_ws_prepass_linked_list(uint32_t a) { (void)a; }
void gpu_ws_restore_linked_list_rank(uint32_t r) { (void)r; }
/* ---- render passes (render_pass.c; never active in this test) ------------- */
/* memory.c routes stores through the pass store policy and dma.c walks a GPU
* linked list synchronously while a pass runs; with the flag at 0 neither
* path is taken, so the doubles trap if they ever are. */
int g_psx_render_pass_active;
int render_pass_store_to(const void *t, uint32_t a, uint32_t v, uint32_t w) {
(void)t; (void)a; (void)v; (void)w; __builtin_trap();
}
void dma_gpu_ll_advance(void *s, uint32_t c, const void *ops, void *o) {
(void)s; (void)c; (void)ops; (void)o; __builtin_trap();
}
void gpu_set_gp0_linked_list_node(uint32_t a, uint32_t n) { (void)a; (void)n; }
void gpu_ws_validate_linked_list_header(uint32_t a, uint32_t h) { (void)a; (void)h; }
void gpu_ws_validate_linked_list_node(uint32_t a, uint32_t n) { (void)a; (void)n; }
/* MMIO readers: the render-pass store path in memory.c keeps them in this
* link (mingw-w64 GCC 15 + LTO fails without them). No test access reaches
* MMIO, so these trap too. */
void debug_server_trace_mmio_read(uint32_t a, uint32_t v, uint8_t w) {
(void)a; (void)v; (void)w; __builtin_trap();
}
uint32_t cdrom_read(uint32_t a) { (void)a; __builtin_trap(); }
uint32_t mdec_read(uint32_t a) { (void)a; __builtin_trap(); }
uint32_t gpu_read_gpustat(void) { __builtin_trap(); }
uint32_t spu_read(uint32_t a) { (void)a; __builtin_trap(); }
uint32_t timers_read(uint32_t a) { (void)a; __builtin_trap(); }
uint32_t sio_read(uint32_t a) { (void)a; __builtin_trap(); }
void sio_tick(int c) { (void)c; __builtin_trap(); }
/* ---- device snapshot sections (fixed-size doubles; readers count) --------- */
#define STUB_SECTION(name, size) \
uint32_t name##_snapshot_bytes(void) { return (size); } \
+6 -2
View File
@@ -66,7 +66,11 @@ def main():
return result
fixture = args.fixture or framework / "runtime/tests/test_gl_readback_region.c"
for name, source in [("probe", fixture), ("sw", framework / "runtime/src/gpu_sw_renderer.c")]:
# The renderer's render-pass paths call the journal and VRAM policy in
# render_pass_plan.c (self-contained: no other runtime symbols).
sources = [("probe", fixture), ("sw", framework / "runtime/src/gpu_sw_renderer.c"),
("rp", framework / "runtime/src/render_pass_plan.c")]
for name, source in sources:
if run([compiler / "gcc.exe", "-std=c11", "-O2", "-flto", "-DPSX_SDL3=1",
"-DPSX_NO_DEBUG_TOOLS=1", *includes, "-c", source,
"-o", dest / (name + ".o")]).returncode:
@@ -74,7 +78,7 @@ def main():
libraries = ["m", "kernel32", "user32", "gdi32", "winmm", "imm32", "ole32",
"oleaut32", "version", "uuid", "advapi32", "setupapi", "shell32",
"dinput8", "opengl32"]
if run([compiler / "g++.exe", "-O2", "-flto", dest / "probe.o", dest / "sw.o",
if run([compiler / "g++.exe", "-O2", "-flto", *[dest / (name + ".o") for name, _ in sources],
sdl_library, *["-l" + name for name in libraries],
"-o", dest / "probe.exe"]).returncode:
return 2
@@ -127,6 +127,7 @@ for reset in (
"g_bezel_path.clear();",
"g_frame_interpolation_blend = g_frame_interpolation_blend_default;",
"g_frame_interpolation_source = PSX_MOD_FRAME_SOURCE_VBLANK;",
"render_pass_reset_session();",
):
assert reset in after, f"mod-owned session reset is missing: {reset}"
literal = re.fullmatch(r"(g_\w+) = (false|true|\d+);", reset)
+383
View File
@@ -0,0 +1,383 @@
/* Render-pass watchdog abort (render_pass.c psx_mod_render_pass).
*
* A pass that runs away is cut off by a longjmp from the frozen clock's
* watchdog, deep inside nested guest frames. That longjmp skips every exit
* those frames would have run: generated functions' bb_defer cleanup,
* overlay_loader_call_native's unit depth, the DMA kick's exec depth, the
* interpreter's active/phase/precise flags and pending load, the native-shard
* stack and its cycle flush hook. The live game must resume exactly as if
* the pass never happened, so this test aborts a pass from such frames and
* checks each of those values, the clock and RAM afterwards. The GPU, VRAM,
* device snapshots and presenter are mocks; render_pass.c and psx_cycles.c
* are the real ones. Build/run: ctest -R render_pass_abort_test */
#include "render_pass.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cpu_state.h"
#include "dirty_ram_interp.h"
#include "mod_plugins.h"
#include "psx_cyc.h"
static int failures;
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)
/* ---- runtime globals the two units share with the rest of the runtime -- */
int g_ls_replay_active = 0;
int g_ls_mode = 0;
int g_precise_mode = 0;
int g_psx_call_bail = 0;
int g_psx_dispatch_depth = 0;
uint32_t i_stat = 0, i_mask = 0;
uint64_t g_guest_store_count = 0;
uint64_t g_mmio_access_count = 0;
uint32_t g_debug_last_store_pc = 0;
uint32_t g_debug_current_func_addr = 0;
uint32_t g_psx_icache_tv[1024];
uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES];
void (*g_overlay_flush_pending_cycles)(void) = NULL;
int g_call_unit_depth = 0;
int g_dma_exec_depth = 0;
int g_dma_cur_ch = -1;
uint32_t g_dma_cur_madr = 0, g_dma_cur_bcr = 0, g_dma_initiator_pc = 0;
int g_dirty_interp_active = 0;
int g_exec_phase = 0;
uint32_t g_dirty_safe_resume_pc = 0;
/* ---- mock devices and memory ------------------------------------------ */
static uint8_t s_ram[2u * 1024u * 1024u];
static uint8_t s_spad[1024];
static uint8_t s_dma_regs[64];
static uint32_t s_csv = 1234;
static uint16_t s_tc[3] = {1, 2, 3}, s_tt[3] = {4, 5, 6};
static uint32_t s_tm[3] = {7, 8, 9}, s_tf[3];
static int32_t s_ti[3];
uint8_t *memory_get_ram_ptr(void) { return s_ram; }
uint32_t memory_get_ram_bytes(void) { return (uint32_t)sizeof s_ram; }
uint8_t *memory_get_scratchpad_ptr(void) { return s_spad; }
uint32_t dma_snapshot_bytes(void) { return sizeof s_dma_regs; }
void dma_snapshot_write(uint8_t *p) { memcpy(p, s_dma_regs, sizeof s_dma_regs); }
int dma_snapshot_read(const uint8_t *p, uint32_t len) {
if (len != sizeof s_dma_regs) return 0;
memcpy(s_dma_regs, p, len);
return 1;
}
int dma_gpu_linked_list_active(void) { return 0; }
uint32_t interrupts_get_cycles_since_vblank(void) { return s_csv; }
void interrupts_set_cycles_since_vblank(uint32_t v) { s_csv = v; }
void psx_irq_refresh_cause_ip2(void) {}
void timers_get_snapshot(uint16_t c[3], uint32_t m[3], uint16_t t[3],
int32_t irq[3], uint32_t f[3]) {
memcpy(c, s_tc, sizeof s_tc); memcpy(m, s_tm, sizeof s_tm);
memcpy(t, s_tt, sizeof s_tt); memcpy(irq, s_ti, sizeof s_ti);
memcpy(f, s_tf, sizeof s_tf);
}
void timers_set_snapshot(const uint16_t c[3], const uint32_t m[3],
const uint16_t t[3], const int32_t irq[3],
const uint32_t f[3]) {
memcpy(s_tc, c, sizeof s_tc); memcpy(s_tm, m, sizeof s_tm);
memcpy(s_tt, t, sizeof s_tt); memcpy(s_ti, irq, sizeof s_ti);
memcpy(s_tf, f, sizeof s_tf);
}
uint32_t cdrom_snapshot_bytes(void) { return 0; }
void cdrom_snapshot_write(uint8_t *p) { (void)p; }
uint32_t spu_snapshot_bytes(void) { return 0; }
void spu_snapshot_write(uint8_t *p) { (void)p; }
uint32_t sio_snapshot_bytes(void) { return 0; }
void sio_snapshot_write(uint8_t *p) { (void)p; }
uint32_t mdec_snapshot_bytes(void) { return 0; }
void mdec_snapshot_write(uint8_t *p) { (void)p; }
int psx_get_in_exception(void) { return 0; }
int psx_netplay_active(void) { return 0; }
int psx_netplay_is_resimulating(void) { return 0; }
int psx_selfcheck_resim_active(void) { return 0; }
int psx_selfcheck_enabled(void) { return 0; }
int psx_rewind_is_open(void) { return 0; }
/* psx_cycles.c device side: nothing may advance inside a pass. */
static uint64_t s_dev_cycles;
void sio_advance(uint32_t c) { s_dev_cycles += c; }
void cdrom_advance(uint32_t c) { s_dev_cycles += c; }
void dma_advance(uint32_t c) { s_dev_cycles += c; }
void timers_advance(uint32_t c) { s_dev_cycles += c; }
void interrupts_advance_cycles(uint32_t c) { s_dev_cycles += c; }
void interrupts_service_scheduled_events(void) {}
uint32_t interrupts_cycles_to_vblank(void) { return 564480u; }
uint32_t timers_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t cdrom_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t dma_cycles_to_internal_event(void) { return UINT32_MAX; }
uint32_t dma_cycles_to_deliverable_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t sio_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t psx_spu_sample_event_cycles_to_next(void) { return UINT32_MAX; }
void psx_spu_sample_event_service(void) {}
void starvation_watchdog_check(void) {}
void starvation_ring_pc_sample(void) {}
void dirty_ram_irq_ambient_resync_after_restore(void) {}
/* The interpreter's deferred load and the native-shard stack live in those
* modules' statics; model them here the same way. */
static DirtyRamLoadDelay s_ld;
void dirty_ram_ld_delay_discard(void) { memset(&s_ld, 0, sizeof s_ld); }
void dirty_ram_ld_delay_save(DirtyRamLoadDelay *o) { *o = s_ld; }
void dirty_ram_ld_delay_restore(const DirtyRamLoadDelay *i) { s_ld = *i; }
static int s_ov_active_depth;
static uint32_t s_ov_inprogress;
void overlay_loader_native_nesting(int *d, uint32_t *ip) {
*d = s_ov_active_depth; *ip = s_ov_inprogress;
}
void overlay_loader_set_native_nesting(int d, uint32_t ip) {
s_ov_active_depth = d; s_ov_inprogress = ip;
}
/* GPU and presenter. */
uint64_t gpu_pass_state_hash(void) { return 42; }
int gpu_pass_checkpoint_save(void) { return 1; }
void gpu_pass_checkpoint_restore(void) {}
static uint64_t s_ticks;
uint64_t gl_renderer_perf_ticks(void) { return s_ticks += 1000; }
uint64_t gl_renderer_perf_frequency(void) { return 1000000000u; }
uint32_t gl_renderer_pass_unavailable(void) { return 0; }
int psx_presentation_fast_forward(void) { return 0; }
uint32_t gl_renderer_pass_plan(uint32_t p, uint32_t s, uint32_t *a,
uint32_t max, uint32_t *wanted) {
(void)p; (void)s; (void)a; (void)max;
if (wanted) *wanted = 0;
return 0;
}
static int s_open_passes, s_kept;
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
uint32_t period, int reuse_backup) {
(void)x; (void)y; (void)w; (void)h; (void)open_gen; (void)period;
(void)reuse_backup;
s_open_passes++;
return 1;
}
void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
(void)alpha_q16;
s_open_passes--;
if (keep) s_kept++;
}
uint32_t gl_renderer_pass_leaks(void) { return 0; }
int gl_renderer_pass_verify_vram(void) { return 1; }
void gl_renderer_pass_note_cost(uint64_t t) { (void)t; }
void gl_renderer_pass_service_presents(void) {}
/* ---- an overlay shard's cycle shim (overlay_dispatch_preamble.c.inc) --- */
static uint32_t s_shard_pending;
static void shard_flush(void) {
uint32_t c = s_shard_pending;
s_shard_pending = 0;
if (c) psx_advance_cycles(c);
}
static void live_flush(void) {}
/* ---- guest frames ------------------------------------------------------ */
typedef struct Frames {
int depth; /* nested frames to open */
int run_away; /* innermost frame spins until the watchdog fires */
} Frames;
/* One generated function that calls through a native overlay unit, a DMA
* kick and the interpreter, each with the runtime's own entry/exit protocol,
* then recurses. */
static void guest_frame(CPUState *cpu, const Frames *f, int level) {
__attribute__((cleanup(psx_cyc_bb_defer_cleanup))) int guard = 1;
int prev_unit = g_call_unit_depth, prev_active = g_dirty_interp_active;
int prev_phase = g_exec_phase, prev_precise = g_precise_mode;
int prev_ov_depth = s_ov_active_depth;
uint32_t prev_ov_ip = s_ov_inprogress;
void (*prev_flush)(void) = g_overlay_flush_pending_cycles;
(void)guard;
psx_cyc_bb_defer_begin();
g_call_unit_depth = prev_unit + 1; /* overlay_loader_call_native */
s_ov_active_depth = prev_ov_depth + 1; /* overlay_loader_dispatch */
s_ov_inprogress = 0x80100000u + (uint32_t)level;
g_overlay_flush_pending_cycles = shard_flush;
g_exec_phase = 2;
g_dma_exec_depth++; /* dma.c kick */
g_dma_cur_ch = 2;
g_dirty_interp_active = 1; /* dirty_ram_dispatch */
g_precise_mode = 1; /* psx_run_precise */
s_ld.armed = 1; s_ld.rt = 8; s_ld.val = 0xDEAD0000u + (uint32_t)level;
s_shard_pending = 777; /* shard-side charges */
s_ram[0x1000 + level] = 0xAA; /* the pass draws */
cpu->gpr[8] = 0x1234u + (uint32_t)level;
if (level + 1 < f->depth) {
guest_frame(cpu, f, level + 1);
} else if (f->run_away) {
/* A wait loop that never ends in frozen time: charges per
* instruction, publishes at every branch edge. */
for (uint32_t i = 0; i < 100000000u; i++) {
psx_cyc_charge(4);
psx_cyc_bb_defer_flush();
}
}
s_ld.armed = 0; /* retired on interpreter exit */
g_precise_mode = prev_precise;
g_dirty_interp_active = prev_active;
g_dma_exec_depth--;
g_dma_cur_ch = -1;
g_exec_phase = prev_phase;
shard_flush();
g_overlay_flush_pending_cycles = prev_flush;
s_ov_inprogress = prev_ov_ip;
s_ov_active_depth = prev_ov_depth;
g_call_unit_depth = prev_unit;
}
static int pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
(void)alpha_q16;
guest_frame(cpu, (const Frames *)user, 0);
return 1;
}
static int leaky_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
(void)cpu; (void)user; (void)alpha_q16;
g_call_unit_depth++; /* forgets its exit */
return 1;
}
typedef struct Live {
int bb_defer, unit, dma, dma_ch, active, phase, precise, dispatch;
int ov_depth;
uint32_t ov_ip, batch, resume;
void (*flush)(void);
DirtyRamLoadDelay ld;
uint64_t cycles, next_service, dev_cycles;
} Live;
static void snap(Live *l) {
memset(l, 0, sizeof *l);
l->bb_defer = g_psx_cyc_bb_defer;
l->unit = g_call_unit_depth;
l->dma = g_dma_exec_depth;
l->dma_ch = g_dma_cur_ch;
l->active = g_dirty_interp_active;
l->phase = g_exec_phase;
l->precise = g_precise_mode;
l->dispatch = g_psx_dispatch_depth;
l->ov_depth = s_ov_active_depth;
l->ov_ip = s_ov_inprogress;
l->batch = g_psx_cyc_batch;
l->resume = g_dirty_safe_resume_pc;
l->flush = g_overlay_flush_pending_cycles;
l->ld = s_ld;
l->cycles = psx_cycle_count;
l->next_service = psx_next_service_cycle;
l->dev_cycles = s_dev_cycles;
}
static void check_live(const Live *a, const char *when) {
Live b;
char m[160];
snap(&b);
#define SAME(field, what) do { \
snprintf(m, sizeof m, "%s: %s restored", when, what); \
CHECK(a->field == b.field, m); } while (0)
SAME(bb_defer, "g_psx_cyc_bb_defer");
SAME(unit, "g_call_unit_depth");
SAME(dma, "g_dma_exec_depth");
SAME(dma_ch, "g_dma_cur_ch");
SAME(active, "g_dirty_interp_active");
SAME(phase, "g_exec_phase");
SAME(precise, "g_precise_mode");
SAME(dispatch, "g_psx_dispatch_depth");
SAME(ov_depth, "native-shard stack depth");
SAME(ov_ip, "native in-progress entry");
SAME(batch, "pending cycle batch");
SAME(resume, "interpreter resume latch");
SAME(flush, "shard cycle-flush hook");
SAME(ld.armed, "pending load (armed)");
SAME(ld.val, "pending load (value)");
SAME(cycles, "guest clock");
SAME(next_service, "service deadline");
SAME(dev_cycles, "device time (nothing advanced)");
#undef SAME
}
int main(void) {
CPUState cpu;
PSXModRenderPass pass;
RenderPassStats st;
Frames runaway = {5, 1}, clean = {5, 0};
Live live;
uint8_t ram_before[64];
#ifdef _WIN32
_putenv_s("PSX_RENDER_PASS_VERIFY", "1");
#else
setenv("PSX_RENDER_PASS_VERIFY", "1", 1);
#endif
memset(&cpu, 0, sizeof cpu);
cpu.gpr[8] = 0x55u;
psx_cycles_resync_after_restore(NULL);
psx_advance_cycles(50000);
/* The live context the pass interrupts: inside two generated functions
* (the game's VSync entry hook), a native shard's flush hook installed,
* the interpreter's resume latch and a pending load of its own. */
g_psx_cyc_bb_defer = 2;
g_psx_cyc_batch = 9;
g_psx_dispatch_depth = 3;
g_exec_phase = 3;
g_dirty_safe_resume_pc = 0x80012344u;
g_overlay_flush_pending_cycles = live_flush;
s_ld.armed = 1; s_ld.rt = 3; s_ld.val = 0x11112222u; s_ld.age = 1;
s_ov_active_depth = 1;
s_ov_inprogress = 0x80110000u;
memcpy(ram_before, s_ram + 0x1000, sizeof ram_before);
memset(&pass, 0, sizeof pass);
pass.struct_size = sizeof pass;
pass.w = 320; pass.h = 240;
pass.alpha_q16 = 32768u;
/* 1. A pass that runs away inside five nested frames. */
snap(&live);
CHECK(psx_mod_render_pass(&cpu, &pass, pass_fn, &runaway) == 0,
"a runaway pass is rolled back, not presented");
render_pass_get_stats(&st);
CHECK(st.watchdog == 1 && st.aborted == 1 && st.passes == 0,
"the watchdog cut it off once");
CHECK(st.nesting_repairs == 1, "the landing found and undid skipped exits");
CHECK(!st.disabled, "one fault does not disable passes");
CHECK(s_open_passes == 0 && s_kept == 0, "the presenter closed the pass, no image");
CHECK(!g_psx_render_pass_active, "time is live again");
check_live(&live, "after a watchdog abort");
CHECK(s_shard_pending == 0,
"the shard's unpublished pass cycles went into the frozen clock");
CHECK(memcmp(ram_before, s_ram + 0x1000, sizeof ram_before) == 0,
"RAM the pass wrote is restored");
CHECK(cpu.gpr[8] == 0x55u, "CPU registers are restored");
CHECK(st.verify_mismatch == 0,
"PSX_RENDER_PASS_VERIFY: machine state identical after the abort");
/* 2. The same frames, returning normally, present an image. */
snap(&live);
CHECK(psx_mod_render_pass(&cpu, &pass, pass_fn, &clean) == 1,
"a pass that completes is presented");
render_pass_get_stats(&st);
CHECK(st.passes == 1 && s_kept == 1, "one image kept");
CHECK(st.nesting_repairs == 1, "a normal return needs no repair");
check_live(&live, "after a normal pass");
CHECK(st.verify_mismatch == 0, "a balanced pass verifies clean");
/* 3. Verify mode flags a pass that leaves the nesting unbalanced (the
* restore would otherwise hide it), and still restores it. */
snap(&live);
(void)psx_mod_render_pass(&cpu, &pass, leaky_pass_fn, NULL);
render_pass_get_stats(&st);
CHECK(st.verify_mismatch == 1, "an unbalanced pass is reported in verify mode");
check_live(&live, "after an unbalanced pass");
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}
+135
View File
@@ -0,0 +1,135 @@
/* Render-pass time freeze (psx_cycle_freeze.h psx_cycle_freeze_begin/end).
* A pass counts guest cycles but never services a device, and afterwards
* every clock value is exactly as before. Build/run:
* ctest -R render_pass_freeze_test */
#include "psx_cycles.h"
#include "psx_cycle_freeze.h"
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
int g_ls_replay_active = 0;
int g_ls_mode = 0;
int g_precise_mode = 0;
int g_psx_call_bail = 0;
uint32_t i_mask = 0;
uint64_t g_guest_store_count = 0;
uint64_t g_mmio_access_count = 0;
/* Mock devices: count every advance and event. The "CD" raises an event
* every 1000 cycles; the VBlank every 564480. */
static uint64_t s_dev_cycles = 0;
static uint64_t s_dev_calls = 0;
static uint64_t s_cd_events = 0;
static uint32_t s_cd_phase = 0;
static uint64_t s_vblank_phase = 0;
void sio_advance(uint32_t cycles) { (void)cycles; }
void cdrom_advance(uint32_t cycles) {
s_dev_calls++;
s_dev_cycles += cycles;
s_cd_phase += cycles;
while (s_cd_phase >= 1000u) { s_cd_phase -= 1000u; s_cd_events++; }
}
void dma_advance(uint32_t cycles) { (void)cycles; }
void timers_advance(uint32_t cycles) { (void)cycles; }
void interrupts_advance_cycles(uint32_t cycles) { s_vblank_phase += cycles; }
void interrupts_service_scheduled_events(void) {}
uint32_t interrupts_cycles_to_vblank(void) {
return (uint32_t)(564480u - (s_vblank_phase % 564480u));
}
uint32_t timers_cycles_to_irq(uint32_t mask) { (void)mask; return UINT32_MAX; }
uint32_t cdrom_cycles_to_irq(uint32_t mask) {
(void)mask;
return 1000u - s_cd_phase;
}
uint32_t dma_cycles_to_internal_event(void) { return UINT32_MAX; }
uint32_t dma_cycles_to_deliverable_irq(uint32_t mask) { (void)mask; return UINT32_MAX; }
uint32_t sio_cycles_to_irq(uint32_t mask) { (void)mask; return UINT32_MAX; }
uint32_t psx_spu_sample_event_cycles_to_next(void) { return UINT32_MAX; }
void psx_spu_sample_event_service(void) {}
int psx_get_in_exception(void) { return 0; }
void starvation_watchdog_check(void) {}
void starvation_ring_pc_sample(void) {}
int psx_netplay_active(void) { return 0; }
int psx_selfcheck_enabled(void) { return 0; }
void dirty_ram_ld_delay_discard(void) {}
void dirty_ram_irq_ambient_resync_after_restore(void) {}
static int failures;
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)
static jmp_buf s_jmp;
static int s_overruns;
static void overrun(void) { s_overruns++; longjmp(s_jmp, 1); }
int main(void) {
PsxCycleFreeze save;
uint64_t cyc0, next0, dev_calls0, dev_cycles0, cd0;
uint32_t local = 7;
psx_cycles_resync_after_restore(NULL);
psx_advance_cycles(12345); /* some live history */
g_psx_cyc_batch = 40; /* deferred charges of the guest code */
g_psx_cyc_local_acc = &local;
cyc0 = psx_cycle_count;
next0 = psx_next_service_cycle;
dev_calls0 = s_dev_calls;
dev_cycles0 = s_dev_cycles;
cd0 = s_cd_events;
CHECK(psx_cycle_freeze_begin(&save, 0, NULL), "freeze begins");
CHECK(g_psx_render_pass_active == 1, "pass flag set");
CHECK(!psx_cycle_freeze_begin(&save, 0, NULL), "freezes do not nest");
CHECK(g_psx_cyc_batch == 0 && g_psx_cyc_local_acc == NULL,
"the guest's pending charges are parked, not published");
for (int i = 0; i < 1000; i++) psx_advance_cycles(1000); /* 10^6 cycles */
psx_advance_cycles_slow(5000);
psx_devices_service_to_now();
psx_devices_mmio_sync();
CHECK(psx_cycle_count - cyc0 == 1005000u,
"cycles are still counted inside the pass (GTE/muldiv deadlines)");
CHECK(s_dev_calls == dev_calls0 && s_dev_cycles == dev_cycles0 &&
s_cd_events == cd0,
"no device advanced and no device event fired in a pass");
psx_cycle_freeze_end(&save);
CHECK(g_psx_render_pass_active == 0, "pass flag cleared");
CHECK(psx_cycle_count == cyc0, "cycle count restored exactly");
CHECK(psx_next_service_cycle == next0, "service deadline restored exactly");
CHECK(g_psx_cyc_batch == 40 && g_psx_cyc_local_acc == &local && local == 7,
"the guest's pending charges come back untouched");
/* The live timeline continues exactly as if the pass never happened. */
g_psx_cyc_batch = 0;
g_psx_cyc_local_acc = NULL;
psx_advance_cycles(3000);
CHECK(s_cd_events == cd0 + (uint64_t)((12345u % 1000u + 3000u) / 1000u) ||
s_cd_events >= cd0 + 3u,
"devices resume on the live clock after the pass");
/* Watchdog: a pass that runs away is cut off. The longjmp skips the
* bb_defer exits of the generated frames it leaves (their cleanup
* handlers never run); end() puts the interrupted depth back. */
s_overruns = 0;
cyc0 = psx_cycle_count;
g_psx_cyc_bb_defer = 2;
if (setjmp(s_jmp) == 0) {
CHECK(psx_cycle_freeze_begin(&save, 200000u, overrun), "freeze with watchdog");
g_psx_cyc_bb_defer += 3; /* three nested generated frames */
for (int i = 0; i < 1000; i++) psx_advance_cycles(1000);
CHECK(0, "watchdog should have fired");
}
psx_cycle_freeze_end(&save);
CHECK(s_overruns == 1, "watchdog fired once");
CHECK(psx_cycle_count == cyc0 && !g_psx_render_pass_active,
"state restored after a watchdog abort");
CHECK(g_psx_cyc_bb_defer == 2,
"cycle deferral depth restored after a watchdog abort");
g_psx_cyc_bb_defer = 0;
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""Pin the render-pass sandbox (docs/RENDER_PASSES.md) at its choke points.
A render pass runs guest draw code in frozen time and restores the machine
afterwards. That only holds if every path that could let time pass, deliver an
interrupt, touch a device the restore does not cover, or record the pass into
the live timeline checks g_psx_render_pass_active first. These are the choke
points; each guard below names the hole it closes.
"""
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parents[2]
SRC = ROOT / "runtime" / "src"
def body(text, signature):
"""Return the first ~40 lines of the function whose definition starts with
`signature` (enough to see its early-return gates)."""
i = text.index(signature)
return "\n".join(text[i:].splitlines()[:40])
cycles = (SRC / "psx_cycles.c").read_text(encoding="utf-8")
b = body(cycles, "void psx_devices_service_to_now(void) {")
assert re.search(r"if \(g_psx_render_pass_active\)[^\n]*return;", b), (
"device servicing must stop during a pass (time would advance)")
b = body(cycles, "void psx_devices_mmio_sync(void) {")
assert "g_psx_render_pass_active" in b, (
"MMIO sync must not catch devices up during a pass")
b = body(cycles, "void psx_advance_cycles_slow(uint32_t cycles) {")
assert "g_psx_render_pass_active" in b, (
"the slow/conservative cycle path must not advance devices during a pass")
irq = (SRC / "interrupts.c").read_text(encoding="utf-8")
for sig in ("int psx_interrupt_delivery_needed(const CPUState* cpu) {",
"void psx_check_interrupts(CPUState* cpu) {"):
lines = body(irq, sig).splitlines()[:6]
assert any("g_psx_render_pass_active" in l and "return" in l for l in lines), (
"no interrupt may be delivered inside a pass: " + sig)
dma = (SRC / "dma.c").read_text(encoding="utf-8")
assert "while (g_psx_render_pass_active && gpu_linked_list.active)" in dma, (
"GPU linked-list DMA must complete synchronously in a pass")
b = body(dma, "static void schedule_delayed_complete(int ch, uint32_t total_words,")
assert "g_psx_render_pass_active" in b, (
"delayed DMA completion would never arrive in frozen time")
mem = (SRC / "memory.c").read_text(encoding="utf-8")
for fn in ("void psx_write_word(uint32_t addr, uint32_t val) {",
"void psx_write_half(uint32_t addr, uint16_t val) {",
"void psx_write_byte(uint32_t addr, uint8_t val) {"):
lines = body(mem, fn).splitlines()[:6]
assert any("render_pass_store" in l for l in lines), (
"pass stores must bypass live-timeline observers: " + fn)
b = body(mem, "static void render_pass_store(")
assert "render_pass_store_to(&t, addr, val, width)" in b and \
"g_render_pass_dropped_writes[cls]++" in b, (
"pass stores must go through the tested store policy "
"(render_pass_store_to, render_pass_sandbox_test) and count drops")
assert "t.ram_size = psx_ram_live_bytes();" in b, (
"pass stores must fold RAM through the LIVE 2/8 MiB geometry, as "
"psx_ram_map_write does (not the 8 MiB backing size)")
rp = (SRC / "render_pass.c").read_text(encoding="utf-8")
assert "uint32_t ram_bytes = memory_get_ram_bytes();" in body(
rp, "static int checkpoint_save(") and \
"memcpy(memory_get_ram_ptr(), s_ram_copy, s_ck.ram_bytes);" in body(
rp, "static void checkpoint_restore(") and \
"memory_get_ram_bytes()" in body(rp, "static uint64_t state_hash("), (
"the pass checkpoint, restore and verify hash must cover the live RAM "
"size (8 MiB with the 8 MB RAM mod)")
plan = (SRC / "render_pass_plan.c").read_text(encoding="utf-8")
assert "render_pass_mmio_class(phys, val, width)" in body(
plan, "int render_pass_store_to("), (
"pass MMIO stores must go through the MMIO allow-list")
gl = (SRC / "gpu_gl_renderer.c").read_text(encoding="utf-8")
assert "render_pass_vram_policy(&s_pj_cpu" in body(
gl, "static int pass_refuse_write("), (
"out-of-rect VRAM writes must go through the tested journal policy")
assert "render_pass_journal_rollback(&s_pj_cpu" in body(
gl, "static void pass_journal_rollback("), (
"the journal rollback must restore the CPU VRAM rows")
# The journal backs up the hr surface, the raw mirror and the CPU rows. That
# is complete in native-wide too only while the out-of-rect write paths never
# touch a wide surface; R4 copies 2x1 pixels outside the display every frame,
# so refusing them under native-wide rolled back every widescreen pass.
def definition(text, name):
"""The whole body of function `name` (its definition, not a prototype)."""
m = re.search(r"^static [\w ]+\b" + name + r"\([^;{]*\)\s*\{", text, re.M)
assert m, "no definition of " + name
depth, i = 0, m.end() - 1
while True:
depth += {"{": 1, "}": -1}.get(text[i], 0)
if depth == 0:
return text[m.start():i + 1]
i += 1
assert "g_wide" not in definition(gl, "pass_journal_protect"), (
"out-of-rect writes are journaled in native-wide as well")
for name in ("gpu_fill", "gpu_copy_rect", "flush_cpu_upload",
"glb_vram_transfer_in", "glb_vram_write"):
assert "wide" not in definition(gl, name), (
"journaled writes must not reach a native-wide surface: " + name)
# One-time pass allocations are not cost samples (render_pass_plan_test):
# every path that makes a pass texture or framebuffer must count it, and the
# cost average must go through the tested sampling rule.
for name in ("pass_make_color_fbo", "pass_gen_reserve"):
assert "s_pass_allocs++" in definition(gl, name), (
"pass allocations must be counted: " + name)
assert "render_pass_cost_add(" in body(
gl, "void gl_renderer_pass_note_cost(uint64_t ticks) {"), (
"the pass-cost average must leave allocating passes out")
assert "s_pass_allocs_begin = s_pass_allocs;" in body(
gl, "int gl_renderer_pass_begin("), (
"each pass must mark where its allocations start")
# The average belongs to one presented image size. A plan at another size
# passes an unknown cost (0), for which render_pass_plan_phases plans a single
# measuring pass (render_pass_plan_test): passes run on the emulation thread.
nc = body(gl, "void gl_renderer_pass_note_cost(uint64_t ticks) {")
assert "s_pass_cost_w != s_interp_w || s_pass_cost_h != s_interp_h" in nc and \
"memset(&s_pass_cost, 0, sizeof s_pass_cost);" in nc, (
"a new image size must start the pass-cost average over")
assert re.search(r"in\.pass_cost = \(s_pass_cost_w == s_interp_w && "
r"s_pass_cost_h == s_interp_h\)\s*"
r"\? render_pass_cost_estimate\(&s_pass_cost\) : 0\.0;",
gl[gl.index("uint32_t gl_renderer_pass_plan("):][:4000]), (
"a plan must not use a cost measured at another image size")
# Only passes that run are measured: an estimate that prices every plan out
# (measured in a transient) must be measured again, or passes stay shed and a
# plugin's crossfade fallback never ends (render_pass_plan_test).
gp = gl[gl.index("uint32_t gl_renderer_pass_plan("):]
gp = gp[:gp.index("\n}\n")]
assert re.search(r"if \(want && s_pass_cost_w == s_interp_w && "
r"s_pass_cost_h == s_interp_h &&\s*"
r"render_pass_cost_note_plan\(&s_pass_cost\)\) \{", gp) and \
re.search(r"in\.pass_cost = 0\.0;\s*n = render_pass_plan_phases\(&in, alpha_q16, NULL\);",
gp), (
"every plan that wants passes must let a stale estimate be re-measured, "
"and the re-measuring plan must ask for one pass")
# A frame on screen longer than planned (a lagging tick) must hold its newest
# pass image, never fall back to the older capture (render_pass_plan_test).
pgp = definition(gl, "pass_gen_present")
assert "render_pass_gen_select(g->phase, g->n, p, &lo, &hi, &t)" in pgp, (
"pass images must be selected through the tested late-flip rule")
assert "render_pass_select(" not in pgp and "0.5 / (double)g->period" not in pgp, (
"no early expiry of a late frame's images")
cls = body(plan, "int render_pass_mmio_class(")
for dev in ("RENDER_PASS_DROP_SPU", "RENDER_PASS_DROP_CD",
"RENDER_PASS_DROP_TIMER"):
assert dev in cls, "a pass must never reach " + dev
dbg = (SRC / "debug_server.c").read_text(encoding="utf-8")
for fn in ("void debug_server_trace_write_check(",
"void debug_server_trace_mmio_write("):
assert "g_psx_render_pass_active" in body(dbg, fn), (
"rolled-back pass writes must stay out of the frame fingerprints: " + fn)
rp = (SRC / "render_pass.c").read_text(encoding="utf-8")
for gate in ("psx_netplay_active()", "psx_rewind_is_open()",
"psx_selfcheck_resim_active()", "psx_get_in_exception()",
"dma_gpu_linked_list_active()",
"psx_presentation_fast_forward()"):
assert gate in rp, "passes must refuse to run when " + gate
main = (SRC / "main.cpp").read_text(encoding="utf-8")
ff = main[main.index("const int present_every ="):]
ff = ff[:ff.index("turbo_skip = (turbo_skip + 1) % present_every;")]
assert "s_presentation_fast_forward = 1;" in ff, (
"manual fast-forward must refuse render passes (it presents every 2nd "
"VBlank, so the plan would otherwise still see presents)")
b = body(rp, "static void checkpoint_restore(CPUState *cpu) {")
assert "nesting_restore(&s_ck.nest);" in b, (
"a watchdog abort longjmps past the exits of the frames it leaves: the "
"restore must put the host nesting back (render_pass_abort_test)")
gl = (SRC / "gpu_gl_renderer.c").read_text(encoding="utf-8")
b = body(gl, "static int pass_gen_reserve(int gi, uint32_t need, int w, int h) {")
assert "need > pass_slot_cap(w, h)" in b, (
"pass image textures must stay inside the slot cap's memory budget")
assert "pass_gen_reserve(gi, g->n + 1u, g->tex_w, g->tex_h)" in gl, (
"pass image textures are made as slots fill, not all PASS_SLOTS up front")
assert "for (uint32_t i = 0; i < PASS_SLOTS; i++) {\n if (!s_pgen_tex" not in gl, (
"no eager allocation of every slot texture")
assert "pass_resources_release();" in body(gl, "void gl_renderer_shutdown(void) {"), (
"pass textures die with the context: forget them at shutdown")
# Default off means no cost for other titles: the headers folded into the
# overlay codegen hash (runtime/codegen_hash_sources.cmake) must not carry the
# render-pass API, or every title's overlay cache and savestates would be
# invalidated by a feature they never use. The freeze lives in the
# runtime-only psx_cycle_freeze.h.
INC = ROOT / "runtime" / "include"
hash_list = (ROOT / "runtime" / "codegen_hash_sources.cmake").read_text(
encoding="utf-8")
for header in re.findall(r"runtime/include/([\w.]+)", hash_list):
text = (INC / header).read_text(encoding="utf-8")
for name in ("g_psx_render_pass_active", "PsxCycleFreeze",
"psx_cycle_freeze", "render_pass"):
assert name not in text, (
f"{header} is in the codegen hash and must not mention {name}")
assert "psx_cycle_freeze.h" not in hash_list, (
"psx_cycle_freeze.h is runtime-only; keep it out of the codegen hash")
print("render-pass sandbox guards passed")
+307
View File
@@ -0,0 +1,307 @@
/* Render-pass planning and presentation-selection math.
* Build/run: ctest -R render_pass_plan_test */
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "render_pass.h"
#include "render_pass_plan.h"
static int failures;
#define CHECK(condition, message) do { \
if (!(condition)) { fprintf(stderr, "FAIL: %s\n", message); failures++; } \
} while (0)
/* One 30 Hz game frame (2 VBlanks at 59.94 Hz) in nanosecond ticks. */
static const double kFrame = 2.0 * 1e9 / 59.94;
static void test_counts_per_rate(void) {
static const struct { double hz; uint32_t lo, hi; } rates[] = {
{60.0, 1, 2}, {100.0, 3, 4}, {120.0, 3, 4}, {200.0, 6, 7},
{240.0, 7, 8}, {300.0, 9, 10},
};
for (size_t r = 0; r < sizeof rates / sizeof rates[0]; r++) {
double period = 1e9 / rates[r].hz;
/* Slide the output grid across one output period. */
for (int k = 0; k < 16; k++) {
RenderPassPlanInput in = {0};
uint32_t a[RENDER_PASS_MAX_PHASES], wanted = 0, n;
char msg[160];
in.frame_start = 1e12;
in.frame_length = kFrame;
in.target_period = period;
in.next_deadline = in.frame_start - 3.0 * period +
period * (double)k / 16.0;
in.budget = -1.0;
in.max = RENDER_PASS_MAX_PHASES;
n = render_pass_plan_phases(&in, a, &wanted);
snprintf(msg, sizeof msg, "%.0f Hz offset %d/16: %u passes",
rates[r].hz, k, (unsigned)n);
CHECK(n >= rates[r].lo && n <= rates[r].hi, msg);
CHECK(wanted == n, "unlimited budget sheds nothing");
for (uint32_t i = 0; i < n; i++) {
CHECK(a[i] > 0 && a[i] < 65536u, "phase inside (0, 1)");
if (i) CHECK(a[i] > a[i - 1], "phases ascend");
/* Each phase is an actual output deadline. */
double d = in.frame_start + (double)a[i] / 65536.0 * kFrame;
double m = fmod(d - in.next_deadline, period);
if (m > period / 2) m -= period;
snprintf(msg, sizeof msg,
"%.0f Hz phase %u lands on a deadline (off %.0f ns)",
rates[r].hz, (unsigned)i, m);
CHECK(fabs(m) < kFrame / 65536.0 + 1.0, msg);
}
}
}
}
static void test_shedding(void) {
RenderPassPlanInput in = {0};
uint32_t a[RENDER_PASS_MAX_PHASES], wanted = 0, n;
in.frame_start = 1e12;
in.frame_length = kFrame;
in.target_period = 1e9 / 300.0;
in.next_deadline = in.frame_start + 1e9 / 600.0; /* half-period offset */
in.max = RENDER_PASS_MAX_PHASES;
in.pass_cost = 2e6; /* 2 ms per pass */
in.budget = 5e6; /* 5 ms */
n = render_pass_plan_phases(&in, a, &wanted);
CHECK(wanted == 10, "300 Hz half-offset wants ten phases");
CHECK(n == 2, "a 5 ms budget at 2 ms per pass affords two");
CHECK(n == 2 && a[0] < 32768u && a[1] > 32768u,
"a shed subset spreads across the frame");
in.budget = 1e6;
CHECK(render_pass_plan_phases(&in, a, &wanted) == 0 && wanted == 10,
"less than one pass of budget renders none");
/* No pass measured yet (first plans, or a new image size): passes run on
* the emulation thread, so one measures the cost; a whole plan of
* unknown cost could stall the guest for frames (46.9 ms each at 4K). */
in.pass_cost = 0.0;
in.budget = 26e6;
n = render_pass_plan_phases(&in, a, &wanted);
CHECK(n == 1 && wanted == 10, "unknown cost plans one pass");
CHECK(n == 1 && a[0] > 16384u && a[0] < 49152u,
"the one pass sits mid-frame, as a shed subset of one does");
in.budget = 0.0;
CHECK(render_pass_plan_phases(&in, a, NULL) == 0,
"no budget: not even the measuring pass");
in.pass_cost = 2e6;
in.budget = -1.0;
in.max = 4;
CHECK(render_pass_plan_phases(&in, a, NULL) == 4,
"capacity caps the plan");
in.max = RENDER_PASS_MAX_PHASES;
in.target_period = 0.0;
CHECK(render_pass_plan_phases(&in, a, &wanted) == 0 && wanted == 0,
"no live output schedule plans nothing");
}
static void test_select(void) {
const uint32_t ph[] = {0u, 16384u, 32768u, 49152u};
uint32_t lo = 99, hi = 99;
float t = -1.0f;
CHECK(render_pass_select(ph, 4, 0.0, &lo, &hi, &t) && lo == 0 && hi == 0,
"phase 0 shows the game's image");
CHECK(render_pass_select(ph, 4, 0.25, &lo, &hi, &t) && lo == 1 && hi == 1,
"an exact pass phase shows that pass alone");
CHECK(render_pass_select(ph, 4, 0.375, &lo, &hi, &t) && lo == 1 && hi == 2 &&
fabsf(t - 0.5f) < 1e-4f,
"between passes the neighbours blend");
CHECK(render_pass_select(ph, 4, 0.9, &lo, &hi, &t) && lo == 3 && hi == 3,
"past the last pass it holds (the next frame is not known yet)");
CHECK(render_pass_select(ph, 1, 0.6, &lo, &hi, &t) && lo == 0 && hi == 0,
"with only the game image it holds");
CHECK(!render_pass_select(ph, 0, 0.5, &lo, &hi, &t), "no items, no pick");
CHECK(render_pass_select(ph, 4, -0.1, &lo, &hi, &t) && lo == 0 && hi == 0,
"before the frame starts show its first image");
}
/* A frame on screen longer than planned (a 30 Hz tick that takes three
* VBlanks) keeps its newest image until the next flip; it must never go back
* to the game's own image (phase 0), which is older. */
static void test_gen_select_late(void) {
const uint32_t ph[] = {0u, 16384u, 32768u, 49152u};
const double late[] = {1.0, 1.25, 1.2501, 1.3, 1.5, 2.0, 3.99,
RENDER_PASS_GEN_HOLD_MAX};
uint32_t lo = 99, hi = 99;
float t = -1.0f;
CHECK(render_pass_gen_select(ph, 4, 0.375, &lo, &hi, &t) && lo == 1 &&
hi == 2 && fabsf(t - 0.5f) < 1e-4f,
"inside the frame it selects as render_pass_select");
for (unsigned i = 0; i < sizeof late / sizeof late[0]; i++) {
char msg[96];
lo = hi = 99;
snprintf(msg, sizeof msg, "late flip (p = %.4f) holds the newest image",
late[i]);
CHECK(render_pass_gen_select(ph, 4, late[i], &lo, &hi, &t) &&
lo == 3 && hi == 3 && t == 0.0f, msg);
}
CHECK(!render_pass_gen_select(ph, 4, RENDER_PASS_GEN_HOLD_MAX + 0.01,
&lo, &hi, &t),
"a game that stops flipping expires the frame after the hold");
CHECK(!render_pass_gen_select(ph, 4, NAN, &lo, &hi, &t),
"a phase that is not a number expires it");
CHECK(!render_pass_gen_select(ph, 0, 0.5, &lo, &hi, &t), "no items, no pick");
}
static void test_budget_and_ema(void) {
CHECK(fabs(render_pass_budget(0, 0, 100.0, 0.5) - 50.0) < 1e-9,
"no history spends the share of the frame");
CHECK(fabs(render_pass_budget(20.0, 10.0, 100.0, 0.8) - 24.0) < 1e-9,
"idle plus pass time, scaled");
CHECK(render_pass_budget(500.0, 0.0, 100.0, 0.8) == 100.0,
"never more than a frame");
CHECK(render_pass_budget(10.0, 10.0, 0.0, 0.8) == 0.0, "no frame, no budget");
CHECK(render_pass_ema(0.0, 4.0) == 4.0, "first sample seeds the average");
CHECK(fabs(render_pass_ema(4.0, 8.0) - 5.0) < 1e-9, "quarter-weight update");
CHECK(render_pass_ema(4.0, -1.0) == 4.0, "bad samples are ignored");
{
/* First-use allocations (70 ms at 4K) must not set the average a
* shed-for-time plan then never revisits; steady passes (10 ms) do. */
RenderPassCost c;
memset(&c, 0, sizeof c);
render_pass_cost_add(&c, 70.0, 1);
CHECK(render_pass_cost_estimate(&c) == 0.0 && c.skips == 1 && c.kept == 0,
"an allocating pass is not a sample");
render_pass_cost_add(&c, 10.0, 0);
render_pass_cost_add(&c, 10.0, 0);
CHECK(render_pass_cost_estimate(&c) == 0.0 && c.skips == 0,
"two samples: still warming up (unknown: one pass per plan)");
render_pass_cost_add(&c, 10.0, 0);
CHECK(render_pass_cost_estimate(&c) == 10.0, "three samples seed the average");
render_pass_cost_add(&c, 70.0, 1);
CHECK(render_pass_cost_estimate(&c) == 10.0,
"a later allocating pass leaves it alone");
for (unsigned i = 1; i < RENDER_PASS_ALLOC_SKIPS; i++)
render_pass_cost_add(&c, 70.0, 1);
CHECK(render_pass_cost_estimate(&c) == 10.0 &&
c.skips == RENDER_PASS_ALLOC_SKIPS,
"up to RENDER_PASS_ALLOC_SKIPS in a row");
render_pass_cost_add(&c, 70.0, 1);
CHECK(fabs(render_pass_cost_estimate(&c) - 25.0) < 1e-9 && c.skips == 0,
"then an allocating pass counts, so the average cannot freeze");
render_pass_cost_add(&c, -1.0, 0);
render_pass_cost_add(&c, NAN, 0);
CHECK(fabs(render_pass_cost_estimate(&c) - 25.0) < 1e-9,
"bad samples are ignored");
}
{
/* One slow first pass (a busy host: 24 ms against a steady 3 ms)
* must not price passes out: the median of the warm-up wins. */
RenderPassCost c;
memset(&c, 0, sizeof c);
render_pass_cost_add(&c, 24.0, 0);
render_pass_cost_add(&c, 3.0, 0);
render_pass_cost_add(&c, 3.5, 0);
CHECK(render_pass_cost_estimate(&c) == 3.5,
"warm-up median ignores one outlier");
memset(&c, 0, sizeof c);
render_pass_cost_add(&c, 47.0, 0);
render_pass_cost_add(&c, 46.0, 0);
render_pass_cost_add(&c, 48.0, 0);
CHECK(render_pass_cost_estimate(&c) == 47.0,
"a truly expensive size (4K) still prices itself out");
}
{
/* The first passes of a race can all run in a transient (14.8 ms
* against a steady 6.9 ms in a verify run). Priced out, no pass runs
* to correct it: an estimate no pass was measured against for
* RENDER_PASS_REWARM_MIN plans is measured again. */
RenderPassCost c;
unsigned i, rewarms = 0;
memset(&c, 0, sizeof c);
CHECK(!render_pass_cost_note_plan(&c), "unknown cost: nothing to re-measure");
for (i = 0; i < RENDER_PASS_COST_WARMUP; i++) render_pass_cost_add(&c, 14.8, 0);
CHECK(render_pass_cost_estimate(&c) == 14.8, "the transient sets the estimate");
for (i = 1; i < RENDER_PASS_REWARM_MIN; i++)
rewarms += (unsigned)render_pass_cost_note_plan(&c);
CHECK(rewarms == 0 && render_pass_cost_estimate(&c) == 14.8,
"29 plans without a pass: still trusted");
CHECK(render_pass_cost_note_plan(&c) && render_pass_cost_estimate(&c) == 0.0,
"the 30th: unknown again, so plans ask for one pass");
CHECK(!render_pass_cost_note_plan(&c), "no second restart while warming up");
for (i = 0; i < RENDER_PASS_COST_WARMUP; i++) render_pass_cost_add(&c, 6.9, 0);
CHECK(render_pass_cost_estimate(&c) == 6.9, "the steady cost replaces it");
CHECK(c.rewarm_after == 0, "a stale estimate found: the wait stays at the minimum");
/* Passes run on it: each measured pass restarts the count. */
for (i = 0; i < 10u * RENDER_PASS_REWARM_MIN; i++) {
rewarms += (unsigned)render_pass_cost_note_plan(&c);
render_pass_cost_add(&c, 6.9, 0);
}
CHECK(rewarms == 0, "an estimate passes run against is never restarted");
/* Priced out again (a busier host): re-measured after the minimum. */
for (i = 0; i < RENDER_PASS_REWARM_MIN; i++)
rewarms += (unsigned)render_pass_cost_note_plan(&c);
CHECK(rewarms == 1, "priced out again: re-measured after the minimum wait");
}
{
/* A size that is truly too expensive (47 ms passes), or a machine at
* its limit (10 ms re-measured as 9): each re-measure confirms the
* estimate, so the waits double to RENDER_PASS_REWARM_MAX and the
* one-pass warm-ups become rare. */
static const double cases[][2] = {{47.0, 47.0}, {10.0, 9.0}};
for (unsigned k = 0; k < 2; k++) {
RenderPassCost c;
unsigned i, plans = 0, waits[8] = {0}, w = 0, first = 1;
memset(&c, 0, sizeof c);
while (w < 8u && plans < 4000u) {
if (render_pass_cost_estimate(&c) == 0.0) {
/* the warm-up's passes */
render_pass_cost_add(&c, first ? cases[k][0] : cases[k][1], 0);
} else {
first = 0;
plans++;
if (render_pass_cost_note_plan(&c)) {
waits[w++] = plans;
plans = 0;
}
}
}
CHECK(w == 8u && waits[0] == RENDER_PASS_REWARM_MIN &&
waits[1] == 2u * RENDER_PASS_REWARM_MIN &&
waits[2] == 4u * RENDER_PASS_REWARM_MIN,
"a confirmed estimate: the waits double");
for (i = 5; i < 8; i++)
CHECK(waits[i] == RENDER_PASS_REWARM_MAX, "and stop at the maximum");
}
}
}
static void test_store_policy(void) {
CHECK(render_pass_mmio_class(0x1F801810u, 0x28000000u, 4) == -1, "GP0 reaches the GPU");
CHECK(render_pass_mmio_class(0x1F801814u, 0x04000002u, 4) == -1, "GP1 DMA mode allowed");
CHECK(render_pass_mmio_class(0x1F801814u, 0x10000007u, 4) == -1, "GP1 info query allowed");
CHECK(render_pass_mmio_class(0x1F801814u, 0x05000000u, 4) == RENDER_PASS_DROP_GPU,
"GP1 display start (a flip) is dropped");
CHECK(render_pass_mmio_class(0x1F801814u, 0x00000000u, 4) == RENDER_PASS_DROP_GPU,
"GP1 reset is dropped");
CHECK(render_pass_mmio_class(0x1F8010A8u, 0x01000401u, 4) == -1, "GPU DMA CHCR allowed");
CHECK(render_pass_mmio_class(0x1F8010E8u, 0x11000002u, 4) == -1, "OTC DMA allowed");
CHECK(render_pass_mmio_class(0x1F8010F4u, 0, 4) == -1, "DICR allowed (restored)");
CHECK(render_pass_mmio_class(0x1F8010C8u, 0x01000201u, 4) == RENDER_PASS_DROP_DMA,
"SPU DMA is dropped");
CHECK(render_pass_mmio_class(0x1F801074u, 0, 4) == -1, "I_MASK allowed (restored)");
CHECK(render_pass_mmio_class(0x1F801D88u, 0x00FFu, 2) == RENDER_PASS_DROP_SPU,
"SPU key-on is dropped (cannot be undone)");
CHECK(render_pass_mmio_class(0x1F801C00u, 0, 2) == RENDER_PASS_DROP_SPU, "SPU voice regs dropped");
CHECK(render_pass_mmio_class(0x1F801801u, 0x1Bu, 1) == RENDER_PASS_DROP_CD, "CD command dropped");
CHECK(render_pass_mmio_class(0x1F801104u, 0, 4) == RENDER_PASS_DROP_TIMER, "timer mode dropped");
CHECK(render_pass_mmio_class(0x1F801040u, 0, 1) == RENDER_PASS_DROP_OTHER, "SIO dropped");
CHECK(render_pass_mmio_class(0x1F801820u, 0, 4) == RENDER_PASS_DROP_OTHER, "MDEC dropped");
}
int main(void) {
test_store_policy();
test_counts_per_rate();
test_shedding();
test_select();
test_gen_select_late();
test_budget_and_ema();
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}
+567
View File
@@ -0,0 +1,567 @@
/* Render-pass sandbox: what a pass may write and what it gets back.
*
* 1. Store policy (render_pass_plan.c render_pass_store_to, the function
* memory.c routes every store made inside a pass through): RAM and
* scratchpad are written directly; SPU, CD, timer, other-DMA, GP1 and
* everything else outside the allow-list is dropped under its class and
* never reaches the device; GP0, GP1 DMA mode, I_STAT/I_MASK, GPU/OTC DMA
* and DPCR/DICR reach it.
* 2. A real pass (render_pass.c, psx_cycles.c and the real timers.c) that
* runs 10^6 guest cycles, makes those stores, acknowledges and masks
* interrupts: the drops are counted per class, no device advances, no
* timer IRQ is raised, and RAM, scratchpad, I_STAT/I_MASK, timers and the
* clock are exactly as before. Status gates refuse the pass without
* running it. The same with 8 MiB RAM live (the opt-in 8 MB RAM map):
* stores reach unique RAM up to 0x7FFFFF and all 8 MiB are restored; a
* retail pass after it folds mirrors again.
* 3. VRAM journal (render_pass_vram_policy / _journal_add / _rollback, the
* CPU side of gpu_gl_renderer.c's out-of-rect journal): writes inside the
* pass rect are allowed, writes outside are backed up first and rolled
* back exactly, covered writes are not journaled twice, a full journal or
* a renderer mode that cannot journal refuses.
*
* The GPU, presenter and remaining devices are mocks. Build/run:
* ctest -R render_pass_sandbox_test */
#include "render_pass.h"
#include "render_pass_plan.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cpu_state.h"
#include "dirty_ram_interp.h"
#include "mod_plugins.h"
#include "psx_cycles.h"
#include "timers.h"
static int failures;
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)
/* ---- runtime globals the units share with the rest of the runtime ----- */
int g_ls_replay_active = 0;
int g_ls_mode = 0;
int g_precise_mode = 0;
int g_psx_call_bail = 0;
int g_psx_dispatch_depth = 0;
uint32_t i_stat = 0, i_mask = 0;
uint64_t g_guest_store_count = 0;
uint64_t g_mmio_access_count = 0;
uint32_t g_debug_last_store_pc = 0;
uint32_t g_debug_current_func_addr = 0;
uint32_t g_psx_icache_tv[1024];
uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES];
void (*g_overlay_flush_pending_cycles)(void) = NULL;
int g_call_unit_depth = 0;
int g_dma_exec_depth = 0;
int g_dma_cur_ch = -1;
uint32_t g_dma_cur_madr = 0, g_dma_cur_bcr = 0, g_dma_initiator_pc = 0;
int g_dirty_interp_active = 0;
int g_exec_phase = 0;
uint32_t g_dirty_safe_resume_pc = 0;
/* ---- memory and the MMIO devices a pass may reach ----------------------- */
/* Backing store sized for the largest geometry, as memory.c's; the live size
* (what memory_get_ram_bytes() answers and memory.c passes the store policy)
* is 2 MiB retail or 8 MiB with the 8 MB RAM mod. */
#define RAM_2MB (2u * 1024u * 1024u)
#define RAM_8MB (8u * 1024u * 1024u)
static uint8_t s_ram[RAM_8MB];
static uint32_t s_ram_live = RAM_2MB;
static uint8_t s_spad[1024];
static uint8_t s_dma_regs[64];
static uint32_t s_csv = 1234;
static int s_mmio_calls, s_gp0_words, s_gp1_calls, s_irq_refresh;
static uint32_t s_last_mmio;
static void mock_mmio_write(uint32_t phys, uint32_t val, uint32_t width) {
(void)width;
s_mmio_calls++;
s_last_mmio = phys;
if (phys == 0x1F801070u) i_stat &= val; /* acknowledge */
else if (phys == 0x1F801074u) i_mask = val;
else if (phys == 0x1F801810u) s_gp0_words++;
else if (phys == 0x1F801814u) s_gp1_calls++;
else if (phys >= 0x1F801100u && phys <= 0x1F80112Fu)
timers_write(phys, val); /* would move a timer */
}
static RenderPassStoreTarget target(void) {
RenderPassStoreTarget t;
memset(&t, 0, sizeof t);
t.ram = s_ram;
t.ram_size = s_ram_live;
t.scratchpad = s_spad;
t.scratchpad_size = sizeof s_spad;
t.mmio_write = mock_mmio_write;
return t;
}
/* What memory.c psx_write_word/half/byte do while a pass is active. */
static void guest_store(uint32_t addr, uint32_t val, uint32_t width) {
RenderPassStoreTarget t = target();
int cls;
if (!g_psx_render_pass_active) {
fprintf(stderr, "FAIL: guest_store outside a pass\n");
failures++;
return;
}
cls = render_pass_store_to(&t, addr, val, width);
if (cls >= 0) g_render_pass_dropped_writes[cls]++;
}
uint8_t *memory_get_ram_ptr(void) { return s_ram; }
uint32_t memory_get_ram_bytes(void) { return s_ram_live; }
uint8_t *memory_get_scratchpad_ptr(void) { return s_spad; }
uint32_t dma_snapshot_bytes(void) { return sizeof s_dma_regs; }
void dma_snapshot_write(uint8_t *p) { memcpy(p, s_dma_regs, sizeof s_dma_regs); }
int dma_snapshot_read(const uint8_t *p, uint32_t len) {
if (len != sizeof s_dma_regs) return 0;
memcpy(s_dma_regs, p, len);
return 1;
}
int dma_gpu_linked_list_active(void) { return 0; }
uint32_t interrupts_get_cycles_since_vblank(void) { return s_csv; }
void interrupts_set_cycles_since_vblank(uint32_t v) { s_csv = v; }
void psx_irq_refresh_cause_ip2(void) { s_irq_refresh++; }
static int s_irqs_raised;
void psx_irq_raise(uint32_t bit, uint32_t detail) {
(void)detail;
s_irqs_raised++;
i_stat |= 1u << bit;
}
void event_ring_record_aux(int ev, uint8_t src, uint32_t aux) {
(void)ev; (void)src; (void)aux;
}
uint32_t cdrom_snapshot_bytes(void) { return 0; }
void cdrom_snapshot_write(uint8_t *p) { (void)p; }
uint32_t spu_snapshot_bytes(void) { return 0; }
void spu_snapshot_write(uint8_t *p) { (void)p; }
uint32_t sio_snapshot_bytes(void) { return 0; }
void sio_snapshot_write(uint8_t *p) { (void)p; }
uint32_t mdec_snapshot_bytes(void) { return 0; }
void mdec_snapshot_write(uint8_t *p) { (void)p; }
int psx_get_in_exception(void) { return 0; }
int psx_netplay_active(void) { return 0; }
int psx_netplay_is_resimulating(void) { return 0; }
int psx_selfcheck_resim_active(void) { return 0; }
int psx_selfcheck_enabled(void) { return 0; }
int psx_rewind_is_open(void) { return 0; }
static int s_fast_forward;
int psx_presentation_fast_forward(void) { return s_fast_forward; }
/* psx_cycles.c device side (timers are the real ones). */
static uint64_t s_dev_cycles;
void sio_advance(uint32_t c) { s_dev_cycles += c; }
void cdrom_advance(uint32_t c) { s_dev_cycles += c; }
void dma_advance(uint32_t c) { s_dev_cycles += c; }
void interrupts_advance_cycles(uint32_t c) { s_dev_cycles += c; }
void interrupts_service_scheduled_events(void) {}
uint32_t interrupts_cycles_to_vblank(void) { return 564480u; }
uint32_t cdrom_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t dma_cycles_to_internal_event(void) { return UINT32_MAX; }
uint32_t dma_cycles_to_deliverable_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t sio_cycles_to_irq(uint32_t m) { (void)m; return UINT32_MAX; }
uint32_t psx_spu_sample_event_cycles_to_next(void) { return UINT32_MAX; }
void psx_spu_sample_event_service(void) {}
void starvation_watchdog_check(void) {}
void starvation_ring_pc_sample(void) {}
void dirty_ram_irq_ambient_resync_after_restore(void) {}
static DirtyRamLoadDelay s_ld;
void dirty_ram_ld_delay_discard(void) { memset(&s_ld, 0, sizeof s_ld); }
void dirty_ram_ld_delay_save(DirtyRamLoadDelay *o) { *o = s_ld; }
void dirty_ram_ld_delay_restore(const DirtyRamLoadDelay *i) { s_ld = *i; }
void overlay_loader_native_nesting(int *d, uint32_t *ip) { *d = 0; *ip = 0; }
void overlay_loader_set_native_nesting(int d, uint32_t ip) { (void)d; (void)ip; }
/* GPU and presenter. */
uint64_t gpu_pass_state_hash(void) { return 42; }
int gpu_pass_checkpoint_save(void) { return 1; }
void gpu_pass_checkpoint_restore(void) {}
static uint64_t s_ticks;
uint64_t gl_renderer_perf_ticks(void) { return s_ticks += 1000; }
uint64_t gl_renderer_perf_frequency(void) { return 1000000000u; }
static uint32_t s_gl_status = PSX_MOD_RENDER_PASS_READY;
uint32_t gl_renderer_pass_unavailable(void) { return s_gl_status; }
uint32_t gl_renderer_pass_plan(uint32_t p, uint32_t s, uint32_t *a,
uint32_t max, uint32_t *wanted) {
(void)p; (void)s; (void)a; (void)max;
if (wanted) *wanted = 0;
return 0;
}
static int s_open_passes, s_kept;
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
uint32_t period, int reuse_backup) {
(void)x; (void)y; (void)w; (void)h; (void)open_gen; (void)period;
(void)reuse_backup;
s_open_passes++;
return 1;
}
void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
(void)alpha_q16;
s_open_passes--;
if (keep) s_kept++;
}
uint32_t gl_renderer_pass_leaks(void) { return 0; }
int gl_renderer_pass_verify_vram(void) { return 1; }
void gl_renderer_pass_note_cost(uint64_t t) { (void)t; }
void gl_renderer_pass_service_presents(void) {}
/* ---- 1. store policy --------------------------------------------------- */
static void test_store_policy(void) {
RenderPassStoreTarget t = target();
int calls;
memset(s_ram, 0, sizeof s_ram);
memset(s_spad, 0, sizeof s_spad);
s_mmio_calls = 0;
CHECK(render_pass_store_to(&t, 0x80001000u, 0x11223344u, 4) == -1 &&
s_ram[0x1000] == 0x44 && s_ram[0x1003] == 0x11, "KSEG0 RAM word written");
CHECK(render_pass_store_to(&t, 0xA0001004u, 0xBEEFu, 2) == -1 &&
s_ram[0x1004] == 0xEF && s_ram[0x1005] == 0xBE, "KSEG1 RAM half written");
CHECK(render_pass_store_to(&t, 0x00201008u, 0x5Au, 1) == -1 &&
s_ram[0x1008] == 0x5A, "RAM mirror folds onto main RAM");
CHECK(render_pass_store_to(&t, 0x1F800010u, 0xCAFEF00Du, 4) == -1 &&
s_spad[0x10] == 0x0D && s_spad[0x13] == 0xCA, "scratchpad written");
CHECK(s_mmio_calls == 0, "memory stores never touch MMIO");
/* Dropped, per class, and never delivered. */
CHECK(render_pass_store_to(&t, 0x1F801D88u, 0xFFFFu, 2) == RENDER_PASS_DROP_SPU,
"SPU key-on dropped (SPU)");
CHECK(render_pass_store_to(&t, 0x1F801C00u, 0x3FFFu, 2) == RENDER_PASS_DROP_SPU,
"SPU voice volume dropped (SPU)");
CHECK(render_pass_store_to(&t, 0x1F801801u, 0x1Bu, 1) == RENDER_PASS_DROP_CD,
"CD command dropped (CD)");
CHECK(render_pass_store_to(&t, 0x1F801104u, 0x58u, 4) == RENDER_PASS_DROP_TIMER,
"timer mode dropped (TIMER)");
CHECK(render_pass_store_to(&t, 0x1F801128u, 100u, 2) == RENDER_PASS_DROP_TIMER,
"timer target dropped (TIMER)");
CHECK(render_pass_store_to(&t, 0x1F8010C0u, 0x80100000u, 4) == RENDER_PASS_DROP_DMA,
"SPU DMA MADR dropped (DMA)");
CHECK(render_pass_store_to(&t, 0x1F8010B8u, 0x11000000u, 4) == RENDER_PASS_DROP_DMA,
"CD DMA CHCR dropped (DMA)");
CHECK(render_pass_store_to(&t, 0x1F801814u, 0x00000000u, 4) == RENDER_PASS_DROP_GPU,
"GP1 reset dropped (GPU)");
CHECK(render_pass_store_to(&t, 0x1F801810u, 0xE1000000u, 2) == RENDER_PASS_DROP_GPU,
"a half-word GP0 store is dropped (GPU)");
CHECK(render_pass_store_to(&t, 0x1F801040u, 0x11u, 1) == RENDER_PASS_DROP_OTHER,
"SIO dropped (OTHER)");
CHECK(render_pass_store_to(&t, 0x1F801000u, 0x1F000000u, 4) == RENDER_PASS_DROP_OTHER,
"memory control dropped (OTHER)");
CHECK(render_pass_store_to(&t, 0x1F801820u, 0u, 4) == RENDER_PASS_DROP_OTHER,
"MDEC dropped (OTHER)");
CHECK(render_pass_store_to(&t, 0x1F000000u, 1u, 1) == RENDER_PASS_DROP_OTHER,
"expansion 1 dropped (OTHER)");
CHECK(render_pass_store_to(&t, 0xFFFE0130u, 0x804u, 4) == RENDER_PASS_DROP_OTHER,
"KSEG2 cache control dropped (OTHER)");
CHECK(s_mmio_calls == 0, "no dropped store reached a device");
/* Allowed: all restored after the pass. */
calls = s_mmio_calls;
CHECK(render_pass_store_to(&t, 0x1F801810u, 0x28FFFFFFu, 4) == -1, "GP0 word allowed");
CHECK(render_pass_store_to(&t, 0x1F801814u, 0x04000002u, 4) == -1, "GP1 DMA mode allowed");
CHECK(render_pass_store_to(&t, 0x1F801814u, 0x10000007u, 4) == -1, "GP1 info allowed");
CHECK(render_pass_store_to(&t, 0x1F801070u, 0xFFFFFFFEu, 4) == -1, "I_STAT ack allowed");
CHECK(render_pass_store_to(&t, 0x1F801074u, 0x0000000Du, 4) == -1, "I_MASK allowed");
CHECK(render_pass_store_to(&t, 0x1F8010A8u, 0x01000401u, 4) == -1, "GPU DMA allowed");
CHECK(render_pass_store_to(&t, 0x1F8010E8u, 0x11000002u, 4) == -1, "OTC DMA allowed");
CHECK(render_pass_store_to(&t, 0x1F8010F0u, 0x0F6F4321u, 4) == -1, "DPCR allowed");
CHECK(s_mmio_calls == calls + 8, "allowed stores reached their device");
/* Isolated cache: nothing but the I-cache would change. */
t.isolate_cache = 1;
s_ram[0x2000] = 0x77;
CHECK(render_pass_store_to(&t, 0x80002000u, 0u, 4) == -1 && s_ram[0x2000] == 0x77,
"IsC store absorbed, RAM untouched");
}
/* ---- 2. a real pass ------------------------------------------------------ */
static int pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
(void)user; (void)alpha_q16;
cpu->gpr[8] = 0xDEADu;
guest_store(0x80010000u, 0x12345678u, 4); /* RAM */
guest_store(0x1F800020u, 0x99u, 1); /* scratchpad */
guest_store(0x1F801D88u, 0x00FFu, 2); /* SPU key-on */
guest_store(0x1F801DAAu, 0xC000u, 2); /* SPU control */
guest_store(0x1F801801u, 0x0Au, 1); /* CD: Init */
guest_store(0x1F801124u, 0x0000u, 4); /* timer 2 mode */
guest_store(0x1F8010C8u, 0x01000201u, 4); /* SPU DMA CHCR */
guest_store(0x1F801814u, 0x03000001u, 4); /* GP1 display off */
guest_store(0x1F801044u, 0x0001u, 2); /* SIO */
guest_store(0x1F801810u, 0x02000000u, 4); /* GP0 fill: allowed */
guest_store(0x1F801070u, 0u, 4); /* ack every IRQ */
guest_store(0x1F801074u, 0u, 4); /* mask every IRQ */
for (int i = 0; i < 1000; i++) psx_advance_cycles(1000u); /* 10^6 */
/* A device-side change the store policy cannot see (as if some runtime
* path wrote a timer directly): the checkpoint must still undo it. */
timers_write(0x1F801100u, 0x1234u);
return 1;
}
static int never_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
(void)cpu; (void)user; (void)alpha_q16;
failures++;
fprintf(stderr, "FAIL: a refused pass ran its callback\n");
return 1;
}
static void test_pass(void) {
CPUState cpu;
PSXModRenderPass pass;
RenderPassStats st;
uint16_t tc0[3], tt0[3], tc1[3], tt1[3];
uint32_t tm0[3], tf0[3], tm1[3], tf1[3];
int32_t ti0[3], ti1[3];
uint8_t ram0[64], spad0[64];
uint64_t cyc0, dev0, next0;
uint32_t stat0, mask0;
int irqs0;
memset(&cpu, 0, sizeof cpu);
cpu.gpr[8] = 0x55u;
memset(s_ram, 0xA5, sizeof s_ram);
memset(s_spad, 0x3C, sizeof s_spad);
psx_cycles_resync_after_restore(NULL);
/* Live machine: timer 2 counts the system clock and interrupts on its
* target every 1000 cycles; timer 0 runs free. */
timers_init();
timers_write(0x1F801128u, 1000u);
timers_write(0x1F801124u, 0x0058u);
i_mask = 0x0040u; /* timer 2 unmasked */
i_stat = 0x0001u; /* a pending VBlank */
psx_advance_cycles(2500u);
CHECK(s_irqs_raised > 0, "sanity: timer 2 interrupts while time is live");
memset(&pass, 0, sizeof pass);
pass.struct_size = sizeof pass;
pass.w = 320; pass.h = 240;
pass.alpha_q16 = 32768u;
timers_get_snapshot(tc0, tm0, tt0, ti0, tf0);
memcpy(ram0, s_ram + 0x10000, sizeof ram0);
memcpy(spad0, s_spad, sizeof spad0);
cyc0 = psx_cycle_count;
next0 = psx_next_service_cycle;
dev0 = s_dev_cycles;
stat0 = i_stat;
mask0 = i_mask;
irqs0 = s_irqs_raised;
s_mmio_calls = 0;
CHECK(psx_mod_render_pass(&cpu, &pass, pass_fn, NULL) == 1, "the pass ran and was kept");
render_pass_get_stats(&st);
CHECK(st.passes == 1 && s_kept == 1 && s_open_passes == 0, "one image, pass closed");
CHECK(st.dropped[RENDER_PASS_DROP_SPU] == 2, "2 SPU stores dropped and counted");
CHECK(st.dropped[RENDER_PASS_DROP_CD] == 1, "1 CD store dropped and counted");
CHECK(st.dropped[RENDER_PASS_DROP_TIMER] == 1, "1 timer store dropped and counted");
CHECK(st.dropped[RENDER_PASS_DROP_DMA] == 1, "1 other-DMA store dropped and counted");
CHECK(st.dropped[RENDER_PASS_DROP_GPU] == 1, "1 GP1 store dropped and counted");
CHECK(st.dropped[RENDER_PASS_DROP_OTHER] == 1, "1 SIO store dropped and counted");
CHECK(s_mmio_calls == 3, "only GP0, I_STAT and I_MASK reached a device");
CHECK(s_irqs_raised == irqs0, "no interrupt raised in 10^6 frozen cycles");
CHECK(s_dev_cycles == dev0, "no device advanced");
CHECK(psx_cycle_count == cyc0 && psx_next_service_cycle == next0,
"guest clock and service deadline restored");
CHECK(i_stat == stat0 && i_mask == mask0, "I_STAT / I_MASK restored");
CHECK(s_irq_refresh > 0, "CAUSE IP2 recomputed after the restore");
timers_get_snapshot(tc1, tm1, tt1, ti1, tf1);
CHECK(memcmp(tc0, tc1, sizeof tc0) == 0 && memcmp(tm0, tm1, sizeof tm0) == 0 &&
memcmp(tt0, tt1, sizeof tt0) == 0 && memcmp(ti0, ti1, sizeof ti0) == 0 &&
memcmp(tf0, tf1, sizeof tf0) == 0, "timers exactly as before");
CHECK(memcmp(ram0, s_ram + 0x10000, sizeof ram0) == 0, "RAM restored");
CHECK(memcmp(spad0, s_spad, sizeof spad0) == 0, "scratchpad restored");
CHECK(cpu.gpr[8] == 0x55u, "CPU registers restored");
CHECK(!g_psx_render_pass_active, "time is live again");
/* Time is live again: the same cycles now move the timer and interrupt. */
psx_advance_cycles(2500u);
CHECK(s_irqs_raised > irqs0, "timer 2 interrupts again after the pass");
/* Status gates refuse without running the callback. */
s_fast_forward = 1;
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_FAST_FORWARD,
"fast-forward status");
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0,
"no pass during fast-forward");
s_fast_forward = 0;
s_gl_status = PSX_MOD_RENDER_PASS_BACKEND;
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_BACKEND,
"backend status");
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0,
"no pass while the backend declines");
s_gl_status = PSX_MOD_RENDER_PASS_READY;
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_READY, "ready again");
}
/* ---- 2b. 8 MiB main RAM live (psx.enhancement.8mb-ram) ------------------ */
static int s_retail_fold_seen;
static int pass8_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
(void)user; (void)alpha_q16;
cpu->gpr[9] = 0xBEEFu;
guest_store(0x80600000u, 0xCAFEBABEu, 4); /* unique above 2 MiB */
guest_store(0xA07FFFFCu, 0x01020304u, 4); /* last word of the map */
guest_store(0x80200010u, 0x55u, 1); /* no retail fold */
guest_store(0x80000100u, 0x66u, 1); /* low RAM as well */
return 1;
}
static int pass_retail_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
const uint8_t *before = (const uint8_t *)user;
(void)cpu; (void)alpha_q16;
guest_store(0x80600010u, 0x77u, 1); /* retail: folds onto 0x10 */
s_retail_fold_seen = s_ram[0x10] == 0x77 && s_ram[0x600010] == before[0x600010];
return 1;
}
static void test_ram_8mb(void) {
RenderPassStoreTarget t;
CPUState cpu;
PSXModRenderPass pass;
RenderPassStats st0, st1;
uint8_t *ram0 = (uint8_t *)malloc(RAM_8MB);
if (!ram0) { CHECK(0, "8 MiB test buffer"); return; }
/* Store policy with the expanded map: the window no longer mirrors. */
s_ram_live = RAM_8MB;
t = target();
memset(s_ram, 0, sizeof s_ram);
CHECK(render_pass_store_to(&t, 0x80600000u, 0x11223344u, 4) == -1 &&
s_ram[0x600000] == 0x44 && s_ram[0x600003] == 0x11 && s_ram[0] == 0,
"8 MiB: 0x80600000 is its own RAM, not a mirror of 0");
CHECK(render_pass_store_to(&t, 0x00201008u, 0x5Au, 1) == -1 &&
s_ram[0x201008] == 0x5A && s_ram[0x1008] == 0,
"8 MiB: 0x00201008 is not folded onto 0x1008");
CHECK(render_pass_store_to(&t, 0xA07FFFFEu, 0xBEEFu, 2) == -1 &&
s_ram[0x7FFFFE] == 0xEF && s_ram[0x7FFFFF] == 0xBE,
"8 MiB: the top of the window is RAM");
CHECK(render_pass_store_to(&t, 0x80800000u, 1u, 1) == RENDER_PASS_DROP_OTHER,
"8 MiB: past the decode window is not RAM");
/* A real pass: all 8 MiB come back, the upper 6 MiB included. */
for (uint32_t i = 0; i < RAM_8MB; i++)
s_ram[i] = (uint8_t)((i * 2654435761u) >> 24);
memcpy(ram0, s_ram, RAM_8MB);
memset(&cpu, 0, sizeof cpu);
memset(&pass, 0, sizeof pass);
pass.struct_size = sizeof pass;
pass.w = 320; pass.h = 240;
pass.alpha_q16 = 16384u;
render_pass_get_stats(&st0);
CHECK(psx_mod_render_pass(&cpu, &pass, pass8_fn, NULL) == 1,
"8 MiB: the pass ran and was kept");
render_pass_get_stats(&st1);
CHECK(st1.passes == st0.passes + 1, "8 MiB: one more pass");
CHECK(memcmp(ram0, s_ram, RAM_8MB) == 0,
"8 MiB: all of RAM restored, above 2 MiB too");
CHECK(cpu.gpr[9] == 0u, "8 MiB: CPU registers restored");
/* The next session without the mod (retail again): stores fold into the
* low 2 MiB and the restore covers them; the backing bytes above the
* live size are neither written nor needed. */
s_ram_live = RAM_2MB;
s_retail_fold_seen = 0;
CHECK(psx_mod_render_pass(&cpu, &pass, pass_retail_fn, ram0) == 1,
"retail after 8 MiB: the pass ran");
CHECK(s_retail_fold_seen, "retail after 8 MiB: 0x80600010 folded onto 0x10");
CHECK(memcmp(ram0, s_ram, RAM_8MB) == 0,
"retail after 8 MiB: RAM restored");
free(ram0);
}
/* ---- 3. VRAM journal ----------------------------------------------------- */
#define VW 1024
#define VH 512
static uint16_t s_vram[VW * VH], s_vram0[VW * VH];
static void paint(int x, int y, int w, int h, uint16_t v) {
for (int r = y; r < y + h; r++)
for (int c = x; c < x + w; c++) s_vram[r * VW + c] = v;
}
static void test_journal(void) {
RenderPassJournal j;
int x, y, w, h, n;
memset(&j, 0, sizeof j);
for (int i = 0; i < VW * VH; i++) s_vram[i] = (uint16_t)(i * 2654435761u >> 16);
memcpy(s_vram0, s_vram, sizeof s_vram);
/* The pass draws buffer 1 at (0, 240) 320x240. */
#define POLICY(X, Y, W, H, CAN) \
(x = (X), y = (Y), w = (W), h = (H), \
render_pass_vram_policy(&j, 0, 240, 320, 240, VW, VH, (CAN), &x, &y, &w, &h))
CHECK(POLICY(10, 250, 100, 100, 1) == RENDER_PASS_VRAM_ALLOW, "inside the rect: allowed");
paint(10, 250, 100, 100, 0x1111);
CHECK(POLICY(0, 0, 0, 5, 1) == RENDER_PASS_VRAM_ALLOW, "empty write: allowed");
CHECK(POLICY(640, 0, 64, 32, 1) == RENDER_PASS_VRAM_JOURNAL, "outside: journal");
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 0, "journaled as entry 0");
paint(640, 0, 64, 32, 0x2222);
CHECK(POLICY(650, 4, 16, 16, 1) == RENDER_PASS_VRAM_ALLOW, "covered: not journaled twice");
paint(650, 4, 16, 16, 0x3333);
CHECK(POLICY(300, 200, 40, 60, 1) == RENDER_PASS_VRAM_JOURNAL, "straddling the rect: journal");
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 1, "entry 1");
paint(300, 200, 40, 60, 0x4444);
/* Overlapping entry 0 partly: rolled back newest first. */
CHECK(POLICY(600, 16, 64, 32, 1) == RENDER_PASS_VRAM_JOURNAL, "overlapping: journal");
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 2, "entry 2");
paint(600, 16, 64, 32, 0x5555);
/* Clipped to VRAM. */
CHECK(POLICY(-8, -8, 24, 24, 1) == RENDER_PASS_VRAM_JOURNAL && x == 0 && y == 0 &&
w == 16 && h == 16, "negative origin clipped");
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 3, "entry 3");
paint(x, y, w, h, 0x6666);
CHECK(POLICY(1016, 508, 32, 32, 1) == RENDER_PASS_VRAM_JOURNAL && w == 8 && h == 4,
"far edge clipped");
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == 4, "entry 4");
paint(x, y, w, h, 0x7777);
CHECK(POLICY(800, 300, 8, 8, 0) == RENDER_PASS_VRAM_REFUSE,
"a mode that cannot journal refuses");
n = j.n;
while (j.n < RENDER_PASS_JOURNAL_MAX) {
int k = j.n;
CHECK(POLICY(400 + 8 * k, 100, 4, 4, 1) == RENDER_PASS_VRAM_JOURNAL, "fill journal");
render_pass_journal_add(&j, s_vram, VW, x, y, w, h);
paint(x, y, w, h, (uint16_t)(0x8000 + k));
}
CHECK(j.n == RENDER_PASS_JOURNAL_MAX && n == 5, "journal full");
CHECK(POLICY(900, 400, 4, 4, 1) == RENDER_PASS_VRAM_REFUSE, "full journal refuses");
CHECK(render_pass_journal_add(&j, s_vram, VW, 900, 400, 4, 4) == -1,
"add refuses past the limit");
/* End of pass: roll back, then the pass rect itself is restored by the
* renderer's own backup; outside it VRAM must equal the original. */
render_pass_journal_rollback(&j, s_vram, VW);
CHECK(j.n == 0, "journal emptied");
paint(0, 240, 320, 240, 0);
{
int bad = 0;
uint16_t *a = s_vram, *b = s_vram0;
for (int r = 0; r < VH; r++)
for (int c = 0; c < VW; c++) {
if (r >= 240 && r < 480 && c < 320) continue;
if (a[r * VW + c] != b[r * VW + c]) bad++;
}
CHECK(bad == 0, "every out-of-rect write rolled back exactly");
}
render_pass_journal_free(&j);
#undef POLICY
}
int main(void) {
test_store_policy();
test_pass();
test_ram_8mb();
test_journal();
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}