Skip to content

序章 · 一帧画面是怎么诞生的

听书
0:00 / 0:00

你按下 A 键,屏幕上的马里奥跳了起来。整个过程快到你来不及思考——但你看到的,其实只是一帧画面。一帧只在屏幕上停留约 16 毫秒,比你眨一次眼还短得多。

就在这短短一瞬间里,机器内部发生了几百万件事:处理器在飞速地算,内存在不停地读写,有专门的部件在搬运数据,还有部件在一行一行地画像素。它们彼此配合、分秒不差,最后才凑成你眼前这一帧。

这一切到底是怎么发生的?本系列就把这一帧拆开,看看它是怎么诞生的。

一、什么是模拟

先问一个最朴素的问题:模拟器到底在模拟什么?

想象一台真正的 GBA 掌机,把它拆开,里面是一堆芯片:有负责计算的,有负责存数据的,有负责画面的,有负责声音的。它们各干各的活,又彼此协同——正是这种协同,让游戏跑了起来。

那模拟器做的事其实很简单:用软件,把每一块芯片的行为重新复刻一遍。 真机用电路实现的逻辑,模拟器用代码实现。我们这个系列讲的,就是 mGBA 这套开源内核。

在 mGBA 的源码里,整台机器就是一个结构体——一个叫 GBA 的结构体,把所有部件装在了一起:

c
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;      // 时钟:统一管理时间
	// ...
};

↗ 源码:include/mgba/internal/gba/gba.h#L65

这一集我们不深讲代码,先认认脸,记住主角是谁。

二、主角登场

这台机器有五大件,外加一位隐形的指挥。

  • CPU:GBA 用的是一颗 ARM7 处理器。它负责执行游戏里的每一条指令,是整台机器的大脑。
  • 内存:游戏的代码、数据,还有正在显示的画面,都存在这里。它就像一块巨大的草稿纸,谁都能来读、来写。
  • PPU(画面处理单元):把内存里的数据画成一个个像素。你在屏幕上看到的一切,都出自它的手。
  • APU(声音处理单元):负责合成游戏的音效和音乐。
  • DMA:一个高速搬运工,能搬运大量数据,而且不打扰 CPU 干活。

最后,是那位隐形的指挥——一根贯穿全场的时钟,让所有部件都对上同一个拍子。

三、一帧的旅程 · 上

旅程从你的手指开始。你按下 A 键,这个动作会被记录到内存里一个固定的位置,从此机器就知道:玩家按了键。

接力棒交到 CPU 手上。CPU 干活,其实是在不停地重复一个循环——取指、解码、执行

取指 Fetch
从内存取出下一条指令(PC 指向的地址)
解码 Decode
解析这条指令要 CPU 做什么(mGBA 用查表法)
执行 Execute
真正执行:读写寄存器/内存、跳转、运算

CPU 周而复始地重复这个循环,每秒数百万次。

顺着游戏的逻辑,CPU 算出了马里奥这一帧该站在哪里。算完了,结果要写回内存——它把这一帧的画面数据,写进了专门存画面的那块显存

四、一帧的旅程 · 下

数据写进了显存,但它还只是一堆数字。要变成画面,还得有人来搬、有人来画。

这时候 DMA 登场了,它高速地把数据搬进画面用的缓冲区。整个过程不打扰 CPU,CPU 可以继续算下一帧。

接下来轮到 PPU 出场。PPU 画画的方式很有意思——它是一行一行画的。屏幕从上到下被切成很多很多条横线,每一条叫做一条扫描线。PPU 从最上面那一行开始画起,画完一行往下挪一行,再画下一行,一直画到屏幕最底下那一行。当最后一行画完,一整帧就完整地亮了起来。

与此同时,APU 也合成好了这一帧该有的声音。

五、隐形的主宰

到这里你可能会冒出一个问题:这么多部件各干各的,凭什么能对得这么齐?

答案就是刚才那位隐形的指挥——那根贯穿全场的时钟。在这台机器里,每个部件做每一件事,都要花掉确定数量的时钟周期:取一条指令、搬一批数据、画一行像素,各有各的耗时。时钟一拍一拍地走,谁也快不了,谁也慢不了。

mGBA 是怎么管住这一切的呢?它用了一个事件调度器,专门统一管理时间:

c
void mTimingSchedule(struct mTiming* timing,
                     struct mTimingEvent* event,
                     int32_t when);

↗ 源码:src/core/timing.c#L36

谁该在第几个周期做哪件事,都被安排得明明白白。正是这套安排,让所有部件严丝合缝地对上了拍子。而这,恰恰就是写一个模拟器最核心的难题:周期精确

六、系列地图

把刚才这趟旅程画成一张地图:按键、CPU、内存、DMA、PPU、APU,还有那根贯穿全场的时钟——这张地图就是我们整个系列的骨架。往后的每一集,都会停在地图上的某一站,往里深挖。

下一集,我们就从 CPU 开始,聊聊一个最根本的问题:软件,到底怎么假装成一块芯片。

一帧画面的诞生,今天我们只是看了个轮廓。这趟旅程,才刚刚开始。