序章 · 一帧画面是怎么诞生的
你按下 A 键,屏幕上的马里奥跳了起来。整个过程快到你来不及思考——但你看到的,其实只是一帧画面。一帧只在屏幕上停留约 16 毫秒,比你眨一次眼还短得多。
就在这短短一瞬间里,机器内部发生了几百万件事:处理器在飞速地算,内存在不停地读写,有专门的部件在搬运数据,还有部件在一行一行地画像素。它们彼此配合、分秒不差,最后才凑成你眼前这一帧。
这一切到底是怎么发生的?本系列就把这一帧拆开,看看它是怎么诞生的。
一、什么是模拟
先问一个最朴素的问题:模拟器到底在模拟什么?
想象一台真正的 GBA 掌机,把它拆开,里面是一堆芯片:有负责计算的,有负责存数据的,有负责画面的,有负责声音的。它们各干各的活,又彼此协同——正是这种协同,让游戏跑了起来。
那模拟器做的事其实很简单:用软件,把每一块芯片的行为重新复刻一遍。 真机用电路实现的逻辑,模拟器用代码实现。我们这个系列讲的,就是 mGBA 这套开源内核。
在 mGBA 的源码里,整台机器就是一个结构体——一个叫 GBA 的结构体,把所有部件装在了一起:
struct GBA {
struct mCPUComponent d;
struct ARMCore* cpu; // CPU:一颗 ARM7 处理器
struct GBAMemory memory; // 内存
struct GBAVideo video; // PPU:画面
struct GBAAudio audio; // APU:声音
struct GBASIO sio; // 串口/联机
struct mCoreSync* sync;
struct mTiming timing; // 时钟:统一管理时间
// ...
};这一集我们不深讲代码,先认认脸,记住主角是谁。
二、主角登场
这台机器有五大件,外加一位隐形的指挥。
- CPU:GBA 用的是一颗 ARM7 处理器。它负责执行游戏里的每一条指令,是整台机器的大脑。
- 内存:游戏的代码、数据,还有正在显示的画面,都存在这里。它就像一块巨大的草稿纸,谁都能来读、来写。
- PPU(画面处理单元):把内存里的数据画成一个个像素。你在屏幕上看到的一切,都出自它的手。
- APU(声音处理单元):负责合成游戏的音效和音乐。
- DMA:一个高速搬运工,能搬运大量数据,而且不打扰 CPU 干活。
最后,是那位隐形的指挥——一根贯穿全场的时钟,让所有部件都对上同一个拍子。
三、一帧的旅程 · 上
旅程从你的手指开始。你按下 A 键,这个动作会被记录到内存里一个固定的位置,从此机器就知道:玩家按了键。
接力棒交到 CPU 手上。CPU 干活,其实是在不停地重复一个循环——取指、解码、执行:
CPU 周而复始地重复这个循环,每秒数百万次。
顺着游戏的逻辑,CPU 算出了马里奥这一帧该站在哪里。算完了,结果要写回内存——它把这一帧的画面数据,写进了专门存画面的那块显存。
四、一帧的旅程 · 下
数据写进了显存,但它还只是一堆数字。要变成画面,还得有人来搬、有人来画。
这时候 DMA 登场了,它高速地把数据搬进画面用的缓冲区。整个过程不打扰 CPU,CPU 可以继续算下一帧。
接下来轮到 PPU 出场。PPU 画画的方式很有意思——它是一行一行画的。屏幕从上到下被切成很多很多条横线,每一条叫做一条扫描线。PPU 从最上面那一行开始画起,画完一行往下挪一行,再画下一行,一直画到屏幕最底下那一行。当最后一行画完,一整帧就完整地亮了起来。
与此同时,APU 也合成好了这一帧该有的声音。
五、隐形的主宰
到这里你可能会冒出一个问题:这么多部件各干各的,凭什么能对得这么齐?
答案就是刚才那位隐形的指挥——那根贯穿全场的时钟。在这台机器里,每个部件做每一件事,都要花掉确定数量的时钟周期:取一条指令、搬一批数据、画一行像素,各有各的耗时。时钟一拍一拍地走,谁也快不了,谁也慢不了。
mGBA 是怎么管住这一切的呢?它用了一个事件调度器,专门统一管理时间:
void mTimingSchedule(struct mTiming* timing,
struct mTimingEvent* event,
int32_t when);谁该在第几个周期做哪件事,都被安排得明明白白。正是这套安排,让所有部件严丝合缝地对上了拍子。而这,恰恰就是写一个模拟器最核心的难题:周期精确。
六、系列地图
把刚才这趟旅程画成一张地图:按键、CPU、内存、DMA、PPU、APU,还有那根贯穿全场的时钟——这张地图就是我们整个系列的骨架。往后的每一集,都会停在地图上的某一站,往里深挖。
下一集,我们就从 CPU 开始,聊聊一个最根本的问题:软件,到底怎么假装成一块芯片。
一帧画面的诞生,今天我们只是看了个轮廓。这趟旅程,才刚刚开始。