先搞懂:Recompiled 和模擬器差在哪First: how a recompilation differs from an emulator
模擬器像同步口譯:遊戲每執行一條機器指令,模擬器當下才去看懂它、照做一次。看一條、做一條,一秒要做幾千萬次。
Recompiled 像事先把整本書翻好:在你玩之前,就把遊戲的機器碼一段一段翻成 C++ 程式碼,再交給電腦自己的編譯器編成原生程式。玩的時候不用再「看懂」,直接執行翻好的版本。
但遊戲不只是程式,還有畫面晶片、音效晶片、計時器、中斷。這些硬體行為沒辦法「翻譯」,必須另外寫一層程式把它們重現出來。所以一個 Recompiled 移植其實是兩半:翻好的遊戲程式,加上一層硬體相容層。
An emulator is like a live interpreter: each time the game runs a machine instruction, the emulator reads it and carries it out, one at a time, tens of millions of times a second.
A recompilation is like translating the whole book in advance: before you play, the game's machine code is translated piece by piece into C++, which your computer's own compiler turns into a native program. While you play, nothing has to be read and understood again.
But a game is more than code. It talks to a video chip, sound chips, timers and interrupts. That hardware behaviour can't be translated; it has to be reproduced by a separate layer. So a recompiled port is two halves: the translated game code, and a hardware compatibility layer.
README.md專案說明:它是什麼、怎麼建置What it is and how to build itARCHITECTURE.md架構文件(繁中):每個元件採用什麼策略、為什麼Architecture (in Traditional Chinese): the strategy for each part and why
5835d4a2026-09-19 Knuckles' Chaotix static recompilation: toolchain, runtime and SDL3 frontend
劃清法律界線,再寫第一行程式Draw the legal line before the first line of code
Recompiled 專案能公開,前提是不散布任何遊戲本身的東西。這一步要在一開始就定好,因為它會決定之後所有工具怎麼設計。
- 玩家自己提供合法取得的 ROM。程式會先檢查 SHA-1(這裡是
0c2fff7b…7ced7),不符就不使用翻好的程式碼。 - ROM、從 ROM 翻出來的程式碼(
generated/)、截圖,全部列在.gitignore,永遠不進 git,也不上傳任何雲端。 - CI(GitHub 上的自動測試)拿不到遊戲檔,所以 CI 只測不需要遊戲的部分,例如指令語意、各個元件;需要遊戲的測試在開發者自己的電腦上跑。
- 不需要 32X 的 BIOS:開機過程用程式重現(HLE),使用者不必另找 BIOS 檔。
A recompilation can be public only if it ships nothing from the game itself. Decide this on day one, because it shapes how every tool is designed.
- Players supply their own legally obtained ROM. The program checks its SHA-1 (here
0c2fff7b…7ced7) and won't use the translated code with anything else. - The ROM, the code generated from it (
generated/) and screenshots are all in.gitignore. They never go into git or any cloud service. - CI (the automatic tests on GitHub) never has the game, so it tests what doesn't need it: instruction semantics, individual components. Tests that need the game run on the developer's own machine.
- No 32X BIOS is needed: the boot sequence is reproduced in code (HLE), so players don't have to find a BIOS file.
LEGAL.md法律界線The legal boundaries.gitignoreROM 與 generated/ 永遠不進 gitThe ROM and generated/ never go into gitsrc/runtime/rom.cpp載入 ROM 時計算 SHA-1SHA-1 computed when the ROM loads.github/workflows/ci.ymlCI 的 No game data 檢查(含 512 KB 上限)CI's No game data job (with the 512 KB limit)src/runtime/system.cpp不需要 BIOS:開機流程的 HLENo BIOS needed: the boot HLE
把機器摸透:這台主機裡有什麼Know the machine: what's inside a 32X
翻譯之前要先知道原文的「文法」。32X 是接在 Mega Drive 上的擴充裝置,所以一台 32X 遊戲機裡同時有四顆處理器在跑,彼此透過共用的暫存器溝通:
Before translating, learn the grammar of the original. The 32X plugs into a Mega Drive, so a 32X game has four processors running at once, talking through shared registers:
這一步的產出是一份架構文件:每顆處理器的時脈、記憶體地圖(哪個位址是 ROM、RAM、哪個是硬體暫存器)、這款遊戲實際用到哪些硬體。這個專案的 ARCHITECTURE.md 就是這樣開始的,而且每個說法都標了「已驗證」或「待驗證」。
The output of this step is an architecture document: each processor's clock, the memory map (which addresses are ROM, RAM, hardware registers), and which hardware this particular game actually uses. This project's ARCHITECTURE.md started that way, and every claim in it is marked verified or still to verify.
src/runtime/md_bus.cpp68000 這一側的位址空間The 68000 side's address spacesrc/runtime/sh2_bus.cppSH-2 位址空間與 32X 暫存器The SH-2 address space and 32X registerssrc/runtime/sh2_onchip.cppSH-2 內建的計時器、除法器、DMA、中斷The SH-2's on-chip timers, divider, DMA, interruptstools/rom_analyzer/找出遊戲實際用到哪些硬體(MMIO 報表)Finds which hardware the game really touches (MMIO report)
5835d4a2026-09-19 Knuckles' Chaotix static recompilation: toolchain, runtime and SDL3 frontend
先寫「標準答案」:參考直譯器Write the answer key first: a reference interpreter
聽起來繞遠路,但這是整個專案最重要的決定:在寫翻譯器之前,先寫一個一條一條執行指令的直譯器(也就是一個小模擬器)。68000、SH-2、Z80 各一個。
它慢沒關係,它的工作是當標準答案。之後翻好的程式碼跑出來的結果,每一格都要跟它一模一樣。
關鍵技巧:每條指令「做什麼」只寫一次,寫在共用的語意檔(m68k_ops.h、sh2_ops.h)裡。直譯器用它,翻譯出來的 C++ 也呼叫同一份。這樣兩邊不會各自寫錯成不同的樣子。
It sounds like a detour, but it's the most important decision in the project: before writing the translator, write an interpreter that runs instructions one at a time (a small emulator). One each for the 68000, the SH-2 and the Z80.
It can be slow. Its job is to be the answer key: later, the translated code has to produce exactly the same result, every frame.
The key trick: what each instruction does is written once, in shared semantics headers (m68k_ops.h, sh2_ops.h). The interpreter uses them, and the generated C++ calls the very same code, so the two can't drift apart.
src/cpu/m68k/m68k_ops.h68000 指令語意(直譯器和翻好的程式共用)68000 instruction semantics, shared by interpreter and generated codesrc/cpu/m68k/m68k_interp.cpp68000 參考直譯器68000 reference interpretersrc/cpu/sh2/sh2_ops.hSH-2 指令語意SH-2 instruction semanticssrc/cpu/sh2/sh2_interp.cppSH-2 參考直譯器SH-2 reference interpretersrc/cpu/z80/z80.cppZ80(音效處理器)直譯器Z80 (sound CPU) interpretertests/test_m68k.cpp用手冊答案寫的指令測試(另有 tests/test_sh2.cpp)Instruction tests from manual answers (also tests/test_sh2.cpp)
5835d4a2026-09-19 Knuckles' Chaotix static recompilation: toolchain, runtime and SDL3 frontend
找出 ROM 裡哪些是程式:ROM 分析器Find which bytes are code: the ROM analyzer
ROM 是一大串數字,裡面混著程式、圖、聲音、文字。翻譯器只能翻程式,所以要先畫出地圖:
- 從已知的入口(開機位址、中斷向量)開始,沿著每個跳躍、呼叫往下走,標出所有走得到的程式(控制流程圖、函式表)。
- 遇到「跳到表格裡某個位址」的間接跳躍時,要解析那張表。
- 有些入口靜態看不出來,就實際玩一遍,記錄執行過的位址(coverage 檔,只存位址,不含 ROM 內容),再回饋給分析器。
A ROM is a long run of numbers mixing code, graphics, sound and text. The translator can only translate code, so first you draw a map:
- Start from the known entry points (the reset address, interrupt vectors), follow every jump and call, and mark all reachable code (a control-flow graph, a function list).
- When a jump goes through a table of addresses, decode the table.
- Some entry points can't be found statically, so play the game and record which addresses run (a coverage file holding only addresses, no ROM content), then feed that back to the analyzer.
tools/rom_analyzer/analysis.cpp控制流程、函式、跳躍表的分析Control flow, functions, jump tablestools/coverage/record_sessions.py錄下實際執行過的位址Records which addresses actually rancoverage/coverage 檔(只有位址,沒有 ROM 內容)The coverage files (addresses only, no ROM content)
真正的翻譯:重編譯器The translation itself: the recompiler
重編譯器把地圖上的每個程式區塊,翻成一段 C++。翻出來的程式碼每行都附上原本的組合語言當註解,可以直接下中斷點除錯。
為什麼翻成 C++ 而不是更底層的 LLVM IR?因為 C++ 在每個平台都有現成的編譯器(MSVC、GCC、Clang、Android NDK、Xcode),翻一次就能到處編。
The recompiler turns each block on the map into a piece of C++. Every generated line carries the original assembly as a comment, and you can set breakpoints in it.
Why C++ and not lower-level LLVM IR? Every platform already has a C++ compiler (MSVC, GCC, Clang, the Android NDK, Xcode), so one translation builds everywhere.
三個一定會遇到的難題Three problems you will hit
| 難題Problem | 這款遊戲裡的實例In this game | 怎麼處理What was done |
|---|---|---|
| 遊戲在執行中產生的程式Code made at run time | 68000 在 RAM 裡寫一個跳板,每關改跳的目標The 68000 writes a jump trampoline into RAM and retargets it each level | 交給直譯器執行那一條,再接回翻好的程式The interpreter runs that one jump, then hands back to native code |
| 程式被改寫Code that gets overwritten | SH-2 的程式放在 RAM,關卡中每格有數百次寫入落在旁邊SH-2 code lives in RAM, and hundreds of writes a frame land right next to it | 每個函式記錄自己的程式是否被真的改過,改了才重新檢查或改用直譯器Each function tracks whether its own bytes really changed, and is re-checked (or interpreted) only then |
| 中斷可能在任何時刻發生Interrupts can come at any moment | V-blank、H-blank、PWM、指令中斷V-blank, H-blank, PWM, command interrupts | 直譯器和翻好的程式用同一條規則:只在區塊邊界接受中斷,所以兩邊能逐格比對Interpreter and native code share one rule: interrupts are taken only at block boundaries, so the two can be compared frame by frame |
tools/recompiler/emit_m68k.cpp把 68000 程式區塊翻成 C++Turns 68000 blocks into C++tools/recompiler/emit_sh2.cpp把 SH-2 程式區塊翻成 C++(含 delay slot)Turns SH-2 blocks into C++ (delay slots included)src/game/recomp_dispatch.cppRAM 跳板交給直譯器,再接回原生碼RAM trampolines go to the interpreter, then back to native codesrc/runtime/sh2_bus.cpp程式真的被改寫時,才通知對應的函式Tells a function only when its own code really changedcmake/ChaotixGenerated.cmake建置時在本機產生並編譯 C++Generates and compiles the C++ locally at build time
補上硬體:相容層與時間Fill in the hardware: the compatibility layer and timing
翻好的程式會去讀寫硬體暫存器,這些要由相容層接住:兩組畫面晶片(逐條掃描線合成)、DMA、計時器、除法器、COMM 暫存器、FM/PSG/PWM 音源,以及開機流程。
最需要想清楚的是時間。四顆處理器在真機上是同時跑的,程式裡只能輪流跑。這個專案把每條掃描線切成 4 段固定時間片,每段依序跑 68000 → Z80 → 主 SH-2 → 副 SH-2。整個模型是確定性的:同樣的輸入,永遠得到同樣的結果,不受電腦快慢影響。
The translated code reads and writes hardware registers, and the compatibility layer answers: both video chips (composited line by line), DMA, timers, the divider, the COMM registers, the FM/PSG/PWM sound, and the boot sequence.
The part that needs the most thought is time. On the real console the four processors run at once; in a program they take turns. This project cuts each scanline into 4 fixed slices and runs 68000 → Z80 → master SH-2 → slave SH-2 in each. The whole model is deterministic: the same input always gives the same result, however fast the computer is.
src/runtime/system.h每條掃描線 4 段時間片4 time slices per scanlinesrc/runtime/system.cpp每段時間片依序跑四顆處理器Each slice runs the four processors in turnsrc/runtime/system.cppissue #50 的修正:Z80 補跑到 68000 的時間The issue #50 fix: run the Z80 up to the 68000's timesrc/runtime/vdp.cppMega Drive 畫面晶片The Mega Drive video chipsrc/renderer/compositor.cpp32X 畫面與合成32X video and compositingsrc/audio/YM2612 FM、SN76489 PSGYM2612 FM, SN76489 PSGdocs/ISSUE_50_SOUND_TEST.mdSOUND TEST 沒聲音的完整調查The full write-up of the silent SOUND TEST
對答案:逐格 lockstep 驗證Check against the key: frame-by-frame lockstep
這一步回答「你怎麼知道翻譯沒翻錯?」。做法是讓兩台機器同時跑同一段遊戲:一台用翻好的原生程式,一台用第 3 步的直譯器。每一格結束時比較兩邊的所有東西:每顆 CPU 的暫存器、cycle 數、全部 RAM、顯示記憶體、32X 畫面緩衝、輸出的畫面。只要有一個 byte 不同,測試就失敗,並指出是哪一格、哪裡不同。
This step answers "how do you know the translation is right?". Run two machines on the same play session: one on the native translated code, one on the step-03 interpreter. At the end of every frame, compare everything: each CPU's registers and cycle counts, all of RAM, video memory, the 32X frame buffer, the output picture. If a single byte differs, the test fails and says which frame and where.
- 開機到關卡:3,200 格(開機 → 標題 → 選單 → 進關卡 → 跑、跳、吃環),每格完全一致。
- 33 組隨機輸入(sweep):不同關卡、不同角色、寬螢幕開關,亂按按鈕,每一組都要 bit-identical。每次發版都重跑。
- 換一台電腦也一樣:在 Apple M1(ARM)上跑同樣的比對,也完全一致。
- Boot to level: 3,200 frames (boot → title → menus → into a level → run, jump, collect rings), identical every frame.
- 33 random-input runs (the sweep): different levels, characters, widescreen on and off, random buttons; every one must be bit-identical. It is re-run for every release.
- On another CPU too: the same comparison on an Apple M1 (ARM) is identical as well.
src/frontend/headless_main.cpp--lockstep:兩台機器並排、每格比對--lockstep: two machines side by side, compared every frametests/CMakeLists.txtlockstep_boot_to_level 與 golden_frames 測試The lockstep_boot_to_level and golden_frames teststests/golden/固定輸入下每 50 格的畫面雜湊Picture hashes every 50 frames for fixed inputs
做成能玩的程式:前端Make it playable: the frontend
前端用 SDL3 處理視窗、全螢幕、畫面縮放、鍵盤/手把/觸控、聲音輸出和設定檔。還有一個沒有畫面的版本(chaotix_headless),專門給測試和量測用:可以照腳本按鍵、存截圖、輸出 WAV、印出每個 CPU 花了多少時間。
一個好習慣:聲音和畫面依模擬時間產生,不讓電腦的音效裝置反過來決定遊戲速度。
The frontend uses SDL3 for the window, fullscreen, scaling, keyboard/gamepad/touch, sound output and settings. There's also a windowless build, chaotix_headless, made for tests and measurement: it presses buttons from a script, saves screenshots, writes WAV files and reports how long each CPU took.
A good habit: produce sound and pictures from emulated time, and never let the computer's audio device decide how fast the game runs.
src/platform/sdl_main.cppSDL3 前端:視窗、輸入、聲音、設定The SDL3 frontend: window, input, sound, settingssrc/frontend/headless_main.cpp無畫面版:腳本、截圖、WAV、效能統計The headless build: scripts, screenshots, WAV, profilingdocs/FRAME_PACING.md畫面節奏怎麼對齊 59.92 HzHow frames are paced to 59.92 Hz
b48de502026-09-27 Fix frame pacing and release 0.3.3
讓它比原版更好,但不破壞原版Improve on the original without breaking it
移植的價值常在「原版做不到的事」。這個專案加了真正的寬螢幕、時間挑戰、成就、作弊碼、存檔/讀檔、倒帶、網路連線、角色與音樂 mod 等等。原則是:不准偷偷改壞原本的遊戲。
以寬螢幕為例:不是把 4:3 畫面拉寬,而是讓遊戲自己的關卡繪圖程式多畫兩側的地形、物件和角色。並且有一項測試保證中間那 320 像素跟原版一模一樣。
A port earns its keep with things the original can't do. This one added real widescreen, time attack, achievements, cheats, save states, rewind, netplay, character and music mods and more. The rule: never quietly break the original game.
Take widescreen: the 4:3 picture isn't stretched; the game's own level renderer is extended to draw real tiles, objects and sprites at the sides. A test guarantees the centre 320 pixels stay identical to the original.
另一個例子:時間挑戰模式沒有自己發明選關畫面,而是找到遊戲裡原本就有、但正式版沒開放的 STAGE SELECT,照它的程式碼呼叫「開始關卡」,所以進關卡的方式跟遊戲本身完全相同。
Another example: time attack doesn't invent its own level picker. It uses a STAGE SELECT that is in the game but never reachable in the shipped version, calling its own "start level" routine, so a level starts exactly as the game itself would start it.
src/runtime/patches.cpp寬螢幕等對遊戲的修改,集中在這裡Widescreen and other game-side changes, all in one placetests/CMakeLists.txtwidescreen_centre_matches_4_3:中間 320 像素必須相同widescreen_centre_matches_4_3: the centre 320 px must matchdocs/ISSUE_7_WIDESCREEN.md真寬螢幕的做法How real widescreen workssrc/runtime/stage_select.cpp呼叫遊戲自己隱藏的 STAGE SELECT 進關卡Starting a level through the game's own hidden STAGE SELECTdocs/存檔、倒帶、連線、mod 等功能各自的說明Docs for save states, rewind, netplay, mods and more
搬到每個平台Bring it to every platform
因為產出是 C++,每個平台用自己的編譯器。但每個平台都有自己的坑,要實際在那個平台上跑過才算數:
Because the output is C++, each platform uses its own compiler. Each still has its own traps, and a platform only counts once it has really run there:
| 平台Platform | 在哪裡驗證Verified on | 遇過的真實狀況A real thing that happened |
|---|---|---|
| Windows | 桌機,只用系統 PATH 跑啟動測試Desktop; startup test with only the system PATH | 非 ASCII 路徑(中文資料夾名)會讓 ROM 搜尋當掉,由外部貢獻者修好Non-ASCII folder names crashed the ROM search; an outside contributor fixed it |
| Linux | WSL Ubuntu 22.04,clang-15WSL Ubuntu 22.04, clang-15 | 用 gcc 11.4 建出來的版本一進 SOUND TEST 就卡住,改用 clang-15 並記錄原因The gcc 11.4 build hung on entering SOUND TEST; releases use clang-15, with the reason written down |
| macOS / iOS | Mac mini M1:全部測試+lockstepMac mini M1: all tests + lockstep | ARM 上逐格比對也要完全一致Frame-by-frame comparison must match on ARM too |
| Android 64 / 32-bit | 實體掌機 AYN Thor(Android 13)A real AYN Thor handheld (Android 13) | 每版直接覆蓋安裝舊版、開 FPS 顯示跑關卡、放著選單 60 秒確認不會疊在示範畫面上Every release installs over the previous one, runs a level with the FPS counter, and leaves the menu alone 60 s to make sure it doesn't float over the demo |
CMakePresets.json各平台的建置設定Build presets for each platformplatforms/android/Android 專案(64-bit 與 32-bit)The Android project (64- and 32-bit)scripts/package_apple.shmacOS 與 iOS 打包macOS and iOS packagingtests/test_rom_startup.py打包後的啟動測試The startup test for packaged builds
ba32f752026-09-24 Android: touch-driven setup, landscape, and a build that runs897da812026-10-10 Android: document the 32-bit ARM build (issue #19), drop an unused constant1540ae92026-10-23 macOS and iOS: build the app bundlesa1f1d262026-10-02 Windows: non-ASCII names no longer crash the ROM search or break the install
跑得更快:先量,再改,再證明Make it faster: measure, change, prove
有玩家說在慢手機上跑不順(issue #62)。目標定為:在同一台手機上比 PicoDrive 模擬器快,而且不能犧牲準確度,不准用跳格之類的偷吃步。
做法每次都一樣:在實體手機上用 headless 版量 3,000 格,找出最花時間的部分,改它,再用 lockstep 證明行為沒變,最後再量一次。
A player said it stuttered on slow phones (issue #62). The goal: be faster than the PicoDrive emulator on the same phone, without giving up accuracy; no frame-skipping shortcuts.
The method was the same every time: measure 3,000 frames with the headless build on the real phone, find the biggest cost, change it, prove with lockstep that behaviour didn't change, then measure again.
其中一次加速的故事:關卡中,遊戲每格大約 370 次把資料寫在 SH-2 程式碼的旁邊。原本每次寫入都讓「所有」翻好的函式重新檢查自己有沒有被改,3,000 格裡檢查了 4,049,824 次。改成每個函式只在自己的 byte 真的被改變時才重新檢查之後,同樣 3,000 格只剩 75 次。
One speed-up in detail: in a level the game writes about 370 times a frame right next to some SH-2 code. Each write used to make every translated function re-check itself: 4,049,824 checks in 3,000 frames. After each function was made to re-check only when its own bytes actually changed, the same 3,000 frames needed 75.
docs/PERFORMANCE_ANDROID.md每次加速的量測數字The measurements behind each speed-updocs/IDLE_SKIP.md等待迴圈精確快轉,以及為什麼結果不變Exact fast-forward of wait loops, and why results don't changesrc/runtime/sh2_bus.cpp每個函式各自記錄程式寫入Per-function code-write trackingissue #62整個加速過程與實機數字The whole speed-up story with phone numbers
24150732026-10-28 Faster drawing: no division call per line; tests for the fast paths and caches (issue #62)dce18472026-10-28 WIP: fast-forward time-dependent SH-2 polls and 68000 wait loops (issue #62)0bb42302026-10-28 Find SH-2 writes over recompiled code by bitmap; unit tests (issue #62)9884ed62026-10-30 SH-2: count writes over code per function; cache array inline (#62)
接玩家回報:先重現,再修,再用同一招驗證Handle reports: reproduce, fix, verify the same way
這個流程是專案擁有者訂的規則,每一個 issue 都照走。下面是一個完整的真實例子,就發生在這份文件寫成的同一天。
This loop is a rule set by the project owner, and every issue goes through it. Here is a complete real case from the same day this page was written.
案例:「音樂好像少了一些 32X 聲道」Case: "the music missed some 32X audio channels"
- 先看證據:留言掛在一段實機錄影下。把影片的音訊抽出來,跟模擬輸出對齊比較。結果 PWM(32X 的取樣音源)四個音軌都有在播,沒有少。
- 找標準:自己的程式不能當自己的標準。下載 PicoDrive 的 PC 版核心,寫一個小程式讓它不開視窗,跑同一個 ROM、按同樣的鍵、進同一關,把聲音和畫面存下來。
- 逐格比對:Mighty 的跑、跳、滾球、spin dash,兩邊逐格相同,所以「Mighty 動畫」不需要改。聲音方面,PicoDrive 的大鼓明顯比較響。
- 讀原始碼確認:讀 PicoDrive 的音源程式碼(只讀,不複製),確定每種音源的最大音量:FM ±8191、PSG ±2304、PWM ±32768,最後整體乘 0.875。跟實測的差距吻合。
- 修:照這些比例改混音;另外發現 PSG 雜訊聲道只有一半大,一起修正。
- 用同樣方法驗證:標題音樂 -23.3 dB 對 PicoDrive -23.5 dB(原本 -31.2),關卡 -20.2 對 -20.1,大鼓頻段從差 9.4 dB 變成 1 dB 以內。
- 修好之後又發現新問題:音量變大後,關掉音樂時的「啵」一聲也變大了。追查成因(PWM 停在非中點、波形被瞬間切斷),改成音量在 38 毫秒內漸變,並加一個測試:沒有漸變就會失敗。
- 發版:0.15.12,六個平台、全部測試、33 組逐格比對、Mac、實體手機都通過才發。
- Look at the evidence: the comment was under a gameplay video. Extract its audio and line it up with the emulated output. All four PWM tracks (the 32X's sample channel) were playing: nothing was missing.
- Find a reference: a program can't be its own yardstick. Download PicoDrive's PC core and write a small windowless host that runs the same ROM, presses the same buttons, enters the same level and saves the sound and frames.
- Compare frame by frame: Mighty's running, jumping, rolling and spin dash were identical frame for frame, so the "Mighty animation" needed no change. In the sound, PicoDrive's bass drum was clearly louder.
- Confirm in the source: read PicoDrive's sound code (read only, nothing copied) for each source's peak level: FM ±8191, PSG ±2304, PWM ±32768, the sum times 0.875. That matched the measured gap.
- Fix: mix at those levels; the PSG noise channel turned out to be half as loud as it should be, and was fixed too.
- Verify the same way: title music -23.3 dB vs PicoDrive -23.5 (was -31.2), level -20.2 vs -20.1, and the bass-drum band from 9.4 dB short to within 1 dB.
- The fix exposed another problem: with the louder mix, muting the music popped louder. Trace the cause (PWM parked off-centre, the waveform cut in one sample), ease volume changes over 38 ms, and add a test that fails without the easing.
- Release: 0.15.12, only after six platforms, every test, 33 lockstep runs, the Mac and the real phone all passed.
docs/AUDIO_MIX.md混音比例的來源、量測與「啵」的修法Where the mix levels come from, the measurements, the pop fixsrc/runtime/system.cppFM、PSG、PWM 混音Mixing FM, PSG and PWMsrc/audio/gain_ramp.h音量漸變(不再「啵」)Volume easing (no more pop)tests/test_pwm_audio.cppPWM 聲道與漸變的測試Tests for PWM routing and easingPR #87這次修正的完整 PRThe full PR for this fix
發版前的檢查清單The release checklist
每個版本都照這張清單走,任何一項沒過就不發:
Every release goes through this list; if any item fails, it doesn't ship:
| 1 | 改版號的 PR,GitHub 上 5 項 CI 全綠(Linux、macOS、Windows、SDL3 前端、No game data)A version-bump PR with all 5 CI jobs green (Linux, macOS, Windows, SDL3 frontend, No game data) |
| 2 | Windows:0 警告建置、139 項測試、33 組逐格比對Windows: warning-free build, 139 tests, 33 lockstep runs |
| 3 | Linux(Ubuntu 22.04):139 項測試,打包後的版本再跑啟動測試Linux (Ubuntu 22.04): 139 tests, plus a startup test of the packaged build |
| 4 | Mac M1:139 項測試、逐格比對、macOS 與 iOS 打包Mac M1: 139 tests, lockstep, macOS and iOS packages |
| 5 | Windows 包:在只有系統 PATH 的環境跑啟動測試,確定沒漏掉 DLLWindows package: startup test with only the system PATH, so no DLL is missing |
| 6 | 實體 Android 掌機:兩個 APK 覆蓋安裝舊版,關卡中看 FPS,選單放 60 秒Real Android handheld: both APKs over the previous version, FPS in a level, menu left alone 60 s |
| 7 | 發版,然後才回覆 issue:先用一般人聽得懂的話說明,再附開發細節與真實截圖Release, and only then reply on the issue: plain words first, then developer detail and real screenshots |
.github/workflows/ci.yml5 項 CI 檢查The 5 CI jobstests/CMakeLists.txt139 項測試的定義Where the 139 tests are definedReleases每個版本的說明與驗證結果Every release with its validation results
e93fcd22026-09-24 CI: default generators, no third-party actions
AI 在這裡做了什麼,人做了什麼What AI did here, and what people did
直接說清楚:這個專案大部分的程式碼由 AI(Anthropic 的 Claude)撰寫。264 個 commit 裡有 228 個標註了 Claude 為共同作者,紀錄都在 git 裡。
「AI 寫的」不等於「垃圾」,也不等於「好」。決定品質的是有沒有辦法證明它是對的。這份文件裡每一步都有一個「怎麼確認」,那才是重點:
To be plain about it: most of this project's code was written by AI (Anthropic's Claude). 228 of its 264 commits name Claude as co-author, and that record is in git for anyone to read.
"Written by AI" doesn't mean "slop", and it doesn't mean "good" either. What decides quality is whether you can prove it's right. Every step on this page has a "how you know"; that is the point:
- 定方向和規則:先重現再修、不准用繞過問題的方式「修好」、截圖必須是真的、要比模擬器快但不能犧牲準確度
- 擁有遊戲檔、測試機器和實體手機,實際玩、實際聽
- 決定什麼時候合併、什麼時候發版
- 玩家回報問題:57 個 issue,其中 44 個來自玩家,已關閉 53 個
- 外部貢獻者也送過修正(例如非 ASCII 路徑)
- Set the direction and the rules: reproduce before fixing, no workaround "fixes", screenshots must be real, be faster than an emulator without losing accuracy
- Own the game file, the test machines and the real phone; actually play and listen
- Decide when to merge and when to release
- Players report problems: 57 issues, 44 of them from players, 53 closed
- Outside contributors sent fixes too (for example non-ASCII paths)
- 寫直譯器、分析器、重編譯器、硬體層、前端
- 寫測試,並在每次修改後跑完整驗證
- 重現問題、量測、查原因、寫說明文件(目前 38 份)
- 照清單建置六個平台、在實體手機上測試、發版
- Wrote the interpreters, analyzer, recompiler, hardware layer and frontend
- Wrote the tests and ran the full verification after every change
- Reproduced problems, measured, traced causes, wrote the docs (38 of them so far)
- Built all six platforms from the checklist, tested on the real phone, released
不管是誰寫的,你都可以直接檢查:程式碼、每個版本的驗證結果、每個 issue 的處理過程,全部公開在 GitHub 上。
Whoever wrote it, you can check it yourself: the code, each release's validation results and how every issue was handled are all public on GitHub.
Commits每個 commit 與共同作者紀錄Every commit, with its co-author linesIssues每個回報與處理過程Every report and how it was handledPull requests每個修改的 PREvery change as a PR
數字一覽At a glance
| 第一個 commitFirst commit | 2026-09-19 |
| 第一個能玩的版本(0.1.0)First playable release (0.1.0) | 2026-09-24 |
| 正式版本數(到 0.15.12)Releases (to 0.15.12) | 50 |
| Commit 數Commits | 264 |
| 程式碼(src、tools、tests 的 C++/Python)Code (C++/Python in src, tools, tests) | ≈ 54,700 lines |
| 自動測試Automated tests | 139 |
| 逐格比對(每版)Lockstep runs (every release) | 33 × bit-identical |
| 翻好的程式執行比例Share of code running natively | 68K 99.7–100% · SH-2 100% |
| 手機每格時間(Thor 32-bit)Phone frame time (Thor 32-bit) | 4.39 → 2.14 ms |
| 說明文件Docs | 38 + ARCHITECTURE.md |
目前的程式碼在這裡Where the code is today
上面的行號連結都固定在 commit 1d0d519(0.15.12 發布時的 main),之後程式碼改了也不會跑掉。最新的程式碼請看:
The line links above are pinned to commit 1d0d519 (main at the 0.15.12 release), so they don't drift as the code changes. For the latest code:
YuutaTsubasa/ChaotixRecompiledGitHub 上的專案首頁The project on GitHubmain目前的程式碼The current code1d0d519這份文件對照的版本The version this page describesreleases/latest最新版本下載與驗證結果The latest release and its validation