aboutsummaryrefslogtreecommitdiffstats
path: root/src/ppu.c
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-04 22:11:10 +0200
committergbc dev <gbc@localhost>2026-07-04 22:11:10 +0200
commitbc48df9f7162a4a816fdc0edaa6ed08304008c9c (patch)
tree30129f7655c177a78e62505c55243a5c0540126f /src/ppu.c
parentscaffolding: build system, cartridge loader with MBC1/2/3/5, core types (diff)
downloadsl0pboy-bc48df9f7162a4a816fdc0edaa6ed08304008c9c.tar.gz
sl0pboy-bc48df9f7162a4a816fdc0edaa6ed08304008c9c.tar.xz
sl0pboy-bc48df9f7162a4a816fdc0edaa6ed08304008c9c.zip
core emulator: SM83 CPU, MMU, timer, PPU, CGB support
Passes blargg cpu_instrs (all 11), instr_timing, mem_timing.
Diffstat (limited to 'src/ppu.c')
-rw-r--r--src/ppu.c355
1 files changed, 355 insertions, 0 deletions
diff --git a/src/ppu.c b/src/ppu.c
new file mode 100644
index 0000000..16ecc2e
--- /dev/null
+++ b/src/ppu.c
@@ -0,0 +1,355 @@
+#include "ppu.h"
+#include <string.h>
+
+// LCDC bits
+#define LCDC_BG_EN 0x01
+#define LCDC_OBJ_EN 0x02
+#define LCDC_OBJ_SIZE 0x04
+#define LCDC_BG_MAP 0x08
+#define LCDC_TILE_DATA 0x10
+#define LCDC_WIN_EN 0x20
+#define LCDC_WIN_MAP 0x40
+#define LCDC_LCD_EN 0x80
+
+// STAT bits
+#define STAT_LYC_INT 0x40
+#define STAT_M2_INT 0x20
+#define STAT_M1_INT 0x10
+#define STAT_M0_INT 0x08
+#define STAT_LYC_FLAG 0x04
+
+void gb_hdma_hblank(GB *gb); // from gb.c
+
+// DMG shades (greenish palette), index 0..3
+static const u8 dmg_shades[4][3] = {
+ {224, 248, 208}, {136, 192, 112}, {52, 104, 86}, {8, 24, 32}
+};
+
+void ppu_reset(GB *gb) {
+ PPU *p = &gb->ppu;
+ p->lcdc = 0x91;
+ p->stat = 0x85;
+ p->bgp = 0xFC;
+ p->obp0 = 0xFF;
+ p->obp1 = 0xFF;
+ p->ly = 0;
+ p->mode = 2;
+ p->dots = 0;
+ p->vram_bank = 0;
+ memset(p->bg_pal, 0xFF, sizeof(p->bg_pal));
+ memset(p->obj_pal, 0xFF, sizeof(p->obj_pal));
+}
+
+// ---- VRAM / OAM access ----
+u8 ppu_read(GB *gb, u16 addr) {
+ PPU *p = &gb->ppu;
+ if (addr >= 0x8000 && addr < 0xA000)
+ return p->vram[p->vram_bank * 0x2000 + (addr - 0x8000)];
+ if (addr >= 0xFE00 && addr < 0xFEA0)
+ return p->oam[addr - 0xFE00];
+ return 0xFF;
+}
+void ppu_write(GB *gb, u16 addr, u8 val) {
+ PPU *p = &gb->ppu;
+ if (addr >= 0x8000 && addr < 0xA000)
+ p->vram[p->vram_bank * 0x2000 + (addr - 0x8000)] = val;
+ else if (addr >= 0xFE00 && addr < 0xFEA0)
+ p->oam[addr - 0xFE00] = val;
+}
+
+static inline u8 vram_at(PPU *p, int bank, u16 addr) {
+ return p->vram[bank * 0x2000 + (addr - 0x8000)];
+}
+
+// ---- registers ----
+u8 ppu_read_reg(GB *gb, u16 addr) {
+ PPU *p = &gb->ppu;
+ switch (addr) {
+ case 0xFF40: return p->lcdc;
+ case 0xFF41: return p->stat | 0x80;
+ case 0xFF42: return p->scy;
+ case 0xFF43: return p->scx;
+ case 0xFF44: return p->ly;
+ case 0xFF45: return p->lyc;
+ case 0xFF47: return p->bgp;
+ case 0xFF48: return p->obp0;
+ case 0xFF49: return p->obp1;
+ case 0xFF4A: return p->wy;
+ case 0xFF4B: return p->wx;
+ case 0xFF68: return p->bcps;
+ case 0xFF69: return p->bg_pal[p->bcps & 0x3F];
+ case 0xFF6A: return p->ocps;
+ case 0xFF6B: return p->obj_pal[p->ocps & 0x3F];
+ }
+ return 0xFF;
+}
+
+static void check_lyc(GB *gb) {
+ PPU *p = &gb->ppu;
+ if (p->ly == p->lyc) p->stat |= STAT_LYC_FLAG;
+ else p->stat &= ~STAT_LYC_FLAG;
+}
+
+void ppu_write_reg(GB *gb, u16 addr, u8 val) {
+ PPU *p = &gb->ppu;
+ switch (addr) {
+ case 0xFF40: {
+ bool was_on = p->lcdc & LCDC_LCD_EN;
+ p->lcdc = val;
+ if (was_on && !(val & LCDC_LCD_EN)) {
+ p->ly = 0; p->dots = 0; p->mode = 0;
+ p->stat &= ~0x03;
+ } else if (!was_on && (val & LCDC_LCD_EN)) {
+ p->mode = 2; p->dots = 0; p->ly = 0;
+ check_lyc(gb);
+ }
+ break;
+ }
+ case 0xFF41: p->stat = (p->stat & 0x07) | (val & 0x78); break;
+ case 0xFF42: p->scy = val; break;
+ case 0xFF43: p->scx = val; break;
+ case 0xFF44: break; // LY read-only
+ case 0xFF45: p->lyc = val; check_lyc(gb); break;
+ case 0xFF47: p->bgp = val; break;
+ case 0xFF48: p->obp0 = val; break;
+ case 0xFF49: p->obp1 = val; break;
+ case 0xFF4A: p->wy = val; break;
+ case 0xFF4B: p->wx = val; break;
+ case 0xFF68: p->bcps = val & 0xBF; break;
+ case 0xFF69:
+ p->bg_pal[p->bcps & 0x3F] = val;
+ if (p->bcps & 0x80) p->bcps = 0x80 | ((p->bcps + 1) & 0x3F);
+ break;
+ case 0xFF6A: p->ocps = val & 0xBF; break;
+ case 0xFF6B:
+ p->obj_pal[p->ocps & 0x3F] = val;
+ if (p->ocps & 0x80) p->ocps = 0x80 | ((p->ocps + 1) & 0x3F);
+ break;
+ }
+}
+
+// ---- palette helpers ----
+static void cgb_color(u8 *pal, int palnum, int cidx, u8 out[3]) {
+ int i = (palnum * 4 + cidx) * 2;
+ u16 c = pal[i] | (pal[i+1] << 8);
+ int r = c & 0x1F, g = (c >> 5) & 0x1F, b = (c >> 10) & 0x1F;
+ // scale 5-bit to 8-bit with a mild color-correction curve
+ out[0] = (r * 13 + g * 2 + b) >> 1;
+ out[1] = (g * 3 + b) << 1;
+ out[2] = (r * 3 + g * 2 + b * 11) >> 1;
+}
+
+// ---- scanline rendering ----
+static void render_scanline(GB *gb) {
+ PPU *p = &gb->ppu;
+ int ly = p->ly;
+ if (ly >= SCREEN_H) return;
+
+ u8 bg_prio[SCREEN_W]; // BG color index (for sprite priority)
+ u8 bg_hasprio[SCREEN_W]; // CGB BG-to-OAM priority bit
+
+ bool cgb = gb->cgb_mode;
+ bool bg_master = (p->lcdc & LCDC_BG_EN) || cgb; // in CGB, bit0 = master priority
+
+ // ----- Background & Window -----
+ for (int x = 0; x < SCREEN_W; x++) {
+ bg_prio[x] = 0;
+ bg_hasprio[x] = 0;
+
+ bool use_win = (p->lcdc & LCDC_WIN_EN) && (ly >= p->wy) && (x + 7 >= p->wx);
+ int px, py;
+ u16 map_base;
+ if (use_win) {
+ px = x + 7 - p->wx;
+ py = p->wly;
+ map_base = (p->lcdc & LCDC_WIN_MAP) ? 0x9C00 : 0x9800;
+ } else {
+ px = (x + p->scx) & 0xFF;
+ py = (ly + p->scy) & 0xFF;
+ map_base = (p->lcdc & LCDC_BG_MAP) ? 0x9C00 : 0x9800;
+ }
+ int tx = px / 8, ty = py / 8;
+ u16 map_addr = map_base + ty * 32 + tx;
+ u8 tile = vram_at(p, 0, map_addr);
+
+ u8 attr = cgb ? vram_at(p, 1, map_addr) : 0;
+ int tbank = (attr & 0x08) ? 1 : 0;
+ bool xflip = attr & 0x20, yflip = attr & 0x40;
+ int palnum = attr & 0x07;
+ bool prio = attr & 0x80;
+
+ int row = py % 8, col = px % 8;
+ if (yflip) row = 7 - row;
+ if (xflip) col = 7 - col;
+
+ u16 tile_addr;
+ if (p->lcdc & LCDC_TILE_DATA)
+ tile_addr = 0x8000 + tile * 16 + row * 2;
+ else
+ tile_addr = 0x9000 + ((s8)tile) * 16 + row * 2;
+
+ u8 lo = vram_at(p, tbank, tile_addr);
+ u8 hi = vram_at(p, tbank, tile_addr + 1);
+ int bit = 7 - col;
+ int cidx = ((lo >> bit) & 1) | (((hi >> bit) & 1) << 1);
+
+ bg_prio[x] = cidx;
+ bg_hasprio[x] = prio;
+
+ u8 rgb[3];
+ if (!bg_master) {
+ rgb[0] = dmg_shades[0][0]; rgb[1] = dmg_shades[0][1]; rgb[2] = dmg_shades[0][2];
+ } else if (cgb) {
+ cgb_color(p->bg_pal, palnum, cidx, rgb);
+ } else {
+ int shade = (p->bgp >> (cidx * 2)) & 3;
+ memcpy(rgb, dmg_shades[shade], 3);
+ }
+ memcpy(p->fb[ly][x], rgb, 3);
+ }
+ if ((p->lcdc & LCDC_WIN_EN) && ly >= p->wy && p->wx < 167)
+ p->wly++;
+
+ // ----- Sprites -----
+ if (!(p->lcdc & LCDC_OBJ_EN)) return;
+ int h = (p->lcdc & LCDC_OBJ_SIZE) ? 16 : 8;
+
+ // gather up to 10 sprites on this line
+ int chosen[10], n = 0;
+ for (int i = 0; i < 40 && n < 10; i++) {
+ int sy = p->oam[i*4] - 16;
+ if (ly >= sy && ly < sy + h) chosen[n++] = i;
+ }
+ // draw back-to-front: lower priority first.
+ // DMG: smaller X = higher priority (drawn last). CGB: lower OAM index = higher.
+ for (int a = 0; a < n; a++) {
+ for (int b = a + 1; b < n; b++) {
+ bool swap;
+ if (cgb) swap = chosen[b] < chosen[a];
+ else {
+ int xa = p->oam[chosen[a]*4+1], xb = p->oam[chosen[b]*4+1];
+ swap = (xb > xa) || (xb == xa && chosen[b] < chosen[a]);
+ }
+ if (swap) { int t = chosen[a]; chosen[a] = chosen[b]; chosen[b] = t; }
+ }
+ }
+
+ for (int k = 0; k < n; k++) {
+ int i = chosen[k];
+ int sy = p->oam[i*4] - 16;
+ int sx = p->oam[i*4+1] - 8;
+ u8 tile = p->oam[i*4+2];
+ u8 attr = p->oam[i*4+3];
+ bool xflip = attr & 0x20, yflip = attr & 0x40;
+ bool behind = attr & 0x80;
+ if (h == 16) tile &= 0xFE;
+
+ int row = ly - sy;
+ if (yflip) row = h - 1 - row;
+ int tbank = (cgb && (attr & 0x08)) ? 1 : 0;
+
+ u16 tile_addr = 0x8000 + tile * 16 + row * 2;
+ u8 lo = vram_at(p, tbank, tile_addr);
+ u8 hi = vram_at(p, tbank, tile_addr + 1);
+
+ for (int col = 0; col < 8; col++) {
+ int x = sx + col;
+ if (x < 0 || x >= SCREEN_W) continue;
+ int bit = xflip ? col : 7 - col;
+ int cidx = ((lo >> bit) & 1) | (((hi >> bit) & 1) << 1);
+ if (cidx == 0) continue; // transparent
+
+ // priority resolution
+ if (cgb) {
+ // BG master priority (LCDC.0) forces BG over OBJ when set
+ if ((p->lcdc & LCDC_BG_EN)) {
+ if ((behind || bg_hasprio[x]) && bg_prio[x] != 0) continue;
+ }
+ } else {
+ if (behind && bg_prio[x] != 0) continue;
+ }
+
+ u8 rgb[3];
+ if (cgb) {
+ cgb_color(p->obj_pal, attr & 0x07, cidx, rgb);
+ } else {
+ u8 pal = (attr & 0x10) ? p->obp1 : p->obp0;
+ int shade = (pal >> (cidx * 2)) & 3;
+ memcpy(rgb, dmg_shades[shade], 3);
+ }
+ memcpy(p->fb[ly][x], rgb, 3);
+ }
+ }
+}
+
+// ---- STAT interrupt line (edge-triggered) ----
+static void update_stat(GB *gb, bool lyc_check) {
+ PPU *p = &gb->ppu;
+ static bool prev_line = false;
+ if (lyc_check) check_lyc(gb);
+
+ bool line = false;
+ switch (p->mode) {
+ case 0: if (p->stat & STAT_M0_INT) line = true; break;
+ case 1: if (p->stat & STAT_M1_INT) line = true; break;
+ case 2: if (p->stat & STAT_M2_INT) line = true; break;
+ }
+ if ((p->stat & STAT_LYC_INT) && (p->stat & STAT_LYC_FLAG)) line = true;
+
+ if (line && !prev_line) gb_request_interrupt(gb, INT_LCDSTAT);
+ prev_line = line;
+}
+
+void ppu_tick(GB *gb, int tcycles) {
+ PPU *p = &gb->ppu;
+ if (!(p->lcdc & LCDC_LCD_EN)) return;
+
+ for (int t = 0; t < tcycles; t++) {
+ p->dots++;
+
+ switch (p->mode) {
+ case 2: // OAM scan
+ if (p->dots >= 80) { p->mode = 3; p->stat = (p->stat & ~3) | 3; }
+ break;
+ case 3: // drawing
+ if (p->dots >= 80 + 172) {
+ p->mode = 0;
+ p->stat = (p->stat & ~3);
+ render_scanline(gb);
+ gb_hdma_hblank(gb);
+ update_stat(gb, false);
+ }
+ break;
+ case 0: // hblank
+ if (p->dots >= 456) {
+ p->dots -= 456;
+ p->ly++;
+ if (p->ly == 144) {
+ p->mode = 1;
+ p->stat = (p->stat & ~3) | 1;
+ p->wly = 0;
+ gb_request_interrupt(gb, INT_VBLANK);
+ p->frame_ready = true;
+ update_stat(gb, true);
+ } else {
+ p->mode = 2;
+ p->stat = (p->stat & ~3) | 2;
+ update_stat(gb, true);
+ }
+ }
+ break;
+ case 1: // vblank
+ if (p->dots >= 456) {
+ p->dots -= 456;
+ p->ly++;
+ if (p->ly > 153) {
+ p->ly = 0;
+ p->mode = 2;
+ p->stat = (p->stat & ~3) | 2;
+ }
+ update_stat(gb, true);
+ }
+ break;
+ }
+ }
+}