專案 GitHub:boykopovar/AnyPS5
重點
- 不模擬 CPU:PS5 和一般 PC 一樣是 x86-64,AnyPS5 把遊戲執行檔「重新連結」成 Linux 的 ELF 或 Windows 的 PE,讓它以普通行程執行,沒有另外的模擬器行程。
- 讓作業系統幫忙連結:自寫的系統函式庫編好後,把匯出符號改名成 PS5 用的 NID;執行檔的匯入表也改成標準格式,宿主系統的載入器就會「照名字」把兩邊綁在一起。
- 著色器重編成 SPIR-V:AMD RDNA 機器碼經過解碼、控制流程結構化、中間表示與最佳化,輸出 Vulkan 能吃的 SPIR-V,結果存在記憶體與磁碟。
- 用 pipeline library 減少卡頓:繪圖管線拆成四塊分別建置與重用,先快速連結讓畫面不等,後台再做最佳化版本替換。
- 非常早期,法律上也敏感:最新版 v0.1.1,遇到不支援的狀態直接丟例外結束。專案不附韌體或金鑰,本文只談工程設計。
先說清楚
這類把主機遊戲搬到 PC 的工具處在法律灰色地帶。AnyPS5 的 README 說專案用於互通、研究、保存與相容性,不包含、不散布也不需要受著作權保護的軟體、韌體、加密金鑰或專有函式庫,使用者要自行確認來源與用途合法。程式也還在很早期的階段。這篇文章只說明它的程式架構,不涉及如何取得或執行遊戲,也不涉及任何保護機制。
以 2026 年 10 月 13 日從 GitHub API 查到的資料,AnyPS5 在 2026 年 8 月 4 日(台北時間)建立,有約 32,955 顆星、651 個開啟中的 issue,授權是 GPL-2.0(README 註明 only 版),最新版本是 9 月 29 日發布的 v0.1.1。程式以 C++ 和 CMake 撰寫,繪圖用 Vulkan。
內容根據 README、docs/dev/ARCHITECTURE.md,以及 core/libs/nid/src/NidCompute.cpp 等原始碼。
先認識幾個名詞
- ELF 與 PE:分別是 Linux 和 Windows 的執行檔格式。裡頭除了程式碼,還記錄「這個程式需要哪些外部函式」的匯入表。PS5 的執行檔也是 ELF,只是匯入的寫法和一般 Linux 不同。
- 動態連結與載入器:程式啟動時,作業系統的載入器(Linux 的
ld.so、Windows loader)依匯入表上的名字,去共用函式庫(.so、.dll)裡找對應的函式位址填進去。 - HLE(high-level emulation):不模擬硬體細節,直接用宿主機器上的程式重新實作系統函式。AnyPS5 自寫的系統函式庫放在
core/libs/prx,prx 是 PlayStation 對系統模組的叫法。 - RDNA:AMD 的 GPU 架構。PS5 的著色器(在 GPU 上跑的小程式)以 RDNA 機器碼存在遊戲裡。
- SPIR-V:Vulkan 使用的著色器中間格式,由顯示卡驅動程式再編譯成自家 GPU 的機器碼。
整體流程
flowchart LR
elf["PS5 執行檔"] --> relinker["relinker:改匯入表、轉成 ELF/PE"]
relinker --> app["原生執行檔"]
prx["core/libs/prx 自寫系統函式庫"] --> nid["nid_patcher:匯出符號改名成 NID"]
nid --> libs["libs/*.prx"]
app -- "宿主載入器依 NID 綁定" --> libs
app -- "執行時送出著色器" --> gpu["RDNA → SPIR-V → Vulkan"]
架構文件開宗明義寫著:沒有任何東西被模擬,轉換後的執行檔以普通行程執行,呼叫系統函式庫的原生實作。整個工具有兩半:
- relinker(
core/relinker):讀出執行檔的匯入、檢查系統呼叫、過濾用不到的匯入,建立標準的 SysV 動態區段,最後由LinuxElfPatcher或WindowsPePatcher輸出。遊戲附帶的模組也一併轉換。 - 系統函式庫(
core/libs/prx):每個函式庫編成共用函式庫,編完後交給nid_patcher改名。
關鍵技巧:把符號改名成 NID
PS5 的執行檔匯入系統函式時,不寫函式名稱,寫的是一個 11 字元的雜湊值,稱為 NID。NidCompute.cpp 的算法如下:
- 函式名稱的位元組後面接上一段固定的 16 位元組;
- 算 SHA-1;
- 取前 8 個位元組,順序反過來;
- 用一種把
/換成-的 Base64 字母表編碼,取 11 個字元。
// core/libs/nid/src/NidCompute.cpp(節錄)
const auto digest = Sha1(input); // symbolName + 16-byte suffix
uint8_t reversed[8];
for (int i = 0; i < 8; i++)
reversed[i] = digest[7 - i];
常見的模擬器做法,是自己寫一個載入器:讀遊戲的匯入表、逐一查 NID、把位址指到自己的 HLE 函式。AnyPS5 把方向反過來:讓自寫的函式庫本身就以 NID 為名匯出,再把遊戲的匯入表改成 Linux 或 Windows 看得懂的格式。兩邊的名字一致了,宿主系統原本的載入器就能完成綁定,不需要額外的執行期。函式名稱未知、只知道 NID 時,可以用 APS5_EXPORT("<nid>", func) 直接指定。
CPU 指令本身不必翻譯,因為 PS5 就是 x86-64。唯一的例外是 AMD 專有的指令,選用的 --to-intel 步驟會把它們改寫成 Intel CPU 也能執行的寫法。
著色器:從 RDNA 機器碼到 SPIR-V
CPU 那一半可以直接跑,GPU 就不行了。遊戲送給 GPU 的是 RDNA 機器碼和一串命令緩衝區,PC 上的 Vulkan 只接受 SPIR-V。core/shader/recompiler/Recompiler.cpp 依序跑這些階段:
- RdnaDecoder:把機器碼解成指令;
- ControlFlow:建控制流程圖並結構化。機器碼是一堆跳轉,SPIR-V 要求有結構的 if 與迴圈,這一步要把前者整理成後者;
- Translation:RDNA 指令轉成中間表示(IR);
- Optimization:轉成 SSA 形式,整理資源與綁定;
- SpirvBackend:輸出 SPIR-V。編譯時開啟
ANYPS5_ENABLE_SPIRV_TOOLS會再用 SPIRV-Tools 驗證與最佳化。
編好的結果放在記憶體,ShaderDiskCache 也存到磁碟,下次執行可以重用。
幾個工程上的取捨:
- 錯誤留在原地,重活丟到後台。遊戲登記著色器時,驗證暫存器、解碼、建立重編譯請求都在登記的那個執行緒同步完成,無效的登記就在那裡報錯。RDNA 到 IR 的翻譯和資源規劃交給後台執行緒池,大小是 CPU 執行緒數的一半(至少 2 個)。用到結果的地方會先等後台工作完成。
- 共用記憶體不夠時改用儲存緩衝區。運算著色器用的 LDS(GPU 上同一組執行緒共用的快速記憶體)如果超過宿主顯示卡的上限,就改放在 descriptor set 1 的一塊 storage buffer,每個 workgroup 分一段。放得下的程式照舊用共用記憶體。
- 執行時才知道呼叫誰。有些運算著色器用
s_swappc_b64呼叫一個來自使用者資料的指標,子程式位址要等到派發時才確定。ShaderRegistry.cpp到那時才從 guest 記憶體讀出子程式,接在呼叫者後面一起編譯;不同的子程式各自有一份編好的版本。
繪圖管線:拆成四塊來重用
Vulkan 的「繪圖管線」(pipeline)把著色器和一大堆狀態(頂點格式、混色方式、輸出格式……)編在一起,建一個要花不少時間。主機遊戲在 PC 上常見的卡頓,很多就是第一次遇到某種組合時現場建管線造成的。狀態的組合數量又很多,全部預先建好也不實際。
AnyPS5 在顯示卡支援 VK_EXT_graphics_pipeline_library(且支援快速連結)、VK_EXT_extended_dynamic_state 和 VK_KHR_dynamic_rendering 時,把管線拆成四個 library:
| library | 內容 |
|---|---|
| vertex input | 頂點資料的格式 |
| pre-rasterization shaders | 頂點著色器等光柵化前的階段 |
| fragment shader | 像素著色器 |
| fragment output | 輸出目標格式與混色 |
每塊依裝置快取。下一個管線只差混色或頂點格式時,只需要重建那一塊;剔除方向、正面方向、深度與模板狀態設成動態狀態,改了也只要重新連結。繪圖呼叫只等「快速連結」完成,後台執行緒再用連結時最佳化(LTO)重新連一次,下一次繪圖起換成最佳化過的版本。
還有一個小設計:push constant(每次繪圖前快速寫給著色器的一小塊資料)用 256 位元組,切成兩個 128 位元組的區段。光柵化前的階段從第 0 個位元組開始放,像素著色器固定從第 128 個位元組開始。這樣像素著色器的 library 不必管頂點著色器用了多少空間,可以單獨建、單獨重用。
不支援這些擴充的顯示卡(或設定 APS5_NO_GPL=1)退回傳統的 render pass 與整個管線。幾種特殊情況,例如幾何著色器、rect list,也一律建整個管線。
其他看得出功夫的地方
- 執行緒取消語意:
libkernel的 pthread 取消行為以 FreeBSD libthr 的特定 commit 為參考規格,文件也註明這是參考語意,沒有在主機上量測過。 - 未追蹤記憶體的比對快取:
Texture.cpp的compareUntracked對沒有寫入監看的記憶體,用「排序點 epoch、最新寫入戳記、記憶體釋放序號、待處理的 GPU 寫入」組成戳記,任一項變了才重新比對。 - 沒把握就停:README 寫明,遇到不支援或非預期的狀態一律丟出
std::runtime_error,印出訊息後結束行程。著色器的 MODE 暫存器也是如此,只接受不改變初始捨入模式的寫入,其他寫法直接報錯並附上指令位址。
跟常見做法比
像 PS4 模擬器 shadPS4 這類專案,由模擬器自己的載入器載入遊戲,再把系統呼叫導向 HLE 實作(AnyPS5 v0.1.1 的更新紀錄裡也有移植 shadPS4 字型模組的 PR)。AnyPS5 把連結這一步提前到轉換時完成,產出一個宿主系統看來完全正常的執行檔。好處是沒有中間層,除錯工具、效能分析器都能直接使用;代價是所有函式都要以宿主系統的呼叫慣例、執行緒模型重新實作,遇到系統呼叫也要在 relinker 階段就處理。
限制與成熟度
- 非常早期:最新版本是 v0.1.1。README 的系統函式庫進度徽章計算的是「專案目前已知的函式」,分母會隨著宣告更多函式而變大,無法代表對全部 PS5 系統函式的涵蓋率。
- 遇到未支援功能就結束:這個策略讓問題容易定位,也表示大多數情況下還跑不了太遠。
- 技術債公開列表:
docs/dev/TechnicalDebt.md列出尚未完成的部分,架構文件提到的包括非同步取消與其他硬體暫存器的寫入。 - 法律風險:見文章開頭的說明。
- 本文沒有實測:以上根據文件與原始碼整理,沒有編譯或執行。