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 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 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 _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 _bgLine = List.filled(_width, 0); List<_OamColor> _oamLine = List.generate(_width, (_) => _OamColor()); List _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 _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 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 _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()); } }