Files
Sylpheed/tools/canary-patches/0004-RE-log-a-CONTENT-hash-beside-every-sampled-texture.patch
sylph-decoder 3f114a28af tools: export ALL FOUR container-only Canary commits, not just my own
Applying my own lesson to my own corpus rather than only stating it. I
exported the content-hash patch and stopped; auditing the docs for
"/canary sha" citations found three pages naming d90d14e02, and checking
/canary's history found the exposure is four commits deep, not one.

Bounded by measurement rather than guess: branch -r --contains puts
590912722 on origin/sylpheed-re and finds no remote for anything after it,
so the container-only stack is exactly these four.

  0f920e645  blend= per draw          <- the ENTIRE blend decode rests on this
  fa1e4c221  vertex dump 8 -> 64      <- at 8 the log silently dropped four
                                         quads of the EXTRAS 24-index batch
  d90d14e02  RT state, resolves, PS constants
  ab3203f79  h= content hash

The sharpest case is 0f920e645. Without it a draw log records no blend
state at all, so ui-blend-mode-decoded.md's 35-element oracle -- which
overturned a REFUTED entry and deleted the port's authored blend map --
could not be re-derived by anyone who cloned this repository.

The failure mode is silent: the recipe LOOKS complete and only fails for
someone on a different machine, long after its author could say what the
flag did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-01 19:59:14 +00:00

96 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 4/4] 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 ──