diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 25ada855a..c53ce3194 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -277,7 +277,21 @@ void CommandProcessor::CaptureUiDrawForRE( return; } if (frame != ui_last_frame) { - ui_out << fmt::format("--- frame {} ---\n", frame); + // ── RE: GUEST time on the frame boundary, not host time ───────────────── + // The question this exists for: the animation clock's rate. A step in a + // sprite's alpha per FRAME is not a rate — under this emulator the frame + // rate is whatever the host can manage — and a host wall-clock duration is + // the instrument that has already cost this corpus four withdrawn claims. + // + // `Clock::QueryGuestTickCount()` is the timebase the GUEST reads, at + // `guest_tick_frequency()` (set to 50 MHz in emulator.cc) with the guest + // time scalar applied. So a duration computed from these two numbers is the + // duration the GAME experienced, which is the only one its own integrator + // could have used. Printing the frequency beside the count means the reader + // does not have to know what it was set to. + ui_out << fmt::format("--- frame {} gtick={} gfreq={} ---\n", frame, + Clock::QueryGuestTickCount(), + Clock::guest_tick_frequency()); ui_last_frame = frame; } @@ -372,11 +386,42 @@ void CommandProcessor::CaptureUiDrawForRE( 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)); + // ── RE: a CONTENT hash of the sampled texture, not just its address ── + // The question this answers: "did the guest DECODE a new frame, or did it + // merely ROTATE to the next buffer of a triple-buffered set?" A base + // address cannot tell those apart — a rotating buffer visits the same three + // addresses whether or not anything was written into them — and reading a + // clean 1-base-change-per-present as "one decode per present" is exactly + // how `guest-frame-rate-measured.md` reached a conclusion it had to + // withdraw. Identical content on consecutive presents means rotation + // without decode; changing content means a genuine decode. + // + // Sampled, not full: a 1280x720 plane is 900 KB and hashing all of it per + // draw would change the thing being measured. 4096 bytes spread across the + // whole allocation is plenty to separate "identical" from "different" and + // costs nothing. `h=` is omitted rather than faked when the address does + // not translate, so a missing hash can never be read as a matching one. + uint32_t tbase = uint32_t(tf.base_address) << 12; + uint32_t tw = uint32_t(tf.size_2d.width) + 1; + uint32_t th = uint32_t(tf.size_2d.height) + 1; + ui_out << fmt::format(" tex[base=0x{:08X} {}x{} fmt={}", + tbase, tw, th, uint32_t(tf.format)); + if (tw > 1 && th > 1) { + const uint8_t* tp = + memory_->TranslatePhysical(tbase); + if (tp) { + // FNV-1a over a fixed stride so the sample set is deterministic and + // does not depend on the format's true bytes-per-pixel. + uint64_t hsh = 1469598103934665603ull; + uint32_t span = tw * th; // >= 1 byte per texel for every format here + uint32_t step = std::max(1u, span / 4096u); + for (uint32_t o = 0; o < span; o += step) { + hsh = (hsh ^ tp[o]) * 1099511628211ull; + } + ui_out << fmt::format(" h={:016X}", hsh); + } + } + ui_out << "]"; } } // ── RE: the PIXEL SHADER's float constants, as the shader itself indexes ──