|
|
|
|
@@ -125,8 +125,138 @@ 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";
|
|
|
|
|
|
|
|
|
|
// Vertex-shader float constants: the buffer holds LOCAL positions, so the
|
|
|
|
|
// per-part world (or world-view-projection) matrix that places the part lives
|
|
|
|
|
// here as a run of float4 constants. Diffing two parts' constants isolates the
|
|
|
|
|
// matrix (the camera VP block is shared). Dump the first 48 vec4 from the VS
|
|
|
|
|
// constant base as host-float (the register file stores them host-endian).
|
|
|
|
|
auto vsc = register_file_->Get<reg::SQ_VS_CONST>();
|
|
|
|
|
uint32_t cbase = vsc.base; // starting float4 index
|
|
|
|
|
cap_out << fmt::format(" vsconst base={}:", cbase);
|
|
|
|
|
for (uint32_t i = 0; i < 48; ++i) {
|
|
|
|
|
uint32_t idx = cbase + i;
|
|
|
|
|
if (idx >= 256) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
uint32_t r = XE_GPU_REG_SHADER_CONSTANT_000_X + 4 * idx;
|
|
|
|
|
float cx = register_file_->Get<float>(r);
|
|
|
|
|
float cy = register_file_->Get<float>(r + 1);
|
|
|
|
|
float cz = register_file_->Get<float>(r + 2);
|
|
|
|
|
float cw = register_file_->Get<float>(r + 3);
|
|
|
|
|
cap_out << fmt::format(" c{}=({:.4f},{:.4f},{:.4f},{:.4f})", i, cx, cy, cz,
|
|
|
|
|
cw);
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|