diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 940a9a48a..c8584405e 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -9,6 +9,9 @@ #include "xenia/gpu/command_processor.h" +#include +#include + #include "third_party/fmt/include/fmt/format.h" #include "xenia/base/byte_stream.h" #include "xenia/base/clock.h" @@ -18,6 +21,8 @@ #include "xenia/gpu/gpu_flags.h" #include "xenia/gpu/graphics_system.h" #include "xenia/gpu/packet_disassembler.h" +#include "xenia/gpu/registers.h" +#include "xenia/gpu/shader.h" #include "xenia/gpu/sampler_info.h" #include "xenia/gpu/texture_info.h" #include "xenia/gpu/xenos_zpd_report.h" @@ -77,6 +82,14 @@ DEFINE_string( UPDATE_from_string(readback_resolve, 2025, 12, 4, 21, "fast"); +DEFINE_bool( + log_draws, false, + "Reverse-engineering aid: write each distinct draw's primitive type, index " + "buffer, and per-stream vertex declaration (stream base/stride + per-element " + "format/offset) to xenia_re_draws.log in the working directory. For decoding " + "game mesh formats against GPU ground truth.", + "GPU"); + DEFINE_bool( readback_memexport, false, "Read data written by memory export in shaders on the CPU. " @@ -88,6 +101,126 @@ DEFINE_bool( namespace xe { namespace gpu { +namespace { +// Short readable name for a guest vertex element format (RE logging only). +const char* ReVertexFormatName(xenos::VertexFormat f) { + switch (f) { + case xenos::VertexFormat::k_32_FLOAT: return "f32"; + case xenos::VertexFormat::k_32_32_FLOAT: return "f32x2"; + case xenos::VertexFormat::k_32_32_32_FLOAT: return "f32x3"; + case xenos::VertexFormat::k_32_32_32_32_FLOAT: return "f32x4"; + case xenos::VertexFormat::k_16_16_FLOAT: return "f16x2"; + case xenos::VertexFormat::k_16_16_16_16_FLOAT: return "f16x4"; + case xenos::VertexFormat::k_16_16: return "s16x2"; + case xenos::VertexFormat::k_16_16_16_16: return "s16x4"; + case xenos::VertexFormat::k_8_8_8_8: return "8888"; + case xenos::VertexFormat::k_2_10_10_10: return "2_10_10_10"; + case xenos::VertexFormat::k_10_11_11: return "10_11_11"; + case xenos::VertexFormat::k_11_11_10: return "11_11_10"; + case xenos::VertexFormat::k_32: return "u32"; + case xenos::VertexFormat::k_32_32: return "u32x2"; + case xenos::VertexFormat::k_32_32_32_32: return "u32x4"; + default: return "?"; + } +} +} // namespace + +void CommandProcessor::LogDrawForRE(uint32_t vgt_draw_initiator_value, + const IndexBufferInfo* index_buffer_info) { + if (!cvars::log_draws) { + return; + } + static std::mutex re_mutex; + static std::ofstream re_out; + static std::unordered_set re_seen; + std::lock_guard lock(re_mutex); + if (!re_out.is_open()) { + re_out.open("xenia_re_draws.log", std::ios::out | std::ios::trunc); + XELOGI("[RE-DRAW] logging distinct draws to xenia_re_draws.log"); + } + if (!re_out.is_open()) { + return; + } + + reg::VGT_DRAW_INITIATOR init; + init.value = vgt_draw_initiator_value; + Shader* vs = active_vertex_shader_; + + // De-dup by the VERTEX-DECLARATION FINGERPRINT (shader + primitive type + + // per-stream element formats/offsets), NOT by buffer address. Animated UI + // that redraws the same mesh format into fresh buffers every frame therefore + // collapses to a single record, keeping the logging near-free — while every + // distinct mesh format (the player plane, each weapon) is still captured once. + uint64_t sig = 1469598103934665603ull; // FNV-ish seed + auto mix = [&sig](uint64_t v) { sig = (sig ^ v) * 1099511628211ull; }; + mix(uint64_t(init.prim_type)); + if (vs) { + mix(vs->ucode_data_hash()); + } + bool analyzed = vs && vs->is_ucode_analyzed(); + if (analyzed) { + for (const auto& binding : vs->vertex_bindings()) { + mix(binding.fetch_constant); + mix(binding.stride_words); + for (const auto& attr : binding.attributes) { + mix(uint64_t(attr.fetch_instr.attributes.data_format)); + mix(uint64_t(uint32_t(attr.fetch_instr.attributes.offset))); + } + } + } else { + mix(uint64_t(init.num_indices)); + } + if (!re_seen.insert(sig).second) { + return; + } + // Safety cap on distinct formats, so a pathological title can't grow the log + // (and the working set) without bound. + if (re_seen.size() > 4096) { + return; + } + + re_out << fmt::format("DRAW prim={} indices={} src={} ", + uint32_t(init.prim_type), uint32_t(init.num_indices), + uint32_t(init.source_select)); + if (index_buffer_info) { + re_out << fmt::format( + "index[base=0x{:08X} count={} fmt={} endian={} len={}] ", + index_buffer_info->guest_base, index_buffer_info->count, + index_buffer_info->format == xenos::IndexFormat::kInt16 ? "u16" : "u32", + uint32_t(index_buffer_info->endianness), index_buffer_info->length); + } else { + re_out << "index[auto] "; + } + if (vs) { + re_out << fmt::format("vs=0x{:016X}", vs->ucode_data_hash()); + } + re_out << "\n"; + + if (analyzed) { + for (const auto& binding : vs->vertex_bindings()) { + xenos::xe_gpu_vertex_fetch_t fetch = + register_file_->GetVertexFetch(binding.fetch_constant); + re_out << fmt::format( + " stream fc={} base=0x{:08X} stride_words={} size_words={} " + "endian={} type={}\n", + binding.fetch_constant, uint32_t(fetch.address) << 2, + binding.stride_words, uint32_t(fetch.size), uint32_t(fetch.endian), + uint32_t(fetch.type)); + for (const auto& attr : binding.attributes) { + const auto& a = attr.fetch_instr.attributes; + re_out << fmt::format( + " attr fmt={}({}) offset_words={} stride_words={} signed={} " + "int={} exp_adjust={}\n", + uint32_t(a.data_format), ReVertexFormatName(a.data_format), a.offset, + a.stride, a.is_signed ? 1 : 0, a.is_integer ? 1 : 0, a.exp_adjust); + } + } + } else { + re_out << " (vertex shader not analyzed yet)\n"; + } + re_out.flush(); +} + // This should be written completely differently with support for different // types. void SaveGPUSetting(GPUSetting setting, uint64_t value) { diff --git a/src/xenia/gpu/command_processor.h b/src/xenia/gpu/command_processor.h index 9fce69114..4c1962963 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -446,6 +446,13 @@ class CommandProcessor { } virtual bool IssueCopy() { return false; } + // Reverse-engineering aid (cvar `log_draws`): dump the guest's exact + // primitive type + index buffer + per-stream vertex declaration for each + // distinct draw to a dedicated file, for decoding game mesh formats. + // No-op unless the cvar is enabled. Defined in command_processor.cc. + void LogDrawForRE(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 8f8519feb..b09e0c877 100644 --- a/src/xenia/gpu/pm4_command_processor_implement.h +++ b/src/xenia/gpu/pm4_command_processor_implement.h @@ -1152,6 +1152,11 @@ bool COMMAND_PROCESSOR::ExecutePacketType3Draw( uint32_t(vgt_draw_initiator.prim_type), uint32_t(vgt_draw_initiator.source_select)); } + // Reverse-engineering aid (no-op unless the `log_draws` cvar is set): + // record the guest's primitive type + index buffer + vertex declaration. + // Placed after IssueDraw so the vertex shader has been analyzed. + COMMAND_PROCESSOR::LogDrawForRE( + vgt_draw_initiator.value, is_indexed ? &index_buffer_info : nullptr); } }