diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 6f2e01201..f62243e91 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -1071,6 +1071,11 @@ void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) { // RE: snapshot the next frame's draws (world-space vertex positions) to // xenia_ship_capture.log for capital-ship placement correlation. xe::gpu::RequestShipCaptureFrame(); + // …and, when `log_ui_draws` is set, the next few frames' draws in + // SUBMISSION ORDER for 2D screen paint-order correlation. It shares this + // hotkey because every F-key in this switch is already bound; the cvar + // keeps it off the normal ship-capture path. + xe::gpu::RequestUiDrawCapture(); } break; case ui::VirtualKey::kEscape: { diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 245543515..b52a46d7d 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -90,6 +90,30 @@ DEFINE_bool( "game mesh formats against GPU ground truth.", "GPU"); +DEFINE_bool( + log_ui_draws, false, + "Reverse-engineering aid: arm a UI DRAW-ORDER capture with F10. Writes " + "every draw of the next few swapped frames to xenia_re_ui_draws_NN.log in " + "SUBMISSION ORDER and undeduplicated, with each draw's bound texture base " + "and dimensions. For recovering the paint order of a 2D screen, which " + "log_draws cannot give (it de-dups by vertex declaration) and the F10 ship " + "capture cannot either (it skips draws with no f32x3 position stream).", + "GPU"); + +DEFINE_int32( + ui_draw_capture_frames, 3, + "Reverse-engineering aid: how many submitted frames one `log_ui_draws` " + "capture covers. 3 is enough for a screen that redraws its elements every " + "frame; a screen that composites once and then re-blits needs a window long " + "enough to contain the rebuild.", + "GPU"); + +DEFINE_int32( + ui_draw_capture_max, 20000, + "Reverse-engineering aid: hard stop on the number of draws one " + "`log_ui_draws` capture records, whatever the frame window says.", + "GPU"); + DEFINE_bool( readback_memexport, false, "Read data written by memory export in shaders on the CPU. " @@ -146,6 +170,174 @@ void RequestShipCaptureFrame() { XELOGI("[SHIP-CAP] capture armed → xenia_ship_capture_{:02d}.log", gen); } +// ── UI draw-order capture (RE) ────────────────────────────────────────────── +// F9 arms a snapshot of the next few swapped frames. Unlike the two hooks +// above this one does NOT de-duplicate: the whole question it answers is what +// order the guest submits a 2D screen's sprites in, and a de-duplicating log +// cannot answer that. Bounded by a frame count rather than a draw count so the +// records line up with what is on screen. +namespace { +std::atomic g_ui_capture_gen{0}; +std::atomic g_ui_capture_armed{false}; +// Frames are counted HERE rather than from `counter_`, which looks like a frame +// number and is not one: MarkVblank() also increments it, from the vblank +// thread, so it advances between two draws of the SAME frame. A capture bounded +// by it ended after a single draw. This one is bumped only by the VdSwap packet, +// on the command-processor thread, so it is exactly "frames submitted". +std::atomic g_ui_swap_count{0}; +} // namespace + +void NoteUiDrawCaptureSwap() { + if (g_ui_capture_armed.load(std::memory_order_relaxed)) { + g_ui_swap_count.fetch_add(1, std::memory_order_relaxed); + } +} + +void RequestUiDrawCapture() { + if (!cvars::log_ui_draws) { + return; + } + uint32_t gen = g_ui_capture_gen.fetch_add(1, std::memory_order_relaxed) + 1; + g_ui_swap_count.store(0, std::memory_order_relaxed); + g_ui_capture_armed.store(true, std::memory_order_relaxed); + XELOGI("[UI-CAP] capture armed -> xenia_re_ui_draws_{:02d}.log", gen); +} + +void CommandProcessor::CaptureUiDrawForRE( + uint32_t vgt_draw_initiator_value, + const IndexBufferInfo* index_buffer_info) { + if (!g_ui_capture_armed.load(std::memory_order_relaxed)) { + return; + } + static std::mutex ui_mutex; + static std::ofstream ui_out; + static uint32_t ui_gen = 0; + static uint32_t ui_first_frame = 0; + static int ui_draws = 0; + static uint32_t ui_last_frame = 0; + std::lock_guard lock(ui_mutex); + if (!g_ui_capture_armed.load(std::memory_order_relaxed)) { + return; + } + + uint32_t gen = g_ui_capture_gen.load(std::memory_order_relaxed); + if (gen != ui_gen || !ui_out.is_open()) { + if (ui_out.is_open()) { + ui_out.close(); + } + auto name = fmt::format("xenia_re_ui_draws_{:02d}.log", gen); + ui_out.open(name, std::ios::out | std::ios::trunc); + ui_gen = gen; + ui_first_frame = g_ui_swap_count.load(std::memory_order_relaxed); + ui_last_frame = ui_first_frame; + ui_draws = 0; + XELOGI("[UI-CAP] writing {} (from frame {})", name, ui_first_frame); + ui_out << fmt::format( + "# every draw in SUBMISSION ORDER, undeduplicated, frames {}..{}\n" + "# tex dimensions identify the sprite; base is the guest address\n", + ui_first_frame, + ui_first_frame + uint32_t(std::max(1, cvars::ui_draw_capture_frames)) - + 1); + } + if (!ui_out.is_open()) { + g_ui_capture_armed.store(false, std::memory_order_relaxed); + return; + } + uint32_t frame = g_ui_swap_count.load(std::memory_order_relaxed); + const uint32_t capture_frames = + uint32_t(std::max(1, cvars::ui_draw_capture_frames)); + if (frame - ui_first_frame >= capture_frames || + ui_draws >= std::max(1, cvars::ui_draw_capture_max)) { + ui_out.flush(); + g_ui_capture_armed.store(false, std::memory_order_relaxed); + XELOGI("[UI-CAP] done: {} draws over {} frames", ui_draws, + frame - ui_first_frame); + return; + } + if (frame != ui_last_frame) { + ui_out << fmt::format("--- frame {} ---\n", frame); + ui_last_frame = frame; + } + + reg::VGT_DRAW_INITIATOR init; + init.value = vgt_draw_initiator_value; + ui_out << fmt::format("{:4} prim={} indices={}", ui_draws++, + uint32_t(init.prim_type), + uint32_t(init.num_indices)); + if (index_buffer_info) { + ui_out << fmt::format(" ib=0x{:08X}", index_buffer_info->guest_base); + } + if (active_vertex_shader_) { + ui_out << fmt::format(" vs=0x{:016X}", + active_vertex_shader_->ucode_data_hash()); + } + Shader* ps = active_pixel_shader_; + if (ps) { + ui_out << fmt::format(" ps=0x{:016X}", ps->ucode_data_hash()); + } + if (ps && ps->is_ucode_analyzed()) { + for (const auto& tb : ps->texture_bindings()) { + xenos::xe_gpu_texture_fetch_t tf = + register_file_->GetTextureFetch(tb.fetch_constant); + // Dimensions are stored as (actual - 1). + ui_out << fmt::format(" tex[base=0x{:08X} {}x{} fmt={}]", + uint32_t(tf.base_address) << 12, + uint32_t(tf.size_2d.width) + 1, + uint32_t(tf.size_2d.height) + 1, + uint32_t(tf.format)); + } + } + // The quad's geometry is the only thing that says WHICH element a draw is: + // these sprites all share one shader and sample big texture pages, so the + // vertex data is the identity. Dump attribute 0 of binding 0 for the first + // few vertices, raw and as big-endian floats — the format is not assumed. + Shader* vs = active_vertex_shader_; + if (vs && vs->is_ucode_analyzed() && !vs->vertex_bindings().empty()) { + const auto& binding = vs->vertex_bindings()[0]; + xenos::xe_gpu_vertex_fetch_t fetch = + register_file_->GetVertexFetch(binding.fetch_constant); + uint32_t stride = binding.stride_words * 4; + uint32_t vbase = uint32_t(fetch.address) << 2; + ui_out << fmt::format("\n vb=0x{:08X} stride={} attrs=[", vbase, stride); + int32_t pos_off = -1; + uint32_t pos_fmt = 0; + for (const auto& attr : binding.attributes) { + uint32_t f = uint32_t(attr.fetch_instr.attributes.data_format); + uint32_t off = attr.fetch_instr.attributes.offset * 4; + ui_out << fmt::format("{}@{} ", f, off); + if (pos_off < 0) { + pos_off = int32_t(off); + pos_fmt = f; + } + } + ui_out << "]"; + if (stride && vbase && pos_off >= 0) { + auto be_u32 = [](const uint8_t* q) { + return (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) | + (uint32_t(q[2]) << 8) | uint32_t(q[3]); + }; + uint32_t nv = uint32_t(init.num_indices); + if (nv > 8) { + nv = 8; + } + ui_out << fmt::format(" fmt0={} v:", pos_fmt); + for (uint32_t v = 0; v < nv; ++v) { + const uint8_t* q = memory_->TranslatePhysical( + vbase + v * stride + uint32_t(pos_off)); + if (!q) { + break; + } + uint32_t w0 = be_u32(q), w1 = be_u32(q + 4); + float f0, f1; + std::memcpy(&f0, &w0, 4); + std::memcpy(&f1, &w1, 4); + ui_out << fmt::format(" [{:08X},{:08X}={:.2f},{:.2f}]", w0, w1, f0, f1); + } + } + } + ui_out << "\n"; +} + void CommandProcessor::CaptureShipDrawForRE( uint32_t vgt_draw_initiator_value, const IndexBufferInfo* index_buffer_info) { @@ -327,8 +519,9 @@ void CommandProcessor::CaptureShipDrawForRE( 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. + // One-shot captures run independently of the log_draws cvar. CaptureShipDrawForRE(vgt_draw_initiator_value, index_buffer_info); + CaptureUiDrawForRE(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 3214d6c8c..b1f1cde23 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -39,6 +39,15 @@ namespace gpu { // Called from the UI thread (F10 hotkey); thread-safe. Defined in command_processor.cc. void RequestShipCaptureFrame(); +// Arm a one-shot UI DRAW-ORDER capture (see CommandProcessor::CaptureUiDrawForRE). +// Shares the F10 hotkey with the ship capture; the `log_ui_draws` cvar is what +// separates them. +void RequestUiDrawCapture(); + +// Count one submitted frame for that capture. Called from the VdSwap packet +// handler, on the command-processor thread. +void NoteUiDrawCaptureSwap(); + enum class GPUSetting { ClearMemoryPageState, ReadbackMemexport }; enum class ReadbackResolveMode { @@ -465,6 +474,24 @@ class CommandProcessor { void CaptureShipDrawForRE(uint32_t vgt_draw_initiator_value, const IndexBufferInfo* index_buffer_info); + // UI draw-order capture (RE): a one-shot snapshot armed by + // RequestUiDrawCapture() (F9 hotkey). Writes EVERY draw of the next few + // swapped frames to xenia_re_ui_draws_NN.log **in submission order and + // undeduplicated**, with each draw's bound texture base and dimensions. + // + // This exists because neither of the other two hooks can answer "in what + // order does the game paint a 2D screen": + // * `log_draws` de-dups by vertex-declaration fingerprint, so a screen's + // sprites — which share a declaration — collapse to one record, and what + // survives is first-seen order, not per-frame submission order; + // * CaptureShipDrawForRE returns early on any draw with no f32x3 position + // stream, which is every UI quad. + // Texture dimensions are the identity here: a screen's sprites decode to + // near-unique sizes, so `640x360` names the title screen's background as + // surely as a resource name would. Defined in command_processor.cc. + void CaptureUiDrawForRE(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 { diff --git a/src/xenia/gpu/pm4_command_processor_implement.h b/src/xenia/gpu/pm4_command_processor_implement.h index b09e0c877..3ef27d527 100644 --- a/src/xenia/gpu/pm4_command_processor_implement.h +++ b/src/xenia/gpu/pm4_command_processor_implement.h @@ -663,6 +663,10 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_XE_SWAP(uint32_t packet, COMMAND_PROCESSOR::IssueSwap(frontbuffer_ptr, frontbuffer_width, frontbuffer_height); + // RE: bracket the UI draw-order capture on real submitted frames (`counter_` + // below is also bumped by MarkVblank, from another thread, so it cannot). + xe::gpu::NoteUiDrawCaptureSwap(); + ++counter_; return true; }