diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 8e3a31540..6f2e01201 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -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). diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index c7b01575c..68670281d 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -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 g_ship_capture_gen{0}; +std::atomic 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 cap_seen; // by vertex-buffer address + static uint32_t cap_gen = 0; + std::lock_guard 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(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; } diff --git a/src/xenia/gpu/command_processor.h b/src/xenia/gpu/command_processor.h index 4c1962963..3214d6c8c 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -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 {