[GPU] Add log_draws: dump per-draw vertex declaration for RE
Reverse-engineering aid for decoding game mesh formats against GPU ground
truth. When the `log_draws` cvar is set, each distinct draw's primitive
type, index buffer (guest base / count / format / endianness), and full
per-stream vertex declaration (fetch-constant base + stride, and every
element's format + offset) is written to xenia_re_draws.log.
- command_processor.{h,cc}: CommandProcessor::LogDrawForRE(), no-op unless
the cvar is set. De-dups by the vertex-declaration fingerprint (shader +
primitive + element formats/offsets), so animated UI that redraws the
same format into fresh buffers every frame collapses to one record --
keeping it near-free (an earlier address-keyed de-dup flooded the log and
stalled the GPU thread). Capped at 4096 distinct formats.
- pm4_command_processor_implement.h: call it from ExecutePacketType3Draw
after IssueDraw (so the vertex shader has been analyzed). Backend-agnostic
base path -- works for the Vulkan build.
Used to confirm Project Sylpheed's XBG7 mesh layout (triangle list, pos
f32x3 / normal f16x4 / uv f16x2, variable stride).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,9 @@
|
|||||||
|
|
||||||
#include "xenia/gpu/command_processor.h"
|
#include "xenia/gpu/command_processor.h"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
#include "third_party/fmt/include/fmt/format.h"
|
#include "third_party/fmt/include/fmt/format.h"
|
||||||
#include "xenia/base/byte_stream.h"
|
#include "xenia/base/byte_stream.h"
|
||||||
#include "xenia/base/clock.h"
|
#include "xenia/base/clock.h"
|
||||||
@@ -18,6 +21,8 @@
|
|||||||
#include "xenia/gpu/gpu_flags.h"
|
#include "xenia/gpu/gpu_flags.h"
|
||||||
#include "xenia/gpu/graphics_system.h"
|
#include "xenia/gpu/graphics_system.h"
|
||||||
#include "xenia/gpu/packet_disassembler.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/sampler_info.h"
|
||||||
#include "xenia/gpu/texture_info.h"
|
#include "xenia/gpu/texture_info.h"
|
||||||
#include "xenia/gpu/xenos_zpd_report.h"
|
#include "xenia/gpu/xenos_zpd_report.h"
|
||||||
@@ -77,6 +82,14 @@ DEFINE_string(
|
|||||||
|
|
||||||
UPDATE_from_string(readback_resolve, 2025, 12, 4, 21, "fast");
|
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(
|
DEFINE_bool(
|
||||||
readback_memexport, false,
|
readback_memexport, false,
|
||||||
"Read data written by memory export in shaders on the CPU. "
|
"Read data written by memory export in shaders on the CPU. "
|
||||||
@@ -88,6 +101,126 @@ DEFINE_bool(
|
|||||||
namespace xe {
|
namespace xe {
|
||||||
namespace gpu {
|
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<uint64_t> re_seen;
|
||||||
|
std::lock_guard<std::mutex> 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
|
// This should be written completely differently with support for different
|
||||||
// types.
|
// types.
|
||||||
void SaveGPUSetting(GPUSetting setting, uint64_t value) {
|
void SaveGPUSetting(GPUSetting setting, uint64_t value) {
|
||||||
|
|||||||
@@ -446,6 +446,13 @@ class CommandProcessor {
|
|||||||
}
|
}
|
||||||
virtual bool IssueCopy() { return false; }
|
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
|
// "Actual" is for the command processor thread, to be read by the
|
||||||
// implementations.
|
// implementations.
|
||||||
SwapPostEffect GetActualSwapPostEffect() const {
|
SwapPostEffect GetActualSwapPostEffect() const {
|
||||||
|
|||||||
@@ -1152,6 +1152,11 @@ bool COMMAND_PROCESSOR::ExecutePacketType3Draw(
|
|||||||
uint32_t(vgt_draw_initiator.prim_type),
|
uint32_t(vgt_draw_initiator.prim_type),
|
||||||
uint32_t(vgt_draw_initiator.source_select));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user