tools: export the Canary logger patch the clock findings depend on
/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
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
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
|
||||||
|
|
||||||
24
tools/canary-patches/README.md
Normal file
24
tools/canary-patches/README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Canary patches the corpus depends on
|
||||||
|
|
||||||
|
`/canary` is a **separate checkout that lives only in the container** and is not
|
||||||
|
pushed anywhere. A finding whose reproduce recipe needs a modified logger is not
|
||||||
|
reproducible if that modification exists only there — so any Canary change a
|
||||||
|
committed finding rests on is exported here as a patch.
|
||||||
|
|
||||||
|
Apply with `git -C /canary am < <patch>`, then rebuild:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sfn /canary /work/xenia-canary # the build needs this symlink present
|
||||||
|
cmake --build /sylph-home/re/canary-build --config Release --parallel 2 --target xenia_canary
|
||||||
|
rm /work/xenia-canary
|
||||||
|
```
|
||||||
|
|
||||||
|
| patch | canary sha | what needs it |
|
||||||
|
|---|---|---|
|
||||||
|
| `0001-content-hash-in-ui-draw-logger.patch` | `ab3203f79` on branch `sylpheed-re` | [`../../docs/re/guest-frame-rate-resolved.md`](../../docs/re/guest-frame-rate-resolved.md) and its withdrawal, [`../../docs/re/clock-is-frame-based-one-unit-per-present.md`](../../docs/re/clock-is-frame-based-one-unit-per-present.md) |
|
||||||
|
|
||||||
|
⚠️ The `h=` field this adds is what separates *"the buffer rotated"* from *"a
|
||||||
|
frame was decoded"*. Without it a draw log records only base addresses, and a
|
||||||
|
triple buffer rotating once per present looks identical to one decode per
|
||||||
|
present — the confusion that cost this corpus two withdrawn positions on
|
||||||
|
`units/second` in a single day.
|
||||||
Reference in New Issue
Block a user