Compare commits
20 Commits
iterate-4B
...
iterate-4C
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb7c6f98cd | ||
|
|
0672f37c9f | ||
|
|
3f509ebc5c | ||
|
|
0e24e980fb | ||
|
|
f562e3ec2b | ||
|
|
d132fb6d8d | ||
|
|
fec9e8de28 | ||
|
|
cce35658a5 | ||
|
|
7ec6941fe8 | ||
|
|
1ffa3fb56d | ||
|
|
506b9554a5 | ||
|
|
4194ed77a3 | ||
|
|
afc3692223 | ||
|
|
e8d0dc4a2d | ||
|
|
781a82cc0a | ||
|
|
701e4c399a | ||
|
|
5e521f7f53 | ||
|
|
0f1130e2e6 | ||
|
|
231c35a28f | ||
|
|
1d56218d83 |
61
Cargo.lock
generated
61
Cargo.lock
generated
@@ -1056,6 +1056,33 @@ dependencies = [
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dynasm"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7d4c414c94bc830797115b8e5f434d58e7e80cb42ba88508c14bc6ea270625"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dynasmrt"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "602f7458a3859195fb840e6e0cce5f4330dd9dfbfece0edaf31fe427af346f55"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"dynasm",
|
||||
"fnv",
|
||||
"memmap2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endian-type"
|
||||
version = "0.1.2"
|
||||
@@ -2888,6 +2915,28 @@ dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -5047,6 +5096,7 @@ dependencies = [
|
||||
"xenia-debugger",
|
||||
"xenia-gpu",
|
||||
"xenia-hid",
|
||||
"xenia-jit",
|
||||
"xenia-kernel",
|
||||
"xenia-memory",
|
||||
"xenia-types",
|
||||
@@ -5117,6 +5167,17 @@ dependencies = [
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-jit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dynasm",
|
||||
"dynasmrt",
|
||||
"tracing",
|
||||
"xenia-cpu",
|
||||
"xenia-memory",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-kernel"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -4,6 +4,7 @@ members = [
|
||||
"crates/xenia-types",
|
||||
"crates/xenia-memory",
|
||||
"crates/xenia-cpu",
|
||||
"crates/xenia-jit",
|
||||
"crates/xenia-xex",
|
||||
"crates/xenia-vfs",
|
||||
"crates/xenia-kernel",
|
||||
@@ -26,6 +27,7 @@ license = "BSD-3-Clause"
|
||||
xenia-types = { path = "crates/xenia-types" }
|
||||
xenia-memory = { path = "crates/xenia-memory" }
|
||||
xenia-cpu = { path = "crates/xenia-cpu" }
|
||||
xenia-jit = { path = "crates/xenia-jit" }
|
||||
xenia-xex = { path = "crates/xenia-xex" }
|
||||
xenia-vfs = { path = "crates/xenia-vfs" }
|
||||
xenia-kernel = { path = "crates/xenia-kernel" }
|
||||
@@ -37,6 +39,9 @@ xenia-analysis = { path = "crates/xenia-analysis" }
|
||||
xenia-ui = { path = "crates/xenia-ui" }
|
||||
|
||||
# External dependencies
|
||||
# JIT (PPC->x64 block recompiler; runtime-gated by XENIA_JIT)
|
||||
dynasm = "3"
|
||||
dynasmrt = "3"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "registry"] }
|
||||
tracing-appender = "0.2"
|
||||
|
||||
@@ -12,6 +12,7 @@ path = "src/main.rs"
|
||||
xenia-types = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
xenia-cpu = { workspace = true }
|
||||
xenia-jit = { workspace = true }
|
||||
xenia-xex = { workspace = true }
|
||||
xenia-vfs = { workspace = true }
|
||||
xenia-kernel = { workspace = true }
|
||||
|
||||
@@ -966,19 +966,23 @@ fn cmd_exec_inner(
|
||||
let v = v.trim().to_ascii_lowercase();
|
||||
v == "1" || v == "true" || v == "yes"
|
||||
});
|
||||
// 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")
|
||||
// A.5 — threaded GPU under `--ui`, now the DEFAULT. The GPU command
|
||||
// processing + per-swap UI publish run on the worker thread, freeing the
|
||||
// emulation thread from the ~12 ms/frame inline PM4 drain — measured ~5×
|
||||
// faster boot under `--ui` (~12 → ~63 MIPS), visuals confirmed identical to
|
||||
// the inline path. See `run_with_ui` (hook install) and
|
||||
// `GpuSystem::run_ui_publish`. Opt back to the inline backend with
|
||||
// `--gpu-inline` or `XENIA_UI_GPU_INLINE=1` (the deterministic golden path
|
||||
// already uses `--gpu-inline`, so goldens are unaffected). `XENIA_UI_GPU_THREAD`
|
||||
// is retained as a no-op alias for back-compat.
|
||||
let env_ui_inline = std::env::var("XENIA_UI_GPU_INLINE")
|
||||
.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 ui_inline_optout = gpu_inline || env_inline || env_ui_inline;
|
||||
let ui_threaded = ui && !ui_inline_optout;
|
||||
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 {
|
||||
@@ -1998,6 +2002,10 @@ fn cmd_exec_inner(
|
||||
info!("run digest matches golden");
|
||||
}
|
||||
}
|
||||
// Diagnostic (XENIA_JIT_STATS): dump the JIT fallback-opcode
|
||||
// histogram so we can see which un-ported opcodes dominate. No-op
|
||||
// unless the env var is set.
|
||||
xenia_jit::dump_fallback_stats();
|
||||
Ok(())
|
||||
})()
|
||||
};
|
||||
@@ -2499,15 +2507,29 @@ struct WorkerCtx {
|
||||
block_cache: xenia_cpu::block_cache::BlockCache,
|
||||
decode_cache: xenia_cpu::decoder::DecodeCache,
|
||||
force_per_instr: bool,
|
||||
/// PPC→x64 JIT code cache for this HW slot. `Some` only when `XENIA_JIT`
|
||||
/// is set (and the RET-CAPTURE debug env is not — the JIT's fallback path
|
||||
/// bypasses `step_block`'s head-of-block capture print). Substitutes for
|
||||
/// the `step_block` call in `run_superblock`; produces byte-identical
|
||||
/// state so goldens are unaffected.
|
||||
jit_cache: Option<xenia_jit::JitCache>,
|
||||
}
|
||||
|
||||
impl WorkerCtx {
|
||||
fn new(hw_id: u8, force_per_instr: bool) -> Self {
|
||||
let jit_cache = if xenia_jit::env_enabled()
|
||||
&& std::env::var("XENIA_RET_CAPTURE_PC").is_err()
|
||||
{
|
||||
Some(xenia_jit::JitCache::new())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
hw_id,
|
||||
block_cache: xenia_cpu::block_cache::BlockCache::new(),
|
||||
decode_cache: xenia_cpu::decoder::DecodeCache::new(),
|
||||
force_per_instr,
|
||||
jit_cache,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3083,7 +3105,6 @@ fn run_superblock(
|
||||
first_pc_before: u32,
|
||||
) -> SlotOutcome {
|
||||
use xenia_cpu::interpreter::{step_block, StepResult};
|
||||
const LR_HALT: u32 = xenia_cpu::context::LR_HALT_SENTINEL as u32;
|
||||
|
||||
let budget = superblock_budget();
|
||||
|
||||
@@ -3141,6 +3162,20 @@ fn run_superblock(
|
||||
let mut pc_before = first_pc_before;
|
||||
let mut total_executed: u64 = 0;
|
||||
|
||||
// PERF: the running thread (`thread_ref`) is FIXED for the entire
|
||||
// superblock chain — no thread spawn/exit/migration happens between
|
||||
// iterations. `step_block` is pure guest interpretation; an import thunk
|
||||
// or any sync-sensitive/MMIO op BREAKS the chain (below) before any kernel
|
||||
// mutation could restructure the runqueue; `fire_block_entry_probes` is
|
||||
// read-only; `block_cache.lookup_or_build` touches only `wc`. So this
|
||||
// thread's `PpcContext` heap slot is stable for the whole loop and we can
|
||||
// resolve it ONCE here — instead of a bounds-checked double slot lookup
|
||||
// (`ctx_mut_ref` + `ctx`) on EVERY chained block (~10M/round). Same
|
||||
// raw-pointer discipline as `block_ptr`; and `ctx(hw_id) ==
|
||||
// ctx_mut_ref(thread_ref)` throughout because `running_idx` is unchanged.
|
||||
// Byte-identical.
|
||||
let ctx_ptr: *mut xenia_cpu::PpcContext = kernel.scheduler.ctx_mut_ref(thread_ref);
|
||||
|
||||
let (result, last_block_ptr, last_pc_before) = loop {
|
||||
let mmio_before = mem.mmio_access_count();
|
||||
let block = unsafe { &*block_ptr };
|
||||
@@ -3149,9 +3184,17 @@ fn run_superblock(
|
||||
// `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);
|
||||
let ctx = unsafe { &mut *ctx_ptr };
|
||||
let cycle_before = ctx.cycle_count;
|
||||
let result = step_block(ctx, mem, block);
|
||||
// JIT seam (XENIA_JIT): run the JIT-compiled block if enabled, else
|
||||
// the interpreter. The JIT leaves ctx.cycle_count/pc and
|
||||
// mmio_access_count in exactly the interpreter's state, so all the
|
||||
// surrounding accounting (executed, sync/MMIO/budget chain checks)
|
||||
// is untouched and goldens stay byte-identical.
|
||||
let result = match wc.jit_cache.as_mut() {
|
||||
Some(jit) => jit.run_or_compile(block, ctx, mem),
|
||||
None => step_block(ctx, mem, block),
|
||||
};
|
||||
let executed = ctx.cycle_count.saturating_sub(cycle_before);
|
||||
(result, executed)
|
||||
};
|
||||
@@ -3179,12 +3222,10 @@ fn run_superblock(
|
||||
|
||||
// Decide whether the NEXT PC is an ordinary guest block we can
|
||||
// chain into. Anything else (thunk / halt sentinel / unmapped)
|
||||
// needs the full prologue dispatch next round.
|
||||
let next_pc = kernel.scheduler.ctx(wc.hw_id).pc;
|
||||
if next_pc == LR_HALT
|
||||
|| (kernel.pc_in_thunk_band(next_pc) && thunk_map.contains_key(&next_pc))
|
||||
|| !mem.is_mapped(next_pc)
|
||||
{
|
||||
// needs the full prologue dispatch next round. `ctx_ptr` aliases the
|
||||
// running thread's context (stable for the chain — see above).
|
||||
let next_pc = unsafe { (*ctx_ptr).pc };
|
||||
if next_pc_breaks_chain(kernel, mem, thunk_map, next_pc) {
|
||||
break (result, block_ptr, pc_before);
|
||||
}
|
||||
|
||||
@@ -3227,6 +3268,308 @@ fn run_superblock(
|
||||
)
|
||||
}
|
||||
|
||||
/// Shared chain-break predicate for the NEXT pc: anything that is not an
|
||||
/// ordinary, mapped, non-thunk guest block ends the superblock (the next slot
|
||||
/// visit re-dispatches it through the full prologue). Used by BOTH the
|
||||
/// interpreter `run_superblock` and the JIT `run_superblock_jit` so their
|
||||
/// chaining decisions can never drift apart.
|
||||
#[inline]
|
||||
fn next_pc_breaks_chain(
|
||||
kernel: &xenia_kernel::KernelState,
|
||||
mem: &xenia_memory::GuestMemory,
|
||||
thunk_map: &HashMap<u32, (ModuleId, u16, String)>,
|
||||
next_pc: u32,
|
||||
) -> bool {
|
||||
const LR_HALT: u32 = xenia_cpu::context::LR_HALT_SENTINEL as u32;
|
||||
next_pc == LR_HALT
|
||||
|| (kernel.pc_in_thunk_band(next_pc) && thunk_map.contains_key(&next_pc))
|
||||
|| !mem.is_mapped(next_pc)
|
||||
}
|
||||
|
||||
/// JIT-specialized superblock runner (used when `wc.jit_cache.is_some()`).
|
||||
///
|
||||
/// Identical scheduling/accounting to `run_superblock`, with ONE optimization:
|
||||
/// chained (2nd..Nth) blocks run straight from the per-slot JIT cache via
|
||||
/// `JitCache::run_fresh`, SKIPPING the interpreter `BlockCache.lookup_or_build`
|
||||
/// (the ~9.7% "block decode/cache" bucket) whenever the compiled block is fresh.
|
||||
/// The interpreter path (`run_superblock`) does a DOUBLE lookup per block
|
||||
/// (BlockCache to get the `DecodedBlock`, then the JIT cache); on a JIT hit the
|
||||
/// `DecodedBlock` is not needed at all (the freshness key is
|
||||
/// `(start_pc, mem.page_version(pc))`, reconstructible from `mem`).
|
||||
///
|
||||
/// Byte-identical to `run_superblock` under `XENIA_JIT`: `run_fresh` runs the
|
||||
/// exact same compiled block `run_or_compile` would (same slot, same
|
||||
/// `(pc, page_version)` gate); a JIT miss rebuilds via `BlockCache` inline so
|
||||
/// the chain length — and therefore the schedule — is unchanged. `sync_sensitive`
|
||||
/// travels on the `CompiledBlock`; SYSCALL/Trap epilogue diagnostics get a valid
|
||||
/// `block_ptr` via a lazy rebuild on the (rare) non-Continue break.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run_superblock_jit(
|
||||
wc: &mut WorkerCtx,
|
||||
kernel: &mut xenia_kernel::KernelState,
|
||||
mem: &xenia_memory::GuestMemory,
|
||||
debugger: &mut xenia_debugger::Debugger,
|
||||
thunk_map: &HashMap<u32, (ModuleId, u16, String)>,
|
||||
stats: &mut ExecStats,
|
||||
tid: Option<u32>,
|
||||
thread_ref: xenia_cpu::ThreadRef,
|
||||
first_block_ptr: *const xenia_cpu::block_cache::DecodedBlock,
|
||||
first_pc_before: u32,
|
||||
) -> SlotOutcome {
|
||||
use xenia_cpu::block_cache::DecodedBlock;
|
||||
use xenia_cpu::interpreter::StepResult;
|
||||
|
||||
let budget = superblock_budget();
|
||||
let chain_allowed = budget > 1;
|
||||
|
||||
// Same per-block-entry diagnostic observation as `run_superblock` (see the
|
||||
// detailed rationale there): fired at every chained block's entry PC so
|
||||
// arming a probe/mem-watch never changes chaining (and thus the schedule).
|
||||
let probe_hw_id = wc.hw_id;
|
||||
let fire_block_entry_probes =
|
||||
|kernel: &mut xenia_kernel::KernelState, mem: &xenia_memory::GuestMemory| {
|
||||
let hw_id = probe_hw_id;
|
||||
if kernel.any_probe_active() {
|
||||
kernel.fire_ctor_probe_if_match(hw_id, mem);
|
||||
kernel.fire_branch_probe_if_match(hw_id);
|
||||
kernel.fire_audit_pc_probe_if_match(hw_id, mem);
|
||||
kernel.fire_lr_trace_if_match(hw_id);
|
||||
}
|
||||
if mem.has_mem_watch() {
|
||||
let ctx = kernel.scheduler.ctx(hw_id);
|
||||
let tid_w = kernel.scheduler.tid(hw_id).unwrap_or(0);
|
||||
xenia_memory::set_writer_ctx(tid_w, ctx.pc, ctx.lr as u32);
|
||||
}
|
||||
};
|
||||
|
||||
// Running thread is fixed for the chain — resolve its context ptr ONCE (same
|
||||
// raw-pointer discipline + justification as `run_superblock`).
|
||||
let ctx_ptr: *mut xenia_cpu::PpcContext = kernel.scheduler.ctx_mut_ref(thread_ref);
|
||||
|
||||
// Native block-chaining (XENIA_JIT_CHAIN): when compiled in AND no
|
||||
// probe/mem-watch is armed (native chaining skips the per-block-entry
|
||||
// observation — which is a no-op when nothing is armed), run the superblock
|
||||
// as a native tail-chain (blocks jmp straight to their successors, checking
|
||||
// the budget/mmio/sync yield guards inline) instead of the per-block Rust
|
||||
// loop. Same schedule; far less per-block dispatch. Falls back to the loop
|
||||
// below when disabled / a probe is armed / budget==1.
|
||||
if xenia_jit::chain_active()
|
||||
&& chain_allowed
|
||||
&& !kernel.any_probe_active()
|
||||
&& !mem.has_mem_watch()
|
||||
{
|
||||
return run_superblock_jit_chained(
|
||||
wc, kernel, mem, debugger, thunk_map, stats, tid, thread_ref, ctx_ptr,
|
||||
first_block_ptr, first_pc_before, budget,
|
||||
);
|
||||
}
|
||||
|
||||
let mut pc_before = first_pc_before;
|
||||
let mut total_executed: u64 = 0;
|
||||
// `Some(bp)` = a `DecodedBlock` is already in hand for the block at
|
||||
// `pc_before` (the first block) → run via `run_or_compile`. `None` = chained
|
||||
// block → run from the JIT cache directly (`run_fresh`); on a miss, rebuild
|
||||
// that one block via `BlockCache`.
|
||||
let mut pending_block: Option<*const DecodedBlock> = Some(first_block_ptr);
|
||||
// Last VALID DecodedBlock ptr for `worker_epilogue`'s SYSCALL/Trap
|
||||
// diagnostics; kept current whenever a block runs from a `DecodedBlock`.
|
||||
let mut last_block_ptr: *const DecodedBlock = first_block_ptr;
|
||||
|
||||
let (result, epilogue_block_ptr, last_pc_before) = loop {
|
||||
let mmio_before = mem.mmio_access_count();
|
||||
let _prof_t0 = xenia_gpu::prof::is_on().then(std::time::Instant::now);
|
||||
|
||||
// Run the block at `pc_before`. `ran_fresh` = it ran via a JIT-cache hit
|
||||
// (no live `DecodedBlock` ptr → lazy rebuild if it breaks non-Continue).
|
||||
let (result, executed, sync_sensitive, ran_fresh) = {
|
||||
let ctx = unsafe { &mut *ctx_ptr };
|
||||
let cycle_before = ctx.cycle_count;
|
||||
let (r, sync, ran_fresh) = match pending_block {
|
||||
Some(bp) => {
|
||||
let block = unsafe { &*bp };
|
||||
let jit = wc.jit_cache.as_mut().expect("jit active in run_superblock_jit");
|
||||
(jit.run_or_compile(block, ctx, mem), block.sync_sensitive, false)
|
||||
}
|
||||
None => {
|
||||
match wc
|
||||
.jit_cache
|
||||
.as_mut()
|
||||
.expect("jit active in run_superblock_jit")
|
||||
.run_fresh(pc_before, ctx, mem)
|
||||
{
|
||||
// JIT-cache hit — skipped BlockCache entirely.
|
||||
Some((r, sync)) => (r, sync, true),
|
||||
// JIT miss: rebuild this one block via BlockCache (times
|
||||
// as BUILD), compile+run. Disjoint field borrows
|
||||
// (block_cache vs jit_cache).
|
||||
None => {
|
||||
let _pt = xenia_gpu::prof::is_on().then(|| {
|
||||
xenia_gpu::prof::ScopeTimer::new(
|
||||
&xenia_gpu::prof::BUILD_NS,
|
||||
&xenia_gpu::prof::BUILD_CALLS,
|
||||
)
|
||||
});
|
||||
let block = wc.block_cache.lookup_or_build(pc_before, mem);
|
||||
let bp = block as *const DecodedBlock;
|
||||
let sync = block.sync_sensitive;
|
||||
let r = wc
|
||||
.jit_cache
|
||||
.as_mut()
|
||||
.expect("jit active")
|
||||
.run_or_compile(block, ctx, mem);
|
||||
last_block_ptr = bp;
|
||||
(r, sync, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let executed = ctx.cycle_count.saturating_sub(cycle_before);
|
||||
(r, executed, sync, ran_fresh)
|
||||
};
|
||||
if !ran_fresh {
|
||||
// Ran from a DecodedBlock (first block or miss-rebuild) — that ptr is
|
||||
// `last_block_ptr` (set above / initialized to first_block_ptr).
|
||||
last_block_ptr = if let Some(bp) = pending_block { bp } else { last_block_ptr };
|
||||
}
|
||||
|
||||
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 — identical order/semantics to `run_superblock`, with
|
||||
// `sync_sensitive` sourced from the run (the `CompiledBlock` on a JIT hit).
|
||||
if !chain_allowed
|
||||
|| !matches!(result, StepResult::Continue)
|
||||
|| sync_sensitive
|
||||
|| mem.mmio_access_count() != mmio_before
|
||||
|| total_executed >= budget
|
||||
{
|
||||
// Epilogue diagnostics (SYSCALL/Trap) read `block.instrs.last()`; if
|
||||
// the breaking block ran via a JIT hit (no ptr) AND the result is
|
||||
// non-Continue, lazily rebuild it. For Continue breaks the epilogue
|
||||
// never touches the block, so a stale `last_block_ptr` is unused.
|
||||
let epilogue_bp = if ran_fresh && !matches!(result, StepResult::Continue) {
|
||||
wc.block_cache.lookup_or_build(pc_before, mem) as *const _
|
||||
} else {
|
||||
last_block_ptr
|
||||
};
|
||||
break (result, epilogue_bp, pc_before);
|
||||
}
|
||||
|
||||
// Next-pc chain-break decision (shared helper — identical to interp).
|
||||
let next_pc = unsafe { (*ctx_ptr).pc };
|
||||
if next_pc_breaks_chain(kernel, mem, thunk_map, next_pc) {
|
||||
break (result, last_block_ptr, pc_before);
|
||||
}
|
||||
|
||||
// Chain into the next block: fire the per-block-entry observation at its
|
||||
// entry PC, then loop to run it from the JIT cache (no BlockCache on hit).
|
||||
pc_before = next_pc;
|
||||
fire_block_entry_probes(kernel, mem);
|
||||
pending_block = None;
|
||||
};
|
||||
|
||||
worker_epilogue(
|
||||
wc,
|
||||
kernel,
|
||||
debugger,
|
||||
stats,
|
||||
tid,
|
||||
thread_ref,
|
||||
epilogue_block_ptr,
|
||||
last_pc_before,
|
||||
result,
|
||||
total_executed,
|
||||
)
|
||||
}
|
||||
|
||||
/// Native tail-chaining superblock runner (XENIA_JIT_CHAIN increment). Drives
|
||||
/// `xenia_jit::run_jit_chain`: compile+enter the first block, whose chaining
|
||||
/// epilogue tail-jumps through subsequent fresh compiled blocks (checking the
|
||||
/// budget/mmio/sync yield guards INLINE) until it yields or hits an
|
||||
/// uncompiled-but-chainable block (a "miss"), which returns here to build+compile
|
||||
/// it and re-enter. Byte-identical schedule to `run_superblock_jit` (same yield
|
||||
/// guards evaluated at the same per-block boundaries, same chain length); only
|
||||
/// the per-block dispatch mechanism differs (native jmp vs Rust loop).
|
||||
///
|
||||
/// Preconditions (checked by the caller): chaining compiled in, budget>1, and no
|
||||
/// probe/mem-watch armed — native chaining skips the per-block-entry observation,
|
||||
/// which is a no-op precisely when nothing is armed.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run_superblock_jit_chained(
|
||||
wc: &mut WorkerCtx,
|
||||
kernel: &mut xenia_kernel::KernelState,
|
||||
mem: &xenia_memory::GuestMemory,
|
||||
debugger: &mut xenia_debugger::Debugger,
|
||||
thunk_map: &HashMap<u32, (ModuleId, u16, String)>,
|
||||
stats: &mut ExecStats,
|
||||
tid: Option<u32>,
|
||||
thread_ref: xenia_cpu::ThreadRef,
|
||||
ctx_ptr: *mut xenia_cpu::PpcContext,
|
||||
first_block_ptr: *const xenia_cpu::block_cache::DecodedBlock,
|
||||
first_pc_before: u32,
|
||||
budget: u64,
|
||||
) -> SlotOutcome {
|
||||
use xenia_cpu::block_cache::DecodedBlock;
|
||||
use xenia_cpu::interpreter::StepResult;
|
||||
use xenia_jit::ChainStop;
|
||||
|
||||
// Budget is measured over the whole slot visit: yield when
|
||||
// cycle_count >= start + budget (== interp's `total_executed >= budget`).
|
||||
let slot_start_cycle = unsafe { (*ctx_ptr).cycle_count };
|
||||
let deadline = slot_start_cycle.wrapping_add(budget);
|
||||
let mmio_ptr = mem.mmio_access_count_ptr();
|
||||
let cache_ptr = wc.jit_cache.as_mut().expect("jit active").as_ptr();
|
||||
|
||||
// Compile the first block; get its entry func.
|
||||
let mut cur_func = {
|
||||
let block = unsafe { &*first_block_ptr };
|
||||
wc.jit_cache.as_mut().expect("jit active").ensure_compiled(block, mem)
|
||||
};
|
||||
// Diagnostics ptr for worker_epilogue (only SYSCALL/Trap read block.instrs;
|
||||
// scheduling-affecting handling uses `result`, not this). Tracks the last
|
||||
// block THIS loop built — the golden boot yields Continue so it's unused.
|
||||
let mut last_block_ptr: *const DecodedBlock = first_block_ptr;
|
||||
let mut last_pc_before = first_pc_before;
|
||||
|
||||
let result = loop {
|
||||
let ctx = unsafe { &mut *ctx_ptr };
|
||||
let (result, stop) =
|
||||
xenia_jit::run_jit_chain(cur_func, ctx, mem, cache_ptr, deadline, mmio_ptr, true);
|
||||
|
||||
match stop {
|
||||
// Dispatch miss on a Continue: next_pc is either a chainable but
|
||||
// not-yet-JIT-compiled block, or halt/thunk/unmapped (which are never
|
||||
// compiled → also a miss). Distinguish exactly like the interp loop.
|
||||
ChainStop::Miss if matches!(result, StepResult::Continue) => {
|
||||
let next_pc = unsafe { (*ctx_ptr).pc };
|
||||
if next_pc_breaks_chain(kernel, mem, thunk_map, next_pc) {
|
||||
break result; // halt/thunk/unmapped → end the superblock
|
||||
}
|
||||
// Build + compile the chainable next block, then re-enter from it.
|
||||
last_pc_before = next_pc;
|
||||
let block = wc.block_cache.lookup_or_build(next_pc, mem);
|
||||
last_block_ptr = block as *const DecodedBlock;
|
||||
cur_func = wc.jit_cache.as_mut().expect("jit active").ensure_compiled(block, mem);
|
||||
}
|
||||
// Yield (budget/mmio/sync — handled inline) or any non-Continue
|
||||
// result: end the superblock. `result` drives worker_epilogue.
|
||||
_ => break result,
|
||||
}
|
||||
};
|
||||
|
||||
let total_executed = unsafe { (*ctx_ptr).cycle_count }.wrapping_sub(slot_start_cycle);
|
||||
worker_epilogue(
|
||||
wc, kernel, debugger, stats, tid, thread_ref, last_block_ptr, last_pc_before,
|
||||
result, total_executed,
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(max = ?max_instructions, ips = ?ips_limit))]
|
||||
fn run_execution(
|
||||
mem: &xenia_memory::GuestMemory,
|
||||
@@ -3423,18 +3766,22 @@ fn run_execution(
|
||||
// the per-round (timebase / coord / round_schedule)
|
||||
// and per-slot (prologue) tax over hundreds of
|
||||
// instructions instead of ~6. See `run_superblock`.
|
||||
match run_superblock(
|
||||
wc,
|
||||
kernel,
|
||||
mem,
|
||||
debugger,
|
||||
thunk_map,
|
||||
&mut stats,
|
||||
tid,
|
||||
thread_ref,
|
||||
block_ptr,
|
||||
pc_before,
|
||||
) {
|
||||
//
|
||||
// When the JIT is active, use `run_superblock_jit` — same
|
||||
// scheduling, but chained blocks run straight from the JIT
|
||||
// cache (skipping the redundant BlockCache lookup on hits).
|
||||
let outcome = if wc.jit_cache.is_some() {
|
||||
run_superblock_jit(
|
||||
wc, kernel, mem, debugger, thunk_map, &mut stats, tid,
|
||||
thread_ref, block_ptr, pc_before,
|
||||
)
|
||||
} else {
|
||||
run_superblock(
|
||||
wc, kernel, mem, debugger, thunk_map, &mut stats, tid,
|
||||
thread_ref, block_ptr, pc_before,
|
||||
)
|
||||
};
|
||||
match outcome {
|
||||
SlotOutcome::Continue => continue,
|
||||
SlotOutcome::BreakOuter => break 'outer,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"instructions": 200000239,
|
||||
"imports": 575447,
|
||||
"instructions": 200000203,
|
||||
"imports": 575647,
|
||||
"unimpl": 0,
|
||||
"draws": 3165,
|
||||
"swaps": 895,
|
||||
"draws": 3208,
|
||||
"swaps": 910,
|
||||
"unique_render_targets": 2,
|
||||
"shader_blobs_live": 6,
|
||||
"texture_cache_entries": 1
|
||||
|
||||
@@ -63,7 +63,7 @@ const GUEST_PAGE_MASK: u32 = !(GUEST_PAGE_SIZE - 1);
|
||||
/// One cached basic block. Owned by [`BlockCache`]; a `&DecodedBlock`
|
||||
/// is handed to the interpreter via [`BlockCache::lookup_or_build`] and
|
||||
/// stays valid until the next `lookup_or_build` on the same slot.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DecodedBlock {
|
||||
/// Guest PC at which this block starts. Used as the slot tag.
|
||||
pub start_pc: u32,
|
||||
@@ -185,6 +185,18 @@ impl BlockCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a standalone `DecodedBlock` at `start_pc` against `mem`, computing
|
||||
/// its `page_version` from memory (the same `(start_pc, page_version)` key the
|
||||
/// `BlockCache` uses). Unlike [`BlockCache::lookup_or_build`] this does not
|
||||
/// touch any cache — it is used by the JIT region compiler to decode the
|
||||
/// straight-line/same-page successor blocks it stitches into one compiled
|
||||
/// region. `build_block` stops at the 4 KiB page boundary, so the returned
|
||||
/// block is fully contained in the page whose version this records.
|
||||
pub fn decode_block(start_pc: u32, mem: &dyn MemoryAccess) -> DecodedBlock {
|
||||
let page_version = mem.page_version(start_pc);
|
||||
build_block(start_pc, mem, page_version)
|
||||
}
|
||||
|
||||
/// Walk forward from `pc`, decoding instructions and collecting them
|
||||
/// into a `DecodedBlock`. The walk stops on the first of:
|
||||
/// - a [`PpcOpcode::terminates_block`] true (the terminator IS
|
||||
|
||||
@@ -213,6 +213,22 @@ pub fn step_block(
|
||||
result
|
||||
}
|
||||
|
||||
/// Execute exactly one already-decoded instruction — the JIT's interpreter
|
||||
/// fallback (`xenia-jit`). Identical to the body of [`step`]/`step_block`
|
||||
/// EXCEPT it does **not** bump `cycle_count`/`timebase`: the JIT owns the
|
||||
/// per-instruction counter increments so that a mix of native and
|
||||
/// fallback opcodes retires exactly one tick each, in order, byte-identical
|
||||
/// to the interpreter. `execute` itself advances `ctx.pc` (each arm does
|
||||
/// `ctx.pc += 4` or sets a branch target), same as the interpreter path.
|
||||
#[inline]
|
||||
pub fn interpret_one(
|
||||
ctx: &mut PpcContext,
|
||||
mem: &dyn MemoryAccess,
|
||||
instr: &DecodedInstr,
|
||||
) -> StepResult {
|
||||
execute(ctx, mem, instr)
|
||||
}
|
||||
|
||||
/// Execute a decoded instruction, updating context and memory.
|
||||
fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -> StepResult {
|
||||
match instr.opcode {
|
||||
|
||||
12
crates/xenia-jit/Cargo.toml
Normal file
12
crates/xenia-jit/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "xenia-jit"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-cpu = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
dynasm = { workspace = true }
|
||||
dynasmrt = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
1514
crates/xenia-jit/src/emit.rs
Normal file
1514
crates/xenia-jit/src/emit.rs
Normal file
File diff suppressed because it is too large
Load Diff
1252
crates/xenia-jit/src/lib.rs
Normal file
1252
crates/xenia-jit/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
1275
crates/xenia-jit/src/tests.rs
Normal file
1275
crates/xenia-jit/src/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -125,6 +125,29 @@ fn gamepad_key(state: &xenia_hid::GamepadState) -> u128 {
|
||||
u128::from_be_bytes(bytes)
|
||||
}
|
||||
|
||||
/// Whether user 0 has a default always-connected, idle (no-buttons) virtual
|
||||
/// controller. **Default ON** — matches canary, whose default input drivers
|
||||
/// (xinput / winkey) present a connected pad, so a title screen advances to its
|
||||
/// "Press A" input-poll state instead of stalling forever on
|
||||
/// DEVICE_NOT_CONNECTED (a console always exposes controller slots; a real
|
||||
/// gamepad under `--ui` supplies actual button state, otherwise the pad is idle).
|
||||
/// Set `XENIA_NO_PAD=1` to present NO controller (reproduces the raw headless
|
||||
/// no-HID trajectory).
|
||||
fn virtual_pad_enabled() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static V: OnceLock<bool> = OnceLock::new();
|
||||
*V.get_or_init(|| {
|
||||
!std::env::var("XENIA_NO_PAD")
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// True iff user 0 has a controller: a real host gamepad, or the default pad.
|
||||
fn user_connected(state: &KernelState, user: u32) -> bool {
|
||||
state.ui.as_ref().is_some_and(|ui| ui.is_connected(user)) || (user == 0 && virtual_pad_enabled())
|
||||
}
|
||||
|
||||
fn xam_input_get_capabilities(
|
||||
ctx: &mut PpcContext,
|
||||
mem: &GuestMemory,
|
||||
@@ -133,8 +156,7 @@ fn xam_input_get_capabilities(
|
||||
// r3 = user_index, r4 = flags, r5 = out X_INPUT_CAPABILITIES*
|
||||
let user = ctx.gpr[3] as u32;
|
||||
let out_ptr = ctx.gpr[5] as u32;
|
||||
let connected = state.ui.as_ref().is_some_and(|ui| ui.is_connected(user));
|
||||
if !connected {
|
||||
if !user_connected(state, user) {
|
||||
ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
|
||||
return;
|
||||
}
|
||||
@@ -146,15 +168,16 @@ fn xam_input_get_state(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut Kern
|
||||
// r3 = user_index, r4 = flags, r5 = out X_INPUT_STATE*
|
||||
let user = ctx.gpr[3] as u32;
|
||||
let out_ptr = ctx.gpr[5] as u32;
|
||||
let Some(ui) = state.ui.as_ref() else {
|
||||
ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
|
||||
return;
|
||||
};
|
||||
if !ui.is_connected(user) {
|
||||
if !user_connected(state, user) {
|
||||
ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
|
||||
return;
|
||||
}
|
||||
let gamepad = ui.snapshot_gamepad();
|
||||
// Real host gamepad if present, else the idle virtual pad (XENIA_VIRTUAL_PAD).
|
||||
let gamepad = state
|
||||
.ui
|
||||
.as_ref()
|
||||
.map(|ui| ui.snapshot_gamepad())
|
||||
.unwrap_or_default();
|
||||
let key = gamepad_key(&gamepad);
|
||||
if key != state.last_input_bytes {
|
||||
state.input_packet_number = state.input_packet_number.wrapping_add(1);
|
||||
|
||||
@@ -155,6 +155,15 @@ impl GuestMemory {
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Raw address of the MMIO access counter, as `*const u64`. The JIT's native
|
||||
/// block-chaining epilogue reads it with a plain aligned load to detect a
|
||||
/// mid-block MMIO touch inline (equivalent to a `Relaxed` load on x86-64 —
|
||||
/// `AtomicU64` has the same layout as `u64`). Single-thread use only.
|
||||
#[inline]
|
||||
pub fn mmio_access_count_ptr(&self) -> *const u64 {
|
||||
self.mmio_access_count.as_ptr() as *const u64
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bump_mmio_access(&self) {
|
||||
self.mmio_access_count
|
||||
|
||||
Reference in New Issue
Block a user