How did code handle 24-bit-per-pixel formats when using video cards with bank-switched memory?
1990年代的顯示卡將記憶體切割為 64KB 分塊,面對每像素 24 位元的格式時會產生無法整除的跨界難題,引發無限迴圈當機。
- 早期 Windows 95 透過 VFLATD 驅動程式,將 64KB 分塊切換記憶體模擬為平坦的定址空間。
- 跨越 64KB 分頁邊界的單一像素存取,會讓 CPU 在兩分頁間不斷觸發違規,引發無限切換迴圈。
- 開發者必須確保記憶體對齊,將跨界像素手動拆解為 3 次獨立的位元組存取指令以避開系統崩潰。
在 1990 年代的個人電腦顯示技術中,每像素 24 位元的色彩深度與傳統 64KB 記憶體分塊機制的結合,曾帶來一個無法整除的數學難題。微軟資深工程師 Raymond Chen 透過早期 Windows 95 的底層架構,解析圖形開發者如何透過強制記憶體對齊與拆解位元組,避開跨越記憶體分頁時可能引發的無限切換崩潰迴圈。
早期顯示卡的 64KB 分塊與 VFLATD 驅動
在 DOS 與早期 Windows 時代,許多顯示卡的硬體記憶體映射空間被嚴格限制在 64KB 的定址範圍內。然而,一張高解析度的圖像畫面緩衝區(Frame Buffer)往往需要數百 KB 甚至數 MB 的實體空間。為了突破硬體定址的限制,顯示卡採用了分塊切換記憶體(bank-switched memory)的設計。這意味著 CPU 同一時間只能「看見」並存取這 64KB 的顯示記憶體視窗,若要繪製其他區域的像素,必須透過硬體指令將記憶體視窗切換到下一個區塊。
進入 Windows 95 時代後,微軟為了解決這項繁瑣的硬體操作,引入了名為 VFLATD(Virtual Flat Display)的虛擬視訊驅動程式輔助工具。這個機制的目的是向應用程式「模擬」出一個連續且平坦的視訊記憶體位址空間,讓開發者不需要手動管理繁雜的區塊切換。
當應用程式嘗試將像素寫入這個虛擬的線性空間時,VFLATD 會在幕後進行轉換。如果程式存取的位址對應到目前生效的 64KB 區塊,一切運作正常;如果存取到尚未映射的區塊,系統會觸發一次分頁錯誤(Page Fault)。此時驅動程式便會攔截錯誤,動態切換顯示卡硬體上的記憶體分塊,並將新的映射位址移動到應用程式預期的虛擬平坦位址上。
24 位元像素與 64KB 無法整除的硬傷
這樣的動態分塊切換機制在多數情況下運作良好,但工程師 Gil-Ad Ben Or 提出了一個極具挑戰性的極端案例:當系統處理 24-bit-per-pixel(每像素 24 位元)的圖像格式時,程式碼該如何應對跨越記憶體邊界的狀況?
核心的數學問題在於,24 位元的色彩深度代表每個單一像素需要佔用 3 個位元組(Bytes) 的記憶體空間。然而,顯示卡的每個記憶體分塊大小是 64KB(即 65,536 個位元組)。這兩個數字之間存在著一個致命的衝突:65,536 無法被 3 整除。
這意味著,如果在沒有使用額外雙重緩衝(Double Buffering,在主記憶體先繪製再整批傳送)機制的系統中,開發者逐一寫入畫面上的像素資料時,必定會遇到一個剛好被硬生生切斷的像素。這個跨界像素的某些位元組會落在前一個 64KB 分塊的最後方,而剩下的位元組則會落在下一個 64KB 分塊的最前端。這種硬體邊界與資料單位不對齊的現象,成為了當時底層圖形開發者必須面對的棘手難題。
跨越分頁存取觸發的無限切換迴圈
如果開發者沒有特別處理,直接用單一指令去讀取或寫入這個跨越兩個記憶體分塊的 24 位元像素,VFLATD 的模擬機制將會面臨徹底崩潰。這個崩潰的過程源於 CPU 與驅動程式之間的記憶體存取違規(Access Violation)處理邏輯。
當 CPU 執行一個跨越記憶體分頁的存取指令時,硬體會先在第一個分頁上觸發存取違規。VFLATD 驅動程式介入後,會將第一塊記憶體分塊映射進來,同時為了騰出硬體資源空間,宣告第二個分塊失效。然而,由於該指令的存取範圍跨越了兩塊分頁,當 CPU 繼續嘗試完成同一個指令並存取後半段資料時,立刻又會在第二個分頁上觸發新的存取違規。
為了處理第二次違規,驅動程式只好將第二塊記憶體分塊重新映射進來,但這又會導致剛才才載入的第一個分塊遭到取消映射。如此一來,這個跨界像素的單一存取指令,會讓 CPU 在兩個分頁之間來回觸發錯誤,導致系統陷入無窮無盡的分塊切換迴圈,最終使作業系統癱瘓。
拆解位元組以確保記憶體對齊存取
面對這種硬體與作業系統底層機制的限制,當時的程式碼必須遵循最嚴格的底層規則:所有的記憶體存取都必須是正確對齊的(Properly-aligned)。在計算機架構中,只要是符合記憶體對齊規範的存取指令,在硬體層面上就絕對不會發生跨越分頁邊界的情況。
這意味著,編寫直接存取視訊記憶體程式碼的開發者,必須放棄使用任何取巧的手段。例如,他們不能直接使用一條 32-bit 的讀取指令去抓取一個 24-bit 的像素,然後再用軟體邏輯忽略掉最高位的 8 個位元。只要指令涵蓋的範圍跨界,硬體就會無情地觸發違規。
如果遇到一個剛好跨越 64KB 邊界的像素,開發者別無選擇,只能在程式碼中手動將這個像素拆解。他們必須將其轉換為三次獨立的 1 位元組(Byte) 存取指令;或者將其拆分為一次 1 位元組存取加上一次 2 位元組(Word)存取,且前提是該 Word 存取必須符合位址對齊規範。在實際的開發環境中,因為判斷何時該用 Byte+Word 還是 Word+Byte 所耗費的運算成本太高,多數開發者乾脆統一採用連續三次 Byte 讀寫的粗暴方式來處理這個跨界像素。
整行像素處理的 32-bit 對齊最佳化
雖然逐一拆解位元組可以避開當機,但如果整個畫面都用 1 位元組的方式慢慢繪製,顯示效能將會慘不忍睹。因此,當開發者需要操作一整行的像素資料時,他們會採用另一套經過計算的記憶體對齊策略,以便在安全的前提下利用更快速的 32 位元存取指令。
在處理整行資料時,標準的做法是先以位元組為單位逐一複製資料,直到記憶體的目標位址剛好達到 32 位元對齊(32-bit aligned)的邊界。一旦進入對齊狀態,開發者就可以切換到 32 位元的讀寫指令,用最高效的方式處理整行像素的絕大部分資料。直到接近行尾的邊界,再退回使用單一位元組指令來處理剩下的畸零資料。
在這個效能最佳化的過程中,32 位元的讀取指令不可避免地會「跨越像素邊界」,因為 32 位元無法完全對齊 24 位元的像素結構。但在底層硬體的邏輯中,這完全不成問題,因為這些經過對齊的 32 位元指令,絕對不會跨越那條致命的 64KB 分頁邊界。早期的圖形開發者,就是透過這些扎實的底層記憶體操作,穩定了作業系統的運作。
面對硬體限制與作業系統的分頁管理衝突,最穩健的解決方案往往不是尋找捷徑,而是嚴格遵循記憶體對齊的基礎規範。