Play-test finding 4 asked, in order: is there a pass, what is it, where do its parameters come from, only then what curve. All four, from GPU state. 1. NO post-process pass. Over all 1 048 draws of frames 4..226 (both boot splashes): rt0=[tile=0 fmt=0 exp=0] on 1048/1048, pitch=1280 msaa=0 on 1048/1048, edram_mode only ever kColorDepth or kCopy, resolve dests only the two alternating front buffers, and NO texture base anywhere in the capture equals a resolve destination. The only texture bound in the whole splash region is the sprite page 0x11A50000. No blur, no bloom, no fade quad over a resolved image, no tone curve, no resolve-and-resample. 2. What it is: per frame, a full-screen replace triangle (the clear), a full-screen black quad through the ordinary blend, ONE batched sprite draw carrying every visible element (indices 4/8/12/24), and the two presentation resolves. 3. Where the parameters come from: NOT the constant banks -- the splash pixel shaders read zero float constants, ps_c[n=0] on 1048/1048, taken off each shader's own float_bitmap. NOT immediates. The fade is the per-vertex k_8_8_8_8 colour in a vertex buffer the guest rewrites every frame. 4. The curve falls out of 3 and is the corpus's existing 34/frame law, reproduced on an independent capture. Not claimed as new. And the composite is ORDINARY SOURCE-OVER, confirmed from the shader ucode rather than inferred from the blend register: the register reads ONE/ONE_MINUS_SRC_ALPHA, which looks premultiplied, and is -- because the shader premultiplies. The two together are src*A + dst*(1-A). So the 'more pronounced' fade is neither a blend difference nor a pass. The likely mechanism instead: the developer splash submits SIX quads, three logos plus three slightly-larger companions that lead them by 8 frames and are gone 79 frames early. Two over-blended copies of the same art at different scale is a halo, on screen only during the entry. Refutation attempt, recorded: REFUTED.md's re-opened rest() pair states its settling condition as 'a draw capture of the developer splash naming which of the three glows is submitted at rest'. This is that capture. All three companions ARE submitted in all 21 frames -- so no rule that hides one describes this stream -- but they are NOT interchangeable: Q6 leaves the plateau on its own -5.6/frame decay while Q4 and Q5 hold 255. The one-byte sibling difference is drawn. Trap recorded with the evidence: censusing the whole 600-frame log finds six 640x360 textures sampled ~330 times each, which reads exactly like a half-resolution blur chain. They are the attract movie's chroma planes and first appear at frame 234, after both splashes. Restricting the window is what separates them. Evidence: docs/re/data/splash-draw-pass-census.txt, docs/re/data/splash-quad-timeline.txt, docs/re/data/shaders/*.ucode.frag. Logger: canary sylpheed-re d90d14e02.
14 KiB
The splash draw pass — there is no post-process, and the fade is in the vertex stream
Status: ✅ decoded (GPU state) for the pass structure; ✅ measured for the ramp. 2026-09-01. Instrument: ⟨capture⟩ — the real game in Xenia Canary, per-draw, with the render-target/resolve/constant fields added to the draw logger for this question. Nothing here rests on a renderer of ours.
Asked by ../agents/PLAYTEST-2026-09-01.md
finding 4, in the order the human set: is there a pass, what is it, where do its
parameters come from, and only then what curve.
The four answers, shortest first
- Is there a post-process pass at all? NO. Not a blur, not a bloom, not a fade quad over a resolved image, not a tone curve, not a resolve-and-resample. One pass, one render target, one texture.
- What is it, then? Per splash frame: a full-screen replace triangle (the clear), a full-screen black quad through the normal blend, one batched sprite draw carrying every visible splash element, and the two presentation resolves. Three pixel shaders in total, all trivial.
- Where do the parameters come from? Not the constant banks — the splash
pixel shaders read zero float constants, on all 1 048 draws. Not
immediates. The fade is the per-vertex
k_8_8_8_8colour, in a vertex buffer the guest rewrites every frame. - The curve falls out of 3 and is not fitted: alpha rises in integer steps of exactly 34/255 per presented frame, clamped at 255.
And the composite the game performs is ordinary source-over. Read the shader
below before concluding otherwise from the blend register: the blend is
ONE / ONE_MINUS_SRC_ALPHA, which looks like a premultiplied pipeline, and it
is — because the shader premultiplies. The two together are algebraically
src·α + dst·(1−α). A renderer compositing these sprites with straight
alpha-over is using the right equation.
1 — the evidence that there is no second pass
data/splash-draw-pass-census.txt, over all
1 048 draws of frames 4…226, which is both splashes end to end.
| what a post-process would show | what the census shows |
|---|---|
| a second render target | rt0=[tile=0 fmt=0 exp=0] on 1 048 / 1 048 draws — one EDRAM tile, one format, throughout |
| a reduced-resolution pass | pitch=1280 msaa=0 on 1 048 / 1 048 |
| an extra pass' draws | mode= is only ever 4 (kColorDepth, 628) or 6 (kCopy, 420). Nothing else |
| resolve-and-resample | resolve destinations are only 0x14570000 and 0x14910000, 210 each — one pair per frame, the alternating front buffers. No texture base anywhere in the capture equals a resolve destination |
| a blur kernel sampling a target | the only texture bound in the whole splash region is 0x11A50000 1280×768 fmt=6, the sprite page |
⚠️ The trap this census is written to avoid. Censusing the whole 600-frame
log finds six 640×360 textures and three 1280×720 ones, sampled ~330 times
each — which reads exactly like a half-resolution blur chain. They are the
attract movie's chroma and luma planes, triple-buffered, and they first
appear at frame 234, after both splashes are gone. Restricting to frames 4…226 is
what separates them; a census that did not would have "found" a post-process that
is not there.
The tex[base=0x10000000 1×1 fmt=26] on the resolve records is the previous
draw's binding still in the register file. A resolve does not sample it.
2 — what the pass actually is
Three pixel shaders, dumped from the running game with --dump_shaders and
committed at data/shaders/:
| ps hash | draws | blend | what it is |
|---|---|---|---|
0x2E372EA28CC404B7 |
210 (mode 4) | 0x00010001 = ONE / ZERO |
the clear. A prim=8 indices=3 triangle at pixel coordinates (-0.5,-0.5)…(1279.5,719.5) — the whole screen, written unconditionally |
0x5773DC18083C4C20 |
210 (mode 4) | 0x07010701 = ONE / ONE_MINUS_SRC_ALPHA |
the backdrop. A four-vertex NDC quad (-1,-1)…(1,1), vertex colour FF000000 — opaque black, laid down through the blend rather than as a clear |
0xE59B2B3DA4AA9008 |
208 (mode 4) | 0x07010701 |
the splash sprites. One draw per frame, batched: indices= 4, 8, 12 or 24, i.e. one to six quads in a single submission |
; shader_E59B2B3DA4AA9008.ucode.frag — the splash sprite shader, in full
tfetch2D r2, r1.xy, tf0 ; r2 = texture sample (straight alpha)
mul r1.___w, r2.wwww, r0.wwww ; A = tex.a * vcol.a
mul r0.xyz_, r2.xyzz, r0.xyzz ; rgb = tex.rgb * vcol.rgb
mul r1.xyz_, r0.xyzz, r1.wwww ; rgb = rgb * A <-- PREMULTIPLY
max oC0, r1, r1 ; oC0 = (rgb*A, A)
With src=ONE, dst=ONE_MINUS_SRC_ALPHA that composites to
dst' = tex.rgb·vcol.rgb·A + dst·(1 − A), A = tex.a · vcol.a
which is source-over. The backdrop shader is the same premultiply with no
texture (oC0 = (vcol.rgb·vcol.a, vcol.a)).
📌 So the "more pronounced fade/blur" the play-test reports is not a blend-mode difference and not a post-process. Both are now excluded by measurement. What is left is the alpha values themselves and the set of quads submitted — and the capture gives both, below.
3 — where the parameters come from
ps_c[n=0] on 1 048 / 1 048 splash draws. The pixel shaders declare no
float constants at all — this is read off each shader's own float_bitmap, the
same one the backend uploads from, so it is the shader's dependency set and not a
window that might have missed one.
That eliminates the constant banks. It also eliminates immediates in the command
stream: the only thing that differs between two consecutive splash draws is the
vertex buffer, and vb= is a different address every frame (0x14D50550,
0x14D90790, 0x14DB0950, …) — a per-frame ring the guest writes into.
The fade parameter is per-vertex colour, format k_8_8_8_8 at attribute offset
16, in a vertex buffer rebuilt by guest code every frame. Every vertex of a
given quad carries the same colour in every frame observed (the
uniform_colour column of the timeline is True throughout), so it is a
per-element scalar, not a gradient.
4 — the curve, which falls out of 3
data/splash-quad-timeline.txt — every quad of
both splashes, per frame, straight off the vertex stream: NDC rect, alpha, rgb.
Eight distinct quads. Splash A (frames 4…123) is one logo plus one companion; splash B (frames 127…226) is three logos plus three companions, the companions being the same three rects scaled slightly larger.
| quad | NDC rect | frames | n |
|---|---|---|---|
| Q7 | x −0.530…+0.540, y −0.130…+0.120 | 4…14 | 8 |
| Q0 | x −0.520…+0.520, y −0.100…+0.080 | 7…123 | 111 |
| Q4 | x −0.410…+0.410, y +0.320…+0.570 | 127…147 | 21 |
| Q5 | x −0.200…+0.210, y −0.150…+0.150 | 127…147 | 21 |
| Q6 | x −0.320…+0.310, y −0.650…−0.220 | 127…147 | 21 |
| Q1 | x −0.390…+0.390, y +0.350…+0.550 | 135…226 | 87 |
| Q2 | x −0.190…+0.190, y −0.120…+0.120 | 135…226 | 87 |
| Q3 | x −0.300…+0.300, y −0.620…−0.250 | 135…226 | 87 |
The ramp, splash B, on consecutive frames with no gaps in the run:
Q4, Q5 17 51 85 119 153 187 221 255 steps: 34 34 34 34 34 34 34
Q6 17 51 85 119 153 187 221 254 steps: 34 34 34 34 34 34 33
Q1,Q2,Q3 34 68 102 136 170 204 238 255 steps: 34 34 34 34 34 34 17*
(*238+34 = 272, clamped to 255)
Alpha steps by exactly 34 per presented frame, on every one of the six quads, until it clamps. 34 = 255/7.5, so a fade-in is 8 frames, and it is an integer recurrence, not a sampled continuous curve.
📌 This number is not new and is not claimed as a finding. It is exactly the
already-✅ law in ui-keyframe-time-unit.md — the
ramp is linear, the clock advances 2 time units per submitted frame, and a
declared 15-unit fade lands on round(255·k/15) — since 2 units/frame ×
(255/15 per unit) = 34 per frame. What this capture adds is the route: the
34 is not applied by a shader, a constant or a tint register. The guest computes
it on the CPU and writes it into the vertex colour, and that is the only place it
exists on the GPU side. The two families' first samples differ (17 = one time
unit for Q4–Q6, 34 = two for Q1–Q3), so their clocks are offset by one unit.
⚠️ This is stated in frames and quoted as counts and differences, deliberately.
../agents/TEMPORAL-VERIFICATION.md — and
four withdrawn claims — say not to put a wall clock on this. I am not
converting 34/frame into 34/(1/60 s): whether a guest animation tick is a
presented frame is exactly the open Q1, and it is not settled by this capture.
What is settled is the step size and the step count, which have no phase.
The frame counter has gaps where the swap count advanced twice between two draw
batches — (147,149) (152,154) (193,195) (217,219) (221,223) for Q1–Q3, and six
for Q0/Q7. None of them falls inside a rising run, which is why the step is
quotable at all; Q0's and Q7's rises do straddle gaps, so splash A's step is
NOT established by this capture and is not claimed here.
Two things in the timeline that a fitted curve would never produce
- Q6 leaves the plateau while Q4 and Q5 sit on it. Q4/Q5 hold 255 for seven
frames; Q6 runs
254 249 243 237 232 226 220 214over the same frames — its own decay, about −5.6/frame, an order of magnitude gentler than the ±34 ramp. Three structurally identical siblings are not interchangeable. - Q0's fade-out is the same anomaly this corpus already has open. Its drops
are
1 5 12 22 34 33 33 34 33 33— a −34/frame tail with an eased shoulder, which is the shapeui-keyframe-time-unit.mdrecords as1, 11, 6, 22, 34, 33, 17, 33, 33, 17, 25, 8, 8and cannot explain. Reproduced here on an independent capture, so it is a property of the game and not of that run. - The companions lead the logos by eight frames and leave 79 frames early. Q4/Q5/Q6 run 127…147; Q1/Q2/Q3 run 135…226. They overlap for 13 frames and then the companions are gone for the whole of the logos' hold.
What this changes for the port
- Composite the splash sprites with source-over. The equation is confirmed from the shader, not assumed.
- There is no blur to add and none to remove. If a screenshot comparison wants more softness, that softness is in the texture or in which quads are drawn, not in a pass.
- The alpha ramp is integer, +34 per frame, clamped at 255 — not an eased curve, and not the declared 80-unit fade-in (see the refutation below).
- Splash B draws six quads, not three. If an export drops the three companions, the game will look softer than the port at exactly the moment the play-test describes.
Refutation attempt on another agent's claim — recorded per the adversarial duty
Target: REFUTED.md's re-opened pair, "rest() for a plateau-less element
should be the last keyframe", whose sibling argument says a rule which "makes
palogo_anima_eff alone invisible while gamearts_eff and seta_eff stay lit"
is wrong because the three are structurally identical. Both legs of that pair ran
through our renderer, which is why the register re-opened it, and its stated
settling condition is "a draw capture of the developer splash naming which of the
three glows is submitted at rest." This capture is that.
Result: the premise SURVIVES in part and FAILS in part, and the failure is the interesting half.
- ✅ All three companions are submitted, in every frame they exist. Q4, Q5 and Q6 appear together in all 21 frames, 127…147. None is ever suppressed. A rule that renders one of the three invisible does not describe this draw stream.
- ❌ But "structurally identical, therefore identical alpha" is false. Q6
departs the plateau on its own decay while Q4 and Q5 hold 255. The one-byte
difference (
a=212vsa=255att=45) that the sibling argument treated as noise is drawn. So the symmetry premise the argument rests on is refuted by the oracle even though its conclusion about suppression happens to hold.
⚠️ Reach. This is one capture of the boot splashes, so it is ⟨capture⟩ and
generalises to this boot. The pass structure (§1, §2, §3) is a property of the
shaders and the register state and I would expect it to hold for every screen
that uses the same three shaders — but that is a prediction, not a measurement,
and the way to test it is the same census on the title and the menu.
What is NOT settled here
- Splash A's step size — its rising run straddles dropped frames.
- Whether one presented frame is one guest animation tick. Q1 is untouched.
- Which disc field produces 34. The ramp is measured in the vertex stream;
where the guest computes it is not decoded, and the +34 recurrence is not the
declared 80-unit fade-in — see
ui-keyframe-time-unit.md, whose "the declared timeline reproduces the captured splash" entry is 🟡 and is not re-derived by this page. - The plate-late finding (play-test 3). Not touched this iteration.
How to reproduce
# 1. the logger (canary sylpheed-re d90d14e02, already built)
ln -sfn /canary /work/xenia-canary
cmake --build /sylph-home/re/canary-build --config Release --parallel 4 --target xenia_canary
rm /work/xenia-canary
# 2. the capture — GRACE=1 is what makes it catch the splashes at all
GRACE=1 NOTAP=1 ARM=early FRAMES=600 MAXDRAWS=400000 \
tools/re-capture/ui_draw_capture.sh 420 /sylph-home/re/splashdraw
# 3. the shaders
run-canary --dump_shaders=/some/dir # 60 s of boot is enough