Files
ARMSX1/psx/perf.h

313 lines
17 KiB
C

#ifndef PSX_PERF_H
#define PSX_PERF_H
/*
Per-frame work counters for a host performance overlay.
WHY THIS EXISTS
---------------
The Android front-end draws its OSD in Jetpack Compose; there is no imgui/FSUI left in
the core to draw one natively, and no way for the UI thread to ask a running machine
what it just did. These counters are the answer: the emulation thread bumps them in
place as work happens, frontend/main.cpp drains them once per frame, and the numbers
reach Kotlin through psxe_host_stats() -> NativeApp -> com.armsx2.ui.GameOsd.
WHAT IS COUNTED, AND WHAT IS NOT
--------------------------------
Only things the core genuinely does. Every field below is an exact event count taken at
the site that performs the work — there is no sampling, no estimation and no modelling.
Notably absent is a per-subsystem TIME split: this core interleaves the R3000A, the GPU
rasteriser and the devices at instruction granularity (psx_update() runs ~565k times a
frame), so timing them apart would cost far more than the work being measured. Host wall
time is measured instead where it is cheap and meaningful — around whole frame phases,
in frontend/main.cpp.
The software rasteriser is the one place where a count IS a time: every rasteriser in
psx/dev/gpu.c is a bounding-box loop, so the box area is exactly the number of inner-loop
iterations the primitive will cost. gpu_raster_pixels is therefore a precise measure of
rasteriser work, obtained for free from values the rasteriser already computed.
COST WHEN DISABLED
------------------
Every macro is wrapped in `if (g_psx_perf_enabled)`. Disabled, a counter site is one load
of a hot global plus a perfectly-predicted branch. Nothing here is placed inside a
per-instruction or per-pixel loop — the two paths that run millions of times a frame
(psx_cpu_cycle and the rasteriser inner loops) are deliberately untouched, and their
numbers are derived once per frame from state the core already maintains.
THREADING
---------
Written only from the emulation thread. psx_perf_take_frame() is likewise emulation-thread
only (frontend/main.cpp calls it at the frame boundary); the host-visible snapshot it
produces is what crosses threads, published as atomics by the frontend.
*/
#include <stdint.h>
enum {
PSX_PERF_PRIM_TRIANGLE = 0,
PSX_PERF_PRIM_RECT,
PSX_PERF_PRIM_LINE,
PSX_PERF_PRIM_KINDS
};
/* Indices match the DMA channel numbers in the hardware (DMA0..DMA6). */
enum {
PSX_PERF_DMA_MDEC_IN = 0,
PSX_PERF_DMA_MDEC_OUT,
PSX_PERF_DMA_GPU,
PSX_PERF_DMA_CDROM,
PSX_PERF_DMA_SPU,
PSX_PERF_DMA_PIO,
PSX_PERF_DMA_OTC,
PSX_PERF_DMA_CHANNELS
};
typedef struct {
/* R3000A. Both are read once per frame from state the CPU already keeps, so the
interpreter's inner loop carries no instrumentation at all. */
uint64_t cpu_instructions;
uint64_t cpu_cycles;
/* Geometry coprocessor: one count per executed GTE operation, across all three
dispatch paths (interpreter, cached interpreter, IRQ fast path). */
uint64_t gte_ops;
/* Rasteriser. gpu_raster_pixels is the exact inner-loop iteration count (see above),
gpu_vram_words counts halfwords moved by the VRAM transfer/fill/copy commands. */
uint32_t gpu_primitives[PSX_PERF_PRIM_KINDS];
uint64_t gpu_raster_pixels;
uint64_t gpu_vram_words;
/* SPU. Voices are accumulated per generated sample; the host divides to get the mean
number of voices that were actually playing over the frame. */
uint64_t spu_samples;
uint64_t spu_voice_samples;
/* MDEC: macroblock decode calls, and the 8x8 blocks run through the IDCT inside them. */
uint32_t mdec_macroblocks;
uint32_t mdec_blocks;
/* Sectors pulled off the disc image, whatever the container (cue/bin, CHD, raw). */
uint32_t cdrom_sectors;
uint64_t dma_words[PSX_PERF_DMA_CHANNELS];
} psx_perf_counters_t;
extern int g_psx_perf_enabled;
extern psx_perf_counters_t g_psx_perf;
/* Arming also zeroes the counters, so the first frame after the overlay is switched on is
not a partial one. Safe to call redundantly. */
void psx_perf_set_enabled(int enabled);
/* Copy the live counters into [out] and reset them for the next frame. Emulation thread. */
void psx_perf_take_frame(psx_perf_counters_t* out);
#define PSX_PERF_ADD(field, n) \
do { if (g_psx_perf_enabled) g_psx_perf.field += (uint64_t)(n); } while (0)
#define PSX_PERF_INC(field) PSX_PERF_ADD(field, 1)
/* One call per primitive, placed after the rasteriser has clamped its bounding box (and
after any degenerate-primitive bail-out) so the area recorded is the work really done. */
#define PSX_PERF_RASTER(kind, w, h) \
do { \
if (g_psx_perf_enabled) { \
int _pw = (int)(w), _ph = (int)(h); \
++g_psx_perf.gpu_primitives[(kind)]; \
if (_pw > 0 && _ph > 0) \
g_psx_perf.gpu_raster_pixels += (uint64_t)_pw * (uint64_t)_ph; \
} \
} while (0)
/* ---- audio_diag: one-shot audio capture -------------------------------------------------
A player reporting "the audio is broken" cannot tell apart four things that need opposite
fixes: the emulator not keeping up (host underruns), the XA decoder producing the wrong
stream, the mixer being at the wrong level, and the SPU reverb feedback loop railing. All
four sound bad. This block is what separates them in ONE run.
Armed by a marker file named `audio_diag` next to armsx.log; frontend/main.cpp polls for
it, deletes it, opens a bounded capture window and writes `audio_diag.txt`. Same mechanism
as `gpu_prim_dump`, `perf_log` and gl_debug_marker().
COST WHEN DISABLED
------------------
Every write site is inside `if (g_psx_audio_diag_enabled)` — one load of a hot global and a
perfectly-predicted branch. The hottest site is psx_spu_get_sample(), which runs 44100 times
a second, i.e. ~0.07% of the rate of the paths perf.h already refuses to touch. NOTHING here
is per-CPU-instruction or per-pixel, and the XA sub-header ring is written once per 2352-byte
sector (~19 times a second), never per sample.
PEAKS ARE PRE-CLAMP WHERE IT MATTERS
------------------------------------
dry_peak / revin_peak are the raw accumulator values BEFORE psx_spu_get_sample()'s int16
clamp, so "how far past full scale is this" is answerable rather than being flattened to
32767. Inside spu_get_reverb_sample() the values are read back AFTER the existing SAT()
macros on purpose: hoisting those expressions to see them pre-clamp would have meant editing
the reverb arithmetic while investigating the reverb, and a value pinned at the rail is the
same evidence. `*_railed` counts samples landing within 68 of full scale — chance alone puts
a sample there about 1 time in 240, so a railed loop is unmistakable against the sample count.
*/
#define PSX_AUDIO_DIAG_XA_HDRS 32
#define PSX_AUDIO_DIAG_CD_CMDS 24
/* Why every sector decision is recorded, not just the accepted ones: "the music plays but the
dialogue does not" is a statement about TWO XA streams in one track, and it can only be
answered by seeing the fetcher's verdict on each sector next to the filter that was in force
when it decided. An accept-only log cannot distinguish "the dialogue channel never arrived"
from "it arrived and was rejected" from "the walk to find it was aborted on the way". */
enum {
PSX_XA_VERDICT_ACCEPT_NOFILTER = 0, /* audio sector, MODE_XA_FILTER off */
PSX_XA_VERDICT_ACCEPT_MATCH, /* audio sector, file+channel both matched */
PSX_XA_VERDICT_REJECT_FILTER, /* audio sector, file/channel mismatch -> keep walking */
PSX_XA_VERDICT_SKIP_NONAUDIO, /* submode bit2 clear -> keep walking */
PSX_XA_VERDICT_STOP_EOR, /* submode bit0 set -> XA playback abandoned entirely */
PSX_XA_VERDICT_KINDS
};
typedef struct {
/* --- SPU mixer, per generated sample --------------------------------------------- */
uint32_t spu_samples; /* psx_spu_get_sample() calls inside the window */
uint32_t spu_silent; /* returns of 0 because SPUCNT bit15 (enable) is clear */
uint32_t spu_voices_peak; /* most voices sounding in any single sample */
uint64_t spu_voices_sum; /* / spu_samples = mean voices sounding */
int32_t dry_peak_l, dry_peak_r; /* |voice sum| BEFORE the int16 clamp */
uint32_t dry_clip; /* samples where that clamp engaged */
int32_t revin_peak_l, revin_peak_r; /* |EON voice sum| entering the reverb, pre-clamp */
uint32_t revin_clip;
uint32_t revsum_clip; /* dry+wet clamp at the SPU output */
int32_t mainvol_l, mainvol_r; /* raw 1F801D80/D82 as LAST seen by the mixer */
/* ...and the smallest seen anywhere in the window. The last-value-only fields above cannot
tell a constant main volume from one that dipped and recovered inside the period, which
is the one thing that could make an output peak smaller than the gain predicts. Set to
0xFFFFFFFF when a window opens so the first sample always wins. */
uint32_t mainvol_min_l, mainvol_min_r;
/* --- reverb network (spu_get_reverb_sample) --------------------------------------- */
uint32_t rev_calls; /* 22050 Hz while reverb is on: 0 means it never ran */
uint32_t revfb_railed; /* mLSAME/mRSAME writes at the rail: a saturating loop */
uint32_t revout_railed; /* reverb output at the rail */
int32_t revout_peak_l, revout_peak_r;
/* --- SPU RAM transfer path (CD data -> SPU RAM -> a voice) -------------------------
The route streamed speech and music actually take. spu_ram_writes == 0 while voices are
keyed on and sounding means the sample data is never arriving and the voices are looping
over whatever was already there. spu_fifo_drop > 0 means the 32-entry transfer FIFO was
overrun — which before the bound in spu.c was a write straight through tfifo_index and
into the voice decoder states. */
uint32_t spu_ram_writes;
uint32_t spu_fifo_drop;
/* Sound RAM IRQs raised. Zero while SPUCNT bit6 is set means the game armed the interrupt
it uses to time streaming refills and never received one.
Rate matters as much as presence. A streaming game arms SPUIRQA on a point its own
metronome voice crosses at a fixed rate, and that rate is a property of the voice, not
of the video output — so spu_irq_raised landing at or just under the frame rate is the
signature of interrupts being quantised to the frame rather than delivered when the
voice actually got there. See the block comment above psx_spu_tick(). */
uint32_t spu_irq_raised;
/* Where the frame's samples were produced. ticked = spread across the frame by
psx_spu_tick() on the CPU's timeline, which is what lets the guest service an SPU
interrupt between two samples. inline = generated by the front-end in a lump at the
frame boundary with the CPU stopped, which cannot deliver an interrupt at all. inline
should be a small remainder; inline >> ticked means the SPU is effectively back on the
old burst model and any streaming game will starve. */
uint32_t spu_gen_ticked;
uint32_t spu_gen_inline;
uint32_t spu_ram_lo; /* seeded to 0xFFFFFFFF when a window opens */
uint32_t spu_ram_hi;
/* Per-voice contribution, post-decode and pre-mix. Indexed by voice number. */
int32_t voice_peak_l[24];
int32_t voice_peak_r[24];
/* Streaming behaviour: the last ADPCM block flags seen (bit0 loop-end, bit1 loop-repeat,
bit2 loop-start), how many times the voice looped back to its repeat address, and how
many times it was stopped by an end-without-repeat block. A stream that dies shows
voice_stop going 0 -> 1 at the instant the audio cuts out. */
uint8_t voice_flags[24];
uint32_t voice_loopend[24];
uint32_t voice_stop[24];
/* --- voice volume register formats seen at key-on ---------------------------------- */
uint32_t kon_voices;
uint32_t kon_vol_negative; /* bit14 set, bit15 clear: a NEGATIVE 15-bit volume */
uint32_t kon_vol_sweep; /* bit15 set: sweep mode */
/* --- CD / XA ---------------------------------------------------------------------- */
uint32_t xa_sectors; /* sub-headers accepted and decoded */
uint32_t xa_filter_rejects; /* skipped because file/channel did not match Setfilter */
uint32_t xa_skip_nonaudio; /* walked past because the submode audio bit was clear */
uint32_t xa_stop_eor; /* fetch abandoned on submode bit0 */
uint32_t xa_stop_far; /* fetch abandoned because the read ran off the disc */
uint32_t xa_starved; /* cdrom_get_xa_samples() gave up mid-buffer */
uint32_t xa_walk_peak; /* most sectors walked to satisfy one fetch */
int32_t xa_peak_l, xa_peak_r; /* |XA| as written into the host buffer, post-volume */
uint32_t cdda_sectors;
/* Ring of the most recent XA sector DECISIONS — accepted, rejected and stopped alike.
Drained and reset by the host each snapshot, so the file shows how the stream evolved
rather than one instant. */
uint32_t xa_hdr_seen; /* pushed since the last drain (may exceed the ring) */
uint32_t xa_hdr_head;
struct {
uint32_t lba;
uint8_t file;
uint8_t chan;
uint8_t submode; /* 0x12: bit0 EOR, 2 audio, 3 data, 5 form2, 7 EOF */
uint8_t coding; /* 0x13: bit0 stereo, bit2 18.9kHz, bit4 8bit, bit6 emph */
/* The filter in force at the moment of the verdict. Recorded per entry because
Setfilter can be reissued between sectors, and a stale filter is one of the
candidate explanations for a stream that goes quiet. */
uint8_t filter_file;
uint8_t filter_chan;
uint8_t filter_on; /* MODE_XA_FILTER at that instant */
uint8_t verdict; /* PSX_XA_VERDICT_* */
} xa_hdr[PSX_AUDIO_DIAG_XA_HDRS];
/* --- CD-ROM command log ------------------------------------------------------------
"XA never runs" has exactly two explanations and they need opposite fixes: the game
never asked for it, or it asked and we dropped the request. Only the command stream
tells them apart, so every command is logged with its parameters AND the drive state it
found — a Setmode arriving while read_ongoing=1 is a different event from one arriving
at idle, because cdrom_cmd_setmode() calls cdrom_pause(). Commands are rare (tens per
second at most), so this is nowhere near a hot path. */
uint32_t cd_cmd_seen;
uint32_t cd_cmd_head;
struct {
uint8_t cmd;
uint8_t nparams;
uint8_t param[4];
uint8_t mode_before; /* cdrom->mode as it stood when the command arrived */
uint8_t xa_playing; /* drive state at that instant, before execution */
uint8_t read_ongoing;
uint8_t state;
} cd_cmd[PSX_AUDIO_DIAG_CD_CMDS];
/* --- host mixer (frontend/main.cpp MixPsxAudio) ------------------------------------ */
uint32_t mix_wrap; /* CD+SPU sums that left int16 range */
uint32_t mix_sat; /* ...and were saturated instead of wrapping */
int32_t mix_peak_l, mix_peak_r;
} psx_audio_diag_t;
extern int g_psx_audio_diag_enabled;
extern psx_audio_diag_t g_psx_audio_diag;
/* Arming zeroes the block, so a window never carries counts in from before it opened. */
void psx_audio_diag_set_enabled(int enabled);
/* max(dst, |v|), for the peak fields above. */
#define PSX_AUDIO_DIAG_PEAK(dst, v) \
do { \
int32_t _av = (int32_t)(v); \
if (_av < 0) _av = -_av; \
if (_av > (dst)) (dst) = _av; \
} while (0)
/* Within 68 of int16 full scale in either direction. */
#define PSX_AUDIO_DIAG_RAILED(v) (((v) >= 32700) || ((v) <= -32700))
#endif