Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/Cpu/Cpu.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Gb\Cpu\Register\FlagRegister;
use Gb\Cpu\Register\Register16;
use Gb\Interrupts\InterruptController;
use Gb\System\CgbController;

/**
* LR35902 CPU (Sharp SM83)
Expand Down Expand Up @@ -45,6 +46,9 @@ final class Cpu
private int $pendingCycles = 0;
private ?\Closure $cycleCallback = null;

// CGB controller for speed switching (optional, only needed for CGB)
private ?CgbController $cgbController = null;

/**
* @param BusInterface $bus Memory bus for reading/writing memory
* @param InterruptController $interruptController Interrupt controller
Expand Down Expand Up @@ -454,6 +458,36 @@ public function setStopped(bool $stopped): void
$this->stopped = $stopped;
}

/**
* Set the CGB controller for speed switching support.
*
* @param CgbController $cgbController CGB controller instance
*/
public function setCgbController(CgbController $cgbController): void
{
$this->cgbController = $cgbController;
}

/**
* Execute STOP instruction.
* In CGB mode with speed switch prepared, triggers speed switch.
* Otherwise, stops the CPU until button press.
*/
public function executeStop(): void
{
// Check if CGB speed switch is prepared
if ($this->cgbController !== null && $this->cgbController->isSpeedSwitchPrepared()) {
// Trigger speed switch (toggles between normal and double speed)
$this->cgbController->triggerSpeedSwitch();
// Don't stop the CPU - execution continues at new speed
$this->stopped = false;
} else {
// Normal STOP behavior: halt and stop until button press
$this->halted = true;
$this->stopped = true;
}
}

/**
* Get the Interrupt Master Enable flag.
*
Expand Down
6 changes: 3 additions & 3 deletions src/Cpu/InstructionSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ private static function buildInstruction(int $opcode): Instruction
},
),

// 0x10: STOP - Stop CPU and LCD until button press
// 0x10: STOP - Stop CPU and LCD until button press (or trigger speed switch in CGB)
0x10 => new Instruction(
opcode: 0x10,
mnemonic: 'STOP',
Expand All @@ -380,8 +380,8 @@ private static function buildInstruction(int $opcode): Instruction
handler: static function (Cpu $cpu): int {
// Read next byte (should be 0x00)
self::readImm8($cpu);
$cpu->setHalted(true);
$cpu->setStopped(true);
// Execute STOP - handles both speed switching and normal stop
$cpu->executeStop();
return 4;
},
),
Expand Down
13 changes: 12 additions & 1 deletion src/Emulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ private function initializeSystem(): void
$this->interruptController
);

// Enable CGB mode if cartridge supports it
if ($this->cartridge->getHeader()->isCgbSupported()) {
$this->ppu->enableCgbMode(true);
}

// Create APU
$this->apu = new Apu($this->audioSink);

Expand Down Expand Up @@ -177,8 +182,11 @@ private function initializeSystem(): void
// HDMA registers: HDMA1-HDMA5
$this->bus->attachIoDevice($this->hdma, 0xFF51, 0xFF52, 0xFF53, 0xFF54, 0xFF55);

// Connect HDMA to PPU for H-Blank triggers
$this->ppu->setHdmaController($this->hdma);

// Create CGB controller
$this->cgb = new CgbController($vram);
$this->cgb = new CgbController($vram, $wram);
// CGB registers: KEY1, VBK, RP, SVBK
$this->bus->attachIoDevice($this->cgb, 0xFF4D, 0xFF4F, 0xFF56, 0xFF70);

Expand All @@ -197,6 +205,9 @@ private function initializeSystem(): void
// Create CPU
$this->cpu = new Cpu($this->bus, $this->interruptController);

// Set CGB controller on CPU for speed switching support
$this->cpu->setCgbController($this->cgb);

// Optimization (Step 14): Pre-build all 512 instructions for faster dispatch
// Expected: 1-2% performance gain by eliminating lazy initialization checks
\Gb\Cpu\InstructionSet::warmCache();
Expand Down
64 changes: 55 additions & 9 deletions src/Memory/Wram.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,25 @@
* In DMG mode: Single 8KB bank
* In CGB mode: Bank 0 (0xC000-0xCFFF) + switchable banks 1-7 (0xD000-0xDFFF)
*
* For now, implements DMG-style 8KB WRAM. CGB bank switching will be added later.
* CGB WRAM: 32KB (8 banks of 4KB each)
* - Bank 0: Always at 0xC000-0xCFFF
* - Banks 1-7: Switchable at 0xD000-0xDFFF via SVBK register
*/
final class Wram implements DeviceInterface
{
/** @var array<int, int> Working RAM storage (8KB = 8192 bytes) */
private array $ram;
/** @var array<int, array<int, int>> Working RAM storage (8 banks × 4KB) */
private array $banks;

/** @var int Currently selected bank for 0xD000-0xDFFF (1-7, defaults to 1) */
private int $currentBank = 1;

public function __construct()
{
// Initialize 8KB of RAM with 0x00
$this->ram = array_fill(0, 8192, 0x00);
// Initialize 8 banks of 4KB each with 0x00
$this->banks = [];
for ($bank = 0; $bank < 8; $bank++) {
$this->banks[$bank] = array_fill(0, 4096, 0x00);
}
}

/**
Expand All @@ -34,8 +42,16 @@ public function __construct()
*/
public function readByte(int $address): int
{
$offset = $address & 0x1FFF; // Mask to 8KB
return $this->ram[$offset];
$offset = $address & 0x1FFF;

if ($offset < 0x1000) {
// 0xC000-0xCFFF: Always bank 0
return $this->banks[0][$offset];
} else {
// 0xD000-0xDFFF: Switchable bank (1-7)
$bankOffset = $offset - 0x1000;
return $this->banks[$this->currentBank][$bankOffset];
}
}

/**
Expand All @@ -46,7 +62,37 @@ public function readByte(int $address): int
*/
public function writeByte(int $address, int $value): void
{
$offset = $address & 0x1FFF; // Mask to 8KB
$this->ram[$offset] = $value & 0xFF;
$offset = $address & 0x1FFF;
$value = $value & 0xFF;

if ($offset < 0x1000) {
// 0xC000-0xCFFF: Always bank 0
$this->banks[0][$offset] = $value;
} else {
// 0xD000-0xDFFF: Switchable bank (1-7)
$bankOffset = $offset - 0x1000;
$this->banks[$this->currentBank][$bankOffset] = $value;
}
}

/**
* Set the current WRAM bank (CGB only, controlled by SVBK register).
*
* @param int $bank Bank number (0-7, where 0 is treated as 1)
*/
public function setBank(int $bank): void
{
// Bank 0 is treated as bank 1 (banks 1-7 are valid)
$this->currentBank = ($bank & 0x07) === 0 ? 1 : ($bank & 0x07);
}

/**
* Get the current WRAM bank number.
*
* @return int Current bank (1-7)
*/
public function getBank(): int
{
return $this->currentBank;
}
}
11 changes: 9 additions & 2 deletions src/Ppu/ColorPalette.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,15 @@ final class ColorPalette
public function __construct()
{
// Initialize palettes with white (0x7FFF = all 1s in 15-bit RGB)
$this->bgPalette = array_fill(0, 64, 0xFF);
$this->objPalette = array_fill(0, 64, 0xFF);
// Each color is 2 bytes: low byte (0xFF), high byte (0x7F)
$this->bgPalette = [];
$this->objPalette = [];
for ($i = 0; $i < 64; $i += 2) {
$this->bgPalette[$i] = 0xFF; // Low byte
$this->bgPalette[$i + 1] = 0x7F; // High byte
$this->objPalette[$i] = 0xFF; // Low byte
$this->objPalette[$i + 1] = 0x7F; // High byte
}
}

/**
Expand Down
62 changes: 49 additions & 13 deletions src/Ppu/Ppu.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Gb\Ppu;

use Gb\Bus\DeviceInterface;
use Gb\Dma\HdmaController;
use Gb\Interrupts\InterruptController;
use Gb\Interrupts\InterruptType;
use Gb\Memory\Vram;
Expand Down Expand Up @@ -97,12 +98,19 @@ final class Ppu implements DeviceInterface
/** @var array<int, int> */
private array $bgColorBuffer = [];

// Background priority buffer for CGB priority rules (stores BG-to-OAM priority bit)
/** @var array<int, bool> */
private array $bgPriorityBuffer = [];

/** @var ColorPalette Color palette system (CGB) */
private readonly ColorPalette $colorPalette;

/** @var bool CGB mode enabled */
private bool $cgbMode = false;

/** @var HdmaController|null HDMA controller for H-Blank DMA transfers (CGB) */
private ?HdmaController $hdmaController = null;

public function __construct(
private readonly Vram $vram,
private readonly Oam $oam,
Expand Down Expand Up @@ -209,6 +217,11 @@ private function setMode(PpuMode $mode): void
// Update STAT register mode bits
$this->stat = ($this->stat & ~self::STAT_MODE_MASK) | $mode->getStatBits();

// Trigger HDMA H-Blank transfer if entering H-Blank mode
if ($mode === PpuMode::HBlank && $this->hdmaController !== null) {
$this->hdmaController->onHBlank();
}

// Trigger STAT interrupt if enabled for this mode
$statInterrupt = match ($mode) {
PpuMode::HBlank => ($this->stat & self::STAT_MODE0_INT) !== 0,
Expand Down Expand Up @@ -241,6 +254,7 @@ private function renderScanline(): void
// Initialize scanline buffer and BG color buffer
$this->scanlineBuffer = array_fill(0, ArrayFramebuffer::WIDTH, Color::fromDmgShade(0));
$this->bgColorBuffer = array_fill(0, ArrayFramebuffer::WIDTH, 0);
$this->bgPriorityBuffer = array_fill(0, ArrayFramebuffer::WIDTH, false);

// Render layers
if (($this->lcdc & self::LCDC_BG_WINDOW_ENABLE) !== 0) {
Expand Down Expand Up @@ -287,6 +301,7 @@ private function renderBackground(): void
$vramBank = ($attributes & 0x08) !== 0 ? 1 : 0; // Bit 3: VRAM bank
$xFlip = ($attributes & 0x20) !== 0; // Bit 5: horizontal flip
$yFlip = ($attributes & 0x40) !== 0; // Bit 6: vertical flip
$bgPriority = ($attributes & 0x80) !== 0; // Bit 7: BG-to-OAM priority

// Apply flips
$finalTileY = $yFlip ? (7 - $tileY) : $tileY;
Expand All @@ -299,8 +314,9 @@ private function renderBackground(): void
// Get pixel color
$color = $this->getTilePixel($vramData, $tileDataAddr, $finalTileX, $finalTileY);

// Store raw color for sprite priority checking
// Store raw color and priority for sprite priority checking
$this->bgColorBuffer[$x] = $color;
$this->bgPriorityBuffer[$x] = $bgPriority;

// Apply palette
if ($this->cgbMode) {
Expand Down Expand Up @@ -346,6 +362,7 @@ private function renderWindow(): void
$vramBank = ($attributes & 0x08) !== 0 ? 1 : 0; // Bit 3: VRAM bank
$xFlip = ($attributes & 0x20) !== 0; // Bit 5: horizontal flip
$yFlip = ($attributes & 0x40) !== 0; // Bit 6: vertical flip
$bgPriority = ($attributes & 0x80) !== 0; // Bit 7: BG-to-OAM priority

// Apply flips
$finalTileY = $yFlip ? (7 - $tileY) : $tileY;
Expand All @@ -357,8 +374,9 @@ private function renderWindow(): void

$color = $this->getTilePixel($vramData, $tileDataAddr, $finalTileX, $finalTileY);

// Store raw color for sprite priority checking
// Store raw color and priority for sprite priority checking
$this->bgColorBuffer[$x] = $color;
$this->bgPriorityBuffer[$x] = $bgPriority;

// Apply palette
if ($this->cgbMode) {
Expand Down Expand Up @@ -466,9 +484,17 @@ private function renderSprite(array $sprite, int $spriteHeight, array $vramData)
}

// Check priority (behind BG)
if ($behindBg && $this->bgColorBuffer[$pixelX] !== 0) {
// If BG pixel is not color 0, sprite is hidden behind BG
continue;
$bgColor = $this->bgColorBuffer[$pixelX];
if ($bgColor !== 0) {
// In CGB mode, check BG priority bit first
if ($this->cgbMode && $this->bgPriorityBuffer[$pixelX]) {
// BG priority bit is set - BG always wins
continue;
}
// Otherwise, use normal sprite priority (behindBg flag)
if ($behindBg) {
continue;
}
}

if ($this->cgbMode) {
Expand Down Expand Up @@ -536,6 +562,14 @@ public function isCgbMode(): bool
return $this->cgbMode;
}

/**
* Set the HDMA controller for H-Blank DMA transfers.
*/
public function setHdmaController(HdmaController $hdmaController): void
{
$this->hdmaController = $hdmaController;
}

// DeviceInterface implementation for I/O registers
public function readByte(int $address): int
{
Expand All @@ -551,10 +585,11 @@ public function readByte(int $address): int
self::OBP1 => $this->obp1,
self::WY => $this->wy,
self::WX => $this->wx,
self::BCPS => $this->colorPalette->readBgIndex(),
self::BCPD => $this->colorPalette->readBgData(),
self::OCPS => $this->colorPalette->readObjIndex(),
self::OCPD => $this->colorPalette->readObjData(),
// CGB color palette registers - only accessible in CGB mode
self::BCPS => $this->cgbMode ? $this->colorPalette->readBgIndex() : 0xFF,
self::BCPD => $this->cgbMode ? $this->colorPalette->readBgData() : 0xFF,
self::OCPS => $this->cgbMode ? $this->colorPalette->readObjIndex() : 0xFF,
self::OCPD => $this->cgbMode ? $this->colorPalette->readObjData() : 0xFF,
default => 0xFF,
};
}
Expand All @@ -573,10 +608,11 @@ public function writeByte(int $address, int $value): void
self::OBP1 => $this->obp1 = $value,
self::WY => $this->wy = $value,
self::WX => $this->wx = $value,
self::BCPS => $this->colorPalette->writeBgIndex($value),
self::BCPD => $this->colorPalette->writeBgData($value),
self::OCPS => $this->colorPalette->writeObjIndex($value),
self::OCPD => $this->colorPalette->writeObjData($value),
// CGB color palette registers - only writable in CGB mode
self::BCPS => $this->cgbMode ? $this->colorPalette->writeBgIndex($value) : null,
self::BCPD => $this->cgbMode ? $this->colorPalette->writeBgData($value) : null,
self::OCPS => $this->cgbMode ? $this->colorPalette->writeObjIndex($value) : null,
self::OCPD => $this->cgbMode ? $this->colorPalette->writeObjData($value) : null,
default => null,
};

Expand Down
Loading