summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.md24
-rw-r--r--boot.php13
-rw-r--r--roms/.gitignore2
-rw-r--r--src/Canvas/TerminalCanvas.php7
-rw-r--r--src/Core.php229
-rw-r--r--src/LcdController.php120
7 files changed, 171 insertions, 226 deletions
diff --git a/.gitignore b/.gitignore
index f1da856..d9223cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@ material/
vendor/
roms/*rom
cache.properties
+bin/phpcs
+bin/phpcbf \ No newline at end of file
diff --git a/README.md b/README.md
index 1e18ac1..c82921d 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/boot.php b/boot.php
index b652f0d..4abda2f 100644
--- a/boot.php
+++ b/boot.php
@@ -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
+ }
+ }
+ }
+ }
+ }
+}