Added Gameboy Emulator

This commit is contained in:
TheEmeraldStarr 2020-10-07 20:19:42 -07:00
parent b17dff81d6
commit 98487c2906
28 changed files with 6063 additions and 1 deletions

26
Gruntfile.js Normal file
View file

@ -0,0 +1,26 @@
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
dist: {
src: ['src/**/*.js'],
dest: 'dist/gameboy.js'
}
},
uglify: {
dist: {
files: {
"dist/gameboy.min.js": ["dist/gameboy.js"]
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['concat', 'uglify']);
};

3038
dist/gameboy.js vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"test": "test", "test": "test",
"start": "node app.js auth.js" "start": "node app.js auth.js ./node_modules/.bin/grunt"
}, },
"keywords": [ "keywords": [
"proxy", "proxy",

232
src/cpu.js Normal file
View file

@ -0,0 +1,232 @@
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 = {})));

73
src/debug.js Normal file
View file

@ -0,0 +1,73 @@
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 = {})));

388
src/display/gpu.js Normal file
View file

@ -0,0 +1,388 @@
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 = {})));

70
src/display/screen.js Normal file
View file

@ -0,0 +1,70 @@
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();
};
// Palette colors (RGB)
Screen.colors = [
[0xFF, 0xFF, 0xFF],
[0xAA, 0xAA, 0xAA],
[0x55, 0x55, 0x55],
[0x00, 0x00, 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);
for (var i = 0; i < this.imageData.data.length; i++) {
this.imageData.data[i] = 255;
}
};
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 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];
}
}
}
}
};
Screen.prototype.render = function(buffer) {
this.fillImageData(buffer);
this.context.putImageData(this.imageData, 0, 0);
};
GameboyJS.Screen = Screen;
}(GameboyJS || (GameboyJS = {})));

16
src/exception.js Normal file
View file

@ -0,0 +1,16 @@
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 = {})));

50
src/ext_ram.js Normal file
View file

@ -0,0 +1,50 @@
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 = {})));

83
src/input/gamepad.js Normal file
View file

@ -0,0 +1,83 @@
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 = {})));

57
src/input/input.js Normal file
View file

@ -0,0 +1,57 @@
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 = {})));

71
src/input/keyboard.js Normal file
View file

@ -0,0 +1,71 @@
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 = {})));

186
src/instructions.js Normal file
View file

@ -0,0 +1,186 @@
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 = {})));

123
src/main.js Normal file
View file

@ -0,0 +1,123 @@
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 = {})));

134
src/mbc.js Normal file
View file

@ -0,0 +1,134 @@
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 = {})));

153
src/memory.js Normal file
View file

@ -0,0 +1,153 @@
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 = {})));

559
src/opcodes.js Normal file
View file

@ -0,0 +1,559 @@
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 = {})));

36
src/rom/ajax_reader.js Normal file
View file

@ -0,0 +1,36 @@
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 = {})));

View file

@ -0,0 +1,65 @@
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();
if (e.target !== self.dropElement) {
return;
}
self.dropElement.classList.add('drag-active');
});
this.dropElement.addEventListener('dragleave', function(e) {
e.preventDefault();
if (e.target !== self.dropElement) {
return;
}
self.dropElement.classList.remove('drag-active');
});
this.dropElement.addEventListener('dragover', function(e) {
e.preventDefault();
self.dropElement.classList.add('drag-active');
});
this.dropElement.addEventListener('drop', function (e) {
self.dropElement.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 = {})));

45
src/rom/file_reader.js Normal file
View file

@ -0,0 +1,45 @@
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 = {})));

35
src/rom/rom.js Normal file
View file

@ -0,0 +1,35 @@
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 = {})));

43
src/serial.js Normal file
View file

@ -0,0 +1,43 @@
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 = {})));

212
src/sound/apu.js Normal file
View file

@ -0,0 +1,212 @@
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;
var 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 = {})));

129
src/sound/channel1.js Normal file
View file

@ -0,0 +1,129 @@
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 = {})));

86
src/sound/channel3.js Normal file
View file

@ -0,0 +1,86 @@
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 = {})));

45
src/sound/channel4.js Normal file
View file

@ -0,0 +1,45 @@
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 = {})));

66
src/timer.js Normal file
View file

@ -0,0 +1,66 @@
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 = {})));

41
src/util.js Normal file
View file

@ -0,0 +1,41 @@
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 = {})));