Showing preview only (231K chars total). Download the full file or copy to clipboard to get everything.
Repository: juchi/gameboy.js
Branch: master
Commit: e1456e6e4ade
Files: 33
Total size: 220.1 KB
Directory structure:
gitextract_nzcsu177/
├── .gitignore
├── LICENSE
├── README.md
├── dist/
│ └── gameboy.js
├── package.json
├── src/
│ ├── cpu.ts
│ ├── debug.ts
│ ├── display/
│ │ ├── gpu.ts
│ │ └── screen.ts
│ ├── exception.ts
│ ├── ext_ram.ts
│ ├── input/
│ │ ├── gamepad.ts
│ │ ├── input.ts
│ │ └── keyboard.ts
│ ├── instructions.ts
│ ├── main.ts
│ ├── mbc.ts
│ ├── memory.ts
│ ├── opcodes.ts
│ ├── rom/
│ │ ├── ajax_reader.ts
│ │ ├── drop_file_reader.ts
│ │ ├── file_reader.ts
│ │ └── rom.ts
│ ├── serial.ts
│ ├── sound/
│ │ ├── apu.ts
│ │ ├── channel.ts
│ │ ├── channel1.ts
│ │ ├── channel3.ts
│ │ └── channel4.ts
│ ├── timer.ts
│ └── util.ts
├── tsconfig.json
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Julien Chichignoud
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Gameboy.js
==========
Gameboy.js is a Gameboy emulator written in JavaScript.
It's a work in progress, see the Features section.
## Browser support
Tested on:
* Firefox
* Chrome
* Safari
## Usage
### Run now
You can try the emulator directly on the [demo page](http://juchi.github.io/gameboy.js/).
Many tests ROM can be downloaded [here](https://github.com/c-sp/game-boy-test-roms) and are runnable, but some tests fail.
See the Tests section for more details.
Currently, most tested games run (tested with Tetris, Super Mario, Pokémon Red)
but have some glitches.
### Run on a custom page
You can directly use the distributed compiled file in a custom HTML page of your own,
and create a new Gameboy object. It will expect a Canvas element and an optional options object.
```javascript
var canvas = document.getElementById('canvas');
new GameboyJS.Gameboy(canvas);
```
### Options
You can customize the configuration by passing a list of options to the Gameboy.
```javascript
var options, canvas;
//...
new GameboyJS.Gameboy(canvas, options);
```
* `pad`: Object representing the pad to use as a physical gamepad. The `class` key is mandatory and
should contain the class implementing the device you want to play with.
You can implement any kind of pad as long as this class implements the init() method.
See GameboyJS.Keyboard class for an example of implementation. You may also provide
a `mapping` object that will be used if you choose the GameboyJS.Gamepad class.
Default is `{class: GameboyJS.Keyboard, mapping: null}`
* `zoom`: The zoom level as an integer. Default is 1
* `romReaders`: An array of ROM reader objects that can read a ROM file
and send the data to the Gameboy.
Default is empty (`[]`), leading to a GameboyJS.RomFileReader to be created.
* `statusContainerId`: ID of the HTML element for status display. Default is 'status'.
* `gameNameContainerId`: ID of the HTML element for game name display. Default is 'game-name'.
* `errorContainerId`: ID of the HTML element for error display. Default is 'error'.
### Build from source
If you want to build the compiled JavaScript file from source
to be sure you have the latest updates, just clone the repository
and run the build script:
```
git clone https://github.com/juchi/gameboy.js
npm install
npm run build
```
## Features
### Devices
The LCD screen is working but still has some sprite glitches.
User input is available : arrow keys are mapped to the keyboard arrows,
and A, B, START and SELECT are mapped to G, B, H, N respectively.
Also, it's possible to use a gamepad using a custom `pad` option.
Game saves are working fine and are stored as serialized data in the LocalStorage.
Sound is partially implemented (the noise channel is not done yet) and is quite
good on Firefox (latest release) but really bad on Chrome and Safari.
This seems to be due to the implementation of the Web Audio API in webkit.
The serial port can be used by the program as an output,
the received bytes are displayed in the console (this is mainly used for tests).
The ROM files are accessed using an explorer on your computer.
Other methods may be included (AJAX and Drag & Drop are supported).
### Internal processes
There is no boot program provided (nor supported).
The execution starts automatically at address 0x0100 which is the start address of all ROMs.
All the standard Gameboy instructions are implemented. Super Gameboy and Gameboy Color are not supported.
The following features are in progress or partially working:
* sprites (some glitches)
* MBC (only MBC 1, MBC 3 and MBC 5 are partially supported)
* sound (ok on Firefox, poor on Chrome)
The following features are not currently supported at all:
* boot program
## Tests
The tests perform as follow :
| Test | status |
|--------------------|:--------:|
| CPU instructions | pass |
| instruction timing | pass |
| memory timing | fail |
| DMG sound | fail |
| OAM bug | fail |
================================================
FILE: dist/gameboy.js
================================================
function loadboot(p) {
var boot = [
0x31, 0xFE, 0xFF, 0xAF, 0x21, 0xFF, 0x9F, 0x32, 0xCB, 0x7C, 0x20, 0xFB, 0x21, 0x26, 0xFF, 0x0E,
0x11, 0x3E, 0x80, 0x32, 0xE2, 0x0C, 0x3E, 0xF3, 0xE2, 0x32, 0x3E, 0x77, 0x77, 0x3E, 0xFC, 0xE0,
0x47, 0x11, 0x04, 0x01, 0x21, 0x10, 0x80, 0x1A, 0xCD, 0x95, 0x00, 0xCD, 0x96, 0x00, 0x13, 0x7B,
0xFE, 0x34, 0x20, 0xF3, 0x11, 0xD8, 0x00, 0x06, 0x08, 0x1A, 0x13, 0x22, 0x23, 0x05, 0x20, 0xF9,
0x3E, 0x19, 0xEA, 0x10, 0x99, 0x21, 0x2F, 0x99, 0x0E, 0x0C, 0x3D, 0x28, 0x08, 0x32, 0x0D, 0x20,
0xF9, 0x2E, 0x0F, 0x18, 0xF3, 0x67, 0x3E, 0x64, 0x57, 0xE0, 0x42, 0x3E, 0x91, 0xE0, 0x40, 0x04,
0x1E, 0x02, 0x0E, 0x0C, 0xF0, 0x44, 0xFE, 0x90, 0x20, 0xFA, 0x0D, 0x20, 0xF7, 0x1D, 0x20, 0xF2,
0x0E, 0x13, 0x24, 0x7C, 0x1E, 0x83, 0xFE, 0x62, 0x28, 0x06, 0x1E, 0xC1, 0xFE, 0x64, 0x20, 0x06,
0x7B, 0xE2, 0x0C, 0x3E, 0x87, 0xE2, 0xF0, 0x42, 0x90, 0xE0, 0x42, 0x15, 0x20, 0xD2, 0x05, 0x20,
0x4F, 0x16, 0x20, 0x18, 0xCB, 0x4F, 0x06, 0x04, 0xC5, 0xCB, 0x11, 0x17, 0xC1, 0xCB, 0x11, 0x17,
0x05, 0x20, 0xF5, 0x22, 0x23, 0x22, 0x23, 0xC9, 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B,
0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E,
0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC,
0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, 0x3C, 0x42, 0xB9, 0xA5, 0xB9, 0xA5, 0x42, 0x3C,
0x21, 0x04, 0x01, 0x11, 0xA8, 0x00, 0x1A, 0x13, 0xBE, 0x00, 0x00, 0x23, 0x7D, 0xFE, 0x34, 0x20,
0xF5, 0x06, 0x19, 0x78, 0x86, 0x23, 0x05, 0x20, 0xFB, 0x86, 0x00, 0x00, 0x3E, 0x01, 0xE0, 0x50
];
for (var i in boot) {
p.memory[i] = boot[i];
}
p.r.pc = 0;
p.usingBootRom = true;
}
var GameboyJS;
(function (GameboyJS) {
"use strict";
// CPU class
var CPU = function(gameboy) {
this.gameboy = gameboy;
this.r = {A:0, F: 0, B:0, C:0, D:0, E:0, H:0, L:0, pc:0, sp:0};
this.IME = true;
this.clock = {c: 0, serial: 0};
this.isHalted = false;
this.isPaused = false;
this.usingBootRom = false;
this.createDevices();
};
CPU.INTERRUPTS = {
VBLANK: 0,
LCDC: 1,
TIMER: 2,
SERIAL: 3,
HILO: 4
};
CPU.interruptRoutines = {
0: function(p){GameboyJS.cpuOps.RSTn(p, 0x40);},
1: function(p){GameboyJS.cpuOps.RSTn(p, 0x48);},
2: function(p){GameboyJS.cpuOps.RSTn(p, 0x50);},
3: function(p){GameboyJS.cpuOps.RSTn(p, 0x58);},
4: function(p){GameboyJS.cpuOps.RSTn(p, 0x60);}
};
CPU.prototype.createDevices = function() {
this.memory = new GameboyJS.Memory(this);
this.timer = new GameboyJS.Timer(this, this.memory);
this.apu = new GameboyJS.APU(this.memory);
this.SERIAL_INTERNAL_INSTR = 512; // instr to wait per bit if internal clock
this.enableSerial = 0;
this.serialHandler = GameboyJS.ConsoleSerial;
};
CPU.prototype.reset = function() {
this.memory.reset();
this.r.sp = 0xFFFE;
};
CPU.prototype.loadRom = function(data) {
this.memory.setRomData(data);
};
CPU.prototype.getRamSize = function() {
var size = 0;
switch (this.memory.rb(0x149)) {
case 1:
size = 2048;
break;
case 2:
size = 2048 * 4;
break;
case 3:
size = 2048 * 16;
break;
}
return size;
};
CPU.prototype.getGameName = function() {
var name = '';
for (var i = 0x134; i < 0x143; i++) {
var char = this.memory.rb(i) || 32;
name += String.fromCharCode(char);
}
return name;
};
// Start the execution of the emulator
CPU.prototype.run = function() {
if (this.usingBootRom) {
this.r.pc = 0x0000;
} else {
this.r.pc = 0x0100;
}
this.frame();
};
CPU.prototype.stop = function() {
clearTimeout(this.nextFrameTimer);
};
// Fetch-and-execute loop
// Will execute instructions for the duration of a frame
//
// The screen unit will notify the vblank period which
// is considered the end of a frame
//
// The function is called on a regular basis with a timeout
CPU.prototype.frame = function() {
if (!this.isPaused) {
this.nextFrameTimer = setTimeout(this.frame.bind(this), 1000 / GameboyJS.Screen.physics.FREQUENCY);
}
try {
var vblank = false;
while (!vblank) {
var oldInstrCount = this.clock.c;
if (!this.isHalted) {
var opcode = this.fetchOpcode();
GameboyJS.opcodeMap[opcode](this);
this.r.F &= 0xF0; // tmp fix
if (this.enableSerial) {
var instr = this.clock.c - oldInstrCount;
this.clock.serial += instr;
if (this.clock.serial >= 8 * this.SERIAL_INTERNAL_INSTR) {
this.endSerialTransfer();
}
}
} else {
this.clock.c += 4;
}
var elapsed = this.clock.c - oldInstrCount;
vblank = this.gpu.update(elapsed);
this.timer.update(elapsed);
this.input.update();
this.apu.update(elapsed);
this.checkInterrupt();
}
this.clock.c = 0;
} catch (e) {
this.gameboy.handleException(e);
}
};
CPU.prototype.fetchOpcode = function() {
var opcode = this.memory.rb(this.r.pc++);
if (opcode === undefined) {console.log(opcode + ' at ' + (this.r.pc-1).toString(16));this.stop();return;}
if (!GameboyJS.opcodeMap[opcode]) {
console.error('Unknown opcode '+opcode.toString(16)+' at address '+(this.r.pc-1).toString(16)+', stopping execution...');
this.stop();
return null;
}
return opcode;
};
// read register
CPU.prototype.rr = function(register) {
return this.r[register];
};
// write register
CPU.prototype.wr = function(register, value) {
this.r[register] = value;
};
CPU.prototype.halt = function() {
this.isHalted = true;
};
CPU.prototype.unhalt = function() {
this.isHalted = false;
};
CPU.prototype.pause = function() {
this.isPaused = true;
};
CPU.prototype.unpause = function() {
if (this.isPaused) {
this.isPaused = false;
this.frame();
}
};
// Look for interrupt flags
CPU.prototype.checkInterrupt = function() {
if (!this.IME) {
return;
}
for (var i = 0; i < 5; i++) {
var IFval = this.memory.rb(0xFF0F);
if (GameboyJS.Util.readBit(IFval, i) && this.isInterruptEnable(i)) {
IFval &= (0xFF - (1<<i));
this.memory.wb(0xFF0F, IFval);
this.disableInterrupts();
this.clock.c += 4; // 20 clocks to serve interrupt, with 16 for RSTn
CPU.interruptRoutines[i](this);
break;
}
}
};
// Set an interrupt flag
CPU.prototype.requestInterrupt = function(type) {
var IFval = this.memory.rb(0xFF0F);
IFval |= (1 << type)
this.memory.wb(0xFF0F, IFval) ;
this.unhalt();
};
CPU.prototype.isInterruptEnable = function(type) {
return GameboyJS.Util.readBit(this.memory.rb(0xFFFF), type) != 0;
};
CPU.prototype.enableInterrupts = function() {
this.IME = true;
};
CPU.prototype.disableInterrupts = function() {
this.IME = false;
};
CPU.prototype.enableSerialTransfer = function() {
this.enableSerial = 1;
this.clock.serial = 0;
};
CPU.prototype.endSerialTransfer = function() {
this.enableSerial = 0;
var data = this.memory.rb(0xFF01);
this.memory.wb(0xFF02, 0);
this.serialHandler.out(data);
this.memory.wb(0xFF01, this.serialHandler.in());
};
CPU.prototype.resetDivTimer = function() {
this.timer.resetDiv();
};
GameboyJS.CPU = CPU;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
var Debug = {};
// Output a range of 16 memory addresses
Debug.view_memory = function(addr, gameboy) {
var memory = gameboy.cpu.memory;
addr = addr & 0xFFF0;
var pad = '00';
var str = addr.toString(16) + ':';
for (var i = addr; i < addr + 0x10; i++) {
if ((i & 0x1) == 0) {
str += ' ';
}
var val = memory[i] || 0;
val = val.toString(16);
str += pad.substring(val.length) + val;
}
return str;
};
Debug.view_tile = function(gameboy, index, dataStart) {
var memory = gameboy.cpu.memory;
var screen = gameboy.screen;
var LCDC = screen.deviceram(screen.LCDC);
if (typeof dataStart === 'undefined') {
dataStart = 0x8000;
if (!GameboyJS.Util.readBit(LCDC, 4)) {
dataStart = 0x8800;
index = GameboyJS.cpuOps._getSignedValue(index) + 128;
}
}
var tileData = screen.readTileData(index, dataStart);
var pixelData = new Array(8 * 8)
for (var line = 0; line < 8; line++) {
var b1 = tileData.shift();
var b2 = tileData.shift();
for (var pixel = 0; pixel < 8; pixel++) {
var mask = (1 << (7-pixel));
var colorValue = ((b1 & mask) >> (7-pixel)) + ((b2 & mask) >> (7-pixel))*2;
pixelData[line * 8 + pixel] = colorValue;
}
}
var i = 0;
while (pixelData.length) {
console.log(i++ + ' ' + pixelData.splice(0, 8).join(''));
}
};
Debug.list_visible_sprites = function(gameboy) {
var memory = gameboy.cpu.memory;
var indexes = new Array();
for (var i = 0xFE00; i < 0xFE9F; i += 4) {
var x = memory.oamram(i + 1);
var y = memory.oamram(i);
var tileIndex = memory.oamram(i + 2);
if (x == 0 || x >= 168) {
continue;
}
indexes.push({oamIndex:i, x:x, y:y, tileIndex:tileIndex});
}
return indexes;
};
GameboyJS.Debug = Debug;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
var Screen;
var GPU = function(screen, cpu) {
this.cpu = cpu;
this.screen = screen;
this.LCDC= 0xFF40;
this.STAT= 0xFF41;
this.SCY = 0xFF42;
this.SCX = 0xFF43;
this.LY = 0xFF44;
this.LYC = 0xFF45;
this.BGP = 0xFF47;
this.OBP0= 0xFF48;
this.OBP1= 0xFF49;
this.WY = 0xFF4A;
this.WX = 0xFF4B;
this.vram = cpu.memory.vram.bind(cpu.memory);
this.OAM_START = 0xFE00;
this.OAM_END = 0xFE9F;
this.deviceram = cpu.memory.deviceram.bind(cpu.memory);
this.oamram = cpu.memory.oamram.bind(cpu.memory);
this.VBLANK_TIME = 70224;
this.clock = 0;
this.mode = 2;
this.line = 0;
Screen = GameboyJS.Screen;
this.buffer = new Array(Screen.physics.WIDTH * Screen.physics.HEIGHT);
this.tileBuffer = new Array(8);
this.bgTileCache = {};
};
GPU.tilemap = {
HEIGHT: 32,
WIDTH: 32,
START_0: 0x9800,
START_1: 0x9C00,
LENGTH: 0x0400 // 1024 bytes = 32*32
};
GPU.prototype.update = function(clockElapsed) {
this.clock += clockElapsed;
var vblank = false;
switch (this.mode) {
case 0: // HBLANK
if (this.clock >= 204) {
this.clock -= 204;
this.line++;
this.updateLY();
if (this.line == 144) {
this.setMode(1);
vblank = true;
this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.VBLANK);
this.drawFrame();
} else {
this.setMode(2);
}
}
break;
case 1: // VBLANK
if (this.clock >= 456) {
this.clock -= 456;
this.line++;
if (this.line > 153) {
this.line = 0;
this.setMode(2);
}
this.updateLY();
}
break;
case 2: // SCANLINE OAM
if (this.clock >= 80) {
this.clock -= 80;
this.setMode(3);
}
break;
case 3: // SCANLINE VRAM
if (this.clock >= 172) {
this.clock -= 172;
this.drawScanLine(this.line);
this.setMode(0);
}
break;
}
return vblank;
};
GPU.prototype.updateLY = function() {
this.deviceram(this.LY, this.line);
var STAT = this.deviceram(this.STAT);
if (this.deviceram(this.LY) == this.deviceram(this.LYC)) {
this.deviceram(this.STAT, STAT | (1 << 2));
if (STAT & (1 << 6)) {
this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.LCDC);
}
} else {
this.deviceram(this.STAT, STAT & (0xFF - (1 << 2)));
}
};
GPU.prototype.setMode = function(mode) {
this.mode = mode;
var newSTAT = this.deviceram(this.STAT);
newSTAT &= 0xFC;
newSTAT |= mode;
this.deviceram(this.STAT, newSTAT);
if (mode < 3) {
if (newSTAT & (1 << (3+mode))) {
this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.LCDC);
}
}
};
// Push one scanline into the main buffer
GPU.prototype.drawScanLine = function(line) {
var LCDC = this.deviceram(this.LCDC);
var enable = GameboyJS.Util.readBit(LCDC, 7);
if (enable) {
var lineBuffer = new Array(Screen.physics.WIDTH);
this.drawBackground(LCDC, line, lineBuffer);
this.drawSprites(LCDC, line, lineBuffer);
// TODO draw a line for the window here too
}
};
GPU.prototype.drawFrame = function() {
var LCDC = this.deviceram(this.LCDC);
var enable = GameboyJS.Util.readBit(LCDC, 7);
if (enable) {
//this.drawSprites(LCDC);
this.drawWindow(LCDC);
}
this.bgTileCache = {};
this.screen.render(this.buffer);
};
GPU.prototype.drawBackground = function(LCDC, line, lineBuffer) {
if (!GameboyJS.Util.readBit(LCDC, 0)) {
return;
}
var mapStart = GameboyJS.Util.readBit(LCDC, 3) ? GPU.tilemap.START_1 : GPU.tilemap.START_0;
var dataStart, signedIndex = false;
if (GameboyJS.Util.readBit(LCDC, 4)) {
dataStart = 0x8000;
} else {
dataStart = 0x8800;
signedIndex = true;
}
var bgx = this.deviceram(this.SCX);
var bgy = this.deviceram(this.SCY);
var tileLine = ((line + bgy) & 7);
// browse BG tilemap for the line to render
var tileRow = ((((bgy + line) / 8) | 0) & 0x1F);
var firstTile = ((bgx / 8) | 0) + 32 * tileRow;
var lastTile = firstTile + Screen.physics.WIDTH / 8 + 1;
if ((lastTile & 0x1F) < (firstTile & 0x1F)) {
lastTile -= 32;
}
var x = (firstTile & 0x1F) * 8 - bgx; // x position of the first tile's leftmost pixel
for (var i = firstTile; i != lastTile; i++, (i & 0x1F) == 0 ? i-=32 : null) {
var tileIndex = this.vram(i + mapStart);
if (signedIndex) {
tileIndex = GameboyJS.Util.getSignedValue(tileIndex) + 128;
}
// try to retrieve the tile data from the cache, or use readTileData() to read from ram
// TODO find a better cache system now that the BG is rendered line by line
var tileData = this.bgTileCache[tileIndex] || (this.bgTileCache[tileIndex] = this.readTileData(tileIndex, dataStart));
this.drawTileLine(tileData, tileLine);
this.copyBGTileLine(lineBuffer, this.tileBuffer, x);
x += 8;
}
this.copyLineToBuffer(lineBuffer, line);
};
// Copy a tile line from a tileBuffer to a line buffer, at a given x position
GPU.prototype.copyBGTileLine = function(lineBuffer, tileBuffer, x) {
// copy tile line to buffer
for (var k = 0; k < 8; k++, x++) {
if (x < 0 || x >= Screen.physics.WIDTH) continue;
lineBuffer[x] = tileBuffer[k];
}
};
// Copy a scanline into the main buffer
GPU.prototype.copyLineToBuffer = function(lineBuffer, line) {
var bgPalette = GPU.getPalette(this.deviceram(this.BGP));
for (var x = 0; x < Screen.physics.WIDTH; x++) {
var color = lineBuffer[x];
this.drawPixel(x, line, bgPalette[color]);
}
};
// Write a line of a tile (8 pixels) into a buffer array
GPU.prototype.drawTileLine = function(tileData, line, xflip, yflip) {
xflip = xflip | 0;
yflip = yflip | 0;
var l = yflip ? 7 - line : line;
var byteIndex = l * 2;
var b1 = tileData[byteIndex++];
var b2 = tileData[byteIndex++];
var offset = 8;
for (var pixel = 0; pixel < 8; pixel++) {
offset--;
var mask = (1 << offset);
var colorValue = ((b1 & mask) >> offset) + ((b2 & mask) >> offset)*2;
var p = xflip ? offset : pixel;
this.tileBuffer[p] = colorValue;
}
};
GPU.prototype.drawSprites = function(LCDC, line, lineBuffer) {
if (!GameboyJS.Util.readBit(LCDC, 1)) {
return;
}
var spriteHeight = GameboyJS.Util.readBit(LCDC, 2) ? 16 : 8;
var sprites = new Array();
for (var i = this.OAM_START; i < this.OAM_END && sprites.length < 10; i += 4) {
var y = this.oamram(i);
var x = this.oamram(i+1);
var index = this.oamram(i+2);
var flags = this.oamram(i+3);
if (y - 16 > line || y - 16 < line - spriteHeight) {
continue;
}
sprites.push({x:x, y:y, index:index, flags:flags})
}
if (sprites.length == 0) return;
// cache object to store read tiles from this frame
var cacheTile = {};
var spriteLineBuffer = new Array(Screen.physics.WIDTH);
for (var i = 0; i < sprites.length; i++) {
var sprite = sprites[i];
var tileLine = line - sprite.y + 16;
var paletteNumber = GameboyJS.Util.readBit(flags, 4);
var xflip = GameboyJS.Util.readBit(sprite.flags, 5);
var yflip = GameboyJS.Util.readBit(sprite.flags, 6);
var tileData = cacheTile[sprite.index] || (cacheTile[sprite.index] = this.readTileData(sprite.index, 0x8000, spriteHeight * 2));
this.drawTileLine(tileData, tileLine, xflip, yflip);
this.copySpriteTileLine(spriteLineBuffer, this.tileBuffer, sprite.x - 8, paletteNumber);
}
this.copySpriteLineToBuffer(spriteLineBuffer, line);
};
// Copy a tile line from a tileBuffer to a line buffer, at a given x position
GPU.prototype.copySpriteTileLine = function(lineBuffer, tileBuffer, x, palette) {
// copy tile line to buffer
for (var k = 0; k < 8; k++, x++) {
if (x < 0 || x >= Screen.physics.WIDTH || tileBuffer[k] == 0) continue;
lineBuffer[x] = {color:tileBuffer[k], palette: palette};
}
};
// Copy a sprite scanline into the main buffer
GPU.prototype.copySpriteLineToBuffer = function(spriteLineBuffer, line) {
var spritePalettes = {};
spritePalettes[0] = GPU.getPalette(this.deviceram(this.OBP0));
spritePalettes[1] = GPU.getPalette(this.deviceram(this.OBP1));
for (var x = 0; x < Screen.physics.WIDTH; x++) {
if (!spriteLineBuffer[x]) continue;
var color = spriteLineBuffer[x].color;
if (color === 0) continue;
var paletteNumber = spriteLineBuffer[x].palette;
this.drawPixel(x, line, spritePalettes[paletteNumber][color]);
}
};
GPU.prototype.drawTile = function(tileData, x, y, buffer, bufferWidth, xflip, yflip, spriteMode) {
xflip = xflip | 0;
yflip = yflip | 0;
spriteMode = spriteMode | 0;
var byteIndex = 0;
for (var line = 0; line < 8; line++) {
var l = yflip ? 7 - line : line;
var b1 = tileData[byteIndex++];
var b2 = tileData[byteIndex++];
for (var pixel = 0; pixel < 8; pixel++) {
var mask = (1 << (7-pixel));
var colorValue = ((b1 & mask) >> (7-pixel)) + ((b2 & mask) >> (7-pixel))*2;
if (spriteMode && colorValue == 0) continue;
var p = xflip ? 7 - pixel : pixel;
var bufferIndex = (x + p) + (y + l) * bufferWidth;
buffer[bufferIndex] = colorValue;
}
}
};
// get an array of tile bytes data (16 entries for 8*8px)
GPU.prototype.readTileData = function(tileIndex, dataStart, tileSize) {
tileSize = tileSize || 0x10; // 16 bytes / tile by default (8*8 px)
var tileData = new Array();
var tileAddressStart = dataStart + (tileIndex * 0x10);
for (var i = tileAddressStart; i < tileAddressStart + tileSize; i++) {
tileData.push(this.vram(i));
}
return tileData;
};
GPU.prototype.drawWindow = function(LCDC) {
if (!GameboyJS.Util.readBit(LCDC, 5)) {
return;
}
var buffer = new Array(256*256);
var mapStart = GameboyJS.Util.readBit(LCDC, 6) ? GPU.tilemap.START_1 : GPU.tilemap.START_0;
var dataStart, signedIndex = false;
if (GameboyJS.Util.readBit(LCDC, 4)) {
dataStart = 0x8000;
} else {
dataStart = 0x8800;
signedIndex = true;
}
// browse Window tilemap
for (var i = 0; i < GPU.tilemap.LENGTH; i++) {
var tileIndex = this.vram(i + mapStart);
if (signedIndex) {
tileIndex = GameboyJS.Util.getSignedValue(tileIndex) + 128;
}
var tileData = this.readTileData(tileIndex, dataStart);
var x = i % GPU.tilemap.WIDTH;
var y = (i / GPU.tilemap.WIDTH) | 0;
this.drawTile(tileData, x * 8, y * 8, buffer, 256);
}
var wx = this.deviceram(this.WX) - 7;
var wy = this.deviceram(this.WY);
for (var x = Math.max(0, -wx); x < Math.min(Screen.physics.WIDTH, Screen.physics.WIDTH - wx); x++) {
for (var y = Math.max(0, -wy); y < Math.min(Screen.physics.HEIGHT, Screen.physics.HEIGHT - wy); y++) {
var color = buffer[(x & 255) + (y & 255) * 256];
this.drawPixel(x + wx, y + wy, color);
}
}
};
GPU.prototype.drawPixel = function(x, y, color) {
this.buffer[y * 160 + x] = color;
};
GPU.prototype.getPixel = function(x, y) {
return this.buffer[y * 160 + x];
};
// Get the palette mapping from a given palette byte as stored in memory
// A palette will map a tile color to a final palette color index
// used with Screen.colors to get a shade of grey
GPU.getPalette = function(paletteByte) {
var palette = [];
for (var i = 0; i < 8; i += 2) {
var shade = (paletteByte & (3 << i)) >> i;
palette.push(shade);
}
return palette;
};
GameboyJS.GPU = GPU;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// Screen device
var Screen = function(canvas, pixelSize) {
this.context = canvas.getContext('2d');
this.canvas = canvas;
this.pixelSize = pixelSize || 1;
this.initImageData();
};
Screen.colors = [
0xFF,
0xAA,
0x55,
0x00
];
Screen.physics = {
WIDTH : 160,
HEIGHT : 144,
FREQUENCY: 60
};
Screen.prototype.setPixelSize = function(pixelSize) {
this.pixelSize = pixelSize;
this.initImageData();
};
Screen.prototype.initImageData = function() {
this.canvas.width = Screen.physics.WIDTH * this.pixelSize;
this.canvas.height = Screen.physics.HEIGHT * this.pixelSize;
this.imageData = this.context.createImageData(this.canvas.width, this.canvas.height);
};
Screen.prototype.clearScreen = function() {
this.context.fillStyle = '#FFF';
this.context.fillRect(0, 0, Screen.physics.WIDTH * this.pixelSize, Screen.physics.HEIGHT * this.pixelSize);
};
Screen.prototype.fillImageData = function(buffer) {
for (var y = 0; y < Screen.physics.HEIGHT; y++) {
for (var py = 0; py < this.pixelSize; py++) {
var _y = y * this.pixelSize + py;
for (var x = 0; x < Screen.physics.WIDTH; x++) {
for (var px = 0; px < this.pixelSize; px++) {
var offset = _y * this.canvas.width + (x * this.pixelSize + px);
var v = Screen.colors[buffer[y * Screen.physics.WIDTH + x]];
this.imageData.data[offset * 4] = v;
this.imageData.data[offset * 4 + 1] = v;
this.imageData.data[offset * 4 + 2] = v;
this.imageData.data[offset * 4 + 3] = 255;
}
}
}
}
};
Screen.prototype.render = function(buffer) {
this.fillImageData(buffer);
this.context.putImageData(this.imageData, 0, 0);
};
GameboyJS.Screen = Screen;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// This exception should be thrown whenever a critical feature that
// has not been implemented is requested
function UnimplementedException(message, fatal) {
this.message = message;
this.name = UnimplementedException;
if (fatal === undefined) {
fatal = true;
}
this.fatal = fatal;
}
GameboyJS.UnimplementedException = UnimplementedException;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// Object for mapping the cartridge RAM
var ExtRam = function() {
this.extRam = null;
this.ramSize = 0;
this.ramBank = 0;
};
ExtRam.prototype.loadRam = function(game, size) {
this.gameName = game;
this.ramSize = size;
this.ramBanksize = this.ramSize >= 0x2000 ? 8192 : 2048;
var key = this.getStorageKey();
var data = localStorage.getItem(key);
if (data == null) {
this.extRam = Array.apply(null, new Array(this.ramSize)).map(function(){return 0;});
} else {
this.extRam = JSON.parse(data);
if (this.extRam.length != size) {
console.error('Found RAM data but not matching expected size.');
}
}
};
ExtRam.prototype.setRamBank = function(bank) {
this.ramBank = bank;
};
ExtRam.prototype.manageWrite = function(offset, value) {
this.extRam[this.ramBank * 8192 + offset] = value;
};
ExtRam.prototype.manageRead = function(offset) {
return this.extRam[this.ramBank * 8192 + offset];
};
ExtRam.prototype.getStorageKey = function() {
return this.gameName + '_EXTRAM';;
};
// Actually save the RAM in the physical storage (localStorage)
ExtRam.prototype.saveRamData = function() {
localStorage.setItem(this.getStorageKey(), JSON.stringify(this.extRam));
};
GameboyJS.ExtRam = ExtRam;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// This is the default buttons mapping for the Gamepad
// It's optimized for the XBOX pad
//
// Any other mapping can be provided as a constructor argument of the Gamepad object
// An alternative mapping should be an object with keys being the indexes
// of the gamepad buttons and values the normalized gameboy button names
var xboxMapping = {
0: 'UP',
1: 'DOWN',
2: 'LEFT',
3: 'RIGHT',
4: 'START',
5: 'SELECT',
11: 'A',
12: 'B'
};
// Gamepad listener
// Communication layer between the Gamepad API and the Input class
// Any physical controller can be used but the mapping should be provided
// in order to get an optimal layout of the buttons (see above)
var Gamepad = function(mapping) {
this.gamepad = null;
this.state = {A:0,B:0,START:0,SELECT:0,LEFT:0,RIGHT:0,UP:0,DOWN:0};
this.pullInterval = null;
this.buttonMapping = mapping || xboxMapping;
};
// Initialize the keyboard listeners and set up the callbacks
// for button press / release
Gamepad.prototype.init = function(onPress, onRelease) {
this.onPress = onPress;
this.onRelease = onRelease;
var self = this;
window.addEventListener('gamepadconnected', function(e) {
self.gamepad = e.gamepad;
self.activatePull();
});
window.addEventListener('gamepaddisconnected', function(e) {
self.gamepad = null;
self.deactivatePull();
});
};
Gamepad.prototype.activatePull = function() {
this.deactivatePull();
this.pullInterval = setInterval(this.pullState.bind(this), 100);
};
Gamepad.prototype.deactivatePull = function() {
clearInterval(this.pullInterval);
};
// Check the state of the current gamepad in order to detect any press/release action
Gamepad.prototype.pullState = function() {
for (var index in this.buttonMapping) {
var button = this.buttonMapping[index];
var oldState = this.state[button];
this.state[button] = this.gamepad.buttons[index].pressed;
if (this.state[button] == 1 && oldState == 0) {
this.managePress(button);
} else if (this.state[button] == 0 && oldState == 1) {
this.manageRelease(button);
}
}
};
Gamepad.prototype.managePress = function(key) {
this.onPress(key);
};
Gamepad.prototype.manageRelease = function(key) {
this.onRelease(key);
};
GameboyJS.Gamepad = Gamepad;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// The Input management system
//
// The pressKey() and releaseKey() functions should be called by a device class
// like GameboyJS.Keyboard after a physical button trigger event
//
// They rely on the name of the original buttons as parameters (see Input.keys)
var Input = function(cpu, pad) {
this.cpu = cpu;
this.memory = cpu.memory;
this.P1 = 0xFF00;
this.state = 0;
pad.init(this.pressKey.bind(this), this.releaseKey.bind(this));
};
Input.keys = {
START: 0x80,
SELECT: 0x40,
B: 0x20,
A: 0x10,
DOWN: 0x08,
UP: 0x04,
LEFT: 0x02,
RIGHT: 0x01
};
Input.prototype.pressKey = function(key) {
this.state |= Input.keys[key];
this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.HILO);
};
Input.prototype.releaseKey = function(key) {
var mask = 0xFF - Input.keys[key];
this.state &= mask;
};
Input.prototype.update = function() {
var value = this.memory.rb(this.P1);
value = ((~value) & 0x30); // invert the value so 1 means 'active'
if (value & 0x10) { // direction keys listened
value |= (this.state & 0x0F);
} else if (value & 0x20) { // action keys listened
value |= ((this.state & 0xF0) >> 4);
} else if ((value & 0x30) == 0) { // no keys listened
value &= 0xF0;
}
value = ((~value) & 0x3F); // invert back
this.memory[this.P1] = value;
};
GameboyJS.Input = Input;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// Keyboard listener
// Does the mapping between the keyboard and the Input class
var Keyboard = function() {};
// Initialize the keyboard listeners and set up the callbacks
// for button press / release
Keyboard.prototype.init = function(onPress, onRelease) {
this.onPress = onPress;
this.onRelease = onRelease;
var self = this;
document.addEventListener('keydown', function(e) {
self.managePress(e.keyCode);
});
document.addEventListener('keyup', function(e) {
self.manageRelease(e.keyCode);
});
}
Keyboard.prototype.managePress = function(keycode) {
var key = this.translateKey(keycode);
if (key) {
this.onPress(key);
}
};
Keyboard.prototype.manageRelease = function(keycode) {
var key = this.translateKey(keycode);
if (key) {
this.onRelease(key);
}
};
// Transform a keyboard keycode into a key of the Input.keys object
Keyboard.prototype.translateKey = function(keycode) {
var key = null;
switch (keycode) {
case 71: // G
key = 'A';
break;
case 66: // B
key = 'B';
break;
case 72: // H
key = 'START';
break;
case 78: // N
key = 'SELECT';
break;
case 37: // left
key = 'LEFT';
break;
case 38: // up
key = 'UP';
break;
case 39: // right
key = 'RIGHT';
break;
case 40: // down
key = 'DOWN';
break;
}
return key;
};
GameboyJS.Keyboard = Keyboard;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// List of CPU operations
// Most operations have been factorized here to limit code redundancy
//
// How to read operations:
// Uppercase letters qualify the kind of operation (LD = LOAD, INC = INCREMENT, etc.)
// Lowercase letters are used to hint parameters :
// r = register, n = 1 memory byte, sp = sp register,
// a = suffix for memory address, i = bit index
// Example : LDrrar = LOAD operation with two-registers memory address
// as first parameter and one register value as second
//
// Underscore-prefixed functions are here to delegate the logic between similar operations,
// they should not be called from outside
//
// It's up to each operation to update the CPU clock
var ops = {
LDrrnn: function(p, r1, r2) {p.wr(r2, p.memory.rb(p.r.pc));p.wr(r1, p.memory.rb(p.r.pc+1)); p.r.pc+=2;p.clock.c += 12;},
LDrrar: function(p, r1, r2, r3) {ops._LDav(p, GameboyJS.Util.getRegAddr(p, r1, r2), p.r[r3]);p.clock.c += 8;},
LDrrra: function(p, r1, r2, r3) {p.wr(r1, p.memory.rb(GameboyJS.Util.getRegAddr(p, r2, r3)));p.clock.c += 8;},
LDrn: function(p, r1) {p.wr(r1, p.memory.rb(p.r.pc++));p.clock.c += 8;},
LDrr: function(p, r1, r2) {p.wr(r1, p.r[r2]);p.clock.c += 4;},
LDrar: function(p, r1, r2) {p.memory.wb(p.r[r1]+0xFF00, p.r[r2]);p.clock.c += 8;},
LDrra: function(p, r1, r2) {p.wr(r1, p.memory.rb(p.r[r2]+0xFF00));p.clock.c += 8;},
LDspnn: function(p) {p.wr('sp', (p.memory.rb(p.r.pc + 1) << 8) + p.memory.rb(p.r.pc));p.r.pc+=2;p.clock.c += 12;},
LDsprr: function(p, r1, r2) {p.wr('sp', GameboyJS.Util.getRegAddr(p, r1, r2));p.clock.c += 8;},
LDnnar: function(p, r1) {var addr=(p.memory.rb(p.r.pc + 1) << 8) + p.memory.rb(p.r.pc);p.memory.wb(addr,p.r[r1]);p.r.pc+=2; p.clock.c += 16;},
LDrnna: function(p, r1) {var addr=(p.memory.rb(p.r.pc + 1) << 8) + p.memory.rb(p.r.pc);p.wr(r1, p.memory.rb(addr));p.r.pc+=2; p.clock.c += 16;},
LDrrspn:function(p, r1, r2) {var rel = p.memory.rb(p.r.pc++);rel=GameboyJS.Util.getSignedValue(rel);var val=p.r.sp + rel;
var c = (p.r.sp&0xFF) + (rel&0xFF) > 0xFF;var h = (p.r.sp & 0xF) + (rel & 0xF) > 0xF;val &= 0xFFFF;
var f = 0; if(h)f|=0x20;if(c)f|=0x10;p.wr('F', f);
p.wr(r1, val >> 8);p.wr(r2, val&0xFF);
p.clock.c+=12;},
LDnnsp: function(p) {var addr = p.memory.rb(p.r.pc++) + (p.memory.rb(p.r.pc++)<<8); ops._LDav(p, addr, p.r.sp & 0xFF);ops._LDav(p, addr+1, p.r.sp >> 8);p.clock.c+=20;},
LDrran: function(p, r1, r2){var addr = GameboyJS.Util.getRegAddr(p, r1, r2);ops._LDav(p, addr, p.memory.rb(p.r.pc++));p.clock.c+=12;},
_LDav: function(p, addr, val){p.memory.wb(addr, val);},
LDHnar: function(p, r1){p.memory.wb(0xFF00 + p.memory.rb(p.r.pc++), p.r[r1]);p.clock.c+=12;},
LDHrna: function(p, r1){p.wr(r1, p.memory.rb(0xFF00 + p.memory.rb(p.r.pc++)));p.clock.c+=12;},
INCrr: function(p, r1, r2) {p.wr(r2, (p.r[r2]+1)&0xFF); if (p.r[r2] == 0) p.wr(r1, (p.r[r1]+1)&0xFF);p.clock.c += 8;},
INCrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var val = (p.memory.rb(addr)+1)&0xFF;var z = val==0;var h=(p.memory.rb(addr)&0xF)+1 > 0xF;
p.memory.wb(addr, val);
p.r.F&=0x10;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80;
p.clock.c+=12;},
INCsp: function(p){p.wr('sp', p.r.sp+1); p.r.sp &= 0xFFFF; p.clock.c+=8;},
INCr: function(p, r1) {var h = ((p.r[r1]&0xF) + 1)&0x10;p.wr(r1, (p.r[r1] + 1)&0xFF);var z = p.r[r1]==0;
p.r.F&=0x10;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80;
p.clock.c += 4;},
DECrr: function(p, r1, r2) {p.wr(r2, (p.r[r2] - 1) & 0xFF); if (p.r[r2] == 0xFF) p.wr(r1, (p.r[r1] - 1)&0xFF);p.clock.c += 8;},
DECsp: function(p){p.wr('sp', p.r.sp-1); p.r.sp &= 0xFFFF; p.clock.c+=8;},
DECr: function(p, r1) {var h = (p.r[r1]&0xF) < 1;p.wr(r1, (p.r[r1] - 1) & 0xFF);var z = p.r[r1]==0;
p.r.F&=0x10;p.r.F|=0x40;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80;
p.clock.c += 4;},
DECrra: function(p, r1, r2){var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var val = (p.memory.rb(addr)-1)&0xFF;var z = val==0;var h=(p.memory.rb(addr)&0xF) < 1;
p.memory.wb(addr, val);
p.r.F&=0x10;p.r.F|=0x40;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80;
p.clock.c+=12;},
ADDrr: function(p, r1, r2) {var n = p.r[r2];ops._ADDrn(p, r1, n); p.clock.c += 4;},
ADDrn: function(p, r1) {var n = p.memory.rb(p.r.pc++);ops._ADDrn(p, r1, n); p.clock.c+=8;},
_ADDrn: function(p, r1, n) {var h=((p.r[r1]&0xF)+(n&0xF))&0x10;p.wr(r1, p.r[r1]+n);var c=p.r[r1]&0x100;p.r[r1]&=0xFF;
var f = 0;if (p.r[r1]==0)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.wr('F', f);},
ADDrrrr:function(p, r1, r2, r3, r4) {ops._ADDrrn(p, r1, r2, (p.r[r3]<<8) + p.r[r4]); p.clock.c+=8;},
ADDrrsp:function(p, r1, r2) {ops._ADDrrn(p, r1, r2, p.r.sp); p.clock.c += 8;},
ADDspn: function(p) {var v = p.memory.rb(p.r.pc++);v = GameboyJS.Util.getSignedValue(v);
var c = ((p.r.sp&0xFF) + (v&0xFF)) > 0xFF; var h = (p.r.sp & 0xF) + (v&0xF) > 0xF;
var f = 0; if(h)f|=0x20;if(c)f|=0x10;p.wr('F', f);
p.wr('sp', (p.r.sp + v) & 0xFFFF);
p.clock.c+=16;},
_ADDrrn:function(p, r1, r2, n) {var v1 = (p.r[r1]<<8) + p.r[r2];var v2 = n;
var res = v1 + v2;var c = res&0x10000;var h = ((v1&0xFFF) + (v2&0xFFF))&0x1000;var z = p.r.F&0x80;
res&=0xFFFF;p.r[r2]=res&0xFF;res=res>>8;p.r[r1]=res&0xFF;
var f=0;if(z)f|=0x80;if(h)f|=0x20;if(c)f|=0x10;p.r.F=f;},
ADCrr: function(p, r1, r2) {var n = p.r[r2]; ops._ADCrn(p, r1, n); p.clock.c += 4;},
ADCrn: function(p, r1) {var n = p.memory.rb(p.r.pc++); ops._ADCrn(p, r1, n); p.clock.c += 8;},
_ADCrn: function(p, r1, n) {
var c = p.r.F&0x10?1:0;var h=((p.r[r1]&0xF)+(n&0xF)+c)&0x10;
p.wr(r1, p.r[r1]+n+c);c=p.r[r1]&0x100;p.r[r1]&=0xFF;
var f = 0;if (p.r[r1]==0)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.r.F=f;},
ADCrrra:function(p, r1, r2, r3) {var n = p.memory.rb(GameboyJS.Util.getRegAddr(p, r2, r3)); ops._ADCrn(p, r1, n); p.clock.c += 8;},
ADDrrra:function(p, r1, r2, r3) {var v = p.memory.rb(GameboyJS.Util.getRegAddr(p, r2, r3));var h=((p.r[r1]&0xF)+(v&0xF))&0x10;p.wr(r1, p.r[r1]+v);var c=p.r[r1]&0x100;p.r[r1]&=0xFF;
var f = 0;if (p.r[r1]==0)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.wr('F', f);
p.clock.c += 8;},
SUBr: function(p, r1) {var n = p.r[r1];ops._SUBn(p, n);p.clock.c += 4;},
SUBn: function(p) {var n = p.memory.rb(p.r.pc++);ops._SUBn(p, n);p.clock.c += 8;},
SUBrra: function(p, r1, r2) {var n = p.memory.rb(GameboyJS.Util.getRegAddr(p, r1, r2));ops._SUBn(p, n);p.clock.c+=8;},
_SUBn: function(p, n) {var c = p.r.A < n;var h = (p.r.A&0xF) < (n&0xF);
p.wr('A', p.r.A - n);p.r.A&=0xFF; var z = p.r.A==0;
var f = 0x40;if (z)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.wr('F', f);},
SBCn: function(p) {var n = p.memory.rb(p.r.pc++); ops._SBCn(p, n); p.clock.c += 8;},
SBCr: function(p, r1) {var n = p.r[r1]; ops._SBCn(p, n); p.clock.c += 4;},
SBCrra: function(p, r1, r2) {var v = p.memory.rb((p.r[r1] << 8) + p.r[r2]); ops._SBCn(p, v); p.clock.c += 8;},
_SBCn: function(p, n) {var carry = p.r.F&0x10 ? 1 : 0;
var c = p.r.A < n + carry;var h = (p.r.A&0xF) < (n&0xF) + carry;
p.wr('A', p.r.A - n - carry); p.r.A&=0xFF; var z = p.r.A == 0;
var f = 0x40;if (z)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.r.F=f;},
ORr: function(p, r1) {p.r.A|=p.r[r1];p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 4;},
ORn: function(p) {p.r.A|=p.memory.rb(p.r.pc++);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;},
ORrra: function(p, r1, r2) {p.r.A|=p.memory.rb((p.r[r1] << 8)+ p.r[r2]);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;},
ANDr: function(p, r1) {p.r.A&=p.r[r1];p.r.F=(p.r.A==0)?0xA0:0x20;p.clock.c += 4;},
ANDn: function(p) {p.r.A&=p.memory.rb(p.r.pc++);p.r.F=(p.r.A==0)?0xA0:0x20;p.clock.c += 8;},
ANDrra: function(p, r1, r2) {p.r.A&=p.memory.rb(GameboyJS.Util.getRegAddr(p, r1, r2));p.r.F=(p.r.A==0)?0xA0:0x20;p.clock.c += 8;},
XORr: function(p, r1) {p.r.A^=p.r[r1];p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 4;},
XORn: function(p) {p.r.A^=p.memory.rb(p.r.pc++);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;},
XORrra: function(p, r1, r2) {p.r.A^=p.memory.rb((p.r[r1] << 8)+ p.r[r2]);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;},
CPr: function(p, r1) {var n = p.r[r1];ops._CPn(p, n); p.clock.c += 4;},
CPn: function(p) {var n =p.memory.rb(p.r.pc++);ops._CPn(p, n);p.clock.c+=8;},
CPrra: function(p, r1, r2) {var n = p.memory.rb(GameboyJS.Util.getRegAddr(p, r1, r2));ops._CPn(p, n);p.clock.c+=8;},
_CPn: function(p, n) {
var c = p.r.A < n;var z = p.r.A == n;var h = (p.r.A&0xF) < (n&0xF);
var f = 0x40;if(z)f+=0x80;if (h)f+=0x20;if (c)f+=0x10;p.r.F=f;},
RRCr: function(p, r1) {p.r.F=0;var out=p.r[r1] & 0x01;if(out)p.r.F|=0x10;p.r[r1]=(p.r[r1]>>1)|(out*0x80);if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
RRCrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F=0;var out=p.memory.rb(addr)&0x01;if(out)p.r.F|=0x10;p.memory.wb(addr, (p.memory.rb(addr)>>1)|(out*0x80));if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
RLCr: function(p, r1) {p.r.F=0;var out=p.r[r1]&0x80?1:0;if(out)p.r.F|=0x10;p.r[r1]=((p.r[r1]<<1)+out)&0xFF;if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
RLCrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F=0;var out=p.memory.rb(addr)&0x80?1:0;if(out)p.r.F|=0x10;p.memory.wb(addr, ((p.memory.rb(addr)<<1)+out)&0xFF);if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
RLr: function(p, r1) {var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.r[r1]&0x80;out?p.r.F|=0x10:p.r.F&=0xEF;p.r[r1]=((p.r[r1]<<1)+c)&0xFF;if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
RLrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.memory.rb(addr)&0x80;out?p.r.F|=0x10:p.r.F&=0xEF;p.memory.wb(addr,((p.memory.rb(addr)<<1)+c)&0xFF);if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
RRr: function(p, r1) {var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.r[r1]&0x01;out?p.r.F|=0x10:p.r.F&=0xEF;p.r[r1]=(p.r[r1]>>1)|(c*0x80);if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
RRrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.memory.rb(addr)&0x01;out?p.r.F|=0x10:p.r.F&=0xEF;p.memory.wb(addr,(p.memory.rb(addr)>>1)|(c*0x80));if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
SRAr: function(p, r1) {p.r.F = 0;if (p.r[r1]&0x01)p.r.F|=0x10;var msb=p.r[r1]&0x80;p.r[r1]=(p.r[r1]>>1)|msb;if (p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
SRArra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F = 0;if (p.memory.rb(addr)&0x01)p.r.F|=0x10;var msb=p.memory.rb(addr)&0x80;p.memory.wb(addr, (p.memory.rb(addr)>>1)|msb);if (p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
SLAr: function(p, r1) {p.r.F = 0;if (p.r[r1]&0x80)p.r.F|=0x10;p.r[r1]=(p.r[r1]<<1)&0xFF;if (p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
SLArra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F = 0;if (p.memory.rb(addr)&0x80)p.r.F|=0x10;p.memory.wb(addr, (p.memory.rb(addr)<<1)&0xFF);if (p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
SRLr: function(p, r1) {p.r.F = 0;if (p.r[r1]&0x01)p.r.F|=0x10;p.r[r1]=p.r[r1]>>1;if (p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
SRLrra: function(p, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);p.r.F = 0;if (p.memory.rb(addr)&0x01)p.r.F|=0x10;p.memory.wb(addr, p.memory.rb(addr)>>1);if (p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
BITir: function(p, i, r1) {var mask=1<<i;var z=(p.r[r1]&mask)?0:1;var f=p.r.F&0x10;f |= 0x20;if(z)f|=0x80;p.r.F=f;p.clock.c+=4;},
BITirra:function(p, i, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var mask=1<<i;var z=(p.memory.rb(addr)&mask)?0:1;var f=p.r.F&0x10;f |= 0x20;if(z)f|=0x80;p.r.F=f;p.clock.c+=8;},
SETir: function(p, i, r1) {var mask=1<<i;p.r[r1]|=mask;p.clock.c += 4;},
SETirra:function(p, i, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var mask=1<<i;p.memory.wb(addr, p.memory.rb(addr)|mask);p.clock.c += 12;},
RESir: function(p, i, r1) {var mask=0xFF - (1<<i);p.r[r1]&=mask;p.clock.c += 4;},
RESirra:function(p, i, r1, r2) {var addr = GameboyJS.Util.getRegAddr(p, r1, r2);var mask=0xFF - (1<<i);p.memory.wb(addr, p.memory.rb(addr)&mask);p.clock.c += 12;},
SWAPr: function(p, r1) {p.r[r1] = ops._SWAPn(p, p.r[r1]);p.clock.c+=4;},
SWAPrra:function(p, r1, r2){var addr = (p.r[r1] << 8)+ p.r[r2]; p.memory.wb(addr, ops._SWAPn(p, p.memory.rb(addr))); p.clock.c+=12;},
_SWAPn: function(p, n){p.r.F = n==0?0x80:0;return ((n&0xF0) >> 4) | ((n&0x0F) << 4);},
JPnn: function(p) {p.wr('pc', (p.memory.rb(p.r.pc+1) << 8) + p.memory.rb(p.r.pc));p.clock.c += 16;},
JRccn: function(p, cc) {if (GameboyJS.Util.testFlag(p, cc)){var v=p.memory.rb(p.r.pc++);v=GameboyJS.Util.getSignedValue(v);p.r.pc += v;p.clock.c+=4;}else{p.r.pc++;}p.clock.c += 8;},
JPccnn: function(p, cc) {if (GameboyJS.Util.testFlag(p, cc)){p.wr('pc', (p.memory.rb(p.r.pc+1) << 8) + p.memory.rb(p.r.pc));p.clock.c+=4;}else{p.r.pc+=2;}p.clock.c += 12;},
JPrr: function(p, r1, r2) {p.r.pc = (p.r[r1] << 8) + p.r[r2];p.clock.c += 4;},
JRn: function(p) {var v=p.memory.rb(p.r.pc++);v=GameboyJS.Util.getSignedValue(v);p.r.pc += v;p.clock.c += 12;},
PUSHrr: function(p, r1, r2) {p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp, p.r[r1]);p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp, p.r[r2]);p.clock.c+=16;},
POPrr: function(p, r1, r2) {p.wr(r2, p.memory.rb(p.r.sp));p.wr('sp', p.r.sp+1);p.wr(r1, p.memory.rb(p.r.sp));p.wr('sp', p.r.sp+1);p.clock.c+=12;},
RSTn: function(p, n) {p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp,p.r.pc>>8);p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp,p.r.pc&0xFF);p.r.pc=n;p.clock.c+=16;},
RET: function(p) {p.r.pc = p.memory.rb(p.r.sp);p.wr('sp', p.r.sp+1);p.r.pc+=p.memory.rb(p.r.sp)<<8;p.wr('sp', p.r.sp+1);p.clock.c += 16;},
RETcc: function(p, cc) {if (GameboyJS.Util.testFlag(p, cc)){p.r.pc = p.memory.rb(p.r.sp);p.wr('sp', p.r.sp+1);p.r.pc+=p.memory.rb(p.r.sp)<<8;p.wr('sp', p.r.sp+1);p.clock.c+=12;}p.clock.c+=8;},
CALLnn: function(p) {ops._CALLnn(p); p.clock.c+=24;},
CALLccnn:function(p, cc) {if (GameboyJS.Util.testFlag(p, cc)){ops._CALLnn(p);p.clock.c+=12;}else{p.r.pc+=2;}p.clock.c+=12; },
_CALLnn:function(p){p.wr('sp', p.r.sp - 1); p.memory.wb(p.r.sp, ((p.r.pc+2)&0xFF00)>>8);
p.wr('sp', p.r.sp - 1); p.memory.wb(p.r.sp, (p.r.pc+2)&0x00FF);
var j=p.memory.rb(p.r.pc)+(p.memory.rb(p.r.pc+1)<<8);p.r.pc=j;},
CPL: function(p) {p.wr('A', (~p.r.A)&0xFF);p.r.F|=0x60,p.clock.c += 4;},
CCF: function(p) {p.r.F&=0x9F;p.r.F&0x10?p.r.F&=0xE0:p.r.F|=0x10;p.clock.c += 4;},
SCF: function(p) {p.r.F&=0x9F;p.r.F|=0x10;p.clock.c+=4;},
DAA: function(p) {
var sub = (p.r.F&0x40) ? 1 : 0; var h = (p.r.F&0x20)?1:0;var c = (p.r.F&0x10)?1:0;
if (sub) {
if (h) {
p.r.A = (p.r.A - 0x6) & 0xFF;
}
if (c) {
p.r.A -= 0x60;
}
} else {
if ((p.r.A&0xF) > 9 || h) {
p.r.A += 0x6;
}
if (p.r.A > 0x9F || c) {
p.r.A += 0x60;
}
}
if (p.r.A&0x100) c = 1;
p.r.A &= 0xFF;
p.r.F &= 0x40;if (p.r.A == 0) p.r.F|=0x80;if (c) p.r.F|=0x10;
p.clock.c += 4;
},
HALT: function(p) {p.halt(); p.clock.c+=4;},
DI: function(p) {p.disableInterrupts();p.clock.c += 4;},
EI: function(p) {p.enableInterrupts();p.clock.c += 4;},
RETI: function(p) {p.enableInterrupts();ops.RET(p);},
CB: function(p) {var opcode = p.memory.rb(p.r.pc++);
GameboyJS.opcodeCbmap[opcode](p);
p.clock.c+=4;}
};
GameboyJS.cpuOps = ops;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
var defaultOptions = {
pad: {class: GameboyJS.Keyboard, mapping: null},
zoom: 1,
romReaders: [],
statusContainerId: 'status',
gameNameContainerId: 'game-name',
errorContainerId: 'error'
};
// Gameboy class
//
// This object is the entry point of the application
// Will delegate user actions to the emulated devices
// and provide information where needed
var Gameboy = function(canvas, options) {
options = options || {};
this.options = GameboyJS.Util.extend({}, defaultOptions, options);
var cpu = new GameboyJS.CPU(this);
var screen = new GameboyJS.Screen(canvas, this.options.zoom);
var gpu = new GameboyJS.GPU(screen, cpu);
cpu.gpu = gpu;
var pad = new this.options.pad.class(this.options.pad.mapping);
var input = new GameboyJS.Input(cpu, pad);
cpu.input = input;
this.cpu = cpu;
this.screen = screen;
this.input = input;
this.pad = pad;
this.createRom(this.options.romReaders);
this.statusContainer = document.getElementById(this.options.statusContainerId) || document.createElement('div');
this.gameNameContainer = document.getElementById(this.options.gameNameContainerId) || document.createElement('div');
this.errorContainer = document.getElementById(this.options.errorContainerId) || document.createElement('div');
};
// Create the ROM object and bind one or more readers
Gameboy.prototype.createRom = function (readers) {
var rom = new GameboyJS.Rom(this);
if (readers.length == 0) {
// add the default rom reader
var romReader = new GameboyJS.RomFileReader();
rom.addReader(romReader);
} else {
for (var i in readers) {
if (readers.hasOwnProperty(i)) {
rom.addReader(readers[i]);
}
}
}
};
Gameboy.prototype.startRom = function(rom) {
this.errorContainer.classList.add('hide');
this.cpu.reset();
try {
this.cpu.loadRom(rom.data);
this.setStatus('Game Running :');
this.setGameName(this.cpu.getGameName());
this.cpu.run();
} catch (e) {
this.handleException(e);
}
};
Gameboy.prototype.pause = function(value) {
if (value) {
this.setStatus('Game Paused :');
this.cpu.pause();
} else {
this.setStatus('Game Running :');
this.cpu.unpause();
}
};
Gameboy.prototype.error = function(message) {
this.setStatus('Error during execution');
this.setError('An error occurred during execution:' + message);
this.cpu.stop();
};
Gameboy.prototype.setStatus = function(status) {
this.statusContainer.innerHTML = status;
};
// Display an error message
Gameboy.prototype.setError = function(message) {
this.errorContainer.classList.remove('hide');
this.errorContainer.innerHTML = message;
};
// Display the name of the game running
Gameboy.prototype.setGameName = function(name) {
this.gameNameContainer.innerHTML = name;
};
Gameboy.prototype.setSoundEnabled = function(value) {
if (value) {
this.cpu.apu.connect();
} else {
this.cpu.apu.disconnect();
}
};
Gameboy.prototype.setScreenZoom = function(value) {
this.screen.setPixelSize(value);
};
Gameboy.prototype.handleException = function(e) {
if (e instanceof GameboyJS.UnimplementedException) {
if (e.fatal) {
this.error('This cartridge is not supported ('+ e.message +')');
} else {
console.error(e.message);
}
} else {
throw e;
}
};
GameboyJS.Gameboy = Gameboy;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// Memory bank controllers
var MBC = {};
// Create an MBC instance depending on the type specified in the cartridge
MBC.getMbcInstance = function(memory, type) {
var instance;
switch (type) {
case 0x00:
instance = new MBC0(memory);
break;
case 0x01: case 0x02: case 0x03:
instance = new MBC1(memory);
break;
case 0x0F: case 0x10: case 0x11: case 0x12: case 0x13:
instance = new MBC3(memory);
break;
case 0x19: case 0x1A: case 0x1B: case 0x1C: case 0x1D: case 0x1E:
instance = new MBC5(memory);
break;
default:
throw new GameboyJS.UnimplementedException('MBC type not supported');
}
return instance;
};
var MBC1 = function(memory) {
this.memory = memory;
this.romBankNumber = 1;
this.mode = 0; // mode 0 = ROM, mode 1 = RAM
this.ramEnabled = true;
this.extRam = new GameboyJS.ExtRam();
};
MBC1.prototype.loadRam = function(game, size) {
this.extRam.loadRam(game, size);
};
MBC1.prototype.manageWrite = function(addr, value) {
switch (addr & 0xF000) {
case 0x0000: case 0x1000: // enable RAM
this.ramEnabled = (value & 0x0A) ? true : false;
if (this.ramEnabled) {
this.extRam.saveRamData();
}
break;
case 0x2000: case 0x3000: // ROM bank number lower 5 bits
value &= 0x1F;
if (value == 0) value = 1;
var mask = this.mode ? 0 : 0xE0;
this.romBankNumber = (this.romBankNumber & mask) +value;
this.memory.loadRomBank(this.romBankNumber);
break;
case 0x4000: case 0x5000: // RAM bank or high bits ROM
value &= 0x03;
if (this.mode == 0) { // ROM upper bits
this.romBankNumber = (this.romBankNumber&0x1F) | (value << 5);
this.memory.loadRomBank(this.romBankNumber);
} else { // RAM bank
this.extRam.setRamBank(value);
}
break;
case 0x6000: case 0x7000: // ROM / RAM mode
this.mode = value & 1;
break;
case 0xA000: case 0xB000:
this.extRam.manageWrite(addr - 0xA000, value);
break;
}
};
MBC1.prototype.readRam = function(addr) {
return this.extRam.manageRead(addr - 0xA000);
};
var MBC3 = function(memory) {
this.memory = memory;
this.romBankNumber = 1;
this.ramEnabled = true;
this.extRam = new GameboyJS.ExtRam();
};
MBC3.prototype.loadRam = function(game, size) {
this.extRam.loadRam(game, size);
};
MBC3.prototype.manageWrite = function(addr, value) {
switch (addr & 0xF000) {
case 0x0000: case 0x1000: // enable RAM
this.ramEnabled = (value & 0x0A) ? true : false;
if (this.ramEnabled) {
this.extRam.saveRamData();
}
break;
case 0x2000: case 0x3000: // ROM bank number
value &= 0x7F;
if (value == 0) value = 1;
this.romBankNumber = value;
this.memory.loadRomBank(this.romBankNumber);
break;
case 0x4000: case 0x5000: // RAM bank
this.extRam.setRamBank(value);
break;
case 0x6000: case 0x7000: // Latch clock data
throw new GameboyJS.UnimplementedException('cartridge clock not supported', false);
break;
case 0xA000: case 0xB000:
this.extRam.manageWrite(addr - 0xA000, value);
break;
}
};
MBC3.prototype.readRam = function(addr) {
return this.extRam.manageRead(addr - 0xA000);
};
// declare MBC5 for compatibility with most cartriges
// does not support rumble feature
var MBC5 = MBC3;
// MBC0 exists for consistency and manages the no-MBC cartriges
var MBC0 = function(memory) {this.memory = memory;};
MBC0.prototype.manageWrite = function(addr, value) {
this.memory.loadRomBank(value);
};
MBC0.prototype.readRam = function(addr) {return 0;};
MBC0.prototype.loadRam = function() {};
GameboyJS.MBC = MBC;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// Memory unit
var Memory = function(cpu) {
this.MEM_SIZE = 65536; // 64KB
this.MBCtype = 0;
this.banksize = 0x4000;
this.rom = null;
this.mbc = null;
this.cpu = cpu;
};
Memory.addresses = {
VRAM_START : 0x8000,
VRAM_END : 0x9FFF,
EXTRAM_START : 0xA000,
EXTRAM_END : 0xBFFF,
OAM_START : 0xFE00,
OAM_END : 0xFE9F,
DEVICE_START: 0xFF00,
DEVICE_END: 0xFF7F
};
// Memory can be accessed as an Array
Memory.prototype = new Array();
Memory.prototype.reset = function() {
this.length = this.MEM_SIZE;
for (var i = Memory.addresses.VRAM_START; i <= Memory.addresses.VRAM_END; i++) {
this[i] = 0;
}
for (var i = Memory.addresses.DEVICE_START; i <= Memory.addresses.DEVICE_END; i++) {
this[i] = 0;
}
this[0xFFFF] = 0;
};
Memory.prototype.setRomData = function(data) {
this.rom = data;
this.loadRomBank(0);
this.mbc = GameboyJS.MBC.getMbcInstance(this, this[0x147]);
this.loadRomBank(1);
this.mbc.loadRam(this.cpu.getGameName(), this.cpu.getRamSize());
};
Memory.prototype.loadRomBank = function(index) {
var start = index ? 0x4000 : 0x0;
var romStart = index * 0x4000;
for (var i = 0; i < this.banksize; i++) {
this[i + start] = this.rom[romStart + i];
}
};
// Video ram accessor
Memory.prototype.vram = function(address) {
if (address < Memory.addresses.VRAM_START || address > Memory.addresses.VRAM_END) {
throw 'VRAM access in out of bounds address ' + address;
}
return this[address];
};
// OAM ram accessor
Memory.prototype.oamram = function(address) {
if (address < Memory.addresses.OAM_START || address > Memory.addresses.OAM_END) {
throw 'OAMRAM access in out of bounds address ' + address;
}
return this[address];
};
// Device ram accessor
Memory.prototype.deviceram = function(address, value) {
if (address < Memory.addresses.DEVICERAM_START || address > Memory.addresses.DEVICERAM_END) {
throw 'Device RAM access in out of bounds address ' + address;
}
if (typeof value === "undefined") {
return this[address];
} else {
this[address] = value;
}
};
// Memory read proxy function
// Used to centralize memory read access
Memory.prototype.rb = function (addr) {
if (addr >= 0xFF10 && addr < 0xFF40) {
var mask = apuMask[addr - 0xFF10];
return this[addr] | mask;
}
if ((addr >= 0xA000 && addr < 0xC000)) {
return this.mbc.readRam(addr);
}
return this[addr];
};
// Bitmasks for audio addresses reads
var apuMask = [
0x80,0x3F,0x00,0xFF,0xBF, // NR10-NR15
0xFF,0x3F,0x00,0xFF,0xBF, // NR20-NR25
0x7F,0xFF,0x9F,0xFF,0xBF, // NR30-NR35
0xFF,0xFF,0x00,0x00,0xBF, // NR40-NR45
0x00,0x00,0x70, // NR50-NR52
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Wave RAM
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
];
// Memory write proxy function
// Used to centralize memory writes and delegate specific behaviour
// to the correct units
Memory.prototype.wb = function(addr, value) {
if (addr < 0x8000 || (addr >= 0xA000 && addr < 0xC000)) { // MBC
this.mbc.manageWrite(addr, value);
} else if (addr >= 0xFF10 && addr <= 0xFF3F) { // sound registers
this.cpu.apu.manageWrite(addr, value);
} else if (addr == 0xFF00) { // input register
this[addr] = ((this[addr] & 0x0F) | (value & 0x30));
} else {
this[addr] = value;
if ((addr & 0xFF00) == 0xFF00) {
if (addr == 0xFF02) {
if (value & 0x80) {
this.cpu.enableSerialTransfer();
}
}
if (addr == 0xFF04) {
this.cpu.resetDivTimer();
}
if (addr == 0xFF46) { // OAM DMA transfer
this.dmaTransfer(value);
}
}
}
}
// Start a DMA transfer (OAM data from cartrige to RAM)
Memory.prototype.dmaTransfer = function(startAddressPrefix) {
var startAddress = (startAddressPrefix << 8);
for (var i = 0; i < 0xA0; i++) {
this[Memory.addresses.OAM_START + i] = this[startAddress + i];
}
};
GameboyJS.Memory = Memory;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
var ops = GameboyJS.cpuOps;
// Each opcode (0 to 0xFF) is associated to a CPU operation
// CPU operations are implemented separately
// The cbmap object holds operations for CB prefixed opcodes (0xCB00 to 0xCBFF)
// Non existent opcodes are commented out and marked empty
var map = {
0x00: function(p){p.clock.c += 4;},
0x01: function(p){ops.LDrrnn(p, 'B', 'C');},
0x02: function(p){ops.LDrrar(p, 'B', 'C', 'A');},
0x03: function(p){ops.INCrr(p, 'B', 'C');},
0x04: function(p){ops.INCr(p, 'B');},
0x05: function(p){ops.DECr(p, 'B');},
0x06: function(p){ops.LDrn(p, 'B');},
0x07: function(p){var out=p.r.A & 0x80?1:0; out ? p.r.F=0x10:p.r.F=0; p.wr('A', ((p.r.A<<1)+out)&0xFF);p.clock.c+=4;},
0x08: function(p){ops.LDnnsp(p);},
0x09: function(p){ops.ADDrrrr(p, 'H', 'L', 'B', 'C');},
0x0A: function(p){ops.LDrrra(p, 'A', 'B', 'C');},
0x0B: function(p){ops.DECrr(p, 'B', 'C');},
0x0C: function(p){ops.INCr(p, 'C');},
0x0D: function(p){ops.DECr(p, 'C');},
0x0E: function(p){ops.LDrn(p, 'C');},
0x0F: function(p){var out=p.r.A & 0x01; out ? p.r.F=0x10:p.r.F=0; p.wr('A', (p.r.A>>1)|(out*0x80));p.clock.c+=4;},
0x10: function(p){p.r.pc++;p.clock.c+=4;},
0x11: function(p){ops.LDrrnn(p, 'D', 'E');},
0x12: function(p){ops.LDrrar(p, 'D', 'E', 'A');},
0x13: function(p){ops.INCrr(p, 'D', 'E');},
0x14: function(p){ops.INCr(p, 'D');},
0x15: function(p){ops.DECr(p, 'D');},
0x16: function(p){ops.LDrn(p, 'D');},
0x17: function(p){var c = (p.r.F&0x10)?1:0;var out=p.r.A & 0x80?1:0; out ? p.r.F=0x10:p.r.F=0; p.wr('A',((p.r.A<<1)+c)&0xFF);p.clock.c+=4;},
0x18: function(p){ops.JRn(p);},
0x19: function(p){ops.ADDrrrr(p, 'H', 'L', 'D', 'E');},
0x1A: function(p){ops.LDrrra(p, 'A', 'D', 'E');},
0x1B: function(p){ops.DECrr(p, 'D', 'E');},
0x1C: function(p){ops.INCr(p, 'E');},
0x1D: function(p){ops.DECr(p, 'E');},
0x1E: function(p){ops.LDrn(p, 'E');},
0x1F: function(p){var c = (p.r.F&0x10)?1:0;var out=p.r.A & 0x01; out ? p.r.F=0x10:p.r.F=0; p.wr('A', (p.r.A>>1)|(c*0x80));p.clock.c+=4;},
0x20: function(p){ops.JRccn(p, 'NZ');},
0x21: function(p){ops.LDrrnn(p, 'H', 'L');},
0x22: function(p){ops.LDrrar(p, 'H', 'L', 'A');ops.INCrr(p, 'H', 'L');p.clock.c -= 8;},
0x23: function(p){ops.INCrr(p, 'H', 'L');},
0x24: function(p){ops.INCr(p, 'H');},
0x25: function(p){ops.DECr(p, 'H');},
0x26: function(p){ops.LDrn(p, 'H');},
0x27: function(p){ops.DAA(p);},
0x28: function(p){ops.JRccn(p, 'Z');},
0x29: function(p){ops.ADDrrrr(p, 'H', 'L', 'H', 'L');},
0x2A: function(p){ops.LDrrra(p, 'A', 'H', 'L');ops.INCrr(p, 'H', 'L');p.clock.c -= 8;},
0x2B: function(p){ops.DECrr(p, 'H', 'L');},
0x2C: function(p){ops.INCr(p, 'L');},
0x2D: function(p){ops.DECr(p, 'L');},
0x2E: function(p){ops.LDrn(p, 'L');},
0x2F: function(p){ops.CPL(p);},
0x30: function(p){ops.JRccn(p, 'NC');},
0x31: function(p){ops.LDspnn(p);},
0x32: function(p){ops.LDrrar(p, 'H', 'L', 'A');ops.DECrr(p, 'H', 'L');p.clock.c -= 8;},
0x33: function(p){ops.INCsp(p);},
0x34: function(p){ops.INCrra(p, 'H', 'L');},
0x35: function(p){ops.DECrra(p, 'H', 'L');},
0x36: function(p){ops.LDrran(p, 'H', 'L');},
0x37: function(p){ops.SCF(p);},
0x38: function(p){ops.JRccn(p, 'C');},
0x39: function(p){ops.ADDrrsp(p, 'H', 'L');},
0x3A: function(p){ops.LDrrra(p, 'A', 'H', 'L');ops.DECrr(p, 'H', 'L');p.clock.c -= 8;},
0x3B: function(p){ops.DECsp(p);},
0x3C: function(p){ops.INCr(p, 'A');},
0x3D: function(p){ops.DECr(p, 'A');},
0x3E: function(p){ops.LDrn(p, 'A');},
0x3F: function(p){ops.CCF(p);},
0x40: function(p){ops.LDrr(p, 'B', 'B');},
0x41: function(p){ops.LDrr(p, 'B', 'C');},
0x42: function(p){ops.LDrr(p, 'B', 'D');},
0x43: function(p){ops.LDrr(p, 'B', 'E');},
0x44: function(p){ops.LDrr(p, 'B', 'H');},
0x45: function(p){ops.LDrr(p, 'B', 'L');},
0x46: function(p){ops.LDrrra(p, 'B', 'H', 'L');},
0x47: function(p){ops.LDrr(p, 'B', 'A');},
0x48: function(p){ops.LDrr(p, 'C', 'B');},
0x49: function(p){ops.LDrr(p, 'C', 'C');},
0x4A: function(p){ops.LDrr(p, 'C', 'D');},
0x4B: function(p){ops.LDrr(p, 'C', 'E');},
0x4C: function(p){ops.LDrr(p, 'C', 'H');},
0x4D: function(p){ops.LDrr(p, 'C', 'L');},
0x4E: function(p){ops.LDrrra(p, 'C', 'H', 'L');},
0x4F: function(p){ops.LDrr(p, 'C', 'A');},
0x50: function(p){ops.LDrr(p, 'D', 'B');},
0x51: function(p){ops.LDrr(p, 'D', 'C');},
0x52: function(p){ops.LDrr(p, 'D', 'D');},
0x53: function(p){ops.LDrr(p, 'D', 'E');},
0x54: function(p){ops.LDrr(p, 'D', 'H');},
0x55: function(p){ops.LDrr(p, 'D', 'L');},
0x56: function(p){ops.LDrrra(p, 'D', 'H', 'L');},
0x57: function(p){ops.LDrr(p, 'D', 'A');},
0x58: function(p){ops.LDrr(p, 'E', 'B');},
0x59: function(p){ops.LDrr(p, 'E', 'C');},
0x5A: function(p){ops.LDrr(p, 'E', 'D');},
0x5B: function(p){ops.LDrr(p, 'E', 'E');},
0x5C: function(p){ops.LDrr(p, 'E', 'H');},
0x5D: function(p){ops.LDrr(p, 'E', 'L');},
0x5E: function(p){ops.LDrrra(p, 'E', 'H', 'L');},
0x5F: function(p){ops.LDrr(p, 'E', 'A');},
0x60: function(p){ops.LDrr(p, 'H', 'B');},
0x61: function(p){ops.LDrr(p, 'H', 'C');},
0x62: function(p){ops.LDrr(p, 'H', 'D');},
0x63: function(p){ops.LDrr(p, 'H', 'E');},
0x64: function(p){ops.LDrr(p, 'H', 'H');},
0x65: function(p){ops.LDrr(p, 'H', 'L');},
0x66: function(p){ops.LDrrra(p, 'H', 'H', 'L');},
0x67: function(p){ops.LDrr(p, 'H', 'A');},
0x68: function(p){ops.LDrr(p, 'L', 'B');},
0x69: function(p){ops.LDrr(p, 'L', 'C');},
0x6A: function(p){ops.LDrr(p, 'L', 'D');},
0x6B: function(p){ops.LDrr(p, 'L', 'E');},
0x6C: function(p){ops.LDrr(p, 'L', 'H');},
0x6D: function(p){ops.LDrr(p, 'L', 'L');},
0x6E: function(p){ops.LDrrra(p, 'L', 'H', 'L');},
0x6F: function(p){ops.LDrr(p, 'L', 'A');},
0x70: function(p){ops.LDrrar(p, 'H', 'L', 'B');},
0x71: function(p){ops.LDrrar(p, 'H', 'L', 'C');},
0x72: function(p){ops.LDrrar(p, 'H', 'L', 'D');},
0x73: function(p){ops.LDrrar(p, 'H', 'L', 'E');},
0x74: function(p){ops.LDrrar(p, 'H', 'L', 'H');},
0x75: function(p){ops.LDrrar(p, 'H', 'L', 'L');},
0x76: function(p){ops.HALT(p);},
0x77: function(p){ops.LDrrar(p, 'H', 'L', 'A');},
0x78: function(p){ops.LDrr(p, 'A', 'B');},
0x79: function(p){ops.LDrr(p, 'A', 'C');},
0x7A: function(p){ops.LDrr(p, 'A', 'D');},
0x7B: function(p){ops.LDrr(p, 'A', 'E');},
0x7C: function(p){ops.LDrr(p, 'A', 'H');},
0x7D: function(p){ops.LDrr(p, 'A', 'L');},
0x7E: function(p){ops.LDrrra(p, 'A', 'H', 'L');},
0x7F: function(p){ops.LDrr(p, 'A', 'A');},
0x80: function(p){ops.ADDrr(p, 'A', 'B');},
0x81: function(p){ops.ADDrr(p, 'A', 'C');},
0x82: function(p){ops.ADDrr(p, 'A', 'D');},
0x83: function(p){ops.ADDrr(p, 'A', 'E');},
0x84: function(p){ops.ADDrr(p, 'A', 'H');},
0x85: function(p){ops.ADDrr(p, 'A', 'L');},
0x86: function(p){ops.ADDrrra(p, 'A', 'H', 'L');},
0x87: function(p){ops.ADDrr(p, 'A', 'A');},
0x88: function(p){ops.ADCrr(p, 'A', 'B');},
0x89: function(p){ops.ADCrr(p, 'A', 'C');},
0x8A: function(p){ops.ADCrr(p, 'A', 'D');},
0x8B: function(p){ops.ADCrr(p, 'A', 'E');},
0x8C: function(p){ops.ADCrr(p, 'A', 'H');},
0x8D: function(p){ops.ADCrr(p, 'A', 'L');},
0x8E: function(p){ops.ADCrrra(p, 'A', 'H', 'L');},
0x8F: function(p){ops.ADCrr(p, 'A', 'A');},
0x90: function(p){ops.SUBr(p, 'B');},
0x91: function(p){ops.SUBr(p, 'C');},
0x92: function(p){ops.SUBr(p, 'D');},
0x93: function(p){ops.SUBr(p, 'E');},
0x94: function(p){ops.SUBr(p, 'H');},
0x95: function(p){ops.SUBr(p, 'L');},
0x96: function(p){ops.SUBrra(p, 'H', 'L');},
0x97: function(p){ops.SUBr(p, 'A');},
0x98: function(p){ops.SBCr(p, 'B');},
0x99: function(p){ops.SBCr(p, 'C');},
0x9A: function(p){ops.SBCr(p, 'D');},
0x9B: function(p){ops.SBCr(p, 'E');},
0x9C: function(p){ops.SBCr(p, 'H');},
0x9D: function(p){ops.SBCr(p, 'L');},
0x9E: function(p){ops.SBCrra(p, 'H', 'L');},
0x9F: function(p){ops.SBCr(p, 'A');},
0xA0: function(p){ops.ANDr(p, 'B');},
0xA1: function(p){ops.ANDr(p, 'C');},
0xA2: function(p){ops.ANDr(p, 'D');},
0xA3: function(p){ops.ANDr(p, 'E');},
0xA4: function(p){ops.ANDr(p, 'H');},
0xA5: function(p){ops.ANDr(p, 'L');},
0xA6: function(p){ops.ANDrra(p, 'H', 'L');},
0xA7: function(p){ops.ANDr(p, 'A');},
0xA8: function(p){ops.XORr(p, 'B');},
0xA9: function(p){ops.XORr(p, 'C');},
0xAA: function(p){ops.XORr(p, 'D');},
0xAB: function(p){ops.XORr(p, 'E');},
0xAC: function(p){ops.XORr(p, 'H');},
0xAD: function(p){ops.XORr(p, 'L');},
0xAE: function(p){ops.XORrra(p, 'H', 'L');},
0xAF: function(p){ops.XORr(p, 'A');},
0xB0: function(p){ops.ORr(p, 'B');},
0xB1: function(p){ops.ORr(p, 'C');},
0xB2: function(p){ops.ORr(p, 'D');},
0xB3: function(p){ops.ORr(p, 'E');},
0xB4: function(p){ops.ORr(p, 'H');},
0xB5: function(p){ops.ORr(p, 'L');},
0xB6: function(p){ops.ORrra(p, 'H', 'L');},
0xB7: function(p){ops.ORr(p, 'A');},
0xB8: function(p){ops.CPr(p, 'B');},
0xB9: function(p){ops.CPr(p, 'C');},
0xBA: function(p){ops.CPr(p, 'D');},
0xBB: function(p){ops.CPr(p, 'E');},
0xBC: function(p){ops.CPr(p, 'H');},
0xBD: function(p){ops.CPr(p, 'L');},
0xBE: function(p){ops.CPrra(p, 'H', 'L');},
0xBF: function(p){ops.CPr(p, 'A');},
0xC0: function(p){ops.RETcc(p, 'NZ');},
0xC1: function(p){ops.POPrr(p, 'B', 'C');},
0xC2: function(p){ops.JPccnn(p, 'NZ');},
0xC3: function(p){ops.JPnn(p);},
0xC4: function(p){ops.CALLccnn(p, 'NZ');},
0xC5: function(p){ops.PUSHrr(p, 'B', 'C');},
0xC6: function(p){ops.ADDrn(p, 'A');},
0xC7: function(p){ops.RSTn(p, 0x00);},
0xC8: function(p){ops.RETcc(p, 'Z');},
0xC9: function(p){ops.RET(p);},
0xCA: function(p){ops.JPccnn(p, 'Z');},
0xCB: function(p){ops.CB(p);},
0xCC: function(p){ops.CALLccnn(p, 'Z');},
0xCD: function(p){ops.CALLnn(p);},
0xCE: function(p){ops.ADCrn(p, 'A');},
0xCF: function(p){ops.RSTn(p, 0x08);},
0xD0: function(p){ops.RETcc(p, 'NC');},
0xD1: function(p){ops.POPrr(p, 'D', 'E');},
0xD2: function(p){ops.JPccnn(p, 'NC');},
//0xD3 empty
0xD4: function(p){ops.CALLccnn(p, 'NC');},
0xD5: function(p){ops.PUSHrr(p, 'D', 'E');},
0xD6: function(p){ops.SUBn(p);},
0xD7: function(p){ops.RSTn(p, 0x10);},
0xD8: function(p){ops.RETcc(p, 'C');},
0xD9: function(p){ops.RETI(p);},
0xDA: function(p){ops.JPccnn(p, 'C');},
//0xDB empty
0xDC: function(p){ops.CALLccnn(p, 'C');},
//0xDD empty
0xDE: function(p){ops.SBCn(p);},
0xDF: function(p){ops.RSTn(p, 0x18);},
0xE0: function(p){ops.LDHnar(p, 'A');},
0xE1: function(p){ops.POPrr(p, 'H', 'L');},
0xE2: function(p){ops.LDrar(p, 'C', 'A');},
//0xE3 empty
//0xE4 empty
0xE5: function(p){ops.PUSHrr(p, 'H', 'L');},
0xE6: function(p){ops.ANDn(p);},
0xE7: function(p){ops.RSTn(p, 0x20);},
0xE8: function(p){ops.ADDspn(p);},
0xE9: function(p){ops.JPrr(p, 'H', 'L');},
0xEA: function(p){ops.LDnnar(p, 'A');},
//0xEB empty
//0xEC empty
//0xED empty
0xEE: function(p){ops.XORn(p);},
0xEF: function(p){ops.RSTn(p, 0x28);},
0xF0: function(p){ops.LDHrna(p, 'A');},
0xF1: function(p){ops.POPrr(p, 'A', 'F');},
0xF2: function(p){ops.LDrra(p, 'A', 'C');},
0xF3: function(p){ops.DI(p);},
//0xF4 empty
0xF5: function(p){ops.PUSHrr(p, 'A', 'F');},
0xF6: function(p){ops.ORn(p);},
0xF7: function(p){ops.RSTn(p, 0x30);},
0xF8: function(p){ops.LDrrspn(p, 'H', 'L');},
0xF9: function(p){ops.LDsprr(p, 'H', 'L');},
0xFA: function(p){ops.LDrnna(p, 'A');},
0xFB: function(p){ops.EI(p);},
//0xFC empty
//0xFD empty
0xFE: function(p){ops.CPn(p);},
0xFF: function(p){ops.RSTn(p, 0x38);}
};
var cbmap = {
0x00: function(p){ops.RLCr(p, 'B');},
0x01: function(p){ops.RLCr(p, 'C');},
0x02: function(p){ops.RLCr(p, 'D');},
0x03: function(p){ops.RLCr(p, 'E');},
0x04: function(p){ops.RLCr(p, 'H');},
0x05: function(p){ops.RLCr(p, 'L');},
0x06: function(p){ops.RLCrra(p, 'H', 'L');},
0x07: function(p){ops.RLCr(p, 'A');},
0x08: function(p){ops.RRCr(p, 'B');},
0x09: function(p){ops.RRCr(p, 'C');},
0x0A: function(p){ops.RRCr(p, 'D');},
0x0B: function(p){ops.RRCr(p, 'E');},
0x0C: function(p){ops.RRCr(p, 'H');},
0x0D: function(p){ops.RRCr(p, 'L');},
0x0E: function(p){ops.RRCrra(p, 'H', 'L');},
0x0F: function(p){ops.RRCr(p, 'A');},
0x10: function(p){ops.RLr(p, 'B');},
0x11: function(p){ops.RLr(p, 'C');},
0x12: function(p){ops.RLr(p, 'D');},
0x13: function(p){ops.RLr(p, 'E');},
0x14: function(p){ops.RLr(p, 'H');},
0x15: function(p){ops.RLr(p, 'L');},
0x16: function(p){ops.RLrra(p, 'H', 'L');},
0x17: function(p){ops.RLr(p, 'A');},
0x18: function(p){ops.RRr(p, 'B');},
0x19: function(p){ops.RRr(p, 'C');},
0x1A: function(p){ops.RRr(p, 'D');},
0x1B: function(p){ops.RRr(p, 'E');},
0x1C: function(p){ops.RRr(p, 'H');},
0x1D: function(p){ops.RRr(p, 'L');},
0x1E: function(p){ops.RRrra(p, 'H', 'L');},
0x1F: function(p){ops.RRr(p, 'A');},
0x20: function(p){ops.SLAr(p, 'B');},
0x21: function(p){ops.SLAr(p, 'C');},
0x22: function(p){ops.SLAr(p, 'D');},
0x23: function(p){ops.SLAr(p, 'E');},
0x24: function(p){ops.SLAr(p, 'H');},
0x25: function(p){ops.SLAr(p, 'L');},
0x26: function(p){ops.SLArra(p, 'H', 'L');},
0x27: function(p){ops.SLAr(p, 'A');},
0x28: function(p){ops.SRAr(p, 'B');},
0x29: function(p){ops.SRAr(p, 'C');},
0x2A: function(p){ops.SRAr(p, 'D');},
0x2B: function(p){ops.SRAr(p, 'E');},
0x2C: function(p){ops.SRAr(p, 'H');},
0x2D: function(p){ops.SRAr(p, 'L');},
0x2E: function(p){ops.SRArra(p, 'H', 'L');},
0x2F: function(p){ops.SRAr(p, 'A');},
0x30: function(p){ops.SWAPr(p, 'B');},
0x31: function(p){ops.SWAPr(p, 'C');},
0x32: function(p){ops.SWAPr(p, 'D');},
0x33: function(p){ops.SWAPr(p, 'E');},
0x34: function(p){ops.SWAPr(p, 'H');},
0x35: function(p){ops.SWAPr(p, 'L');},
0x36: function(p){ops.SWAPrra(p, 'H', 'L');},
0x37: function(p){ops.SWAPr(p, 'A');},
0x38: function(p){ops.SRLr(p, 'B');},
0x39: function(p){ops.SRLr(p, 'C');},
0x3A: function(p){ops.SRLr(p, 'D');},
0x3B: function(p){ops.SRLr(p, 'E');},
0x3C: function(p){ops.SRLr(p, 'H');},
0x3D: function(p){ops.SRLr(p, 'L');},
0x3E: function(p){ops.SRLrra(p, 'H', 'L');},
0x3F: function(p){ops.SRLr(p, 'A');},
0x40: function(p){ops.BITir(p, 0, 'B');},
0x41: function(p){ops.BITir(p, 0, 'C');},
0x42: function(p){ops.BITir(p, 0, 'D');},
0x43: function(p){ops.BITir(p, 0, 'E');},
0x44: function(p){ops.BITir(p, 0, 'H');},
0x45: function(p){ops.BITir(p, 0, 'L');},
0x46: function(p){ops.BITirra(p, 0, 'H', 'L');},
0x47: function(p){ops.BITir(p, 0, 'A');},
0x48: function(p){ops.BITir(p, 1, 'B');},
0x49: function(p){ops.BITir(p, 1, 'C');},
0x4A: function(p){ops.BITir(p, 1, 'D');},
0x4B: function(p){ops.BITir(p, 1, 'E');},
0x4C: function(p){ops.BITir(p, 1, 'H');},
0x4D: function(p){ops.BITir(p, 1, 'L');},
0x4E: function(p){ops.BITirra(p, 1, 'H', 'L');},
0x4F: function(p){ops.BITir(p, 1, 'A');},
0x50: function(p){ops.BITir(p, 2, 'B');},
0x51: function(p){ops.BITir(p, 2, 'C');},
0x52: function(p){ops.BITir(p, 2, 'D');},
0x53: function(p){ops.BITir(p, 2, 'E');},
0x54: function(p){ops.BITir(p, 2, 'H');},
0x55: function(p){ops.BITir(p, 2, 'L');},
0x56: function(p){ops.BITirra(p, 2, 'H', 'L');},
0x57: function(p){ops.BITir(p, 2, 'A');},
0x58: function(p){ops.BITir(p, 3, 'B');},
0x59: function(p){ops.BITir(p, 3, 'C');},
0x5A: function(p){ops.BITir(p, 3, 'D');},
0x5B: function(p){ops.BITir(p, 3, 'E');},
0x5C: function(p){ops.BITir(p, 3, 'H');},
0x5D: function(p){ops.BITir(p, 3, 'L');},
0x5E: function(p){ops.BITirra(p, 3, 'H', 'L');},
0x5F: function(p){ops.BITir(p, 3, 'A');},
0x60: function(p){ops.BITir(p, 4, 'B');},
0x61: function(p){ops.BITir(p, 4, 'C');},
0x62: function(p){ops.BITir(p, 4, 'D');},
0x63: function(p){ops.BITir(p, 4, 'E');},
0x64: function(p){ops.BITir(p, 4, 'H');},
0x65: function(p){ops.BITir(p, 4, 'L');},
0x66: function(p){ops.BITirra(p, 4, 'H', 'L');},
0x67: function(p){ops.BITir(p, 4, 'A');},
0x68: function(p){ops.BITir(p, 5, 'B');},
0x69: function(p){ops.BITir(p, 5, 'C');},
0x6A: function(p){ops.BITir(p, 5, 'D');},
0x6B: function(p){ops.BITir(p, 5, 'E');},
0x6C: function(p){ops.BITir(p, 5, 'H');},
0x6D: function(p){ops.BITir(p, 5, 'L');},
0x6E: function(p){ops.BITirra(p, 5, 'H', 'L');},
0x6F: function(p){ops.BITir(p, 5, 'A');},
0x70: function(p){ops.BITir(p, 6, 'B');},
0x71: function(p){ops.BITir(p, 6, 'C');},
0x72: function(p){ops.BITir(p, 6, 'D');},
0x73: function(p){ops.BITir(p, 6, 'E');},
0x74: function(p){ops.BITir(p, 6, 'H');},
0x75: function(p){ops.BITir(p, 6, 'L');},
0x76: function(p){ops.BITirra(p, 6, 'H', 'L');},
0x77: function(p){ops.BITir(p, 6, 'A');},
0x78: function(p){ops.BITir(p, 7, 'B');},
0x79: function(p){ops.BITir(p, 7, 'C');},
0x7A: function(p){ops.BITir(p, 7, 'D');},
0x7B: function(p){ops.BITir(p, 7, 'E');},
0x7C: function(p){ops.BITir(p, 7, 'H');},
0x7D: function(p){ops.BITir(p, 7, 'L');},
0x7E: function(p){ops.BITirra(p, 7, 'H', 'L');},
0x7F: function(p){ops.BITir(p, 7, 'A');},
0x80: function(p){ops.RESir(p, 0, 'B');},
0x81: function(p){ops.RESir(p, 0, 'C');},
0x82: function(p){ops.RESir(p, 0, 'D');},
0x83: function(p){ops.RESir(p, 0, 'E');},
0x84: function(p){ops.RESir(p, 0, 'H');},
0x85: function(p){ops.RESir(p, 0, 'L');},
0x86: function(p){ops.RESirra(p, 0, 'H', 'L');},
0x87: function(p){ops.RESir(p, 0, 'A');},
0x88: function(p){ops.RESir(p, 1, 'B');},
0x89: function(p){ops.RESir(p, 1, 'C');},
0x8A: function(p){ops.RESir(p, 1, 'D');},
0x8B: function(p){ops.RESir(p, 1, 'E');},
0x8C: function(p){ops.RESir(p, 1, 'H');},
0x8D: function(p){ops.RESir(p, 1, 'L');},
0x8E: function(p){ops.RESirra(p, 1, 'H', 'L');},
0x8F: function(p){ops.RESir(p, 1, 'A');},
0x90: function(p){ops.RESir(p, 2, 'B');},
0x91: function(p){ops.RESir(p, 2, 'C');},
0x92: function(p){ops.RESir(p, 2, 'D');},
0x93: function(p){ops.RESir(p, 2, 'E');},
0x94: function(p){ops.RESir(p, 2, 'H');},
0x95: function(p){ops.RESir(p, 2, 'L');},
0x96: function(p){ops.RESirra(p, 2, 'H', 'L');},
0x97: function(p){ops.RESir(p, 2, 'A');},
0x98: function(p){ops.RESir(p, 3, 'B');},
0x99: function(p){ops.RESir(p, 3, 'C');},
0x9A: function(p){ops.RESir(p, 3, 'D');},
0x9B: function(p){ops.RESir(p, 3, 'E');},
0x9C: function(p){ops.RESir(p, 3, 'H');},
0x9D: function(p){ops.RESir(p, 3, 'L');},
0x9E: function(p){ops.RESirra(p, 3, 'H', 'L');},
0x9F: function(p){ops.RESir(p, 3, 'A');},
0xA0: function(p){ops.RESir(p, 4, 'B');},
0xA1: function(p){ops.RESir(p, 4, 'C');},
0xA2: function(p){ops.RESir(p, 4, 'D');},
0xA3: function(p){ops.RESir(p, 4, 'E');},
0xA4: function(p){ops.RESir(p, 4, 'H');},
0xA5: function(p){ops.RESir(p, 4, 'L');},
0xA6: function(p){ops.RESirra(p, 4, 'H', 'L');},
0xA7: function(p){ops.RESir(p, 4, 'A');},
0xA8: function(p){ops.RESir(p, 5, 'B');},
0xA9: function(p){ops.RESir(p, 5, 'C');},
0xAA: function(p){ops.RESir(p, 5, 'D');},
0xAB: function(p){ops.RESir(p, 5, 'E');},
0xAC: function(p){ops.RESir(p, 5, 'H');},
0xAD: function(p){ops.RESir(p, 5, 'L');},
0xAE: function(p){ops.RESirra(p, 5, 'H', 'L');},
0xAF: function(p){ops.RESir(p, 5, 'A');},
0xB0: function(p){ops.RESir(p, 6, 'B');},
0xB1: function(p){ops.RESir(p, 6, 'C');},
0xB2: function(p){ops.RESir(p, 6, 'D');},
0xB3: function(p){ops.RESir(p, 6, 'E');},
0xB4: function(p){ops.RESir(p, 6, 'H');},
0xB5: function(p){ops.RESir(p, 6, 'L');},
0xB6: function(p){ops.RESirra(p, 6, 'H', 'L');},
0xB7: function(p){ops.RESir(p, 6, 'A');},
0xB8: function(p){ops.RESir(p, 7, 'B');},
0xB9: function(p){ops.RESir(p, 7, 'C');},
0xBA: function(p){ops.RESir(p, 7, 'D');},
0xBB: function(p){ops.RESir(p, 7, 'E');},
0xBC: function(p){ops.RESir(p, 7, 'H');},
0xBD: function(p){ops.RESir(p, 7, 'L');},
0xBE: function(p){ops.RESirra(p, 7, 'H', 'L');},
0xBF: function(p){ops.RESir(p, 7, 'A');},
0xC0: function(p){ops.SETir(p, 0, 'B');},
0xC1: function(p){ops.SETir(p, 0, 'C');},
0xC2: function(p){ops.SETir(p, 0, 'D');},
0xC3: function(p){ops.SETir(p, 0, 'E');},
0xC4: function(p){ops.SETir(p, 0, 'H');},
0xC5: function(p){ops.SETir(p, 0, 'L');},
0xC6: function(p){ops.SETirra(p, 0, 'H', 'L');},
0xC7: function(p){ops.SETir(p, 0, 'A');},
0xC8: function(p){ops.SETir(p, 1, 'B');},
0xC9: function(p){ops.SETir(p, 1, 'C');},
0xCA: function(p){ops.SETir(p, 1, 'D');},
0xCB: function(p){ops.SETir(p, 1, 'E');},
0xCC: function(p){ops.SETir(p, 1, 'H');},
0xCD: function(p){ops.SETir(p, 1, 'L');},
0xCE: function(p){ops.SETirra(p, 1, 'H', 'L');},
0xCF: function(p){ops.SETir(p, 1, 'A');},
0xD0: function(p){ops.SETir(p, 2, 'B');},
0xD1: function(p){ops.SETir(p, 2, 'C');},
0xD2: function(p){ops.SETir(p, 2, 'D');},
0xD3: function(p){ops.SETir(p, 2, 'E');},
0xD4: function(p){ops.SETir(p, 2, 'H');},
0xD5: function(p){ops.SETir(p, 2, 'L');},
0xD6: function(p){ops.SETirra(p, 2, 'H', 'L');},
0xD7: function(p){ops.SETir(p, 2, 'A');},
0xD8: function(p){ops.SETir(p, 3, 'B');},
0xD9: function(p){ops.SETir(p, 3, 'C');},
0xDA: function(p){ops.SETir(p, 3, 'D');},
0xDB: function(p){ops.SETir(p, 3, 'E');},
0xDC: function(p){ops.SETir(p, 3, 'H');},
0xDD: function(p){ops.SETir(p, 3, 'L');},
0xDE: function(p){ops.SETirra(p, 3, 'H', 'L');},
0xDF: function(p){ops.SETir(p, 3, 'A');},
0xE0: function(p){ops.SETir(p, 4, 'B');},
0xE1: function(p){ops.SETir(p, 4, 'C');},
0xE2: function(p){ops.SETir(p, 4, 'D');},
0xE3: function(p){ops.SETir(p, 4, 'E');},
0xE4: function(p){ops.SETir(p, 4, 'H');},
0xE5: function(p){ops.SETir(p, 4, 'L');},
0xE6: function(p){ops.SETirra(p, 4, 'H', 'L');},
0xE7: function(p){ops.SETir(p, 4, 'A');},
0xE8: function(p){ops.SETir(p, 5, 'B');},
0xE9: function(p){ops.SETir(p, 5, 'C');},
0xEA: function(p){ops.SETir(p, 5, 'D');},
0xEB: function(p){ops.SETir(p, 5, 'E');},
0xEC: function(p){ops.SETir(p, 5, 'H');},
0xED: function(p){ops.SETir(p, 5, 'L');},
0xEE: function(p){ops.SETirra(p, 5, 'H', 'L');},
0xEF: function(p){ops.SETir(p, 5, 'A');},
0xF0: function(p){ops.SETir(p, 6, 'B');},
0xF1: function(p){ops.SETir(p, 6, 'C');},
0xF2: function(p){ops.SETir(p, 6, 'D');},
0xF3: function(p){ops.SETir(p, 6, 'E');},
0xF4: function(p){ops.SETir(p, 6, 'H');},
0xF5: function(p){ops.SETir(p, 6, 'L');},
0xF6: function(p){ops.SETirra(p, 6, 'H', 'L');},
0xF7: function(p){ops.SETir(p, 6, 'A');},
0xF8: function(p){ops.SETir(p, 7, 'B');},
0xF9: function(p){ops.SETir(p, 7, 'C');},
0xFA: function(p){ops.SETir(p, 7, 'D');},
0xFB: function(p){ops.SETir(p, 7, 'E');},
0xFC: function(p){ops.SETir(p, 7, 'H');},
0xFD: function(p){ops.SETir(p, 7, 'L');},
0xFE: function(p){ops.SETirra(p, 7, 'H', 'L');},
0xFF: function(p){ops.SETir(p, 7, 'A');}
};
GameboyJS.opcodeMap = map;
GameboyJS.opcodeCbmap = cbmap;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// A RomAjaxReader is able to load a file through an AJAX request
var RomAjaxReader = function() {
};
// The callback argument will be called when a file is successfully
// read, with the data as argument (Uint8Array)
RomAjaxReader.prototype.setCallback = function(onLoadCallback) {
this.callback = onLoadCallback;
};
// This function should be called by application code
// and will trigger the AJAX call itself and push data to the ROM object
RomAjaxReader.prototype.loadFromUrl = function(url) {
if (!url) {
throw 'No url has been set in order to load a ROM file.';
}
var cb = this.callback;
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = "arraybuffer";
xhr.onload = function() {
var rom = new Uint8Array(xhr.response);
cb && cb(rom);
};
xhr.send();
};
GameboyJS.RomAjaxReader = RomAjaxReader;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// A RomDropFileReader is able to load a drag and dropped file
var RomDropFileReader = function(el) {
this.dropElement = el;
if (!this.dropElement) {
throw 'The RomDropFileReader needs a drop zone.';
}
var self = this;
this.dropElement.addEventListener('dragenter', function(e) {
e.preventDefault();
e.target.classList.add('drag-active');
});
this.dropElement.addEventListener('dragleave', function(e) {
e.preventDefault();
e.target.classList.remove('drag-active');
});
this.dropElement.addEventListener('dragover', function(e) {
e.preventDefault();
});
this.dropElement.addEventListener('drop', function (e) {
e.target.classList.remove('drag-active');
if (e.dataTransfer.files.length == 0) {
return;
}
e.preventDefault();
self.loadFromFile(e.dataTransfer.files[0]);
});
};
// The callback argument will be called when a file is successfully
// read, with the data as argument (Uint8Array)
RomDropFileReader.prototype.setCallback = function(onLoadCallback) {
this.callback = onLoadCallback;
};
// The file loading logic is the same as the regular file reader
RomDropFileReader.prototype.loadFromFile = function(file) {
if (file === undefined) {
return;
}
var fr = new FileReader();
var cb = this.callback;
fr.onload = function() {
cb && cb(new Uint8Array(fr.result));
};
fr.onerror = function(e) {
console.log('Error reading the file', e.target.error.code)
};
fr.readAsArrayBuffer(file);
};
GameboyJS.RomDropFileReader = RomDropFileReader;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// A RomFileReader is able to load a local file from an input element
//
// Expects to be provided a file input element,
// or will try to find one with the "file" DOM ID
var RomFileReader = function(el) {
this.domElement = el || document.getElementById('file');
if (!this.domElement) {
throw 'The RomFileReader needs a valid input element.';
}
var self = this;
this.domElement.addEventListener('change', function(e){
self.loadFromFile(e.target.files[0]);
});
};
// The callback argument will be called when a file is successfully
// read, with the data as argument (Uint8Array)
RomFileReader.prototype.setCallback = function(onLoadCallback) {
this.callback = onLoadCallback;
};
// Automatically called when the DOM input is provided with a file
RomFileReader.prototype.loadFromFile = function(file) {
if (file === undefined) {
return;
}
var fr = new FileReader();
var cb = this.callback;
fr.onload = function() {
cb && cb(new Uint8Array(fr.result));
};
fr.onerror = function(e) {
console.log('Error reading the file', e.target.error.code)
};
fr.readAsArrayBuffer(file);
};
GameboyJS.RomFileReader = RomFileReader;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
var Rom = function(gameboy, romReader) {
this.gameboy = gameboy;
if (romReader) {
this.addReader(romReader);
}
};
Rom.prototype.addReader = function(romReader) {
var self = this;
romReader.setCallback(function(data) {
if (!validate(data)) {
self.gameboy.error('The file is not a valid GameBoy ROM.');
return;
}
self.data = data;
self.gameboy.startRom(self);
});
};
// Validate the checksum of the cartridge header
function validate(data) {
var hash = 0;
for (var i = 0x134; i <= 0x14C; i++) {
hash = hash - data[i] - 1;
}
return (hash & 0xFF) == data[0x14D];
};
GameboyJS.Rom = Rom;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// Handlers for the Serial port of the Gameboy
// The ConsoleSerial is an output-only serial port
// designed for debug purposes as some test roms output data on the serial port
//
// Will regularly output the received byte (converted to string) in the console logs
// This handler always push the value 0xFF as an input
var ConsoleSerial = {
current: '',
timeout: null,
out: function(data) {
ConsoleSerial.current += String.fromCharCode(data);
if (data == 10) {
ConsoleSerial.print();
} else {
clearTimeout(ConsoleSerial.timeout);
ConsoleSerial.timeout = setTimeout(ConsoleSerial.print, 500);
}
},
in: function() {
return 0xFF;
},
print: function() {
clearTimeout(ConsoleSerial.timeout);
console.log('serial: '+ConsoleSerial.current);
ConsoleSerial.current = '';
}
};
GameboyJS.ConsoleSerial = ConsoleSerial;
// A DummySerial outputs nothing and always inputs 0xFF
var DummySerial = {
out: function() {},
in: function() {
return 0xFF;
}
};
GameboyJS.DummySerial = DummySerial;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// Audio Processing unit
// Listens the write accesses to the audio-reserved memory addresses
// and dispatches the data to the sound channels
var APU = function(memory) {
this.memory = memory;
this.enabled = false;
AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
this.channel1 = new GameboyJS.Channel1(this, 1, audioContext);
this.channel2 = new GameboyJS.Channel1(this, 2, audioContext);
this.channel3 = new GameboyJS.Channel3(this, 3, audioContext);
this.channel4 = new GameboyJS.Channel4(this, 4, audioContext);
};
APU.prototype.connect = function() {
this.channel1.enable();
this.channel2.enable();
this.channel3.enable();
};
APU.prototype.disconnect = function() {
this.channel1.disable();
this.channel2.disable();
this.channel3.disable();
};
// Updates the states of each channel given the elapsed time
// (in instructions) since last update
APU.prototype.update = function(clockElapsed) {
if (this.enabled == false) return;
this.channel1.update(clockElapsed);
this.channel2.update(clockElapsed);
this.channel3.update(clockElapsed);
this.channel4.update(clockElapsed);
};
APU.prototype.setSoundFlag = function(channel, value) {
var mask = 0xFF - (1 << (channel - 1));
value = value << (channel - 1)
var byteValue = this.memory.rb(APU.registers.NR52);
byteValue &= mask;
byteValue |= value;
this.memory[APU.registers.NR52] = byteValue;
};
// Manage writes to audio registers
// Will update the channels depending on the address
APU.prototype.manageWrite = function(addr, value) {
if (this.enabled == false && addr < APU.registers.NR52) {
return;
}
this.memory[addr] = value;
switch (addr) {
// Channel 1 addresses
case 0xFF10:
this.channel1.clockSweep = 0;
this.channel1.sweepTime = ((value & 0x70) >> 4);
this.channel1.sweepSign = (value & 0x08) ? -1 : 1;
this.channel1.sweepShifts = (value & 0x07);
this.channel1.sweepCount = this.channel1.sweepShifts;
break;
case 0xFF11:
// todo : bits 6-7
this.channel1.setLength(value & 0x3F);
break;
case 0xFF12:
this.channel1.envelopeSign = (value & 0x08) ? 1 : -1;
var envelopeVolume = (value & 0xF0) >> 4;
this.channel1.setEnvelopeVolume(envelopeVolume);
this.channel1.envelopeStep = (value & 0x07);
break;
case 0xFF13:
var frequency = this.channel1.getFrequency();
frequency &= 0xF00;
frequency |= value;
this.channel1.setFrequency(frequency);
break;
case 0xFF14:
var frequency = this.channel1.getFrequency();
frequency &= 0xFF;
frequency |= (value & 7) << 8;
this.channel1.setFrequency(frequency);
this.channel1.lengthCheck = (value & 0x40) ? true : false;
if (value & 0x80) this.channel1.play();
break;
// Channel 2 addresses
case 0xFF16:
// todo : bits 6-7
this.channel2.setLength(value & 0x3F);
break;
case 0xFF17:
this.channel2.envelopeSign = (value & 0x08) ? 1 : -1;
var envelopeVolume = (value & 0xF0) >> 4;
this.channel2.setEnvelopeVolume(envelopeVolume);
this.channel2.envelopeStep = (value & 0x07);
break;
case 0xFF18:
var frequency = this.channel2.getFrequency();
frequency &= 0xF00;
frequency |= value;
this.channel2.setFrequency(frequency);
break;
case 0xFF19:
var frequency = this.channel2.getFrequency();
frequency &= 0xFF;
frequency |= (value & 7) << 8;
this.channel2.setFrequency(frequency);
this.channel2.lengthCheck = (value & 0x40) ? true : false;
if (value & 0x80) {
this.channel2.play();
}
break;
// Channel 3 addresses
case 0xFF1A:
// todo
break;
case 0xFF1B:
this.channel3.setLength(value);
break;
case 0xFF1C:
// todo
break;
case 0xFF1D:
var frequency = this.channel3.getFrequency();
frequency &= 0xF00;
frequency |= value;
this.channel3.setFrequency(frequency);
break;
case 0xFF1E:
var frequency = this.channel3.getFrequency();
frequency &= 0xFF;
frequency |= (value & 7) << 8;
this.channel3.setFrequency(frequency);
this.channel3.lengthCheck = (value & 0x40) ? true : false;
if (value & 0x80) {
this.channel3.play();
}
break;
// Channel 4 addresses
case 0xFF20:
this.channel4.setLength(value & 0x3F);
break;
case 0xFF21:
// todo
break;
case 0xFF22:
// todo
break;
case 0xFF23:
this.channel4.lengthCheck = (value & 0x40) ? true : false;
if (value & 0x80) {
this.channel4.play();
}
break;
// channel 3 wave bytes
case 0xFF30:case 0xFF31:case 0xFF32:case 0xFF33:case 0xFF34:case 0xFF35:case 0xFF36:case 0xFF37:
case 0xFF38:case 0xFF39:case 0xFF3A:case 0xFF3B:case 0xFF3C:case 0xFF3D:case 0xFF3E:case 0xFF3F:
var index = addr - 0xFF30;
this.channel3.setWaveBufferByte(index, value);
break;
// general audio switch
case 0xFF26:
value &= 0xF0;
this.memory[addr] = value;
this.enabled = (value & 0x80) == 0 ? false : true;
if (!this.enabled) {
for (var i = 0xFF10; i < 0xFF27; i++)
this.memory[i] = 0;
// todo stop sound
}
break;
}
};
APU.registers = {
NR10: 0xFF10,
NR11: 0xFF11,
NR12: 0xFF12,
NR13: 0xFF13,
NR14: 0xFF14,
NR21: 0xFF16,
NR22: 0xFF17,
NR23: 0xFF18,
NR24: 0xFF19,
NR30: 0xFF1A,
NR31: 0xFF1B,
NR32: 0xFF1C,
NR33: 0xFF1D,
NR34: 0xFF1E,
NR41: 0xFF20,
NR42: 0xFF21,
NR43: 0xFF22,
NR44: 0xFF23,
NR50: 0xFF24,
NR51: 0xFF25,
NR52: 0xFF26
};
GameboyJS.APU = APU;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
var Channel1 = function(apu, channelNumber, audioContext) {
this.apu = apu;
this.channelNumber = channelNumber;
this.playing = false;
this.soundLengthUnit = 0x4000; // 1 / 256 second of instructions
this.soundLength = 64; // defaults to 64 periods
this.lengthCheck = false;
this.sweepTime = 0; // from 0 to 7
this.sweepStepLength = 0x8000; // 1 / 128 seconds of instructions
this.sweepCount = 0;
this.sweepShifts = 0;
this.sweepSign = 1; // +1 / -1 for increase / decrease freq
this.frequency = 0;
this.envelopeStep = 0;
this.envelopeStepLength = 0x10000;// 1 / 64 seconds of instructions
this.envelopeCheck = false;
this.envelopeSign = 1;
this.clockLength = 0;
this.clockEnvelop = 0;
this.clockSweep = 0;
var gainNode = audioContext.createGain();
gainNode.gain.value = 0;
var oscillator = audioContext.createOscillator();
oscillator.type = 'square';
oscillator.frequency.value = 1000;
oscillator.connect(gainNode);
oscillator.start(0);
this.audioContext = audioContext;
this.gainNode = gainNode;
this.oscillator = oscillator;
};
Channel1.prototype.play = function() {
if (this.playing) return;
this.playing = true;
this.apu.setSoundFlag(this.channelNumber, 1);
this.gainNode.connect(this.audioContext.destination);
this.clockLength = 0;
this.clockEnvelop = 0;
this.clockSweep = 0;
if (this.sweepShifts > 0) this.checkFreqSweep();
};
Channel1.prototype.stop = function() {
this.playing = false;
this.apu.setSoundFlag(this.channelNumber, 0);
this.gainNode.disconnect();
};
Channel1.prototype.checkFreqSweep = function() {
var oldFreq = this.getFrequency();
var newFreq = oldFreq + this.sweepSign * (oldFreq >> this.sweepShifts);
if (newFreq > 0x7FF) {
newFreq = 0;
this.stop();
}
return newFreq;
};
Channel1.prototype.update = function(clockElapsed) {
this.clockEnvelop += clockElapsed;
this.clockSweep += clockElapsed;
if ((this.sweepCount || this.sweepTime) && this.clockSweep > (this.sweepStepLength * this.sweepTime)) {
this.clockSweep -= (this.sweepStepLength * this.sweepTime);
this.sweepCount--;
var newFreq = this.checkFreqSweep(); // process and check new freq
this.apu.memory[0xFF13] = newFreq & 0xFF;
this.apu.memory[0xFF14] &= 0xF8;
this.apu.memory[0xFF14] |= (newFreq & 0x700) >> 8;
this.setFrequency(newFreq);
this.checkFreqSweep(); // check again with new value
}
if (this.envelopeCheck && this.clockEnvelop > this.envelopeStepLength) {
this.clockEnvelop -= this.envelopeStepLength;
this.envelopeStep--;
this.setEnvelopeVolume(this.envelopeVolume + this.envelopeSign);
if (this.envelopeStep <= 0) {
this.envelopeCheck = false;
}
}
if (this.lengthCheck) {
this.clockLength += clockElapsed;
if (this.clockLength > this.soundLengthUnit) {
this.soundLength--;
this.clockLength -= this.soundLengthUnit;
if (this.soundLength == 0) {
this.setLength(0);
this.stop();
}
}
}
};
Channel1.prototype.setFrequency = function(value) {
this.frequency = value;
this.oscillator.frequency.value = 131072 / (2048 - this.frequency);
};
Channel1.prototype.getFrequency = function() {
return this.frequency;
};
Channel1.prototype.setLength = function(value) {
this.soundLength = 64 - (value & 0x3F);
};
Channel1.prototype.setEnvelopeVolume = function(volume) {
this.envelopeCheck = volume > 0 && volume < 16 ? true : false;
this.envelopeVolume = volume;
this.gainNode.gain.value = this.envelopeVolume * 1/100;
};
Channel1.prototype.disable = function() {
this.oscillator.disconnect();
};
Channel1.prototype.enable = function() {
this.oscillator.connect(this.gainNode);
};
GameboyJS.Channel1 = Channel1;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
var Channel3 = function(apu, channelNumber, audioContext) {
this.apu = apu;
this.channelNumber = channelNumber;
this.playing = false;
this.soundLength = 0;
this.soundLengthUnit = 0x4000; // 1 / 256 second of instructions
this.lengthCheck = false;
this.clockLength = 0;
this.buffer = new Float32Array(32);
var gainNode = audioContext.createGain();
gainNode.gain.value = 1;
this.gainNode = gainNode;
this.baseSpeed = 65536;
var waveBuffer = audioContext.createBuffer(1, 32, this.baseSpeed);
var bufferSource = audioContext.createBufferSource();
bufferSource.buffer = waveBuffer;
bufferSource.loop = true;
bufferSource.connect(gainNode);
bufferSource.start(0);
this.audioContext = audioContext;
this.waveBuffer = waveBuffer;
this.bufferSource = bufferSource;
};
Channel3.prototype.play = function() {
if (this.playing) return;
this.playing = true;
this.apu.setSoundFlag(this.channelNumber, 1);
this.waveBuffer.copyToChannel(this.buffer, 0, 0);
this.gainNode.connect(this.audioContext.destination);
this.clockLength = 0;
};
Channel3.prototype.stop = function() {
this.playing = false;
this.apu.setSoundFlag(this.channelNumber, 0);
this.gainNode.disconnect();
};
Channel3.prototype.update = function(clockElapsed) {
if (this.lengthCheck){
this.clockLength += clockElapsed;
if (this.clockLength > this.soundLengthUnit) {
this.soundLength--;
this.clockLength -= this.soundLengthUnit;
if (this.soundLength == 0) {
this.setLength(0);
this.stop();
}
}
}
};
Channel3.prototype.setFrequency = function(value) {
value = 65536 / (2048 - value);
this.bufferSource.playbackRate.value = value / this.baseSpeed;
};
Channel3.prototype.getFrequency = function() {
var freq = 2048 - 65536 / (this.bufferSource.playbackRate.value * this.baseSpeed);
return freq | 1;
};
Channel3.prototype.setLength = function(value) {
this.soundLength = 256 - value;
};
Channel3.prototype.setWaveBufferByte = function(index, value) {
var bufferIndex = index * 2;
this.buffer[bufferIndex] = (value >> 4) / 8 - 1; // value in buffer is in -1 -> 1
this.buffer[bufferIndex+1] = (value & 0x0F) / 8 - 1;
};
Channel3.prototype.disable = function() {
this.bufferSource.disconnect();
};
Channel3.prototype.enable = function() {
this.bufferSource.connect(this.gainNode);
};
GameboyJS.Channel3 = Channel3;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
var Channel4 = function(apu, channelNumber, audioContext) {
this.apu = apu;
this.channelNumber = channelNumber;
this.playing = false;
this.soundLengthUnit = 0x4000; // 1 / 256 second of instructions
this.soundLength = 64; // defaults to 64 periods
this.lengthCheck = false;
this.clockLength = 0;
this.audioContext = audioContext;
};
Channel4.prototype.play = function() {
if (this.playing) return;
this.playing = true;
this.apu.setSoundFlag(this.channelNumber, 1);
this.clockLength = 0;
};
Channel4.prototype.stop = function() {
this.playing = false;
this.apu.setSoundFlag(this.channelNumber, 0);
};
Channel4.prototype.update = function(clockElapsed) {
if (this.lengthCheck) {
this.clockLength += clockElapsed;
if (this.clockLength > this.soundLengthUnit) {
this.soundLength--;
this.clockLength -= this.soundLengthUnit;
if (this.soundLength == 0) {
this.setLength(0);
this.stop();
}
}
}
};
Channel4.prototype.setLength = function(value) {
this.soundLength = 64 - (value & 0x3F);
};
GameboyJS.Channel4 = Channel4;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
var Timer = function(cpu, memory) {
this.cpu = cpu;
this.memory = memory;
this.DIV = 0xFF04;
this.TIMA = 0xFF05;
this.TMA = 0xFF06;
this.TAC = 0xFF07;
this.mainTime = 0;
this.divTime = 0;
};
Timer.prototype.update = function(clockElapsed) {
this.updateDiv(clockElapsed);
this.updateTimer(clockElapsed);
};
Timer.prototype.updateTimer = function(clockElapsed) {
if (!(this.memory.rb(this.TAC) & 0x4)) {
return;
}
this.mainTime += clockElapsed;
var threshold = 64;
switch (this.memory.rb(this.TAC) & 3) {
case 0: threshold=64; break; // 4KHz
case 1: threshold=1; break; // 256KHz
case 2: threshold=4; break; // 64KHz
case 3: threshold=16; break; // 16KHz
}
threshold *= 16;
while (this.mainTime >= threshold) {
this.mainTime -= threshold;
this.memory.wb(this.TIMA, this.memory.rb(this.TIMA) + 1);
if (this.memory.rb(this.TIMA) > 0xFF) {
this.memory.wb(this.TIMA, this.memory.rb(this.TMA));
this.cpu.requestInterrupt(GameboyJS.CPU.INTERRUPTS.TIMER);
}
}
};
// Update the DIV register internal clock
// Increment it if the clock threshold is elapsed and
// reset it if its value overflows
Timer.prototype.updateDiv = function(clockElapsed) {
var divThreshold = 256; // DIV is 16KHz
this.divTime += clockElapsed;
if (this.divTime > divThreshold) {
this.divTime -= divThreshold;
var div = this.memory.rb(this.DIV) + 1;
this.memory.wb(this.DIV, div&0xFF);
}
};
Timer.prototype.resetDiv = function() {
this.divTime = 0;
this.memory[this.DIV] = 0; // direct write to avoid looping
};
GameboyJS.Timer = Timer;
}(GameboyJS || (GameboyJS = {})));
var GameboyJS;
(function (GameboyJS) {
"use strict";
// Utility functions
var Util = {
// Add to the first argument the properties of all other arguments
extend: function(target /*, source1, source2, etc. */) {
var sources = Array.prototype.slice.call(arguments);
for (var i in sources) {
var source = sources[i];
for (var name in source) {
target[name] = source[name];
}
}
return target;
},
testFlag: function(p, cc) {
var test=1;
var mask=0x10;
if (cc=='NZ'||cc=='NC') test=0;
if (cc=='NZ'||cc=='Z') mask=0x80;
return (test && p.r.F&mask) || (!test && !(p.r.F&mask));
},
getRegAddr: function(p, r1, r2) {return Util.makeword(p.r[r1], p.r[r2]);},
// make a 16 bits word from 2 bytes
makeword: function(b1, b2) {return (b1 << 8) + b2;},
// return the integer signed value of a given byte
getSignedValue: function(v) {return v & 0x80 ? v-256 : v;},
// extract a bit from a byte
readBit: function(byte, index) {
return (byte >> index) & 1;
}
};
GameboyJS.Util = Util;
}(GameboyJS || (GameboyJS = {})));
================================================
FILE: package.json
================================================
{
"name": "gameboy-js",
"author": "Julien Chichignoud",
"description": "JavaScript Gameboy emulator",
"repository": {
"type": "git",
"url": "https://github.com/juchi/gameboy.js"
},
"homepage": "https://juchi.github.io/gameboy.js/",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"ts-loader": "^9.5.2",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1"
}
}
================================================
FILE: src/cpu.ts
================================================
import Memory from './memory';
import Timer from './timer';
import APU from './sound/apu';
import Screen from './display/screen';
import GPU from './display/gpu';
import Util from './util';
import { ConsoleSerial, SerialInterface } from './serial'
import {cpuOps} from './instructions'
import {opcodeMap} from './opcodes'
// CPU class
class CPU {
gameboy;
r;
clock;
gpu: GPU;
apu: APU;
input;
timer: Timer;
memory: Memory;
IME = false;
isHalted = false;
isPaused = false;
usingBootRom = false;
SERIAL_INTERNAL_INSTR = 512; // instr to wait per bit if internal clock
enableSerial = 0;
serialHandler: SerialInterface;
nextFrameTimer: ReturnType<typeof setTimeout>;
constructor(gameboy) {
this.gameboy = gameboy;
this.r = {A:0, F: 0, B:0, C:0, D:0, E:0, H:0, L:0, pc:0, sp:0};
this.clock = {c: 0, serial: 0};
this.createDevices();
}
static INTERRUPTS = {
VBLANK: 0,
LCDC: 1,
TIMER: 2,
SERIAL: 3,
HILO: 4
};
static interruptRoutines = {
0: function(p){cpuOps.RSTn(p, 0x40);},
1: function(p){cpuOps.RSTn(p, 0x48);},
2: function(p){cpuOps.RSTn(p, 0x50);},
3: function(p){cpuOps.RSTn(p, 0x58);},
4: function(p){cpuOps.RSTn(p, 0x60);}
}
createDevices() {
this.memory = new Memory(this);
this.timer = new Timer(this, this.memory);
this.apu = new APU(this.memory);
this.enableSerial = 0;
this.serialHandler = new ConsoleSerial;
}
reset() {
this.memory.reset();
this.r = {A:0x01, F: 0, B:0xFF, C:0x13, D:0, E:0xC1, H:0x84, L:0x03, pc:0, sp:0xFFFE};
}
loadRom(data) {
this.memory.setRomData(data);
}
getRamSize() {
let size = 0;
switch (this.memory.rb(0x149)) {
case 1:
size = 2048;
break;
case 2:
size = 2048 * 4;
break;
case 3:
size = 2048 * 16;
break;
}
return size;
}
getGameName() {
var name = '';
for (var i = 0x134; i < 0x143; i++) {
var char = this.memory.rb(i) || 32;
name += String.fromCharCode(char);
}
return name;
}
// Start the execution of the emulator
run() {
if (this.usingBootRom) {
this.r.pc = 0x0000;
} else {
this.r.pc = 0x0100;
}
this.frame();
}
stop() {
clearTimeout(this.nextFrameTimer);
}
// Fetch-and-execute loop
// Will execute instructions for the duration of a frame
//
// The screen unit will notify the vblank period which
// is considered the end of a frame
//
// The function is called on a regular basis with a timeout
frame() {
if (!this.isPaused) {
this.nextFrameTimer = setTimeout(this.frame.bind(this), 1000 / Screen.physics.FREQUENCY);
}
try {
var vblank = false;
while (!vblank) {
var oldInstrCount = this.clock.c;
if (!this.isHalted) {
let opcode = this.fetchOpcode();
opcodeMap[opcode](this);
this.r.F &= 0xF0; // tmp fix
if (this.enableSerial) {
var instr = this.clock.c - oldInstrCount;
this.clock.serial += instr;
if (this.clock.serial >= 8 * this.SERIAL_INTERNAL_INSTR) {
this.endSerialTransfer();
}
}
} else {
this.clock.c += 4;
}
var elapsed = this.clock.c - oldInstrCount;
vblank = this.gpu.update(elapsed);
this.timer.update(elapsed);
this.input.update();
this.apu.update(elapsed);
this.checkInterrupt();
}
this.clock.c = 0;
} catch (e) {
this.gameboy.handleException(e);
}
}
fetchOpcode(): number {
let opcode = this.memory.rb(this.r.pc++);
if (!opcodeMap[opcode]) {
this.stop();
throw 'Unknown opcode '+opcode.toString(16)+' at address '+(this.r.pc-1).toString(16)+', stopping execution...';
}
return opcode;
}
// read register
rr(register) {
return this.r[register];
}
// write register
wr(register, value) {
this.r[register] = value;
}
halt() {
this.isHalted = true;
}
unhalt() {
this.isHalted = false;
}
pause() {
this.isPaused = true;
}
unpause() {
if (this.isPaused) {
this.isPaused = false;
this.frame();
}
}
// Look for interrupt flags
checkInterrupt() {
if (!this.IME) {
return;
}
for (var i = 0; i < 5 && this.IME; i++) {
var IFval = this.memory.rb(0xFF0F);
if (Util.readBit(IFval, i) && this.isInterruptEnable(i)) {
IFval &= (0xFF - (1<<i));
this.memory.wb(0xFF0F, IFval);
this.disableInterrupts();
this.clock.c += 4; // 20 clocks to serve interrupt, with 16 for RSTn
CPU.interruptRoutines[i](this);
}
}
}
// Set an interrupt flag
requestInterrupt(type) {
var IFval = this.memory.rb(0xFF0F);
IFval |= (1 << type)
this.memory.wb(0xFF0F, IFval) ;
this.unhalt();
}
isInterruptEnable(type) {
return Util.readBit(this.memory.rb(0xFFFF), type) != 0;
}
enableInterrupts() {
this.IME = true;
}
disableInterrupts() {
this.IME = false;
}
enableSerialTransfer() {
this.enableSerial = 1;
this.clock.serial = 0;
}
endSerialTransfer() {
this.enableSerial = 0;
var data = this.memory.rb(0xFF01);
this.memory.wb(0xFF02, 0);
this.serialHandler.out(data);
this.memory.wb(0xFF01, this.serialHandler.in());
}
resetDivTimer() {
this.timer.resetDiv();
}
}
export default CPU;
================================================
FILE: src/debug.ts
================================================
import {Gameboy} from './main';
import Util from './util';
let Debug: any = {};
// Output a range of 16 memory addresses
Debug.view_memory = function(addr: number, gameboy: Gameboy): string {
let memory = gameboy.cpu.memory;
addr = addr & 0xFFF0;
let pad = '00';
let str = addr.toString(16) + ':';
for (let i = addr; i < addr + 0x10; i++) {
if ((i & 0x1) == 0) {
str += ' ';
}
let val: number = memory[i] || 0;
let stringVal = val.toString(16)
str += pad.substring(stringVal.length) + stringVal;
}
return str;
};
Debug.view_tile = function(gameboy: Gameboy, index: number, dataStart?: number): void {
let memory = gameboy.cpu.memory;
let gpu = gameboy.cpu.gpu;
let LCDC = memory.deviceram(gpu.LCDC);
if (typeof dataStart === 'undefined') {
dataStart = 0x8000;
if (!Util.readBit(LCDC, 4)) {
dataStart = 0x8800;
index = Util.getSignedValue(index) + 128;
}
}
let tileData = gameboy.cpu.gpu.readTileData(index, dataStart);
let pixelData = new Array(8 * 8)
for (let line = 0; line < 8; line++) {
let b1 = tileData.shift();
let b2 = tileData.shift();
for (let pixel = 0; pixel < 8; pixel++) {
let mask = (1 << (7-pixel));
let colorValue = ((b1 & mask) >> (7-pixel)) + ((b2 & mask) >> (7-pixel))*2;
pixelData[line * 8 + pixel] = colorValue;
}
}
let i = 0;
while (pixelData.length) {
console.log(i++ + ' ' + pixelData.splice(0, 8).join(''));
}
};
Debug.list_visible_sprites = function(gameboy: Gameboy) {
let memory = gameboy.cpu.memory;
let indexes: Array<object> = [];
for (let i = 0xFE00; i < 0xFE9F; i += 4) {
let x = memory.oamram(i + 1);
let y = memory.oamram(i);
let tileIndex = memory.oamram(i + 2);
if (x == 0 || x >= 168) {
continue;
}
indexes.push({oamIndex:i, x:x, y:y, tileIndex:tileIndex});
}
return indexes;
};
export default Debug;
================================================
FILE: src/display/gpu.ts
================================================
import Screen from './screen';
import CPU from '../cpu';
import Util from '../util';
class GPU {
LCDC= 0xFF40;
STAT= 0xFF41;
SCY = 0xFF42;
SCX = 0xFF43;
LY = 0xFF44;
LYC = 0xFF45;
BGP = 0xFF47;
OBP0= 0xFF48;
OBP1= 0xFF49;
WY = 0xFF4A;
WX = 0xFF4B;
OAM_START = 0xFE00;
OAM_END = 0xFE9F;
VBLANK_TIME = 70224;
cpu: CPU;
screen: Screen;
vram: Function;
deviceram: Function;
oamram: Function;
clock: number;
mode: number;
line: number;
buffer: number[];
tileBuffer: number[];
constructor(screen, cpu) {
this.cpu = cpu;
this.screen = screen;
this.vram = cpu.memory.vram.bind(cpu.memory);
this.deviceram = cpu.memory.deviceram.bind(cpu.memory);
this.oamram = cpu.memory.oamram.bind(cpu.memory);
this.clock = 0;
this.mode = 2;
this.line = 0;
this.buffer = new Array(Screen.physics.WIDTH * Screen.physics.HEIGHT);
this.tileBuffer = new Array(8);
}
static tilemap = {
HEIGHT: 32,
WIDTH: 32,
START_0: 0x9800,
START_1: 0x9C00,
LENGTH: 0x0400 // 1024 bytes = 32*32
};
update(clockElapsed) {
this.clock += clockElapsed;
var vblank = false;
switch (this.mode) {
case 0: // HBLANK
if (this.clock >= 204) {
this.clock -= 204;
this.line++;
this.updateLY();
if (this.line == 144) {
this.setMode(1);
vblank = true;
this.cpu.requestInterrupt(CPU.INTERRUPTS.VBLANK);
this.drawFrame();
} else {
this.setMode(2);
}
}
break;
case 1: // VBLANK
if (this.clock >= 456) {
this.clock -= 456;
this.line++;
if (this.line > 153) {
this.line = 0;
this.setMode(2);
}
this.updateLY();
}
break;
case 2: // SCANLINE OAM
if (this.clock >= 80) {
this.clock -= 80;
this.setMode(3);
}
break;
case 3: // SCANLINE VRAM
if (this.clock >= 172) {
this.clock -= 172;
this.drawScanLine(this.line);
this.setMode(0);
}
break;
}
return vblank;
}
updateLY() {
this.deviceram(this.LY, this.line);
var STAT = this.deviceram(this.STAT);
if (this.deviceram(this.LY) == this.deviceram(this.LYC)) {
this.deviceram(this.STAT, STAT | (1 << 2));
if (STAT & (1 << 6)) {
this.cpu.requestInterrupt(CPU.INTERRUPTS.LCDC);
}
} else {
this.deviceram(this.STAT, STAT & (0xFF - (1 << 2)));
}
}
setMode(mode) {
this.mode = mode;
var newSTAT = this.deviceram(this.STAT);
newSTAT &= 0xFC;
newSTAT |= mode;
this.deviceram(this.STAT, newSTAT);
if (mode < 3) {
if (newSTAT & (1 << (3+mode))) {
this.cpu.requestInterrupt(CPU.INTERRUPTS.LCDC);
}
}
}
// Push one scanline into the main buffer
drawScanLine(line) {
var LCDC = this.deviceram(this.LCDC);
var enable = Util.readBit(LCDC, 7);
if (enable) {
var lineBuffer = new Array(Screen.physics.WIDTH);
this.drawBackground(LCDC, line, lineBuffer);
this.drawSprites(LCDC, line, lineBuffer);
// TODO draw a line for the window here too
}
}
drawFrame() {
var LCDC = this.deviceram(this.LCDC);
var enable = Util.readBit(LCDC, 7);
if (enable) {
//this.drawSprites(LCDC);
this.drawWindow(LCDC);
}
this.screen.render(this.buffer);
}
drawBackground(LCDC, line, lineBuffer) {
if (!Util.readBit(LCDC, 0)) {
return;
}
var mapStart = Util.readBit(LCDC, 3) ? GPU.tilemap.START_1 : GPU.tilemap.START_0;
var dataStart, signedIndex = false;
if (Util.readBit(LCDC, 4)) {
dataStart = 0x8000;
} else {
dataStart = 0x8800;
signedIndex = true;
}
var bgx = this.deviceram(this.SCX);
var bgy = this.deviceram(this.SCY);
var tileLine = ((line + bgy) & 7);
// browse BG tilemap for the line to render
var tileRow = ((((bgy + line) / 8) | 0) & 0x1F);
var firstTile = ((bgx / 8) | 0) + 32 * tileRow;
var lastTile = firstTile + Screen.physics.WIDTH / 8 + 1;
if ((lastTile & 0x1F) < (firstTile & 0x1F)) {
lastTile -= 32;
}
var x = (firstTile & 0x1F) * 8 - bgx; // x position of the first tile's leftmost pixel
for (var i = firstTile; i != lastTile; i++, (i & 0x1F) == 0 ? i-=32 : null) {
var tileIndex = this.vram(i + mapStart);
if (signedIndex) {
tileIndex = Util.getSignedValue(tileIndex) + 128;
}
var tileData = this.readTileData(tileIndex, dataStart);
this.drawTileLine(tileData, tileLine);
this.copyBGTileLine(lineBuffer, this.tileBuffer, x);
x += 8;
}
this.copyLineToBuffer(lineBuffer, line);
}
// Copy a tile line from a tileBuffer to a line buffer, at a given x position
copyBGTileLine(lineBuffer, tileBuffer, x) {
// copy tile line to buffer
for (var k = 0; k < 8; k++, x++) {
if (x < 0 || x >= Screen.physics.WIDTH) continue;
lineBuffer[x] = tileBuffer[k];
}
}
// Copy a scanline into the main buffer
copyLineToBuffer(lineBuffer, line) {
var bgPalette = GPU.getPalette(this.deviceram(this.BGP));
for (var x = 0; x < Screen.physics.WIDTH; x++) {
var color = lineBuffer[x];
this.drawPixel(x, line, bgPalette[color]);
}
}
// Write a line of a tile (8 pixels) into a buffer array
drawTileLine(tileData, line: number, xflip = 0, yflip = 0) {
var l = yflip ? 7 - line : line;
var byteIndex = l * 2;
var b1 = tileData[byteIndex++];
var b2 = tileData[byteIndex++];
var offset = 8;
for (var pixel = 0; pixel < 8; pixel++) {
offset--;
var mask = (1 << offset);
var colorValue = ((b1 & mask) >> offset) + ((b2 & mask) >> offset)*2;
var p = xflip ? offset : pixel;
this.tileBuffer[p] = colorValue;
}
}
drawSprites(LCDC, line, bgLineBuffer) {
if (!Util.readBit(LCDC, 1)) {
return;
}
var spriteHeight = Util.readBit(LCDC, 2) ? 16 : 8;
var sprites = new Array();
for (var i = this.OAM_START; i < this.OAM_END && sprites.length < 10; i += 4) {
var y = this.oamram(i);
var x = this.oamram(i+1);
var index = this.oamram(i+2);
if (spriteHeight === 16) index = index & 0xFE;
var flags = this.oamram(i+3);
if (y - 16 > line || y - 16 < line - spriteHeight) {
continue;
}
sprites.push({x:x, y:y, index:index, flags:flags})
}
sprites.sort((a, b) => a.x - b.x);
if (sprites.length == 0) return;
// cache object to store read tiles from this frame
var cacheTile = {};
var spriteLineBuffer = new Array(Screen.physics.WIDTH);
for (var i = 0; i < sprites.length; i++) {
var sprite = sprites[i];
var tileLine = line - sprite.y + 16;
var paletteNumber = Util.readBit(sprite.flags, 4);
var xflip = Util.readBit(sprite.flags, 5);
var yflip = Util.readBit(sprite.flags, 6);
var priority = Util.readBit(sprite.flags, 7);
var tileData = cacheTile[sprite.index] || (cacheTile[sprite.index] = this.readTileData(sprite.index, 0x8000, spriteHeight * 2));
this.drawTileLine(tileData, tileLine, xflip, yflip);
this.copySpriteTileLine(spriteLineBuffer, this.tileBuffer, sprite.x - 8, paletteNumber, priority, bgLineBuffer);
}
this.copySpriteLineToBuffer(spriteLineBuffer, line);
}
// Copy a tile line from a tileBuffer to a line buffer, at a given x position
copySpriteTileLine = function(lineBuffer, tileBuffer, x, palette, priority, bgLineBuffer) {
// copy tile line to buffer
for (var k = 0; k < 8; k++, x++) {
if (x < 0 || x >= Screen.physics.WIDTH || tileBuffer[k] == 0) continue;
if (lineBuffer[x]) continue;
if (priority === 1 && bgLineBuffer[x] > 0) {
lineBuffer[x] = {color:0, palette: palette};
continue;
}
lineBuffer[x] = {color:tileBuffer[k], palette: palette};
}
}
// Copy a sprite scanline into the main buffer
copySpriteLineToBuffer(spriteLineBuffer, line) {
var spritePalettes = {};
spritePalettes[0] = GPU.getPalette(this.deviceram(this.OBP0));
spritePalettes[1] = GPU.getPalette(this.deviceram(this.OBP1));
for (var x = 0; x < Screen.physics.WIDTH; x++) {
if (!spriteLineBuffer[x]) continue;
var color = spriteLineBuffer[x].color;
if (color === 0) continue;
var paletteNumber = spriteLineBuffer[x].palette;
this.drawPixel(x, line, spritePalettes[paletteNumber][color]);
}
}
drawTile(tileData, x, y, buffer, bufferWidth, xflip = 0, yflip = 0) {
var byteIndex = 0;
for (var line = 0; line < 8; line++) {
var l = yflip ? 7 - line : line;
var b1 = tileData[byteIndex++];
var b2 = tileData[byteIndex++];
for (var pixel = 0; pixel < 8; pixel++) {
var mask = (1 << (7-pixel));
var colorValue = ((b1 & mask) >> (7-pixel)) + ((b2 & mask) >> (7-pixel))*2;
var p = xflip ? 7 - pixel : pixel;
var bufferIndex = (x + p) + (y + l) * bufferWidth;
buffer[bufferIndex] = colorValue;
}
}
}
// get an array of tile bytes data (16 entries for 8*8px)
readTileData(tileIndex: number, dataStart: number, tileSize?: number) {
tileSize = tileSize || 0x10; // 16 bytes / tile by default (8*8 px)
var tileData = new Array();
var tileAddressStart = dataStart + (tileIndex * 0x10);
for (var i = tileAddressStart; i < tileAddressStart + tileSize; i++) {
tileData.push(this.vram(i));
}
return tileData;
}
drawWindow(LCDC) {
if (!Util.readBit(LCDC, 5)) {
return;
}
var buffer = new Array(256*256);
var mapStart = Util.readBit(LCDC, 6) ? GPU.tilemap.START_1 : GPU.tilemap.START_0;
var dataStart, signedIndex = false;
if (Util.readBit(LCDC, 4)) {
dataStart = 0x8000;
} else {
dataStart = 0x8800;
signedIndex = true;
}
// browse Window tilemap
for (var i = 0; i < GPU.tilemap.LENGTH; i++) {
var tileIndex = this.vram(i + mapStart);
if (signedIndex) {
tileIndex = Util.getSignedValue(tileIndex) + 128;
}
var tileData = this.readTileData(tileIndex, dataStart);
var x = i % GPU.tilemap.WIDTH;
var y = (i / GPU.tilemap.WIDTH) | 0;
this.drawTile(tileData, x * 8, y * 8, buffer, 256);
}
var wx = this.deviceram(this.WX) - 7;
var wy = this.deviceram(this.WY);
for (var x = Math.max(0, -wx); x < Math.min(Screen.physics.WIDTH, Screen.physics.WIDTH - wx); x++) {
for (var y = Math.max(0, -wy); y < Math.min(Screen.physics.HEIGHT, Screen.physics.HEIGHT - wy); y++) {
var color = buffer[(x & 255) + (y & 255) * 256];
this.drawPixel(x + wx, y + wy, color);
}
}
}
drawPixel(x, y, color) {
this.buffer[y * 160 + x] = color;
}
getPixel(x, y) {
return this.buffer[y * 160 + x];
}
// Get the palette mapping from a given palette byte as stored in memory
// A palette will map a tile color to a final palette color index
// used with Screen.colors to get a shade of grey
static getPalette(paletteByte) {
let palette: number[] = [];
for (var i = 0; i < 8; i += 2) {
let shade = (paletteByte & (3 << i)) >> i;
palette.push(shade);
}
return palette;
}
}
export default GPU;
================================================
FILE: src/display/screen.ts
================================================
// Screen device
class Screen {
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
pixelSize: number;
imageData: ImageData;
constructor(canvas: HTMLCanvasElement, pixelSize: number) {
this.context = canvas.getContext('2d') as CanvasRenderingContext2D;
this.canvas = canvas;
this.pixelSize = pixelSize || 1;
this.initImageData();
}
// Palette colors (RGB)
static colors = [
[0xFF, 0xFF, 0xFF],
[0xAA, 0xAA, 0xAA],
[0x55, 0x55, 0x55],
[0x00, 0x00, 0x00]
];
static physics = {
WIDTH : 160,
HEIGHT : 144,
FREQUENCY: 60
};
setPixelSize(pixelSize: number) {
this.pixelSize = pixelSize;
this.initImageData();
}
initImageData() {
this.canvas.width = Screen.physics.WIDTH * this.pixelSize;
this.canvas.height = Screen.physics.HEIGHT * this.pixelSize;
this.imageData = this.context.createImageData(this.canvas.width, this.canvas.height);
for (var i = 0; i < this.imageData.data.length; i++) {
this.imageData.data[i] = 255;
}
}
clearScreen() {
this.context.fillStyle = '#FFF';
this.context.fillRect(0, 0, Screen.physics.WIDTH * this.pixelSize, Screen.physics.HEIGHT * this.pixelSize);
}
fillImageData(buffer) {
for (var y = 0; y < Screen.physics.HEIGHT; y++) {
for (var py = 0; py < this.pixelSize; py++) {
var yOffset = (y * this.pixelSize + py) * this.canvas.width;
for (var x = 0; x < Screen.physics.WIDTH; x++) {
for (var px = 0; px < this.pixelSize; px++) {
var offset = yOffset + (x * this.pixelSize + px);
var v = Screen.colors[buffer[y * Screen.physics.WIDTH + x] | 0];
// set RGB values
this.imageData.data[offset * 4] = v[0];
this.imageData.data[offset * 4 + 1] = v[1];
this.imageData.data[offset * 4 + 2] = v[2];
}
}
}
}
}
render(buffer) {
this.fillImageData(buffer);
this.context.putImageData(this.imageData, 0, 0);
}
}
export default Screen;
================================================
FILE: src/exception.ts
================================================
// This exception should be thrown whenever a critical feature that
// has not been implemented is requested
class UnimplementedException extends Error {
fatal: boolean;
constructor(message: string, fatal?: boolean) {
super();
this.message = message;
if (fatal === undefined) {
fatal = true;
}
this.fatal = fatal || false;
}
}
export default UnimplementedException;
================================================
FILE: src/ext_ram.ts
================================================
// Object for mapping the cartridge RAM
class ExtRam {
gameName: string;
extRam: number[];
ramSize: number;
ramBanksize: number;
ramBank: number;
constructor() {
this.ramSize = 0;
this.ramBank = 0;
}
loadRam(game: string, size: number) {
this.gameName = game;
this.ramSize = size;
this.ramBanksize = this.ramSize >= 0x2000 ? 8192 : 2048;
let key = this.getStorageKey();
let data = localStorage.getItem(key);
if (data == null) {
this.extRam = Array.apply(null, new Array(this.ramSize)).map(function(){return 0;});
} else {
this.extRam = JSON.parse(data);
if (this.extRam.length != size) {
console.error('Found RAM data but not matching expected size.');
}
}
}
setRamBank(bank: number) {
this.ramBank = bank;
}
manageWrite(offset: number, value: number) {
this.extRam[this.ramBank * 8192 + offset] = value;
}
manageRead(offset: number) {
return this.extRam[this.ramBank * 8192 + offset];
}
getStorageKey() {
return this.gameName + '_EXTRAM';
}
// Actually save the RAM in the physical storage (localStorage)
saveRamData() {
localStorage.setItem(this.getStorageKey(), JSON.stringify(this.extRam));
}
}
export default ExtRam;
================================================
FILE: src/input/gamepad.ts
================================================
import {JoypadDevice} from './input';
// This is the default buttons mapping for the Gamepad API
//
// Any other mapping can be provided as a constructor argument of the Gamepad object
// An alternative mapping should be an object with keys being the indexes
// of the gamepad buttons and values the normalized gameboy button names
let standardMapping = {
0: 'A',
1: 'B',
8: 'SELECT',
9: 'START',
12: 'UP',
13: 'DOWN',
14: 'LEFT',
15: 'RIGHT',
};
// Gamepad listener
// Communication layer between the Gamepad API and the Input class
// Any physical controller can be used but the mapping should be provided
// in order to get an optimal layout of the buttons (see above)
class Gamepad implements JoypadDevice {
gamepad: globalThis.Gamepad;
state = {A:0,B:0,START:0,SELECT:0,LEFT:0,RIGHT:0,UP:0,DOWN:0};
pullInterval: ReturnType<typeof setInterval>;
onPress: Function;
onRelease: Function;
buttonMapping: object;
constructor(mapping?: object) {
this.buttonMapping = mapping || standardMapping;
}
// Initialize the keyboard listeners and set up the callbacks
// for button press / release
init(canvas: HTMLElement, onPress: Function, onRelease: Function) {
this.onPress = onPress;
this.onRelease = onRelease;
let self = this;
window.addEventListener('gamepadconnected', function(e) {
self.gamepad = e.gamepad;
self.activatePull();
});
window.addEventListener('gamepaddisconnected', function(e) {
self.deactivatePull();
});
}
activatePull() {
this.deactivatePull();
this.pullInterval = setInterval(this.pullState.bind(this), 100);
}
deactivatePull() {
clearInterval(this.pullInterval);
}
// Check the state of the current gamepad in order to detect any press/release action
pullState() {
for (let index in this.buttonMapping) {
let button = this.buttonMapping[index];
let oldState = this.state[button];
this.state[button] = this.gamepad.buttons[index].pressed;
if (this.state[button] == 1 && oldState == 0) {
this.managePress(button);
} else if (this.state[button] == 0 && oldState == 1) {
this.manageRelease(button);
}
}
}
managePress(key) {
this.onPress(key);
}
manageRelease(key) {
this.onRelease(key);
}
}
export default Gamepad;
================================================
FILE: src/input/input.ts
================================================
import CPU from '../cpu';
import Memory from '../memory';
// The Input management system
//
// The pressKey() and releaseKey() functions should be called by a device class
// like GameboyJS.Keyboard after a physical button trigger event
//
// They rely on the name of the original buttons as parameters (see Input.keys)
export interface JoypadDevice {
init(canvas: HTMLElement, onPress: Function, onRelease: Function): void;
}
class Input {
cpu: CPU;
memory: Memory;
P1: number;
state: number;
interruptQueue: Array<any>;
constructor(cpu: CPU, pad: JoypadDevice, canvas) {
this.cpu = cpu;
this.memory = cpu.memory;
this.P1 = 0xFF00;
this.state = 0;
this.interruptQueue = [];
pad.init(canvas, this.pressKey.bind(this), this.releaseKey.bind(this));
}
pressKey(key) {
this.delayInterrupt(key);
}
releaseKey(key) {
var mask = 0xFF - Input.keys[key];
this.state &= mask;
}
// do not send the interrupt right away, due to the way javascript works :
// the key event fires when no other code is running, meaning when the frame()
// in the GPU has finished rendering. This means the interrupt will always run
// at LY = 144, which prevents the game to generate entropy for the key press actions
//
// the event is stored in a queue which is processed the next time the LY register is
// at the randomly determined value
delayInterrupt(key) {
let ly = (Math.random() * 153) | 0;
this.interruptQueue.push({ly: ly, key: key});
}
update() {
if (this.interruptQueue.length > 0) { // check for interrupt to fire
if (this.interruptQueue[0].ly === this.memory.rb(this.cpu.gpu.LY)) {
let v = this.interruptQueue.shift();
this.state |= Input.keys[v.key];
this.cpu.requestInterrupt(CPU.INTERRUPTS.HILO);
}
}
var value = this.memory.rb(this.P1);
value = ((~value) & 0x30); // invert the value so 1 means 'active'
if (value & 0x10) { // direction keys listened
value |= (this.state & 0x0F);
} else if (value & 0x20) { // action keys listened
value |= ((this.state & 0xF0) >> 4);
} else if ((value & 0x30) === 0) { // no keys listened
value &= 0xF0;
}
value = ((~value) & 0x3F); // invert back
this.memory[this.P1] = value;
}
static keys = {
START: 0x80,
SELECT: 0x40,
B: 0x20,
A: 0x10,
DOWN: 0x08,
UP: 0x04,
LEFT: 0x02,
RIGHT: 0x01
};
}
export default Input;
================================================
FILE: src/input/keyboard.ts
================================================
import {JoypadDevice} from './input';
// Keyboard listener
// Does the mapping between the keyboard and the Input class
class Keyboard implements JoypadDevice {
onPress: Function;
onRelease: Function;
// Initialize the keyboard listeners and set up the callbacks
// for button press / release
init(canvas: HTMLElement, onPress: Function, onRelease: Function) {
this.onPress = onPress;
this.onRelease = onRelease;
if (canvas.getAttribute('tabIndex') === null) {
canvas.setAttribute('tabIndex', '1');
}
let self = this;
canvas.addEventListener('keydown', function(e) {
self.managePress(e.keyCode);
if (e.keyCode !== 9) // only keep Tab active
e.preventDefault();
});
canvas.addEventListener('keyup', function(e) {
self.manageRelease(e.keyCode);
if (e.keyCode !== 9) // only keep Tab active
e.preventDefault();
});
}
managePress(keycode) {
let key = this.translateKey(keycode);
if (key) {
this.onPress(key);
}
}
manageRelease(keycode) {
let key = this.translateKey(keycode);
if (key) {
this.onRelease(key);
}
}
// Transform a keyboard keycode into a key of the Input.keys object
translateKey(keycode) {
let key = '';
switch (keycode) {
case 71: // G
key = 'A';
break;
case 66: // B
key = 'B';
break;
case 72: // H
key = 'START';
break;
case 78: // N
key = 'SELECT';
break;
case 37: // left
key = 'LEFT';
break;
case 38: // up
key = 'UP';
break;
case 39: // right
key = 'RIGHT';
break;
case 40: // down
key = 'DOWN';
break;
}
return key;
}
}
export default Keyboard;
================================================
FILE: src/instructions.ts
================================================
import Util from './util';
import {opcodeCbmap} from './opcodes';
// List of CPU operations
// Most operations have been factorized here to limit code redundancy
//
// How to read operations:
// Uppercase letters qualify the kind of operation (LD = LOAD, INC = INCREMENT, etc.)
// Lowercase letters are used to hint parameters :
// r = register, n = 1 memory byte, sp = sp register,
// a = suffix for memory address, i = bit index
// Example : LDrrar = LOAD operation with two-registers memory address
// as first parameter and one register value as second
//
// Underscore-prefixed functions are here to delegate the logic between similar operations,
// they should not be called from outside
//
// It's up to each operation to update the CPU clock
let ops = {
LDrrnn: function(p, r1, r2) {p.wr(r2, p.memory.rb(p.r.pc));p.wr(r1, p.memory.rb(p.r.pc+1)); p.r.pc+=2;p.clock.c += 12;},
LDrrar: function(p, r1, r2, r3) {ops._LDav(p, Util.getRegAddr(p, r1, r2), p.r[r3]);p.clock.c += 8;},
LDrrra: function(p, r1, r2, r3) {p.wr(r1, p.memory.rb(Util.getRegAddr(p, r2, r3)));p.clock.c += 8;},
LDrn: function(p, r1) {p.wr(r1, p.memory.rb(p.r.pc++));p.clock.c += 8;},
LDrr: function(p, r1, r2) {p.wr(r1, p.r[r2]);p.clock.c += 4;},
LDrar: function(p, r1, r2) {p.memory.wb(p.r[r1]+0xFF00, p.r[r2]);p.clock.c += 8;},
LDrra: function(p, r1, r2) {p.wr(r1, p.memory.rb(p.r[r2]+0xFF00));p.clock.c += 8;},
LDspnn: function(p) {p.wr('sp', (p.memory.rb(p.r.pc + 1) << 8) + p.memory.rb(p.r.pc));p.r.pc+=2;p.clock.c += 12;},
LDsprr: function(p, r1, r2) {p.wr('sp', Util.getRegAddr(p, r1, r2));p.clock.c += 8;},
LDnnar: function(p, r1) {var addr=(p.memory.rb(p.r.pc + 1) << 8) + p.memory.rb(p.r.pc);p.memory.wb(addr,p.r[r1]);p.r.pc+=2; p.clock.c += 16;},
LDrnna: function(p, r1) {var addr=(p.memory.rb(p.r.pc + 1) << 8) + p.memory.rb(p.r.pc);p.wr(r1, p.memory.rb(addr));p.r.pc+=2; p.clock.c += 16;},
LDrrspn:function(p, r1, r2) {var rel = p.memory.rb(p.r.pc++);rel=Util.getSignedValue(rel);var val=p.r.sp + rel;
var c = (p.r.sp&0xFF) + (rel&0xFF) > 0xFF;var h = (p.r.sp & 0xF) + (rel & 0xF) > 0xF;val &= 0xFFFF;
var f = 0; if(h)f|=0x20;if(c)f|=0x10;p.wr('F', f);
p.wr(r1, val >> 8);p.wr(r2, val&0xFF);
p.clock.c+=12;},
LDnnsp: function(p) {var addr = p.memory.rb(p.r.pc++) + (p.memory.rb(p.r.pc++)<<8); ops._LDav(p, addr, p.r.sp & 0xFF);ops._LDav(p, addr+1, p.r.sp >> 8);p.clock.c+=20;},
LDrran: function(p, r1, r2){var addr = Util.getRegAddr(p, r1, r2);ops._LDav(p, addr, p.memory.rb(p.r.pc++));p.clock.c+=12;},
_LDav: function(p, addr, val){p.memory.wb(addr, val);},
LDHnar: function(p, r1){p.memory.wb(0xFF00 + p.memory.rb(p.r.pc++), p.r[r1]);p.clock.c+=12;},
LDHrna: function(p, r1){p.wr(r1, p.memory.rb(0xFF00 + p.memory.rb(p.r.pc++)));p.clock.c+=12;},
INCrr: function(p, r1, r2) {p.wr(r2, (p.r[r2]+1)&0xFF); if (p.r[r2] == 0) p.wr(r1, (p.r[r1]+1)&0xFF);p.clock.c += 8;},
INCrra: function(p, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);var val = (p.memory.rb(addr)+1)&0xFF;var z = val==0;var h=(p.memory.rb(addr)&0xF)+1 > 0xF;
p.memory.wb(addr, val);
p.r.F&=0x10;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80;
p.clock.c+=12;},
INCsp: function(p){p.wr('sp', p.r.sp+1); p.r.sp &= 0xFFFF; p.clock.c+=8;},
INCr: function(p, r1) {var h = ((p.r[r1]&0xF) + 1)&0x10;p.wr(r1, (p.r[r1] + 1)&0xFF);var z = p.r[r1]==0;
p.r.F&=0x10;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80;
p.clock.c += 4;},
DECrr: function(p, r1, r2) {p.wr(r2, (p.r[r2] - 1) & 0xFF); if (p.r[r2] == 0xFF) p.wr(r1, (p.r[r1] - 1)&0xFF);p.clock.c += 8;},
DECsp: function(p){p.wr('sp', p.r.sp-1); p.r.sp &= 0xFFFF; p.clock.c+=8;},
DECr: function(p, r1) {var h = (p.r[r1]&0xF) < 1;p.wr(r1, (p.r[r1] - 1) & 0xFF);var z = p.r[r1]==0;
p.r.F&=0x10;p.r.F|=0x40;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80;
p.clock.c += 4;},
DECrra: function(p, r1, r2){var addr = Util.getRegAddr(p, r1, r2);var val = (p.memory.rb(addr)-1)&0xFF;var z = val==0;var h=(p.memory.rb(addr)&0xF) < 1;
p.memory.wb(addr, val);
p.r.F&=0x10;p.r.F|=0x40;if(h)p.r.F|=0x20;if(z)p.r.F|=0x80;
p.clock.c+=12;},
ADDrr: function(p, r1, r2) {var n = p.r[r2];ops._ADDrn(p, r1, n); p.clock.c += 4;},
ADDrn: function(p, r1) {var n = p.memory.rb(p.r.pc++);ops._ADDrn(p, r1, n); p.clock.c+=8;},
_ADDrn: function(p, r1, n) {var h=((p.r[r1]&0xF)+(n&0xF))&0x10;p.wr(r1, p.r[r1]+n);var c=p.r[r1]&0x100;p.r[r1]&=0xFF;
var f = 0;if (p.r[r1]==0)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.wr('F', f);},
ADDrrrr:function(p, r1, r2, r3, r4) {ops._ADDrrn(p, r1, r2, (p.r[r3]<<8) + p.r[r4]); p.clock.c+=8;},
ADDrrsp:function(p, r1, r2) {ops._ADDrrn(p, r1, r2, p.r.sp); p.clock.c += 8;},
ADDspn: function(p) {var v = p.memory.rb(p.r.pc++);v = Util.getSignedValue(v);
var c = ((p.r.sp&0xFF) + (v&0xFF)) > 0xFF; var h = (p.r.sp & 0xF) + (v&0xF) > 0xF;
var f = 0; if(h)f|=0x20;if(c)f|=0x10;p.wr('F', f);
p.wr('sp', (p.r.sp + v) & 0xFFFF);
p.clock.c+=16;},
_ADDrrn:function(p, r1, r2, n) {var v1 = (p.r[r1]<<8) + p.r[r2];var v2 = n;
var res = v1 + v2;var c = res&0x10000;var h = ((v1&0xFFF) + (v2&0xFFF))&0x1000;var z = p.r.F&0x80;
res&=0xFFFF;p.r[r2]=res&0xFF;res=res>>8;p.r[r1]=res&0xFF;
var f=0;if(z)f|=0x80;if(h)f|=0x20;if(c)f|=0x10;p.r.F=f;},
ADCrr: function(p, r1, r2) {var n = p.r[r2]; ops._ADCrn(p, r1, n); p.clock.c += 4;},
ADCrn: function(p, r1) {var n = p.memory.rb(p.r.pc++); ops._ADCrn(p, r1, n); p.clock.c += 8;},
_ADCrn: function(p, r1, n) {
var c = p.r.F&0x10?1:0;var h=((p.r[r1]&0xF)+(n&0xF)+c)&0x10;
p.wr(r1, p.r[r1]+n+c);c=p.r[r1]&0x100;p.r[r1]&=0xFF;
var f = 0;if (p.r[r1]==0)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.r.F=f;},
ADCrrra:function(p, r1, r2, r3) {var n = p.memory.rb(Util.getRegAddr(p, r2, r3)); ops._ADCrn(p, r1, n); p.clock.c += 8;},
ADDrrra:function(p, r1, r2, r3) {var v = p.memory.rb(Util.getRegAddr(p, r2, r3));var h=((p.r[r1]&0xF)+(v&0xF))&0x10;p.wr(r1, p.r[r1]+v);var c=p.r[r1]&0x100;p.r[r1]&=0xFF;
var f = 0;if (p.r[r1]==0)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.wr('F', f);
p.clock.c += 8;},
SUBr: function(p, r1) {var n = p.r[r1];ops._SUBn(p, n);p.clock.c += 4;},
SUBn: function(p) {var n = p.memory.rb(p.r.pc++);ops._SUBn(p, n);p.clock.c += 8;},
SUBrra: function(p, r1, r2) {var n = p.memory.rb(Util.getRegAddr(p, r1, r2));ops._SUBn(p, n);p.clock.c+=8;},
_SUBn: function(p, n) {var c = p.r.A < n;var h = (p.r.A&0xF) < (n&0xF);
p.wr('A', p.r.A - n);p.r.A&=0xFF; var z = p.r.A==0;
var f = 0x40;if (z)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.wr('F', f);},
SBCn: function(p) {var n = p.memory.rb(p.r.pc++); ops._SBCn(p, n); p.clock.c += 8;},
SBCr: function(p, r1) {var n = p.r[r1]; ops._SBCn(p, n); p.clock.c += 4;},
SBCrra: function(p, r1, r2) {var v = p.memory.rb((p.r[r1] << 8) + p.r[r2]); ops._SBCn(p, v); p.clock.c += 8;},
_SBCn: function(p, n) {var carry = p.r.F&0x10 ? 1 : 0;
var c = p.r.A < n + carry;var h = (p.r.A&0xF) < (n&0xF) + carry;
p.wr('A', p.r.A - n - carry); p.r.A&=0xFF; var z = p.r.A == 0;
var f = 0x40;if (z)f|=0x80;if (h)f|=0x20;if (c)f|=0x10;p.r.F=f;},
ORr: function(p, r1) {p.r.A|=p.r[r1];p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 4;},
ORn: function(p) {p.r.A|=p.memory.rb(p.r.pc++);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;},
ORrra: function(p, r1, r2) {p.r.A|=p.memory.rb((p.r[r1] << 8)+ p.r[r2]);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;},
ANDr: function(p, r1) {p.r.A&=p.r[r1];p.r.F=(p.r.A==0)?0xA0:0x20;p.clock.c += 4;},
ANDn: function(p) {p.r.A&=p.memory.rb(p.r.pc++);p.r.F=(p.r.A==0)?0xA0:0x20;p.clock.c += 8;},
ANDrra: function(p, r1, r2) {p.r.A&=p.memory.rb(Util.getRegAddr(p, r1, r2));p.r.F=(p.r.A==0)?0xA0:0x20;p.clock.c += 8;},
XORr: function(p, r1) {p.r.A^=p.r[r1];p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 4;},
XORn: function(p) {p.r.A^=p.memory.rb(p.r.pc++);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;},
XORrra: function(p, r1, r2) {p.r.A^=p.memory.rb((p.r[r1] << 8)+ p.r[r2]);p.r.F=(p.r.A==0)?0x80:0x00;p.clock.c += 8;},
CPr: function(p, r1) {var n = p.r[r1];ops._CPn(p, n); p.clock.c += 4;},
CPn: function(p) {var n =p.memory.rb(p.r.pc++);ops._CPn(p, n);p.clock.c+=8;},
CPrra: function(p, r1, r2) {var n = p.memory.rb(Util.getRegAddr(p, r1, r2));ops._CPn(p, n);p.clock.c+=8;},
_CPn: function(p, n) {
var c = p.r.A < n;var z = p.r.A == n;var h = (p.r.A&0xF) < (n&0xF);
var f = 0x40;if(z)f+=0x80;if (h)f+=0x20;if (c)f+=0x10;p.r.F=f;},
RRCr: function(p, r1) {p.r.F=0;var out=p.r[r1] & 0x01;if(out)p.r.F|=0x10;p.r[r1]=(p.r[r1]>>1)|(out*0x80);if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
RRCrra: function(p, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);p.r.F=0;var out=p.memory.rb(addr)&0x01;if(out)p.r.F|=0x10;p.memory.wb(addr, (p.memory.rb(addr)>>1)|(out*0x80));if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
RLCr: function(p, r1) {p.r.F=0;var out=p.r[r1]&0x80?1:0;if(out)p.r.F|=0x10;p.r[r1]=((p.r[r1]<<1)+out)&0xFF;if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
RLCrra: function(p, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);p.r.F=0;var out=p.memory.rb(addr)&0x80?1:0;if(out)p.r.F|=0x10;p.memory.wb(addr, ((p.memory.rb(addr)<<1)+out)&0xFF);if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
RLr: function(p, r1) {var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.r[r1]&0x80;out?p.r.F|=0x10:p.r.F&=0xEF;p.r[r1]=((p.r[r1]<<1)+c)&0xFF;if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
RLrra: function(p, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.memory.rb(addr)&0x80;out?p.r.F|=0x10:p.r.F&=0xEF;p.memory.wb(addr,((p.memory.rb(addr)<<1)+c)&0xFF);if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
RRr: function(p, r1) {var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.r[r1]&0x01;out?p.r.F|=0x10:p.r.F&=0xEF;p.r[r1]=(p.r[r1]>>1)|(c*0x80);if(p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
RRrra: function(p, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);var c=(p.r.F&0x10)?1:0;p.r.F=0;var out=p.memory.rb(addr)&0x01;out?p.r.F|=0x10:p.r.F&=0xEF;p.memory.wb(addr,(p.memory.rb(addr)>>1)|(c*0x80));if(p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
SRAr: function(p, r1) {p.r.F = 0;if (p.r[r1]&0x01)p.r.F|=0x10;var msb=p.r[r1]&0x80;p.r[r1]=(p.r[r1]>>1)|msb;if (p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
SRArra: function(p, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);p.r.F = 0;if (p.memory.rb(addr)&0x01)p.r.F|=0x10;var msb=p.memory.rb(addr)&0x80;p.memory.wb(addr, (p.memory.rb(addr)>>1)|msb);if (p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
SLAr: function(p, r1) {p.r.F = 0;if (p.r[r1]&0x80)p.r.F|=0x10;p.r[r1]=(p.r[r1]<<1)&0xFF;if (p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
SLArra: function(p, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);p.r.F = 0;if (p.memory.rb(addr)&0x80)p.r.F|=0x10;p.memory.wb(addr, (p.memory.rb(addr)<<1)&0xFF);if (p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
SRLr: function(p, r1) {p.r.F = 0;if (p.r[r1]&0x01)p.r.F|=0x10;p.r[r1]=p.r[r1]>>1;if (p.r[r1]==0)p.r.F|=0x80;p.clock.c+=4;},
SRLrra: function(p, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);p.r.F = 0;if (p.memory.rb(addr)&0x01)p.r.F|=0x10;p.memory.wb(addr, p.memory.rb(addr)>>1);if (p.memory.rb(addr)==0)p.r.F|=0x80;p.clock.c+=12;},
BITir: function(p, i, r1) {var mask=1<<i;var z=(p.r[r1]&mask)?0:1;var f=p.r.F&0x10;f |= 0x20;if(z)f|=0x80;p.r.F=f;p.clock.c+=4;},
BITirra:function(p, i, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);var mask=1<<i;var z=(p.memory.rb(addr)&mask)?0:1;var f=p.r.F&0x10;f |= 0x20;if(z)f|=0x80;p.r.F=f;p.clock.c+=8;},
SETir: function(p, i, r1) {var mask=1<<i;p.r[r1]|=mask;p.clock.c += 4;},
SETirra:function(p, i, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);var mask=1<<i;p.memory.wb(addr, p.memory.rb(addr)|mask);p.clock.c += 12;},
RESir: function(p, i, r1) {var mask=0xFF - (1<<i);p.r[r1]&=mask;p.clock.c += 4;},
RESirra:function(p, i, r1, r2) {var addr = Util.getRegAddr(p, r1, r2);var mask=0xFF - (1<<i);p.memory.wb(addr, p.memory.rb(addr)&mask);p.clock.c += 12;},
SWAPr: function(p, r1) {p.r[r1] = ops._SWAPn(p, p.r[r1]);p.clock.c+=4;},
SWAPrra:function(p, r1, r2){var addr = (p.r[r1] << 8)+ p.r[r2]; p.memory.wb(addr, ops._SWAPn(p, p.memory.rb(addr))); p.clock.c+=12;},
_SWAPn: function(p, n){p.r.F = n==0?0x80:0;return ((n&0xF0) >> 4) | ((n&0x0F) << 4);},
JPnn: function(p) {p.wr('pc', (p.memory.rb(p.r.pc+1) << 8) + p.memory.rb(p.r.pc));p.clock.c += 16;},
JRccn: function(p, cc) {if (Util.testFlag(p, cc)){var v=p.memory.rb(p.r.pc++);v=Util.getSignedValue(v);p.r.pc += v;p.clock.c+=4;}else{p.r.pc++;}p.clock.c += 8;},
JPccnn: function(p, cc) {if (Util.testFlag(p, cc)){p.wr('pc', (p.memory.rb(p.r.pc+1) << 8) + p.memory.rb(p.r.pc));p.clock.c+=4;}else{p.r.pc+=2;}p.clock.c += 12;},
JPrr: function(p, r1, r2) {p.r.pc = (p.r[r1] << 8) + p.r[r2];p.clock.c += 4;},
JRn: function(p) {var v=p.memory.rb(p.r.pc++);v=Util.getSignedValue(v);p.r.pc += v;p.clock.c += 12;},
PUSHrr: function(p, r1, r2) {p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp, p.r[r1]);p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp, p.r[r2]);p.clock.c+=16;},
POPrr: function(p, r1, r2) {p.wr(r2, p.memory.rb(p.r.sp));p.wr('sp', p.r.sp+1);p.wr(r1, p.memory.rb(p.r.sp));p.wr('sp', p.r.sp+1);p.clock.c+=12;},
RSTn: function(p, n) {p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp,p.r.pc>>8);p.wr('sp', p.r.sp-1);p.memory.wb(p.r.sp,p.r.pc&0xFF);p.r.pc=n;p.clock.c+=16;},
RET: function(p) {p.r.pc = p.memory.rb(p.r.sp);p.wr('sp', p.r.sp+1);p.r.pc+=p.memory.rb(p.r.sp)<<8;p.wr('sp', p.r.sp+1);p.clock.c += 16;},
RETcc: function(p, cc) {if (Util.testFlag(p, cc)){p.r.pc = p.memory.rb(p.r.sp);p.wr('sp', p.r.sp+1);p.r.pc+=p.memory.rb(p.r.sp)<<8;p.wr('sp', p.r.sp+1);p.clock.c+=12;}p.clock.c+=8;},
CALLnn: function(p) {ops._CALLnn(p); p.clock.c+=24;},
CALLccnn:function(p, cc) {if (Util.testFlag(p, cc)){ops._CALLnn(p);p.clock.c+=12;}else{p.r.pc+=2;}p.clock.c+=12; },
_CALLnn:function(p){p.wr('sp', p.r.sp - 1); p.memory.wb(p.r.sp, ((p.r.pc+2)&0xFF00)>>8);
p.wr('sp', p.r.sp - 1); p.memory.wb(p.r.sp, (p.r.pc+2)&0x00FF);
var j=p.memory.rb(p.r.pc)+(p.memory.rb(p.r.pc+1)<<8);p.r.pc=j;},
CPL: function(p) {p.wr('A', (~p.r.A)&0xFF);p.r.F|=0x60,p.clock.c += 4;},
CCF: function(p) {p.r.F&=0x9F;p.r.F&0x10?p.r.F&=0xE0:p.r.F|=0x10;p.clock.c += 4;},
SCF: function(p) {p.r.F&=0x9F;p.r.F|=0x10;p.clock.c+=4;},
DAA: function(p) {
var sub = (p.r.F&0x40) ? 1 : 0; var h = (p.r.F&0x20)?1:0;var c = (p.r.F&0x10)?1:0;
if (sub) {
if (h) {
p.r.A = (p.r.A - 0x6) & 0xFF;
}
if (c) {
p.r.A -= 0x60;
}
} else {
if ((p.r.A&0xF) > 9 || h) {
p.r.A += 0x6;
}
if (p.r.A > 0x9F || c) {
p.r.A += 0x60;
}
}
if (p.r.A&0x100) c = 1;
p.r.A &= 0xFF;
p.r.F &= 0x40;if (p.r.A == 0) p.r.F|=0x80;if (c) p.r.F|=0x10;
p.clock.c += 4;
},
HALT: function(p) {p.halt(); p.clock.c+=4;},
DI: function(p) {p.disableInterrupts();p.clock.c += 4;},
EI: function(p) {p.enableInterrupts();p.clock.c += 4;},
RETI: function(p) {p.enableInterrupts();ops.RET(p);},
CB: function(p) {var opcode = p.memory.rb(p.r.pc++);
opcodeCbmap[opcode](p);
p.clock.c+=4;}
};
export {ops as cpuOps};
================================================
FILE: src/main.ts
================================================
import Rom from './rom/rom';
import RomFileReader from './rom/file_reader';
import RomDropFileReader from './rom/drop_file_reader';
import RomAjaxReader from './rom/ajax_reader';
import Keyboard from './input/keyboard';
import Util from './util';
import CPU from './cpu';
import GPU from './display/gpu';
import Screen from './display/screen';
import Input, { JoypadDevice } from './input/input'
import UnimplementedException from './exception';
import Debug from './debug';
let defaultOptions = {
pad: {class: Keyboard, mapping: null},
zoom: 1,
romReaders: [],
statusContainerId: 'status',
gameNameContainerId: 'game-name',
errorContainerId: 'error'
};
// Gameboy class
//
// This object is the entry point of the application
// Will delegate user actions to the emulated devices
// and provide information where needed
class Gameboy {
options;
cpu: CPU;
screen: Screen;
input: Input;
pad: JoypadDevice;
statusContainer: HTMLElement;
gameNameContainer: HTMLElement;
errorContainer: HTMLElement;
constructor(canvas, options) {
options = options || {};
this.options = Util.extend({}, defaultOptions, options);
var cpu = new CPU(this);
var screen = new Screen(canvas, this.options.zoom);
var gpu = new GPU(screen, cpu);
cpu.gpu = gpu;
var pad = new this.options.pad.class(this.options.pad.mapping);
var input = new Input(cpu, pad, canvas);
cpu.input = input;
this.cpu = cpu;
this.screen = screen;
this.input = input;
this.pad = pad;
this.createRom(this.options.romReaders);
this.statusContainer = document.getElementById(this.options.statusContainerId) || document.createElement('div');
this.gameNameContainer = document.getElementById(this.options.gameNameContainerId) || document.createElement('div');
this.errorContainer = document.getElementById(this.options.errorContainerId) || document.createElement('div');
}
// Create the ROM object and bind one or more readers
createRom(readers: any[]) {
var rom = new Rom(this);
if (readers.length == 0) {
// add the default rom reader
var romReader = new RomFileReader();
rom.addReader(romReader);
} else {
for (var i in readers) {
if (readers.hasOwnProperty(i)) {
rom.addReader(readers[i]);
}
}
}
}
startRom(rom) {
this.errorContainer.classList.add('hide');
this.cpu.reset();
try {
this.cpu.loadRom(rom.data);
this.setStatus('Game Running :');
this.setGameName(this.cpu.getGameName());
this.cpu.run();
this.screen.canvas.focus();
} catch (e) {
this.handleException(e);
}
}
pause(value) {
if (value) {
this.setStatus('Game Paused :');
this.cpu.pause();
} else {
this.setStatus('Game Running :');
this.cpu.unpause();
}
}
error(message) {
this.setStatus('Error during execution');
this.setError('An error occurred during execution:' + message);
this.cpu.stop();
}
setStatus(status) {
this.statusContainer.innerHTML = status;
}
// Display an error message
setError(message) {
this.errorContainer.classList.remove('hide');
this.errorContainer.innerHTML = message;
}
// Display the name of the game running
setGameName(name) {
this.gameNameContainer.innerHTML = name;
}
setSoundEnabled(value) {
if (value) {
this.cpu.apu.connect();
} else {
this.cpu.apu.disconnect();
}
}
setScreenZoom(value) {
this.screen.setPixelSize(value);
}
handleException(e) {
if (e instanceof UnimplementedException) {
if (e.fatal) {
this.error('This cartridge is not supported ('+ e.message +')');
} else {
console.error(e.message);
}
} else {
throw e;
}
}
}
export {
Gameboy,
RomFileReader,
RomDropFileReader,
RomAjaxReader,
Util,
Debug
};
================================================
FILE: src/mbc.ts
================================================
import ExtRam from './ext_ram';
import Memory from './memory';
import UnimplementedException from './exception';
// Memory bank controllers
abstract class MBC {
memory: Memory;
extRam: ExtRam;
constructor(memory: Memory) {
this.memory = memory;
this.extRam = new ExtRam();
}
// Create an MBC instance depending on the type specified in the cartridge
static getMbcInstance(memory, type) {
var instance;
switch (type) {
case 0x00:
instance = new MBC0(memory);
break;
case 0x01: case 0x02: case 0x03:
instance = new MBC1(memory);
break;
case 0x0F: case 0x10: case 0x11: case 0x12: case 0x13:
instance = new MBC3(memory);
break;
case 0x19: case 0x1A: case 0x1B: case 0x1C: case 0x1D: case 0x1E:
instance = new MBC5(memory);
break;
default:
throw new UnimplementedException('MBC type not supported');
}
return instance;
}
}
class MBC1 extends MBC {
romBankNumber = 1;
mode = 0; // mode 0 = ROM, mode 1 = RAM
ramEnabled = true;
loadRam(game, size) {
this.extRam.loadRam(game, size);
}
manageWrite(addr, value) {
switch (addr & 0xF000) {
case 0x0000: case 0x1000: // enable RAM
this.ramEnabled = (value & 0x0A) ? true : false;
if (!this.ramEnabled) {
this.extRam.saveRamData();
}
break;
case 0x2000: case 0x3000: // ROM bank number lower 5 bits
value &= 0x1F;
if (value == 0) value = 1;
var mask = this.mode ? 0 : 0xE0;
this.romBankNumber = (this.romBankNumber & mask) +value;
this.memory.loadRomBank(this.romBankNumber);
break;
case 0x4000: case 0x5000: // RAM bank or high bits ROM
value &= 0x03;
if (this.mode == 0) { // ROM upper bits
this.romBankNumber = (this.romBankNumber&0x1F) | (value << 5);
this.memory.loadRomBank(this.romBankNumber);
} else { // RAM bank
this.extRam.setRamBank(value);
}
break;
case 0x6000: case 0x7000: // ROM / RAM mode
this.mode = value & 1;
break;
case 0xA000: case 0xB000:
this.extRam.manageWrite(addr - 0xA000, value);
break;
}
}
readRam(addr) {
return this.extRam.manageRead(addr - 0xA000);
}
}
class MBC3 extends MBC {
romBankNumber = 1;
ramEnabled = true;
loadRam(game, size) {
this.extRam.loadRam(game, size);
}
manageWrite(addr, value) {
switch (addr & 0xF000) {
case 0x0000: case 0x1000: // enable RAM
this.ramEnabled = (value & 0x0A) ? true : false;
if (!this.ramEnabled) {
this.extRam.saveRamData();
}
break;
case 0x2000: case 0x3000: // ROM bank number
value &= 0x7F;
if (value == 0) value = 1;
this.romBankNumber = value;
this.memory.loadRomBank(this.romBankNumber);
break;
case 0x4000: case 0x5000: // RAM bank
this.extRam.setRamBank(value);
break;
case 0x6000: case 0x7000: // Latch clock data
throw new UnimplementedException('cartridge clock not supported', false);
break;
case 0xA000: case 0xB000:
this.extRam.manageWrite(addr - 0xA000, value);
break;
}
}
readRam(addr) {
return this.extRam.manageRead(addr - 0xA000);
}
}
// declare MBC5 for compatibility with most cartriges
// does not support rumble feature
let MBC5 = MBC3;
// MBC0 exists for consistency and manages the no-MBC cartriges
class MBC0 extends MBC {
manageWrite(addr, value) {
this.memory.loadRomBank(value);
if (addr >= 0xA000 && addr < 0xC000) {
this.extRam.manageWrite(addr - 0xA000, value);
this.extRam.saveRamData();
}
}
readRam(addr) {
return this.extRam.manageRead(addr - 0xA000);
}
loadRam(game, size) {
this.extRam.loadRam(game, size);
}
}
export default MBC;
================================================
FILE: src/memory.ts
================================================
import MBC from './mbc';
import CPU from './cpu';
// Memory unit
class Memory extends Array {
MEM_SIZE = 65536; // 64KB
MBCtype = 0;
banksize = 0x4000;
rom: Uint8Array;
mbc;
cpu: CPU;
constructor(cpu: CPU) {
super();
this.cpu = cpu;
}
static addresses = {
VRAM_START : 0x8000,
VRAM_END : 0x9FFF,
EXTRAM_START : 0xA000,
EXTRAM_END : 0xBFFF,
OAM_START : 0xFE00,
OAM_END : 0xFE9F,
DEVICE_START: 0xFF00,
DEVICE_END: 0xFF7F
};
reset() {
this.length = this.MEM_SIZE;
for (let i = Memory.addresses.VRAM_START; i <= Memory.addresses.VRAM_END; i++) {
this[i] = 0;
}
for (let i = Memory.addresses.DEVICE_START; i <= Memory.addresses.DEVICE_END; i++) {
this[i] = 0;
}
this[0xFFFF] = 0;
this[0xFF47] = 0xFC;
this[0xFF04] = 0x18;
}
setRomData(data: Uint8Array) {
this.rom = data;
this.loadRomBank(0);
this.mbc = MBC.getMbcInstance(this, this[0x147]);
this.loadRomBank(1);
this.mbc.loadRam(this.cpu.getGameName(), this.cpu.getRamSize());
}
loadRomBank(index) {
var start = index ? 0x4000 : 0x0;
var romStart = index * 0x4000;
for (var i = 0; i < this.banksize; i++) {
this[i + start] = this.rom[romStart + i];
}
}
// Video ram accessor
vram(address) {
if (address < Memory.addresses.VRAM_START || address > Memory.addresses.VRAM_END) {
throw 'VRAM access in out of bounds address ' + address;
}
return this[address];
}
// OAM ram accessor
oamram(address) {
if (address < Memory.addresses.OAM_START || address > Memory.addresses.OAM_END) {
throw 'OAMRAM access in out of bounds address ' + address;
}
return this[address];
}
// Device ram accessor
deviceram(address: number, value?: number) {
if (address < Memory.addresses.DEVICE_START || address > Memory.addresses.DEVICE_END) {
throw 'Device RAM access in out of bounds address ' + address;
}
if (typeof value === "undefined") {
return this[address];
} else {
this[address] = value;
}
}
// Memory read proxy function
// Used to centralize memory read access
rb(addr: number): number {
if (addr >= 0xFF10 && addr < 0xFF40) {
var mask = apuMask[addr - 0xFF10];
return this[addr] | mask;
}
if ((addr >= 0xA000 && addr < 0xC000)) {
return this.mbc.readRam(addr);
}
return this[addr];
}
// Memory write proxy function
// Used to centralize memory writes and delegate specific behaviour
// to the correct units
wb(addr: number, value: number) {
if (addr < 0x8000 || (addr >= 0xA000 && addr < 0xC000)) { // MBC
this.mbc.manageWrite(addr, value);
} else if (addr >= 0xFF10 && addr <= 0xFF3F) { // sound registers
this.cpu.apu.manageWrite(addr, value);
} else if (addr == 0xFF00) { // input register
this[addr] = ((this[addr] & 0x0F) | (value & 0x30));
} else {
this[addr] = value;
if ((addr & 0xFF00) == 0xFF00) {
if (addr == 0xFF02) {
if (value & 0x80) {
this.cpu.enableSerialTransfer();
}
}
if (addr == 0xFF04) {
this.cpu.resetDivTimer();
}
if (addr == 0xFF46) { // OAM DMA transfer
this.dmaTransfer(value);
}
}
}
}
// Start a DMA transfer (OAM data from cartrige to RAM)
dmaTransfer(startAddressPrefix) {
var startAddress = (startAddressPrefix << 8);
for (var i = 0; i < 0xA0; i++) {
this[Memory.addresses.OAM_START + i] = this[startAddress + i];
}
}
}
// Bitmasks for audio addresses reads
let apuMask = [
0x80,0x3F,0x00,0xFF,0xBF, // NR10-NR15
0xFF,0x3F,0x00,0xFF,0xBF, // NR20-NR25
0x7F,0xFF,0x9F,0xFF,0xBF, // NR30-NR35
0xFF,0xFF,0x00,0x00,0xBF, // NR40-NR45
0x00,0x00,0x70, // NR50-NR52
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Wave RAM
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
];
export default Memory;
================================================
FILE: src/opcodes.ts
================================================
import {cpuOps as ops} from './instructions';
// Each opcode (0 to 0xFF) is associated to a CPU operation
// CPU operations are implemented separately
// The cbmap object holds operations for CB prefixed opcodes (0xCB00 to 0xCBFF)
// Non-existent opcodes are commented out and marked empty
let map = {
0x00: function(p){p.clock.c += 4;},
0x01: function(p){ops.LDrrnn(p, 'B', 'C');},
0x02: function(p){ops.LDrrar(p, 'B', 'C', 'A');},
0x03: function(p){ops.INCrr(p, 'B', 'C');},
0x04: function(p){ops.INCr(p, 'B');},
0x05: function(p){ops.DECr(p, 'B');},
0x06: function(p){ops.LDrn(p, 'B');},
0x07: function(p){var out=p.r.A & 0x80?1:0; out ? p.r.F=0x10:p.r.F=0; p.wr('A', ((p.r.A<<1)+out)&0xFF);p.clock.c+=4;},
0x08: function(p){ops.LDnnsp(p);},
0x09: function(p){ops.ADDrrrr(p, 'H', 'L', 'B', 'C');},
0x0A: function(p){ops.LDrrra(p, 'A', 'B', 'C');},
0x0B: function(p){ops.DECrr(p, 'B', 'C');},
0x0C: function(p){ops.INCr(p, 'C');},
0x0D: function(p){ops.DECr(p, 'C');},
0x0E: function(p){ops.LDrn(p, 'C');},
0x0F: function(p){var out=p.r.A & 0x01; out ? p.r.F=0x10:p.r.F=0; p.wr('A', (p.r.A>>1)|(out*0x80));p.clock.c+=4;},
0x10: function(p){p.r.pc++;p.clock.c+=4;},
0x11: function(p){ops.LDrrnn(p, 'D', 'E');},
0x12: function(p){ops.LDrrar(p, 'D', 'E', 'A');},
0x13: function(p){ops.INCrr(p, 'D', 'E');},
0x14: function(p){ops.INCr(p, 'D');},
0x15: function(p){ops.DECr(p, 'D');},
0x16: function(p){ops.LDrn(p, 'D');},
0x17: function(p){var c = (p.r.F&0x10)?1:0;var out=p.r.A & 0x80?1:0; out ? p.r.F=0x10:p.r.F=0; p.wr('A',((p.r.A<<1)+c)&0xFF);p.clock.c+=4;},
0x18: function(p){ops.JRn(p);},
0x19: function(p){ops.ADDrrrr(p, 'H', 'L', 'D', 'E');},
0x1A: function(p){ops.LDrrra(p, 'A', 'D', 'E');},
0x1B: function(p){ops.DECrr(p, 'D', 'E');},
0x1C: function(p){ops.INCr(p, 'E');},
0x1D: function(p){ops.DECr(p, 'E');},
0x1E: function(p){ops.LDrn(p, 'E');},
0x1F: function(p){var c = (p.r.F&0x10)?1:0;var out=p.r.A & 0x01; out ? p.r.F=0x10:p.r.F=0; p.wr('A', (p.r.A>>1)|(c*0x80));p.clock.c+=4;},
0x20: function(p){ops.JRccn(p, 'NZ');},
0x21: function(p){ops.LDrrnn(p, 'H', 'L');},
0x22: function(p){ops.LDrrar(p, 'H', 'L', 'A');ops.INCrr(p, 'H', 'L');p.clock.c -= 8;},
0x23: function(p){ops.INCrr(p, 'H', 'L');},
0x24: function(p){ops.INCr(p, 'H');},
0x25: function(p){ops.DECr(p, 'H');},
0x26: function(p){ops.LDrn(p, 'H');},
0x27: function(p){ops.DAA(p);},
0x28: function(p){ops.JRccn(p, 'Z');},
0x29: function(p){ops.ADDrrrr(p, 'H', 'L', 'H', 'L');},
0x2A: function(p){ops.LDrrra(p, 'A', 'H', 'L');ops.INCrr(p, 'H', 'L');p.clock.c -= 8;},
0x2B: function(p){ops.DECrr(p, 'H', 'L');},
0x2C: function(p){ops.INCr(p, 'L');},
0x2D: function(p){ops.DECr(p, 'L');},
0x2E: function(p){ops.LDrn(p, 'L');},
0x2F: function(p){ops.CPL(p);},
0x30: function(p){ops.JRccn(p, 'NC');},
0x31: function(p){ops.LDspnn(p);},
0x32: function(p){ops.LDrrar(p, 'H', 'L', 'A');ops.DECrr(p, 'H', 'L');p.clock.c -= 8;},
0x33: function(p){ops.INCsp(p);},
0x34: function(p){ops.INCrra(p, 'H', 'L');},
0x35: function(p){ops.DECrra(p, 'H', 'L');},
0x36: function(p){ops.LDrran(p, 'H', 'L');},
0x37: function(p){ops.SCF(p);},
0x38: function(p){ops.JRccn(p, 'C');},
0x39: function(p){ops.ADDrrsp(p, 'H', 'L');},
0x3A: function(p){ops.LDrrra(p, 'A', 'H', 'L');ops.DECrr(p, 'H', 'L');p.clock.c -= 8;},
0x3B: function(p){ops.DECsp(p);},
0x3C: function(p){ops.INCr(p, 'A');},
0x3D: function(p){ops.DECr(p, 'A');},
0x3E: function(p){ops.LDrn(p, 'A');},
0x3F: function(p){ops.CCF(p);},
0x40: function(p){ops.LDrr(p, 'B', 'B');},
0x41: function(p){ops.LDrr(p, 'B', 'C');},
0x42: function(p){ops.LDrr(p, 'B', 'D');},
0x43: function(p){ops.LDrr(p, 'B', 'E');},
0x44: function(p){ops.LDrr(p, 'B', 'H');},
0x45: function(p){ops.LDrr(p, 'B', 'L');},
0x46: function(p){ops.LDrrra(p, 'B', 'H', 'L');},
0x47: function(p){ops.LDrr(p, 'B', 'A');},
0x48: function(p){ops.LDrr(p, 'C', 'B');},
0x49: function(p){ops.LDrr(p, 'C', 'C');},
0x4A: function(p){ops.LDrr(p, 'C', 'D');},
0x4B: function(p){ops.LDrr(p, 'C', 'E');},
0x4C: function(p){ops.LDrr(p, 'C', 'H');},
0x4D: function(p){ops.LDrr(p, 'C', 'L');},
0x4E: function(p){ops.LDrrra(p, 'C', 'H', 'L');},
0x4F: function(p){ops.LDrr(p, 'C', 'A');},
0x50: function(p){ops.LDrr(p, 'D', 'B');},
0x51: function(p){ops.LDrr(p, 'D', 'C');},
0x52: function(p){ops.LDrr(p, 'D', 'D');},
0x53: function(p){ops.LDrr(p, 'D', 'E');},
0x54: function(p){ops.LDrr(p, 'D', 'H');},
0x55: function(p){ops.LDrr(p, 'D', 'L');},
0x56: function(p){ops.LDrrra(p, 'D', 'H', 'L');},
0x57: function(p){ops.LDrr(p, 'D', 'A');},
0x58: function(p){ops.LDrr(p, 'E', 'B');},
0x59: function(p){ops.LDrr(p, 'E', 'C');},
0x5A: function(p){ops.LDrr(p, 'E', 'D');},
0x5B: function(p){ops.LDrr(p, 'E', 'E');},
0x5C: function(p){ops.LDrr(p, 'E', 'H');},
0x5D: function(p){ops.LDrr(p, 'E', 'L');},
0x5E: function(p){ops.LDrrra(p, 'E', 'H', 'L');},
0x5F: function(p){ops.LDrr(p, 'E', 'A');},
0x60: function(p){ops.LDrr(p, 'H', 'B');},
0x61: function(p){ops.LDrr(p, 'H', 'C');},
0x62: function(p){ops.LDrr(p, 'H', 'D');},
0x63: function(p){ops.LDrr(p, 'H', 'E');},
0x64: function(p){ops.LDrr(p, 'H', 'H');},
0x65: function(p){ops.LDrr(p, 'H', 'L');},
0x66: function(p){ops.LDrrra(p, 'H', 'H', 'L');},
0x67: function(p){ops.LDrr(p, 'H', 'A');},
0x68: function(p){ops.LDrr(p, 'L', 'B');},
0x69: function(p){ops.LDrr(p, 'L', 'C');},
0x6A: function(p){ops.LDrr(p, 'L', 'D');},
0x6B: function(p){ops.LDrr(p, 'L', 'E');},
0x6C: function(p){ops.LDrr(p, 'L', 'H');},
0x6D: function(p){ops.LDrr(p, 'L', 'L');},
0x6E: function(p){ops.LDrrra(p, 'L', 'H', 'L');},
0x6F: function(p){ops.LDrr(p, 'L', 'A');},
0x70: function(p){ops.LDrrar(p, 'H', 'L', 'B');},
0x71: function(p){ops.LDrrar(p, 'H', 'L', 'C');},
0x72: function(p){ops.LDrrar(p, 'H', 'L', 'D');},
0x73: function(p){ops.LDrrar(p, 'H', 'L', 'E');},
0x74: function(p){ops.LDrrar(p, 'H', 'L', 'H');},
0x75: function(p){ops.LDrrar(p, 'H', 'L', 'L');},
0x76: function(p){ops.HALT(p);},
0x77: function(p){ops.LDrrar(p, 'H', 'L', 'A');},
0x78: function(p){ops.LDrr(p, 'A', 'B');},
0x79: function(p){ops.LDrr(p, 'A', 'C');},
0x7A: function(p){ops.LDrr(p, 'A', 'D');},
0x7B: function(p){ops.LDrr(p, 'A', 'E');},
0x7C: function(p){ops.LDrr(p, 'A', 'H');},
0x7D: function(p){ops.LDrr(p, 'A', 'L');},
0x7E: function(p){ops.LDrrra(p, 'A', 'H', 'L');},
0x7F: function(p){ops.LDrr(p, 'A', 'A');},
0x80: function(p){ops.ADDrr(p, 'A', 'B');},
0x81: function(p){ops.ADDrr(p, 'A', 'C');},
0x82: function(p){ops.ADDrr(p, 'A', 'D');},
0x83: function(p){ops.ADDrr(p, 'A', 'E');},
0x84: function(p){ops.ADDrr(p, 'A', 'H');},
0x85: function(p){ops.ADDrr(p, 'A', 'L');},
0x86: function(p){ops.ADDrrra(p, 'A', 'H', 'L');},
0x87: function(p){ops.ADDrr(p, 'A', 'A');},
0x88: function(p){ops.ADCrr(p, 'A', 'B');},
0x89: function(p){ops.ADCrr(p, 'A', 'C');},
0x8A: function(p){ops.ADCrr(p, 'A', 'D');},
0x8B: function(p){ops.ADCrr(p, 'A', 'E');},
0x8C: function(p){ops.ADCrr(p, 'A', 'H');},
0x8D: function(p){ops.ADCrr(p, 'A', 'L');},
0x8E: function(p){ops.ADCrrra(p, 'A', 'H', 'L');},
0x8F: function(p){ops.ADCrr(p, 'A', 'A');},
0x90: function(p){ops.SUBr(p, 'B');},
0x91: function(p){ops.SUBr(p, 'C');},
0x92: function(p){ops.SUBr(p, 'D');},
0x93: function(p){ops.SUBr(p, 'E');},
0x94: function(p){ops.SUBr(p, 'H');},
0x95: function(p){ops.SUBr(p, 'L');},
0x96: function(p){ops.SUBrra(p, 'H', 'L');},
0x97: function(p){ops.SUBr(p, 'A');},
0x98: function(p){ops.SBCr(p, 'B');},
0x99: function(p){ops.SBCr(p, 'C');},
0x9A: function(p){ops.SBCr(p, 'D');},
0x9B: function(p){ops.SBCr(p, 'E');},
0x9C: function(p){ops.SBCr(p, 'H');},
0x9D: function(p){ops.SBCr(p, 'L');},
0x9E: function(p){ops.SBCrra(p, 'H', 'L');},
0x9F: function(p){ops.SBCr(p, 'A');},
0xA0: function(p){ops.ANDr(p, 'B');},
0xA1: function(p){ops.ANDr(p, 'C');},
0xA2: function(p){ops.ANDr(p, 'D');},
0xA3: function(p){ops.ANDr(p, 'E');},
0xA4: function(p){ops.ANDr(p, 'H');},
0xA5: function(p){ops.ANDr(p, 'L');},
0xA6: function(p){ops.ANDrra(p, 'H', 'L');},
0xA7: function(p){ops.ANDr(p, 'A');},
0xA8: function(p){ops.XORr(p, 'B');},
0xA9: function(p){ops.XORr(p, 'C');},
0xAA: function(p){ops.XORr(p, 'D');},
0xAB: function(p){ops.XORr(p, 'E');},
0xAC: function(p){ops.XORr(p, 'H');},
0xAD: function(p){ops.XORr(p, 'L');},
0xAE: function(p){ops.XORrra(p, 'H', 'L');},
0xAF: function(p){ops.XORr(p, 'A');},
0xB0: function(p){ops.ORr(p, 'B');},
0xB1: function(p){ops.ORr(p, 'C');},
0xB2: function(p){ops.ORr(p, 'D');},
0xB3: function(p){ops.ORr(p, 'E');},
0xB4: function(p){ops.ORr(p, 'H');},
0xB5: function(p){ops.ORr(p, 'L');},
0xB6: function(p){ops.ORrra(p, 'H', 'L');},
0xB7: function(p){ops.ORr(p, 'A');},
0xB8: function(p){ops.CPr(p, 'B');},
0xB9: function(p){ops.CPr(p, 'C');},
0xBA: function(p){ops.CPr(p, 'D');},
0xBB: function(p){ops.CPr(p, 'E');},
0xBC: function(p){ops.CPr(p, 'H');},
0xBD: function(p){ops.CPr(p, 'L');},
0xBE: function(p){ops.CPrra(p, 'H', 'L');},
0xBF: function(p){ops.CPr(p, 'A');},
0xC0: function(p){ops.RETcc(p, 'NZ');},
0xC1: function(p){ops.POPrr(p, 'B', 'C');},
0xC2: function(p){ops.JPccnn(p, 'NZ');},
0xC3: function(p){ops.JPnn(p);},
0xC4: function(p){ops.CALLccnn(p, 'NZ');},
0xC5: function(p){ops.PUSHrr(p, 'B', 'C');},
0xC6: function(p){ops.ADDrn(p, 'A');},
0xC7: function(p){ops.RSTn(p, 0x00);},
0xC8: function(p){ops.RETcc(p, 'Z');},
0xC9: function(p){ops.RET(p);},
0xCA: function(p){ops.JPccnn(p, 'Z');},
0xCB: function(p){ops.CB(p);},
0xCC: function(p){ops.CALLccnn(p, 'Z');},
0xCD: function(p){ops.CALLnn(p);},
0xCE: function(p){ops.ADCrn(p, 'A');},
0xCF: function(p){ops.RSTn(p, 0x08);},
0xD0: function(p){ops.RETcc(p, 'NC');},
0xD1: function(p){ops.POPrr(p, 'D', 'E');},
0xD2: function(p){ops.JPccnn(p, 'NC');},
//0xD3 empty
0xD4: function(p){ops.CALLccnn(p, 'NC');},
0xD5: function(p){ops.PUSHrr(p, 'D', 'E');},
0xD6: function(p){ops.SUBn(p);},
0xD7: function(p){ops.RSTn(p, 0x10);},
0xD8: function(p){ops.RETcc(p, 'C');},
0xD9: function(p){ops.RETI(p);},
0xDA: function(p){ops.JPccnn(p, 'C');},
//0xDB empty
0xDC: function(p){ops.CALLccnn(p, 'C');},
//0xDD empty
0xDE: function(p){ops.SBCn(p);},
0xDF: function(p){ops.RSTn(p, 0x18);},
0xE0: function(p){ops.LDHnar(p, 'A');},
0xE1: function(p){ops.POPrr(p, 'H', 'L');},
0xE2: function(p){ops.LDrar(p, 'C', 'A');},
//0xE3 empty
//0xE4 empty
0xE5: function(p){ops.PUSHrr(p, 'H', 'L');},
0xE6: function(p){ops.ANDn(p);},
0xE7: function(p){ops.RSTn(p, 0x20);},
0xE8: function(p){ops.ADDspn(p);},
0xE9: function(p){ops.JPrr(p, 'H', 'L');},
0xEA: function(p){ops.LDnnar(p, 'A');},
//0xEB empty
//0xEC empty
//0xED empty
0xEE: function(p){ops.XORn(p);},
0xEF: function(p){ops.RSTn(p, 0x28);},
0xF0: function(p){ops.LDHrna(p, 'A');},
0xF1: function(p){ops.POPrr(p, 'A', 'F');},
0xF2: function(p){ops.LDrra(p, 'A', 'C');},
0xF3: function(p){ops.DI(p);},
//0xF4 empty
0xF5: function(p){ops.PUSHrr(p, 'A', 'F');},
0xF6: function(p){ops.ORn(p);},
0xF7: function(p){ops.RSTn(p, 0x30);},
0xF8: function(p){ops.LDrrspn(p, 'H', 'L');},
0xF9: function(p){ops.LDsprr(p, 'H', 'L');},
0xFA: function(p){ops.LDrnna(p, 'A');},
0xFB: function(p){ops.EI(p);},
//0xFC empty
//0xFD empty
0xFE: function(p){ops.CPn(p);},
0xFF: function(p){ops.RSTn(p, 0x38);}
};
var cbmap = {
0x00: function(p){ops.RLCr(p, 'B');},
0x01: function(p){ops.RLCr(p, 'C');},
0x02: function(p){ops.RLCr(p, 'D');},
0x03: function(p){ops.RLCr(p, 'E');},
0x04: function(p){ops.RLCr(p, 'H');},
0x05: function(p){ops.RLCr(p, 'L');},
0x06: function(p){ops.RLCrra(p, 'H', 'L');},
0x07: function(p){ops.RLCr(p, 'A');},
0x08: function(p){ops.RRCr(p, 'B');},
0x09: function(p){ops.RRCr(p, 'C');},
0x0A: function(p){ops.RRCr(p, 'D');},
0x0B: function(p){ops.RRCr(p, 'E');},
0x0C: function(p){ops.RRCr(p, 'H');},
0x0D: function(p){ops.RRCr(p, 'L');},
0x0E: function(p){ops.RRCrra(p, 'H', 'L');},
0x0F: function(p){ops.RRCr(p, 'A');},
0x10: function(p){ops.RLr(p, 'B');},
0x11: function(p){ops.RLr(p, 'C');},
0x12: function(p){ops.RLr(p, 'D');},
0x13: function(p){ops.RLr(p, 'E');},
0x14: function(p){ops.RLr(p, 'H');},
0x15: function(p){ops.RLr(p, 'L');},
0x16: function(p){ops.RLrra(p, 'H', 'L');},
0x17: function(p){ops.RLr(p, 'A');},
0x18: function(p){ops.RRr(p, 'B');},
0x19: function(p){ops.RRr(p, 'C');},
0x1A: function(p){ops.RRr(p, 'D');},
0x1B: function(p){ops.RRr(p, 'E');},
0x1C: function(p){ops.RRr(p, 'H');},
0x1D: function(p){ops.RRr(p, 'L');},
0x1E: function(p){ops.RRrra(p, 'H', 'L');},
0x1F: function(p){ops.RRr(p, 'A');},
0x20: function(p){ops.SLAr(p, 'B');},
0x21: function(p){ops.SLAr(p, 'C');},
0x22: function(p){ops.SLAr(p, 'D');},
0x23: function(p){ops.SLAr(p, 'E');},
0x24: function(p){ops.SLAr(p, 'H');},
0x25: function(p){ops.SLAr(p, 'L');},
0x26: function(p){ops.SLArra(p, 'H', 'L');},
0x27: function(p){ops.SLAr(p, 'A');},
0x28: function(p){ops.SRAr(p, 'B');},
0x29: function(p){ops.SRAr(p, 'C');},
0x2A: function(p){ops.SRAr(p, 'D');},
0x2B: function(p){ops.SRAr(p, 'E');},
0x2C: function(p){ops.SRAr(p, 'H');},
0x2D: function(p){ops.SRAr(p, 'L');},
0x2E: function(p){ops.SRArra(p, 'H', 'L');},
0x2F: function(p){ops.SRAr(p, 'A');},
0x30: function(p){ops.SWAPr(p, 'B');},
0x31: function(p){ops.SWAPr(p, 'C');},
0x32: function(p){ops.SWAPr(p, 'D');},
0x33: function(p){ops.SWAPr(p, 'E');},
0x34: function(p){ops.SWAPr(p, 'H');},
0x35: function(p){ops.SWAPr(p, 'L');},
0x36: function(p){ops.SWAPrra(p, 'H', 'L');},
0x37: function(p){ops.SWAPr(p, 'A');},
0x38: function(p){ops.SRLr(p, 'B');},
0x39: function(p){ops.SRLr(p, 'C');},
0x3A: function(p){ops.SRLr(p, 'D');},
0x3B: function(p){ops.SRLr(p, 'E');},
0x3C: function(p){ops.SRLr(p, 'H');},
0x3D: function(p){ops.SRLr(p, 'L');},
0x3E: function(p){ops.SRLrra(p, 'H', 'L');},
0x3F: function(p){ops.SRLr(p, 'A');},
0x40: function(p){ops.BITir(p, 0, 'B');},
0x41: function(p){ops.BITir(p, 0, 'C');},
0x42: function(p){ops.BITir(p, 0, 'D');},
0x43: function(p){ops.BITir(p, 0, 'E');},
0x44: function(p){ops.BITir(p, 0, 'H');},
0x45: function(p){ops.BITir(p, 0, 'L');},
0x46: function(p){ops.BITirra(p, 0, 'H', 'L');},
0x47: function(p){ops.BITir(p, 0, 'A');},
0x48: function(p){ops.BITir(p, 1, 'B');},
0x49: function(p){ops.BITir(p, 1, 'C');},
0x4A: function(p){ops.BITir(p, 1, 'D');},
0x4B: function(p){ops.BITir(p, 1, 'E');},
0x4C: function(p){ops.BITir(p, 1, 'H');},
0x4D: function(p){ops.BITir(p, 1, 'L');},
0x4E: function(p){ops.BITirra(p, 1, 'H', 'L');},
0x4F: function(p){ops.BITir(p, 1, 'A');},
0x50: function(p){ops.BITir(p, 2, 'B');},
0x51: function(p){ops.BITir(p, 2, 'C');},
0x52: function(p){ops.BITir(p, 2, 'D');},
0x53: function(p){ops.BITir(p, 2, 'E');},
0x54: function(p){ops.BITir(p, 2, 'H');},
0x55: function(p){ops.BITir(p, 2, 'L');},
0x56: function(p){ops.BITirra(p, 2, 'H', 'L');},
0x57: function(p){ops.BITir(p, 2, 'A');},
0x58: function(p){ops.BITir(p, 3, 'B');},
0x59: function(p){ops.BITir(p, 3, 'C');},
0x5A: function(p){ops.BITir(p, 3, 'D');},
0x5B: function(p){ops.BITir(p, 3, 'E');},
0x5C: function(p){ops.BITir(p, 3, 'H');},
0x5D: function(p){ops.BITir(p, 3, 'L');},
0x5E: function(p){ops.BITirra(p, 3, 'H', 'L');},
0x5F: function(p){ops.BITir(p, 3, 'A');},
0x60: function(p){ops.BITir(p, 4, 'B');},
0x61: function(p){ops.BITir(p, 4, 'C');},
0x62: function(p){ops.BITir(p, 4, 'D');},
0x63: function(p){ops.BITir(p, 4, 'E');},
0x64: function(p){ops.BITir(p, 4, 'H');},
0x65: function(p){ops.BITir(p, 4, 'L');},
0x66: function(p){ops.BITirra(p, 4, 'H', 'L');},
0x67: function(p){ops.BITir(p, 4, 'A');},
0x68: function(p){ops.BITir(p, 5, 'B');},
0x69: function(p){ops.BITir(p, 5, 'C');},
0x6A: function(p){ops.BITir(p, 5, 'D');},
0x6B: function(p){ops.BITir(p, 5, 'E');},
0x6C: function(p){ops.BITir(p, 5, 'H');},
0x6D: function(p){ops.BITir(p, 5, 'L');},
0x6E: function(p){ops.BITirra(p, 5, 'H', 'L');},
0x6F: function(p){ops.BITir(p, 5, 'A');},
0x70: function(p){ops.BITir(p, 6, 'B');},
0x71: function(p){ops.BITir(p, 6, 'C');},
0x72: function(p){ops.BITir(p, 6, 'D');},
0x73: function(p){ops.BITir(p, 6, 'E');},
0x74: function(p){ops.BITir(p, 6, 'H');},
0x75: function(p){ops.BITir(p, 6, 'L');},
0x76: function(p){ops.BITirra(p, 6, 'H', 'L');},
0x77: function(p){ops.BITir(p, 6, 'A');},
0x78: function(p){ops.BITir(p, 7, 'B');},
0x79: function(p){ops.BITir(p, 7, 'C');},
0x7A: function(p){ops.BITir(p, 7, 'D');},
0x7B: function(p){ops.BITir(p, 7, 'E');},
0x7C: function(p){ops.BITir(p, 7, 'H');},
0x7D: function(p){ops.BITir(p, 7, 'L');},
0x7E: function(p){ops.BITirra(p, 7, 'H', 'L');},
0x7F: function(p){ops.BITir(p, 7, 'A');},
0x80: function(p){ops.RESir(p, 0, 'B');},
0x81: function(p){ops.RESir(p, 0, 'C');},
0x82: function(p){ops.RESir(p, 0, 'D');},
0x83: function(p){ops.RESir(p, 0, 'E');},
0x84: function(p){ops.RESir(p, 0, 'H');},
0x85: function(p){ops.RESir(p, 0, 'L');},
0x86: function(p){ops.RESirra(p, 0, 'H', 'L');},
0x87: function(p){ops.RESir(p, 0, 'A');},
0x88: function(p){ops.RESir(p, 1, 'B');},
0x89: function(p){ops.RESir(p, 1, 'C');},
0x8A: function(p){ops.RESir(p, 1, 'D');},
0x8B: function(p){ops.RESir(p, 1, 'E');},
0x8C: function(p){ops.RESir(p, 1, 'H');},
0x8D: function(p){ops.RESir(p, 1, 'L');},
0x8E: function(p){ops.RESirra(p, 1, 'H', 'L');},
0x8F: function(p){ops.RESir(p, 1, 'A');},
0x90: function(p){ops.RESir(p, 2, 'B');},
0x91: function(p){ops.RESir(p, 2, 'C');},
0x92: function(p){ops.RESir(p, 2, 'D');},
0x93: function(p){ops.RESir(p, 2, 'E');},
0x94: function(p){ops.RESir(p, 2, 'H');},
0x95: function(p){ops.RESir(p, 2, 'L');},
0x96: function(p){ops.RESirra(p, 2, 'H', 'L');},
0x97: function(p){ops.RESir(p, 2, 'A');},
0x98: function(p){ops.RESir(p, 3, 'B');},
0x99: function(p){ops.RESir(p, 3, 'C');},
0x9A: function(p){ops.RESir(p, 3, 'D');},
0x9B: function(p){ops.RESir(p, 3, 'E');},
0x9C: function(p){ops.RESir(p, 3, 'H');},
0x9D: function(p){ops.RESir(p, 3, 'L');},
0x9E: function(p){ops.RESirra(p, 3, 'H', 'L');},
0x9F: function(p){ops.RESir(p, 3, 'A');},
0xA0: function(p){ops.RESir(p, 4, 'B');},
0xA1: function(p){ops.RESir(p, 4, 'C');},
0xA2: function(p){ops.RESir(p, 4, 'D');},
0xA3: function(p){ops.RESir(p, 4, 'E');},
0xA4: function(p){ops.RESir(p, 4, 'H');},
0xA5: function(p){ops.RESir(p, 4, 'L');},
0xA6: function(p){ops.RESirra(p, 4, 'H', 'L');},
0xA7: function(p){ops.RESir(p, 4, 'A');},
0xA8: function(p){ops.RESir(p, 5, 'B');},
0xA9: function(p){ops.RESir(p, 5, 'C');},
0xAA: function(p){ops.RESir(p, 5, 'D');},
0xAB: function(p){ops.RESir(p, 5, 'E');},
0xAC: function(p){ops.RESir(p, 5, 'H');},
0xAD: function(p){ops.RESir(p, 5, 'L');},
0xAE: function(p){ops.RESirra(p, 5, 'H', 'L');},
0xAF: function(p){ops.RESir(p, 5, 'A');},
0xB0: function(p){ops.RESir(p, 6, 'B');},
0xB1: function(p){ops.RESir(p, 6, 'C');},
0xB2: function(p){ops.RESir(p, 6, 'D');},
0xB3: function(p){ops.RESir(p, 6, 'E');},
0xB4: function(p){ops.RESir(p, 6, 'H');},
0xB5: function(p){ops.RESir(p, 6, 'L');},
0xB6: function(p){ops.RESirra(p, 6, 'H', 'L');},
0xB7: function(p){ops.RESir(p, 6, 'A');},
0xB8: function(p){ops.RESir(p, 7, 'B');},
0xB9: function(p){ops.RESir(p, 7, 'C');},
0xBA: function(p){ops.RESir(p, 7, 'D');},
0xBB: function(p){ops.RESir(p, 7, 'E');},
0xBC: function(p){ops.RESir(p, 7, 'H');},
0xBD: function(p){ops.RESir(p, 7, 'L');},
0xBE: function(p){ops.RESirra(p, 7, 'H', 'L');},
0xBF: function(p){ops.RESir(p, 7, 'A');},
0xC0: function(p){ops.SETir(p, 0, 'B');},
0xC1: function(p){ops.SETir(p, 0, 'C');},
0xC2: function(p){ops.SETir(p, 0, 'D');},
0xC3: function(p){ops.SETir(p, 0, 'E');},
0xC4: function(p){ops.SETir(p, 0, 'H');},
0xC5: function(p){ops.SETir(p, 0, 'L');},
0xC6: function(p){ops.SETirra(p, 0, 'H', 'L');},
0xC7: function(p){ops.SETir(p, 0, 'A');},
0xC8: function(p){ops.SETir(p, 1, 'B');},
0xC9: function(p){ops.SETir(p, 1, 'C');},
0xCA: function(p){ops.SETir(p, 1, 'D');},
0xCB: function(p){ops.SETir(p, 1, 'E');},
0xCC: function(p){ops.SETir(p, 1, 'H');},
0xCD: function(p){ops.SETir(p, 1, 'L');},
0xCE: function(p){ops.SETirra(p, 1, 'H', 'L');},
0xCF: function(p){ops.SETir(p, 1, 'A');},
0xD0: function(p){ops.SETir(p, 2, 'B');},
0xD1: function(p){ops.SETir(p, 2, 'C');},
0xD2: function(p){ops.SETir(p, 2, 'D');},
0xD3: function(p){ops.SETir(p, 2, 'E');},
0xD4: function(p){ops.SETir(p, 2, 'H');},
0xD5: function(p){ops.SETir(p, 2, 'L');},
0xD6: function(p){ops.SETirra(p, 2, 'H', 'L');},
0xD7: function(p){ops.SETir(p, 2, 'A');},
0xD8: function(p){ops.SETir(p, 3, 'B');},
0xD9: function(p){ops.SETir(p, 3, 'C');},
0xDA: function(p){ops.SETir(p, 3, 'D');},
0xDB: function(p){ops.SETir(p, 3, 'E');},
0xDC: function(p){ops.SETir(p, 3, 'H');},
0xDD: function(p){ops.SETir(p, 3, 'L');},
0xDE: function(p){ops.SETirra(p, 3, 'H', 'L');},
0xDF: function(p){ops.SETir(p, 3, 'A');},
0xE0: function(p){ops.SETir(p, 4, 'B');},
0xE1: function(p){ops.SETir(p, 4, 'C');
gitextract_nzcsu177/ ├── .gitignore ├── LICENSE ├── README.md ├── dist/ │ └── gameboy.js ├── package.json ├── src/ │ ├── cpu.ts │ ├── debug.ts │ ├── display/ │ │ ├── gpu.ts │ │ └── screen.ts │ ├── exception.ts │ ├── ext_ram.ts │ ├── input/ │ │ ├── gamepad.ts │ │ ├── input.ts │ │ └── keyboard.ts │ ├── instructions.ts │ ├── main.ts │ ├── mbc.ts │ ├── memory.ts │ ├── opcodes.ts │ ├── rom/ │ │ ├── ajax_reader.ts │ │ ├── drop_file_reader.ts │ │ ├── file_reader.ts │ │ └── rom.ts │ ├── serial.ts │ ├── sound/ │ │ ├── apu.ts │ │ ├── channel.ts │ │ ├── channel1.ts │ │ ├── channel3.ts │ │ └── channel4.ts │ ├── timer.ts │ └── util.ts ├── tsconfig.json └── webpack.config.js
SYMBOL INDEX (194 symbols across 23 files)
FILE: dist/gameboy.js
function loadboot (line 1) | function loadboot(p) {
function UnimplementedException (line 797) | function UnimplementedException(message, fatal) {
function validate (line 2400) | function validate(data) {
FILE: src/cpu.ts
class CPU (line 12) | class CPU {
method constructor (line 32) | constructor(gameboy) {
method createDevices (line 55) | createDevices() {
method reset (line 64) | reset() {
method loadRom (line 69) | loadRom(data) {
method getRamSize (line 73) | getRamSize() {
method getGameName (line 90) | getGameName() {
method run (line 101) | run() {
method stop (line 110) | stop() {
method frame (line 121) | frame() {
method fetchOpcode (line 159) | fetchOpcode(): number {
method rr (line 171) | rr(register) {
method wr (line 176) | wr(register, value) {
method halt (line 180) | halt() {
method unhalt (line 183) | unhalt() {
method pause (line 186) | pause() {
method unpause (line 189) | unpause() {
method checkInterrupt (line 197) | checkInterrupt() {
method requestInterrupt (line 214) | requestInterrupt(type) {
method isInterruptEnable (line 221) | isInterruptEnable(type) {
method enableInterrupts (line 225) | enableInterrupts() {
method disableInterrupts (line 228) | disableInterrupts() {
method enableSerialTransfer (line 232) | enableSerialTransfer() {
method endSerialTransfer (line 237) | endSerialTransfer() {
method resetDivTimer (line 245) | resetDivTimer() {
FILE: src/display/gpu.ts
class GPU (line 5) | class GPU {
method constructor (line 33) | constructor(screen, cpu) {
method update (line 54) | update(clockElapsed) {
method updateLY (line 104) | updateLY() {
method setMode (line 117) | setMode(mode) {
method drawScanLine (line 132) | drawScanLine(line) {
method drawFrame (line 143) | drawFrame() {
method drawBackground (line 153) | drawBackground(LCDC, line, lineBuffer) {
method copyBGTileLine (line 198) | copyBGTileLine(lineBuffer, tileBuffer, x) {
method copyLineToBuffer (line 207) | copyLineToBuffer(lineBuffer, line) {
method drawTileLine (line 217) | drawTileLine(tileData, line: number, xflip = 0, yflip = 0) {
method drawSprites (line 233) | drawSprites(LCDC, line, bgLineBuffer) {
method copySpriteLineToBuffer (line 290) | copySpriteLineToBuffer(spriteLineBuffer, line) {
method drawTile (line 304) | drawTile(tileData, x, y, buffer, bufferWidth, xflip = 0, yflip = 0) {
method readTileData (line 322) | readTileData(tileIndex: number, dataStart: number, tileSize?: number) {
method drawWindow (line 334) | drawWindow(LCDC) {
method drawPixel (line 374) | drawPixel(x, y, color) {
method getPixel (line 378) | getPixel(x, y) {
method getPalette (line 385) | static getPalette(paletteByte) {
FILE: src/display/screen.ts
class Screen (line 2) | class Screen {
method constructor (line 8) | constructor(canvas: HTMLCanvasElement, pixelSize: number) {
method setPixelSize (line 29) | setPixelSize(pixelSize: number) {
method initImageData (line 34) | initImageData() {
method clearScreen (line 43) | clearScreen() {
method fillImageData (line 48) | fillImageData(buffer) {
method render (line 66) | render(buffer) {
FILE: src/exception.ts
class UnimplementedException (line 3) | class UnimplementedException extends Error {
method constructor (line 6) | constructor(message: string, fatal?: boolean) {
FILE: src/ext_ram.ts
class ExtRam (line 2) | class ExtRam {
method constructor (line 9) | constructor() {
method loadRam (line 14) | loadRam(game: string, size: number) {
method setRamBank (line 32) | setRamBank(bank: number) {
method manageWrite (line 36) | manageWrite(offset: number, value: number) {
method manageRead (line 40) | manageRead(offset: number) {
method getStorageKey (line 44) | getStorageKey() {
method saveRamData (line 49) | saveRamData() {
FILE: src/input/gamepad.ts
class Gamepad (line 23) | class Gamepad implements JoypadDevice {
method constructor (line 31) | constructor(mapping?: object) {
method init (line 37) | init(canvas: HTMLElement, onPress: Function, onRelease: Function) {
method activatePull (line 51) | activatePull() {
method deactivatePull (line 56) | deactivatePull() {
method pullState (line 61) | pullState() {
method managePress (line 75) | managePress(key) {
method manageRelease (line 79) | manageRelease(key) {
FILE: src/input/input.ts
type JoypadDevice (line 11) | interface JoypadDevice {
class Input (line 15) | class Input {
method constructor (line 21) | constructor(cpu: CPU, pad: JoypadDevice, canvas) {
method pressKey (line 31) | pressKey(key) {
method releaseKey (line 35) | releaseKey(key) {
method delayInterrupt (line 47) | delayInterrupt(key) {
method update (line 52) | update() {
FILE: src/input/keyboard.ts
class Keyboard (line 5) | class Keyboard implements JoypadDevice {
method init (line 11) | init(canvas: HTMLElement, onPress: Function, onRelease: Function) {
method managePress (line 31) | managePress(keycode) {
method manageRelease (line 38) | manageRelease(keycode) {
method translateKey (line 46) | translateKey(keycode) {
FILE: src/main.ts
class Gameboy (line 28) | class Gameboy {
method constructor (line 39) | constructor(canvas, options) {
method createRom (line 65) | createRom(readers: any[]) {
method startRom (line 80) | startRom(rom) {
method pause (line 94) | pause(value) {
method error (line 104) | error(message) {
method setStatus (line 110) | setStatus(status) {
method setError (line 115) | setError(message) {
method setGameName (line 121) | setGameName(name) {
method setSoundEnabled (line 125) | setSoundEnabled(value) {
method setScreenZoom (line 132) | setScreenZoom(value) {
method handleException (line 135) | handleException(e) {
FILE: src/mbc.ts
method constructor (line 11) | constructor(memory: Memory) {
method getMbcInstance (line 17) | static getMbcInstance(memory, type) {
class MBC1 (line 41) | class MBC1 extends MBC {
method loadRam (line 46) | loadRam(game, size) {
method manageWrite (line 50) | manageWrite(addr, value) {
method readRam (line 82) | readRam(addr) {
class MBC3 (line 87) | class MBC3 extends MBC {
method loadRam (line 91) | loadRam(game, size) {
method manageWrite (line 95) | manageWrite(addr, value) {
method readRam (line 120) | readRam(addr) {
constant MBC5 (line 128) | let MBC5 = MBC3;
class MBC0 (line 131) | class MBC0 extends MBC {
method manageWrite (line 132) | manageWrite(addr, value) {
method readRam (line 139) | readRam(addr) {
method loadRam (line 142) | loadRam(game, size) {
FILE: src/memory.ts
class Memory (line 5) | class Memory extends Array {
method constructor (line 13) | constructor(cpu: CPU) {
method reset (line 32) | reset() {
method setRomData (line 45) | setRomData(data: Uint8Array) {
method loadRomBank (line 53) | loadRomBank(index) {
method vram (line 62) | vram(address) {
method oamram (line 71) | oamram(address) {
method deviceram (line 80) | deviceram(address: number, value?: number) {
method rb (line 94) | rb(addr: number): number {
method wb (line 108) | wb(addr: number, value: number) {
method dmaTransfer (line 134) | dmaTransfer(startAddressPrefix) {
FILE: src/rom/ajax_reader.ts
class RomAjaxReader (line 4) | class RomAjaxReader implements RomReader {
method setCallback (line 9) | setCallback(onLoadCallback: Function) {
method loadFromUrl (line 15) | loadFromUrl(url: string) {
FILE: src/rom/drop_file_reader.ts
class RomDropFileReader (line 4) | class RomDropFileReader implements RomReader {
method constructor (line 8) | constructor(el) {
method setCallback (line 45) | setCallback(onLoadCallback: Function) {
method loadFromFile (line 50) | loadFromFile(file) {
FILE: src/rom/file_reader.ts
class RomFileReader (line 7) | class RomFileReader implements RomReader {
method constructor (line 11) | constructor(el?: HTMLElement) {
method setCallback (line 25) | setCallback(onLoadCallback: Function) {
method loadFromFile (line 30) | loadFromFile(file) {
FILE: src/rom/rom.ts
class Rom (line 1) | class Rom {
method constructor (line 5) | constructor(gameboy, romReader?) {
method addReader (line 12) | addReader(romReader: RomReader) {
function validate (line 27) | function validate(data: Uint8Array) {
type RomReader (line 35) | interface RomReader {
FILE: src/serial.ts
type SerialInterface (line 3) | interface SerialInterface {
class ConsoleSerial (line 13) | class ConsoleSerial implements SerialInterface {
method out (line 16) | out(data: number): void {
method in (line 25) | in(): number {
method print (line 28) | print(): void {
class DummySerial (line 36) | class DummySerial implements SerialInterface {
method out (line 37) | out(): void {}
method in (line 38) | in(): number {
FILE: src/sound/apu.ts
class APU (line 9) | class APU {
method constructor (line 17) | constructor(memory: Memory) {
method connect (line 30) | connect() {
method disconnect (line 36) | disconnect() {
method update (line 44) | update(clockElapsed) {
method setSoundFlag (line 53) | setSoundFlag(channel, value) {
method manageWrite (line 64) | manageWrite(addr, value) {
FILE: src/sound/channel.ts
type AudioChannel (line 3) | interface AudioChannel {
method checkLength (line 27) | checkLength(clockElapsed: number) {
method setDAC (line 41) | protected setDAC(value: boolean) {
FILE: src/sound/channel1.ts
class Channel1 (line 4) | class Channel1 extends AbstractAudioChannel {
method constructor (line 27) | constructor(apu: APU, channelNumber, audioContext) {
method play (line 45) | play() {
method stop (line 56) | stop() {
method updateDAC (line 62) | updateDAC(controlRegister: number): void {
method checkFreqSweep (line 66) | checkFreqSweep() {
method update (line 76) | update(clockElapsed) {
method setFrequency (line 105) | setFrequency(value) {
method getFrequency (line 109) | getFrequency() {
method setLength (line 112) | setLength(value) {
method setEnvelopeVolume (line 115) | setEnvelopeVolume(volume) {
method disable (line 120) | disable() {
method enable (line 123) | enable() {
FILE: src/sound/channel3.ts
class Channel3 (line 4) | class Channel3 extends AbstractAudioChannel {
method constructor (line 14) | constructor(apu: APU, channelNumber, audioContext) {
method play (line 38) | play() {
method stop (line 47) | stop() {
method updateDAC (line 52) | updateDAC(controlRegister) {
method update (line 56) | update(clockElapsed) {
method setFrequency (line 59) | setFrequency(value) {
method getFrequency (line 63) | getFrequency() {
method setLength (line 67) | setLength(value) {
method setWaveBufferByte (line 70) | setWaveBufferByte(index, value) {
method disable (line 76) | disable() {
method enable (line 79) | enable() {
FILE: src/sound/channel4.ts
class Channel4 (line 4) | class Channel4 extends AbstractAudioChannel {
method constructor (line 5) | constructor(apu, channelNumber, audioContext) {
method play (line 13) | play() {
method stop (line 19) | stop() {
method updateDAC (line 23) | updateDAC(controlRegister) {
method update (line 27) | update(clockElapsed) {
method setLength (line 30) | setLength(value) {
FILE: src/timer.ts
class Timer (line 4) | class Timer {
method constructor (line 15) | constructor(cpu: CPU, memory: Memory) {
method update (line 20) | update(clockElapsed: number) {
method updateTimer (line 25) | updateTimer(clockElapsed: number) {
method updateDiv (line 54) | updateDiv(clockElapsed: number) {
method resetDiv (line 64) | resetDiv() {
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (233K chars).
[
{
"path": ".gitignore",
"chars": 13,
"preview": "node_modules\n"
},
{
"path": "LICENSE",
"chars": 1085,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Julien Chichignoud\n\nPermission is hereby granted, free of charge, to any perso"
},
{
"path": "README.md",
"chars": 4011,
"preview": "Gameboy.js\n==========\n\nGameboy.js is a Gameboy emulator written in JavaScript.\n\nIt's a work in progress, see the Feature"
},
{
"path": "dist/gameboy.js",
"chars": 107562,
"preview": "function loadboot(p) {\n var boot = [\n 0x31, 0xFE, 0xFF, 0xAF, 0x21, 0xFF, 0x9F, 0x32, 0xCB, 0x7C, 0x20, 0xFB, "
},
{
"path": "package.json",
"chars": 471,
"preview": "{\n \"name\": \"gameboy-js\",\n \"author\": \"Julien Chichignoud\",\n \"description\": \"JavaScript Gameboy emulator\",\n \"r"
},
{
"path": "src/cpu.ts",
"chars": 6356,
"preview": "import Memory from './memory';\nimport Timer from './timer';\nimport APU from './sound/apu';\nimport Screen from './display"
},
{
"path": "src/debug.ts",
"chars": 2082,
"preview": "import {Gameboy} from './main';\nimport Util from './util';\n\nlet Debug: any = {};\n// Output a range of 16 memory addresse"
},
{
"path": "src/display/gpu.ts",
"chars": 13044,
"preview": "import Screen from './screen';\nimport CPU from '../cpu';\nimport Util from '../util';\n\nclass GPU {\n LCDC= 0xFF40;\n "
},
{
"path": "src/display/screen.ts",
"chars": 2313,
"preview": "// Screen device\nclass Screen {\n canvas: HTMLCanvasElement;\n context: CanvasRenderingContext2D;\n pixelSize: num"
},
{
"path": "src/exception.ts",
"chars": 431,
"preview": "// This exception should be thrown whenever a critical feature that\n// has not been implemented is requested\nclass Unimp"
},
{
"path": "src/ext_ram.ts",
"chars": 1396,
"preview": "// Object for mapping the cartridge RAM\nclass ExtRam {\n gameName: string;\n extRam: number[];\n ramSize: number;\n"
},
{
"path": "src/input/gamepad.ts",
"chars": 2528,
"preview": "import {JoypadDevice} from './input';\n\n// This is the default buttons mapping for the Gamepad API\n//\n// Any other mappin"
},
{
"path": "src/input/input.ts",
"chars": 2711,
"preview": "import CPU from '../cpu';\nimport Memory from '../memory';\n\n// The Input management system\n//\n// The pressKey() and relea"
},
{
"path": "src/input/keyboard.ts",
"chars": 2144,
"preview": "import {JoypadDevice} from './input';\n\n// Keyboard listener\n// Does the mapping between the keyboard and the Input class"
},
{
"path": "src/instructions.ts",
"chars": 15683,
"preview": "import Util from './util';\nimport {opcodeCbmap} from './opcodes';\n\n// List of CPU operations\n// Most operations have bee"
},
{
"path": "src/main.ts",
"chars": 4339,
"preview": "import Rom from './rom/rom';\nimport RomFileReader from './rom/file_reader';\nimport RomDropFileReader from './rom/drop_fi"
},
{
"path": "src/mbc.ts",
"chars": 4560,
"preview": "import ExtRam from './ext_ram';\nimport Memory from './memory';\nimport UnimplementedException from './exception';\n\n// Mem"
},
{
"path": "src/memory.ts",
"chars": 4500,
"preview": "import MBC from './mbc';\nimport CPU from './cpu';\n\n// Memory unit\nclass Memory extends Array {\n MEM_SIZE = 65536; // "
},
{
"path": "src/opcodes.ts",
"chars": 23934,
"preview": "import {cpuOps as ops} from './instructions';\n\n// Each opcode (0 to 0xFF) is associated to a CPU operation\n// CPU operat"
},
{
"path": "src/rom/ajax_reader.ts",
"chars": 985,
"preview": "import { RomReader } from './rom'\n\n// A RomAjaxReader is able to load a file through an AJAX request\nclass RomAjaxReader"
},
{
"path": "src/rom/drop_file_reader.ts",
"chars": 2145,
"preview": "import { RomReader } from './rom'\n\n// A RomDropFileReader is able to load a drag and dropped file\nclass RomDropFileReade"
},
{
"path": "src/rom/file_reader.ts",
"chars": 1442,
"preview": "import { RomReader } from './rom';\n\n// A RomFileReader is able to load a local file from an input element\n//\n// Expects "
},
{
"path": "src/rom/rom.ts",
"chars": 886,
"preview": "class Rom {\n gameboy;\n data: Uint8Array;\n\n constructor(gameboy, romReader?) {\n this.gameboy = gameboy;\n "
},
{
"path": "src/serial.ts",
"chars": 1203,
"preview": "// Handlers for the Serial port of the Gameboy\n\ninterface SerialInterface {\n out(data: number): void;\n in(): numbe"
},
{
"path": "src/sound/apu.ts",
"chars": 7518,
"preview": "import Channel1 from './channel1';\nimport Channel3 from './channel3';\nimport Channel4 from './channel4';\nimport Memory f"
},
{
"path": "src/sound/channel.ts",
"chars": 1318,
"preview": "import APU from './apu'\n\ninterface AudioChannel {\n play(): void;\n stop(): void;\n updateDAC(controlRegister: num"
},
{
"path": "src/sound/channel1.ts",
"chars": 3769,
"preview": "import APU from './apu';\nimport {AbstractAudioChannel} from './channel';\n\nclass Channel1 extends AbstractAudioChannel {\n"
},
{
"path": "src/sound/channel3.ts",
"chars": 2347,
"preview": "import APU from './apu';\nimport {AbstractAudioChannel} from './channel';\n\nclass Channel3 extends AbstractAudioChannel {\n"
},
{
"path": "src/sound/channel4.ts",
"chars": 858,
"preview": "import APU from './apu';\nimport {AbstractAudioChannel} from './channel';\n\nclass Channel4 extends AbstractAudioChannel {\n"
},
{
"path": "src/timer.ts",
"chars": 1903,
"preview": "import CPU from './cpu';\nimport Memory from './memory'\n\nclass Timer {\n DIV = 0xFF04;\n TIMA = 0xFF05;\n TMA = 0"
},
{
"path": "src/util.ts",
"chars": 1238,
"preview": "import CPU from './cpu';\n\n// Utility functions\nlet Util = {\n // Add to the first argument the properties of all other"
},
{
"path": "tsconfig.json",
"chars": 157,
"preview": "{\n \"compilerOptions\": {\n \"outDir\": \"./dist/\",\n \"allowJs\": true,\n \"target\": \"es6\",\n \"moduleResolution\": \"nod"
},
{
"path": "webpack.config.js",
"chars": 490,
"preview": "const path = require('path');\n\nmodule.exports = {\n entry: './src/main.ts',\n module: {\n rules: [\n {\n tes"
}
]
About this extraction
This page contains the full source code of the juchi/gameboy.js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (220.1 KB), approximately 76.7k tokens, and a symbol index with 194 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.