|
|
本帖最後由 good7 於 2026-7-8 10:31 編輯
Geargrafx 1.7.14 繁/簡/英文版
這支程式也可以使用 Reshade 著色器,以後不用怕著色器(效果)太少了。
這支程式是用 Open GL 渲染器,選擇的時候要注意一下。
這個正式版有三個功能改變,
第一個在主選單的「除錯」選項,這個一般人很少用到,所以有了選擇可以把它隱藏起來了。
第二、三個功能就是「啟動時載入上次的遊戲」,和「啟用程式睡眠」功能。這兩個功能跟 supersnes9x 的功能一樣,不用多說了。
如果在執行檔所在資料夾放上空檔的 portable.ini 文字檔案,則變成 portable 版本(免安裝的綠色版本),所有的操作都會放在程式檔所在資料夾下,如果沒有的話,
就是會放在
C:\Users\你的使用者名稱\AppData\Roaming\Geardome\Geargrafx
下載:
https://u.pcloud.link/publink/sh ... Auoec4HWfcSyjK80TWy
以下是動過的記錄(應該都有記錄了)
================================================================================
Geargrafx 問題點與修復記錄(整理後)
對象專案:z:\g1714(Geargrafx 模擬器 v1.7.14)
整理日期:2026-07-06
================================================================================
本檔案分兩大部分:
第一部分:效能最佳化與模擬正確性 Bug 修復(迭代 1-10,2026-07-05)
第二部分:穩定性/安全性修復(2026-07-06)
================================================================================
第一部分
效能最佳化與模擬正確性 Bug 修復(迭代 1-10)
================================================================================
所有最佳化均嚴守「可數學證明位元等價」原則,不動 SaveState 格式、
ReadVRAM 副作用、latch 時序、SSE2/AVX2 解碼本體。Bug 修復均對照
Mednafen 參考實作確認。
所有 bug 候選經多次迭代驗證,最終判定如下:
已修(對齊 Mednafen):Timer 0x7F、blur latch、ApplyLowPassFilter
round/clamp、MED-2、MED-3
已撤除(非 bug) :LENR underflow(回滾)、HIGH-2、HW-2、HW-3、
HW-4、MED-1、MED-4、MED-5、MED-6、m_hsync 命名
重要教訓:第 6 迭代曾將 LENR underflow 誤判為 bug 修成 0x0000,經
Mednafen 對照確認 DMA 完成時 LENR 就是 0xFFFF(u16 underflow 作完成
偵測)。結論:判斷疑似 bug 必須對照參考實作,不能只憑直覺。
================================================================================
第一部分 §1 效能最佳化(A 類,6 項全完成)
================================================================================
------------------------------------------------------------------------
[1] RenderBackground() — 像素提取 LUT 最佳化
------------------------------------------------------------------------
檔案:huc6270.cpp RenderBackground(第 699 行起)
問題(熱點排行 #1):
內層迴圈每 pixel 執行 4 次 variable-SHR(無法常數折疊),同一個 tile
內的 byte1~byte4 固定不變卻被重複解算 8 次。每 pixel 約 15 ops。
對策:像素 LUT 預解
在 tile 邊界(if tile_col != prev_tile_col)內,預先解算 8 個像素值
存入 u8 pixels[8]:
pixels[bit] = ((byte1 >> (7-bit)) & 1) | (((byte2 >> (7-bit)) & 1) << 1)
| (((byte3 >> (7-bit)) & 1) << 2) | (((byte4 >> (7-bit)) & 1) << 3);
內層迴圈改為:m_line_buffer = color_shifted | pixels[bg_x & 7];
等價性驗證:
令 k = bg_x & 7,舊 tile_x = 7 - k,pixel = pixels[k] = pixels[bg_x&7],
代入後與舊算式位元級恆等。
效果:內層迴圈從約 15 ops/pixel 降為約 3 ops/pixel,變數移位完全消除。
------------------------------------------------------------------------
[2] RenderBackground() — 像素預解 SSE2 SIMD 化
------------------------------------------------------------------------
檔案:huc6270.cpp RenderBackground(像素預解迴圈,第 740-744 行)
問題(熱點排行 #2):
像素預解迴圈每次 tile 邊界執行,每條掃描線約 32~128 次,8 次反覆
共 96 ops/tile,每秒約 464640 次 tile-change,總計約 44M ops/sec(純量)。
對策:SSE2 PMOVMSKB 位元矩陣轉置
原理:PC Engine 的 4 個 bit-plane byte 視為 4×8 位元矩陣,每條 column
對應一個 pixel(4-bit)。使用 _mm_movemask_epi8 提取每 byte 的 MSB,
配合 _mm_slli_epi32 將目標位元移至 MSB 位置。
演算法:
packed = (byte4<<24) | (byte3<<16) | (byte2<<8) | byte1
v = _mm_cvtsi32_si128(packed) // XMM bytes 0-3 = [byte1..byte4]
pixels[7-N] = _mm_movemask_epi8(v << N) & 0x0F
// = (byte1>>(7-N)&1) | ((byte2>>(7-N)&1)<<1) | ...
平台:SSE2 以上(x86/x64),使用檔案既有 GG_USE_SSE2 / GG_USE_AVX2
編譯分支。非 x86 平台自動降級為純量 for 迴圈(#else 路徑)。
公式推導驗證:
令 XMM bytes 0-3 為 [b1][b2][b3][b4],左移 N 位後新 byte B 的 MSB
= 原 byte B 的 bit (7-N)(0 ≤ N ≤ 7 範圍內不跨 byte)。
movemask 取 byte 0..3 的 MSB:
bit 0 = b1 MSB, bit 1 = b2 MSB, bit 2 = b3 MSB, bit 3 = b4 MSB
∴ movemask(v << N) & 0x0F = pixels[7-N],與純量算式位元級相符。
位元級隨機驗證(byte1=0xAA, byte2=0xCC, byte3=0xF0, byte4=0xFF):
N=0..2 → 15, N=3 → 13, N=4..7 → 5,與純量 pixels[7-N] 全部相符。
邊界:全 0 → 全 0,全 0xFF → 全 15,均相符。
效果:8 次純量反覆 → 8 次 SIMD 指令。
注意事項:
- _mm_slli_epi32(v, N) 僅用於 N=0..7
- movemask 結果僅取低 4 bits(& 0x0F)
- byte1~byte4 為 u8 型別,強轉 u32 避免移位溢位
- 位元順序:pixels[0] = 最左像素(bit 7 優先),與 PC Engine tile 編碼一致
------------------------------------------------------------------------
[3] RenderSprites() — 16-pixel LUT 預解 + SSE2
------------------------------------------------------------------------
檔案:huc6270.cpp RenderSprites(第 767 行起,熱點排行 #5)
問題:
內層 sprite pixel 迴圈每 pixel 算 pixel_x(含 x-flip)+ 4 plane 移位,
同 RenderBackground 的 planar-to-packed 模式(4 個 u16 bit-plane → 16
個 4-bit pixel)。
對策:16-pixel LUT 預解 + SSE2 PMOVMSKB(lo/hi byte 分開處理)
將 16-bit plane 拆為 lo_byte(bits 7-0)和 hi_byte(bits 15-8),
各別套用 PMOVMSKB 技巧。依 x-flip 旗標排列最終 pixel 順序。
非 x-flip(0x0800 SET):sprite_pixels[0..7]=lo[7..0], [8..15]=hi[7..0]
x-flip(0x0800 CLEAR):sprite_pixels[0..7]=hi[0..7](reverse),
[8..15]=lo[0..7](reverse)
內層迴圈改為:pixel = sprite_pixels[x & 0xF](1 table lookup)
等價性驗證:
非 x-flip:x=0→bit0→lo bit0, x=7→bit7→lo bit7, x=8→bit8→hi bit0,
x=15→bit15→hi bit7,mapping 恆等。
x-flip:pixel_x = 15-(x&0xF),反向 mapping 同樣恆等。
效果:內層變數移位 → table lookup。
注意事項:
- 其他熱點(#2 ReadVRAM 純量、#3 m_vram[] 陣列、#4 FetchSprites 僅傳遞、
#6 SATTransfer cycle-accurate)不適合最佳化。
------------------------------------------------------------------------
[4] RenderBackground — tile 為單位迴圈重構(Pass 4-A)
------------------------------------------------------------------------
檔案:huc6270.cpp RenderBackground(lines 699-765)
問題:
原結構每像素算 tile_col = bg_x>>3 並比較 prev_tile_col,第一像素靠
prev_tile_col=-1 強制載入。
對策:
tile 為單位外迴圈(while i<width),每 tile 開頭無條件載入一次,
內 do-while 跑該 tile 像素(pixel_idx 0..7)。
等價性驗證(5 點):
① tile 邊界:pixel_idx 由 7→8 ⟺ bg_x>>3 改變(low 3 bits 全 1)
② wrap 安全:x_mask∈{255,511,1023} 低 3 位全 1,wrap 不擾亂計數
③ VRAM 讀取:每 tile 開頭 BAT+2 tile-data,順序/次數/位址一致,
m_vram_openbus 終值一致(openbus 副作用保留)
④ pixels 索引:pixel_idx 從 bg_x&7 起始遞增,等同 pixels[bg_x&7]
⑤ 邊界:width=0 不進入;起始非對齊;部分 tile 由 do-while (i<width) 處理
效果:移除每像素 shift+比較,分支預測友善。
------------------------------------------------------------------------
[5] RenderSprites — 內迴圈不變量外提(Pass 4-B)
------------------------------------------------------------------------
檔案:huc6270.cpp RenderSprites(內迴圈 lines 786-862)
問題:
sprite 迴圈級有兩個不變量在 16-像素內迴圈被重複讀取。
對策:
外提兩個不變量至 sprite 迴圈級:
bool is_index_zero = (m_sprites.index == 0);
u16 palette_rendered = m_sprites.palette | 0x100 | sprite_rendered_flag;
內 16-像素迴圈改用 local 變數。
等價性:m_sprites.index / palette 在 16-像素內迴圈為常數(sprite 迴圈
內無寫入),外提後內迴圈讀取值位元一致,純讀取無副作用差異。
效果:移除每像素成員解址 ×2。
------------------------------------------------------------------------
[6] HuC6260::Clock — 刪除 dead cycles_to_hsync 分支(A1)
------------------------------------------------------------------------
檔案:huc6260_inline.h(lines 47-61)
問題(每 cycle 熱點):
原 Clock() 每 cycle 計算 cycles_to_hsync 的 ternary 並做第二次 min。
該 ternary 在 HUC6260_HSYNC_END_HPOS==0 且 m_hpos>=0 時永遠走 else,
結果 = LINE_LENGTH - m_hpos = cycles_to_line_end,第二次 min 為 no-op。
對策:刪除 ternary + clamp + min(7 行),加註解。
等價性驗證(5 點):
① HUC6260_HSYNC_END_HPOS == 0(huc6260.h:31 常數)
② m_hpos 恆 >= 0:Reset 寫 0;Clock 內 step <= cycles_to_line_end;
LoadState 後 SanitizeState clamp 到 [0, LINE_LENGTH-1]
③ ternary 永遠走 else → cycles_to_hsync = cycles_to_line_end
④ min(step, cycles_to_hsync) 已在 line 58-59 做過 → 第二次 min 為 no-op
⑤ hsync 觸發點(m_hpos == 0)由 line 115 歸零獨立觸發,不依賴 cycles_to_hsync
編譯器角度:ternary 中 m_hpos 是 s32 member,可能 alias,編譯器無法自消
→ 手動有效。
效果:每 cycle 省 1 次 ternary + 1 次 min 比較。
------------------------------------------------------------------------
[7] RenderLine BG-disable SSE2 零填(A12)
------------------------------------------------------------------------
檔案:huc6270.cpp RenderLine(lines 685-687)
問題:
原版 BG 關閉時用純量 for 寫 0x100 到 m_line_buffer[0..width-1]。
對策:
改用 SSE2:_mm_set1_epi16(0x0100) + _mm_store_si128,8 個 u16 一批,
剩餘純量 tail。純量 fallback 保留。
等價性驗證(3 點):
① m_line_buffer 為 alignas(16) u16[](huc6270.h:162)→ _mm_store_si128 對齊安全
② 寫入值位元 pattern:SSE2 寫 0x0100 per u16,純量寫 0x100,完全相同
③ 寬度處理:SSE2 處理 i+7<width 的前 N*8 個,剩餘 width%8 個純量,
寫入範圍、值與原版全純量完全一致
效果:BG 關閉場景,寬度 1024 時 1024 次寫入 → 128 次 SSE store。
------------------------------------------------------------------------
[8] ApplyLowPassFilter 外提 inv_alpha(A13)
------------------------------------------------------------------------
檔案:huc6260.cpp ApplyLowPassFilter(lines 437-439)
問題:
原版內迴圈(每 pixel)計算 (1.0f - alpha) 三次(r/g/b 各一)。alpha
在 line 390 計算後整個函式內不再改變。
對策:預算 inv_alpha = 1.0f - alpha 一次,內迴圈用 local。
等價性驗證(2 點):
① alpha 在 line 390 後無其他寫入點 → 函式內常數
② inv_alpha = 1.0f - alpha 為純 IEEE 754 減法,預算一次與每次重算位元
結果完全相同(同一運算元、同一運算 → 同一位元 pattern)
浮點運算順序未改變,僅消除重複計算 → 無 ULP 差異風險。
效果:lowpass 啟用時,每 pixel 省 2 次 float 減法
(242 線 × 約 512 pixel = 約 124k 次/frame)。
------------------------------------------------------------------------
[9] HuC6270 transfer gate bool — m_any_transfer_pending(A3)
------------------------------------------------------------------------
檔案:huc6270.h, huc6270.cpp, huc6270_inline.h
問題(每 cycle 熱點):
原 Clock() 每 cycle 測 if (sat>0) else if (vram>0)(兩次 load + 兩次
branch)。99% cycle 兩者皆 0。
對策:
新增 m_any_transfer_pending bool 為嚴格超集閘極,false 時跳過整段。
同步點(全部已實作):
- 設 true:VBlankIRQ(sat=1024)、WriteRegister LENR(vram)
- 清 false:SATTransfer 完成(sat==0 且 vram==0)、VRAMTransfer 完成
- Reset:false
- LoadState:重算 = (sat>0 || vram>0)
- Clock():if (m_any_transfer_pending) { 原 if/else if }
等價性:
bool 為嚴格超集:false ⟺ sat==0 且 vram==0 ⟺ 原兩條件都失敗 → 跳過
不改行為。true 時跑原邏輯。所有排程/清除點已涵蓋。m_any_transfer_pending
為衍生量,不入序列化,LoadState 後重算,不破壞 SaveState。
效果:每 cycle 省 2 次 load + 2 次 branch(99% cycle)。
------------------------------------------------------------------------
[10] HuC6260 Clock member 外提 local(A5/A6)
------------------------------------------------------------------------
檔案:huc6260_inline.h HuC6260::Clock
問題(每 cycle 熱點):
Clock() while 迴圈內 3 個使用點重複載入 member。
對策:
Clock() while 迴圈前外提三個 member 為 local:
const int full_line_width = k_huc6260_full_line_width[m_speed];
const int screen_start_x = m_screen_start_x;
const int screen_end_x = m_screen_end_x;
迴圈內 3 個使用點改用 local。
等價性:
這些 member 在整個 Clock() 呼叫期間不變(CPU 無法插隊寫 VCE 暫存器,
RunInstruction 與 ClockHardware 交替執行)。外提至 local 值不變。
SaveState 載入後,下次 Clock() 開頭讀 local 仍從 member 讀。
編譯器角度:member 可能被 alias,編譯器無法自消 → 手動有效。
效果:每 cycle 省 member 重複載入。
------------------------------------------------------------------------
[11] IsCpuVramSlotAvailable 結果快取(A8)
------------------------------------------------------------------------
檔案:huc6270_inline.h ProcessCpuVramAccesses
問題(每 3-cycle slot,CPU VRAM DMA 期間非常熱):
原 while 迴圈每圈呼叫 IsCpuVramSlotAvailable(3)。該函式讀取
hpos/v_state/burst_mode/latched_cr/sprite_count/load_bg_*/latched_mwr/
divider,這些在 ProcessCpuVramAccesses 內全不變。
對策:
while 前呼叫一次,結果存 slot_available,每圈用 local。
等價性(親自驗證):
ProcessCpuVramAccesses 內部不推進任何硬體時脈(hpos 等不變)。
IsCpuVramSlotAvailable(3) 的 elapsed_cycles=3 固定 → 結果為常數。
m_allow_vram_access 副作用:首次呼叫寫入,之後不呼叫 → 保留首次值
(與原版每圈重寫相同值等價,因輸入相同)。
delay 邏輯(m_transfer_delay)每圈仍跑,不受影響。
效果:CPU VRAM DMA 期間,每 slot 省一次完整 IsCpuVramSlotAvailable 呼叫
(含 CurrentHClock、多重條件分支、dots_per_sprite switch)。
------------------------------------------------------------------------
[12] m_in_display_window bool 快取(B2)
------------------------------------------------------------------------
檔案:huc6270.h, huc6270.cpp, huc6270_inline.h
問題(每 cycle 熱點):
Clock() 每 cycle 測 m_active_line && (m_h_state == HDW)。
對策:
新增 m_in_display_window bool,在 5 個轉換點更新,Clock() 改讀 bool。
5 個更新點:
1. Reset:init false
2. SetHSyncHigh:強制 HSW 後 false(非 NextHorizontalState 路徑,易漏)
3. EndOfLine:m_active_line 設定後重算
4. NextHorizontalState switch 後:反映 post-transition m_h_state
(含 HSW case 內 HSyncStart→EndOfLine 變動)
5. LoadState 結尾:m_active_line + m_h_state 讀回後重算
唯一讀者:Clock()(inline.h:53)→ if (m_in_display_window)
等價性:
m_in_display_window = m_active_line && (m_h_state == HDW)。所有寫入點
涵蓋。Clock 內 m_h_state 只在 NextHorizontalState 改,m_active_line 只
在 EndOfLine 改 → 掃描線內無 drift。bool 為衍生量,不入序列化,
LoadState 後重算 → 不破壞 SaveState。
效果:每 cycle 省 bool load + enum compare。
------------------------------------------------------------------------
[13] m_bg_enabled / m_sprites_enabled 預解碼(B5)
------------------------------------------------------------------------
檔案:huc6270.h, huc6270.cpp, huc6270_inline.h
問題(每 scanline + 每 CPU VRAM cycle):
RenderLine 與 IsCpuVramSlotAvailable 每次測 m_latched_cr & 0x80/0x40。
對策:
新增 m_bg_enabled/m_sprites_enabled bool,在 4 個 decode 點更新,讀者
改用 bool。sprites 有第二讀者(IsCpuVramSlotAvailable),雙重收益。
4 個 decode 點:
1. Reset:m_latched_cr = HUC6270_VAR_CR 後
2. EndOfLine wrap:部分 mask 重寫 bits 6,7 後(剛好是 BG/sprites bit)
3. NextHorizontalState HDS:在 !m_burst_mode guard 內
(burst mode 不 latch,bool 保持 frame wrap 值;burst 下 bits 6,7=0
→ bools=false,正確)
4. LoadState 結尾
4 個讀者替換:
- RenderLine:697 → if(!m_bg_enabled)
- RenderLine:715 → if(m_bg_enabled)
- RenderLine:718 → if(m_sprites_enabled)
- IsCpuVramSlotAvailable:281 → bool sprites_enabled = m_sprites_enabled;
等價性:
m_bg_enabled = (m_latched_cr & 0x80) != 0
m_sprites_enabled = (m_latched_cr & 0x40) != 0
EndOfLine wrap 只重寫 bits 6,7 → decode 在重寫後反映新值。
HDS guard 內 decode → 只在非 burst 追蹤 latch。
兩 bool 為衍生量,不入序列化,LoadState 後重算。
效果:每 scanline + 每 CPU VRAM cycle 省解碼。
================================================================================
第一部分 §2 模擬正確性 Bug 修復(5 項,對齊 Mednafen)
================================================================================
------------------------------------------------------------------------
[Bug-1] ReadTimerRegister 0x7F → m_timer_reload(HIGH-1)
------------------------------------------------------------------------
檔案:huc6280_inline.h ReadTimerRegister(line 272-278)
問題:
reload 視窗(m_timer_counter==0 && m_timer_cycles<=15)回傳硬編碼 0x7F,
而非 m_timer_reload。當 reload != 0x7F 時讀值錯誤。
Mednafen 對照(pce/huc6280.cpp):
TimerRead(line 709-718):先 TimerSync() 再讀 timer_value,
return(timer_value | (IODataBuffer & 0x80))
TimerSync(line 527-560):underflow 後 timer_inreload,
下次同步 timer_value = timer_load(line 539)
→ reload 視窗讀到的是 timer_load,不是常數 0x7F
修復:
reload 視窗回傳 m_timer_reload(對應 Mednafen 的 timer_load)。
非 reload 視窗仍回 m_timer_counter(與原版相同)。
注意:
Geargrafx 的 5*3=15 cycles 視窗是其自己設計,無直接 Mednafen 對應
(Mednafen 用 timer_inreload 狀態機)。本修復保留視窗,只改值。
------------------------------------------------------------------------
[Bug-2] VCE blur/frame-length latch(HW-1)
------------------------------------------------------------------------
檔案:huc6260.h, huc6260.cpp, huc6260_inline.h
問題:
VSync start/end/wrap(inline.h:122,139,161)用 live m_blur 計算
k_huc6260_total_lines[m_blur]。mid-frame 改 blur 會讓 frame 長度不可
預測地變動,三個比較點可能用不同 blur 值。
Mednafen 對照(pce/vce.cpp):
line 546:vblank_counter = vblank ? 4095 :
((lc263 ? 358995 : 357630) - 4095)
→ frame 長度在 vblank 邊界 latch 進 vblank_counter,下個 frame 期間
不再讀 lc263 決定長度。
修復:
新增 m_latched_total_lines member,在 frame wrap 時 latch:
- huc6260.h:新增 s32 m_latched_total_lines(line 139)
- huc6260.cpp Reset:m_latched_total_lines = k_...[m_blur]
- huc6260_inline.h SanitizeState:同上(LoadState 後重算)
- inline.h:122,139,161 三比較點改用 m_latched_total_lines
- inline.h:161 frame wrap 後 latch:
m_latched_total_lines = k_huc6260_total_lines[m_blur]
- inline.h GetTotalLines():回傳 m_latched_total_lines
(huc6270.cpp:348 EndOfLine 的 frame wrap 一致)
等價性:
- blur 不變時:latched 值 = live 值,行為與原版完全相同
- mid-frame 改 blur:整個 frame 用 latched 值,與 Mednafen 一致
SaveState 相容:m_latched_total_lines 為衍生量,不入序列化。
LoadState 後 SanitizeState 重算(line 396),不破壞 SaveState 格式。
實際影響:極小。遊戲鮮少 mid-frame 切 blur bit(lc263 主要用於 262 vs
263 line 模式選擇,是配置而非動態切換)。
------------------------------------------------------------------------
[Bug-3] ApplyLowPassFilter RGBA8888 round/clamp(CONFIRMED-2)
------------------------------------------------------------------------
檔案:huc6260.cpp ApplyLowPassFilter(line 452-454)
問題:
RGB565 路徑(line 444-446):RoundToByte(r) = clamp(0,255) + 四捨五入
RGBA8888 路徑(line 452-454):(u8)r = 純截斷,無 clamp
兩個差異:
(1) 截斷 vs 四捨五入
(2) 無 clamp:r > 255.0f(IIR 浮點誤差累積)時 (u8)r 會 wrap
後果:
(1) 同一 frame,RGBA8888 輸出比 RGB565 系統性偏暗(截斷累積)
(2) r > 255.0f 時 wrap 成小值 → 亮點/壞點
修復:
RGBA8888 路徑改用 RoundToByte(r/g/b),與 RGB565 對稱。
RoundToByte(huc6260_inline.h:525-534):
value <= 0 → 0; value >= 255 → 255; else (u8)(value + 0.5f)
等價性:
RGB565 路徑完全不變。RGBA8888 路徑行為改變(截斷→四捨五入+clamp),
此為 bug 修復。alpha 通道未動(由 RenderFrameTemplate 寫 255)。
影響評估:
正常情形(r 在 [0,255]):四捨五入 vs 截斷差最多 1 LSB,視覺無感。
異常情形(r > 255.0f):原版 wrap 壞點 → 修復後 clamp 到 255。
------------------------------------------------------------------------
[Bug-4] AdjustForMultipleDividers hardcode 242 → end_line(MED-2)
------------------------------------------------------------------------
檔案:huc6260.cpp AdjustForMultipleDividers(line 348)
問題:
最終 memcpy 用 hardcode 242,但迴圈(line 298)只寫入
end_line = GetCurrentHeight() 行。scanline cropping 時 end_line < 242,
memcpy 會 copy 到 m_scale_buffer 中 stale 資料。
修復:242 → end_line,與迴圈寫入範圍一致。
等價性:
- end_line == 242(無 cropping):與原版完全相同
- end_line < 242(有 cropping):只 copy 寫入的行數,不 copy stale
消費端(emu.cpp screenshot、OpenGL 上傳)用 GetCurrentHeight() 決定高度,一致。
------------------------------------------------------------------------
[Bug-5] ClockTimer if → while hardening(MED-3)
------------------------------------------------------------------------
檔案:huc6280_inline.h ClockTimer(line 248)
問題(latent):
原 if (m_timer_cycles <= 0) 只處理一次 underflow。
若 cycles > divisor(=1024*3=3072),m_timer_cycles -= cycles 後可能
遠小於 0,單次 if 只能處理一個 period,會丟掉多次 timer IRQ。
正常 instruction(cycles < 100)不會觸發,為 hardening。
Mednafen 對照(pce/huc6280.cpp TimerSync line 533):
while (timer_div <= 0) { ... timer_div += reload_div; }
→ 用 while 處理多次 underflow
修復:
if → while;m_timer_cycles = divisor + m_timer_cycles → m_timer_cycles
+= divisor(數學等價,放進 while)。
等價性:
- 正常(cycles < divisor):while 至多跑一次,與 if 相同
- 長 stall(cycles >= divisor):while 處理多次 underflow
(hardening,對齊 Mednafen)
timer reload/IRQ 邏輯未動,只改迴圈結構。
================================================================================
第二部分
穩定性/安全性修復(2026-07-06)
================================================================================
經三輪系統性掃描與驗證(廣度掃描 → 逐點判讀 → 完整攻擊鏈 + 啟用條件
+ 反向質疑),找出可能破壞程式運行穩定性的問題,分四波修復。
全部使用專案既有慣例(CLAMP/MIN/MAX/IsValidPointer/Error/TraceProblem/
std::atomic/lock_guard/CLAMP 後置範本)。
共通性質:
- 未改變任何公開 API 介面或 save-state 格式(向後相容)
- 第三方依賴(libchdr/miniz 等)未被修改
================================================================================
第二部分 §1 CD-ROM 子系統(嚴重等級,惡意檔案即可觸發)
================================================================================
根因:libchdr 的 metadata 路徑完全無完整性驗證(VERIFY_BLOCK_CRC、
rawsha1、sha1 皆不含 metadata),chd_get_metadata 直接 core_fread 攻擊者
可控的位元組。
------------------------------------------------------------------------
[C1] CHD u32 下溢污染 TOC(嚴重)
------------------------------------------------------------------------
檔案:cdrom_chd_image.cpp ReadTOC(line 392)
問題:
data_frames = frames - pregap_dv(int - u32 混型),pregap_dv > frames
時下溢成約 UINT32_MAX,污染 sector_count/end_lba/m_toc.sector_count。
CHD metadata 無完整性驗證,攻擊者可控。
修復:
1. frames/pregap/postgap 負值檢查:
if (frames < 0 || pregap < 0 || postgap < 0) { Error; return false; }
2. 下溢保護:
if (pregap_dv > (u32)frames) { Error; return false; }
3. data_frames 改 (u32)frames - pregap_dv
4. L3 對齊計算改 u32 運算:((u32)frames + 3u) & ~3u
注意:用 return false(非 clamp)是因為 CHD 損壞/惡意構造,載入失敗比
載入 TOC 污染的 CD 更安全。99 軌上限(for i<99)只限軌數,不限
frames/pregap 數值,本修復補此缺口。
------------------------------------------------------------------------
[C2] CUE 最後一軌 u32 下溢污染 TOC(嚴重)
------------------------------------------------------------------------
檔案:cdrom_cuebin_image.cpp ParseCueFile TOC 組裝(line 978)
問題:
last_size = file_size - last.file_offset,file_offset 由 CUE INDEX 時間
戳累加(可達 m=99 → 約 1.05GB),可超過小 BIN 的 file_size,u32 下溢。
修復:
if (last.file_offset > f.img_file->file_size) { Error; return false; }
注意:INDEX m<=99 範圍檢查不足以擋 file_offset 累加。
------------------------------------------------------------------------
[C3] ZipSlip 路徑穿越 — 可持久化 RCE(嚴重)
------------------------------------------------------------------------
檔案:common.h extract_zip_to_folder(line 389);新增 is_path_safe helper
問題:
out_path = out_dir + "/" + file_stat.m_filename,條目名未淨化 .. 或絕對
路徑。寫入 %APPDATA%\Geardome\tmp\,../ 可逃逸覆寫 DLL。
修復:
新增 is_path_safe(const char* path):
- 拒絕 NULL/空字串
- 拒絕絕對路徑(/、\、X: 磁碟代號)
- 拒絕含 .. 完整 segment(檢查 segment 邊界,不誤殺 a..b/foo..)
extract 前檢查:if (!is_path_safe(...)) { Error; continue; }
(continue 非 return false,讓其他合法條目仍可解壓)
------------------------------------------------------------------------
[H5] CHD sscanf 棧緩衝區溢位 — RCE 潛力(嚴重)
------------------------------------------------------------------------
檔案:cdrom_chd_image.cpp ReadTOC sscanf(line 343, 355)
問題:
sscanf 用第三方巨集 CDROM_TRACK_METADATA*_FORMAT,%s 無寬度限制,
目標 type[64]/subtype[32] 棧溢位。metadata 無 hash 驗證,sscanf 仍返回 8。
修復:
呼叫端改用本地帶寬度格式字串(不使用第三方巨集):
sscanf(metadata, "TRACK:%d TYPE:%63s SUBTYPE:%31s FRAMES:%d ...", ...)
寬度對應:%63s → type[64]、%31s → subtype[32]/pgtype[32]/pgsub[32]
注意:第三方巨集在 libchdr 標頭,不改(避免污染依賴、升級衝突)。
------------------------------------------------------------------------
[M9] CUE FILE 相對路徑穿越讀任意檔(中)
------------------------------------------------------------------------
檔案:cdrom_cuebin_image.cpp ParseCueFile FILE 處理(line 745)
問題:
../sensitive.bin 不被過濾,可讀 CUE 目錄外檔案。
修復:路徑拼接前用 is_path_safe 檢查(共用 C3 helper),
不通過 return false。
注意:成員名是 m_file_directory(snake_case)。
------------------------------------------------------------------------
[M10] CUE 軌數無上限 — DoS(中)
------------------------------------------------------------------------
檔案:cdrom_cuebin_image.cpp ParseCueFile 主迴圈(line 702)
問題:while(getline) 無軌數上限,百萬 TRACK 行可耗盡記憶體。
修復:加 track_count 計數,超過 k_max_cue_tracks(99) 即 return false
(對齊 CHD 的 99 上限)。
------------------------------------------------------------------------
[M11] WAV data_size 覆寫 file_size — 巨量配置(中)
------------------------------------------------------------------------
檔案:cdrom_cuebin_image.cpp FindWavDataChunk(line 637)
問題:data_size(不可信)直接覆寫 file_size,無交叉驗證。
修復:用 file.GetSize() 取實際大小,data_size > available 時 clamp。
------------------------------------------------------------------------
[M8] SetupToc index0>index1 下溢(低)
------------------------------------------------------------------------
檔案:cdrom_cuebin_image.cpp ParseCueFile TOC 組裝(line 971)
問題:(start_lba - lead_in_lba) 在 INDEX 00 > INDEX 01 時下溢。
修復:if (track.start_lba < track.lead_in_lba) { Error; return false; }
------------------------------------------------------------------------
[M2] PreloadTrackChunks u32 乘法溢位(低)
------------------------------------------------------------------------
檔案:cdrom_cuebin_image.cpp PreloadTrackChunks(line 366)
問題:total_bytes = sector_count * sector_size(u32*u32),獨立不觸發,
需 C1/C2 先污染。
修復:改 u64 計算,> 0xFFFFFFFF 則 return false。
================================================================================
第二部分 §2 save-state 加固(同源缺陷)
================================================================================
根因:save-state 的 payload 完全無 checksum/CRC(types.h:113-126 無欄位),
唯一 rom_crc 是公開值(Redump + Log 印出)。攻擊者改 payload 任意欄位 +
補正確 header 即可 100% 通過。
------------------------------------------------------------------------
[H3] m_step_index 未 clamp → OOB 讀 LUT(高)
------------------------------------------------------------------------
檔案:adpcm.cpp LoadState(line 224)
問題:
LoadState 直接 read(&m_step_index)(u8,可達 255),首解
m_step_delta[(m_step_index<<3)+value] 索引可達 2047,遠超陣列 392 項。
CD 遊戲續播時下一 sample 即觸發。
修復:讀入後 m_step_index = CLAMP(m_step_index, 0, 48)
(魔數 48 與 adpcm_inline.h:286 一致)。
注意:clamp 放在 read 之後立即做,不依賴 version 分支。載入後到首次解碼
之間無其他重設路徑,故必須在 LoadState 內完成。
------------------------------------------------------------------------
[H4] m_sprite_count 未 clamp → 防禦縱深(低)
------------------------------------------------------------------------
檔案:huc6270.cpp LoadState(line 1149)
問題(第三輪確認鏈條中斷):
m_sprite_count 每條 active scanline 由 FetchSprites 清零重建,RCR event
先於 RenderLine,save-state 注入值在第一幀就被覆蓋。故嚴重度從高降為低。
仍加 clamp 是防禦縱深(同源缺陷,未來重構改變時序時可兜底)。
修復:m_sprite_count = CLAMP(m_sprite_count, 0, HUC6270_SPRITES * 2)
------------------------------------------------------------------------
[M1] LFO 負移位 UB(中)
------------------------------------------------------------------------
檔案:huc6280_psg.cpp(line 325)
問題:
((lfo_data-16) << (((m_lfo_control&3)-1)<<1)),control&3==0 時移位量
為 -2(負數左移 UB)。LoadState 將 lfo_enabled 與 lfo_control 獨立讀入
可解耦。
修復:if (m_lfo_control & 3) 守衛,control&3 為 0 時跳過移位。
注意:control&3==0 表示 LFO 頻率為 0,本就不該有 shift 作用。
================================================================================
第二部分 §3 MCP 並發子系統
================================================================================
重要前提:MCP 預設完全停用(mcp_mode=-1,需 --mcp-stdio/--mcp-http CLI
旗標,無 config/選單開關)。一般玩家不碰,僅影響主動啟用的 AI 除錯使用者。
HTTP 預設綁 127.0.0.1(loopback)。
------------------------------------------------------------------------
[C4] MCP Server detach 後 use-after-free(中)
------------------------------------------------------------------------
檔案:mcp_server.h, mcp_server.cpp, mcp_manager.h
問題:
Stop() 將 m_thread detach(非 join),被 detach 線程仍在 recv()/send()
存取 transport;同時 McpServer 被刪,transport 也被 SafeDelete。UAF。
stdio getline 無法被 close 中斷,不能直接改 join。
修復(shared_ptr 徹底方案):
1. McpServer 繼承 enable_shared_from_this<McpServer>
2. Start() 內 m_thread 用 shared_from_this() 捕獲
3. reader_thread 也透過 shared_from_this() 捕獲
4. McpManager m_server 改 shared_ptr,Stop 用 reset() 取代 SafeDelete
效果:detached 線程持有引用,McpServer 存活到所有線程結束,
~McpServer 的 SafeDelete(m_transport) 此時無人存取。
設計理由(為何共享整個 McpServer 而非只共享 transport):
reader/Run 線程跑的是 McpServer 成員函式,深度依賴 this。只共享 transport
不夠(成員函式透過 this 存取成員,this 失效仍 UAF)。共享整個 McpServer
才能確保所有被存取的成員都存活。
注意事項:
- shared_from_this() 前提:物件必須已由 shared_ptr 管理。McpManager::Start()
用 shared_ptr(new ...) 包裹後才呼叫 ->Start()。未來維護時 McpServer
必須由 shared_ptr 管理,不能直接 new 後呼叫 Start()。
- 既有非本次引入的注意點:StdioTransport::close() 只設 m_closed,不解阻塞
getline。stdio 模式 reader 可能滯留(McpServer 存活更久)。根治需讓
close 真正關閉 stdin fd(未處理,屬獨立工程)。
------------------------------------------------------------------------
[H1/M7] 媒體載入中 MCP 讀寫核心裸指標(中/低)
------------------------------------------------------------------------
檔案:mcp_manager.h PumpCommands(line 189)
問題:
emu_update 先 pump commands 再檢查 loading_state,載入執行緒重設
RAM/ROM/VRAM 時 MCP 工具碰裸指標。MCP 自帶 m_pending_media_load guard
不涵蓋 GUI 載入。
修復:PumpCommands 開頭加 if (emu_is_media_loading()) return;
效果:涵蓋所有 core 操作(read_memory、controller input、input macro、
delayed releases),一次擋住 H1 與 M7。載入期間所有 MCP 工具暫停,
載入完成後恢復。
------------------------------------------------------------------------
[M3] HTTP Content-Length 無上限 — DoS(低)
------------------------------------------------------------------------
檔案:mcp_transport.h recv()(line 575)
問題:atoi 無上限,單請求可觸發 GB 級分配。
修復:加 kMaxContentLength(16MB) 上限,超範圍 connection_closed + break。
------------------------------------------------------------------------
[M4] HttpTransport::close() 對 socket 操作未持鎖(低)
------------------------------------------------------------------------
檔案:mcp_transport.h HttpTransport::close()(line 714-731)
問題:close 對 socket 無 lock_guard,與持鎖的 send/recv 競爭
(Windows 雙重 close)。
修復:socket 操作用 std::lock_guard<std::mutex> lock(m_mutex) 包住
(對齊 close_current_client 既有正確寫法)。
注意:m_closed 與 notify_all 在鎖外(m_closed 是 atomic,安全;
notify_all 鎖外合法)。
------------------------------------------------------------------------
[M5] m_closed 為非原子 bool(低)
------------------------------------------------------------------------
檔案:mcp_transport.h StdioTransport(line 97)+ HttpTransport(line 736)
問題:bool m_closed 被 recv(reader 線程)讀、close(主線程)寫,無同步。
修復:兩處改 std::atomic<bool>(對齊 mcp_server.h m_running 先例)。
建構函式皆 m_closed = false 初始化。
================================================================================
第二部分 §4 低風險加固
================================================================================
------------------------------------------------------------------------
[H2] HuCard 載入 int 溢位 → heap overflow(低)
------------------------------------------------------------------------
檔案:media.cpp LoadHuCardFromBuffer(line 241)
問題:(size + 0x1FFF) & ~0x1FFF,size 接近 INT_MAX 時有號溢位。
修復:if (size > 0x40000000) { Error; return false; }(1GB 上限)。
------------------------------------------------------------------------
[H6] AudioLBA 未知模式 assert(false)(低)
------------------------------------------------------------------------
檔案:scsi_controller.cpp AudioLBA default(line 750)
問題:mode==0xC0 時 assert(false),Debug build 終止行程。
修復:移除 assert(false),保留 TraceProblem + return 0
(符合 SCSI 主慣例:TraceProblem 為主,assert 為特例)。
------------------------------------------------------------------------
[H7] BIOS tellg 失敗未檢查(低)
------------------------------------------------------------------------
檔案:media.cpp LoadBios(line 355)
問題:int size = (int)tellg(),失敗回 -1,原僅 Log 不拒絕。
修復:加 if (!file || size < 0) { Error; return false; }
------------------------------------------------------------------------
[H8] loading_thread_active/result 非原子(低)
------------------------------------------------------------------------
檔案:emu.cpp(line 64-65)
問題:兩變數非原子跨執行緒(join + atomic loading_state 已建立
happen-before,無實際並發,但型別不符最佳實踐)。
修復:改 std::atomic<bool>。
------------------------------------------------------------------------
[L1] 記憶體編輯器最後一行越界讀寫(低)
------------------------------------------------------------------------
檔案:gui_debug_memeditor.cpp 渲染迴圈(line 227)
問題:total_rows 無條件進位,最後列 byte_address 可 >= m_mem_size,
存取 m_mem_data[byte_address] 無守衛。僅 debug 工具手動觸發。
修復:if (byte_address >= m_mem_size) continue
(保持表格對齊:skip 分支也呼叫 TableNextColumn + separator)。
------------------------------------------------------------------------
[L2] strncpy_fit 對 src==NULL 無檢查(低)
------------------------------------------------------------------------
檔案:common.h strncpy_fit(line 186)
問題:strlen(src) 對 NULL 為 UB。
修復:加 if (!IsValidPointer(dest) || !IsValidPointer(src) || dest_size==0)
守衛(dest 有效但 src NULL 時 dest[0]='\0',dest NULL 時不寫入)。
------------------------------------------------------------------------
[L5] parse_int long→int 截斷(低)
------------------------------------------------------------------------
檔案:shader_preset.cpp parse_int(line 537)
問題:(int)result 無範圍檢查,LP64 平台(long 64-bit)截斷。
修復:加 <climits>,if (result < INT_MIN || result > INT_MAX)
return default_value。
================================================================================
記錄結束
================================================================================
|
評分
-
查看全部評分
|