diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 48d15e5d9..89d6bbb6b 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -215,7 +215,19 @@ void CommandProcessor::CaptureShipDrawForRE( uint32_t bits = register_file_->values[r]; th = (th ^ bits) * 1099511628211ull; } - uint64_t key = (uint64_t(vbase) << 32) ^ (th & 0xFFFFFFFFull) ^ (th >> 32); + // Mix the index range into the key as well (added 2026-08-13). The engine + // issues SEVERAL draws over one vertex buffer, each with its own index + // sub-range (the first Stage_02 capture showed a 119-vertex hull LOD drawn + // with 21 indices) — keying on (vbase, transform) alone kept only the first + // batch, which reads like a mysteriously short draw. With the range in the key + // every batch is recorded, so the block's full index extent is observable. + uint64_t ib_key = 0; + if (index_buffer_info) { + ib_key = (uint64_t(index_buffer_info->guest_base) << 20) ^ + uint64_t(index_buffer_info->count); + } + uint64_t key = (uint64_t(vbase) << 32) ^ (th & 0xFFFFFFFFull) ^ (th >> 32) ^ + (ib_key * 1099511628211ull); if (!cap_seen.insert(key).second) { return; } @@ -227,6 +239,47 @@ void CommandProcessor::CaptureShipDrawForRE( "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()); + // The guest's INDEX buffer for this draw. Our offline mesh decoder only + // *assumes* the index buffer sits immediately before the vertex buffer; this + // line is the ground truth for that assumption (ib base vs vbase), and the + // decoded min/max index says how much of the vertex pool the draw really + // covers — which is what the `indices=` field alone cannot answer. + if (index_buffer_info) { + const auto& ib = *index_buffer_info; + bool i32 = ib.format == xenos::IndexFormat::kInt32; + uint32_t icount = ib.count; + cap_out << fmt::format( + " ib base=0x{:08X} count={} fmt={} endian={} len={} delta_vb={}", + ib.guest_base, icount, i32 ? "u32" : "u16", uint32_t(ib.endianness), + ib.length, int64_t(vbase) - int64_t(ib.guest_base)); + const uint8_t* ip = memory_->TranslatePhysical(ib.guest_base); + if (ip && icount) { + uint32_t scan = icount < 65536 ? icount : 65536; + uint32_t imin = 0xFFFFFFFFu, imax = 0; + auto rd = [&](uint32_t k) -> uint32_t { + // Guest index data is big-endian in memory (the endianness field says + // how the GPU swaps it); read it that way and record the field so the + // offline side can compensate if a draw ever differs. + const uint8_t* q = ip + (i32 ? k * 4 : k * 2); + return i32 ? (uint32_t(q[0]) << 24) | (uint32_t(q[1]) << 16) | + (uint32_t(q[2]) << 8) | uint32_t(q[3]) + : (uint32_t(q[0]) << 8) | uint32_t(q[1]); + }; + for (uint32_t k = 0; k < scan; ++k) { + uint32_t v = rd(k); + if (v < imin) imin = v; + if (v > imax) imax = v; + } + cap_out << fmt::format(" min={} max={} idx:", imin, imax); + uint32_t nd = icount < 24 ? icount : 24; + for (uint32_t k = 0; k < nd; ++k) { + cap_out << fmt::format(" {}", rd(k)); + } + } + cap_out << "\n"; + } else { + cap_out << " ib auto\n"; + } uint32_t n = vcount < 64 ? vcount : 64; cap_out << " pos:"; auto be_f32 = [](const uint8_t* q) {