RECOMPILED HANDBOOK
Knuckles' Chaotix Recompiled · 2026-09-19 → 2026-10-30

把一款 32X 遊戲翻譯成原生程式:手把手製作流程Turning a 32X game into a native program, step by step

這份文件用這個專案實際走過的路,一步一步說明:如果你要自己做一個「Recompiled」移植,該從哪裡開始、每一步做什麼、怎麼知道自己做對了。裡面的數字都來自專案的 git 紀錄、測試和量測,可以自己去查。

This page walks through how a "Recompiled" port is made, using the path this project actually took: where to start, what each step is, and how you know a step is right. Every number here comes from the project's git history, tests and measurements, and you can check them yourself.

50個正式版本(0.1.0 → 0.15.12)releases (0.1.0 → 0.15.12)
139項自動測試,每版全過automated tests, all passing each release
33/33組隨機輸入逐格比對完全一致random-input runs bit-identical, frame by frame
6個平台:Windows、Linux、macOS、iOS、Android ×2packages: Windows, Linux, macOS, iOS, Android ×2
STEP 00

先搞懂: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.

EMULATOR ROM decode execute × 30,000,000 / s RECOMPILED ROM C++ ×1 native .exe hardware layer (VDP, sound…) translated once, before play
左:模擬器每條指令都要當場解讀。右:Recompiled 事先翻好一次,執行時只剩硬體層需要「模擬」。Left: an emulator decodes each instruction as it runs. Right: the code is translated once, and only the hardware layer is simulated at run time.
在專案裡的位置Where it is in the code
  • README.md專案說明:它是什麼、怎麼建置What it is and how to build it
  • ARCHITECTURE.md架構文件(繁中):每個元件採用什麼策略、為什麼Architecture (in Traditional Chinese): the strategy for each part and why
相關 commitCommits
  • 5835d4a2026-09-19 Knuckles' Chaotix static recompilation: toolchain, runtime and SDL3 frontend
STEP 01

劃清法律界線,再寫第一行程式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.
✓ 怎麼確認:CI 有一項叫「No game data」的檢查,專門確保 repo 裡沒有遊戲資料;另外任何超過 512 KB 的檔案都會被擋下。How you know: CI has a "No game data" job that checks the repo holds no game data, and it rejects any tracked file over 512 KB.
在專案裡的位置Where it is in the code
相關 commitCommits
  • e18ae202026-09-24 Prepare the repository for publication
  • 03bcf0c2026-09-24 Ship recompiled builds, and say plainly what that means
STEP 02

把機器摸透:這台主機裡有什麼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:

MEGA DRIVE 32X 680007.67 MHz Z803.58 MHz · sound MD VDP YM2612 FM SN76489 PSG SH-2 master23.01 MHz SH-2 slave23.01 MHz 32X VDP · FB PWM sound COMM ports MCLK 53.693175 MHz · 262 lines/frame · 59.9227 Hz
四顆處理器、兩組畫面晶片、三種音源(FM、PSG、PWM)。中間金色的是兩邊交換訊息用的 COMM 暫存器。Four processors, two video chips, three sound sources (FM, PSG, PWM). The gold path is the COMM registers both sides use to pass messages.

這一步的產出是一份架構文件:每顆處理器的時脈、記憶體地圖(哪個位址是 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.

! 這款遊戲的坑:副 SH-2 把自己的程式複製到 CPU 內部的快取記憶體裡執行,堆疊也放在那裡。一般模擬器常常不用處理這種情況,但這款遊戲少了它就不會動。This game's trap: the slave SH-2 copies its own code into the CPU's cache memory and runs it from there, with its stack there too. Many emulators never need this; without it this game doesn't run.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 5835d4a2026-09-19 Knuckles' Chaotix static recompilation: toolchain, runtime and SDL3 frontend
STEP 03

先寫「標準答案」:參考直譯器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.

✓ 怎麼確認:拿處理器手冊上已知答案的例子寫單元測試,例如旗標、BCD、乘除法、SH-2 的 delay slot 順序。專案一開始就有 33 項這種測試。How you know: unit tests built from known answers in the CPU manuals: flags, BCD, multiply and divide, the SH-2 delay-slot order. The project started with 33 of them.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 5835d4a2026-09-19 Knuckles' Chaotix static recompilation: toolchain, runtime and SDL3 frontend
STEP 04

找出 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.
✓ 怎麼確認:用「沒拿來產生 coverage 的另一段遊玩」去跑,看有多少程式是翻好的版本在執行。這個專案:68000 有 99.7–100%、SH-2 有 100% 的程式區塊直接跑翻好的原生碼。How you know: play a stretch that wasn't used to make the coverage, and count how much runs as translated code. Here, 99.7–100% of 68000 blocks and 100% of SH-2 blocks run as native code.
在專案裡的位置Where it is in the code
相關 commitCommits
  • aad37472026-09-20 Milestone 3: coverage harness, static jump-table recovery, performance
  • a48a2512026-10-20 Special stage recompiled instead of interpreted (issue #31)
STEP 05

真正的翻譯:重編譯器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.

your ROMSHA-1 ✓ analyzerCFG + coverage recompiler68K + SH-2 generated/C++ (local only) native program+ runtime + SDL3 unknown code → interpreter (slower, never wrong)
整條流程在使用者自己的電腦上跑,產生的 C++ 只留在本機。The whole pipeline runs on the player's or developer's own machine; the generated C++ stays local.

三個一定會遇到的難題Three problems you will hit

難題Problem這款遊戲裡的實例In this game怎麼處理What was done
遊戲在執行中產生的程式Code made at run time68000 在 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 overwrittenSH-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 momentV-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
? 設計原則:遇到翻譯器處理不了的程式,就退回直譯器。這樣只會變慢,不會出錯。Design rule: anything the translator can't handle falls back to the interpreter. That can only make things slower, never wrong.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 5835d4a2026-09-19 Knuckles' Chaotix static recompilation: toolchain, runtime and SDL3 frontend
  • aad37472026-09-20 Milestone 3: coverage harness, static jump-table recovery, performance
STEP 06

補上硬體:相容層與時間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.

1 scanline = 3420 MCLK = 4 slices 68000 Z80 SH-2 master SH-2 slave interrupts: only at block edges
! 真實的坑(issue #50):遊戲自己的 SOUND TEST 沒有聲音。原因是 68000 暫停 Z80 時,Z80 沒有剛好跑到被暫停的那一刻。修法是先把 Z80 補跑到 68000 當下的時間,再改變它的狀態,跟真機一樣。A real trap (issue #50): the game's own SOUND TEST was silent. When the 68000 paused the Z80, the Z80 hadn't run up to that exact moment. The fix runs the Z80 up to the 68000's current time before changing its state, as the hardware does.
在專案裡的位置Where it is in the code
相關 commitCommits
  • e3c133d2026-10-24 Z80 runs up to the 68K's time on BUSREQ/RESET writes: the SOUND TEST plays (issue #50)
  • 5b6e1f02026-09-27 Fix PWM stereo and FM modulation timing for 0.3.4
STEP 07

對答案:逐格 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.

示意:每格的狀態雜湊值兩邊完全相同(綠色勾)。Illustration: each frame's state hash matches on both sides (green tick).
  • 開機到關卡: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.
? 這是讓加速和修改都安全的基礎。第 11 步的每一次加速,都必須通過同一套比對,證明「變快了,但遊戲的行為一個 byte 都沒變」。This is what makes speed-ups and changes safe. Every speed-up in step 11 had to pass the same comparison: faster, with not one byte of the game's behaviour changed.
在專案裡的位置Where it is in the code
  • src/frontend/headless_main.cpp--lockstep:兩台機器並排、每格比對--lockstep: two machines side by side, compared every frame
  • tests/CMakeLists.txtlockstep_boot_to_level 與 golden_frames 測試The lockstep_boot_to_level and golden_frames tests
  • tests/golden/固定輸入下每 50 格的畫面雜湊Picture hashes every 50 frames for fixed inputs
相關 commitCommits
  • 5835d4a2026-09-19 Knuckles' Chaotix static recompilation: toolchain, runtime and SDL3 frontend
  • 64b3c592026-09-20 Milestone 4: true widescreen in levels
STEP 08

做成能玩的程式:前端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.

在專案裡的位置Where it is in the code
相關 commitCommits
  • b48de502026-09-27 Fix frame pacing and release 0.3.3
STEP 09

讓它比原版更好,但不破壞原版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.

426×224(16:9)。虛線框內是原版畫面,必須逐像素相同;兩側是遊戲自己畫出來的延伸。426×224 (16:9). Inside the dashed frame is the original picture, required to match pixel for pixel; the sides are drawn by the game's own renderer.

另一個例子:時間挑戰模式沒有自己發明選關畫面,而是找到遊戲裡原本就有、但正式版沒開放的 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.

在專案裡的位置Where it is in the code
  • src/runtime/patches.cpp寬螢幕等對遊戲的修改,集中在這裡Widescreen and other game-side changes, all in one place
  • tests/CMakeLists.txtwidescreen_centre_matches_4_3:中間 320 像素必須相同widescreen_centre_matches_4_3: the centre 320 px must match
  • docs/ISSUE_7_WIDESCREEN.md真寬螢幕的做法How real widescreen works
  • src/runtime/stage_select.cpp呼叫遊戲自己隱藏的 STAGE SELECT 進關卡Starting a level through the game's own hidden STAGE SELECT
  • docs/存檔、倒帶、連線、mod 等功能各自的說明Docs for save states, rewind, netplay, mods and more
相關 commitCommits
  • 64b3c592026-09-20 Milestone 4: true widescreen in levels
  • b9581272026-09-26 Time attack, from the stage select the game already had
  • fef6cf42026-09-24 Achievements: local unlock system with in-game notifications
STEP 10

搬到每個平台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
LinuxWSL 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 / iOSMac mini M1:全部測試+lockstepMac mini M1: all tests + lockstepARM 上逐格比對也要完全一致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
在專案裡的位置Where it is in the code
相關 commitCommits
  • ba32f752026-09-24 Android: touch-driven setup, landscape, and a build that runs
  • 897da812026-10-10 Android: document the 32-bit ARM build (issue #19), drop an unused constant
  • 1540ae92026-10-23 macOS and iOS: build the app bundles
  • a1f1d262026-10-02 Windows: non-ASCII names no longer crash the ROM search or break the install
STEP 11

跑得更快:先量,再改,再證明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.

0.15.4
4.39 ms
0.15.5
≈3.3 ms
0.15.6
2.40 ms
0.15.11
2.14 ms
PicoDrive 64-bit
≈2.10 ms
AYN Thor,32-bit 版,BOTANIC BASE 1,每格平均毫秒數(越短越好)。PicoDrive 是在標題/示範畫面量的,條件不完全相同。AYN Thor, 32-bit build, BOTANIC BASE 1, average milliseconds per frame (shorter is better). PicoDrive was measured on the title/demo stretch, so it's not a perfect like-for-like.

其中一次加速的故事:關卡中,遊戲每格大約 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.

在專案裡的位置Where it is in the code
  • docs/PERFORMANCE_ANDROID.md每次加速的量測數字The measurements behind each speed-up
  • docs/IDLE_SKIP.md等待迴圈精確快轉,以及為什麼結果不變Exact fast-forward of wait loops, and why results don't change
  • src/runtime/sh2_bus.cpp每個函式各自記錄程式寫入Per-function code-write tracking
  • issue #62整個加速過程與實機數字The whole speed-up story with phone numbers
相關 commitCommits
  • 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)
STEP 12

接玩家回報:先重現,再修,再用同一招驗證Handle reports: reproduce, fix, verify the same way

① 用自己的遊戲檔重現① Reproduce locally ② 找到真正原因② Find the cause ③ 真的修,不繞過③ Real fix ④ 同樣方法驗證④ Verify same way ⑤ PR+CI 全綠⑤ PR, CI green ⑥ 發版⑥ Release ⑦ 附真實截圖回覆⑦ Reply, real shots 重現不了?Can't reproduce?說明試過什麼再問say what was tried

這個流程是專案擁有者訂的規則,每一個 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"

  1. 先看證據:留言掛在一段實機錄影下。把影片的音訊抽出來,跟模擬輸出對齊比較。結果 PWM(32X 的取樣音源)四個音軌都有在播,沒有少。
  2. 找標準:自己的程式不能當自己的標準。下載 PicoDrive 的 PC 版核心,寫一個小程式讓它不開視窗,跑同一個 ROM、按同樣的鍵、進同一關,把聲音和畫面存下來。
  3. 逐格比對:Mighty 的跑、跳、滾球、spin dash,兩邊逐格相同,所以「Mighty 動畫」不需要改。聲音方面,PicoDrive 的大鼓明顯比較響。
  4. 讀原始碼確認:讀 PicoDrive 的音源程式碼(只讀,不複製),確定每種音源的最大音量:FM ±8191、PSG ±2304、PWM ±32768,最後整體乘 0.875。跟實測的差距吻合。
  5. 修:照這些比例改混音;另外發現 PSG 雜訊聲道只有一半大,一起修正。
  6. 用同樣方法驗證:標題音樂 -23.3 dB 對 PicoDrive -23.5 dB(原本 -31.2),關卡 -20.2 對 -20.1,大鼓頻段從差 9.4 dB 變成 1 dB 以內。
  7. 修好之後又發現新問題:音量變大後,關掉音樂時的「啵」一聲也變大了。追查成因(PWM 停在非中點、波形被瞬間切斷),改成音量在 38 毫秒內漸變,並加一個測試:沒有漸變就會失敗。
  8. 發版:0.15.12,六個平台、全部測試、33 組逐格比對、Mac、實體手機都通過才發。
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Fix: mix at those levels; the PSG noise channel turned out to be half as loud as it should be, and was fixed too.
  6. 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.
  7. 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.
  8. Release: 0.15.12, only after six platforms, every test, 33 lockstep runs, the Mac and the real phone all passed.
40–150150–400400–1k1k–2.5k2.5k–5k5k–10k Hz
PicoDrive修改前before修改後after
BOTANIC BASE 1 各頻段能量(相對刻度)。修改前每個頻段都偏低,大鼓所在的 40–150 Hz 差最多。Energy per band in BOTANIC BASE 1 (relative scale). Before, every band was low, and 40–150 Hz, where the bass drum is, was lowest.
? 注意第 7 點:修完一個問題,不是交差,而是再量一次、再聽一次。「做到真的對」和「做到看起來能動」的差別,就在這裡。Note point 7: fixing one thing isn't the finish line; measure and listen again. That's the difference between "really right" and "seems to work".
在專案裡的位置Where it is in the code
相關 commitCommits
  • 5fb52352026-10-30 Mix FM, PSG and PWM at PicoDrive's levels
  • ac5c0d72026-10-30 Ease volume changes so muting the music does not pop
STEP 13

發版前的檢查清單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)
2Windows:0 警告建置、139 項測試、33 組逐格比對Windows: warning-free build, 139 tests, 33 lockstep runs
3Linux(Ubuntu 22.04):139 項測試,打包後的版本再跑啟動測試Linux (Ubuntu 22.04): 139 tests, plus a startup test of the packaged build
4Mac M1:139 項測試、逐格比對、macOS 與 iOS 打包Mac M1: 139 tests, lockstep, macOS and iOS packages
5Windows 包:在只有系統 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
在專案裡的位置Where it is in the code
相關 commitCommits
  • e93fcd22026-09-24 CI: default generators, no third-party actions
ABOUT AI

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:

人People
  • 定方向和規則:先重現再修、不准用繞過問題的方式「修好」、截圖必須是真的、要比模擬器快但不能犧牲準確度
  • 擁有遊戲檔、測試機器和實體手機,實際玩、實際聽
  • 決定什麼時候合併、什麼時候發版
  • 玩家回報問題: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)
AI
  • 寫直譯器、分析器、重編譯器、硬體層、前端
  • 寫測試,並在每次修改後跑完整驗證
  • 重現問題、量測、查原因、寫說明文件(目前 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.

在專案裡的位置Where it is in the code
  • Commits每個 commit 與共同作者紀錄Every commit, with its co-author lines
  • Issues每個回報與處理過程Every report and how it was handled
  • Pull requests每個修改的 PREvery change as a PR
BY THE NUMBERS

數字一覽At a glance

第一個 commitFirst commit2026-09-19
第一個能玩的版本(0.1.0)First playable release (0.1.0)2026-09-24
正式版本數(到 0.15.12)Releases (to 0.15.12)50
Commit 數Commits264
程式碼(src、tools、tests 的 C++/Python)Code (C++/Python in src, tools, tests)≈ 54,700 lines
自動測試Automated tests139
逐格比對(每版)Lockstep runs (every release)33 × bit-identical
翻好的程式執行比例Share of code running natively68K 99.7–100% · SH-2 100%
手機每格時間(Thor 32-bit)Phone frame time (Thor 32-bit)4.39 → 2.14 ms
說明文件Docs38 + ARCHITECTURE.md
THE CODE TODAY

目前的程式碼在這裡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: