mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 18:52:11 -04:00
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:
+3
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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` | ✓ | | |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
+844
-17
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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); } \
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user