diff --git a/src/xenia/apu/xma_context_new.cc b/src/xenia/apu/xma_context_new.cc index a154c8a87..3a12c9580 100644 --- a/src/xenia/apu/xma_context_new.cc +++ b/src/xenia/apu/xma_context_new.cc @@ -10,10 +10,19 @@ #include "xenia/apu/xma_context_new.h" #include "xenia/apu/xma_helpers.h" +#include +#include +#include + +#include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/base/platform.h" #include "xenia/base/profiling.h" +// [Phase-A / audio RE] Defined in xma_context_master.cc; declared here so the +// probe fires in the DEFAULT ("new") decoder — the one the game actually uses. +DECLARE_bool(xma_param_probe); + extern "C" { #if XE_COMPILER_MSVC #pragma warning(push) @@ -392,6 +401,66 @@ void XmaContextNew::Decode(XMA_CONTEXT_DATA* data) { uint8_t* current_input_buffer = GetCurrentInputBuffer(data); + // [Phase-A / audio RE] Additive, cvar-gated probe (default-off, read-only). + // Capture the true XMA per-stream parameters the game supplies, keyed by + // (input buffer ptr, packet count) — the pair is unique per sub-wave, so it + // tells us WHICH sub-wave of a movie's `.slb` the game actually decodes. No + // behaviour change. Lives in the DEFAULT decoder so it observes the real + // playback path; probes WHICHEVER buffer is current (0 or 1). + if (cvars::xma_param_probe) { + static std::once_flag xma_probe_alive; + std::call_once(xma_probe_alive, [this]() { + XELOGW("XMA-PROBE active (ctx {} first decode) — cvar parsed OK", id()); + }); + const uint32_t buf = data->current_buffer; + const uint32_t buf_ptr = buf ? data->input_buffer_1_ptr + : data->input_buffer_0_ptr; + const uint32_t buf_pkts = buf ? data->input_buffer_1_packet_count + : data->input_buffer_0_packet_count; + if (data->IsCurrentInputBufferValid() && buf_pkts) { + uint8_t* in = memory()->TranslatePhysical(buf_ptr); + if (in) { + static std::mutex xma_probe_mu; + static std::set xma_probe_seen; + uint64_t key = (static_cast(buf_ptr) << 20) ^ + static_cast(buf_pkts); + bool fresh; + { + std::lock_guard lock(xma_probe_mu); + fresh = xma_probe_seen.insert(key).second; + } + if (fresh) { + char head[65]; + for (int i = 0; i < 32; ++i) { + std::snprintf(head + i * 2, 3, "%02x", in[i]); + } + // Deep content signature: 48 bytes taken well into the stream (the + // first packet header is identical across all voices, so it can't + // discriminate). This uniquely fingerprints the actual recording, so + // the played voice can be matched back to a specific `.slb` blob + // regardless of the (scrambled) file name. `byte_size` = the XMA input + // size (packets * 2048), itself a strong discriminator. + const uint32_t byte_size = buf_pkts * 2048u; + const uint32_t sig_off = byte_size > 1088u ? 1024u : 0u; + char sig[97]; + for (int i = 0; i < 48; ++i) { + std::snprintf(sig + i * 2, 3, "%02x", in[sig_off + i]); + } + XELOGW( + "XMA-PARAM ctx={} buf={} ptr=0x{:08x} read_off={} stereo={} " + "channels={} rate_id={} rate={} packets={} byte_size={} " + "sig_off={} head={} sig={}", + id(), buf, buf_ptr, + static_cast(data->input_buffer_read_offset), + static_cast(data->is_stereo), data->is_stereo ? 2 : 1, + static_cast(data->sample_rate), + GetSampleRate(data->sample_rate), buf_pkts, byte_size, sig_off, + head, sig); + } + } + } + } + input_buffer_.fill(0); // Detect if we're about to decode the loop end frame (before diff --git a/src/xenia/cpu/cpu_flags.cc b/src/xenia/cpu/cpu_flags.cc index cfadac56c..19e87186b 100644 --- a/src/xenia/cpu/cpu_flags.cc +++ b/src/xenia/cpu/cpu_flags.cc @@ -73,6 +73,12 @@ DEFINE_bool(phase_a_trace_args, false, "args_resolved (file I/O path/offset/length/buffer, alloc size, " "handle names) fields, and emit file.read events. Default false.", "Audit"); +DEFINE_bool(phase_a_fileio_only, false, + "Phase A extraction (LIGHTWEIGHT): emit ONLY file opens (path) and " + "file.read events (path/offset/length) — no per-export import/kernel " + "call spam. Use to trace which sound.pak/movie bytes a title reads " + "(e.g. movie->voice mapping) without perturbing timing. Default false.", + "Audit"); DEFINE_string(phase_a_hash_probe, "", "Phase A extraction: CSV of guest PCs — RESERVED (the HIR/emitter " "trap that drives it is not ported to instrument-current). Inert. " diff --git a/src/xenia/kernel/event_log.cc b/src/xenia/kernel/event_log.cc index 219c2b538..230b7669a 100644 --- a/src/xenia/kernel/event_log.cc +++ b/src/xenia/kernel/event_log.cc @@ -30,6 +30,7 @@ DECLARE_string(phase_a_event_log_path); DECLARE_bool(kernel_emit_contention); DECLARE_bool(phase_a_trace_args); +DECLARE_bool(phase_a_fileio_only); DECLARE_string(phase_a_hash_probe); namespace xe { @@ -683,8 +684,20 @@ void MaybeEmitFileRead(const char* name, ::xe::cpu::ppc::PPCContext* ctx) { void EmitImportAndCallWithCtx(const char* module_name, uint16_t ord, const char* name, void* ppc_context) { - ::xe::kernel::phase_a::EmitImportCall(module_name, ord, name); auto* ctx = reinterpret_cast<::xe::cpu::ppc::PPCContext*>(ppc_context); + if (cvars::phase_a_fileio_only) { + // Lightweight file-I/O-only mode: emit ONLY file opens (name + resolved + // path) and file.read events (path/offset/length). No import.call/kernel.call + // per-export spam, so a timing-sensitive title stays performant. For + // non-file exports both helpers early-out on a cheap name check. + std::string path = ResolvePathArg(name, ctx); + if (!path.empty()) { + ::xe::kernel::phase_a::EmitKernelCallWithPath(name, path.c_str()); + } + MaybeEmitFileRead(name, ctx); + return; + } + ::xe::kernel::phase_a::EmitImportCall(module_name, ord, name); std::string path = ResolvePathArg(name, ctx); if (cvars::phase_a_trace_args) { // Extraction mode: raw GPR args + resolved path, plus file.read detail. @@ -700,6 +713,10 @@ void EmitImportAndCallWithCtx(const char* module_name, uint16_t ord, } } void EmitReturn(const char* name, uint64_t return_value) { + // File-I/O-only tracing captures everything on the call side (opens + reads), + // so suppress the per-export return spam — it dominated the log (millions of + // lines / GBs) and perturbs timing. + if (cvars::phase_a_fileio_only) return; ::xe::kernel::phase_a::EmitKernelReturn(name, return_value); } } // namespace phase_a_bridge