Instrument: file.read offset tracer + XMA-PARAM probe (voice RE)
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m52s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m52s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Additive, cvar-gated instrumentation used to reverse-engineer Project Sylpheed's cutscene-voice storage (movie -> continuous sound-stream cue region). Default-off; no behaviour change. - event_log.cc: MaybeEmitFileRead emits `file.read` events with the real NtReadFile ByteOffset (from r10), length, handle->path, and buffer VA, so a sound.pNN read offset can be mapped to a sound.pak TOC entry. Gate: --phase_a_fileio_only=true --phase_a_event_log_path=<file>. - cpu_flags.cc: phase_a_fileio_only cvar (+ xma_param_probe). - xma_context_new.cc: XMA-PARAM probe in XmaContextNew::Decode (the decoder the title actually uses) logging ctx/buffer/read-offset/channels/packets/byte_size /signature — to confirm which bytes get decoded for a given cutscene. Used to produce /tmp/rt_fileio.jsonl (RT01A playthrough), which cracked the movie->voice mapping now implemented in sylpheed-reborn. Next: run the same file.read trace on an unbound hokyu (e.g. hokyu_LS_s03A) to resolve which VOICE_D the game streams for the 13 manifest-unbound resupply cutscenes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,19 @@
|
||||
#include "xenia/apu/xma_context_new.h"
|
||||
#include "xenia/apu/xma_helpers.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
|
||||
#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<uint64_t> xma_probe_seen;
|
||||
uint64_t key = (static_cast<uint64_t>(buf_ptr) << 20) ^
|
||||
static_cast<uint64_t>(buf_pkts);
|
||||
bool fresh;
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<uint32_t>(data->input_buffer_read_offset),
|
||||
static_cast<uint32_t>(data->is_stereo), data->is_stereo ? 2 : 1,
|
||||
static_cast<uint32_t>(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
|
||||
|
||||
@@ -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. "
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user