mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
docs: render passes (API, sandbox, presenter, budget, verification)
docs/RENDER_PASSES.md; FRAME_RATE.md gains HOLD and a link; TCP_COMMANDS.md describes gl_interp, render_pass_stats and render_pass_dump and regenerates the command index (which also picks up three commands master had not indexed yet). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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,108 @@
|
||||
# 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);
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
## 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 |
|
||||
| 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 | `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 are journaled and rolled back (refused under native-wide) | `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,
|
||||
2 MiB RAM, 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.
|
||||
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, on anything but
|
||||
the OpenGL renderer with FLIP-source interpolation, while the presenter is
|
||||
suspended (FMV), and when no VBlank was presented since the last plan (debug
|
||||
turbo, headless).
|
||||
|
||||
## 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.
|
||||
|
||||
Passes cost host time inside the game's frame. 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. Nothing ever slows the guest down: when the budget
|
||||
runs out, fewer passes are rendered.
|
||||
|
||||
Pass images are kept at internal resolution (the size the presenter
|
||||
captures); two generations fit a 256 MiB budget, which limits passes per
|
||||
frame at very high internal resolutions.
|
||||
|
||||
## 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.
|
||||
- `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; `verify_mismatch` must stay 0.
|
||||
- `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: `render_pass_plan_test` and `render_pass_freeze_test` (runtime ctest),
|
||||
`render_pass_guards` (source guard, recompiler ctest).
|
||||
@@ -53,6 +53,9 @@ 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, dropped device stores by class, `verify_mismatch` under `PSX_RENDER_PASS_VERIFY=1`, host-time split per pass, presents made from pass images |
|
||||
| `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` |
|
||||
| `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 |
|
||||
@@ -336,9 +339,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`).
|
||||
**318 commands registered** — 305 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
54 of 316 have prose above; **262 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.
|
||||
57 of 318 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.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -464,7 +467,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` | ✓ | | |
|
||||
@@ -553,6 +556,8 @@ 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_stats` | ✓ | | ✓ |
|
||||
| `restore_trace` | ✓ | | |
|
||||
| `restore_trace_clear` | ✓ | | |
|
||||
| `restore_trace_window` | ✓ | | |
|
||||
|
||||
Reference in New Issue
Block a user