[GPU] F10 ship-placement capture: dump world-space draw vertices for RE

One-shot snapshot for reversing capital-ship part placement. F10 arms
RequestShipCaptureFrame(); the next batch of draws is dumped to
xenia_ship_capture.log with each draw's guest vertex-buffer address, vertex/
index counts, and up to 64 WORLD-space vertex positions. Capital-ship parts are
transformed into world space before the draw (unlike stage geometry, which
matches the .xpr byte-for-byte), so these are ground truth for the assembled
placement; a reborn-side correlator affine-fits each decoded .xpr part to a
captured draw to recover its exact transform. De-duped by buffer address, gated
independently of the log_draws cvar, budget-capped at 8000 draws / 4096 buffers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-24 09:49:45 +02:00
parent 201553aea3
commit 87d02d59f1
3 changed files with 124 additions and 0 deletions

View File

@@ -1067,6 +1067,11 @@ void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) {
case ui::VirtualKey::kF12: {
TakeScreenshot();
} 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: {
// Allow users to escape fullscreen (but not enter it).

View File

@@ -125,8 +125,115 @@ const char* ReVertexFormatName(xenos::VertexFormat f) {
}
} // 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.log with each draw's guest
// vertex-buffer address, vertex/index counts, and up to 64 WORLD-space vertex
// positions. Capital-ship parts are transformed into world space before the
// draw, so these positions are ground truth for the assembled placement — a
// tool correlates each draw to a decoded .xpr part (vertex count → affine fit).
// De-duped by buffer address, so a static ship's parts collapse to one record
// each even across the few frames the budget spans.
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() {
g_ship_capture_gen.fetch_add(1, std::memory_order_relaxed);
g_ship_capture_remaining.store(kShipCaptureBudget, std::memory_order_relaxed);
XELOGI("[SHIP-CAP] capture armed → xenia_ship_capture.log");
}
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<uint32_t> cap_seen; // by vertex-buffer address
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) reopens the file and clears the de-dup set.
uint32_t gen = g_ship_capture_gen.load(std::memory_order_relaxed);
if (gen != cap_gen || !cap_out.is_open()) {
cap_out.open("xenia_ship_capture.log", std::ios::out | std::ios::trunc);
cap_seen.clear();
cap_gen = gen;
XELOGI("[SHIP-CAP] writing xenia_ship_capture.log");
}
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 vertex buffer.
if (!cap_seen.insert(vbase).second) {
return;
}
if (cap_seen.size() > 4096) {
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";
cap_out.flush();
}
void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value,
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) {
return;
}

View File

@@ -35,6 +35,10 @@ class ByteStream;
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 ReadbackResolveMode {
@@ -453,6 +457,14 @@ class CommandProcessor {
void LogDrawForRE(uint32_t vgt_draw_initiator_value,
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
// implementations.
SwapPostEffect GetActualSwapPostEffect() const {