| name | retro-directdraw-hires-cjk |
| description | 把「只有 EXE、無原始碼」的閉源 DirectDraw exclusive-fullscreen 老遊戲(1990s Win95/98)拉高內部畫布解析度,讓 CJK 中文能用 24×24 點陣清晰顯示(而非被 8×8 bitmap font 塞成亂碼)。用 RE + binary patch 實現,非改常數。當使用者談到「老遊戲中文糊掉/亂碼」「8×8 點陣字塞不下中文」「DirectDraw 老遊戲拉高解析度」「拉高畫布 CJK」「SetDisplayMode patch」「present 放大」「閉源 EXE 中文化字型太小」等情境觸發。方法論實證於 Pacific General (SSI 1997) 進行中。 |
retro-directdraw-hires-cjk — 閉源 DirectDraw 老遊戲拉高畫布支援 CJK
⚠️ 本 skill 是方法論框架。 具體遊戲的「present 走 Blt 還是 Lock+copy」「哪個 VA 是 present」「改幾處讓畫面放大」等結論,一律要在乾淨、可靠的 wine + 截圖環境當場做出來,不要照抄任何「先前得出的具體答案」。方法可靠,結論要現做現驗。
這個 skill 解決什麼(跟其他的區隔)
| 資源 | 場景 | 手法 |
|---|
| 本 skill | 閉源 EXE、DirectDraw exclusive fullscreen、8×8 bitmap font 塞不下中文 | RE + binary patch 拉高畫布 + font 高解析 |
rule 81-retro-cjk-hires-canvas | 有原始碼的 remake (openxcom / freesynd / 1oom 皆開源) | 改 SCREEN_W/H 常數 + nearest scale |
skill panzer-general-wine | 老遊戲在 wine 下啟動(256 色、exNilPtr) | wine prefix + shim.dll + registry |
skill art-dat-bitmap-cht | UI 文字烤在點陣圖檔裡 (ART.DAT) | 解碼/重繪/回填 RLE 點陣圖 |
核心區隔:rule 81 是「有源碼改常數」的通則;本 skill 是它在沒有源碼、只有 EXE時的落地 —— 拉高畫布的每一個常數/呼叫點都要反組譯定位 + hex patch,而非改一行 #define。
為什麼需要拉高畫布(第一性原理)
老 DirectDraw 遊戲的字型常是 8×8 單 byte 點陣(TFONT1.DAT 之類)。第一性事實:
- 8×8 格子物理上塞不下可讀中文。中文平均 10+ 筆畫,8×8=64 pixel 連「灣」「鑫」都糊成黑塊;拉丁字母筆畫少,8×8 剛好。
- single-byte 定址 256 slot 裝不下常用中文。
- 就算改成 Big5 2-byte lookup,8×8 仍糊 —— 治了「找得到字」治不了「看得清字」。
- 判斷遊戲是否零 GDI 繪字:查 import 有無
TextOutA/DrawTextA/CreateFontA。全無 = 所有文字走自訂 bitmap font,wine 字型代換無效,只能拉高畫布。
正解(rule 81 的維度轉換):拉高內部畫布,底圖 pixel art 用 nearest 放大保持銳利,CJK 用 24×24 畫在放大後的畫布。
font 格式逆向(純靜態,可靠)
自訂點陣字型格式解剖(以 Pacific General TFONT1.DAT 為例,靜態確認):
Header 16 bytes: 版本字串 / glyph 數(256) / glyph 高(8) / 最大 index(255)
Offset table (0x10 起, 256 × 4 bytes): glyph[ch] 資料 offset = read_u32(0x10 + ch*4)
Glyph 資料: [4 bytes width][width × height bytes bitmap, 1 byte/pixel 0x00背景/0xff前景]
render 驗證:取一個 ASCII glyph 逐 row 印 #/.,應看到可辨字形 → 確認格式理解正確,也確認 height(常 8)是硬牆。這是純靜態 RE,可靠。
RE 流程(閉源 EXE,依序做 —— 方法可靠,結論當場驗)
Step 1:靜態定位 SetDisplayMode
DirectDraw 設解析度走 IDirectDraw::SetDisplayMode(w, h, bpp),vtable offset 0x54。靜態找 push bpp; push height; push width; call [vtable+0x54],通常唯一呼叫點。搜 height/width 立即數(如 640=0x280 / 480=0x1e0)交叉定位。這步純靜態,可靠。
Step 2:改 mode 是否 crash(生死線,當場驗)
fresh source hardlink + patch 兩個 push 到目標解析度,跑遊戲:
- 不 crash → A1 可行,繼續
- crash → surface 建立跟 mode 綁太死,A1 風險高
⚠ 這步要看遊戲跑起來,當場用可靠環境驗,別信任任何「先前說不 crash」的結論。
Step 3:動態確認 present 機制(關鍵,別靠靜態猜 vtable)
靜態猜 vtable offset 會出錯(0x14 在不同 COM 介面是 Blt / SetEntries / 別的)。用動態:
WINEDEBUG=+ddraw wine explorer /desktop=X,WxH ./GAME.EXE >log 2>&1
grep -a 'surface1_Blt' log | grep -av null
grep -a 'SetDisplayMode\|CreateSurface' log
grep -ac 'Lock' log
判讀:每幀穩定重複 Blt dst_rect(0,0)-(w-1,h-1) src=back_buffer flags DDBLT_WAIT = present 走 DirectDraw Blt(可能 StretchBlt);大量 Lock/Unlock = 可能 software copy(改 stride,較難)。⚠ 這是當場 trace 判讀的方法,不是預設答案。
Step 4:找 present 尺寸來源
若 present 走 Blt,用 DDBLT_WAIT(flags 0x1000000)當靜態指紋:
import re
for m in re.finditer(rb'\x68\x00\x00\x00\x01', exe):
print(hex(m.start()-0x400+0x401000))
往上找建 RECT{0,0,w,h} 的 stack mov + call [reg+0x14](Blt vtable);再往上找 caller,present 尺寸常是 caller push 的立即數。定位後再用動態 trace 交叉確認才算數。
放大手法(依 present 機制選,當場驗證每一步)
若 present 走 DirectDraw Blt
概念:SetDisplayMode → 目標解析度 + present blit 的 dst 尺寸 → 目標解析度,src 保持原尺寸 → DirectDraw 自動 StretchBlt(nearest = crisp)。
⚠ dst/src rect 共用陷阱:若 present Blt 的 dst_rect 與 src_rect 指向同一個 RECT(lea eax,[rect]; push 兩次同位址),直接改 RECT 會讓 src 也變大 → 從小 back buffer 讀越界 → 畫面壞。解法擇一,每個都要當場截圖驗:
- 改 caller push 的尺寸(若 present 用傳入尺寸建 dst,src 另從 surface desc 讀)
- dst_rect 設 NULL(用整個 dst surface)+ src_rect 保留小尺寸;但 wine 某些版本 dst=NULL+src_rect 可能不 stretch 只放左上,須實測
- 分離兩個 RECT(EXE stack 空間常不夠放第二個 16-byte RECT)
present 多層陷阱:present 常是多層(render buffer → 中間 surface → primary)。改錯層只修好 stride mismatch 不放大。確認你改的 Blt 的 dst 是 primary(對照 CreateSurface 的 DDSCAPS_PRIMARYSURFACE 全域)。
若 present 走 Lock + software copy
改 copy loop:用 Lock 回傳的實際 lPitch(surface desc)當 dst stride + nearest ×2(每 src pixel 寫 dst 2×2),而非硬編舊寬度。較繁瑣但可控。
font 24×24(中文清晰的關鍵,最重)
放大後 font 仍是舊尺寸(8×8 → 放大糊)。要清晰,font glyph 必須在高解析層以 24×24 畫:
- 確認 glyph 尺寸來源:繪字管線讀 font header height(可改)還是硬編(反組譯繪字核心)
- font atlas 換 24×24(TTF → 點陣 subset,見
build_cjk_font.py)
- 繪字 loop patch:認新 glyph 高 + Big5 lead byte(0x81-0xFE)偵測 → 2-byte lookup;ASCII 走原路徑
- 座標:font 24×24 畫在已放大的高解析畫布
三條整合路徑:
- A(rule 81 標準):back buffer 本身拉到高解析、底圖 blit nearest ×2、font 直接 24×24。工程大。
- B(font 半尺寸):font 在原 back buffer 畫 12×12,經放大 ×2 → 24×24 顯示。省力但 UI 一行字數變少、排版會擠。
- C(不動畫布,只加高 font + 2-byte hook)—— 最輕,已在 Pacific General 實證 ✅:
完全不碰 DirectDraw/present。只要繪字管線的字高是 memory-read(
[font+8] 之類)就把它從 8 升到 16,
中文 16×16(不到 24×24 但已清晰可讀)。省掉 A1 全部的 present RE / 底圖 blit / 座標映射。步驟:
- 確認字高 memory-read:反組譯
drawGlyph 找 mov reg,[font+8] 當 blit row 數 → 改 font header 即生效,EXE 不動。
- atlas 做成與原
drawGlyph 相容的 mini-TFONT(header height=16 + offset table + [width][px] glyph)→
hook 偵測 lead byte 後直接 call 原 drawGlyph 傳 font=atlas, ch=dense,復用原 blit,不自己重寫像素搬移。
- 自訂 dense 2-byte 編碼(非裸 Big5):
dense=(lead-LEAD0)*W+(trail-TRAIL0),兩 byte 皆 ≥0x80、
hook 純算術無查表 → 塞得進任何小 code cave。字串本來就從 glossary 重產,重編碼零成本。
- 無 code cave 就追加 PE 節:
.text 常無 ≥16B 空段;新增一個 .cjk(CODE|EXEC|READ)裝 stub+atlas,
繪字迴圈起點覆寫成 jmp stub。固定 base EXE(preferred base)→ 絕對立即數免 reloc。
- stub 邏輯:重讀字元;lead(如 0x81-0x86)→ 算 dense、call drawGlyph(atlas)、
esi+=2;否則走原 ASCII 路徑 esi+=1;都跳回迴圈終止測試。一處 hook 涵蓋該 drawString 的全部呼叫點。
何時選 C 而非 A:只要「中文可讀」是目標、底圖不需放大,C 的工程量是 A 的零頭(無 present RE)。多行 word-wrap 文字走另一個模組時需各自加同樣的 hook。
關鍵陷阱(RE 紀律)
- wine session 髒污誤導 RE:多輪 patch/revert/kill 後 wineserver 累積髒狀態,連 baseline 都偶發 crash → 誤判成檔案問題。每次 fresh source hardlink(
cp -al)+ wineserver -k + fresh 目錄才可信。
- 不穩環境的「成果」不算數:若 tool 輸出出現污染(命令結果自我重複、pid/log 讀數矛盾)、或截圖無法可靠判讀,當下所有「跑起來看畫面」的結論一律作廢,換乾淨環境重做。這是本 skill 的第一守則。
page fault @ ASCII-looking 位址(如 0x54484320=" CHT")= 隨機記憶體垃圾,別追字面 pattern。
- 靜態 vtable offset 歧義 → 動態
+ddraw trace 破。
- 原版 EXE 絕不就地改:實驗全在
/tmp 或 ~/ 的 fresh hardlink dir,原版留底。/tmp 會被自動清,要保留的截圖/log 存 repo 或 ~/。
工具與手法
- capstone(
pip install capstone)掃指紋:push 0x1000000(DDBLT_WAIT)、call [reg+0x14](Blt)、SetDisplayMode 立即數、push 0x280/0x1e0(640/480)
- objdump
-d -M intel -m i386 -j .text --start-address=VA 反組譯
- WINEDEBUG=+ddraw 動態確認 present(靜態撞牆時對症)
- Xvfb / host :N + explorer /desktop wrapper 隔離跑 + 截圖(避免搶主機解析度)
- VA↔file:image base 0x400000、.text VA 0x1000 raw 0x400 →
file = VA - 0x401000 + 0x400
何時套用 / 何時不
套用:閉源 DirectDraw exclusive-fullscreen 老遊戲、8bpp paletted、自訂 bitmap font、中文塞不下、無源碼。
不套用:有源碼的 remake(用 rule 81)、走 GDI TextOut(wine 字型代換)、文字烤在點陣圖(art-dat-bitmap-cht)、只要「啟動」不是「拉高」(panzer-general-wine)。
Reference case
- Pacific General (SSI/Mindscape 1997) — 本 skill 方法論來源。路徑 C(不動畫布 + 2-byte hook)已實機成功 ✅
- 繪字管線 RE(靜態,可靠):
drawString@0x428312 逐 byte 迴圈、drawGlyph@0x42817f 查表+blit、
字高 [font+8] memory-read(改 font 檔即 16px,EXE 不動)、hook 點 0x428322、.text 無 code cave。
完整見 pacgen/docs/11-2byte-engine.md。
- 落地:自訂 dense 2-byte 編碼 + atlas 做成 drawGlyph 相容 mini-TFONT(531 字)+ 追加
.cjk PE 節(98B stub)
- 繪字迴圈起點
jmp stub。實機:主選單「開始戰役/開始劇本/離開」、劇本標題與選擇畫面簡報全中文清晰
(pacgen/docs/2byte-hook-SUCCESS-scenario.png)。原版 EXE 僅被動 5 byte。
- A1 拉高畫布仍是「要底圖同步放大 / 24×24 更清晰」時的選項,但中文可讀的最低成本是路徑 C,不必先做 A1。
A1 的 present 機制須在乾淨環境當場動態驗(先前不穩環境的「present stretch」結論已作廢)。
- 待做:word-wrap 模組(自走 byte、直接 call drawGlyph)第二 hook → 多行簡報/對話框中文。
- 配套:rule
81-retro-cjk-hires-canvas、skill panzer-general-wine、retro-game-remake、字型烘製 build_cjk_font.py。