百科问答小站 logo
百科问答小站 font logo



iOS/Android端GBA模拟器是如何开发出来的? 第1页

  

user avatar   crazyLucifer 网友的相关建议: 
      

对于实现一个GBA一个档次的以及以下的GBC,NES的模拟器,

对于一个自身素质过硬, 经验丰富,底力够强的程序员来说, 前置条件基本没有

模拟器的完善实质就是不断debug, 不断调试的过程

你要做的就是拿一份, 或者多份优秀,易读的模拟器源代码

对照着参考文档 (no$gba自带文档, 南美任天堂的官方编程手册, 以及tonc的GBA例程)

理解, 然后写demo测试理解

之后完善各个组件, 然后不断调试, 调优 (其实大部分时间都是花在调试上了,对于很多BUG, 自己心里也没底能不能解决, 考验各种体位的调试技巧)

不敢说所有人,但是对于大部分人来说确实都是起初啥都不懂,

先从复读机当起, 在长年累月的练习中慢慢参悟到其中的智慧然后在青出于蓝的

(先天具有智慧的生物毕竟是少数.不是那种依靠不断练习获得经验表现出来的熟练的那种假象...)


附我写的模拟器截图(尚在编写中, BUG很多, 但是多数游戏都能运行[主要是为了写GPU观察器观察GBA图形模式和实践调试器写着玩的]),

C写的核心, 用C++和WTL模板糊了一层GUI的皮,

说完泛泛之谈

接下来说说GBA元件具体的细节,

我不会讲所有, 只讲CPU和GPU这两个是最麻烦的核心元件, 会写这两个其他都不难

在此之前, 先来解决一个大多数人不太明白的问题

程序员怎么写代码控制硬件/ 访问不同区域(cpu内存/显存)的内存的?

在GBA上, 读写硬件主要靠硬件IO寄存器完成, 这些寄存器访问就跟访问内存一样简单,

只需要读写指定地址就能读出/控制相应的硬件IO信息/状态(具体原理可搜索内存IO和端口IO)

如下图 (这图顺带也把 GBA内存映射全讲了)

IO寄存器从0x4000000开始映射

比如 GPUIO分布(其他硬件也一样, 摘自no$gba文档)

控制/访问相应硬件数据全部都由读写指定地址完成

比如读取当前lcd扫描线行数, 直接 ldr r0, [r1] ;; r1:= #0x4000008

每个寄存器具体的参数文档都有, 这就解决了对硬件访问的问题

弄明白这个, 其他的大家应该都懂。


====================== CPU ======================

gba使用了一颗arm7tdmi核心, 2^24hz, 可切换成thumb模式

实质你要模拟两套指令集 (标准v4, 以及扩展thumb)

这个芯片的相关方面的信息你可以直接看no$gba的arm7文档, 这个是最方便的

如果光看no$gba的文档有点迷惑, 想全面的了解一下arm7,

直接上 ARM Architecture Reference Manual

和ARM7TDMI Revision: r4p1

前者提供了详细的arm指令的具体机器码, 以及所有的指令实现的大部分伪代码

后者则提供了关于arm7tdmi详细的每条指令的指令周期和基本编程要则

写cpu模拟程序, 效率最高也是最累的方法, 就是switch case (opcode)码

主流模拟器其实都是这么写的, cpu编码有一个套路, 执行一条指令, execCpuOp

抛出一个指令周期, 那这个周期再去其他硬件时序周期对照触发相应事件

cpu实现又这么几个坑-

实现两套指令集, 很累

对齐, 内存访问不对齐会有稀奇古怪的行为, 有些游戏可能会用到这些细节

指令辅助移位的5位移位(imm31), 寄存器移位(rs)在特定数值下也会有异常行为

标准v4t芯片不允许修改中断标志, 扩展的agb-cpu可以

(见ARM Architecture Reference Manual::A4.1.39 MSR )

cpsr和spsr的读写, 以及中断时的psr的行为

对于内存访问(str/ldr/stm/ldm) 指令rn写回寻址, rn写回时于rd寄存器冲突的行为


====================== GPU ======================

GPU非常麻烦,

GBA由两块 pal[256]的色板, 一块给卷轴(bg)用, 一块给精灵(sp)用

GBA有四个卷轴渲染器, 一个精灵输出层

还有两个裁剪窗口用来创建复合输出层, 或者裁剪图像

还有一个精灵窗口层用来进行图像的镂空操作

这些效果全开的话, 理论上最多要渲染 (4+1) * 4 (win0 + win1 + objwin + winout)

20层卷轴, (当然这有点危言耸听, 实际游戏用的少得多得多得多....)


有6种视频模式 (我称作 v0 ~ v5)

这六种视频模式中又有如下几种渲染模式

渲染模式-r0: NES, GBC强化Tile模式调色板 4位, 这个四位索引16倍数内的调色板像素

渲染模式-r1: NES, GBC强化Tile模式调色板 8位, 这个8位索引整块(bg/sp调色板)

渲染模式-r2: NES, GBC强化Tile模式调色板 8位 + 仿射变换 (这个功能用来实现简单的3d功能)

渲染模式-r3: framebuffer 15位颜色, 用的 vram + pitch 那一套显示光栅

渲染模式-r4: swapbuffer 8位颜色, 用的 vram + pitch 那一套显示光栅, 两帧可flip

渲染模式-r5: swapbuffer 15位颜色, 减少位图尺寸获得俩帧flip显存空间


对于 v0, 四个卷轴渲染器都能用, 渲染模式为r0, r1

对于 v1, 前三个卷轴能用 bg0, bg1 为r0, r1, bg2为 r2

对于 v2, bg2, bg3 这两个能用, 渲染模式只能为 r2

v3 ~ v5 其实就是对应了 r3 ~ r5, 这三种位图渲染模式只有 bg2一个卷轴渲染器能用


下面这段是我很久之前就写了放在某论坛上的, 直接拿过来了

---------------------------------

(* r0 从NES和GBC继承过来的强化Tile模式, *)

(* -------------------------------------------------------------------------------------------*)

此时GPU渲染为一块对齐16个调色板的调色板Bank+Tile本身自带的4位调色板索引显示颜色. (对于每一个Tile来说调色板Bank都是可切换的).

此时渲染一个Tile的每一个像素需要半个字节的信息

(* r1/r2 8位调色板模式 *)

(* -------------------------------------------------------------------------------------------*)

此时一个Tile的每一个像素需要8位字节来选择整块调色板

(* r3/r4/r5 直接的16位颜色渲染 *)

(* -------------------------------------------------------------------------------------------*)

这种渲染模式,不常用, GBA这个性能用起来很勉强, 一般用来显示GBA的游戏CG, 比如王国之心-记忆之链的开场CG mode 3


(* 何为Tile? ------------------------------ *)

对于非16位像素渲染, 程序员能控制的最小的发色单位不是像素, 而是一个8*8的像素块---


这个像素块, 有8位像素, 也有4位像素的,

对于8位像素Tile - 一个Tile块包含64个字节, Tile内的像素编码如下-


dn表示64个字节里第n个字节以此类推

x表示列

x0 x1 x2 x3 ... x7

第0行 d0 d1 d2 d3 .... d7

第1行 d8 d9 d10 d11 ... d15

..

..

第7行 d56 d57 d58 ... d63


对于4位像素Tile - 一个Tile包含32个字节,

Tile内的像素编码跟上面一样, 不过一个字节的低四位给了左边, 高位给右边

例如: 第0行 d0's 底4位是 pos 0, d0's 高4位是 pos 1, d1's 底4位是 pos 2, d2's 高4位是 pos 3 以此类推

(* 有了能显示像素的Tile块, 还有一个用来描述Tile块的命名表 *)

这个命名表里面装的是实际使用的Tile ID编码号码, 以及简单的渲染属性细节 (常规模式下描述一个8*8的Tile需要两个字节).

这两个字节解释如下, 以后这个基本属性我称作 tAttr


d9- d0 tile 号码,

d10- 水平反转致能

d11- 垂直反转致能

d15-d12- 4位调色板 bank, 适用于4位tile渲染模式


GPU显存里有好几个Tile像素池, (这个像素池, 有个专门的名词 chr table, 字符表)

这个像素池的基地址由GPU寄存器给出, (注意, 命名表也可以切换基地址!)

选中某一个然后配合 Tile ID可以寻址到实际的8*8 Tile像素的起始地址,

然后根据Tile内像素编码来写入实际的像素到显示屏


对于4位Tile渲染, 会选择4位调色板 bank *16的基地址的调色板范围内的16个调色板+4位Tile调色板索引来渲染像素


(* 有了命名表和字符表, 如何把这些零碎的信息拼凑成完整的LCD屏幕像素让我们看到实际的游戏画面呢? *)

LCD显示屏显示的实际的像素都是先从命名表取tile attr字节, 然后根据命名表里的tile id配合字符表基地址来渲染像素的

这个命名表,相当于GBA后备视频缓冲..

常规模式下一个单位的视频缓冲, 有256*256个像素, 256*256像素, 也就是32*32个tAttr描述, 一个tAttr两个字节

所以一个256*256个像素的命名表需要2K的内存来保存,


卷轴的渲染需要一个滚动的起点(水平,垂直两个方向).

和视频缓冲的起点来确定, 开始扫描的落点范围


假设此时屏幕不滚动, 从坐标0, 0开始渲染画面 (GBA渲染240*160个像素),

那么水平方向渲染的Tile就是命名表pos_x := 0开始渲染 30个Tile

那么垂直方向渲染的Tile就是命名表pos_y := 0开始渲染 20个Tile


假设此时屏幕滚动, 从坐标16, 32开始渲染画面,

那么水平方向渲染的Tile就是命名表pos_x := 16/8 开始渲染 30个Tile

那么垂直方向渲染的Tile就是命名表pos_y := 32/8 开始渲染 20个Tile


假设此时屏幕滚动, 从坐标56, 32开始渲染画面,

那么水平方向渲染的Tile就是命名表pos_x := 56/8 开始渲染 30个Tile (pos_x := 7开始渲染30个Tile, 超过边界, 会回滚到水平方向的起点..)

那么垂直方向渲染的Tile就是命名表pos_y := 32/8 开始渲染 20个Tile


假设此时屏幕不滚动, 从坐标1, 0开始渲染画面

那么水平方向渲染的Tile就是命名表pos_x := 0内部Tile的偏移pos 1 - 开始渲染 30个Tile (滚动偏移未对齐在一个完整Tile上, 可能会多渲染一些无用的像素用来模拟对齐)

那么垂直方向渲染的Tile就是命名表pos_y := 0开始渲染 20个Tile


这是卷轴滚动的基础概念-

常规模式下,

有四种视频缓冲模式


单屏幕缓冲 - 256*256像素, x, y方向 越过256, 会回滚,

他的屏幕只有一个Block块

_____

| |

| 256 | -> self

|_____|



水平双缓冲 - 512*256像素, x方向 越过512, 会回滚, y方向 越过256, 会回滚,


_____ _____

| | |

| 256 | 256 | -> left | right

|_____|_____|


垂直双缓冲 - 256*512像素, x方向 越过256, 会回滚, y方向 越过512, 会回滚

_____

| |

| 256 |

|_____| top

| | -> -------

| 256 | bottom

|_____|



四分屏缓冲 - 512*512像素, x, y方向 越过512, 会回滚,

_____ _____

| | |

| 256 | 256 | left-top | right-top

|_____|_____| -> -----|-------

| | | left-bottom | right-botoom

| 256 | 256 |

|_____|_____|


单个的256*256像素组成的32*32的Tile组成的命名表BLOCK编码如下图所示-


------------------------------------------------------------------------------> pos-x (0~31)

| tAttr0 | tAttr1 | tAttr2 | tAttr3 | tAttr4...... | tAttr31 -> pos-y := 0

| tAttr32 | tAttr33 | tAttr34 | tAttr35 | tAttr36...... | tAttr63 -> pos-y := 1

| .....

| .....

| .....

|/

pos-y (0~31)

每跨越一个pos_y需要通过32个 tAttr的步长, 也就是64个字节.

水平方向的寻址就是每增加一单元+两个字节单元, 很简单吧.

nt_base:=命名表基地址



单屏幕缓冲命名表寻址 self := nt_base


水平双缓冲命名表寻址 left := nt_base

right := nt_base + 2K byte

垂直双缓冲命名表寻址 top := nt_base

bottom := nt_base + 2K byte


四分屏缓冲命名表寻址 left-top := nt_base

right-top := nt_base + 2K byte

left-bottom := nt_base + 4K byte

right-bottom := nt_base + 6K byte


先举个简单的例子----------------

启用256*256 单屏幕缓冲

如果我滚动设置为 (7.8)

那么 GPU开始渲染的起点0.0就是

先算水平方向, 7/8 := 0.... 7

再算垂直方向, 8/8 := 1.... 0

这个除法获得的整数就是完整的Tile偏移,

余数则是一个8*8 Tile内的偏移, (对于8位像素, Tile内寻址 := 水平余数位置(0..7) + 垂直余数位置(0..7) * 8

所以渲染起点为- NT_BASE + TILE_pos (0, 1)'s TILE_ATTR (7.0)处的像素

跨页的例子----------------

启用256*256 单屏幕缓冲

如果我滚动设置为 (265.169)

那么 GPU开始渲染的起点0.0就是

先算水平方向, 265/8 := 33.... 1

再算垂直方向, 175/8 := 21.... 7

33, 超过256边界了, 需要切换水平页, 对于256*256, 水平切换页面相当于直接回滚到33-32的地方

所以渲染起点为- NT_BASE + TILE_pos (1, 21)'s TILE_ATTR (1.7)处的像素

跨页的例子2----------------

启用 四分屏缓冲模式-

如果我滚动设置为 (257.169)

那么 GPU开始渲染的起点0.0就是

先算水平方向, 257/8 := 32.... 1 (32, 水平方向需要跨页)

再算垂直方向, 175/8 := 21.... 7 (垂直方向不需要跨页)

水平方向需要跨页, 垂直方向不需要跨页 就是 right-top (right-top地址为 nt_base + 2K byte)

所以渲染起点为- NT_BASE + 2K + TILE_pos (0, 21)'s TILE_ATTR (1.7)处的像素

其余的以此类推

每一个像素都需要按照具体的命名表视频缓冲规则进行测试

当你确定了起点, 然后扫描的时候再次跨页计算规则跟上面一样...

比如对于四分屏, 扫描起点是 right-top, 如果水平方向扫描超过512, 他会从right-top切换到 right-left, 并且当前完整的Tile-Block % 32


(* 仿射变换卷轴 *)

GBA的GPU提供简单的仿射变换功能


主要由以下10个参数设置 (这八个参数全是有符号的并且低8位都不是整数(用来模拟小数))

dx (PA), int16_t

dmx (PB), int16_t

dy (PC), int16_t

dmy (PC), int16_t

ref_x, int28_t (d27位为符号位, 下面以此类推)

ref_y, int28_t

dmx_total, int28_t

dmy_total, int28_t

dx_temp, int28_t

dy_temp, int28_t

在 VBLANK开始的时候

dmx_total := ref_x

dmy_total := ref_y


之后从扫描线 0~159间的每次扫描线切换到下一根扫描线的时候

dx_temp = dmx_total

dy_temp = dmx_total

dmx_total += dmx

dmy_total += dmy


然后每次水平扫描的时候每渐进一个像素点,

dx_temp += dx

dy_temp += dy

这个dx_temp, dy_temp把低8位非整数右移掉就是当前渲染的卷轴落点范围

就这么简单,

注意, 非VBLANK期间写入 ref_x, ref_y也会使相应的单个 dmx_total/dmy_total更新 (dmx_total := ref_x/ dmy_total := ref_y)

(* 非常规-仿射变换卷轴-8位标准调色板模式 *)

此时命名表尺寸始终为正矩形, 始终为8位模式, 尺寸有 128/256/512/1024四种.

并且tAttr只有一个字节, 并且只有一个命名表, 尺寸为 128/256/512/1024, 也即命名表的步长Pitch不是固定的32..

pos_limit := [128/256/512/1024]/8 - 1

pos_limit := 15/31/63/127

-----------------------------------------------------------------> pos-x (0~ pos_limit )

| tAttr0 | tAttr1 | tAttr2 | tAttr3 | tAttr4...... | tAttr~pos_limit -> pos-y := 0

| .....

| .....

| .....

|/

pos-y (0~ pos_limit )

(* GBA的调色板 *)

GBA的调色板分为BG调色板和SP调色板, 每一个色板有256子项

对于SP_pal[0]和BG_pal[0], 总是透明颜色

对于4位调色板Bank的像素Tile, SP_pal_bank[0] 和BG_pal_bank[0]也都是透明的颜色


颜色格式 : d15 d14 .... d0

unused bbbbb ggggg rrrrr


GBA的精灵也是十分复杂, 并且也可以切换渲染模式-

精灵的最小像素单元也是一个8*8的Tile,

但是精灵有能像"胶水"一样把多个Tile块拼合在一起显示的能力.

一个精灵可以显示多种尺寸的Tile合集,

(* 精灵的TILE-ID寻址*)

精灵只有4位精灵和8位精灵两种

精灵的属性内存只提供一个Tile块合集的最上角pos(0, 0)的精灵 TileID.

对于精灵的水平方向寻址:4位精灵每次加上此行TileID基地址+1

8位精灵每次加上此行TileID基地址+2

行TileID基地址寻址有两种模式可被GPU随意切换-

固定步长32寻址 ->无论8位/4位, 每次切换到下一个完整的Block8 Tile渲染时, 都随当前行基地址+= 32

如图:

8位模式固定位长寻址: TILE-ID := 16, 16*16

16 | 18

48 | 50

4位模式固定位长寻址: TILE-ID := 16, 16*16

16 | 17

48 | 49

自身连续寻址: 举个例子:

一个32*16的4位精灵, TildID := 55

55 | 56 | 57 | 58

59 | 60 | 61 | 62

(* 精灵的CHR寻址*)

有了Tile ID, 便可寻址到具体的像素字符表

无论像素位数是4还是8位, 都是一个4位Tile的完整Block增量, 这个跟BG Tile寻址不一样(+32)

比如对于寻址 TildID:= 277的Tile, 4位只要写 277, 但是8位就要变成277*2

对于8位Tile寻址, 最后一位会被忽略

(* 精灵的完整寻址例子*)

举个例子:=

8位模式固定位长寻址: TILE-ID := 99, 32*16

| 99 | 101 |

| 99+32 | 101 +32| ---> | A | B |

| C | D |

对于A处的寻址, TildID := 99, 因为是8位忽略最低位, := 98

TilePixel_8_8_Start := 0x6010000 + 98*32

对于B处的寻址, TildID := 101, 因为是8位忽略最低位, := 100

TilePixel_8_8_Start := 0x6010000 + 100*32

...

...

以此类推

(* 精灵的仿射*)

精灵也有仿射功能,

精灵仿射的坐标系如下:

y Negtive

/|

|

|

x Negtive <- -----|----- -> x Postive

|

| _____________ Origin (0, 0) 精灵的中心点

|/

y Postive

对于精灵仿射只有 dx, dmx, dy, dmy 这四个参数使用.

这四个参数跟BG用的那四个参数是一样的...

BG实际是用了这个坐标系右下角的象限进行仿射变换的.

对于GBA游戏机来说, 精灵的渲染坐标确定遵循以下公式

x0, y0 旋转中心点, 也即是精灵的中心点坐标

x1, y1 当前需要扫描线渲染的坐标

x2, y2 实际经过仿射变换获得的坐标

A, B, C, D dx, dmx, dy, dmy

x2 = A(x1-x0) + B(y1-y0) + x0

y2 = C(x1-x0) + D(y1-y0) + y0

这个公式其实很简单, 其实寻址都是相对于中心点向量带符号偏移(水平, 垂直方向) 的比例系数

游戏机就会根据这个公式计算当前坐标仿射过后的实际的像素落点,

超过范围不会被处理, 比如在 pos (8, 8)处的一个32*32的精灵

经过计算落点在 (166, 166)处, 超过本身的(8~39, 8~39)范围, 忽略处理这个像素

如果落点在这个范围, 怎么计算这个像素呢,

减去初始偏移即可, 比如落点27, 16

x偏移:= 27-8 := 19/8 := 2..3

y偏移:= 16-8 := 8/8 := 1...0

那么就会寻址 pos (2, 1)内8*8Tile pos(3.0)的像素....

以此类推

(* 双倍尺寸精灵*)

这个所谓的双倍是对于仿射精灵来说的, 是双倍的画布模式, 不是原始像素放大两倍.

举个例子, 比如一个正矩形, 可被渲染区域就是整个矩形的区域,

你把它旋转45度, 就会有部分像素跑到区域外不被显示了, 怎么办?

我想显示这部分像素怎么办呢? 把显示画布区域调大点不就行了?

双倍精灵就是这个意思.

对于双倍精灵来说, 中线点x, y坐标会被增加一个水平, 垂直尺寸一半的尺寸.

然后两个方向各扩展一半画布尺寸,然后按照正常的仿射套路渲染

(* 双倍尺寸精灵BUG *)

当启用双倍精灵时, 对于此精灵, 如果有像素处于128~160这个范围, 这个范围的像素不会被显示.

(* GBA的窗口功能 ------------------------------ *)

GBA的窗口很麻烦, 有三种窗口, 精灵窗口, "镂空"窗口, 标准窗口0, 1 (其中后两种实际是一种)...

三种可以一起开, 优先级为先渲染 精灵窗口, 然后是标准窗口1, 最后标准窗口0

只要开了窗口功能, "镂空"窗口一定会存在, 并且通常的渲染都不可用, 请注意

"镂空"窗口, 这个窗口会渲染在精灵窗口, 精灵窗口外区域的像素 ...

窗口有这么几个功能 1.裁剪, 标准窗口可以设置裁剪范围区域显示像素.

2.复合使用BG, 每个窗口都能使用整个渲染的BG0~3和精灵输出层...

3.遮罩镂空效果

标准窗口就是通常意义上的裁剪窗口, 没啥好说的,

精灵窗口有点意思, 会在当前窗口整个精灵输出层选择几个精灵来作为影子标记(编程人员可控的),本身不显示像素

这部分精灵的非透明像素会设置影子标记,

然后当前精灵窗口的BG, 精灵输出层的渲染, 只会在当前设置了影子标记的坐标画,渲染像素.

(* GBA的BG优先级 ------------------------------ *)

BG0~BG3 都可设置优先级

优先级高的总是最后被画

0优先级最高, 3最低.

如果优先相同, 那么BG0优先级最大, BG1...BG2... BG3优先级最小...

精灵也有优先级,

比如精灵优先级为3, 那么他会画在也只能画在优先级为3的BG画布上, 画不到优先级为2的BG上.

比如精灵优先级为1, 那么他能画在优先级3~1的画布上, 画不到优先级为0的画布上, 以此类推

对于精灵优先级相同, 第一个精灵精灵0优先级最大, ....越往后优先级越小.

(* GBA的简单像素滤波 ------------------------------ *)

(像素滤波就三种种 -- Alpha混合和亮度调整,以及马赛克效果)

Alpha混合 := 饱和相加 ( 每5位颜色分量*可编程分量系数A/16, 每5位颜色分量*可编程分量系数B/16)

对于像素Alpha, 如果底下的像素是透明的,

那么这个像素会以原始形态写入, 不进行Alpha处理 (这点很重要, 有的游戏不模拟此feature会明显画面异常)

可编程分量系数 := 0~16

亮度增加 := 饱和相加 ( (31-每5位颜色分量)*可编程分量系数/16, 每5位颜色分量)

亮度减少 := 饱和 ( 每5位颜色分量*(16-可编程分量系数/16) )

(* ALPHA的具体细节 ------------------------------ *)

Alpha混合需要两个BG层, 1st作为上层混合

2nd作为下层混合

优先级也必须被适配.

这个应该能懂吧? GPU内部有个寄存器可以被设置选择哪些BG输出层作为 1st

哪些BG输出层作为 1nd

当然精灵输出层也可以被选择...

每一个 1st都能对应多个2nd BG卷轴Alpha混合, 反过来也是

举个例子: 1st : bg0, bg2, bg3

2nd : bg1

bg 渲染优先级:= bg1, bg0, bg2, bg3

先渲染bg1, 这个时候bg1被渲染完成, 底部是bg1的像素.

然后渲染bg0, 刚好这个时候bg0被选中为1st 上层混合.底层是bg1,刚好2nd掩码中bg1也被选中了

进行混合, 此时bg0渲染完毕, 此时底部是bg0的像素,

开始渲染下一个BG, bg2, 再次进行Alpha测试, bg2被1st选中, 可惜底部的bg0没有被2nd选中, Alpha测试失败.

此时底部是bg2的像素, 开始渲染bg3, 很失望, bg3也是测试失败, 不会进行Alpha混合.

Alpha多择 1st : bg0, bg2, bg3

2nd : bg1, bg2, bg3

bg 渲染优先级:= bg1, bg0, bg2, bg3

先渲染bg1, 这个时候bg1被渲染完成, 底部是bg1的像素.

然后渲染bg0, 刚好这个时候bg0被选中为1st 上层混合.底层是bg1,刚好2nd掩码中bg1也被选中了

进行混合, 此时bg0渲染完毕, 此时底部是bg0的像素,

开始渲染下一个BG, bg2, 再次进行Alpha测试, bg2被1st选中, 底部的bg0也被2nd选中, Alpha测试成功,进行Alpha混合.

此时底部是bg2的像素, 开始渲染bg3, 2nd:bg2, 1st:bg3也都被选中继续进行Alpha混合

(* GBA的马赛克 ------------------------------ *)

马赛克很简单, 应该都会吧, 就是每隔几个像素插值一次.. 不多说了.

(* GBA的屏幕渲染流程 ------------------------------ *)

GBA在渲染开始的时候会先填充颜色, (backdrop)

这个颜色就是BG_pal[0]的颜色,注意这部分颜色全是透明的

然后根据是否开启窗口, 开了就进行窗口渲染那一套逻辑渲染,

否则就是常规渲染


(* GBA的GPU IO端口映射--此部分信息大多摘自GBATEK文档---------------------------- *)

4000000h 2 R/W DISPCNT GPU主控

4000002h 2 R/W - 颜色分量交换

4000004h 2 R/W DISPSTAT GPU状态

4000006h 2 R VCOUNT GPU线帧

4000008h 2 R/W BG0CNT BG0 分控

400000Ah 2 R/W BG1CNT BG1 分控

400000Ch 2 R/W BG2CNT BG2 分控

400000Eh 2 R/W BG3CNT BG3 分控

4000010h 2 W BG0HOFS BG0 x-滚动起点

4000012h 2 W BG0VOFS BG0 y-滚动起点

4000014h 2 W BG1HOFS BG1 x-滚动起点

4000016h 2 W BG1VOFS BG1 y-滚动起点

4000018h 2 W BG2HOFS BG2 x-滚动起点

400001Ah 2 W BG2VOFS BG2 y-滚动起点

400001Ch 2 W BG3HOFS BG3 x-滚动起点

400001Eh 2 W BG3VOFS BG3 y-滚动起点

4000020h 2 W BG2PA BG2 仿射参数PA (dx)

4000022h 2 W BG2PB BG2 仿射参数PB (dmx)

4000024h 2 W BG2PC BG2 仿射参数PC (dy)

4000026h 2 W BG2PD BG2 仿射参数PD (dmy)

4000028h 4 W BG2x BG2 仿射引用点x (ref_x)

400002Ch 4 W BG2y BG2 仿射引用点y (ref_y)

4000030h 2 W BG3PA BG3 仿射参数PA (dx)

4000032h 2 W BG3PB BG3 仿射参数PB (dmx)

4000034h 2 W BG3PC BG3 仿射参数PC (dy)

4000036h 2 W BG3PD BG3 仿射参数PD (dmy)

4000038h 4 W BG3x BG3 仿射引用点x (ref_x)

400003Ch 4 W BG3y BG3 仿射引用点y (ref_y)

4000040h 2 W WIN0H 标准窗口 0 水平裁剪设置

4000042h 2 W WIN1H 标准窗口 1 水平裁剪设置

4000044h 2 W WIN0V 标准窗口 0 垂直裁剪设置

4000046h 2 W WIN1V 标准窗口 1 垂直裁剪设置

4000048h 2 R/W WININ 窗口win0, win1 BG选择设置

400004Ah 2 R/W WINOUT 精灵, 镂空窗口 BG选择设置

400004Ch 2 W MOSAIC 马萨克尺寸设置

4000050h 2 R/W BLDCNT 像素滤波设置

4000052h 2 R/W BLDALPHA Alpha混合使用的分量

4000054h 2 W BLDy 亮度调整分量

(* 4000000h 2 R/W DISPCNT GPU主控----------------------------- *)

0-2 图形模式 (6, 7是UB行为)

3 机器类型指示 (用来只是当前处于何种模式 0:= GBA, 1:GBC)

4 帧缓冲选择 (帧缓冲选择, 适用于模式4, 模式5)

5 HBlank时序自由访问显存致能 (1=可以)

6 OAM-TILE寻址方式 (0=精灵OAM-Tile寻址使用固定步长32, 1=精灵OAM-Tile寻址使用自身连续步长)

7 强制变白 (无论何时都能访问显存, 当前渲染线直接变白)

8 BG0显示开关 (0=关, 1=开)

9 BG1显示开关 (0=关, 1=开)

10 BG2显示开关 (0=关, 1=开)

11 BG3显示开关 (0=关, 1=开)

12 精灵输出层显示开关 (0=关, 1=开)

13 标准窗口0 启用开关 (0=关, 1=开)

14 标准窗口1 启用开关 (0=关, 1=开)

15 精灵窗口 启用开关 (0=关, 1=开)

(* 4000002h 2 R/W - 颜色分量交换----------------------------- *)

输出交换 rgb分量的b位和r位, 通常情况不使用, 好像适用于外部设备?不太清楚.....

基本没几个模拟器会管这个东西,.....

(* 4000004h 2 R/W DISPSTAT GPU状态 ----------------------------- *)

0 V-BLANK标志 (只读) (1=VBlank) (此标志会在渲染线160...226的时候被设置)

1 H-Blank标志 (只读) (1=HBlank) (每条扫描线的HBlank阶段都会被设置)

2 线帧扫描匹配测试标志 (只读) (1=Match) (当前扫描线被匹配会设置, 否则清除)

3 V-BLANK 发生IRQ 中断致能 (可读写)

4 H-Blank 发生IRQ 中断致能 (可读写)

5 线帧扫描匹配测试成功 中断致能 (可读写)

6 不使用

7 不使用

8-15 线帧扫描匹配设置行 (可设置0.227) (可读写)

GPU一帧要扫描228根扫描线,

扫完扫描线会回滚到0的地方继续回扫,继续新的一帧

CPU的频率是, 16.78MHz

LCD一秒刷新 59.727次,

很容易得出刷新一帧数需要的CPU 时钟周期:= 16780000/59.727 := 280944.96626316

分给228条扫描线:= 280944.96626316/228 := 1232.2147643121,

也就是每条扫描线可跑这么时钟周期的CPU指令数,

其中前960个Ticks是正常扫描线渲染周期, 剩下的就是HBlank渲染周期.

HBlank实质是电子枪回扫至下一行起点的过程,

VBlank是电子枪回扫至光栅器原点的过程.

前160扫描线全是可视扫描线, 后面的几十条, 实际上是不会进行实际操作的,

就是电子枪回扫至原点的VBlank过程, 这个时候访问显存资源是最安全的.

GPU内部有一个线帧扫描计数器, 会记录当前扫描线的行数.

这个线帧扫描计数器在每次HBlank周期都会更新,

然后匹配测试当前设置的线帧扫描匹配设置行, 如果匹配也会设置 线帧扫描匹配测试标志

HBlank-VBlank阶段, 线帧扫描测试成功都可设置是否触发IRQ中断,

注意啊, 这个触发IRQ中断, 只是设置GBA设备的IF位....仅此而已

(* 4000006h 2 R VCOUNT GPU线帧 ----------------------------- *)

0-7 指示当前扫描线, 其余位不用, 这个扫描线计数器在每条扫描线的H-Blank阶段会更新.

(* 4000008h 2 R/W BG0CNT BG0 分控 ----------------------------- *)

(* 400000Ah 2 R/W BG1CNT BG1 分控 ----------------------------- *)

(* 400000Ch 2 R/W BG2CNT BG2 分控 ----------------------------- *)

(* 400000Eh 2 R/W BG3CNT BG3 分控 ----------------------------- *)

0-1 BG优先级设置, 2位范围0~3

2-3 字符表基地址, 2位范围0~3, 每个单元16K, 从显存0x6000000开始的

4-5 不使用...........................................................

6 当前马赛克致能 1?允许:不允许

7 颜色模式 (0=4位调色板Bank+Tile4位, 1=256位调色板)

8-12 命名表基地址 (0-31, 每个单元2KB, 从显存0x6000000开始的)

13 BG2/BG3: 仿射坐标溢出屏幕处理 (0=透明,不显示, 1=回滚)

14-15 命名表视频缓冲尺寸 (0-3)

Value 标准模式 旋转模式

0 256x256 (2K) 128x128 (256 bytes)

1 512x256 (4K) 256x256 (1K)

2 256x512 (4K) 512x512 (4K)

3 512x512 (8K) 1024x1024 (16K)

(* 4000010h 2 W BG0HOFS BG0 x-滚动起点 ----------------------------- *)

(* 4000012h 2 W BG0VOFS BG0 y-滚动起点 ----------------------------- *)

(* 4000014h 2 W BG1HOFS BG1 x-滚动起点 ----------------------------- *)

(* 4000016h 2 W BG1VOFS BG1 y-滚动起点 ----------------------------- *)

(* 4000018h 2 W BG2HOFS BG2 x-滚动起点 ----------------------------- *)

(* 400001Ah 2 W BG2VOFS BG2 y-滚动起点 ----------------------------- *)

(* 400001Ch 2 W BG3HOFS BG3 x-滚动起点 ----------------------------- *)

(* 400001Eh 2 W BG3VOFS BG3 y-滚动起点 ----------------------------- *)

前9位可用, 范围0~511

每个BG渲染器各自的标准滚动设置, 注意如果是仿射旋转BG, 会忽略此参数使用仿射参数.

(* 4000020h 2 W BG2PA BG2 仿射参数PA (dx) ----------------------------- *)

(* 4000022h 2 W BG2PB BG2 仿射参数PB (dmx) ----------------------------- *)

(* 4000024h 2 W BG2PC BG2 仿射参数PC (dy) ----------------------------- *)

(* 4000026h 2 W BG2PD BG2 仿射参数PD (dmy) ----------------------------- *)

(* 4000028h 4 W BG2x BG2 仿射引用点x (ref_x) ----------------------------- *)

(* 400002Ch 4 W BG2y BG2 仿射引用点y (ref_y) ----------------------------- *)

(* 4000030h 2 W BG3PA BG3 仿射参数PA (dx) ----------------------------- *)

(* 4000032h 2 W BG3PB BG3 仿射参数PB (dmx) ----------------------------- *)

(* 4000034h 2 W BG3PC BG3 仿射参数PC (dy) ----------------------------- *)

(* 4000036h 2 W BG3PD BG3 仿射参数PD (dmy) ----------------------------- *)

(* 4000038h 4 W BG3x BG3 仿射引用点x (ref_x) ----------------------------- *)

(* 400003Ch 4 W BG3y BG3 仿射引用点y (ref_y) ----------------------------- *)

仿射BG那段讲了, 这里不再赘述.多余的位也不会使用啦,

(* 4000040h 2 W WIN0H 标准窗口 0 水平裁剪设置 ----------------------------- *)

(* 4000042h 2 W WIN1H 标准窗口 1 水平裁剪设置 ----------------------------- *)

(* 4000044h 2 W WIN0V 标准窗口 0 垂直裁剪设置 ----------------------------- *)

(* 4000046h 2 W WIN1V 标准窗口 1 垂直裁剪设置 ----------------------------- *)

每个Word的低位都是 Right/bottom,高位则是Left/top

水平方向如果left>right或者right >240都会被解释成right:= 240

垂直方向如果top>bottom或者bottom >160都会被解释成bottom:= 160

注意设置的right/bottom的值, 实际上只会渲染到 pos-1的地方

比如left := 0 right := 240, 那么只会渲染pos 0 ~239

(* 4000048h 2 R/W WININ 窗口win0, win1 BG选择设置 ----------------------------- *)

0-3 标准窗口 0 BG0-BG3 选择显示位域 1:显示

4 精灵输出层 1:显示

5 标准窗口0 像素滤波启用 0:不启用 比如Alpha混合/亮度调整不会发生, 不影响马赛克

8-11 标准窗口 1 BG0-BG3 选择显示位域 1:显示

12 精灵输出层 1:显示

13 标准窗口1 像素滤波启用 0:不启用

其余位域不使用

(* 400004Ah 2 R/W WINOUT 精灵, 镂空窗口 BG选择设置 ----------------------------- *)

0-3 镂空窗口 BG0-BG3 选择显示位域 1:显示

4 镂空窗口精灵输出层 1:显示

5 镂空窗口 像素滤波启用 0:不启用

8-11 精灵窗口 BG0-BG3 选择显示位域 1:显示

12 精灵窗口精灵输出层 1:显示

13 精灵窗口 像素滤波启用 0:不启用

其余位域不使用

(* 400004Ch 2 W MOSAIC 马萨克尺寸设置 ----------------------------- *)

0-3 BG 马赛克水平映射

4-7 BG 马赛克垂直映射

8-11 OBJ 马赛克水平映射

12-15 OBJ 马赛克垂直映射

当参数被设置为0的时候当前的类型,向量的马赛克滤波将不会发生.

举个例子:当马赛克水平映射设置成7

那么每隔7+1个像素就会插值取得一个当前block整数处的像素来填充block余数处的像素, 每次到达block整数处更新一次这个值

比如 pos 0..(7+1) .. 无余数, block整数处, 取 pos (0)的像素..

pos 1..(7+1) .. 1 余数, 填充 pos (0)的像素

...

pos 8..(7+1) ... 0无余数, block整数处, 取 pos (8)的像素..

...

以此类推

(* 4000050h 2 R/W BLDCNT 像素滤波设置 ----------------------------- *)

(* 1st选择, 这个对于非Alpha, 也表示选择该层--------------------*)

0 BG0 1st

1 BG1 1st

2 BG2 1st

3 BG3 1st

4 OBJ 1st

5 BD 1st

6-7 像素滤波选择 (0-3, see below)

0 = 无像素滤波

1 = Alphe混合

2 = 亮度调整-变白

3 = 亮度调整-变暗

(* 2nd选择, 这个对于非Alpha混合是不使用的--------------------*)

8 BG0 2nd

9 BG1 2nd

10 BG2 2nd

11 BG3 2nd

12 OBJ 2nd

13 BD 2nd

其余位不使用

(* 4000052h 2 R/W BLDALPHA Alpha混合使用的分量 ----------------------------- *)

0-4 1st Alpha分量 (1st Target) (0..16 = 0/16..16/16, 17..31=16/16)

8-12 2nd Alpha分量 (2nd Target) (0..16 = 0/16..16/16, 17..31=16/16)

其余位不使用

(* 4000054h 2 W BLDy 亮度调整分量 ----------------------------- *)

前四位有用, 跟Alpha一样的设置, 0~31, 大于16则是16

(* 精灵OAM所使用的结构 ---------------------------- *)

OAM只有1K字节, 有128只精灵

所以这么一算, 1024/128 := 8个字节

每个精灵的属性是按照uint16_t 的尺寸安排的,

也就是每个精灵有四个单元的 attr字,

其中最后一个字是用来凑成仿射参数, 后面在讲, 先解释前三个属性字.

(* attr0 ----*)

0-7 精灵的y坐标 (0-255, 注意超过255会回滚到0显示)

8 仿射精灵致能 (1:启用 0:标准精灵渲染)

启用仿射精灵那么d9就是双倍精灵致能 (1:启用)

标准渲染d9则是渲染致能 (1:不渲染此精灵 0:渲染)

10-11 OBJ Mode (0=标准渲染, 1=Alpha混合, 2=精灵窗口阴影标志精灵, 3=UB值)

如果此处设置Alpha混合, 将会忽略BLDCNT的像素滤波类型参数, 强制启用Alpha混合

12 OBJ 马赛克 1:启用

13 8位/4位指示 (0=4位+调色板Bank 1=256位调色板)

14-15 OBJ Shape (0=Square,1=Horizontal,2=Vertical,3=Prohibited)

(* attr1 ----*)

0-8 x-Coordinate (0-511, 注意超过511会回滚到0显示)

启用仿射精灵:

9-13 仿射参数选择 (0-31)

标准渲染:

9-11 不使用

12 水平翻转 (0=Normal, 1=翻转)

13 垂直反转 (0=Normal, 1=翻转)

14-15 精灵尺寸 (配合attr0的d14-d15可确认精灵的尺寸)

尺寸 Square Horizontal Vertical

0 8x8 16x8 8x16

1 16x16 32x8 8x32

2 32x32 32x16 16x32

3 64x64 64x32 32x64


(* attr2 ----*)


0-9 Tile ID

10-11 精灵优先级

12-15 调色板Bank, 适用于4位Tile渲染.

(* 精灵的仿射参数dx, dmx, dy, dmy 选择 ------------------*)

attr1的d9-d13 可选择0~31的仿射bank

每一个bank开始的第1个OAM的attr3就是dx

第2个OAM的attr3就是dmx

第3个OAM的attr3就是dy

第4个OAM的attr3就是dmy

每四个完整的OAM属性字节结构块(也就是32个字节一个Bank) 是一个完整的Bank


APU的实现其实很简单,

就是GBC那一套加上两个个 DMA 8位PCMFIFO队列(要搭配定时器一起用),


除此之外还要完善卡带数据库, 每个卡带可能也会扩展各种设备硬件, 比如 eeprom和flash芯片

以及rtc时钟, 这些也不是重头戏, 也没多大难度, 主要是GPU和CPU, 以及调试能力


说的很散, 随意发挥, 技术讨论可留言 :)




  

相关话题

  都说swift取代oc,为何五年了依然是oc为主? 
  安卓逐渐走向闭源,为什么我国手机公司却仍不开发自己的系统呢? 
  诺基亚当年为什么不试着做一款 Android 手机? 
  一个 Android 的应用程序可以实现控制操作蓝牙耳机的功能吗? 
  iOS 这么多年已经到了 10.X,可为什么还是不能在手机端自定义铃声? 
  华为的胜算到底有多大? 
  求好评弹框在什么时机弹出最合适? 
  为什么国产手机厂商往往不配置 3D touch 和线性马达? 
  为什么 Android 不能像 iOS 一样在系统级别支持右划返回,非得有个返回键? 
  GBA上你认为最经典的游戏是哪一款或者哪一个系列? 

前一个讨论
如何看待近期上海安路科技发布的ELF2型FPGA新品?
下一个讨论
为什么 GB、GBC、GBA 掌机没有使用带有背光的液晶屏?





© 2024-11-24 - tinynew.org. All Rights Reserved.
© 2024-11-24 - tinynew.org. 保留所有权利