diff --git a/src/GameBoy.Core/Video/Ppu.Render.cs b/src/GameBoy.Core/Video/Ppu.Render.cs new file mode 100644 index 0000000..fe4f16b --- /dev/null +++ b/src/GameBoy.Core/Video/Ppu.Render.cs @@ -0,0 +1,198 @@ +namespace GameBoy.Video +{ + public sealed partial class Ppu + { + private readonly byte[] lineBgColor = new byte[ScreenWidth]; + + private void RenderScanline() + { + RenderBackground(); + RenderWindow(); + RenderSprites(); + } + + private void RenderBackground() + { + int rowBase = ly * ScreenWidth; + bool bgEnabled = (lcdc & 0x01) != 0; + + if (!bgEnabled) + { + for (int x = 0; x < ScreenWidth; x++) + { + lineBgColor[x] = 0; + Framebuffer[rowBase + x] = 0; + } + return; + } + + int tileMapBase = (lcdc & 0x08) != 0 ? 0x1C00 : 0x1800; + bool signed = (lcdc & 0x10) == 0; + int y = (scy + ly) & 0xFF; + int tileRow = y >> 3; + int rowInTile = y & 7; + + for (int x = 0; x < ScreenWidth; x++) + { + int mapX = (scx + x) & 0xFF; + int tileCol = mapX >> 3; + int bit = 7 - (mapX & 7); + + int tileIndex = vram[tileMapBase + tileRow * 32 + tileCol]; + int tileAddr = signed + ? 0x1000 + ((sbyte)tileIndex) * 16 + rowInTile * 2 + : tileIndex * 16 + rowInTile * 2; + + int low = vram[tileAddr]; + int high = vram[tileAddr + 1]; + int color = (((high >> bit) & 1) << 1) | ((low >> bit) & 1); + + lineBgColor[x] = (byte)color; + Framebuffer[rowBase + x] = Shade(bgp, color); + } + } + + private void RenderWindow() + { + if ((lcdc & 0x20) == 0) + return; + if (wy > ly) + return; + if (wx > 166) + return; + + int rowBase = ly * ScreenWidth; + int tileMapBase = (lcdc & 0x40) != 0 ? 0x1C00 : 0x1800; + bool signed = (lcdc & 0x10) == 0; + int winY = windowLine; + int tileRow = winY >> 3; + int rowInTile = winY & 7; + int start = wx - 7; + bool drawn = false; + + for (int x = 0; x < ScreenWidth; x++) + { + if (x < start) + continue; + drawn = true; + + int winX = x - start; + int tileCol = winX >> 3; + int bit = 7 - (winX & 7); + + int tileIndex = vram[tileMapBase + tileRow * 32 + tileCol]; + int tileAddr = signed + ? 0x1000 + ((sbyte)tileIndex) * 16 + rowInTile * 2 + : tileIndex * 16 + rowInTile * 2; + + int low = vram[tileAddr]; + int high = vram[tileAddr + 1]; + int color = (((high >> bit) & 1) << 1) | ((low >> bit) & 1); + + lineBgColor[x] = (byte)color; + Framebuffer[rowBase + x] = Shade(bgp, color); + } + + if (drawn) + windowLine++; + } + + private readonly int[] spriteCandidates = new int[10]; + + private void RenderSprites() + { + if ((lcdc & 0x02) == 0) + return; + + int height = (lcdc & 0x04) != 0 ? 16 : 8; + int count = 0; + + for (int i = 0; i < 40 && count < 10; i++) + { + int spriteY = oam[i * 4] - 16; + if (ly >= spriteY && ly < spriteY + height) + spriteCandidates[count++] = i; + } + + SortByPriority(count); + + int rowBase = ly * ScreenWidth; + for (int x = 0; x < ScreenWidth; x++) + { + for (int c = 0; c < count; c++) + { + int i = spriteCandidates[c]; + int spriteX = oam[i * 4 + 1] - 8; + if (x < spriteX || x >= spriteX + 8) + continue; + + int attr = oam[i * 4 + 3]; + int spriteY = oam[i * 4] - 16; + int row = ly - spriteY; + if ((attr & 0x40) != 0) + row = height - 1 - row; + + int tile = oam[i * 4 + 2]; + if (height == 16) + { + tile &= 0xFE; + if (row >= 8) + { + tile += 1; + row -= 8; + } + } + + int pixelCol = x - spriteX; + int col = (attr & 0x20) != 0 ? 7 - pixelCol : pixelCol; + int bit = 7 - col; + + int tileAddr = tile * 16 + row * 2; + int low = vram[tileAddr]; + int high = vram[tileAddr + 1]; + int color = (((high >> bit) & 1) << 1) | ((low >> bit) & 1); + + if (color == 0) + continue; + + bool behindBg = (attr & 0x80) != 0 && lineBgColor[x] != 0; + if (!behindBg) + { + byte palette = (attr & 0x10) != 0 ? obp1 : obp0; + Framebuffer[rowBase + x] = Shade(palette, color); + } + break; + } + } + } + + private void SortByPriority(int count) + { + for (int a = 1; a < count; a++) + { + int key = spriteCandidates[a]; + int keyX = oam[key * 4 + 1]; + int b = a - 1; + while (b >= 0 && ShouldSwap(spriteCandidates[b], keyX, key)) + { + spriteCandidates[b + 1] = spriteCandidates[b]; + b--; + } + spriteCandidates[b + 1] = key; + } + } + + private bool ShouldSwap(int current, int keyX, int key) + { + int currentX = oam[current * 4 + 1]; + if (currentX != keyX) + return currentX > keyX; + return current > key; + } + + private static byte Shade(byte palette, int color) + { + return (byte)((palette >> (color * 2)) & 0x03); + } + } +}