mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
212 lines
12 KiB
C
212 lines
12 KiB
C
#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);
|
|
/* HOLD presents one native phase image without a temporal crossfade. */
|
|
int render_pass_gen_select_mode(const uint32_t *phases, uint32_t n, double p,
|
|
int hold, uint32_t *lo, uint32_t *hi, float *t);
|
|
|
|
/* Is the flip the FLIP source just saw the one a pending generation waits
|
|
* for? A generation built for the next flip (`shown` 0, the canonical PsyQ
|
|
* VSync-then-PutDispEnv point) waits for the display to flip to its own rect.
|
|
* One built for a frame already on screen (`shown` 1: the game flips each
|
|
* frame as soon as it is drawn) waits for the next flip, to any other rect;
|
|
* its images then show one game frame late. Either way the presented
|
|
* geometry must still be the one the generation was captured at. */
|
|
int render_pass_gen_flip_matches(int shown, int gen_x, int gen_y,
|
|
int gen_source, int gen_w, int gen_h,
|
|
int flip_x, int flip_y, int flip_source,
|
|
int flip_w, int flip_h);
|
|
|
|
/* 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. */
|
|
/* Jersey Devil streams more than 16 small texture/CLUT rectangles in a draw.
|
|
* Keep the transaction bounded, but allow the complete set to roll back. */
|
|
#define RENDER_PASS_JOURNAL_MAX 64
|
|
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);
|
|
|
|
/* A published stereo pair may stay on screen only while the plugin keeps
|
|
* submitting. The pair is fresh while the guest clock is within
|
|
* RENDER_PASS_STEREO_MAX_AGE_VBLANKS vblanks after the pair's capture cycle
|
|
* (3x the 8-vblank maximum draw cadence). A clock behind the pair (savestate
|
|
* or rollback load) is stale. */
|
|
#define RENDER_PASS_STEREO_MAX_AGE_VBLANKS 24u
|
|
int render_pass_stereo_pair_fresh(uint64_t pair_cycle, uint64_t now_cycle,
|
|
uint32_t vblank_cycles);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif
|