mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
VR: bound stereo shedding and recover at the display deadline
This commit is contained in:
@@ -16,9 +16,18 @@ that must be cleared after watchdog rollback.
|
||||
|
||||
`period_vblanks` describes the game's draw cadence (1..8). An EMA of complete-pair
|
||||
host cost is compared with 80% of that cadence, using the runtime's live NTSC/PAL
|
||||
period. Expensive work is shed as a complete pair; every thirtieth request retries
|
||||
to recover a stale estimate. This is a conservative cadence budget, not a measured
|
||||
headset idle budget. Allocating or slow initial pairs may exceed it; completed
|
||||
period, unless `PSX_STEREO_SHED_BUDGET_MS` supplies the real headset deadline (the
|
||||
VBlank cadence is a guest-budget fallback, not an OpenXR display deadline).
|
||||
Expensive work is shed as a complete pair, never as a single eye, and the previous
|
||||
complete pair is retained. Shedding is bounded both by wall clock
|
||||
(`PSX_STEREO_SHED_PROBE_MS`, default 250 ms) and by a consecutive count
|
||||
(`PSX_STEREO_SHED_MAX_STREAK`, default 8), so a stale estimate cannot suppress
|
||||
indefinitely: whichever bound trips first forces a probe, and the probe's
|
||||
measurement replaces the estimate rather than blending into it, so a single spike
|
||||
ends after one bounded streak. Recovery uses a hysteresis band
|
||||
(`PSX_STEREO_SHED_HYSTERESIS`, default 0.9) and scene/mode transitions reset the
|
||||
estimate. `PSX_STEREO_SHED=0` disables shedding entirely for a baseline
|
||||
diagnostic. Allocating or slow initial pairs may exceed the budget; completed
|
||||
guest work cannot be preempted by a host-time budget. Guest-cycle watchdogs remain
|
||||
the runaway bound.
|
||||
|
||||
@@ -59,8 +68,10 @@ sees it. `test_render_pose` covers the override together with `fov_scale`.
|
||||
TCP inspection:
|
||||
|
||||
- `stereo_stats`: pair outcomes, eye entry cycles/state hashes under VERIFY,
|
||||
sampled view offsets, latched failed attempt/eye/retained pair, and published
|
||||
pair metadata. `render_pass_stats` continues to
|
||||
sampled view offsets, latched failed attempt/eye/retained pair, published
|
||||
pair metadata, and the shedding policy (`shedding`, `shed_streak`,
|
||||
`shed_streak_ms`, `shed_streak_max`, `stale_frames`, `budget_resets`,
|
||||
`last_pair_age_ms`). `render_pass_stats` continues to
|
||||
expose shared transaction checks, aborts, leaks and dropped stores; its eye
|
||||
transactions also increment pass counters.
|
||||
- `stereo_dump path=<absolute-directory> count=N`: arm dumps of the next N
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Bound stereo shedding and recover at the display deadline
|
||||
|
||||
Source: [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp).
|
||||
- [cae62891](https://github.com/FractalEngineer/psxrecomp/commit/cae62891e9236e9252e43b80aa9dd47f7554aea4)
|
||||
- [b6a671c1](https://github.com/FractalEngineer/psxrecomp/commit/b6a671c1db4ba15d02369ce5561754d485881a55)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>; retained as commit author
|
||||
and Co-authored-by on this reconstructed port.
|
||||
|
||||
Adapted: this feature and its tests. Excluded: other VR features, game policy,
|
||||
assets and chronological investigation notes.
|
||||
Base: `8400b55dc02d3fa787df1280a9fb474901adfcf4` (current upstream master or the preceding feature).
|
||||
|
||||
Validation: Windows/GCC 13.2 Release: policy, sandbox and abort tests pass (3/3). Sustained overload remains expensive; the MoH launcher disables shedding by default.
|
||||
|
||||
Consumer: [Medal of Honor v0.1.6](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/1957e28a5dc7f5daa79e2b9b6ed867eb06b41ace).
|
||||
Requires a framework pin bump and rebuild; no game/BIOS regeneration.
|
||||
@@ -1638,6 +1638,12 @@ if(BUILD_TESTING)
|
||||
add_executable(frame_trace_test tests/test_frame_trace.c src/frame_trace.c)
|
||||
target_include_directories(frame_trace_test PRIVATE include)
|
||||
add_test(NAME frame_trace_test COMMAND frame_trace_test)
|
||||
# Stereo shedding/recovery policy (docs/STEREO_RENDERING.md): bounded streaks,
|
||||
# authoritative probes, hysteresis, pair age separate from the estimate.
|
||||
add_executable(stereo_shed_policy_test tests/test_stereo_shed_policy.c
|
||||
src/stereo_shed_policy.c)
|
||||
target_include_directories(stereo_shed_policy_test PRIVATE include)
|
||||
add_test(NAME stereo_shed_policy_test COMMAND stereo_shed_policy_test)
|
||||
# Render thread core ([video] render_thread, docs/RENDER_THREAD.md):
|
||||
# ordering across ring wraps, the frame bound, stale-frame detection and
|
||||
# the drain + context hand-off sync point, against a fake executor.
|
||||
@@ -1784,6 +1790,7 @@ if(BUILD_TESTING)
|
||||
tests/test_render_pass_abort.c
|
||||
src/render_pass.c
|
||||
src/frame_trace.c
|
||||
src/stereo_shed_policy.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)
|
||||
@@ -1796,6 +1803,7 @@ if(BUILD_TESTING)
|
||||
tests/test_render_pass_sandbox.c
|
||||
src/render_pass.c
|
||||
src/frame_trace.c
|
||||
src/stereo_shed_policy.c
|
||||
src/render_pass_plan.c
|
||||
src/psx_cycles.c
|
||||
src/timers.c
|
||||
|
||||
@@ -752,6 +752,16 @@ void psx_mod_frame_trace_note(const char *tag);
|
||||
* inside the guest traversal it is part of (game_select in game_traverse). */
|
||||
void psx_mod_frame_trace_stage_begin(uint32_t stage);
|
||||
void psx_mod_frame_trace_stage_end(uint32_t stage);
|
||||
/* Drop the stereo shedding policy's pair-time estimate and derived budget. Call
|
||||
* on a scene/mode transition so a new scene does not inherit the previous one's
|
||||
* timing; cheap and idempotent. Shedding itself is unchanged. */
|
||||
void psx_mod_stereo_reset_budget(void);
|
||||
/* Set the stereo pair budget from a real headset deadline in milliseconds,
|
||||
* pushed by the OpenXR layer from the target display period (11.1 ms at 90 Hz,
|
||||
* 8.3 ms at 120 Hz). 0 clears it. The guest-VBlank-derived value is only a
|
||||
* fallback: a PSX redraw budget is not an OpenXR display deadline, and the two
|
||||
* disagree on every non-60 Hz headset. PSX_STEREO_SHED_BUDGET_MS still wins. */
|
||||
void psx_mod_stereo_set_budget_ms(double ms);
|
||||
/* 0 disables stereo output; 1 shows a complete pair side by side. Capture
|
||||
* itself does not enable presentation. Default 0; applies to this session. */
|
||||
int psx_mod_set_stereo_presentation(uint32_t mode);
|
||||
|
||||
@@ -91,6 +91,17 @@ typedef struct RenderStereoStats {
|
||||
uint64_t last_failed_attempt, retained_pair_id;
|
||||
int last_failed_eye;
|
||||
const char *last_failed_reason; /* latched across later successes */
|
||||
/* Shedding/recovery policy (stereo_shed_policy.c). A shed streak is bounded
|
||||
* by time and count; these report how the last one ended and how stale the
|
||||
* retained pair got. */
|
||||
uint64_t shed_streak, shed_streak_max; /* consecutive sheds now / worst seen */
|
||||
uint32_t shed_streak_ms; /* wall clock of the current streak */
|
||||
uint64_t stale_frames; /* frames that published no new pair */
|
||||
uint64_t budget_resets; /* estimates reset on scene/mode change */
|
||||
double budget_ms; /* pair budget actually in use */
|
||||
uint32_t budget_source; /* 0 vblank (fallback), 1 headset, 2 env */
|
||||
uint64_t last_pair_age_ms; /* age of the retained pair, this frame */
|
||||
int shedding; /* policy is currently suppressing */
|
||||
} RenderStereoStats;
|
||||
void render_stereo_get_stats(RenderStereoStats *out);
|
||||
int render_pass_netplay_enabled(void);
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#ifndef PSX_STEREO_SHED_POLICY_H
|
||||
#define PSX_STEREO_SHED_POLICY_H
|
||||
|
||||
/* Whole-pair stereo shedding policy, extracted so it can be reasoned about and
|
||||
* tested without a GPU, a headset or a guest.
|
||||
*
|
||||
* WHY THIS EXISTS
|
||||
* ---------------
|
||||
* The original rule was `avg_pair_ms > budget_ms && ++retry < 30` — an
|
||||
* attempt counter with no notion of time. One short overload latched the
|
||||
* average above budget and then silently suppressed the next 29 pair requests:
|
||||
* a single heavy frame produced a long run of stale frames. There was also no
|
||||
* way to say "stop shedding and just measure", and no bound on how long
|
||||
* suppression could last.
|
||||
*
|
||||
* THIS POLICY
|
||||
* -----------
|
||||
* * Enters shedding when the pair-time EMA is over budget, and leaves it only
|
||||
* when a fresh measurement is clearly under `hysteresis * budget` (so noise
|
||||
* at the boundary does not flap).
|
||||
* * While shedding, suppression lasts at most `probe_ms` of wall clock and at
|
||||
* most `max_streak` consecutive sheds, whichever comes first. Whichever way
|
||||
* it ends, the next frame is rendered, which takes a fresh measurement.
|
||||
* A stale average can therefore never suppress indefinitely: recovery is
|
||||
* bounded in TIME, not in attempt count.
|
||||
* * A forced probe is authoritative: it REPLACES the running estimate rather
|
||||
* than blending into it. That is what makes a one-off spike cheap — one
|
||||
* good probe ends the streak, instead of the EMA having to decay over
|
||||
* several probes while the headset keeps showing stale pairs.
|
||||
* * `enabled = 0` disables shedding entirely (baseline diagnostic: measure
|
||||
* what happens with no suppression at all).
|
||||
*
|
||||
* The policy decides only whether to skip a pair. It never decides which eyes
|
||||
* to publish; the caller keeps whole-pair publish (a stale frame retains the
|
||||
* previous complete pair, never an unmatched eye).
|
||||
*
|
||||
* All time is passed in by the caller (host monotonic ms), so there is no clock
|
||||
* dependency here and the interesting cases are unit-testable.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct StereoShedPolicy {
|
||||
/* configuration */
|
||||
int enabled; /* 0 = never shed (diagnostic) */
|
||||
double budget_ms; /* per-pair budget; set each frame from the cadence */
|
||||
double probe_ms; /* bound on one shedding streak (wall clock) */
|
||||
uint32_t max_streak; /* bound on one shedding streak (count) */
|
||||
double hysteresis; /* resume threshold as a fraction of budget (0..1) */
|
||||
/* state */
|
||||
double avg_ms; /* EMA of measured pair time */
|
||||
int shedding; /* currently suppressing */
|
||||
int probing; /* the last decision forced a probe; the next
|
||||
* measurement is authoritative */
|
||||
uint32_t streak; /* consecutive sheds in this streak */
|
||||
uint64_t streak_start_ms;/* when this streak began */
|
||||
uint64_t last_pair_ms; /* when the last pair was published */
|
||||
int have_pair;
|
||||
uint64_t pairs_seen; /* measurements folded into the average */
|
||||
uint64_t shed_total; /* pairs skipped since reset */
|
||||
} StereoShedPolicy;
|
||||
|
||||
void stereo_shed_policy_init(StereoShedPolicy *p);
|
||||
/* Reads PSX_STEREO_SHED (0 disables), PSX_STEREO_SHED_PROBE_MS,
|
||||
* PSX_STEREO_SHED_MAX_STREAK and PSX_STEREO_SHED_HYSTERESIS, clamping each to a
|
||||
* safe range. Called once per session. */
|
||||
void stereo_shed_policy_configure_from_env(StereoShedPolicy *p);
|
||||
void stereo_shed_policy_set_budget(StereoShedPolicy *p, double budget_ms);
|
||||
/* Clears the timing estimate and streak but keeps configuration. Call on a
|
||||
* scene/mode transition: a new scene must not inherit the previous one's
|
||||
* estimate, and a cadence change invalidates the budget. */
|
||||
void stereo_shed_policy_reset(StereoShedPolicy *p);
|
||||
|
||||
/* 1 = skip this frame's pair, 0 = render it. */
|
||||
int stereo_shed_policy_should_shed(StereoShedPolicy *p, uint64_t now_ms);
|
||||
/* Fold a measured pair attempt into the estimate. `ok` = a complete pair was
|
||||
* published (so the retained pair is now fresh). */
|
||||
void stereo_shed_policy_note_pair(StereoShedPolicy *p, double pair_ms,
|
||||
uint64_t now_ms, int ok);
|
||||
|
||||
uint32_t stereo_shed_policy_streak_sheds(const StereoShedPolicy *p);
|
||||
uint32_t stereo_shed_policy_streak_ms(const StereoShedPolicy *p, uint64_t now_ms);
|
||||
double stereo_shed_policy_avg_ms(const StereoShedPolicy *p);
|
||||
/* Age of the retained pair, or 0 when no pair has been published yet. */
|
||||
uint64_t stereo_shed_policy_pair_age_ms(const StereoShedPolicy *p, uint64_t now_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PSX_STEREO_SHED_POLICY_H */
|
||||
@@ -487,6 +487,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/guest_cycle_profile.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/latency_ring.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/frame_trace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/stereo_shed_policy.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/present_image_ring.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_timeline.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/data_shards.c
|
||||
|
||||
@@ -9292,6 +9292,10 @@ static void handle_stereo_stats(int id, const char *json) {
|
||||
"\"last_eye\":%d,\"last_failure\":\"%s\","
|
||||
"\"last_failed_attempt\":%llu,\"last_failed_eye\":%d,"
|
||||
"\"last_failed_reason\":\"%s\",\"retained_pair_id\":%llu,"
|
||||
"\"shedding\":%d,\"shed_streak\":%llu,\"shed_streak_ms\":%u,"
|
||||
"\"shed_streak_max\":%llu,\"stale_frames\":%llu,\"budget_resets\":%llu,"
|
||||
"\"budget_ms\":%.3f,\"budget_source\":%u,"
|
||||
"\"last_pair_age_ms\":%llu,"
|
||||
"\"last_pair_ms\":%.3f,\"avg_pair_ms\":%.3f,"
|
||||
"\"published\":{\"valid\":%u,\"pair_id\":%llu,\"guest_cycle\":%llu,"
|
||||
"\"width\":%u,\"height\":%u,\"staged_mask\":%u,\"mode\":%u,\"presents\":%llu},\"captures\":%s}",
|
||||
@@ -9307,6 +9311,10 @@ static void handle_stereo_stats(int id, const char *json) {
|
||||
(unsigned long long)s.last_failed_attempt, s.last_failed_eye,
|
||||
s.last_failed_reason ? s.last_failed_reason : "",
|
||||
(unsigned long long)s.retained_pair_id,
|
||||
s.shedding, (unsigned long long)s.shed_streak, s.shed_streak_ms,
|
||||
(unsigned long long)s.shed_streak_max, (unsigned long long)s.stale_frames,
|
||||
(unsigned long long)s.budget_resets, s.budget_ms, s.budget_source,
|
||||
(unsigned long long)s.last_pair_age_ms,
|
||||
s.last_pair_ms, s.avg_pair_ms, g.valid, (unsigned long long)g.pair_id,
|
||||
(unsigned long long)g.guest_cycle, g.width, g.height, g.staged_mask,
|
||||
g.mode, (unsigned long long)g.presents, captures);
|
||||
|
||||
@@ -581,6 +581,10 @@ int psx_openxr_begin(int width,int height,double units) {
|
||||
XrFrameWaitInfo wait={XR_TYPE_FRAME_WAIT_INFO};XrFrameState fs={XR_TYPE_FRAME_STATE};
|
||||
if(!check(xrWaitFrame(s_session,&wait,&fs),"wait_frame")){
|
||||
psx_frame_trace_add(PSX_TRACE_XR_WAIT_FRAME, xr_trace_us(gl_renderer_perf_ticks()-trace_t));
|
||||
/* Session-level submission failure: the stereo cadence context is gone,
|
||||
* so drop any stale pair-time estimate rather than carrying it into the
|
||||
* recovery attempt. */
|
||||
psx_mod_stereo_reset_budget();
|
||||
(void)service_loss();psx_frame_trace_commit();return 0;}
|
||||
psx_frame_trace_add(PSX_TRACE_XR_WAIT_FRAME, xr_trace_us(gl_renderer_perf_ticks()-trace_t));
|
||||
s_stats.waits++;s_time=fs.predictedDisplayTime;s_stats.predicted_time=(uint64_t)s_time;
|
||||
@@ -594,12 +598,18 @@ int psx_openxr_begin(int width,int height,double units) {
|
||||
}
|
||||
s_prev_display_time=s_time;s_prev_display_valid=1;
|
||||
/* Record the runtime display period with its name for cadence analysis. */
|
||||
/* What the runtime wants us to hit is the only true deadline for the pair.
|
||||
* Hand it to stereo shedding (PSX VBlank is a guest redraw budget, not a
|
||||
* compositor deadline) and record it with the runtime name so a trace can be
|
||||
* read without guessing the headset's refresh. */
|
||||
psx_mod_stereo_set_budget_ms(fs.predictedDisplayPeriod>0?(double)fs.predictedDisplayPeriod/1e6:0.0);
|
||||
psx_frame_trace_session(s_stats.runtime,
|
||||
fs.predictedDisplayPeriod>0?(uint32_t)(fs.predictedDisplayPeriod/1000):0u);
|
||||
trace_t = gl_renderer_perf_ticks();
|
||||
XrFrameBeginInfo begin={XR_TYPE_FRAME_BEGIN_INFO};
|
||||
if(!check(xrBeginFrame(s_session,&begin),"begin_frame")){
|
||||
psx_frame_trace_add(PSX_TRACE_XR_BEGIN_FRAME, xr_trace_us(gl_renderer_perf_ticks()-trace_t));
|
||||
psx_mod_stereo_reset_budget();
|
||||
(void)service_loss();psx_frame_trace_commit();return 0;}
|
||||
psx_frame_trace_add(PSX_TRACE_XR_BEGIN_FRAME, xr_trace_us(gl_renderer_perf_ticks()-trace_t));
|
||||
s_stats.frame_open=1;s_stats.tracking=0;
|
||||
|
||||
+112
-16
@@ -38,6 +38,7 @@
|
||||
#include "timers.h"
|
||||
#include "psx_video_timing.h"
|
||||
#include "gte_view.h"
|
||||
#include "stereo_shed_policy.h"
|
||||
|
||||
extern uint8_t *memory_get_ram_ptr(void);
|
||||
extern uint8_t *memory_get_scratchpad_ptr(void);
|
||||
@@ -283,7 +284,16 @@ static RenderPassFailure s_attempt;
|
||||
static const char *s_checkpoint_failure;
|
||||
static RenderStereoStats s_stereo;
|
||||
static int s_pair_busy;
|
||||
static uint32_t s_pair_retry;
|
||||
/* Whole-pair shedding/recovery policy (bounded in time and count; see
|
||||
* stereo_shed_policy.c). Replaces the old attempt counter, which could suppress
|
||||
* many pairs after a single over-budget frame. */
|
||||
static StereoShedPolicy s_shed;
|
||||
/* Headset deadline pushed by the OpenXR layer (target display period, ms).
|
||||
* 0 = no headset: fall back to the guest-VBlank redraw budget. */
|
||||
static double s_shed_xr_budget_ms;
|
||||
static uint32_t s_shed_period;
|
||||
static int32_t s_shed_w, s_shed_h;
|
||||
static int s_shed_geom_valid;
|
||||
/* A HUD-only capture transaction reuses eye 0's slot but is not an eye draw;
|
||||
* frame tracing must not attribute its cost to the left eye. */
|
||||
static int s_capture_only;
|
||||
@@ -322,6 +332,32 @@ static int verify_on(void) {
|
||||
return s_verify;
|
||||
}
|
||||
|
||||
/* PSX_STEREO_SHED_BUDGET_MS, when set, replaces the guest-VBlank-derived budget
|
||||
* with a real headset deadline (e.g. 11.1 at 90 Hz, 8.3 at 120 Hz). The VBlank
|
||||
* cadence is only a fallback: it is a guest redraw budget, not an OpenXR
|
||||
* display deadline, and conflating the two is wrong on every non-60 Hz headset.
|
||||
* Returns 0 when unset so the caller derives the fallback. */
|
||||
static double shed_budget_override_ms(void) {
|
||||
static double v = -1.0;
|
||||
if (v < 0.0) {
|
||||
const char *e = getenv("PSX_STEREO_SHED_BUDGET_MS");
|
||||
double d = e ? atof(e) : 0.0;
|
||||
v = (d > 0.0 && d < 1000.0) ? d : 0.0;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/* The pair budget actually used, in priority order: the explicit env override,
|
||||
* the headset display deadline the OpenXR layer pushed, then the guest-VBlank
|
||||
* redraw budget as a last resort. */
|
||||
static double shed_budget_ms(uint32_t period_vblanks, uint32_t *source) {
|
||||
double ov = shed_budget_override_ms();
|
||||
if (ov > 0.0) { if (source) *source = 2; return ov; }
|
||||
if (s_shed_xr_budget_ms > 0.0) { if (source) *source = 1; return s_shed_xr_budget_ms; }
|
||||
if (source) *source = 0;
|
||||
return period_vblanks * (double)g_psx_vblank_cycles / 33868.8 * 0.8;
|
||||
}
|
||||
|
||||
void render_pass_get_stats(RenderPassStats *out) {
|
||||
if (!out) return;
|
||||
*out = s_stats;
|
||||
@@ -342,7 +378,12 @@ void render_pass_reset_session(void) {
|
||||
memset(&s_stereo, 0, sizeof s_stereo);
|
||||
s_stereo.last_eye = -1;
|
||||
s_pair_busy = 0;
|
||||
s_pair_retry = 0;
|
||||
/* Shedding policy: defaults, then env overrides (PSX_STEREO_SHED=0 gives the
|
||||
* baseline diagnostic with shedding fully disabled). */
|
||||
stereo_shed_policy_init(&s_shed);
|
||||
stereo_shed_policy_configure_from_env(&s_shed);
|
||||
stereo_shed_policy_reset(&s_shed);
|
||||
s_shed_geom_valid = 0;
|
||||
/* Frame tracing is opt-in per session: re-read the env, install the host
|
||||
* clock, and clear any previous session's records. */
|
||||
psx_frame_trace_configure_from_env();
|
||||
@@ -1029,27 +1070,55 @@ int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
|
||||
* previous traced frame. */
|
||||
int trace_on = psx_frame_trace_enabled();
|
||||
int trace_opened = trace_on ? psx_frame_trace_open() : 0;
|
||||
uint64_t now_ms = psx_host_mono_ms();
|
||||
if (trace_on) {
|
||||
static uint64_t prev_ms;
|
||||
static int prev_valid;
|
||||
uint64_t now_ms = psx_host_mono_ms();
|
||||
if (prev_valid)
|
||||
psx_frame_trace_add(PSX_TRACE_GUEST_SIM, (now_ms - prev_ms) * 1000u);
|
||||
prev_ms = now_ms; prev_valid = 1;
|
||||
psx_frame_trace_attempt(s_stereo.attempts, 0, 0);
|
||||
}
|
||||
budget_ms = frame->period_vblanks * (double)g_psx_vblank_cycles /
|
||||
33868.8 * 0.8;
|
||||
if (s_stereo.avg_pair_ms > budget_ms && ++s_pair_retry < 30u) {
|
||||
s_stereo.shed++;
|
||||
if (trace_on) {
|
||||
psx_frame_trace_attempt(s_stereo.attempts, s_pair_retry, 0);
|
||||
psx_frame_trace_flag(PSX_FRAME_TRACE_STALE_PAIR);
|
||||
if (trace_opened) psx_frame_trace_commit();
|
||||
}
|
||||
return 0;
|
||||
budget_ms = shed_budget_ms(frame->period_vblanks, &s_stereo.budget_source);
|
||||
s_stereo.budget_ms = budget_ms;
|
||||
/* A scene/mode transition changes cadence and workload, so the previous
|
||||
* average and budget do not carry over. */
|
||||
if (!s_shed_geom_valid || frame->period_vblanks != s_shed_period ||
|
||||
(int32_t)frame->w != s_shed_w || (int32_t)frame->h != s_shed_h) {
|
||||
if (s_shed_geom_valid) s_stereo.budget_resets++;
|
||||
s_shed_period = frame->period_vblanks;
|
||||
s_shed_w = (int32_t)frame->w; s_shed_h = (int32_t)frame->h;
|
||||
s_shed_geom_valid = 1;
|
||||
stereo_shed_policy_reset(&s_shed);
|
||||
}
|
||||
s_pair_retry = 0;
|
||||
stereo_shed_policy_set_budget(&s_shed, budget_ms);
|
||||
{
|
||||
int was_shedding = s_shed.shedding;
|
||||
if (stereo_shed_policy_should_shed(&s_shed, now_ms)) {
|
||||
s_stereo.shed++;
|
||||
s_stereo.stale_frames++;
|
||||
s_stereo.shed_streak = stereo_shed_policy_streak_sheds(&s_shed);
|
||||
if (s_stereo.shed_streak > s_stereo.shed_streak_max)
|
||||
s_stereo.shed_streak_max = s_stereo.shed_streak;
|
||||
s_stereo.shed_streak_ms = stereo_shed_policy_streak_ms(&s_shed, now_ms);
|
||||
s_stereo.shedding = 1;
|
||||
if (trace_on) {
|
||||
psx_frame_trace_attempt(s_stereo.attempts, s_stereo.shed_streak,
|
||||
s_stereo.shed_streak_ms);
|
||||
psx_frame_trace_flag(PSX_FRAME_TRACE_STALE_PAIR | PSX_FRAME_TRACE_SHED);
|
||||
if (trace_opened) psx_frame_trace_commit();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* Log once per streak (not per frame), with both the skipped count and
|
||||
* the elapsed wall clock the old attempt counter never tracked. */
|
||||
if (was_shedding)
|
||||
fprintf(stderr, "psxrecomp: stereo shed streak ended after %llu "
|
||||
"skipped, %u ms\n",
|
||||
(unsigned long long)s_stereo.shed_streak,
|
||||
s_stereo.shed_streak_ms);
|
||||
}
|
||||
s_stereo.shedding = s_shed.shedding;
|
||||
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
|
||||
cycle = psx_cycle_count; stores = g_guest_store_count;
|
||||
mismatches = s_stats.verify_mismatch; aborted = s_stats.aborted;
|
||||
@@ -1085,8 +1154,6 @@ int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
|
||||
elapsed = (double)(gl_renderer_perf_ticks() - t0) * 1000.0 /
|
||||
(double)gl_renderer_perf_frequency();
|
||||
s_stereo.last_pair_ms = elapsed;
|
||||
s_stereo.avg_pair_ms = s_stereo.avg_pair_ms > 0.0
|
||||
? s_stereo.avg_pair_ms * 0.9 + elapsed * 0.1 : elapsed;
|
||||
if (ok && !gl_renderer_stereo_publish(s_stereo.attempts, cycle, s_stereo.eye_view)) {
|
||||
ok = 0; s_stereo.last_failure = "pair_publish";
|
||||
}
|
||||
@@ -1102,6 +1169,17 @@ int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
|
||||
s_stereo.retained_pair_id = s_stereo.last_pair_id;
|
||||
gl_renderer_stereo_stage_reset();
|
||||
}
|
||||
/* Fold the attempt into the policy's estimate (it owns the EMA now). A
|
||||
* failed or aborted attempt still counts as a measurement, so a stale high
|
||||
* average cannot outlive the condition that produced it; only a published
|
||||
* pair refreshes the retained-pair age. Pair age is tracked separately from
|
||||
* the compute-time estimate so "how old is the picture" never hides behind
|
||||
* "how fast was the last attempt". */
|
||||
stereo_shed_policy_note_pair(&s_shed, elapsed, now_ms, ok);
|
||||
s_stereo.avg_pair_ms = stereo_shed_policy_avg_ms(&s_shed);
|
||||
s_stereo.last_pair_age_ms = stereo_shed_policy_pair_age_ms(&s_shed, now_ms);
|
||||
s_stereo.shedding = s_shed.shedding;
|
||||
if (!ok) s_stereo.stale_frames++;
|
||||
if (trace_on) {
|
||||
psx_frame_trace_add(PSX_TRACE_PAIR_TOTAL, (uint64_t)(elapsed * 1000.0));
|
||||
psx_frame_trace_result(ok, ok ? s_stereo.last_pair_id : 0,
|
||||
@@ -1113,6 +1191,24 @@ int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Game hook: a scene or mode transition invalidates the pair-time estimate and
|
||||
* the derived budget, so the new scene does not inherit the old one's. Cheap
|
||||
* and idempotent; safe to call on every transition. */
|
||||
void psx_mod_stereo_reset_budget(void) {
|
||||
stereo_shed_policy_reset(&s_shed);
|
||||
s_shed_geom_valid = 0;
|
||||
/* The deadline came from a session that is now gone or whose cadence just
|
||||
* changed; the OpenXR layer re-pushes it on the next frame it runs. */
|
||||
s_shed_xr_budget_ms = 0.0;
|
||||
s_stereo.budget_resets++;
|
||||
}
|
||||
|
||||
void psx_mod_stereo_set_budget_ms(double ms) {
|
||||
/* Push the headset deadline. Ignore nonsense rather than letting a bad
|
||||
* value widen or collapse the budget. */
|
||||
s_shed_xr_budget_ms = (ms > 0.0 && ms < 1000.0) ? ms : 0.0;
|
||||
}
|
||||
|
||||
int psx_mod_render_capture_rgba(CPUState *cpu,const PSXModRenderPass *rect,
|
||||
PSXModRenderPassFn fn,void *user,
|
||||
uint8_t *rgba,uint32_t bytes) {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/* stereo_shed_policy.c — bounded whole-pair shedding. See stereo_shed_policy.h. */
|
||||
|
||||
#include "stereo_shed_policy.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define SHED_EMA_ALPHA 0.1
|
||||
#define SHED_DEFAULT_PROBE_MS 250.0
|
||||
#define SHED_DEFAULT_MAX_STREAK 8u
|
||||
#define SHED_DEFAULT_HYSTERESIS 0.9
|
||||
|
||||
static double clamp_double(double v, double lo, double hi) {
|
||||
if (!(v == v)) return lo; /* NaN */
|
||||
if (v < lo) return lo;
|
||||
if (v > hi) return hi;
|
||||
return v;
|
||||
}
|
||||
|
||||
void stereo_shed_policy_init(StereoShedPolicy *p) {
|
||||
if (!p) return;
|
||||
memset(p, 0, sizeof *p);
|
||||
p->enabled = 1;
|
||||
p->probe_ms = SHED_DEFAULT_PROBE_MS;
|
||||
p->max_streak = SHED_DEFAULT_MAX_STREAK;
|
||||
p->hysteresis = SHED_DEFAULT_HYSTERESIS;
|
||||
}
|
||||
|
||||
void stereo_shed_policy_configure_from_env(StereoShedPolicy *p) {
|
||||
const char *e;
|
||||
if (!p) return;
|
||||
e = getenv("PSX_STEREO_SHED");
|
||||
if (e) p->enabled = (e[0] && e[0] != '0') ? 1 : 0;
|
||||
e = getenv("PSX_STEREO_SHED_PROBE_MS");
|
||||
if (e) p->probe_ms = clamp_double(strtod(e, NULL), 10.0, 5000.0);
|
||||
e = getenv("PSX_STEREO_SHED_MAX_STREAK");
|
||||
if (e) {
|
||||
long v = strtol(e, NULL, 0);
|
||||
p->max_streak = (uint32_t)(v < 1 ? 1 : (v > 240 ? 240 : v));
|
||||
}
|
||||
e = getenv("PSX_STEREO_SHED_HYSTERESIS");
|
||||
if (e) p->hysteresis = clamp_double(strtod(e, NULL), 0.5, 1.0);
|
||||
}
|
||||
|
||||
void stereo_shed_policy_set_budget(StereoShedPolicy *p, double budget_ms) {
|
||||
if (!p) return;
|
||||
p->budget_ms = budget_ms > 0.0 ? budget_ms : 0.0;
|
||||
}
|
||||
|
||||
void stereo_shed_policy_reset(StereoShedPolicy *p) {
|
||||
if (!p) return;
|
||||
p->avg_ms = 0.0;
|
||||
p->shedding = 0;
|
||||
p->probing = 0;
|
||||
p->streak = 0;
|
||||
p->streak_start_ms = 0;
|
||||
p->pairs_seen = 0;
|
||||
}
|
||||
|
||||
int stereo_shed_policy_should_shed(StereoShedPolicy *p, uint64_t now_ms) {
|
||||
if (!p || !p->enabled) return 0;
|
||||
|
||||
if (p->avg_ms > p->budget_ms) {
|
||||
if (!p->shedding) { /* enter shedding at the first over-budget sample */
|
||||
p->shedding = 1;
|
||||
p->streak = 0;
|
||||
p->streak_start_ms = now_ms;
|
||||
}
|
||||
} else if (p->avg_ms < p->budget_ms * p->hysteresis) {
|
||||
p->shedding = 0; /* clearly recovered */
|
||||
p->streak = 0;
|
||||
} else {
|
||||
/* hysteresis band: hold whatever state we are in */
|
||||
}
|
||||
|
||||
if (!p->shedding) return 0;
|
||||
|
||||
/* Bounded recovery: neither an elapsed-time nor an attempt-count rule may
|
||||
* suppress indefinitely. Whichever bound trips first forces a probe, whose
|
||||
* result note_pair() treats as authoritative. The streak's timer restarts
|
||||
* here so a persistent overload probes again after the same bound rather
|
||||
* than every frame. */
|
||||
uint64_t elapsed = now_ms >= p->streak_start_ms ? now_ms - p->streak_start_ms : 0;
|
||||
if (p->streak >= p->max_streak || elapsed >= (uint64_t)p->probe_ms) {
|
||||
p->probing = 1;
|
||||
p->streak = 0;
|
||||
p->streak_start_ms = now_ms;
|
||||
return 0; /* render this one: take a fresh sample */
|
||||
}
|
||||
p->streak++;
|
||||
p->shed_total++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void stereo_shed_policy_note_pair(StereoShedPolicy *p, double pair_ms,
|
||||
uint64_t now_ms, int ok) {
|
||||
if (!p) return;
|
||||
if (pair_ms < 0.0) pair_ms = 0.0;
|
||||
if (p->probing) {
|
||||
/* A forced probe is a deliberate fresh measurement: use it directly
|
||||
* instead of blending it away. Otherwise one spike would keep the
|
||||
* estimate high for many probes, and the headset would keep showing
|
||||
* stale pairs long after the spike ended. */
|
||||
p->probing = 0;
|
||||
p->avg_ms = pair_ms;
|
||||
/* Exit on the deadline itself, not on the hysteresis threshold. A pair
|
||||
* time that settles between the two (here 10.0 to 11.111 ms) would
|
||||
* otherwise sit in the band forever: the band holds whatever state it
|
||||
* is in, so a streak entered during a heavy moment never ends and
|
||||
* shedding starves rendering while the pair is in fact meeting its
|
||||
* deadline. A latched streak is bounded by the probe interval, which is
|
||||
* still the recovery guarantee this policy promises. */
|
||||
if (pair_ms <= p->budget_ms) {
|
||||
p->shedding = 0; /* the overload is over */
|
||||
p->streak = 0;
|
||||
}
|
||||
} else {
|
||||
p->avg_ms = p->pairs_seen ? p->avg_ms * (1.0 - SHED_EMA_ALPHA) + pair_ms * SHED_EMA_ALPHA
|
||||
: pair_ms;
|
||||
}
|
||||
p->pairs_seen++;
|
||||
if (ok) {
|
||||
p->last_pair_ms = now_ms;
|
||||
p->have_pair = 1;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t stereo_shed_policy_streak_sheds(const StereoShedPolicy *p) {
|
||||
return p ? p->streak : 0;
|
||||
}
|
||||
|
||||
uint32_t stereo_shed_policy_streak_ms(const StereoShedPolicy *p, uint64_t now_ms) {
|
||||
if (!p || !p->shedding || now_ms < p->streak_start_ms) return 0;
|
||||
uint64_t d = now_ms - p->streak_start_ms;
|
||||
return d > 0xFFFFFFFFu ? 0xFFFFFFFFu : (uint32_t)d;
|
||||
}
|
||||
|
||||
double stereo_shed_policy_avg_ms(const StereoShedPolicy *p) {
|
||||
return p ? p->avg_ms : 0.0;
|
||||
}
|
||||
|
||||
uint64_t stereo_shed_policy_pair_age_ms(const StereoShedPolicy *p, uint64_t now_ms) {
|
||||
if (!p || !p->have_pair || now_ms < p->last_pair_ms) return 0;
|
||||
return now_ms - p->last_pair_ms;
|
||||
}
|
||||
@@ -780,6 +780,23 @@ static void test_stereo(void) {
|
||||
f.period_vblanks = 0;
|
||||
CHECK(!psx_mod_render_stereo(&cpu, &f, pair_draw, NULL), "invalid cadence refused");
|
||||
|
||||
/* The headset deadline the OpenXR layer pushes outranks the guest-VBlank
|
||||
* fallback, and dropping it restores the fallback. */
|
||||
f.period_vblanks = 2;
|
||||
psx_mod_stereo_reset_budget();
|
||||
psx_mod_stereo_set_budget_ms(11.1);
|
||||
CHECK(psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) == 1,
|
||||
"pair renders under a headset deadline");
|
||||
render_stereo_get_stats(&stats);
|
||||
CHECK(stats.budget_source == 1 && stats.budget_ms > 11.0 && stats.budget_ms < 11.2,
|
||||
"headset deadline drives the pair budget");
|
||||
psx_mod_stereo_reset_budget();
|
||||
CHECK(psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) == 1,
|
||||
"pair renders after the deadline is dropped");
|
||||
render_stereo_get_stats(&stats);
|
||||
CHECK(stats.budget_source == 0 && stats.budget_ms > 0.0,
|
||||
"guest-VBlank fallback restored when no headset deadline is set");
|
||||
|
||||
/* Tracing is observation only: the same pair must publish the same id and
|
||||
* reach the same guest-state hashes with capture on as with it off. */
|
||||
{
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/* test_stereo_shed_policy.c — PR2 shedding/recovery policy.
|
||||
*
|
||||
* Covers the plan's PR2 test list at the policy level: short spike, persistent
|
||||
* overload, alternating load (hysteresis), stale estimate, no unbounded streak,
|
||||
* transient measurement gaps, and the disable switch. Both-eye integrity and
|
||||
* guest-cycle accounting are structural (the policy has no eye or guest state);
|
||||
* the render-pass side already asserts eye-cycle equality.
|
||||
*/
|
||||
|
||||
#include "stereo_shed_policy.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond) do { \
|
||||
if (!(cond)) { printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); g_fail = 1; } \
|
||||
} while (0)
|
||||
|
||||
/* Drive `budget`/`probe`/`streak` config the way a session would, without env. */
|
||||
static void configure(StereoShedPolicy *p, double avg_ms, uint64_t probe_ms,
|
||||
uint32_t max_streak, double hysteresis) {
|
||||
stereo_shed_policy_init(p);
|
||||
p->budget_ms = 16.0;
|
||||
p->probe_ms = (double)probe_ms;
|
||||
p->max_streak = max_streak;
|
||||
p->hysteresis = hysteresis;
|
||||
if (avg_ms > 0.0) { p->avg_ms = avg_ms; p->pairs_seen = 1; }
|
||||
}
|
||||
|
||||
/* 1: disabled means never shed, whatever the timing. */
|
||||
static void test_disabled(void) {
|
||||
StereoShedPolicy p;
|
||||
configure(&p, 40.0, 250, 8, 0.9);
|
||||
p.enabled = 0;
|
||||
for (uint64_t t = 0; t < 2000; t += 8)
|
||||
CHECK(stereo_shed_policy_should_shed(&p, t) == 0);
|
||||
}
|
||||
|
||||
/* 2: a single spike sheds, but the streak is bounded and one good probe ends it
|
||||
* (the probe replaces the estimate instead of decaying into it). */
|
||||
static void test_short_spike_bounded(void) {
|
||||
StereoShedPolicy p;
|
||||
uint32_t sheds = 0;
|
||||
configure(&p, 0.0, 250, 8, 0.9);
|
||||
uint64_t t = 0;
|
||||
for (int i = 0; i < 20; i++) { /* establish a fast baseline (~12 ms) */
|
||||
stereo_shed_policy_note_pair(&p, 12.0, t, 1);
|
||||
t += 16;
|
||||
CHECK(stereo_shed_policy_should_shed(&p, t) == 0);
|
||||
}
|
||||
stereo_shed_policy_note_pair(&p, 80.0, t, 1); /* one heavy frame */
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (stereo_shed_policy_should_shed(&p, t)) { sheds++; t += 16; continue; }
|
||||
stereo_shed_policy_note_pair(&p, 12.0, t, 1); /* the probe measures fast again */
|
||||
t += 16;
|
||||
}
|
||||
CHECK(sheds > 0); /* the spike did suppress something */
|
||||
CHECK(sheds <= 8u); /* ...but never more than one bounded streak */
|
||||
CHECK(stereo_shed_policy_avg_ms(&p) < 16.0); /* and recovery was immediate */
|
||||
}
|
||||
|
||||
/* 3: sustained overload keeps shedding but must still probe periodically (so a
|
||||
* stale average can never suppress forever), and each streak stays bounded. */
|
||||
static void test_persistent_overload_probes(void) {
|
||||
StereoShedPolicy p;
|
||||
uint32_t sheds = 0, attempts = 0;
|
||||
uint32_t run = 0, max_run = 0;
|
||||
configure(&p, 30.0, 250, 8, 0.9);
|
||||
uint64_t t = 0;
|
||||
for (int i = 0; i < 500; i++) { /* 500 * 8 ms = 4 s of sustained overload */
|
||||
if (stereo_shed_policy_should_shed(&p, t)) { sheds++; run++; }
|
||||
else { attempts++; if (run > max_run) max_run = run; run = 0;
|
||||
stereo_shed_policy_note_pair(&p, 30.0, t, 1); } /* still slow */
|
||||
t += 8;
|
||||
}
|
||||
CHECK(sheds > 0);
|
||||
CHECK(attempts > 0); /* overload does not suppress everything */
|
||||
CHECK(max_run <= 8u); /* every streak bounded by max_streak */
|
||||
CHECK(sheds <= 500u - attempts);
|
||||
}
|
||||
|
||||
/* 4: alternating load must not flap: a value inside the hysteresis band holds
|
||||
* the current state instead of toggling every frame. */
|
||||
static void test_alternating_load_hysteresis(void) {
|
||||
StereoShedPolicy p;
|
||||
int transitions = 0, prev = 0;
|
||||
configure(&p, 20.0, 100000, 240, 0.9); /* wide bounds: isolate hysteresis */
|
||||
uint64_t t = 0;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
int s = stereo_shed_policy_should_shed(&p, t);
|
||||
if (i && s != prev) transitions++;
|
||||
prev = s;
|
||||
/* alternate just under budget and inside the band (14.4..16) */
|
||||
stereo_shed_policy_note_pair(&p, (i & 1) ? 15.8 : 15.0, t, 1);
|
||||
t += 8;
|
||||
}
|
||||
CHECK(transitions <= 2); /* entered once, at most one exit: no flapping */
|
||||
}
|
||||
|
||||
/* 5: stale estimate — a long gap with no measurement must still be interrupted
|
||||
* by a probe within probe_ms, and must not suppress indefinitely. */
|
||||
static void test_stale_estimate_recovers(void) {
|
||||
StereoShedPolicy p;
|
||||
configure(&p, 40.0, 250, 1000, 0.9); /* high avg, count bound far away */
|
||||
CHECK(stereo_shed_policy_should_shed(&p, 0) == 1); /* enter */
|
||||
CHECK(stereo_shed_policy_should_shed(&p, 8) == 1);
|
||||
CHECK(stereo_shed_policy_should_shed(&p, 260) == 0); /* >= probe_ms: probe */
|
||||
CHECK(stereo_shed_policy_should_shed(&p, 260) == 1); /* fresh streak begins */
|
||||
CHECK(stereo_shed_policy_should_shed(&p, 520) == 0); /* bounded again */
|
||||
}
|
||||
|
||||
/* 6: a measurement gap (e.g. OpenXR failing so render_stereo never runs) leaves
|
||||
* the average untouched, but the time bound still forces a probe. */
|
||||
static void test_transient_measurement_gap(void) {
|
||||
StereoShedPolicy p;
|
||||
uint64_t first_probe = 0;
|
||||
configure(&p, 30.0, 250, 8, 0.9);
|
||||
uint64_t t = 0;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (stereo_shed_policy_should_shed(&p, t) == 0) { first_probe = t; break; }
|
||||
t += 8; /* no note_pair: the average is never refreshed */
|
||||
}
|
||||
CHECK(first_probe > 0 && first_probe <= 256u);
|
||||
}
|
||||
|
||||
/* 7: a failure that aborts the attempt early is still a measurement, so the
|
||||
* average falls and shedding ends (no unbounded failure loop). */
|
||||
static void test_failure_ends_streak(void) {
|
||||
StereoShedPolicy p;
|
||||
configure(&p, 30.0, 100000, 240, 0.9);
|
||||
CHECK(stereo_shed_policy_should_shed(&p, 0) == 1);
|
||||
stereo_shed_policy_note_pair(&p, 3.0, 8, 0); /* aborted early, not published */
|
||||
for (int i = 0; i < 40; i++)
|
||||
stereo_shed_policy_note_pair(&p, 3.0, (uint64_t)(16 + i * 8), 0);
|
||||
CHECK(stereo_shed_policy_should_shed(&p, 1000) == 0);
|
||||
}
|
||||
|
||||
/* 8: pair age tracks the retained pair independently of the compute estimate. */
|
||||
static void test_pair_age_separate(void) {
|
||||
StereoShedPolicy p;
|
||||
configure(&p, 0.0, 250, 8, 0.9);
|
||||
CHECK(stereo_shed_policy_pair_age_ms(&p, 100) == 0); /* nothing published yet */
|
||||
stereo_shed_policy_note_pair(&p, 12.0, 1000, 1);
|
||||
CHECK(stereo_shed_policy_pair_age_ms(&p, 1000) == 0);
|
||||
CHECK(stereo_shed_policy_pair_age_ms(&p, 1050) == 50);
|
||||
/* a later failed attempt must not refresh the age */
|
||||
stereo_shed_policy_note_pair(&p, 12.0, 1100, 0);
|
||||
CHECK(stereo_shed_policy_pair_age_ms(&p, 1100) == 100);
|
||||
}
|
||||
|
||||
/* 9: reset clears the estimate so a new scene does not inherit it. */
|
||||
static void test_reset_on_transition(void) {
|
||||
StereoShedPolicy p;
|
||||
configure(&p, 40.0, 250, 8, 0.9);
|
||||
CHECK(stereo_shed_policy_should_shed(&p, 0) == 1);
|
||||
stereo_shed_policy_reset(&p);
|
||||
CHECK(stereo_shed_policy_avg_ms(&p) == 0.0);
|
||||
CHECK(stereo_shed_policy_should_shed(&p, 10) == 0); /* fresh, under budget */
|
||||
}
|
||||
|
||||
/* 11: a pair time that settles *inside* the hysteresis band must not latch the
|
||||
* shedding state. Measured case (SteamVR null HMD at 90 Hz): deadline
|
||||
* 11.111 ms, pair 10.4 ms, so the band is 10.0 to 11.111. Shedding entered
|
||||
* during a heavy moment and then never ended, because the band holds whatever
|
||||
* state it is in and the old probe exit test was the band's lower edge. That
|
||||
* cost 26 sheds/s against 3.7 published pairs/s. The probe is authoritative and
|
||||
* must exit on the deadline itself. */
|
||||
static void test_band_latch_recovers(void) {
|
||||
StereoShedPolicy p;
|
||||
uint32_t sheds = 0;
|
||||
configure(&p, 0.0, 250, 8, 0.9);
|
||||
p.budget_ms = 11.111;
|
||||
uint64_t t = 0;
|
||||
stereo_shed_policy_note_pair(&p, 15.0, t, 1); /* heavy moment: enter shedding */
|
||||
for (int i = 0; i < 60; i++) { /* 2 s at a 33 ms frame */
|
||||
if (stereo_shed_policy_should_shed(&p, t)) { sheds++; t += 33; continue; }
|
||||
stereo_shed_policy_note_pair(&p, 10.4, t, 1); /* steady state inside the band */
|
||||
t += 33;
|
||||
}
|
||||
CHECK(sheds <= 12u); /* one bounded streak, then rendering resumes */
|
||||
CHECK(stereo_shed_policy_avg_ms(&p) <= 11.111);
|
||||
}
|
||||
|
||||
/* 10: env parsing clamps to safe ranges (and PSX_STEREO_SHED=0 disables). */
|
||||
static void test_env(void) {
|
||||
StereoShedPolicy p;
|
||||
stereo_shed_policy_init(&p);
|
||||
CHECK(p.enabled == 1 && p.max_streak >= 1u && p.probe_ms > 0.0);
|
||||
stereo_shed_policy_configure_from_env(&p); /* no env set in CI: defaults hold */
|
||||
CHECK(p.max_streak >= 1u && p.max_streak <= 240u);
|
||||
CHECK(p.hysteresis >= 0.5 && p.hysteresis <= 1.0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_disabled();
|
||||
test_short_spike_bounded();
|
||||
test_persistent_overload_probes();
|
||||
test_alternating_load_hysteresis();
|
||||
test_stale_estimate_recovers();
|
||||
test_transient_measurement_gap();
|
||||
test_failure_ends_streak();
|
||||
test_pair_age_separate();
|
||||
test_reset_on_transition();
|
||||
test_band_latch_recovers();
|
||||
test_env();
|
||||
if (g_fail) { printf("stereo_shed_policy: FAILED\n"); return 1; }
|
||||
printf("stereo_shed_policy: all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user