gba 个人开发日志
2023年03月22日 09:21GMT+8
本日志为 libtonc docs 的个人理解,不保证内容正确性
其它参考文档:
-
mgba 模拟器所提供的 gba 文档 使用的寄存器名没有 REG 前缀。
-
libtonc files 有很多下载。
GBA Basics
文档使用的 DKP
表示的是 devkitPro,DKA 则为 devkitARM。
multiboot 是 GBA BIOS 中一个函数的名字,用于通过连接线加载游戏到 GBA RAM 里运行。
GBA 的单卡带多人游戏使用 multiboot,在编译链接时需要使用 -specs=gba_mb.specs
GBA 硬件
GBA 使用的 CPU 是 ARM7tdmi(16.78MHz, 2 ^ 24 周期/秒)精简指令集(RISC), 支持 THUMB(16位)/ARM(32位)双指令集
devkitARM gcc 编译参数:
-mthumb
: THUMB 指令集-marm
: (默认)ARM 指令集-mthumb-interwork
: 允许混合 THUMB 和 ARM 指令集 (devkitARM 专用)
libtonc 文档推荐的是:普通的代码使用 THUMB 指令集放 ROM 中,
而对性能有要求的则使用 ARM 指令集,并将代码放置到 IWRAM 之中,
但要汇编语言才能定义代码所处的代码段(code section),见 tonc_asminc.h
,
对于 c 语言可以使用 tonc_types.h
中的 IWRAM_DATA 或 IWRAM_CODE
宏
通过 7zip 可以打开 elf 文件,可以查看到各分段,下边是某个示例:
偏移 分段 大小 #注释
0 NULL 0
0 .bss 0
4096 .crt0 528 # 在 main 函数执行之前,用于 c 语言
4624 .init 12
4636 .text 528 # 程序代码段
5164 .fini 12
5176 .rodata 5600 # rom? 估计用于取代 .data
10776 .eh_frame 4
12324 .init_array 4
12328 .fini_array 4
16384 .pad 8
16392 .comment 35
16392 .ctors 0
16392 .data 0 # 静态初始化的数据,但这里的大小是 0,估计被 .rodata 取代了
16392 .dtors 0
16392 .ewram 0
16392 .gcc_except_table 0
16392 .iwram 0 # IWRAM 区域
16392 .iwram0 0
16392 .iwram1 0
16392 .iwram2 0
16392 .iwram3 0
16392 .iwram4 0
16392 .iwram5 0
16392 .iwram6 0
16392 .iwram7 0
16392 .iwram8 0
16392 .iwram9 0
16392 .plt 0
16392 .sbss 0
16427 .ARM.attributes 44
16472 .symtab 2368 # 符号表
18840 .shstrtab 271 # section 表
20007 .strtab 1167 # 字符串表
或者在使用 gcc 链接时添加 -Wl,-Map,filename.map
可获得一个链接的信息(-Wl
用于gcc传递参数给链接器, 参数用逗号分隔)
实际上 gba 社区都是直接使用 gcc 来链接,(我尝试过
ld.exe
但是报错了)
更多的 CPU 细节建议单独搜索下 ARM 的文档,很多都是一样的。
CPU 寄存器
共 16 个 32位核心寄存器(r0-r15),可以通过模拟器的 Tools -> Disassembly
可以查看,
-
r0-r12 : 通用。
-
r13(SP) : 堆栈指针,由 BIOS 初始化,默认值因当前模式而异:
User/System: 0x03007F00 IRQ: 0x03007FA0 Supervisor: 0x03007FE0
-
r14(LR) : 链接寄存器,建议单独参考 ARM 的文档。
-
r15(PC) : 程序计数器,建议同上
-
CPSR : 程序当前状态(The Current Program Status Register)。
10000 - User mode 10001 - FIQ mode 10010 - IRQ mode 0-4 : 10011 - Supervisor mode (SVC) 10111 - Abort mode (ABT) 11011 - Undefined mode (UND) 11111 - System mode 5(T) : Thumb 状态,只读。 6(F) : 置 1 将禁用 FIQ 7(I) : 置 1 将禁用 IRQ。当进入 IRQ 模式后,GBA 默认将此值置 1。 8-27 : 保留 28(V) : 溢出(Overflow condition code) 29(C) : 进位/借位/???(Carry/Borrow/Extend) 30(Z) : 相等(Zero/Equal condition code) 31(N) : 小于(Negative/Less than condition code) 处理器模式 User : 用户模式,此模式下某些被保护的系统资源是不能被访问的 System : 特权模式, IRQ : 普通中断模式,GBA 中所有中断都在此模式下运行 FIQ : 快速中断模式,GBA 没有使用这个模式 SVC : 管理模式,是CPU上电后默认模式,用用来做系统的初始化。或者进入软中断(Software interrupt)调用时 ABT : 终止模式,当获取数据或指令失败时,比如访问非法地址,没有权限 UND : 未定义模式,当执行未定义的指令时,(用于支持硬件协处理器的软件仿真)
IO-RAM 映射寄存器才是我们需要关心的,对于 libtonc 可以在 tonc_memmap.h 中找到。
Tips : 通过模拟器的 Tools -> IO-Viewer
可以查看所有 IO-RAM 寄存器的值。
GBA 内存分区
区域 | 起始 | 结束 | 长度 | port-size | 简述 |
---|---|---|---|---|---|
System ROM | 0000:0000h | 0000:3FFFh | 16Kb | 32 bit | BIOS, 可执行但不可读 |
EWRAM | 0200:0000h | 0203:FFFFh | 256Kb | 16 bit | External Work RAM,可用于代码和数据 |
IWRAM | 0300:0000h | 0300:7FFFh | 32Kb | 32 bit | Internal Work RAM,用于代码和数据,32位总线宽度,适合 ARM 指令集代码 位于 CPU 内部,速度最快 |
IO RAM | 0400:0000h | 0400:03FFh | 1Kb | 16 bit | Memory-mapped IO registers,控制图形,声音,按钮和其他功能的地方 |
PAL RAM | 0500:0000h | 0500:03FFh | 1Kb | 16 bit | Palettes RAM,2个调色板,分别用于 BG 和 Sprite |
VRAM | 0600:0000h | 0601:7FFFh | 96Kb | 16 bit | Video RAM |
OAM | 0700:0000h | 0700:03FFh | 1Kb | 32 bit | Object Attribute Memory 用于控制 Sprite。 |
PAK ROM | 0800:0000h | 变化的 | 变长 | 16 bit | 游戏卡带。 除非是 multiboot,那么这里将是游戏所在位置以及游戏运行的地方。 最大限制为 32M,16位总线宽度适合 THUMB 指令集的代码 |
Cart RAM | 0E00:0000h | 变化的 | 变长 | 8 bit | 用于保存游戏进度,长度虽非固定,但 64KB 已经足够。 |
所有 RAM(除了 Cart RAM)在启动时将由 BIOS 清 0。
问题:GBA 运行的内存堆栈有多大了?
misc
显示:
- 240x160像素, 每像素颜色为 15 位
- 3 种 bimap 模式和 3 种 tile/map 模式
- 变换效果有: 旋转, 缩放以及斜切(skew).
- 特殊效果有: 马赛克, 图层混合(blend)
声音:
6通道分别为:4 个波形通道(2方波 + general wave + noise), 2 个 directsound 通道用于播放样本和音乐
其它:
- 10 个按钮
- 14 个硬件中断
- 通过连接线最多支持 4 个玩家
开发环境配置
可从 devkitpro installer 项目处下载
devkitpro 基于 MSys2 环境,为了防止和 cygwin 冲突, 需要删除 devkitpro 对于 PATH 的添加。
Programmer’s Notepad 2 代码编辑器
第一个 GBA 示例
参考 (0-first)
视频规格介绍
GBA 有一个 240 像素宽,160 像素高的 LCD 屏幕,能够显示 32768(15位)颜色, 刷新率略低于每秒 60 帧(59.73 Hz)。 可以有 5 个图层:4 个背景(BG)和一个 Sprite 图层, 能够产生一些特殊效果,包括图层混合(blend)和马赛克,当然还有旋转和缩放。
绘制和空白期间(Draw and blank periods)
CPU 每秒执行 2^24
, 因此 CPU 运行 280896 个周期需要 280896 / (2^24) = 0.0167
秒,
反过来就是每秒接近 60帧。
subject | length | cycles |
---|---|---|
pixel | 1(1像素为2字节) | 4, 绘制 1 个像素需要 4 个时钟周期 |
HDraw | 240px | 960 = 4 x 240 |
HBlank | 68px | 272, 水平空白期间 |
Scanline | HDraw + HBlank | 1232 |
VDraw | 160 x Scanline | 197120 |
VBlank | 68 x Scanline | 83776, 垂直空白期间 |
refresh | VDraw + VBlank | 280896 |
在绘制下一条扫描线(scanline)之前,会有一个 68 x 1
像素宽度的 “水平空白期间(HBlank)”,
同样在绘制完 160 扫描线后,同样会有 68x(240+68)
的“垂直空白期间(VBlank)”。
扫描线空白期间意味着 CPU 处于空闲状态,因此适合在此时填充某些游戏数据或检测按键并执行相关动作 游戏的 MainLoop 应该在 VBlank 期间运行,而对于 HBlank 估计只有那些追求极致的程序才会用到。
libtonc 或 libgba 都提供了 VBlankIntrWait 的函数。
// libtonc 的方式
int main() {
irq_init(NULL); // 初始化中断请求
irq_add(II_VBLANK, NULL); // 启用 VBlank
// 这里放其它初始化的代码。
// ...
while (1) {
VBlankIntrWait();
// 将 mainloop 的代码放在这里,
// ...
}
}
// libgba 的方式
int main() {
irqInit(); // 初始化中断请求
irqEnable(IRQ_VBLANK); // 启用 VBlank
// 这里放其它初始化的代码。
// ...
while (1) {
VBlankIntrWait();
// 将 mainloop 的代码放在这里,
// ...
}
return 0;
}
显示寄存器
-
REG_DISPCNT((vu32*) 0400:0000h)
: 用于显示控制…,是一个文档会频繁提到的寄存器F | E | D | C | B | A | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 ~ 0 ---+---+---+---+---+---+---+---+---+---+---+---+---+------ OW| W1| W0|Obj|BG3|BG2|BG1|BG0| FB| OM| HB| PS| GB| Mode Mode : video 模式。0, 1, 2 为 tile/map 模式, 3, 4, 5 则为 bitmap 模式 GB : 0=GBA, 1=GBC; 只读, 由 BIOS 设置。 PS : 用于 (bitmap 模式 4 或 5),通过切换 0, 1 以实现页面交换(page flipping) HB : 1=允许访问 OAM 当扫描线处于 HBlank 时,通常当处于 VDraw 时 OAM 是锁定的 OM : Sprite 映射模式。 0=2D, 1=1D 控制 GBA 根据 Sprite 第一个 tile 的位置,自动选择剩余的 tiles FB : 1=使屏幕变空白,可用于节省电量当不需要屏幕时。在强制空白期间将提高传输到 VRAM 的速率。 BG0~3 : 1=启用相应的图层 Obj : 1=启用 Sprite 图层 W0~O1 : 1=启用相应的窗口, 窗口用于遮盖某些区域(例如塞尔达中的灯) OW : 1=启用 Sprite 窗口。 不同 video 模式时的一些影响: Mode Sca/Rot BG0-BG3 Size Tiles Colors Features 0 No 0123 256x256...512x512 1024 16/16...256/1 SFMABP 1 Mixed 012- (BG0, BG1 同上,BG2 同下) 2 Yes --23 128x128...1024x1024 256 256/1 S-MABP 3 Yes --2- 240x160 1 32768 --MABP 4 Yes --2- 240x160 2 256/1 --MABP 5 Yes --2- 160x128 2 32768 --MABP 关于 video 模式的选择,通过模拟器打开 “缩小帽”: 发现一直处于 tile/map 的 0 模式也就是说没使用 Sca/Rot(Affine) 变换。 OBJ:人物, 发光的壁炉,桌子,水桶 似乎是有碰撞检测的,包括门和门梁。 BG0: 文本对白框,生命条,宝石袋 (prio: 0) BG1:宝箱, 墙上的画,大树,屋顶 (prio: 1) BG2:背景,地上会动的花也是这图层的,如何做到的了? (prio: 2) BG3:半透明的云阴影, 当角色走到云下也会受影响 (prio: 1) 除了 OBJ 发现 BG1, BG2 也是有碰撞检测的,说明外部大概有个不可见的 “碰撞层” “塞尔达” 对图层的使用比 “缩小帽” 准确。 OBJ: BG0: 文本层 BG1: 一些遮盖,比如房顶的后边,大树的树叶都能遮住主角, BG2: 背景层, BG3: 特效覆盖图层,下雨, 雨点水花 - 查看地图有使用了 Sca/Rot(Affine) 图层,看起来像是 3D 的 - 使用了 64x64 的大图层 - 标题背景只用了一个图层就做出了那种近景快,远景慢移的效果
-
REG_DISPSTAT((vu16*) 0400:0004h)
: 显示状态,用于显示中断。F E D C B A 9 8 |7 6| 5 | 4 | 3 | 2 | 1 | 0 -----------------+---+---+---+---+---+---+----- VcT | - |VcI|HbI|VbI|VcS|HbS|VbS VbS : VBlank status, read only. Will be set inside VBlank, clear in VDraw. HbS : HBlank status, read only. Will be set inside HBlank VcS : VCount trigger status. Set if the current scanline matches the scanline trigger ( REG_VCOUNT == REG_DISPSTAT{8-F} ) VbI : VBlank interrupt request. If set, an interrupt will be fired at VBlank. HbI : HBlank interrupt request. VcI : VCount interrupt request. Fires interrupt if current scanline matches trigger value VcT : VCount trigger value. If the current scanline is at this value, bit 2 is set and an interrupt is fired if requested.
-
REG_VCOUNT((vu16*) 0400:0006h)
: 扫描线计数器F E D C B A 9 8 | 7 6 5 4 3 2 1 0 ----------------+---------------- - | Vc Vc : 扫描线计数器。取值范围: 0-227 (160 + 68)
垂直同步 1(VSyncing Part 1)
本小节的内容只用于探讨 GBA 中实现 VSync 的各种方式,可以跳过直接使用 VBlankIntrWait()
。
有多种方式实现 vsync,最常用的 2 种方法分别为:1) 在循环中检测 REG_VCOUNT 或者 REG_DISPSTAT。 例如:由于 VBlank 开始于第 160 行扫描线,因此可以通过检测 REG_VCOUNT:
#define REG_VCOUNT *(u16*)0x04000006
void vid_vsync()
{
while(REG_VCOUNT < 160);
}
可惜上边代码存在一些问题:首先由于 REG_VCOUNT 是被外部由硬件修改的,
但编译器认识不到这点。因此要使用 volatile
。
其次,仅仅等待 VBlank 是不够的,当处于 VBlank 中再次调用 vid_vsync() 时, 将导致 BUG,使得不能同步到 60 帧,因此还需要等待下一次的 VDraw。 修改后代码类似于:
// 注意 u16 -> vu16, 而实际上 tonc 库中已经改成了 vu16 形式
#define REG_VCOUNT *(vu16*)0x04000006
void vid_vsync()
{
while(REG_VCOUNT >= 160); // wait till VDraw
while(REG_VCOUNT < 160); // wait till VBlank
}
// 这几行和 tonc.video.h 中的一样,但目前不推荐使用这个函数。
// 这里 CPU 将使用“中断” 异步修改 REG_VCOUNT。
上边只是其中的一种方法,而另一种方法是:2) 检测 REG_DISPSTAT 的 0 位.
虽然这两种方法实现了简单的 vsync, 但却是很糟糕的, 尽管在 while 循环中什么也没做,但仍旧浪费了大量的电池。 推荐的 vsync 方法是在完成后使 CPU 进入低电模式,之后使用中断恢复。
Bitmap 模式
位图模式适合 gba 入门,因为内存和屏幕像素存在着一对一的关系。
位图是一个 width x height
的颜色矩阵(或颜色索引矩阵)。
像素可以使用坐标 (x, y)
来表示,(0, 0)表示位图的左上角。
位图在内存中与一维数组相对应。
bpp(Bits Per Pixel)表示每像素颜色的位数,bpp 的值将会影响图像的字节宽度(pitch 或 stride)。
注:原文使用了“扫描线宽度”一词,会让人误以为是 240 + 68 相对应的字节宽度
设置 VRAM 的控制寄存器 REG_DISPCNT 为 3, 4, 5 为 bitmap 模式:
mode | W x H | bpp | size | 页面交换 |
---|---|---|---|---|
3 | 240 x 160 | 16 | 1x12C00h | 不支持,因为只有一个 page |
4 | 240 x 160 | 8 | 2x9600h | 支持 |
5 | 240 x 128 | 16 | 2xA000h | 支持 |
bitmap 模式只有一个图层作为背景层(因为 VRAM 尺寸的限制), 并且不能使用硬件滚动(scroll),所以不适合用于绝大多数游戏。
示例(1-bitmap-mode3)使用了模式 3 并启用 BG2 作为示例(只能选择 BG2)。
模式 4 是另一种 bitmap 模式,同样有着 240x160
的帧缓冲区(VRAM),
只是使用 8bpp 颜色深度。8bpp 在这里将表示为调色板的颜色索引。
使用 8bpp 意味着可以有 2 个页面,因此可以使用“页面交换”来平滑视频输出。
需要注意的是:帧缓冲区(VRAM)要求双字节对齐,不支持直接写入单个字节。 包括 PALRAM,OAM 也同样有这个限制。因此 libtonc 中 m4_plot(SetPixel) 函数如下:
INLINE void m4_plot(int x, int y, u8 clrid)
{
u16 *dst= &vid_page[(y*M4_WIDTH+x)/2]; // Division by 2 due to u8/u16 pointer mismatch!
if(x&1)
*dst= (*dst& 0xFF) | (clrid<<8); // odd pixel
else
*dst= (*dst&~0xFF) | clrid; // even pixel
}
模式 4 有个示例(目录 2-bitmap-mode4-pageflip)演示了如何交换页面。
GBA 没有文件系统,意味着你需要把 图片,音乐,文本等这些文件已二进制的形式 嵌入到 ROM 当中。有很多方法可以做到这一点:
最常见的方式是将文件转换成 c 语言的数组形式,然后通过 #include
引入它们
- incbin 一个 c 头文件,使用了汇编指令嵌入文件
虽然 GBA 没有本地文件系统,但是可以自己实现,一个比较常见的是由 GBADev 社区维护的 GBFS(只是有点难用)。
实际上,现在已经有文件系统了,自 2006 年由 Chishm 实现的 libfat, 一个 FAT-like 的文件系统可用于 GBA 和 NDS,你可以在 libgba 里找到它。
编写将二进制文件转换为 C 或 asm 数组的工具相对容易。
实际上,devkitARM 已经提供了这两个:raw2c.exe
和 bin2s.exe
,
可以在 devkitPro/tools/bin
找到。另外还提供了 gbfs.exe
用于打包多个文件
能够将二进制文件附加到游中只是其中的一部分,还有其它要做的, 例如一个图片,需要转换成 GBA 能识别的格式(r5_g5_b5 的颜色值)。 目前有很多格式转换工具用于 GBA,例如:
游戏通常包含着大量的数据,而 EWRAM(256K) 似乎不够用。 因此需要使用压缩来处理合适的文件,GBA BIOS 自身带有 LZ77 和 Huffman 解码程序, 一些文件转换工具应该能执行相应的压缩
(原文档这里讲的是 makefile,这里就不复述了)
还有一个要注意的是地址对齐,在 RISC 中很重要,将会导致某些写内存操作失效,
要注意的是在 THUMB
状态下,内存默认按 2字节对齐,一些地方可能需要使用 ALIGN 宏来对齐 4字节。
还有 struct 中的字段,以及 struct 的大小,以及 c 语言默认对齐 struct 变量内 sizeof 最大的字段。
memcpy 的快速复制需要 SRC 和 DST 必须是 4字节对齐,并且长度至少 16 字节。
对于 2字节对齐的地址可以使用 memcpy16
memset 其实是单字节的,可以使用 tonc 库中更快的 16, 32位的汇编语言版本。
GBA 按键
REG_KEYINPUT((vu16*) 0400:0130h)
: 按键状态
F - A | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|
- | L | R | down | up | left | right | start | select | B | A |
需要注意的是 REG_KEYINPUT 的默认值是 0x03FF
,也就是说当某个键按下时相应的键位将会被设置为 0
REG_KEYCNT((vu16*) 0400:0132h)
: 按键控制,用于按键中断,类似于显示中断的 REG_DISPSTAT。
F | E | D C B A | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Op | I | - | L | R | down | up | left | right | start | select | B | A |
要使用它首先需要了解中断的相关编程。
bits | name | desc |
---|---|---|
0-9 | keys | 检测按键用于引发按键中断 (KEY_x) |
E | I | 是否启用按键中断 (KCNT_IRQ) |
F | Op | 布尔运算符用于“确定”是否引发按键中断,0 = OR,有“一个”指定的键按下就引发。 1 = AND,“所有”指定的键按下才引发。 |
(通过模拟器发现,按键没有按下时值为 0,和 REG_KEYINPUT 相反, 或者要启用某些开关比如 REG_IE 和 REG_IEM 才能观察 )
libtonc 对按键的处理有几个地方感觉非常不错:
-
使用
curr_input OP prev_input
的方式检测按键的多种状态。( curr ^ prev) & key // 按键刚更改了状态,比如按键刚刚按下或刚刚释放, ( curr & prev) & key // 按键保持按下状态至少 2 帧。u32 key_held(u32 key) ( curr & ~prev) & key // 按键刚按下, u32 key_hit(u32 key) (~curr & prev) & key // 按键刚释放, u32 key_released(u32 key)
那么如何检测例如 A 和 B 同时刚刚按下的那个状态了?
(curr & ~prev) & (A | B)
??? -
通过判断诸如
LEFT - RIGHT
获得的三种状态: -1, 0, 1
Sprite 和 Tile 背景概述
先简单看下 1KB 大小的调色板(PAL-RAM),由于 BG 和 Sprites 各独立占用 512 字节,因此:
512 = 2(15颜色深度) * 16种颜色 * 16个调色板,(简称为:4bpp 或 16/16)
或
512 = 2(15颜色深度) * 256种颜色 * 1个调色板,(简称为:8bpp 或 256/1)
由于 BG 只能占用 512 字节,那么问题来了,如果 BG 既要支持 4bpp 又要支持 8bpp, 那么只能混合放一起,也就是说对于 BG 调色板 4bpp 和 8bpp 是重叠在一起的。 这样的话,在选取调色板颜色时就有点麻烦了。
如果打算制作调色板方面的工具可以参考一些如:颜色量化(Color Quantization),双边滤波(Bilateral Filtering)
这里只讨论 REG_DISPCNT 的 tile/map 模式,这个模式之所以使用了 2 个单词, 是因为在此模式下 VRAM 除了存放图层数据到BG0-BG3(screenblocks) 还要把 charblocks 也塞进去。 由于 charblocks 分为 2 个区域分别用于 BG 和 Sprites 因此 VRAM 将被划分为 3 个区域:
06000000-0600FFFF 64KB BG-charblock 和 screenblock 重叠。
06010000-06017FFF 32KB Sprite-charblock(Sprite-screenblock 则独立存放于 OAM 之中)
REG_BG0CNT-REG_BG3CNT寄存器分别对应 4 个图层(BG):
Bits Desc
0-1 优先级 (0-3,0=最高(前),如果优先级相同则 BG0 排最前(高))
2-3 charblock 偏移 (0-3,以16KB为单位,addr = base + [0-3] * 16KB)
4-5 未使用,必须为 0
6 马赛克特效 (0=关闭)
7 调色板选项 (0=4bpp,1=8bpp)
8-12 screenblock 偏移(0-31,以2KB为单位, addr = base + [0-31] * 2KB)
13 对于 BG0/BG1, 未使用 (似乎 NDS 有使用)
13 对于 BG2/BG3, 图层混合 0=透明,1=包围(Wraparound)
14-15 screenblock 尺寸
screenblock 又分为 2 种类型分别为 text BG 和 Sca/Rot(Affine)-BG.
text-BG 的 screenblock 尺寸
00 = 32 x 32 tiles
01 = 64 x 32 tiles
10 = 32 x 64 tiles
11 = 64 x 64 tiles
Affine-BG 的 screenblock 尺寸
00 = 16 x 16 tiles
01 = 32 x 32 tiles
10 = 64 x 64 tiles
11 = 128 x 128 tiles
一些文档使用 text-tiles, char-tiles 表示 charblocks
screenblock 相当于一个 tile-index 数组,尺寸大小除了依赖于 REG_DISPCNT 的 tile/map 模式, 还和对应的 REG_BGxCNT 寄存器相关连,从 128x128 到 1024x1024 像素。
Sprite 的尺寸相对较小在 8x8 到 64x64 像素之间,共 128(1024 / 8字节)个可以自由移动。
对于 Sprite 只需要提供 tile-index 的第 1 个,就能自动识别剩余的 tile-index,
因为 Sprite 的 tiles 始终相邻,但受标记位 REG_DISPCNT(6)
的影响。
此外 Sprite 也有额外的 flipping 标志位, 或者 4bpp 的调色板属性,
但不一样的是这些标志位或属性值将用于整个 Sprite,而不是 screenblock 其中的单个 tile。
(从这里开始一切变得混乱,各文档使用词都不一致)
Subject | 一般文档(gbadev.net) | libtonc 文档 | mgba-emu 文档
-------------------- | -------------------- | -------------------- | -------------------- Sprite and bg image data | tiles | tiles | tiles? Tile-map entries | tiles (confusion?) | screenblock entries (SE) | bg map? Matrix for transformations | sca/rot matrix | affine matrix (P) | Sprite types | ?? *vs* sca/rot | regular *vs* affine | Background types | text bg *vs* sca/rot bg| regular *vs* affine | text bg *vs* sca/rot bg Depository for sprite tiles (0601:0000) | OAMData | tile_mem_obj | OAM (0700:0000) | OAMMem | oam_mem |
GBA 硬件除了光栅化外,还负责其它几个操作:
- 排除某个的指定颜色(color keying),相当于指定某个颜色为透明。
如果像素值为 0 那么就是透明的(TODO: 到底是像素值还是颜色值为 0 才透明了?)
- 翻转(flipping),透明混合(alpha-blending)和 仿射变换(affine)。
image-data 表示 tiles 和调色板。
单个 tile 是一个 8x8 的 color-index数组(4bpp 或 8bpp 的颜色索引值)。
4bpp-tile 每个字节能表示 2 个颜色像素点,而 8bpp-tile 则为 1 个颜色像素点。
假如使用 8bpp 那么 1像素就占用 1 字节, 那么一个 tile = 8x8
就是 64 字节,
对于 4bpp 者只要 32 字节。
当将 Width*Height tiles
复制到 VRAM 时,是以一维数组的形式复制过去。
TODO:那么如何显示一个 12 x 12 像素大小的字符了?
如果您想制作自己的转换工具这里有一个小技巧就是:分阶段作业。 不要直接将位图写成数据文件,而是创建一个平铺函数并将 tile 转换成 1 格宽 H 格高的位图, 然后正常导出。(???如果允许变宽的 tile(非固定8像素宽度的硬编码),也可用于其它目的。 例如:要创建 16x16 的 Sprite,首先布置宽度=16,然后再 width=8)
每组 tiles(charblocks) 大小为 16KB, (16KB / 32(4bpp) = 512个,16KB / 64(8bpp) = 256个), 那么整个 VRAM 刚好可以放下 6个 charblocks,前边4个用于 BG0-BG3, 剩下的2个用于 Sprites。
至于 charblocks 的地址前边已经描述过了,BG0-BG3 参考 REG_BGxCNT 寄存器, 而 Sprites charblocks 者从第 64KB 开始。
??? 对于 tile-indxing,Sprites 使用 32 字节作为偏移量, ??? 而 BG-map 使用的偏移量依赖于其颜色深度(4bp=32偏移,8bpp=64偏移)。
BG-map 和 Sprites 都使用 10bits 作为索引值的宽度,因此可索引 1024 个 tile。 注意;如果你通过 REG_BGxCNT 越界访问了 Sprite 的 charblocks(REG_BGxCNT 可以从第 0-3 任意一个块开始,以及索引 1024 明显大于 512/4bpp 或 256/8bpp)代码虽然可以在模拟器中可运行, 但实机不允许你这样做,这就是为什么要上实机测试。 参看 cbb_demo
// tile 8x8@4bpp: 32bytes; 8 ints
typedef struct { u32 data[8]; } TILE, TILE4;
// d-tile: double-sized tile (8bpp)
typedef struct { u32 data[16]; } TILE8;
// tile block: 32x16 tiles, 16x16 d-tiles
typedef TILE CHARBLOCK[512];
typedef TILE8 CHARBLOCK8[256];
#define tile_mem ((CHARBLOCK* )0x06000000)
#define tile8_mem ((CHARBLOCK8*)0x06000000)
//In code somewhere
TILE *ptr= &tile_mem[4][12]; // block 4 (== lower object block), tile 12
// Copy a tile from data to sprite-mem, tile 12
tile_mem[4][12] = *(TILE*)spriteData;
常规 Sprite
游戏中提到 Sprite,通常指的是: 一个小的动画对象可以在 BG 上自由移动, 例如:游戏主角,NPC,怪物,子弹或者别的,可以使用模拟器选择显示或隐藏 OBJ 图层来查看。
使用 Sprite 和 bitmap-BG 起来有点棘手,但不是很多。它要求必须是 8x8 像素点的 tile, 其三个基本要点分别是:
- 加载图形和调色板到 Sprites 专用的 VRAM 和调色板空间
- 正确设置 OAM,使用合适的 tile 和正确的尺寸。
- 在 REG_DISPCNT 中启用 Sprite,和相应的映射模式(mapping-mode)。
Sprite 使用其专属的 32KB VRAM(O-VRAM)用于保存 tiles(charblock) 数据,
tile 必须是 8x8 像素,有 4bpp 或 8bpp 两种类型。
Sprite 的计数(Counting)从 O-VRAM 的最低字节开始,并且始终以 32 字节作为偏移量,
那么对于 Sprite#1 指的是 O-VRAM + 1 x 32KB
处的 tiles,无论 4bpp 还是 8bpp。
// 这里 tonclib 库直接使用了 tile_mem 代被整个 VRAM,而不是单独的 O-VRAM
// 对于 O-VRAM 中 #123 tiles,可以有:
tile_mem[4][123];
// 其地址则为:
&tile_mem[4][123];
由于每个 charblock 有 16KB,因此有 16KB / 32 = 512个 tiles,那么 2个charblock就是 1024个, 但如果处于 video bitmap 模式,则只能用最后的 512 个。
大多数游戏的 Sprites 使用多个 tiles,而并非单个 tile。 对于 BG 需要明确地指示每个 tile 的位置,但对于 Sprite 则只需要第一个 tile 位置, 剩下的将根据 REG_DISPCNT 中 mapping-mode 的标志位(默认为2D方向)自动选择。
OAM(Object Attribute Memory)
tile/map 模式时,GBA 具有特殊的硬件绘制 Sprite,但有一个限制是 1 条扫描线最多 只能绘制大概 960像素的 Sprite对象。
注意,由于 OAM 不支持以字节为单位往上边写数据,不知道 bit-field 的形式可不可以
OAM 具有2种 Sprite 属性结构:(不同文档使用的名字不同)
// OBJ_ATTR: 用于 regular sprite attr
typedef struct tagOBJ_ATTR {
u16 attr0;
u16 attr1;
u16 attr2;
s16 fill;
} ALIGN4 OBJ_ATTR;
// OBJ_AFFINE: 包含了 transformation 数据
typedef struct OBJ_AFFINE
{
u16 fill0[3];
s16 pa;
u16 fill1[3];
s16 pb;
u16 fill2[3];
s16 pc;
u16 fill3[3];
s16 pd;
} ALIGN4 OBJ_AFFINE;
// 由于 devkitARM r19 不再对 struct 4字节对齐,(c 语言标准好像就是对齐最大的字段)
// 因此这里使用了 GCC 的对齐属性宏 __attribute__((aligned(4)))
上边的结构你会发现 4 个 OBJ_ATTR 刚好能放入 OBJ_AFFINE 之中。 由于 OAM 的大小为 1KB,因此我们有 128 个 OBJ_ATTR 或 32 个 OBJ_AFFINE 对象。
OBJ_ATTR 用于控制 Sprite,由 3个 16位的属性组成,分别为用于:大小,形状,位置,tile 等等。
Attribute 0:
0-7 Sprite 的 Y 坐标值
8-9 (Affine) object mode
00 标记为 regular sprite (ATTR0_REG)
01 标记为 affine sprite (ATTR0_AFF)
10 关闭渲染,用于 隐藏 sprite (ATTR0_HIDE or ATTR0_AFF_DBL_BIT)
11 Double-size affine object (ATTR0_AFF_DBL)
A-B GFX 模式
00 正常渲染
01 启用透明混合 (ATTR0_BLEND)
10 Object 为 object windows 的部分,Sprite 本身不会被渲染,但用作 BG 或其它 Sprite 的蒙板。(ATTR0_WINDOW)
11 ???禁止(Forbidden)
C 1=启用马赛克效果 (ATTR0_MOSAIC)
D 0=4bpp, 1=8bpp (ATTR0_4BPP, ATTR0_8BPP)
E-F Sprite 形状,与 attr1(E-F) 一起决定了 Sprite 的大小。
00 (ATTR0_SQUARE)
01 (ATTR0_WIDE)
10 (ATTR0_TALL)
11
GBA sprite sizes:
shape/size-> | 00 | 01 | 10 | 11 |
---|---|---|---|---|
00 | 8x8 | 16x16 | 32x32 | 64x64 |
01 | 16x8 | 32x8 | 32x16 | 64x32 |
10 | 8x16 | 8x32 | 16x32 | 32x64 |
Attribute 1:
0-8 Sprite 的 X 坐标值
9-D Affine index,仅 attr0(8)=1 时才起作用。
C-D Horizontal/vertical flipping flags.
E-F Sprite size
Attribute 2:
0-9 the base tile-index. (如果是 video bitmap 模式,那么这个值应该是 512 或更高)
A-B 优先级(Priority)数字小的将呈现在画面的最前边。
C-F Paletee-bank 仅当 4bpp 颜色值时有效。
OAM 双缓冲
感觉没什么必要,因为你只能在 VBlank 期间修改 VRAM 和 OAM,但如果你想要在任何时候都能修改 OAM 可以创建一个 buffer,等到 VBlank 期间复制就行了。
OBJ_ATTR obj_buffer[128];
OBJ_AFFINE *const obj_aff_buffer= (OBJ_AFFINE*)obj_buffer;
常规 Tile BG
一组独特的 tiles 称之为 tileset。charblocks 和 screenblocks 重叠存放于 64KB of VRAM ,
tileset -> charblocks, tilemap -> screenblocks
。
mem | 0600:0000h | 0600:4000h | 0600:8000h | 0600:C000h ----------- | ---------- | ---------- | ---------- | ---------- charblock | 0 | 1 | 2 | 3 screenblock | 0 ... 7 | 8 ... 15 | 16 ... 23 | 24 ... 31
通过 REG_DISPCNT 你可以选择激活相应的图层(BG0-BG3)。
通过 REG_BG0CNT-REG_BG3CNT 设置各图层的 charblock/screenblock 偏移值, 和 tilemap 的大小。 每个 charblock 为 16KB, 每个 screenblock 则为 2KB,要注意分配别让数据发生重叠。
此外每个 BG 还有 2个用于滚动的只写寄存器: REG_BGxHOFS,REG_BGxVOFS
,用于控制偏移,
由于是只写,不能读取数据,因此查看它们只能通过 print 输出“屏幕的坐标”,注意是屏幕坐标,而不是地图坐标。
滚动寄存器只适用于 text(regular) BG,其单位为像素点。
当你增加 scrolling 的值时,你将移动屏幕往地图的右边,现在假设地图坐标 P 和屏幕坐标 Q, 那么有
Q + dx = P
Q = P - dx
text(regular) BG
text(regular)-BG 的 tile-maps 中元素使用了 2字节作为索引值和其它信息。 而 sca/rot(affine)-BG 只使用 1 个字节作为索引值但使用 8bpp 的颜色索引值。
bits | desc (仅适用于text(regular)-BG) |
---|---|
0-9 | tile-index value |
A | 水平翻转(flipping) |
B | 垂直翻转(flipping) |
C-F | 调色板索引值(Palette bank)当使用 4bpp 颜色值时 |
VRAM 总共有 32 个 长度为 2KB 的 screenblock 用于保存 tilemap 数据, 2KB 只能保存 32x32 tiles,对于更大的的地图则需要连续的 screenblock,
video mode = 0
时, BG0-BG3 的布局可能如下:
32x32 | 64x32 | 32x64 | 64x64 |
---|---|---|---|
[0] | [0][1] | [0] [1] |
[0][1] [2][3] |
上边 4 个 screenblock-set 在内存上不是连续的,但 screenblock-set 内部内存是连续的,这没什么好说的。
但是还有一个重要问题是:假设我们使用 64x32 模式(通过设置 REG_DISPCNT 的 map-size 为 1) 这时复制一个 64x32 大小的 tilemap 到 screenblocks 要怎么操作了?
要注意 tilemap(64x32) 和 screenblocks(64x32) 并不是一一对应的,因为每个 screenblock 只有 2KB 大小, 例如对于 tilemap(64x32) 第一行数据在内存上是连续的,但是 screenblocks(64x32) 逻辑上的第一行则是:
how to copy tilemap(64x1) to screenblock-set(64x1)
32 tile -> screenblock-set[0]
32 tile -> screenblock-set[1]
# 我猜这就是为什么文档的前面会有建议:当做 bitmap 转换工具时要求一个 1Width*NHeight 的转换函数。
# 这样做的原因也许是实现 scroll 的方法会更简单。
注: 游戏”缩小帽”, 32x32 模式一样可以滚动地图,模式直接使用 x + y * w 即可。 魂斗罗使用了的 BG2 图层用了 64x32 模式,”三角力量” 使用的 64x64
现在需要一个函数用来计算 tilemap(tx, ty) 与 screenblock-set 的关联, 函数非常适用于动态地在某个位置显示 tile,只要简单地修改 screenblock 中某个 tile-index 的值即可, 一些简单的单图层动画估计就是使用了这个特性
要写出这个函数,首先我们要清楚 screenblock 只有 2KB,
2KB对于 text(regular)-BG 一行只能显示 32 个 tiles,全部是 宽32 x 高32 = 1024 个 tile.
因此当给出坐标 (36, 0)
,那么一眼就能看出一定是在第2个 screenblock 上(screenblock-set[1])
把问题简化下,便于思考:
bitmap -> sceen block
[a][b][c][d] [a][b] | [c][d]
[e][f][g][h] [e][f] | [g][h]
虽然看上去很像,但转换成一维数组后:
bitmap = 'abcdefgh'; 可以使用 x + y * 4 计算数组索引
bitmap = 'abefcdgh'; 使用 x + y * 4 是错误的
已知 bitmap(2, 0) = bitmap[2] = 'c'
需要一个函数计算出 n 的值满足 sblock[n] = 'c'
下别给出 c 语言的实现方式,你不用担心除法或取模计算的性能,因为编译器能优化。
// Get the screen entry index for a tile-coord pair
// pitch 表示 tilemap 的 tile 宽度,
uint se_index(uint tx, uint ty, uint pitch)
{
uint sbb = (ty/32) * (pitch/32) + (tx/32);
return sbb * 1024 + (ty % 32) * 32 + (tx % 32);
}
/*! 更快速的实现(但可能不安全),需要 REG_BGxCNT 寄存器 作参数
*/
uint se_index_fast(uint tx, uint ty, u16 bgcnt)
{
uint n = tx + ty * 32;
if(tx >= 32)
n += 0x03E0; // (h - 1) * w
if(ty >= 32 && (bgcnt & BG_REG_64x64) == BG_REG_64x64)
n += 0x0400; // (h * w)
return n;
}
// 注意返回的 n 可能需要乘以 2,因为 text(regular)-BG 使用2字节的索引值,
// 或者使用2字节宽的数组,这只适用于 text(regular)-BG,
注意:对于 sca/rot(affine)-BG,不需要做这些的换算,直接用 x + y * w
就好,因为其结构是线性的。
2个需要注意到的细节:
首先是我们知道 screenblock 中元素的值为一个指向 charblocks 的索引值,与索引相关联的 offset = index * (4bpp|8bpp)。 每个 4bpp 的 tile 占用 32字节,8bpp 则为 64 字节。
但对于 Sprite 的 tile-index 值(位于OAM内),4bpp 和 8bpp 使用相同的索引值,即统一以 4bpp 为基准。
然后是 “有多少个 tile 你可以索引?” 对于 regular-BG 有 10bit 的宽度因此为 1024,但是通过计算你可以知道 单个 charblock 你最多只能放 512个 4bpp 的 tile,这是怎么回事了?总得来说只要你没有跨到 Sprite-charblock 就可以。
GBA Extended
Advanced Applications
text
使用 Bitunpack 可以解压单色字符或单色图形
Appendixes
Affine
GBA 在屏幕上获取 Sprites 和 BG 的方式非常类似于纹理映射。
对于定点数,应该使用有符号整数,临时变量使用 32位, 只有在最后一步上传到硬件时才转换成 16位(8.8)