test(check): ORACBUG-004 — sylpheed_n50m stable-digest oracle
Adds a regression-catcher golden for Sylpheed boot at -n 50M lockstep,
covering the first VdSwap pair (the n2m oracle is swap-blind because
the first VdSwap fires at ~18M instructions). The new --stable-digest
flag emits/compares only fields that are deterministic in lockstep:
instructions, imports, unimpl, draws, swaps,
unique_render_targets, shader_blobs_live, texture_cache_entries
Excluded:
packets — empirically ±2-8% lockstep variance (GPU thread race per
audit M11)
resolves, interrupts_delivered, interrupts_dropped, texture_decodes —
scheduling-sensitive under --parallel
path — cwd-dependent
Empirical determinism: 3 consecutive lockstep -n 50M runs produce
byte-identical stable-digest output.
The n4b canonical-invocation golden the audit's recommended next sprint
also called for is deferred. Per audit memory `--parallel
--reservations-table` is pathologically slow (>32 min for -n 100M), so
-n 4B in that mode would be many hours per run, not the 5-15 min the
plan estimated. n4b will be captured one-shot post-renderer-unblock as
a manual artifact under audit-runs/post-fix/, not as a test golden. See
crates/xenia-app/tests/golden/README.md.
Test infrastructure:
- crates/xenia-app/tests/sylpheed_oracles.rs — invokes
CARGO_BIN_EXE_xenia-rs against the ISO. Path resolved via SYLPHEED_ISO
env var (skips gracefully if missing).
- #[ignore]-gated; run via:
cargo test --release -p xenia-app --test sylpheed_oracles \\
-- --ignored --nocapture
Closes ORACBUG-004 (P0). Partial: ORACBUG-006 (P1 deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -226,6 +226,13 @@ enum Commands {
|
||||
/// Optional golden digest JSON; `check` exits non-zero on mismatch
|
||||
#[arg(long)]
|
||||
expect: Option<String>,
|
||||
/// Emit/compare only stable fields (excludes timing-sensitive counters
|
||||
/// like `packets`, `interrupts_delivered`, `resolves`). Required for any
|
||||
/// golden captured under `--parallel`; recommended for lockstep goldens
|
||||
/// at -n ≥50M because `packets` has empirical ±2–8% jitter from a GPU
|
||||
/// thread race.
|
||||
#[arg(long)]
|
||||
stable_digest: bool,
|
||||
/// Force the threaded GPU backend (default at M1.9).
|
||||
#[arg(long)]
|
||||
gpu_thread: bool,
|
||||
@@ -324,6 +331,7 @@ fn main() -> Result<()> {
|
||||
max_instructions,
|
||||
out,
|
||||
expect,
|
||||
stable_digest,
|
||||
gpu_thread,
|
||||
gpu_inline,
|
||||
reservations_table,
|
||||
@@ -333,6 +341,7 @@ fn main() -> Result<()> {
|
||||
max_instructions,
|
||||
out.as_deref(),
|
||||
expect.as_deref(),
|
||||
stable_digest,
|
||||
gpu_thread,
|
||||
gpu_inline,
|
||||
reservations_table,
|
||||
@@ -528,6 +537,7 @@ fn cmd_exec(
|
||||
parallel,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -538,6 +548,7 @@ fn cmd_check(
|
||||
max_instructions: u64,
|
||||
out: Option<&str>,
|
||||
expect: Option<&str>,
|
||||
stable_digest: bool,
|
||||
gpu_thread: bool,
|
||||
gpu_inline: bool,
|
||||
reservations_table: bool,
|
||||
@@ -561,6 +572,7 @@ fn cmd_check(
|
||||
parallel,
|
||||
out,
|
||||
expect,
|
||||
stable_digest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -582,6 +594,7 @@ fn cmd_exec_inner(
|
||||
parallel: bool,
|
||||
digest_out: Option<&str>,
|
||||
digest_expect: Option<&str>,
|
||||
stable_digest: bool,
|
||||
) -> Result<()> {
|
||||
let started = Instant::now();
|
||||
let data = load_xex_data(path)?;
|
||||
@@ -1190,10 +1203,14 @@ fn cmd_exec_inner(
|
||||
// catch any drift between runs.
|
||||
if digest_out.is_some() || digest_expect.is_some() {
|
||||
let digest = RunDigest::capture(path, &kernel, &stats);
|
||||
let json = digest.to_json();
|
||||
let json = if stable_digest {
|
||||
digest.stable_fields_json()
|
||||
} else {
|
||||
digest.to_json()
|
||||
};
|
||||
if let Some(out_path) = digest_out {
|
||||
std::fs::write(out_path, &json)?;
|
||||
info!(out = out_path, "run digest written");
|
||||
info!(out = out_path, stable = stable_digest, "run digest written");
|
||||
} else {
|
||||
println!("{json}");
|
||||
}
|
||||
@@ -1340,6 +1357,34 @@ impl RunDigest {
|
||||
self.texture_decodes,
|
||||
)
|
||||
}
|
||||
|
||||
/// Stable-fields-only digest for goldens that need to survive non-determinism.
|
||||
/// Excludes timing-sensitive counters: `packets` has documented ±2.5–8% lockstep
|
||||
/// noise from a GPU thread race; `resolves`, `interrupts_delivered`,
|
||||
/// `interrupts_dropped`, and `texture_decodes` are scheduling-sensitive under
|
||||
/// `--parallel`. Also omits `path` (cwd-dependent). The remaining fields are
|
||||
/// deterministic in lockstep at a fixed instruction budget. Use via
|
||||
/// `--stable-digest`.
|
||||
fn stable_fields_json(&self) -> String {
|
||||
format!(
|
||||
"{{\n \"instructions\": {},\n \
|
||||
\"imports\": {},\n \
|
||||
\"unimpl\": {},\n \
|
||||
\"draws\": {},\n \
|
||||
\"swaps\": {},\n \
|
||||
\"unique_render_targets\": {},\n \
|
||||
\"shader_blobs_live\": {},\n \
|
||||
\"texture_cache_entries\": {}\n}}\n",
|
||||
self.instructions,
|
||||
self.imports,
|
||||
self.unimpl,
|
||||
self.draws,
|
||||
self.swaps,
|
||||
self.unique_render_targets,
|
||||
self.shader_blobs_live,
|
||||
self.texture_cache_entries,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_json_string(s: &str) -> String {
|
||||
|
||||
Reference in New Issue
Block a user