2024-08-31 23:57:44 +02:00

567 lines
11 KiB
Dart

import 'dart:typed_data';
import 'package:dartboy/emulator/utils.dart';
const _width = 256;
const _height = 256;
class _LcdControl extends BitFieldU8 {
_LcdControl() : super({});
_LcdControl.fromU8(int u8) : super.fromU8(u8);
Bit get bgWinEnable => value(0);
Bit get spriteEnable => value(1);
Bit get spriteSize => value(2);
Bit get bgTileMapSelect => value(3);
Bit get tileDataSelect => value(4);
Bit get windowDisplayEnable => value(5);
Bit get windowTileMapSelect => value(6);
Bit get lcdDisplayEnable => value(7);
}
class _LcdStatus extends BitFieldU8 {
_LcdStatus() : super({});
_LcdStatus.fromU8(int u8) : super.fromU8(u8);
Bit get ppuMode0 => value(0);
Bit get ppuMode1 => value(1);
Bit get coincidenceFlag => value(2);
Bit get mode0StatIntEnable => value(3);
Bit get mode1StatIntEnable => value(4);
Bit get mode2StatIntEnable => value(5);
Bit get lycLyStatIntEnable => value(6);
}
class _SpriteFlags extends BitFieldU8 {
_SpriteFlags() : super({});
_SpriteFlags.fromU8(int u8) : super.fromU8(u8);
Bit get paletteNum => value(4);
Bit get xFlip => value(5);
Bit get yFlip => value(6);
Bit get priority => value(7);
}
class _Palette {
_Palette(this.values);
List<int> values;
_Palette.fromU8(int u8)
: values = List.generate(4, (i) => (u8 >> (2 * i)) & 3);
int toU8() => values.fold(0, (acc, col) => (acc << 2) | col);
}
class _Oam {
int yPos = 0;
int xPos = 0;
int tileNum = 0;
_SpriteFlags spriteFlag = _SpriteFlags();
}
enum _Mode {
hBlank,
vBlank,
oamScan,
drawing,
}
class _OamColor {
_OamColor({
this.index = 0,
this.color = 0,
this.blend = false,
});
final int index;
final int color;
final bool blend;
static List<_OamColor> fromColorIndexes(
List<int> indexes, bool blend, _Palette palette) {
return List.generate(8, (i) {
final index = indexes[i];
return _OamColor(
index: index,
blend: blend,
color: palette.values[index],
);
});
}
}
class Ppu {
Ppu();
final List<int> _vram = List.filled(8 * 1024, 0);
_Mode _mode = _Mode.oamScan;
_Mode _prevMode = _Mode.vBlank;
_LcdControl _lcdControl = _LcdControl();
_LcdStatus _lcdStatus = _LcdStatus();
int _windowX = 0;
int _windowY = 0;
int _scrollX = 0;
int _scrollY = 0;
int _cycles = 0;
int _lines = 0;
int _linesCompare = 0;
_Palette _bgPalette = _Palette.fromU8(0x00);
_Palette _objectPalette0 = _Palette.fromU8(0x00);
_Palette _objectPalette1 = _Palette.fromU8(0x00);
bool intVBlank = false;
bool intLcdStat = false;
int _x = 0;
int _y = 0;
final List<_Oam> _oam = List.generate(0xA0, (_) => _Oam());
final List<_Oam> _buffer = [];
List<int> _bgLine = List.filled(_width, 0);
List<_OamColor> _oamLine = List.generate(_width, (_) => _OamColor());
List<int> _curBg = List.filled(8, 0);
bool _drawingWindow = false;
Uint8List pixels = Uint8List(4 * _width * _height);
static const _colorToPixel = [
[0xEF, 0xFD, 0xB4, 0xFF],
[0x8C, 0xAE, 0x04, 0xFF],
[0x65, 0x7F, 0x05, 0xFF],
[0x14, 0x19, 0x01, 0xFF],
];
List<int> _tileToIndexes(int tileNum, int row, bool signed) {
var baseAddr = 0x0000;
if (signed) {
baseAddr = 0x9000 - 0x8000;
}
var indexAddr = row * 2 + tileNum * 16;
if (signed) {
indexAddr = row * 2 + tileNum.toI8() * 16;
}
final addr = baseAddr.wrappingAddU16(indexAddr);
var bit = _vram[addr];
var color = _vram[addr + 1];
List<int> indexes = List.filled(8, 0);
for (var i = 7; i >= 0; i--) {
final index = ((bit & 1) << 1) | (color & 1);
indexes[i] = index;
bit >>= 1;
color >>= 1;
}
return indexes;
}
List<int> _tileMapToColors(int tileX, int tileY, int row, bool high) {
var baseAddr = 0x9800 - 0x8000;
if (high) {
baseAddr = 0x9C00 - 0x8000;
}
final indexAddr = tileX + tileY * 32;
final addr = baseAddr.wrappingAddU16(indexAddr);
final tileNum = _vram[addr];
return _tileToIndexes(tileNum, row, !_lcdControl.tileDataSelect.val);
}
List<_OamColor> _oamToColors(_Oam oam) {
var row = _y + 16 - oam.yPos;
var tile = oam.tileNum;
if (oam.spriteFlag.yFlip.val) {
var limit = 8;
if (_lcdControl.spriteSize.val) {
limit = 16;
}
row = limit - row - 1;
}
if (row >= 8) {
row -= 8;
tile += 1;
}
var palette = _objectPalette0;
if (oam.spriteFlag.paletteNum.val) {
palette = _objectPalette1;
}
final blend = oam.spriteFlag.priority;
var colors = _OamColor.fromColorIndexes(
_tileToIndexes(tile, row, false), blend.val, palette);
if (oam.spriteFlag.xFlip.val) {
colors = colors.reversed.toList();
}
return colors;
}
void _scanOam(int i) {
var size = 8;
if (_lcdControl.spriteSize.val) {
size = 16;
}
final o = _oam[i];
final curY = _lines + 16;
final targetY = o.yPos;
if (o.xPos > 8 &&
curY < targetY + size &&
targetY <= curY &&
_buffer.length < 10) {
_buffer.add(o);
}
}
void _drawBg() {
if (_drawingWindow) {
return;
}
final cx = _x.wrappingAddU8(_scrollX);
final cy = _y.wrappingAddU8(_scrollY);
final col = cx % 8;
final row = cy % 8;
final tileX = cx ~/ 8;
final tileY = cy ~/ 8;
if (col == 0 || _x == 0) {
_curBg =
_tileMapToColors(tileX, tileY, row, _lcdControl.bgTileMapSelect.val);
}
_bgLine[_x] = _curBg[col];
}
void _drawWindow() {
if (!_drawingWindow && !(_x + 7 == _windowX && _y >= _windowY)) {
return;
}
_drawingWindow = true;
final cx = _x.wrappingSubU8(_windowX);
final cy = _y.wrappingSubU8(_windowY);
final col = cx % 8;
final row = cy % 8;
final tileX = cx ~/ 8;
final tileY = cy ~/ 8;
if (col == 0 || _x == 0) {
_curBg = _tileMapToColors(
tileX,
tileY,
row,
_lcdControl.windowTileMapSelect.val,
);
}
_bgLine[_x] = _curBg[col];
}
void _drawSprite() {
for (final oam in _buffer) {
if (oam.xPos == _x + 8) {
final colors = _oamToColors(oam);
for (var i = 0; i < 8; i++) {
_oamLine[_x + i] = colors[i];
}
}
}
}
void _putPixels(int x) {
final index = _bgLine[x];
var color = _bgPalette.values[index];
final oam = _oamLine[x];
if ((!oam.blend || index == 0) && oam.index != 0) {
color = oam.color;
}
final pixel = _colorToPixel[color];
for (var i = 0; i < 4; i++) {
pixels[(x + _y * _width) * 4 + i] = pixel[i];
}
}
void tick() {
_cycles += 1;
if (_cycles >= 456) {
_cycles = 0;
_lines += 1;
_buffer.clear();
_bgLine = List.filled(_width, 0);
_oamLine = List.generate(_width, (_) => _OamColor());
}
if (_lines >= 154) {
_lines = 0;
}
if (_cycles == 80) {
_x = 0;
}
if (_lines == 0) {
_y = 0;
}
if (_lines < 144) {
_y = _lines;
if (0 <= _cycles && _cycles <= 79) {
_mode = _Mode.oamScan;
}
if (_cycles == 80) {
_mode = _Mode.drawing;
}
if (81 <= _cycles && _cycles <= 239) {
_x += 1;
}
if (240 <= _cycles && _cycles <= 455) {
_mode = _Mode.hBlank;
}
}
if (_lines == 144) {
_mode = _Mode.vBlank;
}
switch (_mode) {
case _Mode.drawing:
if (_prevMode != _mode) {
_lcdStatus.ppuMode0.set();
_lcdStatus.ppuMode1.set();
}
if (_lcdControl.bgWinEnable.val) {
if (_lcdControl.windowDisplayEnable.val) {
_drawWindow();
}
_drawBg();
}
if (_lcdControl.spriteEnable.val) {
_drawSprite();
}
break;
case _Mode.hBlank:
if (_prevMode != _mode) {
_lcdStatus.ppuMode0.reset();
_lcdStatus.ppuMode1.reset();
intLcdStat |= _lcdStatus.mode0StatIntEnable.val;
_lcdStatus.coincidenceFlag.val = _lines == _linesCompare;
intLcdStat |= _lcdStatus.lycLyStatIntEnable.val &&
_lcdStatus.coincidenceFlag.val;
_drawingWindow = false;
}
if (_cycles < 400) {
_putPixels(_cycles - 240);
}
break;
case _Mode.oamScan:
if (_prevMode != _mode) {
_lcdStatus.ppuMode0.reset();
_lcdStatus.ppuMode1.set();
intLcdStat |= _lcdStatus.mode2StatIntEnable.val;
}
if (_cycles % 2 == 0) {
_scanOam(_cycles ~/ 2);
}
break;
case _Mode.vBlank:
if (_prevMode != _mode) {
_lcdStatus.ppuMode0.set();
_lcdStatus.ppuMode1.reset();
intVBlank = true;
intLcdStat |= _lcdStatus.mode1StatIntEnable.val;
}
break;
default:
}
_prevMode = _mode;
}
int read(int addr) {
return _vram[addr - 0x8000];
}
void write(int addr, int val) {
_vram[addr - 0x8000] = val;
}
int readOam(int addr) {
final indexAddr = addr - 0xFE00;
final index = indexAddr ~/ 4;
final offset = indexAddr % 4;
final o = _oam[index];
switch (offset) {
case 0:
return o.yPos;
case 1:
return o.xPos;
case 2:
return o.tileNum;
case 3:
return o.spriteFlag.toU8();
default:
return 0;
}
}
void writeOam(int addr, int val) {
final indexAddr = addr - 0xFE00;
final index = indexAddr ~/ 4;
final offset = indexAddr % 4;
switch (offset) {
case 0:
_oam[index].yPos = val;
break;
case 1:
_oam[index].xPos = val;
break;
case 2:
_oam[index].tileNum = val;
break;
case 3:
_oam[index].spriteFlag = _SpriteFlags.fromU8(val);
break;
}
}
int readLcdControl() {
return _lcdControl.toU8();
}
void writeLcdControl(int val) {
_lcdControl = _LcdControl.fromU8(val);
}
int readLcdStatus() {
return _lcdStatus.toU8();
}
void writeLcdStatus(int val) {
_lcdStatus = _LcdStatus.fromU8(val);
}
int readScrollY() {
return _scrollY;
}
void writeScrollY(int val) {
_scrollY = val;
}
int readScrollX() {
return _scrollX;
}
void writeScrollX(int val) {
_scrollX = val;
}
int readLines() {
return _lines;
}
int readLineCompare() {
return _linesCompare;
}
void writeLineCompare(int val) {
_linesCompare = val;
}
int readWindowX() {
return _windowX;
}
void writeWindowX(int val) {
_windowX = val;
}
int readWindowY() {
return _windowY;
}
void writeWindowY(int val) {
_windowY = val;
}
int readBgPalette() {
return _bgPalette.toU8();
}
void writeBgPalette(int val) {
_bgPalette = _Palette.fromU8(val);
}
int readObjectPalette0() {
return _objectPalette0.toU8();
}
void writeObjectPalette0(int val) {
_objectPalette0 = _Palette.fromU8(val);
}
int readObjectPalette1() {
return _objectPalette1.toU8();
}
void writeObjectPalette1(int val) {
_objectPalette1 = _Palette.fromU8(val);
}
Uint8List render() {
return Uint8List.fromList(pixels.toList());
}
}