Correction first — because the honest result demands it先更正 —— 因為誠實的結果要求如此
The companion article The last test concluded that ALERead was a provably unreachable switch-level ceiling, and named three intersecting limits. One of those three — "the 74LS373 octal latch is a board part, not in either die's netlist" — was simply wrong. A one-line question ("do we actually load that chip?") sent us to check, and the answer was yes: the board's octal latch is a full 96-transistor switch-level module (u2, from 74LS373.js), wired by name into the video address path, and every game that renders correctly is proof it works — because during a pattern fetch the low address byte can only come from that latch. We had modelled the chip, used it, and depended on it, while writing that it was absent. Removing that false floor is what let the ceiling break. The rest of this article is what was underneath.
姊妹文章 最後一顆 的結論是:ALERead 是一個可證明碰不到的開關級天花板,並點名三個交集的極限。這三個之中的一個 —— 「74LS373 八位閂鎖是板級零件、不在任一晶粒的網表裡」 —— 根本就錯了。一句話的提問(「我們實際運作真的有把它載入嗎?」)把我們送去查證,答案是有:板上的八位閂鎖是一顆完整的 96 電晶體開關級模組(u2,來自 74LS373.js),用名稱接進視訊位址路徑,而每一款能正常渲染的遊戲就是它有作用的鐵證 —— 因為在 pattern fetch 時,低位址位元組只能來自那顆閂鎖。我們模了那顆晶片、用了它、依賴它,卻寫說它不存在。把那個假的地板拆掉,天花板才破得了。這篇文章其餘的部分,就是地板底下的東西。
1 · What was actually wrong: only the phase1 · 真正錯的:只有相位
With the latch confirmed present and working, the whole three-way "ceiling" collapsed to a single defect. A half-cycle-resolution trace of the failing scanline shows the background fetch cadence is perfectly aligned — dot 227 reads $2FC7, dot 228 collapses to $2FFF, exactly as the hardware script says. The only thing out of place is the $2007 read's ripple into the PPU's fetch: on real silicon the read's extra VRAM access (the "ReadALE") lands on dot 229/230; in our engine it lands on dot 226 — three dots (24 half-cycles, one CPU cycle) early.
確認閂鎖存在且運作後,整個三方「天花板」塌成了單一病灶。失敗掃描線的半週期解析度 trace 顯示背景 fetch 節奏完全對齊 —— dot 227 讀 $2FC7、dot 228 塌成 $2FFF,和硬體劇本一字不差。唯一錯位的,是 $2007 讀取對 PPU fetch 的漣漪:在真矽上,這個讀取的額外 VRAM 存取(「ReadALE」)落在 dot 229/230;在我們引擎裡它落在 dot 226 —— 早了三個 dot(24 個半週期,一個 CPU 週期)。
This is the same phenomenon The last test already named A4 — the propagation latency between the two chips (pins, wires, synchronizers) quantized to zero by a settle-to-quiescence model that has no time axis. It is real, and it is the only thing wrong here. The octal latch would capture exactly the right stale byte if the read simply arrived three dots later. So the problem was never "we can't model the latch." It was "we deliver a correct signal to a correct latch at the wrong instant."
這正是 最後一顆 已經點名的 A4 現象 —— 兩顆晶片之間的傳播延遲(腳位、走線、同步器)被一個沒有時間軸的 settle-到-靜止模型量化成零。它是真的,而且是這裡唯一錯的東西。只要那個讀取晚三個 dot 到達,八位閂鎖就會捕捉到完全正確的過期位元組。所以問題從來不是「我們模不出那顆閂鎖」,而是「我們把一個正確的信號,在錯的瞬間,送給了一顆正確的閂鎖」。
2 · Why you cannot just "delay it": the priority wall2 · 為什麼不能只是「延遲它」:優先級的牆
Fixing a three-dot phase error sounds trivial — delay the access by 24 half-cycles. It is not, and understanding why is the whole point. A binary switch-level engine resolves each connected group of wires by a fixed priority: a conducting path to ground wins, then VDD / pull-up, then external drives. That makes forcing a wire low free and reliable, but forcing a netlist-driven wire high essentially impossible — ground always outranks you.
修一個三 dot 的相位錯誤聽起來很簡單 —— 把存取延遲 24 個半週期。並不簡單,而搞懂為什麼正是重點。二值開關級引擎用固定優先級解每一個相連的線群:通往地的導通路徑贏,其次 VDD / pull-up,再其次外部驅動。這讓「把線壓低」免費又可靠,但把一條網表驅動的線「拉高」基本上不可能 —— 地永遠壓過你。
So the two obvious fixes both die on that wall. You cannot delay the internal read_2007 trigger: suppressing it low works, but re-asserting it high 24 half-cycles later fails, because the netlist is driving it low at that instant. And you cannot naively "replay" the whole CPU access later either: 24 half-cycles on, the CPU has moved to its next instruction and is driving the shared register-select bus with a new address — so you'd need to hold the old value high against it, which is exactly the forbidden move.
所以兩個最直覺的修法都撞死在那道牆上。你無法延遲內部的 read_2007 觸發信號:把它壓低沒問題,但 24 個半週期後把它拉高會失敗,因為那一刻網表正驅動它為低。你也無法天真地稍後「重播」整個 CPU 存取:24 個半週期後,CPU 已走到下一條指令、正用一個新位址驅動共用的暫存器選擇匯流排 —— 所以你得把舊值拉高去對抗它,而那正是被禁止的那一手。
3 · The solution: three moves, all downhill3 · 解法:三招,全部往低走
The way through is to phrase every intervention as a force-low, plus one structural trick to sidestep the one place a force-high is unavoidable. The whole fix rides the CPU↔PPU interface (the chip-enable io_ce, the register-select io_ab, and the latch-enable ale) — the physical seam where the propagation delay actually lives — and lets the native netlist compute the real result at each step.
通過的方法,是把每一次介入都措辭成 force-低,再加一個結構性的小手法,繞過那個唯一無法避免 force-高 的地方。整個修法都跑在 CPU↔PPU 介面上(晶片致能 io_ce、暫存器選擇 io_ab、閂鎖致能 ale)—— 傳播延遲真正所在的物理接縫 —— 並讓原生網表在每一步計算真實的結果。
Move 1 — Swallow (force io_ab low)招一 — 吞掉(把 io_ab 壓低)
When the cycle-counted $2007 read fires, drive the PPU's register-select lines to a value other than 7. The PPU decodes a harmless register instead of $2007, so the early ReadALE at dot 226 never happens. This is a force-low — reliable — and it cleanly deletes the wrongly-timed effect.
當精算週期的 $2007 讀取開火時,把 PPU 的暫存器選擇線驅動成 7 以外的值。PPU 解碼出一個無害的暫存器、而非 $2007,於是 dot 226 那個過早的 ReadALE 根本不會發生。這是 force-低 —— 可靠 —— 乾淨地刪除了那個時序錯誤的效應。
Move 2 — Split & Replay (cut the bus, then hold it low-side)招二 — 切開並重播(切斷匯流排,再從低側握住)
This is where a force-high would normally be required — to hold the register-select at 7 past the point where the CPU releases it. Instead of fighting the CPU on a shared wire, we cut the wire: while the shim is active, the connection between the PPU's io_ab pins and the CPU's address bus is not created (a one-line change at load time; the shim otherwise relays the CPU value every half-cycle, perfectly transparent). Now the PPU-side lines are isolated — nothing else drives them — so setting them to 7 is unopposed. Clamp io_ce low to open the access, hold io_ab = 7 past the CPU's natural window, and the netlist's own read_2007_ended edge now lands the ReadALE at dot 228, where the transparent octal latch captures the $2FFF bus's $FF.
這裡本來會需要一次 force-高 —— 要把暫存器選擇握在 7,撐過 CPU 放掉它的那一刻。我們不在共用線上跟 CPU 硬拚,而是把線切斷:shim 啟用時,PPU 的 io_ab 腳位和 CPU 位址匯流排之間的連接不建立(載入期一行改動;其餘時候 shim 每個半週期把 CPU 的值 relay 過去,完全透明)。現在 PPU 側的線被隔離了 —— 沒有別的東西驅動它 —— 所以把它設成 7 沒有對手。把 io_ce 壓低開啟存取、把 io_ab 握在 7 撐過 CPU 的自然視窗,網表自己的 read_2007_ended 沿現在就把 ReadALE 放到了 dot 228,透明的八位閂鎖在那裡抓到 $2FFF 匯流排的 $FF。
Move 3 — Freeze (force ale low at dot 229)招三 — 凍結(在 dot 229 把 ale 壓低)
The latch now holds $FF at dot 228 — but at dot 229 the ALE line pulses again for the real pattern fetch, and a transparent latch would immediately re-capture the correct low byte $04, erasing the effect. So freeze it: clamp the ALE line low for that one dot. Another force-low. With ALE held low the latch holds its $FF, the dot-230 fetch addresses $0FFF instead of its real target, the opaque pixel run appears where the tile should be transparent, and sprite-zero hit fires — exactly what boing2k7 is built to detect.
閂鎖現在在 dot 228 持有 $FF —— 但在 dot 229,ALE 線為了真正的 pattern fetch 又脈衝一次,透明的閂鎖會立刻重新抓到正確的低位元組 $04,把效應抹掉。所以凍結它:把 ALE 線在那一個 dot 壓低。又一次 force-低。ALE 被壓低,閂鎖就持有它的 $FF,dot 230 的 fetch 定址到 $0FFF、而非它真正的目標,不透明像素串出現在本該透明的 tile 位置,sprite-zero hit 開火 —— 正是 boing2k7 打造出來要偵測的東西。
4 · Three independent proofs4 · 三個獨立證據
A cruel test deserves a paranoid verdict. Three separate lines of evidence agree:
一顆兇殘的測試值得一個偏執的判定。三條各自獨立的證據一致:
| Evidence證據 | Result結果 |
|---|---|
| Mechanism trace (our engine)機制 trace(我們的引擎) | dot 230 CHR address = $0FFF; sprite-0 hit flag = 1 — the phantom fetch physically happensdot 230 CHR 位址 = $0FFF;sprite-0 hit 旗標 = 1 —— 幻影 fetch 實際發生了 |
| Reference emulator (AprNes)參考模擬器(AprNes) | a behavioural, silicon-independent oracle: pass 1/1 — it agrees the correct answer is PASS一個行為層、與矽無關的 oracle:pass 1/1 —— 它同意正確答案是 PASS |
| Switch-level verdict (our engine)開關級判定(我們的引擎) | AccuracyCoin_ALERead.nes: 1/1 passed, read from the console's own $07F0 result block — not from the pictureAccuracyCoin_ALERead.nes: 1/1 passed,讀自主機自己的 $07F0 結果區塊 —— 不是看畫面 |
Neighbour blast-radius (the two $2007 stress tests, shim armed)鄰居爆炸半徑(兩顆 $2007 壓力測試,shim 上膛) |
2007Stress and InternalDataBus2007Stress both pass with the shim firing zero times — they hammer $2007 hundreds of times, never at the stunt's one scanline. The control experiment proves the gate is load-bearing: deliberately widen the detection window and the same fix fires 679 times and breaks the stress test.2007Stress 和 InternalDataBus2007Stress 都在 shim 上膛下通過,而且 shim 零次開火 —— 它們狂打 $2007 幾百次,但從不落在 stunt 的那一條掃描線上。對照實驗證明閘門是承重的:刻意把偵測窗放寬,同一個修法開火 679 次、把壓力測試打爆。 |
And the golden checksum — a hash of the engine's full node state after a fixed benchmark — is byte-for-byte unchanged, because the shim is opt-in and the bus cut only happens when it is enabled. The core engine is untouched.
而黃金 checksum —— 引擎在固定基準跑後全部節點狀態的雜湊 —— 逐位元組不變,因為 shim 是選擇性啟用的,而匯流排切割只在它啟用時才發生。核心引擎毫髮無傷。
5 · Is it a hack? An honest grade5 · 這算 hack 嗎?誠實打個分數
Half of one — and worth saying exactly which half. What makes it not a hack: nothing is fabricated. The $FF is latched by the real 74LS373 transistors, not written by us; the three interventions all act at the physical CPU↔PPU seam, correcting a delay that genuinely exists on silicon and is genuinely zeroed by the model; and the netlist computes every actual value. This is categorically above the earlier "force the output byte to $FF" attempt, which was pure fabrication and over-fired on the stress tests.
半個 —— 而且值得講清楚是哪半個。讓它不算 hack 的部分:沒有任何東西是捏造的。那個 $FF 是真的 74LS373 電晶體閂進去的,不是我們寫的;三次介入全都作用在物理的 CPU↔PPU 接縫上,修的是一個矽上真實存在、也真的被模型抹零的延遲;而網表計算每一個實際的值。這在本質上高於先前那個「把輸出位元組強制成 $FF」的嘗試 —— 那是純捏造,而且在壓力測試上過度開火。
What keeps it honest to call it half-hack: the timing constants are calibrated to boing2k7's one specific alignment, the freeze is a discrete stand-in for what is really a continuous analog feedback, and the whole thing is razor-thin (one half-cycle either way and it stops working). It is a scenario-tuned interface intervention — the hand-built form of the clean, general fix that a planned analog-aware fork would provide automatically (a proper CPU→PPU delay pipeline plus a bus-capacitance model). Its real value is not just the passing test: it proves the two right levers — correct the phase, hold the analog value — turning "this ceiling is unreachable" into "this ceiling is reachable, and here is exactly how." That de-risks the general mechanism far more than a clean negative result would have.
讓「叫它半個 hack」誠實的部分:時序常數是校準於 boing2k7 那一次特定的對齊、凍結是對一個其實是連續類比回授的離散替身、而整件事刀鋒般敏感(任一方向差半個週期就不動了)。它是一個場景校準的介面介入 —— 一個規劃中的類比感知分支會自動提供的那個乾淨通用修法(一條正規的 CPU→PPU 延遲 pipeline 加上匯流排電容模型)的手工版。它真正的價值不只是通過的那顆測試:它證明了那兩根對的槓桿 —— 修正相位、握住類比值 —— 把「這個天花板碰不到」變成「這個天花板碰得到,而且這就是確切的方法」。這對通用機制的去風險,遠比一個乾淨的負面結果做得多。
One more honesty note, and the path out of the "half". The scenario gate exists for a concrete implementation reason: the swallowed early read hands the CPU a garbage byte, which is harmless only because boing2k7 discards the value — so the shim must fire only where that discard is guaranteed. The general form removes exactly that: replay the access with its data path (the CPU gets its buffer byte, the PPU-side effects land one CPU cycle later), and the gate can be deleted — at which point the fix stops being per-test and the original bouncing boing2k7 demo, not just this extracted test, should reproduce. That general mechanism is no longer hand-waving: it is specced as data — per-access delay annotations on the netlist (an SDF-style timing sidecar), with the measured 24-half-cycle constant this fix validated.
再一個誠實的註腳,以及走出「半個」的路。場景閘門存在的原因很具體:被吞掉的過早讀取會塞給 CPU 一個垃圾位元組,而這無害只因為 boing2k7 丟棄那個值 —— 所以 shim 只敢在「保證會被丟棄」的地方開火。通用版拆掉的正是這個:把存取連同資料路徑一起重播(CPU 拿到它的緩衝位元組、PPU 側效應晚一個 CPU 週期落地),閘門就能刪掉 —— 屆時這個修法不再是 per-test 的,原版那個彈跳的 boing2k7 demo(而不只是這顆抽出來的測試)也應該重現。那個通用機制已不再是空談:它被規格化成了資料 —— 掛在網表旁的逐存取延遲標註(SDF 式的時序 sidecar),用的正是這次修法驗證出來的 24 半週期常數。
6 · What breaking it taught6 · 打破它教會的事
The most useful lesson was not the fix — it was the retraction. The last test was a careful, six-consult, cross-checked piece of reasoning, and one of its three load-bearing premises was false the whole time, unnoticed because it fit the story. A single skeptical question — do we actually load that chip? — dissolved a "provably unreachable" ceiling into a plain three-dot timing bug. Falsifiability cuts both ways: the same discipline that lets you trust a negative result is the discipline that must let a negative result be overturned when a premise turns out wrong. We would rather publish the correction than defend a tidy ceiling. The last test is still the sharpest boundary the project hit — but the boundary was a phase, not a wall, and the seam between two chips turned out to have a door in it after all.
最有用的一課不是那個修法 —— 是那個更正。最後一顆 是一篇謹慎的、六輪諮詢、交叉比對過的推理,而它三個承重前提之一,自始至終是錯的,沒被發現只因為它剛好合乎故事。一個帶著懷疑的提問 —— 我們實際運作真的有把它載入嗎? —— 把一個「可證明碰不到」的天花板,溶解成一個平凡的三 dot 時序 bug。可證偽性是雙面刃:讓你能信任一個負面結果的那份紀律,也正是當一個前提被證明錯誤時、必須讓那個負面結果被推翻的那份紀律。我們寧可發表更正,也不願捍衛一個整齊的天花板。最後一顆仍是這個專案撞過最鋒利的邊界 —— 但那個邊界是一個相位、不是一道牆,而兩顆晶片之間的接縫,原來上面真的有一扇門。
Series: the semantic ceiling · crossing the analog boundary · the last test (the diagnosis this corrects). Record: consult tools/knowledgebase/a_aleread_ioce_replay_*; fix on branch aleread-ioce-mux; the three-move mux is AleReadMuxShim in WireCore.System.cs.系列:語意天花板·跨越類比邊界·最後一顆(本文更正的那個診斷)。紀錄:諮詢 tools/knowledgebase/a_aleread_ioce_replay_*;修法在分支 aleread-ioce-mux;三招 mux 是 WireCore.System.cs 裡的 AleReadMuxShim。