FREE RIDERS HANDBOOK
Sonic Free Riders Recompiled · 2026-09-22 → 2026-10-30

把一款 Xbox 360 的 Kinect 遊戲翻成原生程式:製作流程Turning an Xbox 360 Kinect game into a native program, step by step

《Sonic Free Riders》是一款只能用 Kinect 體感玩的 Xbox 360 遊戲。這份文件照這個專案實際走過的路,一步一步說明它怎麼變成能在電腦、手機上執行的程式:每一步在做什麼、遇到什麼坑、怎麼知道做對了。裡面的數字都來自專案的 git 紀錄、文件、測試和發版說明,可以自己去查。

Sonic Free Riders is an Xbox 360 game you can only play with a Kinect, the camera that reads your body. This page follows the path this project actually took to turn it into a program that runs on computers and phones: what each step is, what went wrong, and how you know a step is right. Every number comes from the project's git history, docs, tests and release notes, and you can check them yourself.

86個版本(0.1.0 → 0.10.38)releases (0.1.0 → 0.10.38)
183項單元測試,0.10.38 在 Windows 全過unit tests, all passing on Windows for 0.10.38
24 → 60AYN Thor 手機上比賽的 fps(0.4.4 → 0.10.38)race fps on an AYN Thor handheld (0.4.4 → 0.10.38)
6種安裝包:Windows、Linux、Android ×2、macOS、iOSpackages: Windows, Linux, Android ×2, macOS, iOS
STEP 00

先搞懂:「Recompiled」和模擬器差在哪First: how a recompilation differs from an emulator

模擬器(例如 Xbox 360 的 Xenia)像同步口譯:遊戲每執行一條機器指令,模擬器當下才去讀懂它、照做一次,一條接一條。

Recompiled 像事先把整本書翻好:在玩之前,就把遊戲的機器碼(Xbox 360 用的是 PowerPC 處理器的指令)翻成 C++ 程式碼,再用電腦自己的編譯器編成一般的程式。玩的時候不用再讀懂指令,直接跑翻好的版本。

但遊戲不只是程式。它還會呼叫主機的系統(開執行緒、讀檔、要記憶體)、叫顯示晶片畫圖、讀 Kinect 看到的身體。這些不是翻譯能處理的,要另外寫一層程式來「扮演」Xbox 360。所以這個專案其實是兩半:翻好的遊戲程式,加上一層代替主機的執行環境(runtime)。

翻譯工具用的是現成的開源專案:CPU 程式用 XenonRecomp,畫面用的著色器(shader,跑在顯示卡上的小程式)用 XenosRecomp。它們原本是為《Sonic Unleashed Recompiled》等專案做的。

An emulator (Xenia, for the Xbox 360) is like a live interpreter: each time the game runs a machine instruction, the emulator reads it and carries it out, one after another.

A recompilation is like translating the whole book in advance: before you play, the game's machine code (the Xbox 360 runs PowerPC instructions) is translated into C++, which your computer's own compiler turns into an ordinary program. While you play, nothing has to be read and understood again.

But a game is more than code. It calls on the console's system (to start threads, read files, get memory), asks the graphics chip to draw, and reads the body the Kinect sees. None of that can be translated; it needs a separate layer that plays the part of the Xbox 360. So this project is two halves: the translated game code, and a runtime that stands in for the console.

The translators are existing open-source projects: XenonRecomp for the CPU code and XenosRecomp for the shaders (the small programs that run on the graphics card). They were made for projects like Sonic Unleashed Recompiled.

EMULATOR XEX decode execute every instruction, every time RECOMPILED XEX C++ ×1 native game runtime: system, GPU, Kinect translated once, before play
左:模擬器每條指令都要當場解讀。右:事先翻好一次,執行時只剩「扮演主機」的那一層要做事。XEX 是 Xbox 360 的執行檔格式。Left: an emulator decodes each instruction as it runs. Right: the code is translated once, and only the layer that plays the console works at run time. XEX is the Xbox 360's program file format.
在專案裡的位置Where it is in the code
  • README.md專案說明:它是什麼、怎麼安裝What it is and how to install it
  • docs/progress.md從零開始的實作紀錄(繁中,2026-09-10 起)The build-up from nothing (in Traditional Chinese, from 2026-09-10)
  • config/dependencies.lock.jsonXenonRecomp、XenosRecomp 等工具固定在哪個版本Which versions of XenonRecomp, XenosRecomp and the rest are pinned
相關 commitCommits
  • 9d2ba1c2026-09-22 Free Riders Recompiled
STEP 01

劃清法律界線,再寫第一行程式Draw the legal line before the first line of code

這種專案能公開,前提是不散布任何遊戲本身的東西。這件事會決定之後所有工具怎麼設計,所以一開始就要定好。

  • 玩家用自己的光碟:安裝程式(launcher)讀玩家自己的光碟映像檔(ISO),把遊戲資料裝到自己的電腦上。它會先檢查裡面的 default.xex 是不是支援的版本(目前只支援美版/歐版光碟),不是就不裝。
  • 翻出來的 C++ 永遠只在本機:光碟映像、遊戲檔、從遊戲翻出來的程式碼,全部列在 .gitignore,不進 git、不上傳任何雲端。
  • CI(GitHub 上的自動測試)拿不到遊戲:所以 CI 只測不需要遊戲的部分。需要遊戲的建置和測試,都在開發者自己的電腦上跑。
  • 發布的安裝包照 Unleashed Recompiled 的模式:開發者用自己的光碟建好程式再發布;遊戲的資料(圖、聲音、影片)一個 byte 都不附。

A project like this can be public only if it ships nothing from the game itself. That decides how every tool is designed, so settle it on day one.

  • Players use their own disc: the launcher reads the player's own disc image (ISO) and installs the game's data on their machine. It first checks that the disc's default.xex is the supported version (only the USA/Europe disc for now), and won't install anything else.
  • The translated C++ stays local: disc images, game files and the code generated from the game 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 only what doesn't need it. Builds and tests that need the game run on the developer's own machine.
  • Releases follow the Unleashed Recompiled model: the developer builds the program from their own disc and publishes it; not one byte of the game's data (pictures, sound, movies) goes with it.
The Free Riders Recompiled launcher
Launcher:選自己的光碟映像檔安裝,之後的設定也在這裡改。The launcher: pick your own disc image to install, and change settings here later.
✓ 怎麼確認:CI 有一項叫「No game data」的檢查,只要 repo 裡出現光碟映像、遊戲的檔案類型(.xex、.cpk、.wmv…)、翻出來的 ppc_recomp.*.cpp,或任何超過 4 MB 的檔案,就直接失敗。How you know: CI has a "No game data" job. It fails if the repo holds a disc image, any of the game's file types (.xex, .cpk, .wmv…), a generated ppc_recomp.*.cpp, or any file over 4 MB.
在專案裡的位置Where it is in the code
  • .gitignore光碟、遊戲檔、翻出來的程式碼永遠不進 gitDiscs, game files and generated code never go into git
  • .github/workflows/build.ymlCI 的 No game data 檢查CI's No game data job
  • src/installer.cpp安裝前檢查光碟上的 default.xex 是支援的版本The install checks the disc's default.xex is the supported one
  • scripts/prepare_recomp.py翻譯前再用 SHA-256 確認是同一個執行檔Before translating, SHA-256 confirms it's the same program
  • docs/releasing.md發布包含什麼、絕不包含什麼What a release holds, and what it never holds
相關 commitCommits
  • 28048352026-09-22 Package releases, and carry the shader pack in the APK
STEP 02

把光碟拆開:遊戲到底由什麼組成Take the disc apart: what the game is made of

Xbox 360 光碟用自己的檔案系統(XDVDFS)。拆開之後,這片光碟的遊戲分割區有 685 個檔案。大部分是圖、聲音、影片,真正的程式只有一個:default.xex。

XEX 是加密又壓縮過的執行檔。default.xex 有 14,241,792 bytes,解開後的程式映像是 34,209,792 bytes。裡面記著遊戲要向主機系統借用的 299 個系統函式(例如「開一個執行緒」「讀一個檔」),以及 13 個系統變數。這張清單就是第 4 步「扮演主機」的工作範圍。

An Xbox 360 disc has its own file system (XDVDFS). Taken apart, this disc's game partition holds 685 files. Most are pictures, sound and movies; the actual program is a single file, default.xex.

XEX is an encrypted, compressed program file. default.xex is 14,241,792 bytes, and the decoded program image is 34,209,792 bytes. It lists the 299 system functions the game borrows from the console (like "start a thread" or "read a file") and 13 system variables. That list is the job description for step 04, playing the console.

your disc .isoXDVDFS 685 filesart, sound, movies default.xex14.2 MB image34.2 MB decoded code 299 imports+13 vars only the program is translated; the data is read by the game as it was
翻譯的只有程式本身。其他 684 個檔案,遊戲照原樣自己去讀。Only the program is translated. The other 684 files the game reads itself, as they are.
✓ 怎麼確認:每一步的輸入輸出都有固定的雜湊值(SHA-256/SHA-1)。光碟、解開的執行檔、解碼後的映像只要有一個 byte 不同,工具就拒絕往下做,不會拿錯的東西去翻。How you know: each stage's input and output has a fixed hash (SHA-256/SHA-1). If the disc, the program file or the decoded image differs by a byte, the tools refuse to go on, so nothing wrong gets translated.
在專案裡的位置Where it is in the code
  • src/xex_image.cpp解開 XEX:解密、解壓、讀出系統函式清單Decoding the XEX: decrypt, decompress, read the import list
  • config/source.json支援版本的雜湊值The supported version's hashes
  • docs/xex-loader.md執行檔標頭怎麼交給遊戲How the program header is handed to the game
  • scripts/rom_tool.py讀光碟映像的工具The disc image tool
相關 commitCommits
  • 9d2ba1c2026-09-22 Free Riders Recompiled
STEP 03

翻譯程式:把 PowerPC 翻成 C++Translate the code: PowerPC into C++

XenonRecomp 把程式映像裡的每個函式翻成一段 C++。翻出來的程式不是一次就能用:工具第一次跑完,有 14,475 個函式被標成「翻不出來,先停用」。之後的工作就是一個一個找出原因、把這個數字壓下來。

這個專案的原則跟模擬器不一樣:不認得的東西不猜。翻不了的函式會變成一個有名字的「停止點」,遊戲一跑到那裡就停下來、說出是哪個函式。這樣錯誤會立刻被看見,而不是畫面怪怪的、找不到原因。

XenonRecomp turns each function in the program image into a piece of C++. The output isn't usable in one go: after the first run, 14,475 functions were marked "can't translate, disabled". The work that follows is finding out why, one cause at a time, and bringing that number down.

The rule here differs from an emulator's: never guess at what you don't recognise. A function that can't be translated becomes a named stop; when the game reaches it, it stops and says which function. Mistakes show up at once, instead of as a strange picture nobody can explain.

第一次first run
14,475
修正函式邊界boundaries
628
自己的跳轉表分析jump tables
479
補上缺的指令instructions
143
被停用的函式數(越少越好),來自 docs/progress.md 的紀錄。Functions left disabled (fewer is better), from the record in docs/progress.md.

三個壓下數字的發現Three findings that brought it down

問題Problem原因Cause結果Result
函式被切成碎片Functions cut into fragments工具把函式之間的填充(全是 0)和例外處理用的兩個字,當成指令來讀,從此對不上The tool read the padding between functions (all zeros) and two exception-handler words as instructions, and lost its place14,475 → 628
看不懂 switchSwitches not recognised現成的分析器只認一種指令順序,這款遊戲的編譯器排成另一種,所以一張「跳轉表」都沒找到;改寫一個不看順序的分析器,找到 309 張The stock analyser expects one instruction order and this game's compiler used another, so it found no jump tables at all; a new analyser that doesn't depend on the order found 309628 → 479
缺指令Missing instructions14 種「讀寫後順便更新位址」和 24 種向量運算,補上有測試的版本,改寫 4,042 處14 load/store-with-update forms and 24 vector operations, added with tests, rewriting 4,042 places479 → 143
? 注意第一行:「14,475 個翻不出來」聽起來很嚴重,但查下去,大多是工具讀錯位置造成的假問題。先查清楚數字的意思,再決定要修什麼。Look at the first row: "14,475 can't be translated" sounds dire, but most were false alarms from the tool reading at the wrong place. Find out what a number means before deciding what to fix.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 9d2ba1c2026-09-22 Free Riders Recompiled
STEP 04

扮演主機:一個系統函式接一個Play the console: one system function at a time

翻好的遊戲一開始跑,就會向「主機」要東西:要記憶體、開執行緒、等事件、讀檔、問現在幾點、問系統語言是什麼、問 Kinect 在不在。這些都要由我們的執行環境(runtime)回答,而且要回答得跟真的 Xbox 360 一樣。

做法很樸實:跑到哪裡、停在哪裡,就補哪裡。docs/progress.md 從 2026-09-10 開始,記了一長串「現在停在哪個函式、補了什麼、下一個停在哪」。例如遊戲問兩次 Kinect 狀態,就回答「沒有接」,看遊戲自己怎麼處理;它問國家,就去查 Windows 的設定再換成主機的代碼。

遊戲本來在主機的三個 CPU 核心上開了很多條執行緒。在電腦上,每一條都變成一條真的執行緒。要讓它們安全地同時跑,就要把事件、號誌、臨界區(同時只能一個人進去的區段)都做得跟主機一樣。

As soon as the translated game runs, it asks the "console" for things: memory, threads, events to wait on, files, the time, the system language, whether a Kinect is there. Our runtime has to answer all of it, and answer the way a real Xbox 360 would.

The method is plain: run until it stops, fill in what it stopped on. docs/progress.md, from 2026-09-10 on, is a long chain of "it now stops at this function, this was added, next it stops here". For example, the game asks twice whether a Kinect is connected; the runtime says no, and the game's own code handles that. It asks for the country; the runtime reads Windows' setting and turns it into the console's code.

On the console the game spreads many threads across three CPU cores. On a computer each becomes a real thread. For them to run safely at the same time, events, semaphores and critical sections (sections only one thread may be in at once) have to behave exactly as the console's do.

! 真實的坑:遊戲有一種「無鎖」的資料結構,靠 CPU 的一組特殊指令(lwarx/stwcx.)保證不會兩條執行緒同時改到。最簡單的做法只比對數值,結果讀到一個「數值一樣、但其實已經被釋放」的節點,World Grand Prix 一載入就當掉(錯誤 R6025)。改成每次寫入都帶版本號之後才穩定,這個做法到現在都保留著。A real trap: the game has a "lock-free" structure that relies on a pair of special CPU instructions (lwarx/stwcx.) to stop two threads changing it at once. The simplest way to stand in for them compares values only, and it let a thread pick up a node that had the same value but had already been freed: Grand Prix crashed while loading (error R6025). Giving every write a version number fixed it, and that is still how it works.
✓ 怎麼確認:每個補上的系統函式都有自己的單元測試(執行緒、等待、記憶體、檔案、時鐘…,見 tests/),而且一定要用真的遊戲跑過那一段,才算數。How you know: every system function added has its own unit tests (threads, waits, memory, files, clocks…, see tests/), and it only counts once the real game has run through that part.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 9d2ba1c2026-09-22 Free Riders Recompiled
  • e7ff85a2026-09-22 Delay new guest threads' start; versioned reservations; pack compiles missing DXIL
STEP 05

畫面:把主機的顯示晶片換成電腦的Graphics: swap the console's GPU for the computer's

Xbox 360 的顯示晶片叫 Xenos。遊戲畫圖時,會把指令和著色器(shader)交給它。在電腦上,這些要換成 Direct3D 12 或 Vulkan(電腦和手機的繪圖介面),著色器也要先翻譯。

遊戲在比賽中用到 1,182 個著色器。一開始有 735 個翻不出來,原因是這款遊戲有兩種用法翻譯器沒料到(骨骼動畫的頂點資料、畫圖時才設定的迴圈次數)。在翻譯前後各補一段處理之後,只剩 3 個。

翻譯著色器的工具只能在 Windows 上跑,所以 Linux 和手機版用的是在 Windows 上事先翻好、打包成 shaders.pack 的版本。另外,顯卡第一次用到一種畫法時要先「編譯」,會卡一下;所以按下開始後,遊戲會先把已知的組合(455 個繪圖管線)準備好,比賽中就不會因為這個停頓。

The Xbox 360's graphics chip is called Xenos. To draw, the game hands it commands and shaders. On a computer those become Direct3D 12 or Vulkan (the drawing interfaces of PCs and phones), and the shaders have to be translated first.

In races the game uses 1,182 shaders. At first 735 couldn't be translated, because this game does two things the translator didn't expect (vertex data for skeletal animation, and loop counts set only when drawing). With a step added before and after translation, 3 were left.

The shader translator only runs on Windows, so Linux and phones use shaders translated ahead of time on Windows and packed into shaders.pack. And because a graphics card has to compile each way of drawing the first time it meets it, which causes a hitch, the game prepares the known combinations (455 graphics pipelines) after you press Play, so races don't stall on it.

翻不出來(前)untranslatable, before
735
翻不出來(後)untranslatable, after
3
比賽用到的全部all used in races
1,182
比賽中執行時建立的著色器。數字來自 docs/shader-vertex-palette.md。Shaders created at run time in races. Numbers from docs/shader-vertex-palette.md.
! 真實的坑(v0.2.0):發出去的 shaders.pack 跟程式不是同一代,著色器以為有 3 個資料位址、程式卻給了 5 個,玩家看到全白的畫面,或顯卡直接當掉。之後 pack 檔頭多了一個「版本號」(shader ABI),版本不合的 pack 在打包和載入時都會被拒絕。A real trap (v0.2.0): the shipped shaders.pack came from a different build than the program. The shaders expected 3 buffer addresses and the program supplied 5; players got a white screen or a lost graphics device. Since then the pack carries a version (the shader ABI), and a pack that doesn't match is refused both when packaging and when loading.
在專案裡的位置Where it is in the code
相關 commitCommits
  • bfdec472026-09-22 The shader pack is complete: the game creates every shader while it boots
  • 36547e82026-09-28 Validate shader ABI when building and loading release packs
  • cd43f052026-10-01 Prepare recorded graphics pipelines before game startup
STEP 06

沒有 Kinect 也能玩:假裝有一副身體Play without a Kinect: pretend there's a body

這是這款遊戲和一般移植最不一樣的地方:它沒有手把操作。選單用手的游標和語音指令,比賽用整個身體:往前傾加速、蹲下再站起來跳、一隻腳往後踢地加速。

Kinect 在主機上由一套叫 NUI 的函式庫回報「玩家的骨架」:每個人 20 個關節的位置。遊戲自己有大約 25 個「手勢判斷器」(跳、蹲、踢、傾斜…)讀這副骨架。所以最省事、也最不會改壞遊戲的做法,是在 NUI 這一層用手把假裝出一副骨架:

  • 選單:手把移動游標當成手;按鍵對應遊戲原本就聽得懂的語音指令(START、A=ok、B=back)。
  • 比賽:按住 A 蹲下蓄力、放開跳;按 X 踢地加速;左類比傾斜轉彎、空中旋轉做特技;B 抓欄杆。

This is where this game differs most from other ports: it has no controller controls. Menus use a hand cursor and voice commands; races use your whole body: lean forward to speed up, crouch and stand to jump, kick back with one foot to dash.

On the console a Kinect reports "the player's skeleton" through a library called NUI: 20 joint positions per person. The game has about 25 gesture detectors of its own (jump, crouch, kick, lean…) that read that skeleton. So the simplest way, and the least likely to break the game, is to pretend there's a skeleton at the NUI layer, driven by a controller:

  • Menus: the stick moves the cursor as a hand; buttons stand for voice commands the game already understands (START, A = ok, B = back).
  • Races: hold A to crouch and charge, release to jump; X to kick dash; the left stick to lean, and to spin for tricks in the air; B to grab rails.
controllerA · X · stick pretend skeleton20 joints NUI libraryas on the console game's detectorsjump, crouch, lean… real Kinect / webcam (step 07) replace only this box →
遊戲自己的判斷邏輯完全不動;換的只是「骨架從哪裡來」。The game's own detection logic stays untouched; only where the skeleton comes from changes.
! 真實的坑:手把一度能從標題一路走到角色選擇,卻卡在裝備零件的畫面。換頁要等一個「淡出」動作結束,但遊戲在那一頁不再更新選單,所以那個動作永遠等不到。像這樣,每個畫面都要查清楚遊戲到底在等什麼,不能只看按鍵有沒有送到。A real trap: the controller could get from the title to character select, then stuck on the gear parts screen. Turning the page waited for a fade-out to finish, but on that page the game no longer updated the menu, so the fade never came. Screen by screen, you have to find out what the game is really waiting for, not just whether the button got through.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 9d2ba1c2026-09-22 Free Riders Recompiled
STEP 07

真的身體:Webcam、Kinect、Kinect v2,現在加上 MacA real body: webcam, Kinect, Kinect v2, and now the Mac

既然遊戲只在乎「骨架從哪裡來」,換成真的身體就只是換掉上一步那一格:

  • Webcam(v0.2.0):一般的網路攝影機沒有深度,所以用 MediaPipe 的姿勢模型從畫面「估計」身體的 3D 姿勢。它量不出你離鏡頭多遠,但夠讓遊戲看懂傾身、蹲、跳。
  • Kinect for Xbox 360(v0.4.0):微軟給 Windows 的 Kinect SDK 1.8,其實就是同一套 NUI 函式庫搬到電腦上,骨架一樣是 20 個關節、同樣的座標。所以感測器看到的身體幾乎原封不動交給遊戲,比賽也改回由遊戲自己讀真實的身體和深度影像。這部分有很大一部分是協作者 KnucklesLee 做的。
  • Kinect v2(Xbox One 版):它回報 25 個關節,要換算成遊戲認得的第一代 20 個關節。專案裡沒有人有 Kinect v2,改進全靠一位玩家附上的記錄檔,所以仍是實驗性的,相關回報還開著(issue #165)。
  • Mac(開發中,2026-10-30 合併):Mac 沒有微軟的 Kinect 執行環境,只能透過開源的 libfreenect 拿到彩色和深度畫面,骨架要自己算:用姿勢模型在彩色畫面找關節,再用深度把位置放到公尺座標。老實說:這部分還沒接過真的 Kinect 測試,只確認了沒接 Kinect 時會正常改用手把、整場比賽速度 1.000。

Since the game only cares where the skeleton comes from, a real body just replaces that one box from the last step:

  • Webcam (v0.2.0): an ordinary webcam has no depth, so MediaPipe's pose model estimates the body's 3D pose from the picture. It can't measure how far you are from the camera, but it's enough for the game to read leaning, crouching and jumping.
  • Kinect for Xbox 360 (v0.4.0): Microsoft's Kinect SDK 1.8 for Windows is the same NUI library brought to the PC: 20 joints, the same coordinates. So the body the sensor sees goes to the game almost untouched, and races go back to the game's own detectors reading the real body and depth image. Much of this was built by collaborator KnucklesLee.
  • Kinect v2 (the Xbox One one): it reports 25 joints, which are turned into the first Kinect's 20 that the game knows. Nobody on the project has a Kinect v2; the work relies on logs one player attached, so it's still experimental and the reports about it are open (issue #165).
  • Mac (in development, merged 2026-10-30): the Mac has no Microsoft Kinect runtime. The open-source libfreenect gives only the colour and depth pictures, so the skeleton is made here: the pose model finds the joints in the colour picture, and depth places them in metres. To be plain: this hasn't been tried with a real Kinect yet. What has been checked is that without one it falls back to the controller and races at full speed (1.000).
A race played with a webcam, beside the skeleton debug window
用 webcam 比賽,右邊是除錯視窗:正面與側面看到的 20 個關節。A race with a webcam; on the right, the debug window shows the 20 joints from the front and the side.
✓ 怎麼確認:把同樣的四張人物照片丟給 Windows、Android(AYN Thor)、Linux 三個版本的姿勢模型,三邊都找到人,關節位置跟 Windows 差不到 0.1 像素(0.10.38 的驗證)。Kinect 本身則錄下真實的感測器資料,離線重播比對。How you know: the same four pictures of a person went through the pose model on Windows, Android (AYN Thor) and Linux; all three found the person, with joints within 0.1 pixel of Windows (0.10.38's validation). For the Kinect itself, real sensor data is recorded and replayed offline for comparison.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 79069752026-09-23 Find a body in a camera picture
  • 9fb41962026-09-27 Let a real Kinect play, and a webcam race with the body
  • 33583422026-09-27 Let a Kinect v2 play as well
  • 0a567392026-10-30 Kinect v1 on macOS through libfreenect: skeleton and player index from its cameras (#305)
STEP 08

打掉重練一半:改用 Unleashed/Marathon 的做法(v0.5.0)Rebuild half of it: the Unleashed/Marathon model (v0.5.0)

到 v0.4.x,遊戲能玩了,但在手機上只有大約 24 fps。原因是早期為了「安全、好除錯」,runtime 做得很保守:每次讀寫記憶體都要檢查、所有執行緒輪流拿同一張「通行證」才能跑、所有畫面都畫在同一張 720p 的畫布上。

2026-10-04 先把這個專案跟另外兩個成熟的 Recompiled 專案(Unleashed、Marathon)逐項對照,列出差異,再分五個階段改過去。每一階段都在同一支手機上、用同一個安裝包開關新功能,交替量 A/B/A/B,確定變快而且畫面正確才往下:

By v0.4.x the game was playable, but only about 24 fps on a phone. Early on the runtime had been built cautiously, for safety and easy debugging: every memory access was checked, all threads took turns holding a single "permit" to run, and everything was drawn on one 720p canvas.

On 2026-10-04 the project was compared, item by item, with two mature recompilations (Unleashed and Marathon), and the differences were moved over in five stages. Each stage was measured on the same phone with the same package, switching the change on and off in turn (A/B/A/B), and only went ahead once it was faster and the picture was right:

改之前before
25.0
每個畫面一張畫布one target per surface
29.6
記憶體直接讀寫direct memory
40.6
執行緒全部並行all threads parallel
46.4
v0.5.0 發布v0.5.0 release
49–50
AYN Thor 手機,同一段比賽的 fps(越長越好)。數字來自 docs/architecture-migration.md 和 v0.5.0 的發版說明。AYN Thor handheld, fps over the same stretch of a race (longer is better). From docs/architecture-migration.md and the v0.5.0 release notes.

順帶的好處:每個畫面有了自己的畫布之後,遊戲畫的影子第一次出現了,之前影子讀到的是記憶體裡的舊資料。

A bonus: once each surface had its own target, race shadows appeared for the first time. Before, the shadow map read stale bytes from memory.

! 不能照抄的地方:第 4 步那個「帶版本號」的做法,Unleashed 沒有,照抄就會讓這款遊戲當掉,所以保留。另外 v0.5.0 讓電腦版的比賽改成「照真實經過的時間前進」,結果 Rocky Ridge 兩旁集氣起跳會飛出場外;v0.5.3 改回「每次前進固定 1/60 秒」才正常。學別人的做法,也要用自己的遊戲驗證。What couldn't be copied: the "version number" from step 04. Unleashed doesn't need it, and copying Unleashed crashes this game, so it stayed. And v0.5.0 made desktop races step by real elapsed time; charged jumps off the sides of Rocky Ridge then flew out of the course, until v0.5.3 went back to a fixed 1/60-second step. Borrow other projects' methods, but check them on your own game.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 0263ac42026-10-04 Plan the move toward the Unleashed/Marathon runtime model
  • 3826e382026-10-04 Make the Unleashed/Marathon runtime model the default
  • e6d7cd22026-10-05 Step desktop races one frame at a time again, and whole frames near 60 fps
STEP 09

比原版多一點,但不改壞原版More than the original, without breaking it

移植的價值常在原版做不到的事。這個專案陸續加了:

  • 雙人:兩支手把各自是一位 Kinect 玩家(v0.1.5,之後 v0.6.1 讓 Relay Race 也能用手把)。
  • 網路對戰:原版的 Xbox LIVE 早就關了。v0.7.0 起,改連這個專案自己的伺服器,Windows、Linux、Android 可以一起比賽;伺服器可以由其中一台玩家的遊戲自己開。
  • Mod:讀取 HedgeModManager 格式的替換檔(Unleashed、Marathon 用的同一種),launcher 裡可以開關、排順序。繁體中文化 mod 就是用這個做的。
  • 自己的 VRM 模型:遊戲裡的 AVATAR 角色可以換成你自己的 3D 模型,動作仍由遊戲控制。
  • 成就(不需要 Xbox Live)、可調的內部解析度、超寬螢幕、觸控與傾斜轉彎。

原則是不偷改遊戲本身的行為。例如手機跑不到 60 fps 時,不是讓比賽每格多走一點(那會改變物理),而是照樣每格固定前進、少畫幾格來追上時間(第 11 步)。

A port earns its keep with things the original can't do. Over time this one added:

  • Two players: each controller is a Kinect player of its own (v0.1.5; v0.6.1 made Relay Race work with controllers too).
  • Online races: the original Xbox LIVE service closed long ago. From v0.7.0 the game connects to this project's own server instead, and Windows, Linux and Android players race together; one player's game can host the server itself.
  • Mods: file-replacement mods in HedgeModManager's format (the one Unleashed and Marathon use), switched on and ordered in the launcher. The Traditional Chinese translation is such a mod.
  • Your own VRM model: the in-game AVATAR rider can be your own 3D model, still animated by the game.
  • Achievements (no Xbox Live needed), adjustable internal resolution, ultrawide, touch controls and tilt steering.

The rule: don't quietly change how the game itself behaves. When a phone can't reach 60 fps, for example, the race doesn't take bigger steps (that would change the physics); it keeps its fixed step and leaves some frames undrawn to keep up with real time (step 11).

The same online race on a PC, a ROG Xbox Ally X and an AYN Thor
同一場網路比賽:電腦(主機)、ROG Xbox Ally X 和 AYN Thor。One online race on a PC (the host), a ROG Xbox Ally X and an AYN Thor.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 012a9f22026-09-27 2P support
  • 6fac24a2026-09-28 Integrate experimental VRM rendering for single-player Avatar
  • 482c45f2026-10-06 Load file-replacement mods in HedgeModManager's format, and manage them in the launcher
  • d92ac782026-10-06 Play the title's Xbox LIVE mode on this project's own server (work in progress)
STEP 10

搬到每個平台Bring it to every platform

翻出來的是 C++,所以每個平台用自己的編譯器就能編。但每個平台都有自己的坑,要實際在那裡跑過才算數:

The output is C++, so each platform builds it with 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桌機,D3D12 與 Vulkan 都跑整場比賽Desktop; a whole race on both D3D12 and Vulkan安裝在有非英文字母的資料夾就開不起來(v0.6.3 修好);Windows 10 需要另附 D3D12 執行檔It wouldn't start from a folder with non-Latin letters (fixed in v0.6.3); Windows 10 needs the D3D12 runtime shipped alongside
LinuxWSL 的 Ubuntu 22.04(Vulkan)Ubuntu 22.04 under WSL (Vulkan)沒有著色器翻譯器,只能用 Windows 翻好的 pack;pack 裡沒有的東西就畫不出來No shader translator there, so it relies on the pack from Windows; whatever the pack lacks can't be drawn
Android實體掌機 AYN ThorA real AYN Thor handheld兩個 APK(Android 9/10 以上);Mali 顯示晶片的手機開不起來(第 12 步的案例)Two APKs (Android 9 / 10 and later); phones with Mali GPUs wouldn't start (the case in step 12)
macOSMac mini M1,整場比賽Mac mini M1, whole races第一次在 Mac 上跑就找到三個當機;按下 Start 後顯示晶片出錯,要改成只在需要時重新綁定貼圖The first runs on a Mac found three crashes; a GPU fault after Start needed textures rebound only when a stage needs them
iOS只建置和打包Built and packaged only還沒在 iPhone 上實際測過,發版說明照實寫「未測試」Not yet tried on an iPhone; the release notes say "not tested", plainly
在專案裡的位置Where it is in the code
相關 commitCommits
  • 3e21d342026-09-22 macOS: Metal-layer window for MoltenVK, affinity shim; CI checks the build
  • fb5a7b02026-10-08 Read folders with letters outside the system code page on Windows
  • 436bcf62026-10-16 Fix three crashes found running on macOS
  • 33b39a02026-10-16 Build and package the game for macOS
  • c0dc0ee2026-10-16 Build and package the game for iOS
STEP 11

跑得更快:先量,再改,再證明Make it faster: measure, change, prove

第一次在電腦上跑完整場 Free Race(2026-09-21)時,只有 1.6 fps,一格要 620 毫秒。先量出時間花在哪裡:遊戲自己的計算只佔 53 毫秒,我們的繪圖部分吃掉九成。一項一項改完之後,同一天就到了大約 60 fps、幾乎跟真實時間一樣快。

手機是更大的挑戰。每次都用同一套方法:在實體掌機 AYN Thor 上跑同一段比賽,找出最花時間的部分,改它,再交替開關比對(A/B/A/B),確定真的變快、畫面也沒變。

The first whole Free Race on a PC (2026-09-21) ran at 1.6 fps, 620 milliseconds a frame. Measuring where the time went: the game's own work was 53 ms, and our drawing code took nine tenths. After fixing it piece by piece, the same day it reached about 60 fps, close to real time.

Phones were the bigger challenge. The method was the same each time: run the same stretch of a race on a real AYN Thor handheld, find the biggest cost, change it, then switch it on and off in turn (A/B/A/B) to make sure it really got faster and the picture didn't change.

v0.4.4
23–24
v0.5.0
49–50
v0.6.9
55–57
v0.10.38
60
AYN Thor(Snapdragon 8 Gen 2),比賽中的 fps(越長越好)。來自各版本的發版說明與 PR #90;量的路段不完全相同,看趨勢就好。AYN Thor (Snapdragon 8 Gen 2), fps in a race (longer is better). From each release's notes and PR #90; the stretches measured aren't identical, so read it as a trend.

其中一個改法的故事(v0.6.9):Android 把只是在「等」的繪圖執行緒放在唯一一顆最快的核心上,真正決定 fps 的主執行緒反而在較慢的核心。讓繪圖執行緒離開最快的核心之後,起跑線從 52 fps 變成 60 fps。

另一個(v0.9.0):手機跑不到 60 fps 時,原本讓比賽「每格多走一點時間」來保持速度,但這款遊戲的碰撞是照 1/60 秒設計的,步伐一大,跑得快的角色會從坡道側面穿出場外(issue #123)。改成每格固定前進、少畫幾格來追上時間之後,速度和物理都對了。

One change in detail (v0.6.9): Android put the render thread, which mostly waits, on the one fastest core, while the main thread, which sets the frame rate, ran on slower ones. Moving the render thread off the fastest core took the start line from 52 to 60 fps.

Another (v0.9.0): when a phone couldn't reach 60 fps, races used to take longer steps to keep their speed. But this game's collisions are made for 1/60-second steps; with longer steps a fast racer could pass out through the side of a ramp (issue #123). Keeping the fixed step and leaving some frames undrawn to catch up got both the speed and the physics right.

在專案裡的位置Where it is in the code
  • docs/performance.md電腦上的量測:從 1.6 fps 開始Desktop measurements, starting at 1.6 fps
  • docs/android-performance.md手機上的量測與「少畫幾格」的做法Phone measurements and the undrawn-frame approach
  • src/host_core_plan.h哪條執行緒放在哪顆核心Which thread goes on which core
  • docs/benchmarking.md怎麼跑可比較的量測How to run comparable measurements
  • PR 90v0.6.9 的 Thor 52 → 60 fpsThe v0.6.9 Thor change, 52 → 60 fps
相關 commitCommits
  • d62fb2b2026-10-09 Merge pull request #90 from YuutaTsubasa/claude/thor-60fps
  • 594d47a2026-10-15 Keep the race's fixed step on Android and skip drawing to keep its speed
  • 404dc4d2026-10-16 Leave most of the game's drawing out of frames that are not drawn
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 in plain words 重現不了?Can't reproduce?說明試過什麼再問say what was tried

這個流程是專案擁有者訂的規則。手機的問題沒辦法每台都買來測,所以會用 Google 的 Firebase Test Lab(雲端上的實體手機),或在電腦上故意套用那種手機的限制來重現。下面是一個完整的真實例子。

This loop is a rule set by the project owner. Nobody can buy every phone, so phone problems are reproduced on Google's Firebase Test Lab (real phones in the cloud), or on a PC with that phone's limits forced on. Here is a complete real case.

案例:「Mali 顯示晶片的手機開不起來」(issue #169)Case: "the game crashes on Mali-based phones" (issue #169)

  1. 回報(2026-10-17):一位玩家的兩支手機都開不起來:Pixel 10a(Mali-G715)出現載入畫面就關掉,P30 Pro(Mali-G76)連載入畫面都沒有。他附上了兩支手機的診斷檔。
  2. Pixel 10a 的原因:記錄顯示,顯示卡驅動在編譯第一個繪圖管線時就當掉。著色器裡寫了一個這款晶片自己說不支援的東西(clip distance)。在 Firebase 的 Pixel 10a 上,舊版當得一模一樣;拿掉之後 455 個管線全部編好、畫面正確。v0.10.2 修好。
  3. P30 Pro 的第一層:它的 Vulkan 少了兩個程式要求的擴充功能。查下去發現其實根本沒用到,就不再要求;後面又接連補上三件舊款 Mali 需要的事。v0.10.3。
  4. 玩家回報還是不行:管線準備到最後 5 個就整台手機卡住。他附的記錄顯示第一次畫圖時顯示卡就掛了(device lost)。
  5. 真正的原因:每次畫圖都宣告 20 個頂點輸入,但這代 Mali 最多只支援 16 個。改成只宣告著色器真正用到的(最多 14 個)。在 Firebase 上最接近的舊款 Mali(Galaxy S9,Mali-G72)加一個新測試:455 個管線每個都畫一次。修正打開全部正確,關掉就跟玩家一樣卡死。v0.10.4,同一天發出。
  6. 玩家確認:P30 Pro 終於進得了遊戲。之後他用新版跑了一場 Magma Rift:畫面 20 fps,但比賽時間跟真實時間一致(0.998),瓶頸在顯示晶片,調低 launcher 的 Render size 最有效。問題關閉,速度另外在 issue #176 追。
  1. The report (2026-10-17): a player's two phones wouldn't start the game. The Pixel 10a (Mali-G715) closed at the loading screen; the P30 Pro (Mali-G76) didn't even get that far. They attached diagnostics from both.
  2. The Pixel 10a: the log showed the graphics driver crashing while compiling the first pipeline. The shaders wrote something (a clip distance) this chip itself says it doesn't support. On Firebase's Pixel 10a the old build crashed the same way; without it, all 455 pipelines compiled and frames drew correctly. Fixed in v0.10.2.
  3. The P30 Pro, first layer: its Vulkan lacked two extensions the renderer required. They turned out to be unused, so they're no longer required; three more things old Mali GPUs need came after. v0.10.3.
  4. The player said it still failed: preparing pipelines, the whole phone froze on the last 5. Their log showed the GPU lost on the very first draw.
  5. The real cause: every draw declared 20 vertex inputs, but these Mali GPUs support 16. Now each shader declares only the inputs it reads (14 at most). On Firebase's closest old Mali (a Galaxy S9, Mali-G72), a new test draws once with each of the 455 pipelines: all correct with the fix, and a hang like the player's without it. v0.10.4, the same day.
  6. The player confirmed: the P30 Pro got into the game. Later they ran a Magma Rift race on a newer build: 20 fps shown, but the race kept real time (0.998); the bottleneck is the GPU, so a lower Render size in the launcher helps most. Closed, with speed followed in issue #176.
? 注意第 4 點:修好一層,不代表修好了。玩家再試、再附記錄,才找到第二層真正的原因。每一層都要「舊版重現、新版正確」兩邊都看到,才算數。Note point 4: fixing one layer isn't the finish line. Only the player's next try and next log revealed the real cause underneath. Each layer counts only when the old build reproduces it and the new one doesn't.
在專案裡的位置Where it is in the code
相關 commitCommits
  • 5335c492026-10-18 Compile the vertex shaders without their clip distance on Mali
  • 89564942026-10-18 Run Vulkan without scalar block layout and buffer device addresses
  • 9d6fcfe2026-10-18 Fall back to a D24S8 depth format where D32S8 is missing
  • 28f5c0d2026-10-18 Declare only the vertex inputs a shader reads where a GPU has 16
STEP 13

發版前的檢查清單The release checklist

每個版本都照這張清單走,任何一項沒過就不發(以 0.10.38 為例):

Every release goes through this list; if any item fails, it doesn't ship (0.10.38 as the example):

1改版號的 PR,GitHub 上 5 項 CI 全綠(Windows、Linux、macOS、Android、No game data)A version PR with all 5 CI jobs green (Windows, Linux, macOS, Android, No game data)
2單元測試:Windows 183 項、Linux 175 項全過Unit tests: 183 on Windows and 175 on Linux, all passing
3Windows 包解壓到乾淨的資料夾,用 Vulkan 加上驗證層跑完整場比賽:沒有任何錯誤,455 個管線全部準備好The Windows package, unpacked into a clean folder, runs a whole race on Vulkan with the validation layer: no errors, all 455 pipelines prepared
4Mac(M1)的包跑完整場比賽,速度 1.000The Mac (M1) package runs a whole race at full speed (1.000)
5AYN Thor 實機:預設設定、觸控加傾斜、執行緒擠在一顆核心,三種情況都跑比賽並記下速度A real AYN Thor: default settings, touch plus tilt, and threads squeezed onto one core, each racing with its speed recorded
6APK 用同一把金鑰簽章,能直接覆蓋安裝舊版APKs signed with the same key, so they install over the previous version
7發版說明寫清楚驗證了什麼,也寫清楚沒測到什麼Release notes that say what was verified, and what wasn't
8發版之後才回覆 issue:先用一般人聽得懂的話說明,再附開發細節Only after the release, reply on the issues: plain words first, developer detail after
在專案裡的位置Where it is in the code
ABOUT AI

AI 在這裡做了什麼,人做了什麼What AI did here, and what people did

直接說清楚:這個專案大部分的程式碼由 AI 撰寫,主要是 Anthropic 的 Claude,也有一部分是 OpenAI 的 Codex。879 個 commit 裡有 508 個標註 Claude 為共同作者;合併的 235 個 PR 裡,205 個來自 Claude 的分支、18 個來自 Codex 的分支。紀錄都在 git 裡。

「AI 寫的」不等於「垃圾」,也不等於「好」。決定品質的是能不能證明它是對的。這份文件每一步都有「怎麼確認」,那才是重點。

To be plain about it: most of this project's code was written by AI, mainly Anthropic's Claude, with some by OpenAI's Codex. 508 of its 879 commits name Claude as co-author; of the 235 merged PRs, 205 came from Claude's branches and 18 from Codex's. The 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
  • 定方向和規則:先重現再修、不准繞過問題、截圖必須是真的、發版說明要寫沒測到什麼
  • 擁有光碟、測試用的電腦、Mac 和掌機,實際玩、實際測
  • 決定什麼時候合併、什麼時候發版
  • 協作者 KnucklesLee:95 個 commit,實體 Kinect 與 Kinect v2 的支援,以及好幾份詳細的效能量測報告
  • 玩家回報問題:71 個 issue,65 個來自兩位開發者以外的人,已關閉 52 個
  • Set the direction and the rules: reproduce before fixing, no workarounds, real screenshots only, release notes that say what wasn't tested
  • Own the disc, the test PCs, the Mac and the handheld; actually play and test
  • Decide when to merge and when to release
  • Collaborator KnucklesLee: 95 commits, the real Kinect and Kinect v2 support, and several detailed performance reports
  • Players report problems: 71 issues, 65 of them from people other than the two developers, 52 closed
AI
  • 寫翻譯流程的工具、runtime、繪圖、Kinect 與攝影機、launcher
  • 寫測試,並在每次修改後跑完整驗證
  • 重現問題、量測、查原因、寫說明文件(docs/ 裡 133 份)
  • 照清單建置各平台、在掌機上測試、發版、回覆玩家
  • Wrote the translation tooling, the runtime, graphics, Kinect and camera input, and the launcher
  • Wrote the tests and ran the full verification after every change
  • Reproduced problems, measured, traced causes, wrote the docs (133 in docs/)
  • Built every platform from the checklist, tested on the handheld, released, replied to players

不管是誰寫的,你都可以直接檢查:程式碼、每個版本的驗證結果、每個 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

實作紀錄開始Work log starts2026-09-10
公開 repo 第一個 commit,與第一個能玩的版本(0.1.0)First public commit, and first playable release (0.1.0)2026-09-22
版本數(到 0.10.38)Releases (to 0.10.38)86
Commit 數Commits879
合併的 PRMerged PRs235
程式碼(src、tests、scripts、tools、server、android)Code (src, tests, scripts, tools, server, android)≈ 137,000 lines
單元測試(0.10.38)Unit tests (0.10.38)183 Windows · 175 Linux
遊戲向主機借用的系統函式System functions the game imports299 + 13 variables
開始前準備的繪圖管線Pipelines prepared before play455
AYN Thor 比賽 fpsAYN Thor race fps23–24 → 60
說明文件Docs133
THE CODE TODAY

目前的程式碼在這裡Where the code is today

上面的連結都固定在 commit eb16749(0.10.38 之後、Mac Kinect 合併時的 main),之後程式碼改了也不會跑掉。最新的程式碼請看:

The links above are pinned to commit eb16749 (main after 0.10.38, when the Mac Kinect work was merged), so they don't drift as the code changes. For the latest code: