Compare commits
4 Commits
7b6902e08f
...
capture-sh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
877163792b | ||
|
|
3ba56a5005 | ||
|
|
87d02d59f1 | ||
|
|
201553aea3 |
@@ -1067,6 +1067,11 @@ void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) {
|
|||||||
case ui::VirtualKey::kF12: {
|
case ui::VirtualKey::kF12: {
|
||||||
TakeScreenshot();
|
TakeScreenshot();
|
||||||
} break;
|
} break;
|
||||||
|
case ui::VirtualKey::kF10: {
|
||||||
|
// RE: snapshot the next frame's draws (world-space vertex positions) to
|
||||||
|
// xenia_ship_capture.log for capital-ship placement correlation.
|
||||||
|
xe::gpu::RequestShipCaptureFrame();
|
||||||
|
} break;
|
||||||
|
|
||||||
case ui::VirtualKey::kEscape: {
|
case ui::VirtualKey::kEscape: {
|
||||||
// Allow users to escape fullscreen (but not enter it).
|
// Allow users to escape fullscreen (but not enter it).
|
||||||
|
|||||||
@@ -10,10 +10,19 @@
|
|||||||
#include "xenia/apu/xma_context_new.h"
|
#include "xenia/apu/xma_context_new.h"
|
||||||
#include "xenia/apu/xma_helpers.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/logging.h"
|
||||||
#include "xenia/base/platform.h"
|
#include "xenia/base/platform.h"
|
||||||
#include "xenia/base/profiling.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" {
|
extern "C" {
|
||||||
#if XE_COMPILER_MSVC
|
#if XE_COMPILER_MSVC
|
||||||
#pragma warning(push)
|
#pragma warning(push)
|
||||||
@@ -392,6 +401,66 @@ void XmaContextNew::Decode(XMA_CONTEXT_DATA* data) {
|
|||||||
|
|
||||||
uint8_t* current_input_buffer = GetCurrentInputBuffer(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);
|
input_buffer_.fill(0);
|
||||||
|
|
||||||
// Detect if we're about to decode the loop end frame (before
|
// 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, "
|
"args_resolved (file I/O path/offset/length/buffer, alloc size, "
|
||||||
"handle names) fields, and emit file.read events. Default false.",
|
"handle names) fields, and emit file.read events. Default false.",
|
||||||
"Audit");
|
"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, "",
|
DEFINE_string(phase_a_hash_probe, "",
|
||||||
"Phase A extraction: CSV of guest PCs — RESERVED (the HIR/emitter "
|
"Phase A extraction: CSV of guest PCs — RESERVED (the HIR/emitter "
|
||||||
"trap that drives it is not ported to instrument-current). Inert. "
|
"trap that drives it is not ported to instrument-current). Inert. "
|
||||||
|
|||||||
@@ -125,8 +125,157 @@ const char* ReVertexFormatName(xenos::VertexFormat f) {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// ── Ship-placement capture (RE) ─────────────────────────────────────────────
|
||||||
|
// A hotkey (F10 in the emulator window) requests a one-shot snapshot: the next
|
||||||
|
// batch of draws is dumped to xenia_ship_capture_NN.log (NN = press number —
|
||||||
|
// several angles can be captured in one run without overwriting) with each
|
||||||
|
// draw's guest vertex-buffer address, vertex/index counts, up to 64 LOCAL
|
||||||
|
// vertex positions, and the first 48 VS float constants (c0..c2 = the WVP rows
|
||||||
|
// that place the part). De-duped by (buffer address, c0..c2 transform) — NOT by
|
||||||
|
// address alone — so the SAME part buffer drawn several times (two engine
|
||||||
|
// nacelles, every ship of a fleet) yields one record per distinct placement.
|
||||||
|
namespace {
|
||||||
|
constexpr int kShipCaptureBudget = 8000; // draws to scan per request
|
||||||
|
std::atomic<uint32_t> g_ship_capture_gen{0};
|
||||||
|
std::atomic<int> g_ship_capture_remaining{0};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void RequestShipCaptureFrame() {
|
||||||
|
uint32_t gen = g_ship_capture_gen.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||||
|
g_ship_capture_remaining.store(kShipCaptureBudget, std::memory_order_relaxed);
|
||||||
|
XELOGI("[SHIP-CAP] capture armed → xenia_ship_capture_{:02d}.log", gen);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandProcessor::CaptureShipDrawForRE(
|
||||||
|
uint32_t vgt_draw_initiator_value,
|
||||||
|
const IndexBufferInfo* index_buffer_info) {
|
||||||
|
if (g_ship_capture_remaining.load(std::memory_order_relaxed) <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static std::mutex cap_mutex;
|
||||||
|
static std::ofstream cap_out;
|
||||||
|
static std::unordered_set<uint64_t> cap_seen; // by (vbase, WVP transform)
|
||||||
|
static uint32_t cap_gen = 0;
|
||||||
|
std::lock_guard<std::mutex> lock(cap_mutex);
|
||||||
|
if (g_ship_capture_remaining.load(std::memory_order_relaxed) <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A new request (fresh generation) opens its own numbered snapshot file and
|
||||||
|
// clears the de-dup set, so each F10 press yields an independent capture.
|
||||||
|
uint32_t gen = g_ship_capture_gen.load(std::memory_order_relaxed);
|
||||||
|
if (gen != cap_gen || !cap_out.is_open()) {
|
||||||
|
if (cap_out.is_open()) {
|
||||||
|
cap_out.close();
|
||||||
|
}
|
||||||
|
auto name = fmt::format("xenia_ship_capture_{:02d}.log", gen);
|
||||||
|
cap_out.open(name, std::ios::out | std::ios::trunc);
|
||||||
|
cap_seen.clear();
|
||||||
|
cap_gen = gen;
|
||||||
|
XELOGI("[SHIP-CAP] writing {}", name);
|
||||||
|
}
|
||||||
|
g_ship_capture_remaining.fetch_sub(1, std::memory_order_relaxed);
|
||||||
|
if (!cap_out.is_open()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reg::VGT_DRAW_INITIATOR init;
|
||||||
|
init.value = vgt_draw_initiator_value;
|
||||||
|
Shader* vs = active_vertex_shader_;
|
||||||
|
if (!vs || !vs->is_ucode_analyzed() || vs->vertex_bindings().empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& binding = vs->vertex_bindings()[0];
|
||||||
|
xenos::xe_gpu_vertex_fetch_t fetch =
|
||||||
|
register_file_->GetVertexFetch(binding.fetch_constant);
|
||||||
|
int32_t pos_off_bytes = -1;
|
||||||
|
for (const auto& attr : binding.attributes) {
|
||||||
|
if (attr.fetch_instr.attributes.data_format ==
|
||||||
|
xenos::VertexFormat::k_32_32_32_FLOAT) {
|
||||||
|
pos_off_bytes = attr.fetch_instr.attributes.offset * 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint32_t stride = binding.stride_words * 4;
|
||||||
|
uint32_t vbase = uint32_t(fetch.address) << 2;
|
||||||
|
uint32_t buf_bytes = uint32_t(fetch.size) * 4;
|
||||||
|
if (pos_off_bytes < 0 || stride == 0 || vbase == 0) {
|
||||||
|
return; // no float-position stream (UI/effects) — skip
|
||||||
|
}
|
||||||
|
// One record per distinct (buffer, placement): hash the c0..c2 WVP rows so
|
||||||
|
// repeated draws of the SAME buffer at DIFFERENT transforms (multi-instance
|
||||||
|
// parts, fleet ships) each get their own record.
|
||||||
|
auto vsc0 = register_file_->Get<reg::SQ_VS_CONST>();
|
||||||
|
uint64_t th = 1469598103934665603ull; // FNV-1a over the 12 c0..c2 floats
|
||||||
|
for (uint32_t i = 0; i < 12; ++i) {
|
||||||
|
uint32_t idx = vsc0.base + (i / 4);
|
||||||
|
if (idx >= 256) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint32_t r = XE_GPU_REG_SHADER_CONSTANT_000_X + 4 * idx + (i % 4);
|
||||||
|
uint32_t bits = register_file_->values[r];
|
||||||
|
th = (th ^ bits) * 1099511628211ull;
|
||||||
|
}
|
||||||
|
uint64_t key = (uint64_t(vbase) << 32) ^ (th & 0xFFFFFFFFull) ^ (th >> 32);
|
||||||
|
if (!cap_seen.insert(key).second) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cap_seen.size() > 8192) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t vcount = buf_bytes / stride;
|
||||||
|
cap_out << fmt::format(
|
||||||
|
"DRAW vbase=0x{:08X} stride={} vcount={} indices={} prim={} vs=0x{:016X}\n",
|
||||||
|
vbase, stride, vcount, uint32_t(init.num_indices),
|
||||||
|
uint32_t(init.prim_type), vs->ucode_data_hash());
|
||||||
|
uint32_t n = vcount < 64 ? vcount : 64;
|
||||||
|
cap_out << " pos:";
|
||||||
|
auto be_f32 = [](const uint8_t* q) {
|
||||||
|
uint32_t w = (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) |
|
||||||
|
(uint32_t(q[2]) << 8) | uint32_t(q[3]);
|
||||||
|
float f;
|
||||||
|
std::memcpy(&f, &w, 4);
|
||||||
|
return f;
|
||||||
|
};
|
||||||
|
for (uint32_t v = 0; v < n; ++v) {
|
||||||
|
uint32_t a = vbase + v * stride + uint32_t(pos_off_bytes);
|
||||||
|
const uint8_t* p = memory_->TranslatePhysical<const uint8_t*>(a);
|
||||||
|
if (!p) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cap_out << fmt::format(" ({:.4f},{:.4f},{:.4f})", be_f32(p), be_f32(p + 4),
|
||||||
|
be_f32(p + 8));
|
||||||
|
}
|
||||||
|
cap_out << "\n";
|
||||||
|
|
||||||
|
// Vertex-shader float constants: the buffer holds LOCAL positions, so the
|
||||||
|
// per-part world (or world-view-projection) matrix that places the part lives
|
||||||
|
// here as a run of float4 constants. Diffing two parts' constants isolates the
|
||||||
|
// matrix (the camera VP block is shared). Dump the first 48 vec4 from the VS
|
||||||
|
// constant base as host-float (the register file stores them host-endian).
|
||||||
|
auto vsc = register_file_->Get<reg::SQ_VS_CONST>();
|
||||||
|
uint32_t cbase = vsc.base; // starting float4 index
|
||||||
|
cap_out << fmt::format(" vsconst base={}:", cbase);
|
||||||
|
for (uint32_t i = 0; i < 48; ++i) {
|
||||||
|
uint32_t idx = cbase + i;
|
||||||
|
if (idx >= 256) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint32_t r = XE_GPU_REG_SHADER_CONSTANT_000_X + 4 * idx;
|
||||||
|
float cx = register_file_->Get<float>(r);
|
||||||
|
float cy = register_file_->Get<float>(r + 1);
|
||||||
|
float cz = register_file_->Get<float>(r + 2);
|
||||||
|
float cw = register_file_->Get<float>(r + 3);
|
||||||
|
cap_out << fmt::format(" c{}=({:.4f},{:.4f},{:.4f},{:.4f})", i, cx, cy, cz,
|
||||||
|
cw);
|
||||||
|
}
|
||||||
|
cap_out << "\n";
|
||||||
|
cap_out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||||
const IndexBufferInfo* index_buffer_info) {
|
const IndexBufferInfo* index_buffer_info) {
|
||||||
|
// One-shot ship-placement capture runs independently of the log_draws cvar.
|
||||||
|
CaptureShipDrawForRE(vgt_draw_initiator_value, index_buffer_info);
|
||||||
if (!cvars::log_draws) {
|
if (!cvars::log_draws) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ class ByteStream;
|
|||||||
|
|
||||||
namespace gpu {
|
namespace gpu {
|
||||||
|
|
||||||
|
// Arm a one-shot ship-placement capture (see CommandProcessor::CaptureShipDrawForRE).
|
||||||
|
// Called from the UI thread (F10 hotkey); thread-safe. Defined in command_processor.cc.
|
||||||
|
void RequestShipCaptureFrame();
|
||||||
|
|
||||||
enum class GPUSetting { ClearMemoryPageState, ReadbackMemexport };
|
enum class GPUSetting { ClearMemoryPageState, ReadbackMemexport };
|
||||||
|
|
||||||
enum class ReadbackResolveMode {
|
enum class ReadbackResolveMode {
|
||||||
@@ -453,6 +457,14 @@ class CommandProcessor {
|
|||||||
void LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
void LogDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||||
const IndexBufferInfo* index_buffer_info);
|
const IndexBufferInfo* index_buffer_info);
|
||||||
|
|
||||||
|
// Ship-placement capture (RE): a one-shot snapshot armed by RequestShipCaptureFrame()
|
||||||
|
// (F10 hotkey). Dumps each draw's guest vertex-buffer address, vertex/index
|
||||||
|
// counts, and up to 64 WORLD-space positions to xenia_ship_capture.log, so a
|
||||||
|
// correlator can recover each capital-ship part's exact placement. Defined in
|
||||||
|
// command_processor.cc.
|
||||||
|
void CaptureShipDrawForRE(uint32_t vgt_draw_initiator_value,
|
||||||
|
const IndexBufferInfo* index_buffer_info);
|
||||||
|
|
||||||
// "Actual" is for the command processor thread, to be read by the
|
// "Actual" is for the command processor thread, to be read by the
|
||||||
// implementations.
|
// implementations.
|
||||||
SwapPostEffect GetActualSwapPostEffect() const {
|
SwapPostEffect GetActualSwapPostEffect() const {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
DECLARE_string(phase_a_event_log_path);
|
DECLARE_string(phase_a_event_log_path);
|
||||||
DECLARE_bool(kernel_emit_contention);
|
DECLARE_bool(kernel_emit_contention);
|
||||||
DECLARE_bool(phase_a_trace_args);
|
DECLARE_bool(phase_a_trace_args);
|
||||||
|
DECLARE_bool(phase_a_fileio_only);
|
||||||
DECLARE_string(phase_a_hash_probe);
|
DECLARE_string(phase_a_hash_probe);
|
||||||
|
|
||||||
namespace xe {
|
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,
|
void EmitImportAndCallWithCtx(const char* module_name, uint16_t ord,
|
||||||
const char* name, void* ppc_context) {
|
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);
|
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);
|
std::string path = ResolvePathArg(name, ctx);
|
||||||
if (cvars::phase_a_trace_args) {
|
if (cvars::phase_a_trace_args) {
|
||||||
// Extraction mode: raw GPR args + resolved path, plus file.read detail.
|
// 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) {
|
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);
|
::xe::kernel::phase_a::EmitKernelReturn(name, return_value);
|
||||||
}
|
}
|
||||||
} // namespace phase_a_bridge
|
} // namespace phase_a_bridge
|
||||||
|
|||||||
Reference in New Issue
Block a user