Compare commits
6 Commits
intro-vide
...
iterate-4B
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77c2d7bce9 | ||
|
|
9851873e42 | ||
|
|
913b566a26 | ||
|
|
79d0026a31 | ||
|
|
cc9ebbb5e7 | ||
|
|
2c883e9d5e |
109
HANDOFF-intro-video-done.md
Normal file
109
HANDOFF-intro-video-done.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Handoff — Intro video SOLVED (`iterate-4A/apu-xma-stage1`, tag `intro-video-done`)
|
||||
|
||||
The original frontier — **why `dat/movie/ADV.wmv` never played** vs Xenia Canary — is **solved**.
|
||||
The intro renders authentically: white SQUARE ENIX logo + cyan glow + red diamonds on a near-black
|
||||
background, correct color / geometry / luminance, playing reliably. This doc is the handoff for that
|
||||
state and the newly-opened **emulator-speed** frontier.
|
||||
|
||||
> Method (unchanged): first-divergence vs canary · fix causes not symptoms · NO masking · measure the
|
||||
> oracle, never infer. Full setup for this branch is in `HANDOFF-iterate-4A-milestone2.md` (FFmpeg dev
|
||||
> libs, DuckDB tooling, etc.) — read that first on a fresh machine.
|
||||
|
||||
---
|
||||
|
||||
## 0. What to push / copy for handoff
|
||||
|
||||
**Git (the only repo):** `xenia-rs/`. Branch `iterate-4A/apu-xma-stage1`, **6 commits ahead of origin**.
|
||||
```bash
|
||||
cd xenia-rs
|
||||
git push origin iterate-4A/apu-xma-stage1 # the 6 commits below
|
||||
git push origin intro-video-done # the annotated milestone tag (-> 5573ac1)
|
||||
```
|
||||
**Not in git (machine-local — copy manually for a full-context handoff):**
|
||||
- `../CLAUDE.md` — project working instructions (project root is NOT a git repo).
|
||||
- `~/.claude/projects/-home-fabi-RE---Project-Sylpheed/memory/` — the auto-memory (`MEMORY.md`
|
||||
index + topic files). This is where the detailed step-by-step history lives.
|
||||
- Large local artifacts (NOT pushed): the real ISO (`sylpheed.iso` symlink target), `sylpheed.db`
|
||||
(DuckDB static-analysis DB, ~1h35m to regenerate — see CLAUDE.md), canary build.
|
||||
|
||||
---
|
||||
|
||||
## 1. Commits on this branch (ahead of origin)
|
||||
|
||||
| Commit | What |
|
||||
|---|---|
|
||||
| `645feb8` | ROOT #1 decode-timeout clock (IPM 10k→1M), #1b feeder starvation (incumbent-pick), #2 `k_8` texture decoder |
|
||||
| `4344152` | re-baseline boot golden 50M→200M after the clock fix |
|
||||
| `3559c8f` | ROOT #3 host render path — multi-texture YUV, tfetch dest-swizzle, PS const-bank +256 |
|
||||
| `deb9292` | RectangleList quad completion (fix diagonal seam) |
|
||||
| `5573ac1` | MULSC/ADDSC/SUBSC scalar-const operand addressing (fix squared-luma) — **tag `intro-video-done`** |
|
||||
| `2c883e9` | diagnostics snapshot: `XENIA_PROFILE` profiler + probe/tooling (this handoff) |
|
||||
|
||||
Working tree is **clean** — all diagnostics/tooling are now committed in `2c883e9`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Build & run (see CLAUDE.md for the authoritative version)
|
||||
|
||||
```bash
|
||||
cd xenia-rs
|
||||
export CARGO_BUILD_JOBS=4 && cargo build --release # ~8s incremental
|
||||
# Headless oracle run (threaded GPU, single-thread CPU lockstep):
|
||||
RUST_LOG=warn ./target/release/xenia-rs exec sylpheed.iso -n 3000000000
|
||||
# Watch the movie (windowed): add --ui ; the first on-screen frame is at ~81s WALL
|
||||
# (mostly boot). Give it >=150s. Boot is DETERMINISTIC; only wall speed varies.
|
||||
```
|
||||
Movie oracles: `sylph-run.sh [runs] [n] [timeout]` (default 180s). Static analysis: `zq.py` over
|
||||
`sylpheed.db` (DECIMAL bounds, never `0x`). Both are committed at repo root now.
|
||||
|
||||
---
|
||||
|
||||
## 3. Root-cause summary (the fixes)
|
||||
|
||||
- **#1 clock deadline** — movie handler `sub_821B4968` has a 2000 ms decode-ready timeout;
|
||||
`INSTRUCTIONS_PER_MS` 10k ran ~320× too fast so a legit 720p decode looked timed-out → abort.
|
||||
Fixed to 1_000_000 (`state.rs`; `XENIA_INSTR_PER_MS` overrides).
|
||||
- **#1b feeder starvation** — co-located feeder tid24 lost the equal-priority rotation;
|
||||
`pick_runnable` now has an incumbent-preference tiebreak (`scheduler.rs`; `XENIA_INCUMBENT_PICK`).
|
||||
- **#2 `k_8` decoder** — the movie uploads YUV planes as linear `k_8`; added `decode_k8`.
|
||||
- **#3 host render path** — multi-texture (8 YUV slots), tfetch 12-bit dest-swizzle, PS constant
|
||||
bank +256 (Xenos splits VS 0..255 / PS 256..511), RectangleList quad synthesis (v3=v0+v2−v1),
|
||||
and MULSC operand addressing (src3, not src_a/src_b). Details: memory topic file STEPs 88→97.
|
||||
|
||||
---
|
||||
|
||||
## 4. Open frontier — emulator SPEED (profiled, not yet worked)
|
||||
|
||||
Playback is slow (~14–19 fps). First quantified profile (`XENIA_PROFILE=1`, headless):
|
||||
|
||||
- Effective **~35 MIPS** overall (3 B guest instr / 86 s clean).
|
||||
- **Interpreter body ~40% @ ~95–102 MIPS**; kernel HLE ~4%; block-cache lookup ~5–6%;
|
||||
**texture decode 0.3% (cache works — not a bottleneck); present ~0%; no default IPS throttle.**
|
||||
- The rest is **per-block dispatch + scheduler plumbing** — ~13 instr/block over ~229 M blocks,
|
||||
each paying fixed prologue/epilogue/lookup/borrow costs. **Overhead-bound, not interpreter-body
|
||||
bound.** (Note: default run is single-thread lockstep `run_superblock`, confirming the
|
||||
"serialized single-core" architecture; the console's 6 HW threads are time-sliced onto one host
|
||||
thread. `--parallel` exists but is not validated for the movie.)
|
||||
|
||||
**Ranked levers:**
|
||||
1. **Coarsen the execution unit — longer superblock chains** (raise `superblock_budget`). Cheap,
|
||||
reversible, targets the dominant overhead. ⚠️ chaining is also the scheduling lever (can starve
|
||||
the movie subsystem — validate against the oracles). *Start here.*
|
||||
2. **JIT / block-recompiler** — collapses both the interpreter body and per-block dispatch; the real
|
||||
ceiling (~5–10×), but a multi-week subsystem.
|
||||
3. **True multi-core** (guest threads on separate host threads) — huge for a 6-thread workload but
|
||||
riskiest: would invalidate the IPM=1M + incumbent-pick timing fixes that made the video work.
|
||||
|
||||
**Caveat for the next session:** precise remainder attribution needs a sampling profiler
|
||||
(`perf_event_paranoid=4` blocks perf without sudo here; gdb `ptrace_scope=1` blocks non-descendant
|
||||
attach). The `XENIA_PROFILE` coarse timers are the current best tool and are now gated zero-cost
|
||||
when off.
|
||||
|
||||
---
|
||||
|
||||
## 5. `XENIA_PROFILE` quick reference
|
||||
|
||||
`XENIA_PROFILE=1 <run>` → periodic snapshots (every 500 M guest instr, or every 500 presents) +
|
||||
a clean-exit report on stderr. Buckets: interp `step_block`, kernel HLE `call_export`, block
|
||||
decode/cache `lookup_or_build`, texture decode, host draw, present, and the unaccounted remainder.
|
||||
Zero-cost when unset (cached `is_on()` gate). Source: `crates/xenia-gpu/src/prof.rs`.
|
||||
@@ -966,8 +966,21 @@ fn cmd_exec_inner(
|
||||
let v = v.trim().to_ascii_lowercase();
|
||||
v == "1" || v == "true" || v == "yes"
|
||||
});
|
||||
let force_inline = gpu_inline || env_inline || ui;
|
||||
let force_thread = gpu_thread || env_thread;
|
||||
// A.5 — opt-in threaded GPU under `--ui`. Off by default: `--ui` still
|
||||
// forces the inline backend (the safe, milestone-verified path). When
|
||||
// `XENIA_UI_GPU_THREAD=1` is set alongside `--ui`, the GPU command
|
||||
// processing + per-swap UI publish move to the worker thread, freeing the
|
||||
// emulation thread from the ~12 ms/frame inline PM4 drain. See
|
||||
// `run_with_ui` (hook install) and `GpuSystem::run_ui_publish`.
|
||||
let env_ui_thread = std::env::var("XENIA_UI_GPU_THREAD")
|
||||
.ok()
|
||||
.is_some_and(|v| {
|
||||
let v = v.trim().to_ascii_lowercase();
|
||||
v == "1" || v == "true" || v == "yes"
|
||||
});
|
||||
let ui_threaded = ui && env_ui_thread;
|
||||
let force_inline = gpu_inline || env_inline || (ui && !ui_threaded);
|
||||
let force_thread = gpu_thread || env_thread || ui_threaded;
|
||||
let use_threaded = if force_inline {
|
||||
false
|
||||
} else if force_thread {
|
||||
@@ -1768,8 +1781,17 @@ fn cmd_exec_inner(
|
||||
// `xenia_gpu::handle::GpuWorker::run` for the concurrency model.
|
||||
// M1.3's `spawn_noop_worker` is now superseded for the threaded
|
||||
// path; the no-op helper is retained for unit tests.
|
||||
let gpu_thread_resources = if let Some(worker) = maybe_gpu_worker.take() {
|
||||
let gpu_thread_resources = if let Some(mut worker) = maybe_gpu_worker.take() {
|
||||
info!("gpu: threaded backend — spawning worker thread");
|
||||
// A.5 threaded `--ui`: the UI replays real per-draw geometry, so the
|
||||
// worker's `GpuSystem` needs frame capture on before it starts
|
||||
// draining. (Inline `--ui` enables this inside `run_with_ui` via
|
||||
// `as_inline_mut`; the threaded worker owns the system exclusively
|
||||
// once spawned, so we flip it here first.) Harmless in headless
|
||||
// threaded mode — `ui` is false there.
|
||||
if ui {
|
||||
worker.system.enable_frame_capture();
|
||||
}
|
||||
let join = xenia_gpu::spawn_gpu_worker(worker, mem_arc.clone());
|
||||
Some((shutdown_arc.clone(), join))
|
||||
} else {
|
||||
@@ -1791,17 +1813,14 @@ fn cmd_exec_inner(
|
||||
let result = if ui {
|
||||
run_with_ui(
|
||||
path,
|
||||
// `run_with_ui` consumes `GuestMemory` by value today; M1.4
|
||||
// keeps that path on the inline backend until the UI worker
|
||||
// is migrated to the Arc-shared model. Recover ownership via
|
||||
// `Arc::try_unwrap` — succeeds because the GPU worker is not
|
||||
// spawned in inline mode (`maybe_gpu_worker` is `None`).
|
||||
std::sync::Arc::try_unwrap(mem_arc).unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"M1.4: --ui + --gpu-thread cohabitation not yet wired; \
|
||||
choose one"
|
||||
)
|
||||
}),
|
||||
// A.5: `run_with_ui` now takes the shared `Arc<GuestMemory>`
|
||||
// directly (previously it recovered sole ownership via
|
||||
// `Arc::try_unwrap`, which panicked if a GPU worker held a
|
||||
// clone). Both the CPU worker inside `run_with_ui` and the GPU
|
||||
// worker (threaded `--ui`) borrow `&*mem_arc`; writes are `&self`
|
||||
// post the M1.4(b) trait flip, so concurrent disjoint-range
|
||||
// access is sound.
|
||||
mem_arc.clone(),
|
||||
kernel,
|
||||
debugger,
|
||||
thunk_map,
|
||||
@@ -2713,7 +2732,15 @@ fn worker_prologue(
|
||||
let c = kernel.scheduler.ctx(hw_id);
|
||||
[c.gpr[3], c.gpr[4], c.gpr[5], c.gpr[6]]
|
||||
};
|
||||
kernel.call_export(module, ordinal_u32, mem);
|
||||
{
|
||||
let _pt = xenia_gpu::prof::is_on().then(|| {
|
||||
xenia_gpu::prof::ScopeTimer::new(
|
||||
&xenia_gpu::prof::KERNEL_NS,
|
||||
&xenia_gpu::prof::KERNEL_CALLS,
|
||||
)
|
||||
});
|
||||
kernel.call_export(module, ordinal_u32, mem);
|
||||
}
|
||||
let post_ref = kernel.scheduler.current;
|
||||
let c = match post_ref {
|
||||
Some(r) => kernel.scheduler.ctx_mut_ref(r),
|
||||
@@ -2775,6 +2802,12 @@ fn worker_prologue(
|
||||
.current
|
||||
.expect("begin_slot_visit set scheduler.current to Some when slot has runnable thread");
|
||||
let block_ptr: *const xenia_cpu::block_cache::DecodedBlock = {
|
||||
let _pt = xenia_gpu::prof::is_on().then(|| {
|
||||
xenia_gpu::prof::ScopeTimer::new(
|
||||
&xenia_gpu::prof::BUILD_NS,
|
||||
&xenia_gpu::prof::BUILD_CALLS,
|
||||
)
|
||||
});
|
||||
let pc_for_lookup = kernel.scheduler.ctx(hw_id).pc;
|
||||
let b: &xenia_cpu::block_cache::DecodedBlock =
|
||||
wc.block_cache.lookup_or_build(pc_for_lookup, mem);
|
||||
@@ -2913,9 +2946,9 @@ fn worker_epilogue(
|
||||
|
||||
stats.instruction_count = stats.instruction_count.wrapping_add(executed);
|
||||
|
||||
for _ in 0..executed {
|
||||
kernel.scheduler.decrement_quantum();
|
||||
}
|
||||
// PERF: byte-identical bulk decrement (was `for _ in 0..executed`), the
|
||||
// largest fixed per-superblock cost — see `Scheduler::decrement_quantum_by`.
|
||||
kernel.scheduler.decrement_quantum_by(executed);
|
||||
|
||||
match result {
|
||||
StepResult::Continue => {}
|
||||
@@ -3109,18 +3142,26 @@ fn run_superblock(
|
||||
let mut total_executed: u64 = 0;
|
||||
|
||||
let (result, last_block_ptr, last_pc_before) = loop {
|
||||
let cycle_before = kernel.scheduler.ctx_mut_ref(thread_ref).cycle_count;
|
||||
let mmio_before = mem.mmio_access_count();
|
||||
let block = unsafe { &*block_ptr };
|
||||
let result = {
|
||||
let _prof_t0 = xenia_gpu::prof::is_on().then(std::time::Instant::now);
|
||||
// PERF: resolve the running thread's context ONCE per block (was three
|
||||
// `ctx_mut_ref` slot lookups — for cycle-before, the step, and
|
||||
// cycle-after — each a double bounds-checked index). Byte-identical.
|
||||
let (result, executed) = {
|
||||
let ctx = kernel.scheduler.ctx_mut_ref(thread_ref);
|
||||
step_block(ctx, mem, block)
|
||||
let cycle_before = ctx.cycle_count;
|
||||
let result = step_block(ctx, mem, block);
|
||||
let executed = ctx.cycle_count.saturating_sub(cycle_before);
|
||||
(result, executed)
|
||||
};
|
||||
let executed = kernel
|
||||
.scheduler
|
||||
.ctx_mut_ref(thread_ref)
|
||||
.cycle_count
|
||||
.saturating_sub(cycle_before);
|
||||
if let Some(t0) = _prof_t0 {
|
||||
use xenia_gpu::prof;
|
||||
prof::add(&prof::STEP_NS, t0.elapsed().as_nanos() as u64);
|
||||
prof::add(&prof::STEP_INSTR, executed);
|
||||
prof::add(&prof::STEP_CALLS, 1);
|
||||
prof::maybe_report_by_instr();
|
||||
}
|
||||
total_executed = total_executed.saturating_add(executed);
|
||||
|
||||
// STOP conditions (any → end the superblock, hand to epilogue):
|
||||
@@ -3161,7 +3202,15 @@ fn run_superblock(
|
||||
// invalidates the previous `block_ptr` — but we've already finished
|
||||
// using it (only `sync_sensitive`/diagnostics were read, above), so
|
||||
// the raw-pointer aliasing rule is respected.
|
||||
block_ptr = wc.block_cache.lookup_or_build(next_pc, mem) as *const _;
|
||||
{
|
||||
let _pt = xenia_gpu::prof::is_on().then(|| {
|
||||
xenia_gpu::prof::ScopeTimer::new(
|
||||
&xenia_gpu::prof::BUILD_NS,
|
||||
&xenia_gpu::prof::BUILD_CALLS,
|
||||
)
|
||||
});
|
||||
block_ptr = wc.block_cache.lookup_or_build(next_pc, mem) as *const _;
|
||||
}
|
||||
};
|
||||
|
||||
worker_epilogue(
|
||||
@@ -3247,6 +3296,17 @@ fn run_execution(
|
||||
// loop doesn't heap-allocate a `Vec<u8>` every iteration.
|
||||
let mut order_buf = [0u8; xenia_cpu::scheduler::HW_THREAD_COUNT];
|
||||
|
||||
// SPIKE (multi-core feasibility, 2026-07-03): env-gated histogram of the
|
||||
// per-round "runnable width" — how many HW slots hold a Ready thread at
|
||||
// once. The average width bounds the best-case multi-core speedup (Amdahl):
|
||||
// lockstep runs the round's slots serially; a perfect host-thread-per-guest
|
||||
// design runs them concurrently, so wall shrinks by ~avg-width. Zero cost
|
||||
// unless `XENIA_CONCURRENCY_PROBE` is set. Throwaway diagnostic.
|
||||
let concurrency_probe = std::env::var("XENIA_CONCURRENCY_PROBE").is_ok();
|
||||
let mut width_hist = [0u64; xenia_cpu::scheduler::HW_THREAD_COUNT + 1];
|
||||
let mut rounds_with_work = 0u64;
|
||||
let mut slot_visits = 0u64;
|
||||
|
||||
'outer: loop {
|
||||
// Per-round prologue: budget / shutdown / heartbeat / vsync /
|
||||
// timers / audio-interrupt injection. Carved into
|
||||
@@ -3308,6 +3368,14 @@ fn run_execution(
|
||||
let order_n = kernel.scheduler.round_schedule_into(&mut order_buf);
|
||||
let order = &order_buf[..order_n];
|
||||
|
||||
if concurrency_probe {
|
||||
width_hist[order_n] += 1;
|
||||
if order_n > 0 {
|
||||
rounds_with_work += 1;
|
||||
slot_visits += order_n as u64;
|
||||
}
|
||||
}
|
||||
|
||||
if order.is_empty() {
|
||||
// No Ready threads — advance time to the earliest pending
|
||||
// deadline, fire timers, handle deadline wakes, and on hard
|
||||
@@ -3383,6 +3451,31 @@ fn run_execution(
|
||||
RoundCtl::Continue => {}
|
||||
}
|
||||
}
|
||||
if concurrency_probe {
|
||||
let avg_width = if rounds_with_work > 0 {
|
||||
slot_visits as f64 / rounds_with_work as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
eprintln!("=== XENIA_CONCURRENCY_PROBE (multi-core Amdahl ceiling) ===");
|
||||
for (w, &c) in width_hist.iter().enumerate() {
|
||||
let pct = if rounds_with_work + width_hist[0] > 0 {
|
||||
100.0 * c as f64 / (rounds_with_work + width_hist[0]) as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
eprintln!(" runnable-width {w}: {c:>12} rounds ({pct:>5.1}%)");
|
||||
}
|
||||
eprintln!(
|
||||
" rounds_with_work={rounds_with_work} slot_visits={slot_visits} \
|
||||
idle_rounds={}",
|
||||
width_hist[0]
|
||||
);
|
||||
eprintln!(
|
||||
" AVG RUNNABLE WIDTH = {avg_width:.3} => best-case multi-core \
|
||||
speedup ceiling ~= {avg_width:.2}x (concurrent slots / round)"
|
||||
);
|
||||
}
|
||||
stats
|
||||
}
|
||||
|
||||
@@ -3610,10 +3703,19 @@ fn run_execution_parallel(
|
||||
|
||||
// ── unlocked window ───────────────
|
||||
let block = unsafe { &*block_ptr };
|
||||
let _prof_t0 =
|
||||
xenia_gpu::prof::is_on().then(std::time::Instant::now);
|
||||
let result = step_block(&mut ctx_taken, mem_ref, block);
|
||||
let executed = ctx_taken
|
||||
.cycle_count
|
||||
.saturating_sub(cycle_before);
|
||||
if let Some(t0) = _prof_t0 {
|
||||
use xenia_gpu::prof;
|
||||
prof::add(&prof::STEP_NS, t0.elapsed().as_nanos() as u64);
|
||||
prof::add(&prof::STEP_INSTR, executed);
|
||||
prof::add(&prof::STEP_CALLS, 1);
|
||||
prof::maybe_report_by_instr();
|
||||
}
|
||||
// ──────────────────────────────────
|
||||
|
||||
let mut guard = kernel_w.lock().expect("kernel mutex poisoned");
|
||||
@@ -4305,6 +4407,11 @@ fn dump_thread_diagnostic(
|
||||
}
|
||||
use xenia_kernel::objects::KernelObject;
|
||||
|
||||
// Probe-patch (UNCOMMITTED): env-gated wall-time profile of the run.
|
||||
if xenia_gpu::prof::enabled() {
|
||||
xenia_gpu::prof::report(0);
|
||||
}
|
||||
|
||||
// STEP-10 diagnostic (observe-only, env-gated `XENIA_DUMP_SLOTS=1`).
|
||||
// Prints each scheduler slot's full runqueue with the fields needed to
|
||||
// distinguish "Blocked(Suspended) forever" from "Ready but never picked":
|
||||
@@ -4834,7 +4941,7 @@ fn dump_thread_diagnostic(
|
||||
#[instrument(skip_all, fields(title))]
|
||||
fn run_with_ui(
|
||||
title: &str,
|
||||
mut mem: xenia_memory::GuestMemory,
|
||||
mem: std::sync::Arc<xenia_memory::GuestMemory>,
|
||||
mut kernel: xenia_kernel::KernelState,
|
||||
mut debugger: xenia_debugger::Debugger,
|
||||
thunk_map: HashMap<u32, (ModuleId, u16, String)>,
|
||||
@@ -4851,10 +4958,68 @@ fn run_with_ui(
|
||||
.build()
|
||||
.map_err(|e| anyhow::anyhow!("winit event loop build failed: {e}"))?;
|
||||
let (ui_handles, kernel_bridge) = xenia_ui::build(event_loop.create_proxy());
|
||||
|
||||
// A.5 threaded `--ui`: if the GPU runs on the worker thread, the worker
|
||||
// owns the `GpuSystem`, so the per-swap UI publish must run there. Build
|
||||
// the publish closures (mapped into `xenia-gpu` types) from the live
|
||||
// bridge and install them on the worker via the command channel. No-op on
|
||||
// the inline backend — `install_ui_hooks` only sends under
|
||||
// `GpuBackend::Threaded`, and inline `--ui` keeps publishing directly from
|
||||
// `vd_swap`. Built from `&kernel_bridge` before it moves into `kernel.ui`.
|
||||
{
|
||||
use std::sync::atomic::Ordering;
|
||||
let instr = std::sync::Arc::clone(&ui_handles.instructions_counter);
|
||||
let post = std::sync::Arc::clone(&kernel_bridge.post_swap);
|
||||
let hooks = xenia_gpu::UiPublishHooks {
|
||||
publish_assets: std::sync::Arc::clone(&kernel_bridge.publish_xenos_assets),
|
||||
publish_texture: std::sync::Arc::clone(&kernel_bridge.publish_texture),
|
||||
publish_geometry: std::sync::Arc::clone(&kernel_bridge.publish_geometry),
|
||||
publish_frontbuffer: std::sync::Arc::clone(
|
||||
&kernel_bridge.publish_frontbuffer,
|
||||
),
|
||||
notify_swap: std::sync::Arc::new(
|
||||
move |w: xenia_gpu::WorkerSwapInfo,
|
||||
m: &dyn xenia_memory::MemoryAccess| {
|
||||
let info = xenia_kernel::SwapInfo {
|
||||
frontbuffer_addr: w.frontbuffer_addr,
|
||||
width: w.width,
|
||||
height: w.height,
|
||||
// HUD-only; the worker can't see the guest fetch
|
||||
// pointers `vd_swap` reads these from.
|
||||
texture_format: 0,
|
||||
color_space: 0,
|
||||
frame_index: w.frame_index,
|
||||
draws_total: w.draws_total,
|
||||
packets_total: w.packets_total,
|
||||
last_draw_prim: w.last_draw_prim,
|
||||
last_draw_vertex_count: w.last_draw_vertex_count,
|
||||
indirect_buffer_jumps: w.indirect_buffer_jumps,
|
||||
wait_reg_mem_blocks: w.wait_reg_mem_blocks,
|
||||
instructions_total: instr.load(Ordering::Relaxed),
|
||||
vs_blob_key: w.vs_blob_key,
|
||||
ps_blob_key: w.ps_blob_key,
|
||||
resolves_total: w.resolves_total,
|
||||
resolves_copied_total: w.resolves_copied_total,
|
||||
resolves_skipped_total: w.resolves_skipped_total,
|
||||
unique_render_targets: w.unique_render_targets,
|
||||
// HUD-only; kernel interrupt bookkeeping isn't visible
|
||||
// to the worker on this path.
|
||||
interrupts_delivered: 0,
|
||||
interrupts_dropped: 0,
|
||||
};
|
||||
(post)(info, m);
|
||||
},
|
||||
),
|
||||
};
|
||||
kernel.gpu.install_ui_hooks(hooks);
|
||||
}
|
||||
|
||||
kernel.ui = Some(kernel_bridge);
|
||||
// iterate-3O: enable per-draw geometry capture so the UI can replay real
|
||||
// guest draws. Only on the `--ui` path; headless `check` never gets here,
|
||||
// so the deterministic core/golden stays untouched.
|
||||
// so the deterministic core/golden stays untouched. Threaded `--ui`
|
||||
// enables capture on the worker's `GpuSystem` before spawn (see
|
||||
// `cmd_exec_inner`); this covers the inline backend.
|
||||
if let Some(gpu) = kernel.gpu.as_inline_mut() {
|
||||
gpu.enable_frame_capture();
|
||||
}
|
||||
@@ -4867,12 +5032,15 @@ fn run_with_ui(
|
||||
.to_string();
|
||||
|
||||
let worker_span = tracing::info_span!("cpu_worker");
|
||||
// A.5: the CPU worker borrows the shared `Arc<GuestMemory>` (`&*mem_w`)
|
||||
// instead of owning the buffer, so a threaded GPU worker can share it.
|
||||
let mem_w = std::sync::Arc::clone(&mem);
|
||||
let worker = std::thread::Builder::new()
|
||||
.name("xenia-cpu".into())
|
||||
.spawn(move || -> Result<(ExecStats, xenia_memory::GuestMemory, xenia_kernel::KernelState, xenia_debugger::Debugger, Option<xenia_analysis::DbWriter>)> {
|
||||
.spawn(move || -> Result<(ExecStats, xenia_kernel::KernelState, xenia_debugger::Debugger, Option<xenia_analysis::DbWriter>)> {
|
||||
let _guard = worker_span.enter();
|
||||
let stats = run_execution(
|
||||
&mut mem,
|
||||
&mem_w,
|
||||
&mut kernel,
|
||||
&mut debugger,
|
||||
&thunk_map,
|
||||
@@ -4886,7 +5054,7 @@ fn run_with_ui(
|
||||
if let Some(ref mut db) = db_writer {
|
||||
db.finalize_traces()?;
|
||||
}
|
||||
Ok((stats, mem, kernel, debugger, db_writer))
|
||||
Ok((stats, kernel, debugger, db_writer))
|
||||
})
|
||||
.map_err(|e| anyhow::anyhow!("spawn CPU worker: {e}"))?;
|
||||
|
||||
@@ -4894,7 +5062,7 @@ fn run_with_ui(
|
||||
// flips the shutdown flag itself (e.g. after max_instructions).
|
||||
xenia_ui::run(event_loop, ui_handles, &title_owned)?;
|
||||
|
||||
let (stats, mem, kernel, debugger, db_writer) = match worker.join() {
|
||||
let (stats, kernel, debugger, db_writer) = match worker.join() {
|
||||
Ok(res) => res?,
|
||||
Err(_) => {
|
||||
return Err(anyhow::anyhow!("CPU worker thread panicked"));
|
||||
@@ -4902,7 +5070,7 @@ fn run_with_ui(
|
||||
};
|
||||
|
||||
print_summary(kernel.scheduler.ctx(0), &debugger, &db_writer, quiet);
|
||||
dump_thread_diagnostic(&kernel, &mem, quiet);
|
||||
dump_thread_diagnostic(&kernel, &*mem, quiet);
|
||||
info!(
|
||||
wall_ms = started.elapsed().as_millis() as u64,
|
||||
instructions = stats.instruction_count,
|
||||
|
||||
@@ -15,6 +15,58 @@ use xenia_memory::MemoryAccess;
|
||||
/// `reserved_line = ea & !RESERVATION_MASK` in [context::PpcContext].
|
||||
pub const RESERVATION_MASK: u32 = 0x7F;
|
||||
|
||||
/// RE diagnostic (milestone-2 intro-video). `XENIA_RET_CAPTURE_PC=0x..`
|
||||
/// names a guest block-head PC at which to log r3/r4/r5 every time the
|
||||
/// block is entered. Unlike the block-/slot-level audit probes in
|
||||
/// `KernelState`, this fires in `step_block` at *every* block entry, so
|
||||
/// it reaches mid-function return points (e.g. the instruction right
|
||||
/// after a `bcctrl`) that the slot-visit probes miss. Read-only: only
|
||||
/// emits a `println!`, mutates no guest state, so a captured run is
|
||||
/// byte-identical to an unprobed one. 0 (unset env) → inert (one cached
|
||||
/// `OnceLock` load + a `!= 0` branch per block).
|
||||
fn ret_capture_pc() -> u32 {
|
||||
use std::sync::OnceLock;
|
||||
static RET_CAPTURE_PC: OnceLock<u32> = OnceLock::new();
|
||||
*RET_CAPTURE_PC.get_or_init(|| {
|
||||
std::env::var("XENIA_RET_CAPTURE_PC")
|
||||
.ok()
|
||||
.and_then(|s| {
|
||||
let t = s.trim();
|
||||
u32::from_str_radix(t.trim_start_matches("0x"), 16).ok()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
/// RE diagnostic companion to [`ret_capture_pc`]. When `XENIA_RET_CAPTURE_MEM=1`
|
||||
/// the RET-CAPTURE site also dumps the guest words at `r3+0x30 .. r3+0x48`
|
||||
/// (the decoder *track* ring header: `+0x34` write cursor, `+0x44` read
|
||||
/// cursor — fill = write − read). Read-only (`mem.read_u32` + `println!`).
|
||||
fn ret_capture_mem() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static RET_CAPTURE_MEM: OnceLock<bool> = OnceLock::new();
|
||||
*RET_CAPTURE_MEM.get_or_init(|| {
|
||||
std::env::var("XENIA_RET_CAPTURE_MEM")
|
||||
.map(|s| s.trim() == "1")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Which GPR holds the base pointer for the `XENIA_RET_CAPTURE_MEM` dump.
|
||||
/// Defaults to r3; set `XENIA_RET_CAPTURE_REG=21` to dump the pointee of r21
|
||||
/// (e.g. the ring element the pump pops). Read-only.
|
||||
fn ret_capture_reg() -> usize {
|
||||
use std::sync::OnceLock;
|
||||
static RET_CAPTURE_REG: OnceLock<usize> = OnceLock::new();
|
||||
*RET_CAPTURE_REG.get_or_init(|| {
|
||||
std::env::var("XENIA_RET_CAPTURE_REG")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<usize>().ok())
|
||||
.filter(|&n| n < 32)
|
||||
.unwrap_or(3)
|
||||
})
|
||||
}
|
||||
|
||||
/// Result of executing a single instruction.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepResult {
|
||||
@@ -97,6 +149,48 @@ pub fn step_block(
|
||||
mem: &dyn MemoryAccess,
|
||||
block: &crate::block_cache::DecodedBlock,
|
||||
) -> StepResult {
|
||||
// RE diagnostic: log r3/r4/r5 at a configured block-head PC (e.g. the
|
||||
// return point after a `bcctrl`). Read-only; inert unless the env is set.
|
||||
let cap = ret_capture_pc();
|
||||
if cap != 0 && block.start_pc == cap {
|
||||
// KeTimeStampBundle tick_count (ms): [([0x820007D0]) + 0x10]. Read-only;
|
||||
// lets a capture correlate guest progress with the guest-visible clock.
|
||||
let bundle = mem.read_u32(0x820007D0);
|
||||
let tick_ms = if bundle != 0 { mem.read_u32(bundle.wrapping_add(0x10)) } else { 0 };
|
||||
println!("RET-CAPTURE-TICK pc={:#010x} tid={} tick_ms={} cycle={}", block.start_pc, ctx.thread_id, tick_ms, ctx.cycle_count);
|
||||
println!(
|
||||
"RET-CAPTURE pc={:#010x} tid={} r3={:#010x} r4={:#010x} r5={:#010x} r21={:#010x} lr={:#010x} cycle={}",
|
||||
block.start_pc,
|
||||
ctx.thread_id,
|
||||
ctx.gpr[3] as u32,
|
||||
ctx.gpr[4] as u32,
|
||||
ctx.gpr[5] as u32,
|
||||
ctx.gpr[21] as u32,
|
||||
ctx.lr as u32,
|
||||
ctx.cycle_count,
|
||||
);
|
||||
if ret_capture_mem() {
|
||||
let reg = ret_capture_reg();
|
||||
let base = ctx.gpr[reg] as u32;
|
||||
let w = |off: u32| mem.read_u32(base.wrapping_add(off));
|
||||
// Dump +0x00 (vtable ptr) .. +0x48 so callers can inspect either a
|
||||
// ring element (60-byte struct, vtable at +0) or the track header
|
||||
// (+0x34 write / +0x44 read cursors).
|
||||
println!(
|
||||
"RET-CAPTURE-MEM r{} base={:#010x} +00={:#010x} +04={:#010x} +08={:#010x} +0c={:#010x} +10={:#010x} +14={:#010x} +18={:#010x} +1c={:#010x} +20={:#010x} +24={:#010x} +28={:#010x} +2c={:#010x} +30={:#010x} +34={:#010x} +38={:#010x} +3c={:#010x} +40={:#010x} +44={:#010x} +48={:#010x}",
|
||||
reg, base,
|
||||
w(0x00), w(0x04), w(0x08), w(0x0c), w(0x10), w(0x14), w(0x18), w(0x1c),
|
||||
w(0x20), w(0x24), w(0x28), w(0x2c), w(0x30), w(0x34), w(0x38), w(0x3c),
|
||||
w(0x40), w(0x44), w(0x48),
|
||||
);
|
||||
// Extra slots: stack locals [r1+0x50]/[r1+0x54] when reg=1; engine
|
||||
// worker-resume gate +0xF0 (pending flag) / +0x110 (worker handle).
|
||||
println!(
|
||||
"RET-CAPTURE-MEM2 r{} base={:#010x} +50={:#010x} +54={:#010x} +f0={:#010x} +110={:#010x}",
|
||||
reg, base, w(0x50), w(0x54), w(0xF0), w(0x110),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut result = StepResult::Continue;
|
||||
for instr in &block.instrs {
|
||||
let expected_next = instr.addr.wrapping_add(4);
|
||||
|
||||
@@ -49,10 +49,27 @@ pub const QUANTUM_DEFAULT: u32 = 50_000;
|
||||
/// guarantees *bounded* forward progress, it does not invert priority.
|
||||
pub const STARVE_LIMIT: u32 = 4096;
|
||||
|
||||
/// Toggle for the `pick_runnable` incumbent-preference tiebreak (the fix).
|
||||
/// Default on. `XENIA_INCUMBENT_PICK=0` reverts to the old lowest-index
|
||||
/// tiebreak — a rollback knob to A/B the fix's effect on boot rendering vs
|
||||
/// the movie feeder-starvation cure. Inert unless explicitly set to 0.
|
||||
/// RE diagnostic: `XENIA_STARVE_LIMIT=<n>` overrides [`STARVE_LIMIT`] at
|
||||
/// runtime (cached). Lets a fairness test boost starved co-located threads
|
||||
/// (e.g. the feeder tid24 on hw=1) much sooner without a full monopoly —
|
||||
/// distinguishing "fair scheduling fixes the deadlock" from "needs more".
|
||||
/// Unset → the compiled 4096 default (inert).
|
||||
fn starve_limit() -> u32 {
|
||||
use std::sync::OnceLock;
|
||||
static SL: OnceLock<u32> = OnceLock::new();
|
||||
*SL.get_or_init(|| {
|
||||
std::env::var("XENIA_STARVE_LIMIT")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u32>().ok())
|
||||
.filter(|&n| n > 0)
|
||||
.unwrap_or(STARVE_LIMIT)
|
||||
})
|
||||
}
|
||||
|
||||
/// Diagnostic A/B toggle for the `pick_runnable` incumbent-preference
|
||||
/// tiebreak. Default on (the fix). `XENIA_INCUMBENT_PICK=0` reverts to the
|
||||
/// old lowest-index tiebreak, to measure the fix's effect on boot rendering
|
||||
/// vs the movie feeder-starvation cure. Inert unless explicitly set to 0.
|
||||
fn incumbent_pick() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static IP: OnceLock<bool> = OnceLock::new();
|
||||
@@ -200,6 +217,28 @@ pub enum HwState {
|
||||
ServicingIrq(BlockReason),
|
||||
}
|
||||
|
||||
/// RE diagnostic: gate for the `XENIA_LOG_WAITS=1` `PARK` logger in
|
||||
/// [`Scheduler::park_current`]. Cached; inert unless the env is set.
|
||||
fn log_waits_enabled() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static EN: OnceLock<bool> = OnceLock::new();
|
||||
*EN.get_or_init(|| std::env::var("XENIA_LOG_WAITS").is_ok())
|
||||
}
|
||||
|
||||
/// RE diagnostic: `XENIA_FORCE_TID=<n>` — guest tid that `pick_runnable`
|
||||
/// always prefers when Ready (un-starve test for the feeder deadlock).
|
||||
/// 0 / unset = inert. Cached.
|
||||
fn force_tid() -> u32 {
|
||||
use std::sync::OnceLock;
|
||||
static FT: OnceLock<u32> = OnceLock::new();
|
||||
*FT.get_or_init(|| {
|
||||
std::env::var("XENIA_FORCE_TID")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u32>().ok())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum BlockReason {
|
||||
Suspended,
|
||||
@@ -274,6 +313,20 @@ impl HwSlot {
|
||||
/// `STARVE_LIMIT` visits). The boost is a pure function of the per-thread
|
||||
/// counters/priority/index, so picks stay deterministic.
|
||||
pub fn pick_runnable(&self) -> Option<usize> {
|
||||
// RE diagnostic (milestone-2 intro-video). `XENIA_FORCE_TID=<n>` makes
|
||||
// this slot always prefer the named guest tid whenever it is Ready on
|
||||
// this slot — fully un-starving it. Used to separate "tid is starved"
|
||||
// from "tid is genuinely stuck" for the feeder tid24 deadlock. Slots
|
||||
// not carrying that tid fall through to normal selection. Behaviour-
|
||||
// changing, so strictly env-gated (inert when unset).
|
||||
let force = force_tid();
|
||||
if force != 0 {
|
||||
if let Some((i, _)) = self.runqueue.iter().enumerate().find(|(_, t)| {
|
||||
t.tid == force && matches!(t.state, HwState::Ready | HwState::ServicingIrq(_))
|
||||
}) {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
// Tiebreak among equal-effective-priority Ready threads PREFERS THE
|
||||
// INCUMBENT (`running_idx`) over the lowest index. `decrement_quantum`
|
||||
// rotates `running_idx` to the next same-priority peer when the 50k
|
||||
@@ -307,7 +360,7 @@ impl HwSlot {
|
||||
/// visits is lifted to `i32::MAX` so it wins the next pick regardless of
|
||||
/// peer priority; otherwise its nominal priority is used unchanged.
|
||||
fn effective_priority(t: &GuestThread) -> i32 {
|
||||
if t.steps_starved >= STARVE_LIMIT {
|
||||
if t.steps_starved >= starve_limit() {
|
||||
i32::MAX
|
||||
} else {
|
||||
t.priority
|
||||
@@ -963,6 +1016,52 @@ impl Scheduler {
|
||||
false
|
||||
}
|
||||
|
||||
/// Bulk equivalent of calling [`Self::decrement_quantum`] exactly `n`
|
||||
/// times, producing a **byte-identical** final scheduler state. PERF: the
|
||||
/// superblock epilogue used to loop `for _ in 0..executed { decrement_quantum() }`
|
||||
/// — up to ~128 bounds-checked calls per superblock, ~one per retired guest
|
||||
/// instruction across the whole run (the single largest fixed per-superblock
|
||||
/// cost). Since `QUANTUM_DEFAULT` (50_000) ≫ a superblock's instruction
|
||||
/// count, the quantum boundary is crossed at most once per call, so the
|
||||
/// common path is a single subtraction (O(1)); only the rare
|
||||
/// boundary-crossing step defers to `decrement_quantum` to reproduce the
|
||||
/// exact rotation semantics (reload + same-priority peer hand-off).
|
||||
pub fn decrement_quantum_by(&mut self, mut n: u64) {
|
||||
while n > 0 {
|
||||
let Some(r) = self.current else {
|
||||
return;
|
||||
};
|
||||
let Some(t) = self.slots[r.hw_id as usize]
|
||||
.runqueue
|
||||
.get_mut(r.idx as usize)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let q = t.quantum_remaining as u64;
|
||||
if q > n {
|
||||
// No quantum boundary within these `n` steps — the common
|
||||
// case. Identical to `n` plain decrements that each hit the
|
||||
// early `quantum_remaining != 0` return.
|
||||
t.quantum_remaining = (q - n) as u32;
|
||||
return;
|
||||
}
|
||||
// A rotation (quantum reload + optional peer hand-off) occurs
|
||||
// within these `n` steps. Collapse the `q` leading no-op
|
||||
// decrements into one rotating single-step: set the quantum to 1
|
||||
// so the next `decrement_quantum` drives it to 0 and rotates with
|
||||
// identical semantics (for `q == 0` the first single-step already
|
||||
// rotates, consuming exactly one step).
|
||||
let consumed = if q > 0 {
|
||||
t.quantum_remaining = 1;
|
||||
q
|
||||
} else {
|
||||
1
|
||||
};
|
||||
self.decrement_quantum();
|
||||
n -= consumed;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cooperative yield: the currently-running thread executed a `db16cyc`
|
||||
/// spin-wait hint (see `StepResult::Yield`). It is busy-spinning on a
|
||||
/// guest spinlock/barrier whose release depends on a *co-located* peer
|
||||
@@ -991,7 +1090,7 @@ impl Scheduler {
|
||||
if i == me {
|
||||
t.steps_starved = 0;
|
||||
} else if matches!(t.state, HwState::Ready | HwState::ServicingIrq(_)) {
|
||||
t.steps_starved = STARVE_LIMIT;
|
||||
t.steps_starved = starve_limit();
|
||||
promoted = true;
|
||||
}
|
||||
}
|
||||
@@ -1015,6 +1114,18 @@ impl Scheduler {
|
||||
self.timed_waits.push((d, r));
|
||||
self.timed_waits.sort_by_key(|&(d, _)| d);
|
||||
}
|
||||
// RE diagnostic (milestone-2 intro-video). `XENIA_LOG_WAITS=1` logs a
|
||||
// `PARK` line (tid, pc, cycle, block reason incl. waited handles) every
|
||||
// time a thread blocks — the symmetric counterpart of the resume log,
|
||||
// for pinning the producer↔consumer deadlock (what does the feeder
|
||||
// tid24 wait on after its one pass). Read-only diagnostic.
|
||||
if log_waits_enabled() {
|
||||
let t = self.thread_mut(r);
|
||||
println!(
|
||||
"PARK tid={} pc={:#010x} cycle={} reason={:?}",
|
||||
t.tid, t.ctx.pc, t.ctx.cycle_count, reason,
|
||||
);
|
||||
}
|
||||
self.thread_mut(r).state = HwState::Blocked(reason);
|
||||
self.recompute_slot_runnable(r.hw_id);
|
||||
}
|
||||
|
||||
@@ -369,6 +369,14 @@ pub struct GpuSystem {
|
||||
ib_stack: Vec<RingBufferView>,
|
||||
/// Cached shader blobs keyed by the raw CP register address that loaded them.
|
||||
pub shader_blobs: HashMap<u32, ShaderBlob>,
|
||||
/// PERF (--ui): monotonic counter bumped on every `shader_blobs`
|
||||
/// mutation (insert / overwrite / evict). `vd_swap` compares it against
|
||||
/// `last_published_blobs_version` to skip re-cloning + re-publishing the
|
||||
/// whole blob map to the UI when nothing changed this swap.
|
||||
pub shader_blobs_version: u64,
|
||||
/// Version last handed to the UI bridge (see above). Starts at `u64::MAX`
|
||||
/// so the very first swap always publishes.
|
||||
pub last_published_blobs_version: u64,
|
||||
/// P8 — FIFO of blob keys for bounded eviction. On `IM_LOAD*` the
|
||||
/// new key is pushed to the back; if the blob count exceeds
|
||||
/// [`SHADER_BLOB_CAP`], the front is popped and removed from
|
||||
@@ -451,6 +459,8 @@ impl GpuSystem {
|
||||
ring: RingBufferView::new(),
|
||||
ib_stack: Vec::new(),
|
||||
shader_blobs: HashMap::new(),
|
||||
shader_blobs_version: 0,
|
||||
last_published_blobs_version: u64::MAX,
|
||||
shader_blob_order: std::collections::VecDeque::with_capacity(SHADER_BLOB_CAP + 1),
|
||||
swap_counter: 0,
|
||||
last_swap: None,
|
||||
@@ -486,6 +496,9 @@ impl GpuSystem {
|
||||
/// Never evicts the currently-active VS/PS blobs (if they ended up at
|
||||
/// the front of the queue, we skip past them).
|
||||
fn insert_shader_blob(&mut self, key: u32, blob: ShaderBlob) {
|
||||
// PERF (--ui): every path through here mutates the published map
|
||||
// (insert/overwrite above, eviction below), so bump once here.
|
||||
self.shader_blobs_version = self.shader_blobs_version.wrapping_add(1);
|
||||
let already_present = self.shader_blobs.contains_key(&key);
|
||||
self.shader_blobs.insert(key, blob);
|
||||
if !already_present {
|
||||
@@ -1246,6 +1259,17 @@ impl GpuSystem {
|
||||
let mut ds = draw_state::extract(&self.register_file, vgt, dma_base, dma_size);
|
||||
ds.vs_blob_key = self.active_vs_key;
|
||||
ds.ps_blob_key = self.active_ps_key;
|
||||
if std::env::var("XENIA_CONST_LOG").is_ok() {
|
||||
let c254x = f32::from_bits(self.register_file.read(CONST_BASE_ALU + 254 * 4));
|
||||
let c255x = f32::from_bits(self.register_file.read(CONST_BASE_ALU + 255 * 4));
|
||||
if c254x != 0.0 || c255x != 0.0 {
|
||||
eprintln!(
|
||||
"DRAW-CONST draw={} ps={:#x} c254.x={c254x} c255.x={c255x}",
|
||||
self.stats.draws_seen,
|
||||
self.active_ps_key.unwrap_or(0)
|
||||
);
|
||||
}
|
||||
}
|
||||
let processed = primitive::process(ds.primitive, ds.vertex_count, None);
|
||||
metrics::counter!(
|
||||
"gpu.draw",
|
||||
@@ -1468,6 +1492,15 @@ impl GpuSystem {
|
||||
let v = self.read_payload(mem, 2 + i);
|
||||
self.register_file.write(base + index + i, v);
|
||||
}
|
||||
if std::env::var("XENIA_CONST_LOG").is_ok() {
|
||||
// Compact histogram-able line: type + index-range for every
|
||||
// SET_CONSTANT. `covers254` flags an ALU write hitting c254.
|
||||
eprintln!(
|
||||
"SC t={const_type} i={index} n={} covers254={}",
|
||||
count - 1,
|
||||
const_type == 0 && index <= 1016 && 1016 < index + (count - 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
pm4::PM4_SET_CONSTANT2 => {
|
||||
// payload[0] = 16-bit index; subsequent payloads write consecutive regs.
|
||||
@@ -1476,6 +1509,15 @@ impl GpuSystem {
|
||||
let v = self.read_payload(mem, 2 + i);
|
||||
self.register_file.write(index + i, v);
|
||||
}
|
||||
if std::env::var("XENIA_CONST_LOG").is_ok() {
|
||||
let alu_lo = CONST_BASE_ALU;
|
||||
let alu_hi = CONST_BASE_ALU + 2048;
|
||||
eprintln!(
|
||||
"SC2 i={index:#x} n={} in_alu={}",
|
||||
count - 1,
|
||||
index >= alu_lo && index < alu_hi
|
||||
);
|
||||
}
|
||||
}
|
||||
pm4::PM4_LOAD_ALU_CONSTANT => {
|
||||
// payload[0] = source mem addr, [1] = offset_type, [2] = size_dwords
|
||||
@@ -1496,6 +1538,13 @@ impl GpuSystem {
|
||||
let v = mem.read_u32(src + i * 4);
|
||||
self.register_file.write(base + index + i, v);
|
||||
}
|
||||
if std::env::var("XENIA_CONST_LOG").is_ok() && const_type == 0 {
|
||||
eprintln!(
|
||||
"LOAD-ALU-CONST src={src:#x} idx={index} size={size_dwords} first={:?} covers254={}",
|
||||
f32::from_bits(mem.read_u32(src)),
|
||||
index <= 1016 && 1016 < index + size_dwords
|
||||
);
|
||||
}
|
||||
}
|
||||
pm4::PM4_IM_LOAD | pm4::PM4_IM_LOAD_IMMEDIATE => {
|
||||
// Canary (pm4_command_processor_implement.h:1271-1330):
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
//! `into_handle` on the live `KernelState.gpu` — the constructor exists for
|
||||
//! the unit test below and for the synthetic-test path.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
@@ -33,7 +34,10 @@ use crossbeam_channel::{Receiver, Sender, bounded, unbounded};
|
||||
|
||||
use xenia_memory::GuestMemory;
|
||||
|
||||
use crate::draw_capture::DrawCapture;
|
||||
use crate::gpu_system::{ExecOutcome, GpuMmio, GpuStats, GpuSystem, PendingInterrupt};
|
||||
use crate::texture_cache::TextureKey;
|
||||
use crate::xenos_constants::XenosConstantsBlock;
|
||||
|
||||
/// Reply channel for a [`GpuCommand::DrainFence`]. Single-shot
|
||||
/// `bounded(1)` — the GPU sends `()` once it's drained the ring up to the
|
||||
@@ -41,6 +45,57 @@ use crate::gpu_system::{ExecOutcome, GpuMmio, GpuStats, GpuSystem, PendingInterr
|
||||
/// is the first user of this; step 1 only validates the type fits.
|
||||
pub type DrainReply = crossbeam_channel::Sender<()>;
|
||||
|
||||
/// GPU-derived swap metadata the worker hands to the UI `notify_swap` hook
|
||||
/// under threaded `--ui` (A.5). The app-side glue maps this into the kernel's
|
||||
/// `SwapInfo`, filling the two non-GPU fields itself (`instructions_total`
|
||||
/// from the shared instruction counter; the interrupt counts are HUD-cosmetic
|
||||
/// and passed as 0 on this path — the worker has no view of kernel interrupt
|
||||
/// bookkeeping). `texture_format`/`color_space` likewise aren't visible to the
|
||||
/// worker (they come from guest pointers in `vd_swap`'s args) and are HUD-only.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct WorkerSwapInfo {
|
||||
pub frontbuffer_addr: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub frame_index: u64,
|
||||
pub draws_total: u64,
|
||||
pub packets_total: u64,
|
||||
pub last_draw_prim: u32,
|
||||
pub last_draw_vertex_count: u32,
|
||||
pub indirect_buffer_jumps: u64,
|
||||
pub wait_reg_mem_blocks: u64,
|
||||
pub vs_blob_key: u32,
|
||||
pub ps_blob_key: u32,
|
||||
pub resolves_total: u64,
|
||||
pub resolves_copied_total: u64,
|
||||
pub resolves_skipped_total: u64,
|
||||
pub unique_render_targets: u64,
|
||||
}
|
||||
|
||||
/// UI publish closures the GPU worker calls when it consumes a swap under
|
||||
/// threaded `--ui` (A.5). These mirror the kernel `UiBridge`'s publish
|
||||
/// closures but are expressed purely in `xenia-gpu` types so the worker
|
||||
/// (which lives in this crate and can't depend on `xenia-kernel`) can hold
|
||||
/// them. Built app-side from the live `UiBridge` and installed on the worker
|
||||
/// via [`GpuCommand::InstallUiHooks`]. All closures are `Send + Sync` and are
|
||||
/// invoked from the GPU worker thread, never the emulation thread.
|
||||
#[derive(Clone)]
|
||||
pub struct UiPublishHooks {
|
||||
pub publish_assets:
|
||||
Arc<dyn Fn(Option<HashMap<u32, Vec<u32>>>, XenosConstantsBlock) + Send + Sync>,
|
||||
pub publish_texture: Arc<dyn Fn(Option<(TextureKey, Vec<u8>)>) + Send + Sync>,
|
||||
pub publish_geometry: Arc<dyn Fn(Vec<DrawCapture>) + Send + Sync>,
|
||||
pub publish_frontbuffer: Arc<dyn Fn(u32, u32, Vec<u8>) + Send + Sync>,
|
||||
pub notify_swap:
|
||||
Arc<dyn Fn(WorkerSwapInfo, &dyn xenia_memory::MemoryAccess) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UiPublishHooks {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("UiPublishHooks { .. }")
|
||||
}
|
||||
}
|
||||
|
||||
/// Control-plane RPC the CPU thread sends to the GPU thread. Data-plane
|
||||
/// signals (WPTR/RPTR/INT_STATUS) ride atomic mailboxes instead — see
|
||||
/// [`GpuMmio`]. Channels are for events that need ordered delivery and
|
||||
@@ -80,6 +135,12 @@ pub enum GpuCommand {
|
||||
width: u32,
|
||||
height: u32,
|
||||
},
|
||||
/// A.5 threaded `--ui`: install the UI publish closures on the worker so
|
||||
/// it can run the per-swap capture/publish itself (blobs, constants,
|
||||
/// texture, geometry, frontbuffer detile, notify) off the emulation
|
||||
/// thread. Sent once by `run_with_ui` after the UI bridge is built.
|
||||
/// Boxed to keep [`GpuCommand`] small (the hooks carry five `Arc`s).
|
||||
InstallUiHooks(Box<UiPublishHooks>),
|
||||
/// Tear-down signal. The worker drains any in-flight reply channels,
|
||||
/// drops its `GpuSystem`, and the host thread joins.
|
||||
Shutdown,
|
||||
@@ -189,6 +250,15 @@ pub struct GpuWorker {
|
||||
/// Shutdown flag. Set by `shutdown_and_join_with_timeout`; the worker
|
||||
/// loop checks `Acquire` each iteration.
|
||||
pub shutdown: Arc<AtomicBool>,
|
||||
/// A.5: UI publish closures, installed via [`GpuCommand::InstallUiHooks`].
|
||||
/// `None` in headless / inline modes → the worker does zero UI publish
|
||||
/// (byte-identical to the pre-A.5 headless-threaded path).
|
||||
pub ui_hooks: Option<UiPublishHooks>,
|
||||
/// A.5: last `swaps_seen` value the worker ran the UI publish for. The
|
||||
/// publish is level-triggered on this counter advancing, so multiple
|
||||
/// swaps consumed in one iteration collapse to a single publish of the
|
||||
/// latest state.
|
||||
pub last_published_swaps: u64,
|
||||
}
|
||||
|
||||
impl GpuSystem {
|
||||
@@ -220,6 +290,8 @@ impl GpuSystem {
|
||||
int_tx,
|
||||
digest: digest.clone(),
|
||||
shutdown: shutdown.clone(),
|
||||
ui_hooks: None,
|
||||
last_published_swaps: 0,
|
||||
};
|
||||
let handle = GpuHandle {
|
||||
cmd_tx,
|
||||
@@ -427,6 +499,16 @@ impl GpuBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// A.5 threaded `--ui`: hand the worker the UI publish closures so it can
|
||||
/// run the per-swap capture/publish on its own thread. No-op on the inline
|
||||
/// backend (that path publishes directly from `vd_swap` on the emulation
|
||||
/// thread and never needs the hooks).
|
||||
pub fn install_ui_hooks(&self, hooks: UiPublishHooks) {
|
||||
if let GpuBackend::Threaded(h) = self {
|
||||
let _ = h.send_cmd(GpuCommand::InstallUiHooks(Box::new(hooks)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Bump `swaps_seen` + record `last_swap` + push a swap interrupt.
|
||||
/// Inline calls directly. Threaded sends `NotifyXeSwap` over the
|
||||
/// command channel — fire-and-forget; the worker handles it on its
|
||||
@@ -616,6 +698,9 @@ impl GpuWorker {
|
||||
self.system
|
||||
.notify_xe_swap(frontbuffer_phys, width, height);
|
||||
}
|
||||
GpuCommand::InstallUiHooks(hooks) => {
|
||||
self.ui_hooks = Some(*hooks);
|
||||
}
|
||||
GpuCommand::Shutdown => {
|
||||
self.shutdown.store(true, Ordering::Release);
|
||||
return;
|
||||
@@ -662,6 +747,21 @@ impl GpuWorker {
|
||||
*g = snap;
|
||||
}
|
||||
}
|
||||
// (5c) A.5 threaded `--ui`: if a swap was consumed this iteration
|
||||
// (either an in-stream PM4_XE_SWAP during the drain above or a
|
||||
// `NotifyXeSwap` safety-net command), run the per-swap UI
|
||||
// publish on THIS worker thread — shader blobs, constants,
|
||||
// texture, geometry, frontbuffer detile, and `notify_swap`.
|
||||
// Level-triggered on `swaps_seen` so it fires exactly once per
|
||||
// new frame. Inline / headless modes leave `ui_hooks == None`
|
||||
// and skip this entirely.
|
||||
if let Some(hooks) = self.ui_hooks.as_ref() {
|
||||
let cur = self.system.stats.swaps_seen;
|
||||
if cur > self.last_published_swaps {
|
||||
self.last_published_swaps = cur;
|
||||
self.system.run_ui_publish(&memory, hooks);
|
||||
}
|
||||
}
|
||||
// (6) M1.7 parker — `park_timeout` replaces the polling
|
||||
// sleep. The standard parker idiom defends against the
|
||||
// producer-races-park lost-wakeup:
|
||||
@@ -794,6 +894,150 @@ pub fn shutdown_and_join_with_timeout(
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuSystem {
|
||||
/// A.5 worker-side UI publish. Mirrors the inline `vd_swap` publish block
|
||||
/// (`crates/xenia-kernel/src/exports.rs`) but runs on the GPU worker
|
||||
/// thread against this worker's own `GpuSystem` state + the shared guest
|
||||
/// memory. Called once per consumed swap (level-triggered on
|
||||
/// `stats.swaps_seen`). Takes the concrete `&GuestMemory` because
|
||||
/// `max_page_version`/`read_bulk` are inherent methods, not on the
|
||||
/// `MemoryAccess` trait.
|
||||
///
|
||||
/// Field-for-field parity with the inline path is intentional: the same
|
||||
/// publish-on-change blob gating (A.2), the same slot-0 texture fallback,
|
||||
/// the same bulk frontbuffer detile (A.1). The only differences are the
|
||||
/// two HUD-only fields the worker can't see (see [`WorkerSwapInfo`]).
|
||||
pub fn run_ui_publish(&mut self, mem: &GuestMemory, hooks: &UiPublishHooks) {
|
||||
use crate::gpu_system::{CONST_BASE_FETCH, SwapNotification};
|
||||
|
||||
// Source of truth for this frame is whatever the executor recorded
|
||||
// from the in-stream PM4_XE_SWAP (or the NotifyXeSwap safety net).
|
||||
let swap = self.last_swap.unwrap_or(SwapNotification {
|
||||
frame_index: self.swap_counter,
|
||||
frontbuffer_phys: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
// Shader blobs: rebuild + clone only on change; constants always.
|
||||
let blobs: Option<HashMap<u32, Vec<u32>>> =
|
||||
if self.shader_blobs_version != self.last_published_blobs_version {
|
||||
self.last_published_blobs_version = self.shader_blobs_version;
|
||||
Some(
|
||||
self.shader_blobs
|
||||
.iter()
|
||||
.map(|(k, b)| (*k, b.dwords.clone()))
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let constants = XenosConstantsBlock::snapshot(&self.register_file);
|
||||
(hooks.publish_assets)(blobs, constants);
|
||||
|
||||
// Primary texture: prefer the last draw's sampled texture; else probe
|
||||
// fetch-constant slot 0 directly (flat-shader frames).
|
||||
let published = self
|
||||
.last_draw_textures
|
||||
.first()
|
||||
.map(|(_slot, k, _v, b)| (*k, b.clone()))
|
||||
.or_else(|| {
|
||||
const TEX_SLOT: u32 = 0;
|
||||
let mut fetch6 = [0u32; 6];
|
||||
for (i, slot) in fetch6.iter_mut().enumerate() {
|
||||
*slot = self
|
||||
.register_file
|
||||
.read(CONST_BASE_FETCH + TEX_SLOT * 6 + i as u32);
|
||||
}
|
||||
let key = crate::texture_cache::decode_fetch_constant(fetch6)?;
|
||||
let bi = key.format.block_info();
|
||||
let span_bytes = (key.pitch_texels as u32)
|
||||
* (key.height as u32)
|
||||
* (bi.bytes_per_block as u32)
|
||||
/ (bi.block_w as u32);
|
||||
let version = mem.max_page_version(key.base_address, span_bytes.max(4));
|
||||
match self.texture_cache.ensure_cached(key, version, mem) {
|
||||
Ok(entry) => Some((entry.key, entry.bytes.clone())),
|
||||
Err(_) => None,
|
||||
}
|
||||
});
|
||||
(hooks.publish_texture)(published);
|
||||
|
||||
// Geometry: drain this frame's captured per-draw geometry.
|
||||
if let Some(caps) = self.frame_captures.as_mut() {
|
||||
let drained = std::mem::take(caps);
|
||||
(hooks.publish_geometry)(drained);
|
||||
}
|
||||
|
||||
// Frontbuffer: bulk read the tiled k_8_8_8_8 image and detile (A.1).
|
||||
if swap.frontbuffer_phys != 0 && swap.width > 0 && swap.height > 0 {
|
||||
let pitch_aligned =
|
||||
crate::tiled_address::align_pitch_to_macro_tile(swap.width);
|
||||
let total_tiled_bytes = (pitch_aligned * swap.height * 4) as usize;
|
||||
let fb_backing = crate::physical_to_backing(swap.frontbuffer_phys);
|
||||
let ok = (fb_backing as u64)
|
||||
.checked_add(total_tiled_bytes as u64)
|
||||
.is_some_and(|end| end <= 0x1_0000_0000);
|
||||
if ok {
|
||||
let mut tiled = vec![0u8; total_tiled_bytes];
|
||||
mem.read_bulk(fb_backing, &mut tiled);
|
||||
let mut linear = vec![0u8; (swap.width * swap.height * 4) as usize];
|
||||
if crate::tiled_address::detile_2d(
|
||||
&tiled,
|
||||
&mut linear,
|
||||
swap.width,
|
||||
swap.height,
|
||||
pitch_aligned,
|
||||
4,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
(hooks.publish_frontbuffer)(swap.width, swap.height, linear);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify: assemble the GPU-derived swap metadata for the UI redraw.
|
||||
let (last_draw_prim, last_draw_vertex_count) = match self.last_draw {
|
||||
Some(ds) => {
|
||||
let code = match ds.primitive {
|
||||
crate::draw_state::PrimitiveType::None => 0,
|
||||
crate::draw_state::PrimitiveType::PointList => 1,
|
||||
crate::draw_state::PrimitiveType::LineList => 2,
|
||||
crate::draw_state::PrimitiveType::LineStrip => 3,
|
||||
crate::draw_state::PrimitiveType::TriangleList => 4,
|
||||
crate::draw_state::PrimitiveType::TriangleFan => 5,
|
||||
crate::draw_state::PrimitiveType::TriangleStrip => 6,
|
||||
crate::draw_state::PrimitiveType::RectangleList => 8,
|
||||
crate::draw_state::PrimitiveType::QuadList => 13,
|
||||
crate::draw_state::PrimitiveType::Unknown(x) => x as u32,
|
||||
};
|
||||
(code, ds.vertex_count)
|
||||
}
|
||||
None => (0, 0),
|
||||
};
|
||||
let wsi = WorkerSwapInfo {
|
||||
frontbuffer_addr: swap.frontbuffer_phys,
|
||||
width: swap.width,
|
||||
height: swap.height,
|
||||
frame_index: swap.frame_index,
|
||||
draws_total: self.stats.draws_seen,
|
||||
packets_total: self.stats.packets_executed,
|
||||
last_draw_prim,
|
||||
last_draw_vertex_count,
|
||||
indirect_buffer_jumps: self.stats.indirect_buffer_jumps,
|
||||
wait_reg_mem_blocks: self.stats.wait_reg_mem_blocks,
|
||||
vs_blob_key: self.active_vs_key.unwrap_or(0),
|
||||
ps_blob_key: self.active_ps_key.unwrap_or(0),
|
||||
resolves_total: self.stats.resolves_total,
|
||||
resolves_copied_total: self.stats.resolves_copied_total,
|
||||
resolves_skipped_total: self.stats.resolves_skipped_total,
|
||||
unique_render_targets: self.stats.unique_render_targets,
|
||||
};
|
||||
(hooks.notify_swap)(wsi, mem);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -20,6 +20,7 @@ pub mod handle;
|
||||
pub mod mmio_region;
|
||||
pub mod pm4;
|
||||
pub mod primitive;
|
||||
pub mod prof;
|
||||
pub mod register_file;
|
||||
pub mod ring_drain;
|
||||
pub mod ring_view;
|
||||
@@ -39,7 +40,8 @@ pub use gpu_system::{
|
||||
};
|
||||
pub use handle::{
|
||||
DrainReply, GpuBackend, GpuCommand, GpuDigestSnapshot, GpuHandle, GpuWorker,
|
||||
shutdown_and_join_with_timeout, spawn_gpu_worker, spawn_noop_worker,
|
||||
UiPublishHooks, WorkerSwapInfo, shutdown_and_join_with_timeout, spawn_gpu_worker,
|
||||
spawn_noop_worker,
|
||||
};
|
||||
pub use mmio_region::build_region as build_mmio_region;
|
||||
pub use pm4::{
|
||||
|
||||
193
crates/xenia-gpu/src/prof.rs
Normal file
193
crates/xenia-gpu/src/prof.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
//! Lightweight env-gated wall-time profiler (probe-patch, UNCOMMITTED).
|
||||
//!
|
||||
//! Attributes emulator wall time to coarse buckets so we can tell whether the
|
||||
//! movie-playback slowdown is CPU-interpreter bound, texture-decode bound, or
|
||||
//! GPU-present bound. Enabled only when `XENIA_PROFILE` is set; the hot-path
|
||||
//! cost when disabled is a single relaxed atomic add of already-measured nanos
|
||||
//! (callers still pay `Instant::now()` — acceptable at the coarse boundaries we
|
||||
//! instrument: per basic-block, per texture upload, per present).
|
||||
//!
|
||||
//! Read the buckets with `xenia_gpu::prof::report(wall_ns)` at clean shutdown.
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static START: OnceLock<std::time::Instant> = OnceLock::new();
|
||||
|
||||
/// Lazily anchor the wall-time window (first call wins). Called from `add`.
|
||||
#[inline]
|
||||
fn mark_start() {
|
||||
START.get_or_init(std::time::Instant::now);
|
||||
}
|
||||
|
||||
/// Nanos since the profiler's first accounted event.
|
||||
pub fn wall_ns() -> u64 {
|
||||
START.get().map(|t| t.elapsed().as_nanos() as u64).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub static STEP_NS: AtomicU64 = AtomicU64::new(0); // guest interpreter (step_block)
|
||||
pub static STEP_INSTR: AtomicU64 = AtomicU64::new(0); // guest instructions retired
|
||||
pub static STEP_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static TEXDEC_NS: AtomicU64 = AtomicU64::new(0); // texture decode + host upload
|
||||
pub static TEXDEC_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
pub static TEXDEC_BYTES: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static PRESENT_NS: AtomicU64 = AtomicU64::new(0); // frontbuffer present
|
||||
pub static PRESENT_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static DRAW_NS: AtomicU64 = AtomicU64::new(0); // host draw submission
|
||||
pub static DRAW_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static KERNEL_NS: AtomicU64 = AtomicU64::new(0); // kernel HLE export dispatch
|
||||
pub static KERNEL_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static BUILD_NS: AtomicU64 = AtomicU64::new(0); // block decode / cache lookup
|
||||
pub static BUILD_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Cached on/off state so the per-block hot path never touches the
|
||||
/// environment. 0 = uninitialised, 1 = on, 2 = off.
|
||||
static ENABLED: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
|
||||
|
||||
/// Cheap (one relaxed load + branch) enabled check for hot paths. Resolves
|
||||
/// `XENIA_PROFILE` from the environment exactly once, then caches it.
|
||||
#[inline]
|
||||
pub fn is_on() -> bool {
|
||||
match ENABLED.load(Ordering::Relaxed) {
|
||||
1 => true,
|
||||
2 => false,
|
||||
_ => {
|
||||
let on = std::env::var_os("XENIA_PROFILE").is_some();
|
||||
ENABLED.store(if on { 1 } else { 2 }, Ordering::Relaxed);
|
||||
on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn enabled() -> bool {
|
||||
is_on()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn add(counter: &AtomicU64, v: u64) {
|
||||
mark_start();
|
||||
counter.fetch_add(v, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// RAII timer: adds elapsed nanos to `ns` and bumps `calls` on drop.
|
||||
pub struct ScopeTimer {
|
||||
t0: std::time::Instant,
|
||||
ns: &'static AtomicU64,
|
||||
calls: &'static AtomicU64,
|
||||
}
|
||||
|
||||
impl ScopeTimer {
|
||||
#[inline]
|
||||
pub fn new(ns: &'static AtomicU64, calls: &'static AtomicU64) -> Self {
|
||||
mark_start();
|
||||
Self { t0: std::time::Instant::now(), ns, calls }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ScopeTimer {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
self.ns.fetch_add(self.t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
|
||||
self.calls.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
static NEXT_REPORT_INSTR: AtomicU64 = AtomicU64::new(500_000_000);
|
||||
|
||||
/// Fire a snapshot every ~500M retired guest instructions (headless runs have
|
||||
/// no present() to piggyback on and may never reach the clean-exit report).
|
||||
#[inline]
|
||||
pub fn maybe_report_by_instr() {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
let instr = STEP_INSTR.load(Ordering::Relaxed);
|
||||
let thresh = NEXT_REPORT_INSTR.load(Ordering::Relaxed);
|
||||
if instr >= thresh
|
||||
&& NEXT_REPORT_INSTR
|
||||
.compare_exchange(thresh, thresh + 500_000_000, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
report(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the accumulated buckets against the profiler's own wall window.
|
||||
/// The argument is accepted for call-site convenience but ignored in favour
|
||||
/// of the internally-anchored window (`wall_ns()`).
|
||||
pub fn report(_ignored: u64) {
|
||||
let wall_ns = wall_ns();
|
||||
let g = |c: &AtomicU64| c.load(Ordering::Relaxed);
|
||||
let ms = |ns: u64| ns as f64 / 1e6;
|
||||
let pct = |ns: u64| {
|
||||
if wall_ns == 0 {
|
||||
0.0
|
||||
} else {
|
||||
100.0 * ns as f64 / wall_ns as f64
|
||||
}
|
||||
};
|
||||
let step_ns = g(&STEP_NS);
|
||||
let step_instr = g(&STEP_INSTR);
|
||||
let tex_ns = g(&TEXDEC_NS);
|
||||
let pres_ns = g(&PRESENT_NS);
|
||||
let draw_ns = g(&DRAW_NS);
|
||||
let mips = if step_ns == 0 {
|
||||
0.0
|
||||
} else {
|
||||
step_instr as f64 / (step_ns as f64 / 1e3) // instr / us = MIPS
|
||||
};
|
||||
eprintln!("=== XENIA_PROFILE (wall {:.1} ms) ===", ms(wall_ns));
|
||||
eprintln!(
|
||||
" interp step_block : {:>10.1} ms {:>5.1}% ({} calls, {} instr, {:.1} MIPS)",
|
||||
ms(step_ns),
|
||||
pct(step_ns),
|
||||
g(&STEP_CALLS),
|
||||
step_instr,
|
||||
mips
|
||||
);
|
||||
eprintln!(
|
||||
" texture decode+up : {:>10.1} ms {:>5.1}% ({} calls, {} MiB)",
|
||||
ms(tex_ns),
|
||||
pct(tex_ns),
|
||||
g(&TEXDEC_CALLS),
|
||||
g(&TEXDEC_BYTES) / (1024 * 1024)
|
||||
);
|
||||
eprintln!(
|
||||
" host draw submit : {:>10.1} ms {:>5.1}% ({} calls)",
|
||||
ms(draw_ns),
|
||||
pct(draw_ns),
|
||||
g(&DRAW_CALLS)
|
||||
);
|
||||
let kern_ns = g(&KERNEL_NS);
|
||||
let build_ns = g(&BUILD_NS);
|
||||
eprintln!(
|
||||
" kernel HLE export : {:>10.1} ms {:>5.1}% ({} calls)",
|
||||
ms(kern_ns),
|
||||
pct(kern_ns),
|
||||
g(&KERNEL_CALLS)
|
||||
);
|
||||
eprintln!(
|
||||
" block decode/cache: {:>10.1} ms {:>5.1}% ({} calls)",
|
||||
ms(build_ns),
|
||||
pct(build_ns),
|
||||
g(&BUILD_CALLS)
|
||||
);
|
||||
eprintln!(
|
||||
" frontbuffer present: {:>9.1} ms {:>5.1}% ({} calls)",
|
||||
ms(pres_ns),
|
||||
pct(pres_ns),
|
||||
g(&PRESENT_CALLS)
|
||||
);
|
||||
let accounted = step_ns + tex_ns + draw_ns + pres_ns + kern_ns + build_ns;
|
||||
eprintln!(
|
||||
" ---- accounted {:.1}% ; remainder (locks/kernel/scheduler/idle) {:.1}%",
|
||||
pct(accounted),
|
||||
pct(wall_ns.saturating_sub(accounted))
|
||||
);
|
||||
}
|
||||
@@ -652,6 +652,7 @@ impl TextureCache {
|
||||
}
|
||||
self.restale_total += 1;
|
||||
}
|
||||
let _prof_t0 = std::time::Instant::now();
|
||||
let bytes = match key.format {
|
||||
TextureFormat::K8 => decode_k8(&key, mem)?,
|
||||
TextureFormat::K8888 => decode_k8888_tiled(&key, mem)?,
|
||||
@@ -659,9 +660,37 @@ impl TextureCache {
|
||||
TextureFormat::Dxt1 => decode_dxt1_tiled(&key, mem)?,
|
||||
TextureFormat::Dxt2_3 => decode_dxt23_tiled(&key, mem)?,
|
||||
TextureFormat::Dxt4_5 => decode_dxt45_tiled(&key, mem)?,
|
||||
_ => return Err(DecodeError::UnsupportedFormat),
|
||||
_ => {
|
||||
// XENIA_TEX_REJECT_LOG diagnostic (STEP 87, read-only): the
|
||||
// intro video uploads its YUV420 planes as `k_8` linear
|
||||
// textures; if we have no decoder for the requested format we
|
||||
// reject it here and it never reaches the GPU (→ black video).
|
||||
// Rate-limited so a per-frame flood stays grep-able.
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
static N: AtomicUsize = AtomicUsize::new(0);
|
||||
let n = N.fetch_add(1, Ordering::Relaxed);
|
||||
if n < 120 {
|
||||
tracing::warn!(
|
||||
fmt = ?key.format,
|
||||
w = key.width,
|
||||
h = key.height,
|
||||
base = format_args!("0x{:08x}", key.base_address),
|
||||
dim = ?key.dimension,
|
||||
pitch = key.pitch_texels,
|
||||
n,
|
||||
"TEX-REJECT: unsupported texture format (no host decoder)"
|
||||
);
|
||||
}
|
||||
return Err(DecodeError::UnsupportedFormat);
|
||||
}
|
||||
};
|
||||
self.decodes_total += 1;
|
||||
{
|
||||
use crate::prof;
|
||||
prof::add(&prof::TEXDEC_NS, _prof_t0.elapsed().as_nanos() as u64);
|
||||
prof::add(&prof::TEXDEC_CALLS, 1);
|
||||
prof::add(&prof::TEXDEC_BYTES, bytes.len() as u64);
|
||||
}
|
||||
let entry = CachedTexture {
|
||||
key,
|
||||
version_when_uploaded: current_version,
|
||||
|
||||
@@ -460,6 +460,16 @@ impl EmitCtx {
|
||||
let b = src_operand(alu.src_b, alu.src_b_is_temp, alu.src_b_swiz, alu.src_b_negate, const_base);
|
||||
let c = src_operand(alu.src_c, alu.src_c_is_temp, alu.src_c_swiz, alu.src_c_negate, const_base);
|
||||
|
||||
if (42..=47).contains(&alu.scalar_opcode) && std::env::var("XENIA_SC_LOG").is_ok() {
|
||||
eprintln!(
|
||||
"SC-OP opc={} sa={} atmp={} asw={:#04x} sb={} btmp={} bsw={:#04x} sc={} ctmp={} csw={:#04x}",
|
||||
alu.scalar_opcode,
|
||||
alu.src_a, alu.src_a_is_temp as u8, alu.src_a_swiz,
|
||||
alu.src_b, alu.src_b_is_temp as u8, alu.src_b_swiz,
|
||||
alu.src_c, alu.src_c_is_temp as u8, alu.src_c_swiz,
|
||||
);
|
||||
}
|
||||
|
||||
// Vector pipe.
|
||||
if alu.vector_write_mask != 0 {
|
||||
let expr = vector_expr(alu.vector_opcode, &a, &b, &c)
|
||||
@@ -501,6 +511,9 @@ impl EmitCtx {
|
||||
let expr = match scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps") {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
if std::env::var("XENIA_BIND_LOG").is_ok() {
|
||||
eprintln!("SCL-UNSUPPORTED opcode={:#04x} ({})", alu.scalar_opcode, alu.scalar_opcode);
|
||||
}
|
||||
return Err(reject::SCL_OP_UNSUPPORTED);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1351,10 +1351,16 @@ fn nt_read_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState
|
||||
mem.write_bulk(buffer, slice);
|
||||
*position = start_pos + avail as u64;
|
||||
|
||||
tracing::info!(
|
||||
"NtReadFile: {} bytes from {:?} @ {} (handle={:#x})",
|
||||
avail, path, start_pos, handle,
|
||||
);
|
||||
{
|
||||
// STEP 51 — tag NtReadFile with the calling tid + cycle so the
|
||||
// window-prefetch thread can be correlated against tid24's demux read.
|
||||
let hw_id = state.scheduler.current_hw_id().unwrap_or(0);
|
||||
let rtid = state.scheduler.tid(hw_id).unwrap_or(0);
|
||||
tracing::info!(
|
||||
"NtReadFile: {} bytes from {:?} @ {} (handle={:#x}) tid={} cycle={}",
|
||||
avail, path, start_pos, handle, rtid, ctx.cycle_count,
|
||||
);
|
||||
}
|
||||
write_io_status_block(mem, io_status_block, STATUS_SUCCESS as u32, avail as u32);
|
||||
ctx.gpr[3] = STATUS_SUCCESS;
|
||||
signal_io_completion_event(state, event_handle);
|
||||
@@ -3130,6 +3136,20 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
// comment above). The drain below consumes only the packets the game has
|
||||
// legitimately advanced the write-pointer over.
|
||||
|
||||
// A.5 threaded `--ui`: when the GPU runs on the worker thread AND a UI is
|
||||
// attached, vd_swap must not block-drain or publish on the emulation
|
||||
// thread. The worker drains the ring continuously and runs the entire
|
||||
// per-swap UI publish itself (see `GpuSystem::run_ui_publish`) when it
|
||||
// consumes the in-stream PM4_XE_SWAP. So here we've only filled the
|
||||
// reserved ring slot (above) and return immediately — this is the A.5
|
||||
// decoupling that lifts the ~12 ms/frame GPU work off the CPU thread.
|
||||
// Inline `--ui` (the default) and headless-threaded are unaffected:
|
||||
// `as_inline()` is `Some` for inline, and `state.ui` is `None` headless.
|
||||
if state.ui.is_some() && state.gpu.as_inline().is_none() {
|
||||
ctx.gpr[3] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Drain the ring up to whatever the game has actually submitted; any
|
||||
// in-stream `PM4_INTERRUPT` / draw packets execute in order. The
|
||||
// reserved-slot PM4_XE_SWAP is consumed by the GPU only once the game
|
||||
@@ -3179,14 +3199,39 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
// Do this before `notify_swap` so by the time the UI processes the
|
||||
// SwapInfo the matching assets are visible through `UiHandles`.
|
||||
if let Some(ref ui) = state.ui {
|
||||
let blobs: std::collections::HashMap<u32, Vec<u32>> = gpu_inline
|
||||
.shader_blobs
|
||||
.iter()
|
||||
.map(|(k, b)| (*k, b.dwords.clone()))
|
||||
.collect();
|
||||
// PERF (--ui): only rebuild + clone the shader-blob map when it
|
||||
// actually changed since the last swap; otherwise pass `None` and
|
||||
// the UI keeps its previous map. Constants are always published.
|
||||
let blobs: Option<std::collections::HashMap<u32, Vec<u32>>> =
|
||||
if gpu_inline.shader_blobs_version != gpu_inline.last_published_blobs_version {
|
||||
gpu_inline.last_published_blobs_version = gpu_inline.shader_blobs_version;
|
||||
Some(
|
||||
gpu_inline
|
||||
.shader_blobs
|
||||
.iter()
|
||||
.map(|(k, b)| (*k, b.dwords.clone()))
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let constants = xenia_gpu::xenos_constants::XenosConstantsBlock::snapshot(
|
||||
&gpu_inline.register_file,
|
||||
);
|
||||
if std::env::var("XENIA_CONST_LOG").is_ok() {
|
||||
let nz: Vec<usize> = constants
|
||||
.alu
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, v)| v.iter().any(|c| *c != 0.0))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
eprintln!(
|
||||
"CONST-LOG alu_nonzero={} idxs={:?} c254={:?} c255={:?} c510={:?} c511={:?}",
|
||||
nz.len(), nz, constants.alu[254], constants.alu[255],
|
||||
constants.alu[510], constants.alu[511]
|
||||
);
|
||||
}
|
||||
ui.publish_assets(blobs, constants);
|
||||
|
||||
// P5b: publish the texture the last draw's *active pixel shader*
|
||||
@@ -3289,25 +3334,25 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
let pitch_aligned =
|
||||
xenia_gpu::tiled_address::align_pitch_to_macro_tile(swap.width);
|
||||
let total_tiled_bytes = (pitch_aligned * swap.height * 4) as usize;
|
||||
// The guest address is 32-bit virtual but in the physical heap;
|
||||
// safer to cap the read at the known total size to avoid OOB.
|
||||
let mut tiled = Vec::with_capacity(total_tiled_bytes);
|
||||
let mut ok = true;
|
||||
// The frontbuffer is a guest *physical* address; project onto the
|
||||
// committed backing window (see `xenia_gpu::physical_to_backing`)
|
||||
// so the present reads the pixels the GPU resolved, not a stale /
|
||||
// zero mirror page.
|
||||
let fb_backing = xenia_gpu::physical_to_backing(swap.frontbuffer_phys);
|
||||
for i in 0..total_tiled_bytes {
|
||||
// read_u8 is cheap — the VirtualMemory handler returns 0
|
||||
// for unmapped pages so we get a recognisable dark frame
|
||||
// rather than a crash if the address turned out bogus.
|
||||
let addr = fb_backing.wrapping_add(i as u32);
|
||||
tiled.push(mem.read_u8(addr));
|
||||
if addr < fb_backing {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
// PERF (--ui): read the whole tiled frontbuffer in one bulk copy
|
||||
// instead of ~3.7 MB of per-byte `read_u8` through the MMIO
|
||||
// handler — that byte loop dominated the emulation thread under
|
||||
// `--ui` (~15 ms/swap). Guard against a u32-wrap / out-of-window
|
||||
// read (the physical backing lives in [0x4000_0000, 0x5FFF_FFFF]
|
||||
// within the 4 GiB reservation), reproducing the old wrap
|
||||
// sentinel. Uncommitted pages inside the window read as host-zero
|
||||
// (untouched mmap), matching the old "dark frame" fallback.
|
||||
let ok = (fb_backing as u64)
|
||||
.checked_add(total_tiled_bytes as u64)
|
||||
.is_some_and(|end| end <= 0x1_0000_0000);
|
||||
let mut tiled = vec![0u8; total_tiled_bytes];
|
||||
if ok {
|
||||
mem.read_bulk(fb_backing, &mut tiled);
|
||||
}
|
||||
if ok {
|
||||
let mut linear = vec![0u8; (swap.width * swap.height * 4) as usize];
|
||||
@@ -4470,12 +4515,30 @@ fn nt_yield_execution(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut Ker
|
||||
ctx.gpr[3] = STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/// RE diagnostic (milestone-2 intro-video): `XENIA_LOG_RESUMES=1` makes
|
||||
/// `Ke/NtResumeThread` emit a `RESUME` line naming the resumed thread's
|
||||
/// tid, start_entry (so decode workers — entries 0x82506588/0x825065b8 —
|
||||
/// are identifiable), current pc and the prior suspend_count. Cold-path
|
||||
/// (resume is rare); inert unless the env is set. Read-only diagnostic.
|
||||
fn log_resumes_enabled() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static EN: OnceLock<bool> = OnceLock::new();
|
||||
*EN.get_or_init(|| std::env::var("XENIA_LOG_RESUMES").is_ok())
|
||||
}
|
||||
|
||||
fn ke_resume_thread(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||
let raw = ctx.gpr[3] as u32;
|
||||
let handle = resolve_pseudo_handle(state, raw);
|
||||
match state.scheduler.find_by_handle(handle) {
|
||||
Some(r) => {
|
||||
state.scheduler.resume_ref(r);
|
||||
let prev = state.scheduler.resume_ref(r);
|
||||
if log_resumes_enabled() {
|
||||
let t = state.scheduler.thread(r);
|
||||
tracing::info!(
|
||||
"RESUME(Ke) tid={} start_entry={:#010x} pc={:#010x} prev_suspend={}",
|
||||
t.tid, t.start_entry, t.ctx.pc, prev,
|
||||
);
|
||||
}
|
||||
ctx.gpr[3] = STATUS_SUCCESS;
|
||||
}
|
||||
None => {
|
||||
@@ -4491,6 +4554,13 @@ fn nt_resume_thread(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelS
|
||||
match state.scheduler.find_by_handle(handle) {
|
||||
Some(r) => {
|
||||
let prev = state.scheduler.resume_ref(r);
|
||||
if log_resumes_enabled() {
|
||||
let t = state.scheduler.thread(r);
|
||||
tracing::info!(
|
||||
"RESUME(Nt) tid={} start_entry={:#010x} pc={:#010x} prev_suspend={}",
|
||||
t.tid, t.start_entry, t.ctx.pc, prev,
|
||||
);
|
||||
}
|
||||
if prev_ptr != 0 {
|
||||
mem.write_u32(prev_ptr, prev);
|
||||
}
|
||||
|
||||
@@ -909,6 +909,30 @@ impl KernelState {
|
||||
/// Record a Set/Pulse/Release/etc. call against a handle. `aux` is the
|
||||
/// previous signal state (or per-export-specific data).
|
||||
pub fn audit_signal(&mut self, handle: u32, lr: u32, source: &'static str, aux: u64) {
|
||||
// RE diagnostic (milestone-2 intro-video). `XENIA_LOG_SIGNAL=0x<va>`
|
||||
// (or `0xffffffff` for all) logs every event signal/pulse targeting
|
||||
// that object — to pin who, if anyone, signals the engine event
|
||||
// 0x40D10214 that tid23 waits on (the worker-resume gate). Read-only.
|
||||
{
|
||||
use std::sync::OnceLock;
|
||||
static SIG_T: OnceLock<u32> = OnceLock::new();
|
||||
let t = *SIG_T.get_or_init(|| {
|
||||
std::env::var("XENIA_LOG_SIGNAL")
|
||||
.ok()
|
||||
.and_then(|s| u32::from_str_radix(s.trim().trim_start_matches("0x"), 16).ok())
|
||||
.unwrap_or(0)
|
||||
});
|
||||
if t != 0 && (t == u32::MAX || t == handle) {
|
||||
let tid = self
|
||||
.scheduler
|
||||
.current_hw_id()
|
||||
.and_then(|h| self.scheduler.tid(h))
|
||||
.unwrap_or(0);
|
||||
tracing::info!(
|
||||
"SIGNAL src={source} handle={handle:#010x} tid={tid} lr={lr:#010x} prev_signaled={aux}"
|
||||
);
|
||||
}
|
||||
}
|
||||
if !self.audit.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,8 +118,11 @@ pub struct UiBridge {
|
||||
/// execute the guest draw. Split from `post_swap` so the asset wire
|
||||
/// stays optional — if the UI doesn't need them (headless mode) the
|
||||
/// closure is a no-op.
|
||||
/// `blobs` is `Some` only when the shader-blob map changed since the
|
||||
/// last publish (PERF: avoids re-cloning it every swap); `None` = the UI
|
||||
/// keeps its previous map. Constants are always published.
|
||||
pub publish_xenos_assets:
|
||||
Arc<dyn Fn(HashMap<u32, Vec<u32>>, XenosConstantsBlock) + Send + Sync>,
|
||||
Arc<dyn Fn(Option<HashMap<u32, Vec<u32>>>, XenosConstantsBlock) + Send + Sync>,
|
||||
/// P4 frontbuffer publish: at each `VdSwap`, the kernel CPU-side
|
||||
/// detiles the guest frontbuffer (k_8_8_8_8 Tiled2D) into a linear
|
||||
/// RGBA8 buffer and hands it to the UI. The closure receives
|
||||
@@ -168,7 +171,7 @@ impl UiBridge {
|
||||
/// draw captured in this frame.
|
||||
pub fn publish_assets(
|
||||
&self,
|
||||
blobs: HashMap<u32, Vec<u32>>,
|
||||
blobs: Option<HashMap<u32, Vec<u32>>>,
|
||||
constants: XenosConstantsBlock,
|
||||
) {
|
||||
(self.publish_xenos_assets)(blobs, constants);
|
||||
|
||||
@@ -124,8 +124,12 @@ pub fn build(proxy: EventLoopProxy<SwapEvent>) -> (UiHandles, UiBridge) {
|
||||
let blobs = Arc::clone(&shader_blobs);
|
||||
let consts = Arc::clone(&xenos_constants);
|
||||
Arc::new(move |new_blobs, new_consts| {
|
||||
if let Ok(mut g) = blobs.lock() {
|
||||
*g = new_blobs;
|
||||
// `new_blobs` is `Some` only when the map changed since the
|
||||
// last publish; `None` keeps the previous map (PERF).
|
||||
if let Some(new_blobs) = new_blobs {
|
||||
if let Ok(mut g) = blobs.lock() {
|
||||
*g = new_blobs;
|
||||
}
|
||||
}
|
||||
if let Ok(mut g) = consts.lock() {
|
||||
*g = new_consts;
|
||||
|
||||
@@ -97,6 +97,14 @@ pub struct RenderState {
|
||||
/// and hands the view to the xenos pipeline's `@group(1) @binding(0)`
|
||||
/// slot.
|
||||
host_texture_cache: crate::texture_cache_host::TextureCacheHost,
|
||||
|
||||
/// PERF (--ui): parsed + WGSL-packed shader caches keyed on the guest
|
||||
/// blob key. Shader microcode blobs are immutable once loaded, so we
|
||||
/// parse/pack each key exactly once instead of every draw every frame
|
||||
/// (`dispatch_xenos_captures` re-parsed on the hot path). Keyed on the
|
||||
/// u32 blob key; key 0 is the empty/flat shader.
|
||||
parsed_shader_cache: std::collections::HashMap<u32, xenia_gpu::ucode::ParsedShader>,
|
||||
packed_shader_cache: std::collections::HashMap<u32, Vec<u32>>,
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
@@ -137,11 +145,32 @@ impl RenderState {
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(surface_caps.formats[0]);
|
||||
let present_mode = if surface_caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
|
||||
wgpu::PresentMode::Mailbox
|
||||
} else {
|
||||
wgpu::PresentMode::Fifo
|
||||
};
|
||||
// Default: Mailbox if available (non-vsync-blocking), else Fifo.
|
||||
// `XENIA_PRESENT_MODE=immediate|mailbox|fifo` overrides — but only
|
||||
// if the surface actually supports the requested mode; otherwise we
|
||||
// fall back to the default so a bad env value can't break present.
|
||||
let default_present_mode =
|
||||
if surface_caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
|
||||
wgpu::PresentMode::Mailbox
|
||||
} else {
|
||||
wgpu::PresentMode::Fifo
|
||||
};
|
||||
let present_mode = std::env::var("XENIA_PRESENT_MODE")
|
||||
.ok()
|
||||
.and_then(|v| match v.trim().to_ascii_lowercase().as_str() {
|
||||
"immediate" => Some(wgpu::PresentMode::Immediate),
|
||||
"mailbox" => Some(wgpu::PresentMode::Mailbox),
|
||||
"fifo" => Some(wgpu::PresentMode::Fifo),
|
||||
"fifo-relaxed" | "fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|m| surface_caps.present_modes.contains(m))
|
||||
.unwrap_or(default_present_mode);
|
||||
let frame_latency = std::env::var("XENIA_FRAME_LATENCY")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse::<u32>().ok())
|
||||
.filter(|&n| n >= 1)
|
||||
.unwrap_or(2);
|
||||
let max_dim = adapter_limits.max_texture_dimension_2d.max(1);
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
@@ -151,7 +180,7 @@ impl RenderState {
|
||||
present_mode,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
desired_maximum_frame_latency: frame_latency,
|
||||
};
|
||||
surface.configure(&device, &config);
|
||||
|
||||
@@ -456,6 +485,8 @@ impl RenderState {
|
||||
first_dispatch_logged: false,
|
||||
first_translator_compile_logged: false,
|
||||
host_texture_cache: crate::texture_cache_host::TextureCacheHost::new(),
|
||||
parsed_shader_cache: std::collections::HashMap::new(),
|
||||
packed_shader_cache: std::collections::HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -600,6 +631,8 @@ impl RenderState {
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
let _prof_g =
|
||||
xenia_gpu::prof::ScopeTimer::new(&xenia_gpu::prof::DRAW_NS, &xenia_gpu::prof::DRAW_CALLS);
|
||||
let _span = tracing::debug_span!(
|
||||
"ui.xenos.dispatch",
|
||||
count,
|
||||
@@ -807,18 +840,43 @@ impl RenderState {
|
||||
xenos_pipeline.set_texture_slots(device, &slot_views);
|
||||
}
|
||||
}
|
||||
let raw_vs = shader_blobs.get(&cap.vs_key).cloned().unwrap_or_default();
|
||||
let raw_ps = shader_blobs.get(&cap.ps_key).cloned().unwrap_or_default();
|
||||
let parsed_vs = xenia_gpu::ucode::parse_shader(&raw_vs);
|
||||
let parsed_ps = xenia_gpu::ucode::parse_shader(&raw_ps);
|
||||
if seen.insert((0u8, cap.vs_key)) {
|
||||
// PERF (--ui): parse + pack each shader blob ONCE (blobs are
|
||||
// immutable once loaded) instead of every draw every frame.
|
||||
// Clone the cached values out so the mutable borrow of the caches
|
||||
// is released before the `self.xenos_pipeline` uses below.
|
||||
let (vs_key, ps_key) = (cap.vs_key, cap.ps_key);
|
||||
let parsed_vs = self
|
||||
.parsed_shader_cache
|
||||
.entry(vs_key)
|
||||
.or_insert_with(|| {
|
||||
let raw = shader_blobs.get(&vs_key).cloned().unwrap_or_default();
|
||||
xenia_gpu::ucode::parse_shader(&raw)
|
||||
})
|
||||
.clone();
|
||||
let parsed_ps = self
|
||||
.parsed_shader_cache
|
||||
.entry(ps_key)
|
||||
.or_insert_with(|| {
|
||||
let raw = shader_blobs.get(&ps_key).cloned().unwrap_or_default();
|
||||
xenia_gpu::ucode::parse_shader(&raw)
|
||||
})
|
||||
.clone();
|
||||
if seen.insert((0u8, vs_key)) {
|
||||
xenia_gpu::shader_metrics::emit_for(&parsed_vs, "vs");
|
||||
}
|
||||
if seen.insert((1u8, cap.ps_key)) {
|
||||
if seen.insert((1u8, ps_key)) {
|
||||
xenia_gpu::shader_metrics::emit_for(&parsed_ps, "ps");
|
||||
}
|
||||
let vs_packed = xenia_gpu::ucode::pack_for_wgsl(&parsed_vs);
|
||||
let ps_packed = xenia_gpu::ucode::pack_for_wgsl(&parsed_ps);
|
||||
let vs_packed = self
|
||||
.packed_shader_cache
|
||||
.entry(vs_key)
|
||||
.or_insert_with(|| xenia_gpu::ucode::pack_for_wgsl(&parsed_vs))
|
||||
.clone();
|
||||
let ps_packed = self
|
||||
.packed_shader_cache
|
||||
.entry(ps_key)
|
||||
.or_insert_with(|| xenia_gpu::ucode::pack_for_wgsl(&parsed_ps))
|
||||
.clone();
|
||||
// Upload this draw's shader + constants + real vertex window.
|
||||
self.xenos_pipeline.upload_shader_and_constants(
|
||||
&self.queue,
|
||||
@@ -881,6 +939,36 @@ impl RenderState {
|
||||
cap.ps_key,
|
||||
rstate,
|
||||
);
|
||||
// Log only the "interesting" draws — multi-texture or any non-K8888
|
||||
// (e.g. the movie's k_8 YUV planes) — so the boot's single-K8888
|
||||
// quads don't flood the cap before the movie composites.
|
||||
let interesting = cap.textures.len() > 1
|
||||
|| cap.textures.iter().any(|(_, k, ..)| {
|
||||
!matches!(k.format, xenia_gpu::texture_cache::TextureFormat::K8888)
|
||||
});
|
||||
if std::env::var("XENIA_BIND_LOG").is_ok() && interesting {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
static N: AtomicUsize = AtomicUsize::new(0);
|
||||
let n = N.fetch_add(1, Ordering::Relaxed);
|
||||
if n < 200 {
|
||||
let slots: Vec<u8> = cap.textures.iter().map(|(s, ..)| *s).collect();
|
||||
let fmts: Vec<String> = cap
|
||||
.textures
|
||||
.iter()
|
||||
.map(|(_, k, ..)| format!("{:?}", k.format))
|
||||
.collect();
|
||||
eprintln!(
|
||||
"BIND-LOG ps={:#x} vs={:#x} ntex={} slots={:?} fmts={:?} translated={} verts={}",
|
||||
cap.ps_key,
|
||||
cap.vs_key,
|
||||
cap.textures.len(),
|
||||
slots,
|
||||
fmts,
|
||||
served_translated,
|
||||
cap.host_vertex_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
if served_translated {
|
||||
self.xenos_dispatches_translator =
|
||||
self.xenos_dispatches_translator.saturating_add(1);
|
||||
@@ -901,6 +989,26 @@ impl RenderState {
|
||||
self.xenos_draws_rendered = self
|
||||
.xenos_draws_rendered
|
||||
.saturating_add(captures.len() as u64);
|
||||
// Frontbuffer readback for offline color verification (XENIA_DUMP_FRAME).
|
||||
// Counts frames that contained a movie (k_8 YUV) draw and dumps the
|
||||
// composited frontbuffer at a few fade-in stages so the YUV→RGB output
|
||||
// can be inspected as a raw RGBA image without needing a live viewer.
|
||||
if std::env::var("XENIA_DUMP_FRAME").is_ok() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
static MOVIE_FRAMES: AtomicUsize = AtomicUsize::new(0);
|
||||
let has_movie = captures.iter().any(|c| {
|
||||
c.textures.iter().any(|(_, k, _, _)| {
|
||||
matches!(k.format, xenia_gpu::texture_cache::TextureFormat::K8)
|
||||
})
|
||||
});
|
||||
if has_movie {
|
||||
let n = MOVIE_FRAMES.fetch_add(1, Ordering::Relaxed);
|
||||
if n % 20 == 0 && n <= 2000 {
|
||||
let dir = "/tmp/claude-1000/-home-fabi-RE---Project-Sylpheed/c5711e5b-8a9c-410c-860d-662365e450a4/scratchpad";
|
||||
self.dump_frontbuffer(&format!("{dir}/fb_movie_{n:04}.raw"));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.real_geometry_draws = self
|
||||
.real_geometry_draws
|
||||
.saturating_add(real_count as u64);
|
||||
@@ -915,6 +1023,64 @@ impl RenderState {
|
||||
real_count
|
||||
}
|
||||
|
||||
/// Diagnostic: copy the current frontbuffer back to the CPU and write it
|
||||
/// as tight RGBA8 bytes (`width*height*4`) to `path`. Blocks on the GPU.
|
||||
/// Used to verify rendered colors offline (e.g. the intro-video YUV→RGB).
|
||||
fn dump_frontbuffer(&self, path: &str) {
|
||||
let (w, h) = self.frontbuffer_size;
|
||||
if w == 0 || h == 0 {
|
||||
return;
|
||||
}
|
||||
let bpp = 4u32;
|
||||
let unpadded = w * bpp;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("frontbuffer readback"),
|
||||
size: (padded * h) as u64,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("fb-dump") });
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::ImageCopyTexture {
|
||||
texture: &self.frontbuffer_tex,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::ImageCopyBuffer {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded),
|
||||
rows_per_image: Some(h),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: w,
|
||||
height: h,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
let slice = buffer.slice(..);
|
||||
slice.map_async(wgpu::MapMode::Read, |_| {});
|
||||
self.device.poll(wgpu::Maintain::Wait);
|
||||
let data = slice.get_mapped_range();
|
||||
let mut out = Vec::with_capacity((unpadded * h) as usize);
|
||||
for row in 0..h as usize {
|
||||
let s = row * padded as usize;
|
||||
out.extend_from_slice(&data[s..s + unpadded as usize]);
|
||||
}
|
||||
drop(data);
|
||||
buffer.unmap();
|
||||
let _ = std::fs::write(path, &out);
|
||||
eprintln!("FRONTBUFFER-DUMP {w}x{h} -> {path} ({} bytes)", out.len());
|
||||
}
|
||||
|
||||
/// Count of distinct translator pipelines compiled so far. Surfaced
|
||||
/// on the HUD as `xlated=N` to make "is P7 working?" observable.
|
||||
pub fn translated_pipeline_count(&self) -> usize {
|
||||
@@ -1072,8 +1238,19 @@ impl RenderState {
|
||||
pass.draw(0..self.hud_vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
let _prof_t0 = std::time::Instant::now();
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
frame.present();
|
||||
{
|
||||
use xenia_gpu::prof;
|
||||
prof::add(&prof::PRESENT_NS, _prof_t0.elapsed().as_nanos() as u64);
|
||||
let n = prof::PRESENT_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
// Periodic snapshot so a SIGTERM'd (timed-out) movie run still
|
||||
// yields a profile — the clean-exit report may never be reached.
|
||||
if prof::enabled() && n % 500 == 0 {
|
||||
prof::report(0);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1107,6 +1284,9 @@ fn ensure_translated_pipeline(
|
||||
"reason" => reason,
|
||||
)
|
||||
.increment(1);
|
||||
if std::env::var("XENIA_BIND_LOG").is_ok() {
|
||||
eprintln!("TRANSLATE-REJECT vs={vs_key:#x} ps={ps_key:#x} stage=vs reason={reason}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1119,10 +1299,18 @@ fn ensure_translated_pipeline(
|
||||
"reason" => reason,
|
||||
)
|
||||
.increment(1);
|
||||
if std::env::var("XENIA_BIND_LOG").is_ok() {
|
||||
eprintln!("TRANSLATE-REJECT vs={vs_key:#x} ps={ps_key:#x} stage=ps reason={reason}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let wgsl = combine_stages(&vs_body, &ps_body);
|
||||
if let Ok(dir) = std::env::var("XENIA_DUMP_WGSL") {
|
||||
let path = format!("{dir}/wgsl_vs{vs_key:#x}_ps{ps_key:#x}.wgsl");
|
||||
let _ = std::fs::write(&path, &wgsl);
|
||||
eprintln!("DUMP-WGSL wrote {path}");
|
||||
}
|
||||
xenos_pipeline.insert_translated(device, vs_key, ps_key, &wgsl)
|
||||
}
|
||||
|
||||
@@ -1143,6 +1331,7 @@ fn make_frontbuffer(device: &wgpu::Device, w: u32, h: u32) -> (wgpu::Texture, wg
|
||||
// this texture instead of only consuming CPU-side raw scrapes.
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_DST
|
||||
| wgpu::TextureUsages::COPY_SRC
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
43
sylph-run.sh
Executable file
43
sylph-run.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# sylph-run.sh — run the ours xenia-rs emulator N times and report the standard
|
||||
# intro-video oracles. Inherits any XENIA_* probe knobs from the caller's env
|
||||
# (e.g. XENIA_FORCE_TID=24, XENIA_STARVE_LIMIT=16, XENIA_LOG_SIGNAL=0x40d10214).
|
||||
#
|
||||
# Usage: [XENIA_*=... ] sylph-run.sh [runs] [n_instr] [timeout_s] [extra_grep_regex]
|
||||
# runs default 6
|
||||
# n_instr default 3000000000
|
||||
# timeout_s default 180 (boot is DETERMINISTIC and reaches the intro
|
||||
# movie's first on-screen frame at ~81s wall; the movie then
|
||||
# plays reliably. The old 90s default sat ~9s above that point,
|
||||
# so host-load wall-clock jitter cut some runs off just before
|
||||
# the movie — the apparent "~1/3 of runs" flakiness. 180s clears
|
||||
# it with margin. Guest-instruction execution is identical run
|
||||
# to run; only wall time varies with host speed.)
|
||||
# extra_grep optional ERE; matching stdout lines are saved per-run for ad-hoc inspection
|
||||
#
|
||||
# Always-on oracles: source-read 0x824ff708 count (>1 = feeder looped), decode-worker
|
||||
# resumes (tid25 start_entry 0x82506588 / tid26 0x825065b8). Full per-run log kept under
|
||||
# /tmp/sylph-run/.
|
||||
set -u
|
||||
cd "/home/fabi/RE - Project Sylpheed/xenia-rs" || exit 2
|
||||
RUNS="${1:-6}"; N="${2:-3000000000}"; TO="${3:-180}"; XGREP="${4:-}"
|
||||
OUT=/tmp/sylph-run; mkdir -p "$OUT"
|
||||
BIN=./target/release/xenia-rs
|
||||
[ -x "$BIN" ] || { echo "build first: (cd xenia-rs && export CARGO_BUILD_JOBS=4 && cargo build --release)"; exit 3; }
|
||||
|
||||
echo "sylph-run: runs=$RUNS n=$N timeout=${TO}s knobs=[$(env | grep -oE 'XENIA_[A-Z_]+=[^ ]*' | tr '\n' ' ')]"
|
||||
for i in $(seq 1 "$RUNS"); do
|
||||
F="$OUT/run_$i.log"; REC="$OUT/rec_$i.txt"
|
||||
RUST_LOG="${RUST_LOG:-warn,xenia_kernel::exports=info,xenia_kernel::state=info}" \
|
||||
XENIA_DISPATCH_REC=1 XENIA_DISPATCH_REC_SITES="${XENIA_DISPATCH_REC_SITES:-0x824ff708}" \
|
||||
XENIA_DISPATCH_REC_OUT="$REC" \
|
||||
timeout "$TO" "$BIN" exec sylpheed.iso -n "$N" >"$F" 2>&1
|
||||
pkill -x xenia-rs 2>/dev/null
|
||||
sc=$(grep 0x824ff708 "$REC" 2>/dev/null | awk '{print $3}' | head -1)
|
||||
w25=$(grep -c "RESUME.*0x82506588" "$F" 2>/dev/null)
|
||||
w26=$(grep -c "RESUME.*0x825065b8" "$F" 2>/dev/null)
|
||||
xg=""; [ -n "$XGREP" ] && xg=" match($XGREP)=$(grep -cE "$XGREP" "$F" 2>/dev/null)"
|
||||
reached="movie"; [ "${sc:-0}" = "0" ] && reached="no-movie"
|
||||
echo " run $i: [$reached] source-read=${sc:-0} tid25-resume=$w25 tid26-resume=$w26$xg (log: $F)"
|
||||
done
|
||||
echo "done. per-run logs in $OUT/. Reliable: source-read>1 = feeder looped; tid25/26 resume = pipeline progressed."
|
||||
66
zq.py
Executable file
66
zq.py
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sylpheed static-analysis helper over DuckDB `sylpheed.db`.
|
||||
|
||||
Hides the gotchas: DECIMAL bounds (DuckDB rejects 0x literals), read-only connect,
|
||||
and the fact that the engine vtable / rdata is NOT in the DB (read it from guest
|
||||
memory with `xenia-rs exec ... --dump-addr=0x<va>` instead).
|
||||
|
||||
Usage:
|
||||
zq.py dis <lo_hex> <hi_hex> # disassemble [lo,hi)
|
||||
zq.py fn <pc_hex> # function containing pc (address,name,end)
|
||||
zq.py xref <target_hex> # xrefs whose target == addr (callers)
|
||||
zq.py callers <vtable_off_dec> # call-sites of vtable slot at byte offset N
|
||||
# (finds `lwz r11, N(r11)` + reports the fn)
|
||||
zq.py grep <substr> # instructions whose operands LIKE %substr%
|
||||
zq.py find <word_hex> # instructions whose raw word == value (e.g. a ptr)
|
||||
"""
|
||||
import duckdb, sys
|
||||
|
||||
DB = '/home/fabi/RE - Project Sylpheed/xenia-rs/sylpheed.db'
|
||||
c = duckdb.connect(DB, read_only=True)
|
||||
H = lambda x: '0x%08x' % x
|
||||
|
||||
|
||||
def _fn(pc):
|
||||
r = c.execute('SELECT address,name,end_address FROM functions WHERE address<=? AND end_address>? '
|
||||
'ORDER BY address DESC LIMIT 1', [pc, pc]).fetchall()
|
||||
return f'{r[0][1]}({H(r[0][0])})' if r else '?'
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__); return
|
||||
cmd = sys.argv[1]
|
||||
if cmd == 'dis':
|
||||
lo, hi = int(sys.argv[2], 16), int(sys.argv[3], 16)
|
||||
for a, m, o in c.execute('SELECT address,mnemonic,operands FROM instructions '
|
||||
'WHERE address>=? AND address<? ORDER BY address', [lo, hi]).fetchall():
|
||||
print(H(a), m, o)
|
||||
elif cmd == 'fn':
|
||||
print(_fn(int(sys.argv[2], 16)))
|
||||
elif cmd == 'xref':
|
||||
t = int(sys.argv[2], 16)
|
||||
for s, k, i, sf in c.execute('SELECT source,kind,instruction,source_func FROM xrefs '
|
||||
'WHERE target=? ORDER BY source', [t]).fetchall():
|
||||
print(H(s), k, 'in', _fn(s), ':', i)
|
||||
elif cmd == 'callers':
|
||||
off = int(sys.argv[2]) # decimal byte offset, e.g. 196 for vtable[49]
|
||||
pat = f'r11, {off}(r11)'
|
||||
for (a,) in c.execute("SELECT address FROM instructions WHERE mnemonic='lwz' AND operands=? "
|
||||
'ORDER BY address', [pat]).fetchall():
|
||||
print(H(a), 'in', _fn(a))
|
||||
elif cmd == 'grep':
|
||||
sub = sys.argv[2]
|
||||
for a, m, o in c.execute("SELECT address,mnemonic,operands FROM instructions "
|
||||
"WHERE operands LIKE ? ORDER BY address", [f'%{sub}%']).fetchall():
|
||||
print(H(a), m, o, ' in', _fn(a))
|
||||
elif cmd == 'find':
|
||||
w = int(sys.argv[2], 16)
|
||||
for (a,) in c.execute('SELECT address FROM instructions WHERE raw=? ORDER BY address', [w]).fetchall():
|
||||
print(H(a))
|
||||
else:
|
||||
print(__doc__)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user