[iterate-4E] Stage 0: native-threads mode flag + functional gate

Scaffolding for the canary-model rework (host-thread-per-guest-thread,
opt-in, non-deterministic). No behavior change with the flag off.

- XENIA_NATIVE_THREADS=1 selects the new executor; folded into the
  parallel spawn gate (reuses Arc<Mutex<KernelState>> + worker dispatch).
- native_threads_enabled() = single source of truth for guarded branches.
- run_execution_native(): Stage-0 body delegates to the iterate-4D
  free-run executor verbatim (native == freerun for now); Stage 1' will
  replace the worker set with one host thread per guest thread.
- native-gate.sh: functional oracle replacing byte-goldens for the MT
  path — (1) lockstep golden byte-identity (flag-off safety net),
  (2) native render milestone (draws>0 && swaps>0), (3) native deadlock
  stress. GREEN 3/3 on current code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-05 20:18:28 +02:00
parent fe797556c2
commit 48e166579b
2 changed files with 177 additions and 1 deletions

View File

@@ -1854,7 +1854,18 @@ fn cmd_exec_inner(
let v = v.trim().to_ascii_lowercase();
v == "1" || v == "true" || v == "yes"
});
let do_parallel = parallel || env_parallel;
// iterate-4E: canary-model native-threads mode (host-thread-per-
// guest-thread). Opt-in; reuses the parallel spawn substrate
// (kernel in Arc<Mutex<>>, worker-thread dispatch). `run_execution_
// parallel` routes it to `run_execution_native`. Flag-off leaves the
// lockstep golden path byte-identical.
let env_native = std::env::var("XENIA_NATIVE_THREADS")
.ok()
.is_some_and(|v| {
let v = v.trim().to_ascii_lowercase();
v == "1" || v == "true" || v == "yes"
});
let do_parallel = parallel || env_parallel || env_native;
// Step 04 gate: --parallel runs N=6 workers that release
// the kernel mutex around step_block, so the per-instruction
// observation path (debugger hooks, DB writer, force-per-instr)
@@ -4069,6 +4080,25 @@ fn run_execution_parallel(
// the workload exposes needs fine-grained kernel locking (the single
// Arc<Mutex<KernelState>> serializes the 6 workers); until then the default
// --parallel path stays the per-round barrier executor below.
// iterate-4E: canary-model native-threads mode. Routed here ahead of the
// freerun check because native mode is the evolution of the freerun
// executor (Stage 0 delegates to it verbatim; Stage 1' replaces the worker
// set with one host thread per guest thread). Opt-in via XENIA_NATIVE_THREADS.
if native_threads_enabled() {
return run_execution_native(
mem,
kernel_arc,
debugger,
thunk_map,
db_writer,
max_instructions,
ips_limit,
quiet,
halt_on_deadlock,
shutdown_outer,
);
}
if std::env::var("XENIA_PARALLEL_FREERUN")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
@@ -4559,6 +4589,65 @@ mod freerun_prof {
/// Non-deterministic by design (thread interleaving); this is the opt-in perf
/// mode. Lockstep (`run_execution`) remains the byte-identical golden path.
#[allow(clippy::too_many_arguments)]
/// iterate-4E: is the canary-model native-threads mode selected?
/// `XENIA_NATIVE_THREADS=1`. Single source of truth so every guarded native
/// branch agrees; when unset the emulator behaves exactly as before (the
/// lockstep golden path and the existing `--parallel`/freerun paths are
/// untouched).
fn native_threads_enabled() -> bool {
std::env::var("XENIA_NATIVE_THREADS")
.map(|v| {
let v = v.trim().to_ascii_lowercase();
v == "1" || v == "true" || v == "yes"
})
.unwrap_or(false)
}
/// iterate-4E — canary-model native-threads executor entry.
///
/// Target end state (across Stages 1'4'): one real host OS thread per guest
/// thread, free-running, blocking on host primitives at guest waits, wall-clock
/// timebase — i.e. xenia-canary's model, opt-in and non-deterministic, with the
/// lockstep path retained as the deterministic golden/reference mode.
///
/// STAGE 0 (this commit): pure scaffolding. The flag is wired end-to-end and
/// routed here, but the body delegates to the proven `iterate-4D` free-run
/// executor verbatim, so native mode == freerun for now. This lets the
/// functional gate (milestone boot + stress + deadlock check) run green against
/// the flag before the executor is transformed in Stage 1'. Nothing in the
/// default (flag-off) path changes.
fn run_execution_native(
mem: &xenia_memory::GuestMemory,
kernel_arc: &std::sync::Arc<std::sync::Mutex<xenia_kernel::KernelState>>,
debugger: &mut xenia_debugger::Debugger,
thunk_map: &HashMap<u32, (ModuleId, u16, String)>,
db_writer: Option<&mut xenia_analysis::DbWriter>,
max_instructions: Option<u64>,
ips_limit: Option<u64>,
quiet: bool,
halt_on_deadlock: bool,
shutdown_outer: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
) -> ExecStats {
if !quiet {
info!(
"iterate-4E Stage 0: native-threads mode selected (delegating to \
free-run executor; per-guest-thread model lands in Stage 1')"
);
}
run_execution_parallel_freerun(
mem,
kernel_arc,
debugger,
thunk_map,
db_writer,
max_instructions,
ips_limit,
quiet,
halt_on_deadlock,
shutdown_outer,
)
}
fn run_execution_parallel_freerun(
mem: &xenia_memory::GuestMemory,
kernel_arc: &std::sync::Arc<std::sync::Mutex<xenia_kernel::KernelState>>,

87
native-gate.sh Executable file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# native-gate.sh — functional correctness gate for the iterate-4E canary-model
# native-threads rework (host-thread-per-guest-thread, non-deterministic).
#
# Byte-identical goldens cannot gate a multi-threaded run (OS interleaving is
# nondeterministic), so this script is the REPLACEMENT oracle. It runs three
# checks and exits 0 iff all pass:
#
# 1. LOCKSTEP GOLDEN (flag-off): the deterministic default path stays
# byte-identical on sylpheed_n200m.json. This is the safety net that
# proves the native-mode work did not disturb the reference path.
# 2. NATIVE RENDER MILESTONE: native mode (XENIA_NATIVE_THREADS=1) boots far
# enough to render — draws>0 && swaps>0 in the run digest — proving the
# executor runs guest CPU + drives the GPU end-to-end without hanging.
# 3. NATIVE DEADLOCK STRESS: parallel_stress_short under XENIA_NATIVE_THREADS=1
# with --halt-on-deadlock; N back-to-back short runs with no panic/hang,
# surfacing lost-wakeups / lock-order inversions the single run misses.
#
# Usage: [SYLPHEED_ISO=...] native-gate.sh [milestone_n] [milestone_timeout_s]
# milestone_n default 200000000 (renders; matches the golden anchor)
# milestone_timeout_s default 120
#
# The binary must be built first (the caller owns the build so the OOM guardrail
# CARGO_BUILD_JOBS=4 / free-check stays explicit):
# CARGO_BUILD_JOBS=4 cargo build --release
set -u
cd "$(dirname "$0")" || exit 2
BIN=./target/release/xenia-rs
ISO_CHECK=sylpheed.iso
MN="${1:-200000000}"
MTO="${2:-120}"
DIGEST=/tmp/native-gate-digest.json
LOG=/tmp/native-gate
mkdir -p "$LOG"
fails=0
hr(){ printf '=%.0s' {1..64}; echo; }
[ -x "$BIN" ] || { echo "FAIL: build first: CARGO_BUILD_JOBS=4 cargo build --release"; exit 3; }
# ---------------------------------------------------------------- 1) golden
hr; echo "[1/3] LOCKSTEP GOLDEN — flag-off byte-identity (sylpheed_n200m)"; hr
cargo test --release -p xenia-app --test sylpheed_oracles -- \
--ignored --nocapture sylpheed_n200m >"$LOG/golden.log" 2>&1
rc=$?
if [ $rc -eq 0 ]; then echo " PASS (golden byte-identical)"; else
echo " FAIL rc=$rc — see $LOG/golden.log"; tail -20 "$LOG/golden.log"; fails=$((fails+1)); fi
# ------------------------------------------------------- 2) native milestone
hr; echo "[2/3] NATIVE RENDER MILESTONE — XENIA_NATIVE_THREADS=1, -n $MN"; hr
rm -f "$DIGEST"
XENIA_NATIVE_THREADS=1 timeout "$MTO" "$BIN" check "$ISO_CHECK" \
-n "$MN" --gpu-inline --out "$DIGEST" >"$LOG/milestone.log" 2>&1
rc=$?
pkill -x xenia-rs 2>/dev/null
if [ $rc -ne 0 ]; then
echo " FAIL emulator rc=$rc (timeout=$MTO s) — see $LOG/milestone.log"
tail -20 "$LOG/milestone.log"; fails=$((fails+1))
elif [ ! -f "$DIGEST" ]; then
echo " FAIL no digest written — see $LOG/milestone.log"; fails=$((fails+1))
else
read -r draws swaps instrs < <(python3 - "$DIGEST" <<'PY'
import json,sys
d=json.load(open(sys.argv[1]))
print(d.get("draws",0), d.get("swaps",0), d.get("instructions",0))
PY
)
echo " digest: instructions=$instrs draws=$draws swaps=$swaps"
if [ "${draws:-0}" -gt 0 ] && [ "${swaps:-0}" -gt 0 ]; then
echo " PASS (native mode renders)"
else
echo " FAIL (native mode did not render: draws=$draws swaps=$swaps)"; fails=$((fails+1)); fi
fi
# ---------------------------------------------------------- 3) native stress
hr; echo "[3/3] NATIVE DEADLOCK STRESS — parallel_stress_short (native)"; hr
XENIA_NATIVE_THREADS=1 cargo test --release -p xenia-app --test parallel_stress -- \
--nocapture parallel_stress_short >"$LOG/stress.log" 2>&1
rc=$?
if [ $rc -eq 0 ]; then
grep -o "runs=[0-9]* ok=[0-9]* failed=[0-9]*" "$LOG/stress.log" | tail -1
echo " PASS (no deadlock/panic)"
else
echo " FAIL rc=$rc — see $LOG/stress.log"; tail -20 "$LOG/stress.log"; fails=$((fails+1)); fi
hr
if [ "$fails" -eq 0 ]; then echo "NATIVE GATE: PASS (3/3)"; exit 0
else echo "NATIVE GATE: FAIL ($fails/3 checks failed)"; exit 1; fi