在 GPU 普及前的 DOS 年代,程式設計師必須直接操作 VGA 顯示記憶體 (0xA0000) 才能獲得高效能繪圖。本文解析一套精簡的 C 語言繪圖套件,涵蓋 分頁管理、像素讀寫、Bresenham 畫線、矩形/填滿矩形、中點畫圓 等核心功能,程式碼僅約 200 行,卻展現了底層圖形程式設計的精髓。
#include <dos.h> /* int86(), REGS 聯合體 */
#define MAXX 640 /* 解析度 640x480 */
#define MAXY 480
static float asp_ratio = 1.0f; /* 長寬比修正,預設 1:1 */
VGA 256 色模式 (Mode 0x101) 的顯存僅 64 KB (0xA0000~0xAFFFF),但 640×480=307,200 像素需 300 KB。解決方案是分頁:將顯存劃分為多個 64 KB 頁,透過 BIOS 中斷 0x10 功能 0x4F05 切換可見頁面。
/* 切換顯示頁面 */
static void selectpage(int page)
{
union REGS r;
r.x.ax = 0x4F05;
r.x.bx = 0;
r.x.dx = page;
int86(0x10, &r, &r);
}
/* 進入 640x480x256 模式 (VESA VBE 0x101) */
void init256(void)
{
union REGS r;
r.x.ax = 0x4F02;
r.x.bx = 0x101;
int86(0x10, &r, &r);
}
/* 返回文字模式 (Mode 3) */
void exit256(void)
{
union REGS r;
r.h.ah = 0;
r.h.al = 3;
int86(0x10, &r, &r);
}
putpixel 與 getpixel 核心邏輯相同:計算線性位移 → 判斷是否跨頁 → 必要時切頁 → 直接存取遠指標 0xA0000000L。
void putpixel(int x, int y, int color)
{
long offset = (long)y * MAXX + x; /* 線性位移 */
int page = (int)(offset >> 16); /* 高 16 bits = 頁號 */
offset &= 0xFFFF; /* 低 16 bits = 頁內位移 */
static int current_page = -1; /* 靜態快取目前頁面 */
if (current_page != page)
selectpage(current_page = page);
*( (char far *)0xA0000000L + offset ) = (char)color;
}
int getpixel(int x, int y)
{
long offset = (long)y * MAXX + x;
int page = (int)(offset >> 16);
offset &= 0xFFFF;
static int current_page = -1;
if (current_page != page)
selectpage(current_page = page);
return (int)*( (char far *)0xA0000000L + offset );
}
關鍵優化:靜態變數 current_page 避免重複切頁系统调用,大幅提升連續繪圖效能。
程式碼實作通用的八向線段,僅使用整數加減與比較,無乘除、無浮點:
void line(int x0, int y0, int x1, int y1, int color)
{
int dx = abs(x1 - x0), sx = (x0 < x1) ? 1 : -1;
int dy = abs(y1 - y0), sy = (y0 < y1) ? 1 : -1;
int err = (dx > dy ? dx : -dy) / 2, e2;
for (;;) {
putpixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
e2 = err;
if (e2 > -dx) { err -= dy; x0 += sx; }
if (e2 < dy) { err += dx; y0 += sy; }
}
}
原教材版本使用累積誤差變數 x, y 與 distance,邏輯等價但較冗長;上方為標準 Bresenham 精簡寫法,行為一致。
/* 空心矩形:四條邊 */
void box(int x0, int y0, int x1, int y1, int color)
{
for (int x = x0 + 1; x < x1; x++) {
putpixel(x, y0, color);
putpixel(x, y1, color);
}
for (int y = y0; y <= y1; y++) {
putpixel(x0, y, color);
putpixel(x1, y, color);
}
}
/* 實心矩形:逐列填滿 */
void fillbox(int x0, int y0, int x1, int y1, int color)
{
for (int y = y0; y <= y1; y++)
for (int x = x0; x <= x1; x++)
putpixel(x, y, color);
}
circle 僅計算 1/8 圓弧 (x ≤ y 區域),透過 plot_circle 利用八向對稱一次繪製 8 點,並支援 asp_ratio 修正長寬比:
static void plot_circle(int x, int y, int cx, int cy, int color)
{
int xs = (int)(x * asp_ratio), xe = (int)((x + 1) * asp_ratio);
int ys = (int)(y * asp_ratio), ye = (int)((y + 1) * asp_ratio);
for (int xi = xs; xi < xe; xi++) {
putpixel(cx + xi, cy + y, color);
putpixel(cx + xi, cy - y, color);
putpixel(cx - xi, cy - y, color);
putpixel(cx - xi, cy + y, color);
}
for (int yi = ys; yi < ye; yi++) {
putpixel(cx + yi, cy + x, color);
putpixel(cx + yi, cy - x, color);
putpixel(cx - yi, cy - x, color);
putpixel(cx - yi, cy + x, color);
}
}
void circle(int cx, int cy, int r, int color)
{
int x = 0, y = r, d = 3 - 2 * r;
while (x < y) {
plot_circle(x, y, cx, cy, color);
if (d < 0) d += 4 * x + 6;
else { d += 4 * (x - y) + 10; y--; }
x++;
}
if (x == y) plot_circle(x, y, cx, cy, color); /* 45° 點 */
}
int main(void)
{
init256();
box(100, 100, 200, 200, 35); /* 空心方框 */
circle(300, 250, 200, 35); /* 圓 */
fillbox(200, 200, 350, 350, 35); /* 實心方框 */
getch();
exit256();
return 0;
}
selectpage 即雙緩衝雛形,現代交換鏈本質相同。這套程式碼雖屬 DOS 年代產物,卻浓縮了電腦圖形學最核心的「從數學定義到像素輸出」完整鏈路,值得每位系統程式設計師細讀、重寫、內化。