Answered against the pre-registration committed before the capture.
Units per second was the last open number on the PRESS (A) plate, and both
prior measurements of it were wall-clock readings off an emulator that runs
the guest slow by an unknown factor. They disagreed by 2.9x, because a 30 Hz
guest at full speed and a 60 Hz guest at half speed look identical on a wall
clock.
The ruler here is not a clock. ADV.wmv declares 30.0000 fps in its own ASF
header, so a decoded movie frame is a tick the emulator's speed cannot
stretch. Presented frames per decoded movie frame is guest_fps/30 with no
wall clock in the chain.
predicted H_A 30 fps -> 60 units/s -> 1.0
H_B 60 fps -> 120 units/s -> 2.0
measured 1.0000
Both pre-registered guards pass. Guard 2: a perfect repeating 3-buffer
cycle, 52 uses each (exactly 156/3), 2 chroma planes per luma on 156 of 156.
Guard 1: run lengths are 156 runs ALL of length 1 -- no smear, so the
dropped-movie-frame bias that would have pushed the answer toward 120 is
measurably absent rather than argued away.
So H_A. The port keeps its 60 and changes nothing.
REFUTES the live H3 hypothesis that 120 units/s explains the play-test's
late plate. That hypothesis was well-formed and attractive precisely because
it would have explained the complaint, which is why it needed a ruler that
is not a clock.
Which means finding 3 still has no cause. The strongest remaining candidate
is decoded rather than speculative: the plate's declared onset is t=214, not
t=236 -- a 22-unit fade, matching the T=22 the oracle confirmed by measuring
+23 alpha per presented frame on that element.
The pre-registered control could NOT be run: this logger build emits vb=
addresses, not vertex contents, so there was no alpha to check +34 against.
A weaker control is substituted and labelled -- the splash shader/blend
census, which validates the log's structure (what this measurement uses) and
not alpha extraction (which it does not).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
93 lines
3.8 KiB
Python
Executable File
93 lines
3.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""How many swap labels does the guest present per DECODED MOVIE FRAME?
|
|
|
|
The ruler is a disc fact: ADV.wmv declares 30.0000 fps in its ASF header
|
|
(ExtendedStreamProperties stream #2, avgTimePerFrame = 333333 x100ns). So one
|
|
decoded movie frame is one tick of a clock the emulator's speed cannot stretch,
|
|
and 'labels per movie frame' is guest_fps / 30 with no wall clock in it.
|
|
|
|
Pre-registered in docs/re/guest-frame-rate-preregistration.md BEFORE any capture:
|
|
H_A guest 30 fps -> 60 units/s -> 1.0 labels/frame (accept 1.00 +/- 0.15)
|
|
H_B guest 60 fps -> 120 units/s -> 2.0 labels/frame (accept 2.00 +/- 0.30)
|
|
Anything else is reported as 'neither', not rounded to the closer one.
|
|
|
|
movie_frame_cadence.py <xenia_re_ui_draws_NN.log>
|
|
"""
|
|
import re, sys, collections
|
|
|
|
# The movie's luma plane. The splash census identified the movie's own textures
|
|
# as three 1280x720 and six 640x360, first appearing at frame 234; the 1280x720
|
|
# ones are the luma, triple-buffered.
|
|
TEX = re.compile(r'tex\[base=0x([0-9A-Fa-f]+)\s+(\d+)x(\d+)\s+fmt=(\d+)')
|
|
FRAME = re.compile(r'\bframe=(\d+)')
|
|
|
|
def main(path):
|
|
cur = None
|
|
per_frame = collections.OrderedDict() # frame -> set of luma bases
|
|
alpha_rows = []
|
|
for line in open(path, errors='replace'):
|
|
m = FRAME.search(line)
|
|
if m:
|
|
cur = int(m.group(1))
|
|
per_frame.setdefault(cur, set())
|
|
for base, w, h, fmt in TEX.findall(line):
|
|
if (int(w), int(h)) == (1280, 720) and cur is not None:
|
|
per_frame.setdefault(cur, set()).add(base.upper())
|
|
|
|
movie = [(f, s) for f, s in per_frame.items() if s]
|
|
if not movie:
|
|
print("NO 1280x720 textures found -- this log does not contain the movie.")
|
|
print("The capture must run long enough to reach the attract movie (frame >=234).")
|
|
return 2
|
|
print(f"movie-bearing frames: {len(movie)} (frames {movie[0][0]}..{movie[-1][0]})")
|
|
|
|
# Guard 2: the buffers must cycle through a small fixed set.
|
|
bases = collections.Counter()
|
|
for _, s in movie:
|
|
for b in s: bases[b] += 1
|
|
print(f"\ndistinct 1280x720 bases: {len(bases)}")
|
|
for b, n in bases.most_common(8):
|
|
print(f" 0x{b} in {n} frames")
|
|
if not 2 <= len(bases) <= 4:
|
|
print("!! not a 3-buffer cycle -- guard 2 fails, do not read the ratio below")
|
|
|
|
# Run lengths: how many CONSECUTIVE labels carry the same base set.
|
|
runs = []
|
|
prev = None; n = 0
|
|
for f, s in movie:
|
|
key = tuple(sorted(s))
|
|
if key == prev: n += 1
|
|
else:
|
|
if prev is not None: runs.append(n)
|
|
prev = key; n = 1
|
|
if prev is not None: runs.append(n)
|
|
|
|
dist = collections.Counter(runs)
|
|
total = sum(runs)
|
|
print(f"\nrun-length distribution (labels holding the same luma base set):")
|
|
for k in sorted(dist):
|
|
bar = '#' * min(60, dist[k])
|
|
print(f" {k:>3} label(s): {dist[k]:>4} {bar}")
|
|
ratio = total / len(runs)
|
|
print(f"\n {len(runs)} runs over {total} labels -> {ratio:.3f} labels per movie frame")
|
|
|
|
# Guard 1: a spike, not a smear.
|
|
mode = dist.most_common(1)[0]
|
|
print(f" mode = {mode[0]} label(s), {100*mode[1]/len(runs):.1f}% of runs")
|
|
if 100*mode[1]/len(runs) < 70:
|
|
print(" !! not a clean spike -- frame-dropping smear, guard 1 flags this")
|
|
|
|
print()
|
|
if abs(ratio - 1.0) <= 0.15:
|
|
print(" ==> H_A: guest presents at 30 fps, so 60 UNITS PER SECOND.")
|
|
print(" The plate's t=236 is 3.93 s. The port's current value stands.")
|
|
elif abs(ratio - 2.0) <= 0.30:
|
|
print(" ==> H_B: guest presents at 60 fps, so 120 UNITS PER SECOND.")
|
|
print(" The plate's t=236 is 1.97 s -- the port is 1.96 s LATE.")
|
|
else:
|
|
print(" ==> NEITHER band. Reporting as such rather than rounding to the closer.")
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main(sys.argv[1]))
|