dustpg / BlogFM

Blog for Me
MIT License
155 stars 23 forks source link

Re: 从零开始的红白机模拟 - [30] VRC6 咆哮 #43

Open dustpg opened 5 years ago

dustpg commented 5 years ago

可乐妹的VRC6

那就从NSF的扩展音源最低位开始吧, VRC6有两个变种:

当然所述为日版(FC), NES版由于老任的策略不能使用自己搭载扩展音源的卡带.

BANK

CPU $8000-$BFFF: 16 KB switchable PRG ROM bank
CPU $C000-$DFFF: 8 KB switchable PRG ROM bank
CPU $E000-$FFFF: 8 KB PRG ROM bank, fixed to the last bank
CHR $0000-$03FF: 1 KB switchable CHR ROM bank
CHR $0400-$07FF: 1 KB switchable CHR ROM bank
CHR $0800-$0BFF: 1 KB switchable CHR ROM bank
CHR $0C00-$0FFF: 1 KB switchable CHR ROM bank
CHR $1000-$13FF: 1 KB switchable CHR ROM bank
CHR $1400-$17FF: 1 KB switchable CHR ROM bank
CHR $1800-$1BFF: 1 KB switchable CHR ROM bank
CHR $1C00-$1FFF: 1 KB switchable CHR ROM bank

感觉设计很科学, PRG-ROM方面16kb切换, 8kb切换, 8kb固定. CHR-ROM全是1kb可切换

PRG-ROM支持到256kb, CHR-ROM也是.

寄存器

由于地址线只有0,1,12-15可用, 镜像地址可以用: 与$F003做与运算获得($DE6A -> $D002).

同时两个变种的区别就是: 地址线A0,A1是相反的.

    variant   lines     registers                       Mapper Number
    =================================================================
    VRC6a:    A0, A1    $x000, $x001, $x002, $x003      024
    VRC6b:    A1, A0    $x000, $x002, $x001, $x003      026

换句话说, MAPPER-026实现可以为:

void sfc_mapper_vrc6a_write_high(uint16_t addr, uint8_t data) {
    // xxxxx
}

void sfc_mapper_vrc6b_write_high(uint16_t addr, uint8_t data) {
    uint16_t new_addr = addr & 0xfffc;
    new_addr |= (addr & 1) << 1;
    new_addr |= (addr & 2) >> 1;
    sfc_mapper_vrc6a_write_high(new_addr, data);
}

16k PRG Select ($8000-$8003)

7  bit  0
---------
.... PPPP
     ||||
     ++++- Select 16 KB PRG ROM at $8000

16*16=256

8k PRG Select ($C000-$C003)

7  bit  0
---------
...P PPPP
   | ||||
   +-++++- Select 8 KB PRG ROM at $C000

8*32=256

PPU Banking Style ($B003)

7  bit  0
---------
W.PN MMDD
| || ||||
| || ||++- PPU banking mode; see below
| || ++--- Mirroring varies by banking mode, see below
| |+------ 1: Nametables come from CHRROM, 0: Nametables come from CIRAM
| +------- CHR A10 is 1: subject to further rules 0: according to the latched value
+--------- PRG RAM enable

CIRAM (Console-Internal RAM, FC内部自带的RAM). 是的! VRC6支持用ROM代替RAM.

现有的游戏只有这几种情况: $20, $24, $28, $2C, $A0, $A4, $A8, $AC.

CHR Select 0…7 ($Dxxx, $Exxx)

为了方便描述, 将$D000-$D003, $E000-$E003依次描述为R0-R7. $B003的第三位影响CHR-ROM的切换:

图样表:

[$B003] & $03 0 1 2 / 3
$0000-$03FF R0 R0 R0
$0400-$07FF R1 R0 R1
$0800-$0BFF R2 R1 R2
$0C00-$0FFF R3 R1 R3
$1000-$13FF R4 R2 R4
$1400-$17FF R5 R2 R4
$1800-$1BFF R6 R3 R5
$1C00-$1FFF R7 R3 R5

名称表:

[$B003] & $07 0/6/7 1/5 2/3/4
$2000-$23FF R6 R4 R6
$2400-$27FF R6 R5 R7
$2800-$2BFF R7 R6 R6
$2C00-$2FFF R7 R7 R7

之前提到名称表拥有大致三种模式: 水平镜像、垂直镜像以及四屏模式. 但是实际上还有更多的模式, 比如三屏、一屏. 详细的还是查看引用链接.

图样表还行, 名称表解释起来太麻烦了, 直接上代码(没有考虑切换名称表切换CHR-ROM):

    // 使用CIRAM 镜像模式
    if (!(ppu_style & SFC_18_B3_FROM_CHRROM)) {
        switch (ppu_style & 0x2F) {
        case 0x20:
        case 0x27: // (这个情况现有的游戏不存在)
            sfc_switch_nametable_mirroring(famicom, SFC_NT_MIR_Vertical);
            break;
        case 0x23: // (这个情况现有的游戏不存在)
        case 0x24: 
            sfc_switch_nametable_mirroring(famicom, SFC_NT_MIR_Horizontal);
            break;
        case 0x28:
        case 0x2F: // (这个情况现有的游戏不存在)
            sfc_switch_nametable_mirroring(famicom, SFC_NT_MIR_SingleLow);
            break;
        case 0x2B: // (这个情况现有的游戏不存在)
        case 0x2C:
            sfc_switch_nametable_mirroring(famicom, SFC_NT_MIR_SingleHigh);
            break;
        default:
            // (这个情况现有的游戏不存在)
            switch (ppu_style & 0x07) {
            case 0x00:
            case 0x06:
            case 0x07:
                famicom->ppu.banks[0x8] = famicom->video_memory + 0x400 * (mapper->registers[6] & 1);
                famicom->ppu.banks[0x9] = famicom->ppu.banks[0x8];
                famicom->ppu.banks[0xa] = famicom->video_memory + 0x400 * (mapper->registers[7] & 1);
                famicom->ppu.banks[0xb] = famicom->ppu.banks[0xa];
                break;
            case 0x01:
            case 0x05:
                famicom->ppu.banks[0x8] = famicom->video_memory + 0x400 * (mapper->registers[4] & 1);
                famicom->ppu.banks[0x9] = famicom->video_memory + 0x400 * (mapper->registers[5] & 1);
                famicom->ppu.banks[0xa] = famicom->video_memory + 0x400 * (mapper->registers[6] & 1);
                famicom->ppu.banks[0xb] = famicom->video_memory + 0x400 * (mapper->registers[7] & 1);
                break;
            case 0x02:
            case 0x03:
            case 0x04:
                famicom->ppu.banks[0x8] = famicom->video_memory + 0x400 * (mapper->registers[6] & 1);
                famicom->ppu.banks[0x9] = famicom->video_memory + 0x400 * (mapper->registers[7] & 1);
                famicom->ppu.banks[0xa] = famicom->ppu.banks[0x8];
                famicom->ppu.banks[0xb] = famicom->ppu.banks[0x9];
                break;
            }
            // 镜像
            famicom->ppu.banks[0xc] = famicom->ppu.banks[0x8];
            famicom->ppu.banks[0xd] = famicom->ppu.banks[0x9];
            famicom->ppu.banks[0xe] = famicom->ppu.banks[0xa];
            famicom->ppu.banks[0xf] = famicom->ppu.banks[0xb];
            break;
        }
    }
    // 使用CHR-ROM(现有的游戏没有)
    else {
        // TODO: 完成
        assert(!"NOT IMPL");
    }

IRQ control ($F00x)

VRC系列的IRQ是相同的逻辑, 与其他IRQ系统不同的是, VRC IRQ是基于CPU周期的. 自然, 即便是禁用PPU渲染, 或者VBlank时, 也能触发IRQ.

当然主要功能还是模拟扫描线(切换模式)

       7  bit  0
       ---------
$F000: LLLL LLLL - IRQ Latch
$F001: .... .MEA - IRQ Control
$F002: .... .... - IRQ Acknowledge

A: IRQ Enable after acknowledgement (see IRQ Acknowledge)
E: IRQ Enable (1 = enabled)
M: IRQ Mode (1 = cycle mode, 0 = scanline mode)

$F002确认IRQ中有一个重要的操作E=A 而不是E|=A, 千万不要像自己以为身经百战见得多了, 东边哪个IRQ没有见过? 找了非常长时间的BUG.

扫描线模式, 每根扫描线输出一个时钟信号. 通过将CPU周期除以[114, 114, 113], 然后重复序列. 所以可以认为是除以113.667(NTSC的场合, PAL需要调整至106又16分之5). 不过, 由于目前的架构是基于扫描线进行水平同步的, 所以是反过来的.

(CPU)周期模式, 每个(CPU)时钟周期输出一个信号. 感觉太夸张了, 毕竟一个指令最少也要花2周期, 不知道是不是自己理解错了.

当输出一个时钟信号时:

当然IRQ被禁止的话什么都不会干.

VRC6 扩展声部

这部分才是本篇的本体! 很多NSF作者很喜欢VRC6, 原因是增加两个方波和一个锯齿波, 得到了加强但是又不至于太过分.

Base +0 +1 +2 +3
$9000 方波1占空比-音量 方波1周期-低字节 方波1周期-高字节 频率缩放
$A000 方波2占空比-音量 方波2周期-低字节 方波2周期-高字节 ---
$B000 锯齿波音量 锯齿波周期-低字节 锯齿波周期-高字节 ---

由于NSF的存在, 自己将音频数据放在了核心部分, 而不是依靠Mapper保存.

Frequency Control ($9003)

7  bit  0
---- ----
.... .ABH
      |||
      ||+- Halt
      |+-- 16x frequency (4 octaves up)
      +--- 256x frequency (8 octaves up)

H - halts all oscillators, stopping them in their current state
B - 16x frequency, all oscillators (4 octave increase)
A - 256x frequency, all oscillators (8 octave increase)

那三个游戏都没有使用这个这个寄存器, 仅仅是简单=0. 考虑不用支持?

Pulse Control ($9000,$A000)

7  bit  0
---- ----
MDDD VVVV
|||| ||||
|||| ++++- Volume
|+++------ Duty Cycle
+--------- Mode (1: ignore duty)

Saw Accum Rate ($B000)

7  bit  0
---- ----
..AA AAAA
  ++-++++- Accumulator Rate (controls volume)

Freq Low ($9001,$A001,$B001)

7  bit  0
---- ----
FFFF FFFF
|||| ||||
++++-++++- Low 8 bits of frequency

Freq High ($9002,$A002,$B002)

7  bit  0
---- ----
E... FFFF
|    ||||
|    ++++- High 4 bits of frequency
+--------- Enable (0 = channel disabled)

方波

VRC6的方波与2A03自带的方波类似.

D value   Duty (percent)
0   1/16   6.25%
1   2/16   12.5%
2   3/16   18.75%
3   4/16   25%
4   5/16   31.25%
5   6/16   37.5%
6   7/16   43.75%
7   8/16   50%
M   16/16   100%

M=1时, 持续输出当前音量, 也就是可以认为是[1, 1, ...., 1]

通过E=0, 会复位占空比索引并停止, 输出0. 通过E=1, 会从头恢复.

虽然占空比序列长度是16, 为2A03的2倍. 但是由于是CPU频率驱动的, 所以计算公式还是一致的:

f = CPU / (16 * (t + 1))
t = (CPU / (16 * f)) - 1

具体实现可以考虑像2A03的矩形波一样进行查表, 不过太有规律了, 可以用i <= d进行判断.

这里简单实现为:

squ

锯齿波

这算是一个新东西, 不过和三角波相似, 甚至在函数表述上比三角波还简单.

     *      *
    *      *
---*------*------
  *      *
 *      *
*      *

同样是CPU周期驱动的12bit周期数据. 每两次由周期输出时钟时, 内部的8bit累加器加上[$B000]:A, 每次则输出高5bit. 第七次重置归零:


Step   Accumulator   Output   Comment
0   $00   $00   
1   $00   $00   (odd step, do nothing)
2   $08   $01   (even step, add A to accumulator)
3   $08   $01   
4   $10   $02   
5   $10   $02   
6   $18   $03   
7   $18   $03   
8   $20   $04   
9   $20   $04   
10  $28   $05   
11  $28   $05   
12  $30   $06   
13  $30   $06   
0   $00   $00   (14th step, reset accumulator)
1   $00   $00   
2   $08   $01   

如果A大于42, 会导致8bit数据溢出从而让声音失真. 通过写入E=0累加器会复位到下次E=1. 频率计算公式为:

f = CPU / (14 * (t + 1))
t = (CPU / (14 * f)) - 1

这里简单实现为:

saw

输出

就连wiki上面介绍VRC6输出的口吻也是比较含糊的, 主要有两点:

根据之前DAC中给出的线性逼近的公式: pulse_out = 0.00752 * (pulse1 + pulse2), 可知线性因子大概是0.00752, 由于不太确定, 可以根据情况上下浮动.

title

要点

恶魔城传说模拟出现的问题

bug

(红框处应该有一个蝙蝠状敌人)

同之前的MetalMax, 由于目前精灵是在最后一起渲染的, 导致中途切换BANK会让精灵使用错误的图样表BANK渲染

还有一个小问题就是, 这个画面中, 背景会来回1像素上下跳动, 这个应该是IRQ精度问题

REF