Files
2026-10-23 03:22:42 +08:00

244 lines
13 KiB
C

#ifndef PSX_FRAME_TRACE_H
#define PSX_FRAME_TRACE_H
/* Opt-in, fixed-capacity frame-time tracing for the stereo/OpenXR path.
*
* WHY THIS EXISTS
* ---------------
* VR stutter reports ("it hitches when enemies spawn", "fast head turns judder",
* "one heavy frame causes a run of stutters") could not be attributed. The
* renderer already smoothed a few averages, but only for a handful of stages,
* they were never reset, and there was no per-frame history to correlate a
* visible hitch with. This module is the measurement floor the VR optimization
* program is built on: one record per stereo frame, monotonic, allocation-free
* on the hot path, exported only on demand.
*
* DESIGN CONTRACT
* ---------------
* * DISABLED BY DEFAULT (PSX_FRAME_TRACE=0). Every entry point returns
* immediately while disabled, so an instrumented build costs one branch.
* * NO per-frame heap allocation and NO file I/O on the record path. The ring
* is static; the record holds fixed-size fields only. Export writes into a
* caller buffer, outside the frame.
* * Stages are supplied by the caller in MICROSECONDS, measured with whatever
* host clock it already uses. The module never reads a clock for durations,
* only for the record's open timestamp, through one injectable function
* pointer. That keeps the module dependency-free and unit-testable.
* * CPU submission time is NOT GPU execution time. Stages named "xr_*" are
* wall-clock waits; nothing here claims GPU completion.
* * Emulation/main thread only, like the OpenXR lifecycle it observes.
*/
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Per-frame stages, in the order one stereo frame produces them. The header
* order IS the ABI; keep frame_trace.c's name table in sync. */
enum {
PSX_TRACE_GUEST_SIM = 0, /* guest work outside the stereo pair */
PSX_TRACE_PAIR_TOTAL, /* both eyes, pair open to publish */
PSX_TRACE_EYE_BEGIN_L, /* VRAM backup + checkpoint */
PSX_TRACE_EYE_GUEST_L, /* the plugin's guest draw */
PSX_TRACE_EYE_END_L, /* GPU flush, capture, VRAM restore */
PSX_TRACE_EYE_RESTORE_L, /* machine state restore */
PSX_TRACE_EYE_BEGIN_R,
PSX_TRACE_EYE_GUEST_R,
PSX_TRACE_EYE_END_R,
PSX_TRACE_EYE_RESTORE_R,
PSX_TRACE_XR_WAIT_FRAME, /* xrWaitFrame */
PSX_TRACE_XR_BEGIN_FRAME, /* xrBeginFrame */
PSX_TRACE_XR_ACQUIRE, /* swapchain acquire (both eyes) */
PSX_TRACE_XR_WAIT_IMAGE, /* xrWaitSwapchainImage (both eyes) */
PSX_TRACE_XR_COPY, /* eye blit into the runtime image */
PSX_TRACE_XR_RELEASE, /* swapchain release (both eyes) */
PSX_TRACE_XR_END_FRAME, /* xrEndFrame */
/* How old the submitted pose was: xrLocateViews return to xrEndFrame
* return. This is the pose the compositor reprojects from, so it bounds
* the correction a head turn can need. */
PSX_TRACE_POSE_AGE,
/* Host time a HUD capture transaction costs (psx_mod_render_capture_rgba).
* The wrist HUD captures the same guest slice twice per frame, once per
* matte colour, and both are full render transactions on the frame's own
* thread, so this is frame time the pair stats never showed. */
PSX_TRACE_HUD_CAPTURE,
/* The capture's two halves, so the cost can be aimed at: the transaction is
* the checkpoint plus the guest HUD draw plus its verify, the readback is
* the blit out of the pair target and the synchronous glReadPixels. */
PSX_TRACE_HUD_DRAW,
PSX_TRACE_HUD_READBACK,
/* The guest scene rebuild one eye replays, split by the call the game hook
* makes, so a hitch can be aimed at the guest call that grew instead of at
* the whole eye. These are PAIR totals: both eyes write the same slots.
* Written by the hook through psx_mod_frame_trace_stage_begin/end, which
* only accepts them inside a traced stereo eye.
* game_env draw environment, GP0 clear, ordering-table setup
* game_world the world/level passes
* game_text the generic text renderer
* game_traverse scene traversal: level groups and entities
* game_icon the HUD icon
* game_submit DrawOTag: walks the ordering table and submits it
* game_laser the hook's own per-eye raycast and line (host work)
* game_select the hook's own level visibility override (the LVTX
* entry filter): the eye-view AABB test and the leaf
* queue it writes. It runs INSIDE the guest traversal,
* and game_traverse is that call's wall time, so the
* guest's own traversal work is game_traverse minus
* game_select. Per eye and summed over the pair like the
* rest, and the only one that nests. */
PSX_TRACE_GAME_ENV,
PSX_TRACE_GAME_WORLD,
PSX_TRACE_GAME_TEXT,
PSX_TRACE_GAME_TRAVERSE,
PSX_TRACE_GAME_ICON,
PSX_TRACE_GAME_SUBMIT,
PSX_TRACE_GAME_LASER,
PSX_TRACE_GAME_SELECT,
PSX_TRACE_STAGE_COUNT
};
/* Record flags. A hitch is the interesting case, so absent conditions are
* explicit: a stale frame is one that published no new pair. */
enum {
PSX_FRAME_TRACE_STEREO_PUBLISHED = 1u << 0,
PSX_FRAME_TRACE_STALE_PAIR = 1u << 1,
PSX_FRAME_TRACE_SHED = 1u << 2,
PSX_FRAME_TRACE_FAILED = 1u << 3,
PSX_FRAME_TRACE_XR_EMPTY = 1u << 4,
PSX_FRAME_TRACE_SCENE_TRANSITION = 1u << 5,
PSX_FRAME_TRACE_IO_EVENT = 1u << 6,
PSX_FRAME_TRACE_NO_TELEMETRY = 1u << 7,
/* A stereo eye rendered from the pair's shared baseline instead of taking
* its own machine snapshot (see PR3's pair-scoped checkpoint reuse). */
PSX_FRAME_TRACE_BASELINE_REUSED = 1u << 8,
/* The XR frame carried no new pair, so the retained one was submitted (or
* nothing was, when resubmission is off). */
PSX_FRAME_TRACE_XR_REPEAT = 1u << 9
};
enum {
PSX_FRAME_TRACE_NOTE = 24, /* scene / IO tag */
PSX_FRAME_TRACE_REASON = 24 /* failure reason text */
};
typedef struct PSXFrameTraceRecord {
uint64_t seq; /* monotonic record id (starts at 1) */
uint64_t open_ms; /* host monotonic ms at open */
uint64_t guest_frame; /* guest frame counter, 0 if unknown */
uint64_t guest_cycle; /* guest cycle at open */
uint64_t stereo_attempt; /* stereo attempt id (0 if none) */
uint64_t pair_id; /* published pair id (0 if none) */
uint64_t pair_age_ms; /* ms since the last published pair (0 if none) */
uint32_t stages_us[PSX_TRACE_STAGE_COUNT];
uint32_t shed_skipped; /* pair attempts shed immediately before this frame */
uint32_t shed_streak_ms; /* how long that shed streak had lasted */
uint32_t prims; /* guest draw primitives (0 = not measured) */
uint32_t textured_prims; /* of which textured */
/* The same two counters per eye. `prims`/`textured_prims` above keep their
* meaning (the last eye of the pair wrote them); a change to what ONE eye
* selects needs these, because the pair's total hides which eye moved. */
uint32_t prims_l, prims_r;
uint32_t tex_prims_l, tex_prims_r;
/* Guest cycles each eye's replay executed, the machine work the eye cost in
* the emulated CPU rather than in host submission. 0 = not measured. */
uint32_t eye_cycles_l, eye_cycles_r;
uint32_t vram_leaks; /* GPU writes outside the pass rect this eye */
uint64_t tex_upload_bytes; /* texture bytes uploaded (0 = not measured) */
/* Microseconds between this submission's XrFrameState.predictedDisplayTime
* and the previous one, from the runtime's clock. Divided by the session's
* target_period_us this is how many granted display periods one presented
* pair consumed, which is the cadence the compositor actually saw. 0 on the
* first submission of a session or when there was no frame this record. */
uint32_t xr_display_delta_us;
uint32_t flags; /* PSX_FRAME_TRACE_* */
int32_t eye_failed; /* last eye that failed, -1 if none */
uint8_t source; /* 1 = stereo pair, 2 = native render */
uint8_t xr_submit_fresh; /* 1 = carried a new pair, 0 = repeat/none */
uint8_t reserved[2];
char note[PSX_FRAME_TRACE_NOTE];
char failure[PSX_FRAME_TRACE_REASON];
} PSXFrameTraceRecord;
/* Injectable monotonic millisecond clock. NULL (the default) records 0; the
* runtime installs psx_host_mono_ms(). Tests install a fake. */
typedef uint64_t (*PSXFrameTraceClock)(void);
void psx_frame_trace_set_clock(PSXFrameTraceClock fn);
/* Enables/disables capture. Disabling keeps the ring (read it, then reset).
* Reads PSX_FRAME_TRACE / PSX_FRAME_TRACE_CAP once. */
void psx_frame_trace_configure_from_env(void);
void psx_frame_trace_set_enabled(int on);
int psx_frame_trace_enabled(void);
/* Clears the ring, counters and any open record. */
void psx_frame_trace_reset(void);
/* Opens a record for the frame that is starting. Returns 1 if this call opened
* it, 0 if capture is off or a record is already open (the caller must then NOT
* commit). Cost is a branch while disabled. */
int psx_frame_trace_open(void);
/* Commits the open record, stamping pair age from the last published pair.
* No-op when nothing is open. */
void psx_frame_trace_commit(void);
/* Adds `us` microseconds to `stage` of the open record. Ignores unknown stages;
* saturates instead of wrapping. Cheap while closed. */
void psx_frame_trace_add(uint32_t stage, uint64_t us);
/* Per-frame annotations (open record). Each is a no-op when closed. */
void psx_frame_trace_ids(uint64_t guest_frame, uint64_t guest_cycle);
void psx_frame_trace_attempt(uint64_t attempt_id, uint32_t shed_skipped,
uint32_t shed_streak_ms);
void psx_frame_trace_result(int published, uint64_t pair_id, int eye_failed,
const char *failure);
void psx_frame_trace_source(uint8_t source);
/* Draw counters for `eye` (PSX_MOD_EYE_LEFT/0 or RIGHT/1). Also fills `prims`
* and `textured_prims`, which therefore remain the last eye's values. */
void psx_frame_trace_draw(int eye, uint32_t prims, uint32_t textured);
/* Guest cycles one eye's replay executed (the transaction's own delta). */
void psx_frame_trace_eye_cycles(int eye, uint32_t cycles);
void psx_frame_trace_gpu(uint64_t tex_upload_bytes, uint32_t vram_leaks);
/* XR cadence annotation (open record): the runtime's predicted-display-time
* delta for this submission and whether it carried a newly rendered pair. A
* repeat also sets PSX_FRAME_TRACE_XR_REPEAT. No-op when closed. */
void psx_frame_trace_xr_cadence(uint32_t display_delta_us, int submit_fresh);
void psx_frame_trace_flag(uint32_t flag);
void psx_frame_trace_note(const char *tag);
void psx_frame_trace_io(const char *tag); /* note + IO_EVENT flag */
/* Session-level metadata, emitted once in the export header rather than per
* record: the OpenXR runtime actually in use and the target display period in
* microseconds (0 = unknown, e.g. no headset). Without these a trace cannot be
* interpreted later, because the same pair time is over budget at 90 Hz and
* comfortably under it at 60 Hz. Change-checked internally, so calling it every
* frame is a strcmp; it is a no-op while capture is off. */
void psx_frame_trace_session(const char *runtime, uint32_t target_period_us);
/* Queries. `capacity` is the caller's array length; returns how many records
* were written (oldest first, at most the newest `capacity`). */
uint32_t psx_frame_trace_read(PSXFrameTraceRecord *out, uint32_t capacity);
uint32_t psx_frame_trace_capacity(void);
uint64_t psx_frame_trace_committed(void); /* records committed since reset */
uint64_t psx_frame_trace_dropped(void); /* records overwritten (ring wrap) */
uint64_t psx_frame_trace_missing(void); /* commits with no stage at all */
/* Exports. Each returns the number of bytes the full text needs (like
* snprintf); the buffer is always NUL-terminated when cap > 0. `max_records`
* (0 = all retained) bounds the newest records emitted. */
int psx_frame_trace_to_json(char *buf, int cap, uint32_t max_records);
int psx_frame_trace_to_csv(char *buf, int cap, uint32_t max_records);
/* Chrome trace-event JSON (Perfetto compatible): one span per stage. */
int psx_frame_trace_to_chrome_json(char *buf, int cap, uint32_t max_records);
const char *psx_frame_trace_stage_name(uint32_t stage);
/* Renders the set bits as "a|b|c" into buf; always NUL-terminated. */
const char *psx_frame_trace_flag_names(uint32_t flags, char *buf, int cap);
#ifdef __cplusplus
}
#endif
#endif /* PSX_FRAME_TRACE_H */