diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README.md | 24 | ||||
-rw-r--r-- | boot.php | 13 | ||||
-rw-r--r-- | roms/.gitignore | 2 | ||||
-rw-r--r-- | src/Canvas/TerminalCanvas.php | 7 | ||||
-rw-r--r-- | src/Core.php | 229 | ||||
-rw-r--r-- | src/LcdController.php | 120 |
7 files changed, 171 insertions, 226 deletions
@@ -2,3 +2,5 @@ material/ vendor/ roms/*rom cache.properties +bin/phpcs +bin/phpcbf
\ No newline at end of file @@ -38,14 +38,34 @@ The following PHP versions are supported: You will need a good terminal! I've tested only on MacOSX and Linux. I'm sorry about that Windows guys :disappointed: +## Installation + +```bash +$ composer g require gabrielrcouto/php-terminal-gameboy-emulator:dev-master +``` + ## Running -Before: Put your ROMs files (.gb or .gbc) on "roms/" folder. +Your roms are loaded from the directory you are running the `php-gameboy` command. + +```bash +$ php-gameboy drmario.gb +$ php-gameboy pokemon.gbc +``` + +If you like to run this emulator locally, simple clone the repository: ```bash +$ git clone https://github.com/gabrielrcouto/php-terminal-gameboy-emulator.git +$ cd php-terminal-gameboy-emulator $ composer install -o -$ bin/php-gameboy drmario.gb +``` + +For running roms, pass the full path to your rom or put then in the `php-terminal-gameboy-emulator` folder: + +```bash $ bin/php-gameboy pokemon.gbc +$ bin/php-gameboy /full/path/to/your/rom/drmario.gb ``` ## Controls @@ -1,6 +1,14 @@ <?php -require_once __DIR__.'/vendor/autoload.php'; + +foreach (['../../autoload.php', '../vendor/autoload.php', 'vendor/autoload.php'] as $autoload) { + $autoload = __DIR__.'/'.$autoload; + if (file_exists($autoload)) { + require $autoload; + break; + } +} +unset($autoload); use GameBoy\Canvas\TerminalCanvas; use GameBoy\Core; @@ -15,7 +23,8 @@ if (count($argv) < 2) { throw new RuntimeException('You need to pass the ROM file name (Ex: drmario.rom)'); } -$filename = 'roms/'.$argv[1]; +$filename = $argv[1]; + if (!file_exists($filename)) { throw new RuntimeException(sprintf('"%s" does not exist', $filename)); } diff --git a/roms/.gitignore b/roms/.gitignore deleted file mode 100644 index 6041394..0000000 --- a/roms/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.gb -*.gbc
\ No newline at end of file diff --git a/src/Canvas/TerminalCanvas.php b/src/Canvas/TerminalCanvas.php index 056dc65..05564ab 100644 --- a/src/Canvas/TerminalCanvas.php +++ b/src/Canvas/TerminalCanvas.php @@ -33,17 +33,22 @@ class TerminalCanvas implements DrawContextInterface $this->canvas->set(0, 0); $this->canvas->set(159, 143); + $y = 0; + for ($i = 0; $i < count($canvasBuffer); $i = $i + 4) { // Sum of all colors, Ignore alpha $total = $canvasBuffer[$i] + $canvasBuffer[$i + 1] + $canvasBuffer[$i + 2]; $x = ($i / 4) % 160; - $y = ceil(($i / 4) / 160); // 350 is a good threshold for black and white if ($total > 350) { $this->canvas->set($x, $y); } + + if ($x == 159) { + ++$y; + } } if ($this->currentSecond != time()) { diff --git a/src/Core.php b/src/Core.php index a182ba1..4ef0d5a 100644 --- a/src/Core.php +++ b/src/Core.php @@ -170,13 +170,8 @@ class Core //Is the emulated LCD controller on? public $LCDisOn = false; - //Array of functions to handle each scan line we do (onscreen + offscreen) - public $LINECONTROL; - - public $DISPLAYOFFCONTROL = []; - - //Pointer to either LINECONTROL or DISPLAYOFFCONTROL. - public $LCDCONTROL = null; + //lcdControllerler object + public $lcdController = null; public $gfxWindowY = false; @@ -382,10 +377,6 @@ class Core $this->drawContext = $drawContext; $this->ROMImage = $ROMImage; - $this->DISPLAYOFFCONTROL[] = function ($parentObj) { - //Array of line 0 function to handle the LCD controller when it's off (Do nothing!). - }; - $this->tileCountInvalidator = $this->tileCount * 4; $this->ROMBanks[0x52] = 72; @@ -411,7 +402,8 @@ class Core $this->TICKTable = TICKTables::$primary; $this->SecondaryTICKTable = TICKTables::$secondary; - $this->LINECONTROL = array_fill(0, 154, null); + //Initialize the LCD Controller + $this->lcdController = new LcdController($this); } public function saveState() @@ -624,7 +616,6 @@ class Core $this->tileCountInvalidator = $this->tileCount * 4; $this->fromSaveState = true; $this->checkPaletteType(); - $this->initializeLCDController(); $this->memoryReadJumpCompile(); $this->memoryWriteJumpCompile(); $this->initLCD(); @@ -634,7 +625,6 @@ class Core public function start() { Settings::$settings[4] = 0; //Reset the frame skip setting. - $this->initializeLCDController(); //Compile the LCD controller functions. $this->initMemory(); //Write the startup memory. $this->ROMLoad(); //Load the ROM into memory and get cartridge information from it. $this->initLCD(); //Initializae the graphics. @@ -1249,7 +1239,7 @@ class Core $timedTicks = $this->CPUTicks / $this->multiplier; // LCD Timing $this->LCDTicks += $timedTicks; //LCD timing - $this->LCDCONTROL[$this->actualScanLine]($this); //Scan Line and STAT Mode Control + $this->lcdController->scanLine($this->actualScanLine); //Scan Line and STAT Mode Control //Audio Timing $this->audioTicks += $timedTicks; //Not the same as the LCD timing (Cannot be altered by display on/off changes!!!). @@ -1287,120 +1277,6 @@ class Core } } - public function initializeLCDController() - { - //Display on hanlding: - $line = 0; - - while ($line < 154) { - if ($line < 143) { - //We're on a normal scan line: - $this->LINECONTROL[$line] = function ($parentObj) { - if ($parentObj->LCDTicks < 20) { - $parentObj->scanLineMode2(); // mode2: 80 cycles - } elseif ($parentObj->LCDTicks < 63) { - $parentObj->scanLineMode3(); // mode3: 172 cycles - } elseif ($parentObj->LCDTicks < 114) { - $parentObj->scanLineMode0(); // mode0: 204 cycles - } else { - //We're on a new scan line: - $parentObj->LCDTicks -= 114; - $parentObj->actualScanLine = ++$parentObj->memory[0xFF44]; - $parentObj->matchLYC(); - if ($parentObj->STATTracker != 2) { - if ($parentObj->hdmaRunning && !$parentObj->halt && $parentObj->LCDisOn) { - $parentObj->performHdma(); //H-Blank DMA - } - if ($parentObj->mode0TriggerSTAT) { - $parentObj->memory[0xFF0F] |= 0x2; // set IF bit 1 - } - } - $parentObj->STATTracker = 0; - $parentObj->scanLineMode2(); // mode2: 80 cycles - if ($parentObj->LCDTicks >= 114) { - //We need to skip 1 or more scan lines: - $parentObj->notifyScanline(); - $parentObj->LCDCONTROL[$parentObj->actualScanLine]($parentObj); //Scan Line and STAT Mode Control - } - } - }; - } elseif ($line == 143) { - //We're on the last visible scan line of the LCD screen: - $this->LINECONTROL[143] = function ($parentObj) { - if ($parentObj->LCDTicks < 20) { - $parentObj->scanLineMode2(); // mode2: 80 cycles - } elseif ($parentObj->LCDTicks < 63) { - $parentObj->scanLineMode3(); // mode3: 172 cycles - } elseif ($parentObj->LCDTicks < 114) { - $parentObj->scanLineMode0(); // mode0: 204 cycles - } else { - //Starting V-Blank: - //Just finished the last visible scan line: - $parentObj->LCDTicks -= 114; - $parentObj->actualScanLine = ++$parentObj->memory[0xFF44]; - $parentObj->matchLYC(); - if ($parentObj->mode1TriggerSTAT) { - $parentObj->memory[0xFF0F] |= 0x2; // set IF bit 1 - } - if ($parentObj->STATTracker != 2) { - if ($parentObj->hdmaRunning && !$parentObj->halt && $parentObj->LCDisOn) { - $parentObj->performHdma(); //H-Blank DMA - } - if ($parentObj->mode0TriggerSTAT) { - $parentObj->memory[0xFF0F] |= 0x2; // set IF bit 1 - } - } - $parentObj->STATTracker = 0; - $parentObj->modeSTAT = 1; - $parentObj->memory[0xFF0F] |= 0x1; // set IF flag 0 - //LCD off takes at least 2 frames. - if ($parentObj->drewBlank > 0) { - --$parentObj->drewBlank; - } - if ($parentObj->LCDTicks >= 114) { - //We need to skip 1 or more scan lines: - $parentObj->LCDCONTROL[$parentObj->actualScanLine]($parentObj); //Scan Line and STAT Mode Control - } - } - }; - } elseif ($line < 153) { - //In VBlank - $this->LINECONTROL[$line] = function ($parentObj) { - if ($parentObj->LCDTicks >= 114) { - //We're on a new scan line: - $parentObj->LCDTicks -= 114; - $parentObj->actualScanLine = ++$parentObj->memory[0xFF44]; - $parentObj->matchLYC(); - if ($parentObj->LCDTicks >= 114) { - //We need to skip 1 or more scan lines: - $parentObj->LCDCONTROL[$parentObj->actualScanLine]($parentObj); //Scan Line and STAT Mode Control - } - } - }; - } else { - //VBlank Ending (We're on the last actual scan line) - $this->LINECONTROL[153] = function ($parentObj) { - if ($parentObj->memory[0xFF44] == 153) { - $parentObj->memory[0xFF44] = 0; //LY register resets to 0 early. - $parentObj->matchLYC(); //LY==LYC Test is early here (Fixes specific one-line glitches (example: Kirby2 intro)). - } - if ($parentObj->LCDTicks >= 114) { - //We reset back to the beginning: - $parentObj->LCDTicks -= 114; - $parentObj->actualScanLine = 0; - $parentObj->scanLineMode2(); // mode2: 80 cycles - if ($parentObj->LCDTicks >= 114) { - //We need to skip 1 or more scan lines: - $parentObj->LCDCONTROL[$parentObj->actualScanLine]($parentObj); //Scan Line and STAT Mode Control - } - } - }; - } - ++$line; - } - $this->LCDCONTROL = ($this->LCDisOn) ? $this->LINECONTROL : $this->DISPLAYOFFCONTROL; - } - public function displayShowOff() { if ($this->drewBlank == 0) { @@ -2326,21 +2202,21 @@ class Core if ($data < 60) { $parentObj->RTCSeconds = $data; } else { - echo '(Bank #' + $parentObj->currMBCRAMBank + ') RTC write out of range: ' + $data.PHP_EOL; + echo '(Bank #'.$parentObj->currMBCRAMBank.') RTC write out of range: '.$data.PHP_EOL; } break; case 0x09: if ($data < 60) { $parentObj->RTCMinutes = $data; } else { - echo '(Bank #' + $parentObj->currMBCRAMBank + ') RTC write out of range: ' + $data.PHP_EOL; + echo '(Bank #'.$parentObj->currMBCRAMBank.') RTC write out of range: '.$data.PHP_EOL; } break; case 0x0A: if ($data < 24) { $parentObj->RTCHours = $data; } else { - echo '(Bank #' + $parentObj->currMBCRAMBank + ') RTC write out of range: ' + $data.PHP_EOL; + echo '(Bank #'.$parentObj->currMBCRAMBank.') RTC write out of range: '.$data.PHP_EOL; } break; case 0x0B: @@ -2352,7 +2228,7 @@ class Core $parentObj->RTCDays = (($data & 0x1) << 8) | ($parentObj->RTCDays & 0xFF); break; default: - echo 'Invalid MBC3 bank address selected: ' + $parentObj->currMBCRAMBank.PHP_EOL; + echo 'Invalid MBC3 bank address selected: '.$parentObj->currMBCRAMBank.PHP_EOL; } } }; @@ -2436,87 +2312,6 @@ class Core $parentObj->TIMAEnabled = ($data & 0x04) == 0x04; $parentObj->TACClocker = pow(4, (($data & 0x3) != 0) ? ($data & 0x3) : 4); //TODO: Find a way to not make a conditional in here... }; - - // BEGIN - Audio Writers - $this->memoryWriter[0xFF10] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF11] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF12] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF13] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF14] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF16] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF17] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF18] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF19] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF1A] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF1B] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF1C] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF1D] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF1E] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF20] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF21] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF22] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF23] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF24] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF25] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF26] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF30] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF31] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF32] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF33] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF34] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF35] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF36] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF37] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF38] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF39] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF3A] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF3B] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF3C] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF3D] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF3E] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF3F] = function ($parentObj, $address, $data) { - }; - $this->memoryWriter[0xFF44] = function ($parentObj, $address, $data) { - //Read only - }; - // END - Audio Writers - // $this->memoryWriter[0xFF45] = function ($parentObj, $address, $data) { $parentObj->memory[0xFF45] = $data; if ($parentObj->LCDisOn) { @@ -2566,9 +2361,7 @@ class Core $parentObj->STATTracker = $parentObj->modeSTAT = $parentObj->LCDTicks = $parentObj->actualScanLine = $parentObj->memory[0xFF44] = 0; if ($parentObj->LCDisOn) { $parentObj->matchLYC(); //Get the compare of the first scan line. - $parentObj->LCDCONTROL = $parentObj->LINECONTROL; } else { - $parentObj->LCDCONTROL = $parentObj->DISPLAYOFFCONTROL; $parentObj->displayShowOff(); } $parentObj->memory[0xFF0F] &= 0xFD; @@ -2697,9 +2490,7 @@ class Core $parentObj->STATTracker = $parentObj->modeSTAT = $parentObj->LCDTicks = $parentObj->actualScanLine = $parentObj->memory[0xFF44] = 0; if ($parentObj->LCDisOn) { $parentObj->matchLYC(); //Get the compare of the first scan line. - $parentObj->LCDCONTROL = $parentObj->LINECONTROL; } else { - $parentObj->LCDCONTROL = $parentObj->DISPLAYOFFCONTROL; $parentObj->displayShowOff(); } $parentObj->memory[0xFF0F] &= 0xFD; @@ -2765,7 +2556,7 @@ class Core $this->memoryWriter[0xFF6C] = function ($parentObj, $address, $data) { if ($parentObj->inBootstrap) { $parentObj->cGBC = ($data == 0x80); - echo 'Booted to GBC Mode: ' + $parentObj->cGBC.PHP_EOL; + echo 'Booted to GBC Mode: '.$parentObj->cGBC.PHP_EOL; } $parentObj->memory[0xFF6C] = $data; }; diff --git a/src/LcdController.php b/src/LcdController.php new file mode 100644 index 0000000..5501465 --- /dev/null +++ b/src/LcdController.php @@ -0,0 +1,120 @@ +<?php + +namespace GameBoy; + +class LcdController +{ + protected $core; + + public function __construct($core) + { + $this->core = $core; + } + + /** + * Scan Line and STAT Mode Control + * @param int $line Memory Scanline + */ + public function scanLine($line) + { + //When turned off = Do nothing! + //@TODO - Move LCDisOn to this class + if ($this->core->LCDisOn) { + if ($line < 143) { + //We're on a normal scan line: + if ($this->core->LCDTicks < 20) { + $this->core->scanLineMode2(); // mode2: 80 cycles + } elseif ($this->core->LCDTicks < 63) { + $this->core->scanLineMode3(); // mode3: 172 cycles + } elseif ($this->core->LCDTicks < 114) { + $this->core->scanLineMode0(); // mode0: 204 cycles + } else { + //We're on a new scan line: + $this->core->LCDTicks -= 114; + $this->core->actualScanLine = ++$this->core->memory[0xFF44]; + $this->core->matchLYC(); + if ($this->core->STATTracker != 2) { + if ($this->core->hdmaRunning && !$this->core->halt && $this->core->LCDisOn) { + $this->core->performHdma(); //H-Blank DMA + } + if ($this->core->mode0TriggerSTAT) { + $this->core->memory[0xFF0F] |= 0x2; // set IF bit 1 + } + } + $this->core->STATTracker = 0; + $this->core->scanLineMode2(); // mode2: 80 cycles + if ($this->core->LCDTicks >= 114) { + //We need to skip 1 or more scan lines: + $this->core->notifyScanline(); + $this->scanLine($this->core->actualScanLine); //Scan Line and STAT Mode Control + } + } + } elseif ($line == 143) { + //We're on the last visible scan line of the LCD screen: + if ($this->core->LCDTicks < 20) { + $this->core->scanLineMode2(); // mode2: 80 cycles + } elseif ($this->core->LCDTicks < 63) { + $this->core->scanLineMode3(); // mode3: 172 cycles + } elseif ($this->core->LCDTicks < 114) { + $this->core->scanLineMode0(); // mode0: 204 cycles + } else { + //Starting V-Blank: + //Just finished the last visible scan line: + $this->core->LCDTicks -= 114; + $this->core->actualScanLine = ++$this->core->memory[0xFF44]; + $this->core->matchLYC(); + if ($this->core->mode1TriggerSTAT) { + $this->core->memory[0xFF0F] |= 0x2; // set IF bit 1 + } + if ($this->core->STATTracker != 2) { + if ($this->core->hdmaRunning && !$this->core->halt && $this->core->LCDisOn) { + $this->core->performHdma(); //H-Blank DMA + } + if ($this->core->mode0TriggerSTAT) { + $this->core->memory[0xFF0F] |= 0x2; // set IF bit 1 + } + } + $this->core->STATTracker = 0; + $this->core->modeSTAT = 1; + $this->core->memory[0xFF0F] |= 0x1; // set IF flag 0 + //LCD off takes at least 2 frames. + if ($this->core->drewBlank > 0) { + --$this->core->drewBlank; + } + if ($this->core->LCDTicks >= 114) { + //We need to skip 1 or more scan lines: + $this->scanLine($this->core->actualScanLine); //Scan Line and STAT Mode Control + } + } + } elseif ($line < 153) { + //In VBlank + if ($this->core->LCDTicks >= 114) { + //We're on a new scan line: + $this->core->LCDTicks -= 114; + $this->core->actualScanLine = ++$this->core->memory[0xFF44]; + $this->core->matchLYC(); + if ($this->core->LCDTicks >= 114) { + //We need to skip 1 or more scan lines: + $this->scanLine($this->core->actualScanLine); //Scan Line and STAT Mode Control + } + } + } else { + //VBlank Ending (We're on the last actual scan line) + if ($this->core->memory[0xFF44] == 153) { + $this->core->memory[0xFF44] = 0; //LY register resets to 0 early. + $this->core->matchLYC(); //LY==LYC Test is early here (Fixes specific one-line glitches (example: Kirby2 intro)). + } + if ($this->core->LCDTicks >= 114) { + //We reset back to the beginning: + $this->core->LCDTicks -= 114; + $this->core->actualScanLine = 0; + $this->core->scanLineMode2(); // mode2: 80 cycles + if ($this->core->LCDTicks >= 114) { + //We need to skip 1 or more scan lines: + $this->scanLine($this->core->actualScanLine); //Scan Line and STAT Mode Control + } + } + } + } + } +} |