/canary is a separate checkout that lives only in the container and is pushed nowhere. The content-hash field in the UI draw logger is what separates "the buffer rotated" from "a frame was decoded", and two committed findings -- guest-frame-rate-resolved.md and clock-is-frame-based-one-unit-per-present.md -- cannot be reproduced without it. Committed in /canary as ab3203f79 on branch sylpheed-re for a nameable sha, and exported here as a patch because that sha is not reachable from anywhere outside this box. Includes the rebuild recipe, since the build needs the /work/xenia-canary symlink present and that is not obvious from the failure it gives without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
99 lines
5.1 KiB
Diff
99 lines
5.1 KiB
Diff
From ab3203f7926d155fdeaf0c8f7616cf0a6d8a43b6 Mon Sep 17 00:00:00 2001
|
|
From: Sylpheed RE agent <agent@localhost>
|
|
Date: Tue, 1 Sep 2026 19:56:12 +0000
|
|
Subject: [PATCH] RE: log a CONTENT hash beside every sampled texture
|
|
|
|
A base address cannot distinguish 'the guest decoded a new frame' from 'the
|
|
guest rotated to the next buffer of a triple-buffered set' -- a rotating
|
|
buffer visits the same three addresses either way. Reading a clean
|
|
one-base-change-per-present as one decode per present is how
|
|
docs/re/guest-frame-rate-measured.md reached a conclusion it had to
|
|
withdraw.
|
|
|
|
h=<FNV-1a over 4096 sampled bytes>, omitted rather than faked when the
|
|
address does not translate, so a missing hash cannot read as a matching
|
|
one. Sampled rather than full: a 1280x720 plane is 900 KB and hashing all
|
|
of it per draw would change the thing being measured.
|
|
---
|
|
src/xenia/gpu/command_processor.cc | 57 ++++++++++++++++++++++++++----
|
|
1 file changed, 51 insertions(+), 6 deletions(-)
|
|
|
|
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<const uint8_t*>(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<uint32_t>(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 ──
|
|
--
|
|
2.43.0
|
|
|