├── .github └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── demo ├── index.css └── index.ts ├── index.html ├── nodeDemo └── index.ts ├── nodemon.json ├── package-lock.json ├── package.json ├── src ├── CPU.ts ├── GPU.ts ├── GameBoy.ts ├── Joypad.ts ├── LCD.ts ├── MainInstructions.ts ├── ROM.ts ├── StateManager.ts ├── actions.ts ├── audio │ ├── AudioChannel.ts │ ├── AudioController.ts │ ├── AudioDevice.ts │ ├── Resampler.ts │ ├── SquareAudioChannel.ts │ └── noise.worklet.ts ├── bitInstructions.ts ├── cartridge │ ├── Cartridge.ts │ ├── MBC.ts │ ├── MBC1.ts │ ├── MBC2.ts │ ├── MBC3.ts │ ├── MBC5.ts │ ├── MBC7.ts │ ├── RTC.ts │ └── RUMBLE.ts ├── dutyLookup.ts ├── index.ts ├── initialState.ts ├── memory │ ├── Layout.ts │ └── Memory.ts ├── postBootRomState.ts ├── secondaryTickTable.ts ├── settings.ts ├── storages │ ├── LocalStorage.ts │ ├── MemoryStorage.ts │ └── Storage.ts ├── tickTable.ts ├── types │ ├── index.ts │ └── worklet.ts └── util.ts ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: "16.x" 15 | registry-url: "https://registry.npmjs.org" 16 | - run: sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev 17 | - run: npm ci --no-optional 18 | - run: npm run build 19 | - run: npm publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | lcd.png 4 | /roms 5 | /dist 6 | *.log 7 | *.gbc 8 | *.gb 9 | *.zip -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2021 ardean 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsGBC-core 2 | 3 | [![Build Status](https://travis-ci.org/ardean/jsGBC-core.svg?branch=master)](https://travis-ci.org/ardean/jsGBC) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/ardean/jsGBC-core.svg)](https://greenkeeper.io/) 5 | [![NPM Version][npm-image]][downloads-url] 6 | [![NPM Downloads][downloads-image]][downloads-url] 7 | [![License][license-image]][license-url] 8 | 9 | [Demo](https://ardean.github.io/jsGBC/) 10 | 11 | **jsGBC Core Emulator** 12 | 13 | This is just the core emulator. For a desktop emulator look at [jsGBC](https://github.com/ardean/jsGBC) or for an online emulator please check [jsGBC-web](https://github.com/ardean/jsGBC-web). 14 | 15 | ## Related Projects 16 | 17 | - [jsGBC](https://github.com/ardean/jsGBC) 18 | - [jsGBC-web](https://github.com/ardean/jsGBC-web) 19 | 20 | ## License 21 | 22 | [MIT](LICENSE.md) 23 | 24 | Z80 implementation borrowed from [GameBoy-Online](https://github.com/taisel/GameBoy-Online) 25 | 26 | [downloads-image]: https://img.shields.io/npm/dm/jsgbc.svg 27 | [downloads-url]: https://npmjs.org/package/jsgbc 28 | [npm-image]: https://img.shields.io/npm/v/jsgbc.svg 29 | [npm-url]: https://npmjs.org/package/jsgbc 30 | [license-image]: https://img.shields.io/npm/l/jsgbc.svg 31 | [license-url]: LICENSE.md 32 | -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: monospace; 5 | user-select: none; 6 | } 7 | 8 | .loading { 9 | position: fixed; 10 | top: 0; 11 | left: 0; 12 | right: 0; 13 | bottom: 0; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | background-color: #FFF; 19 | z-index: 1; 20 | } 21 | 22 | .rom-select-text, .download-battery-file-text, .upload-battery-file-text { 23 | font-weight: 600; 24 | cursor: pointer; 25 | } 26 | 27 | .lcd { 28 | background-color: grey; 29 | } 30 | 31 | .pixelated { 32 | /* Legal fallback */ 33 | image-rendering: optimizeSpeed; 34 | /* Firefox */ 35 | image-rendering: -moz-crisp-edges; 36 | /* Opera */ 37 | image-rendering: -o-crisp-edges; 38 | /* Safari */ 39 | image-rendering: -webkit-optimize-contrast; 40 | /* CSS3 Proposed */ 41 | image-rendering: optimize-contrast; 42 | /* CSS4 Proposed */ 43 | image-rendering: crisp-edges; 44 | /* CSS4 Proposed */ 45 | image-rendering: pixelated; 46 | /* IE8+ */ 47 | -ms-interpolation-mode: nearest-neighbor; 48 | } 49 | 50 | .hidden-file { 51 | position: relative; 52 | cursor: pointer; 53 | } 54 | 55 | .hidden-file input[type="file"] { 56 | opacity: 0; 57 | -webkit-appearance: none; 58 | display: block; 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | right: 0; 63 | bottom: 0; 64 | width: 100%; 65 | height: 100%; 66 | cursor: pointer; 67 | } -------------------------------------------------------------------------------- /demo/index.ts: -------------------------------------------------------------------------------- 1 | import { GameBoy, util } from "../src"; 2 | 3 | const keyMap = { 4 | "Enter": "Start", 5 | "Shift": "Select", 6 | "ArrowUp": "Up", 7 | "w": "Up", 8 | "ArrowRight": "Right", 9 | "d": "Right", 10 | "ArrowDown": "Down", 11 | "s": "Down", 12 | "ArrowLeft": "Left", 13 | "a": "Left", 14 | "l": "A", 15 | "v": "A", 16 | "x": "B", 17 | "k": "B", 18 | "1": "Save", 19 | "0": "Load", 20 | "p": "Speed" 21 | }; 22 | 23 | const canvas = document.querySelector(".lcd"); 24 | 25 | const gameboy = new GameBoy({ 26 | lcd: { canvas } 27 | }); 28 | 29 | (window as any).gameboy = gameboy; 30 | 31 | window.addEventListener("keydown", ({ key }) => { 32 | if (keyMap[key]) { 33 | gameboy.actionDown(keyMap[key]); 34 | } 35 | }); 36 | window.addEventListener("keyup", ({ key }) => { 37 | if (keyMap[key]) { 38 | gameboy.actionUp(keyMap[key]); 39 | } 40 | }); 41 | 42 | const selectGbcBootRomElement = document.querySelector(".gbc-boot-rom-select"); 43 | selectGbcBootRomElement.addEventListener("change", async () => { 44 | const file = selectGbcBootRomElement.files[0]; 45 | if (!file) return; 46 | 47 | const rom = await util.readFirstMatchingExtension(file, file.name, ["bin"]); 48 | if (!rom) return; 49 | 50 | gameboy.setGbcBootRom(rom); 51 | }); 52 | 53 | const selectGbBootRomElement = document.querySelector(".gb-boot-rom-select"); 54 | selectGbBootRomElement.addEventListener("change", async () => { 55 | const file = selectGbBootRomElement.files[0]; 56 | if (!file) return; 57 | 58 | const rom = await util.readFirstMatchingExtension(file, file.name, ["bin"]); 59 | if (!rom) return; 60 | 61 | gameboy.setGbBootRom(rom); 62 | }); 63 | 64 | const selectRomElement = document.querySelector(".rom-select"); 65 | selectRomElement.addEventListener("change", async () => { 66 | const file = selectRomElement.files[0]; 67 | if (!file) return; 68 | 69 | const rom = await util.readFirstMatchingExtension(file, file.name, ["gbc", "gb"]); 70 | if (!rom) return; 71 | 72 | gameboy.replaceCartridge(rom); 73 | }); 74 | 75 | document 76 | .querySelector(".download-battery-file-text") 77 | .addEventListener("click", () => { 78 | util.saveAs(gameboy.getBatteryFileArrayBuffer(), gameboy.core.cartridge.name + ".sav"); 79 | }); 80 | 81 | const uploadBatteryFileElement = document.querySelector(".upload-battery-file"); 82 | uploadBatteryFileElement.addEventListener("change", async () => { 83 | const battery = await util.readBlob(uploadBatteryFileElement.files[0]); 84 | await gameboy.loadBatteryFileArrayBuffer(battery); 85 | }); 86 | 87 | document.querySelector(".loading").style.display = "none"; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jsGBC Core 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Loading...

13 |
14 | 15 |

Click 16 | 17 | HERE 18 | 19 | to select a GB BOOT ROM 20 |

21 | 22 |

Click 23 | 24 | HERE 25 | 26 | to select a GBC BOOT ROM 27 |

28 | 29 |

Click 30 | 31 | HERE 32 | 33 | to select a ROM 34 |

35 | 36 | 37 | 38 |
39 | Download Battery File 40 |
41 |
42 | 43 | Upload Battery File 44 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /nodeDemo/index.ts: -------------------------------------------------------------------------------- 1 | const { GameBoy } = require("../dist/jsgbc-core"); 2 | const Canvas = require("canvas"); 3 | const fs = require("fs"); 4 | const { AudioContext: NewAudioContext } = require("web-audio-api"); 5 | const Speaker = require("speaker"); 6 | 7 | const context = new NewAudioContext(); 8 | 9 | context.outStream = new Speaker({ 10 | channels: context.format.numberOfChannels, 11 | bitDepth: context.format.bitDepth, 12 | sampleRate: context.sampleRate 13 | }); 14 | 15 | const canvas = new Canvas(); 16 | const gameboy = new GameBoy({ 17 | audio: { context }, 18 | lcd: { 19 | canvas, 20 | offscreenCanvas: new Canvas() 21 | }, 22 | isSoundEnabled: true 23 | }); 24 | 25 | const buffer = fs.readFileSync("rom.gbc"); 26 | gameboy.replaceCartridge(buffer); 27 | 28 | // setTimeout(() => { 29 | // const data = canvas.toDataURL().replace(/^data:image\/png;base64,/, ""); 30 | // fs.writeFileSync("lcd.png", data, "base64"); 31 | // }, 20 * 1000); 32 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "./nodeDemo", 4 | "./src" 5 | ], 6 | "ext": "ts", 7 | "exec": "node -r ts-node/register --max-old-space-size=10240 ./nodeDemo" 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsgbc", 3 | "version": "0.7.0", 4 | "description": "jsGBC Core Emulator", 5 | "main": "./dist/jsgbc-core.js", 6 | "types": "./dist/types/index.d.ts", 7 | "repository": "https://github.com/ardean/jsGBC-core", 8 | "bugs": "https://github.com/ardean/jsGBC-core/issues", 9 | "author": "ardean", 10 | "license": "MIT", 11 | "keywords": [ 12 | "gameboy", 13 | "color", 14 | "emulator", 15 | "gameboy-color", 16 | "gameboy-color-emulator", 17 | "gbc", 18 | "html5", 19 | "html5-canvas", 20 | "canvas", 21 | "jsgbc" 22 | ], 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "start": "cross-env NODE_ENV=development webpack serve --config webpack.dev.js", 28 | "start-node": "node demo-node/index.js", 29 | "build": "cross-env NODE_ENV=production webpack --config webpack.prod.js", 30 | "test": "npm run build" 31 | }, 32 | "dependencies": { 33 | "file-saver": "^2.0.5", 34 | "jszip": "^3.7.1", 35 | "worker-url": "^1.1.0" 36 | }, 37 | "devDependencies": { 38 | "@types/file-saver": "^2.0.4", 39 | "canvas": "^2.8.0", 40 | "clean-webpack-plugin": "^4.0.0", 41 | "cpy-cli": "^3.1.1", 42 | "cross-env": "^7.0.3", 43 | "del-cli": "^4.0.1", 44 | "ts-loader": "^9.2.6", 45 | "typescript": "^4.5.4", 46 | "webpack": "^5.65.0", 47 | "webpack-cli": "^4.9.1", 48 | "webpack-dev-server": "^4.7.2", 49 | "webpack-merge": "^5.8.0", 50 | "webpack-node-externals": "^3.0.0" 51 | }, 52 | "optionalDependencies": { 53 | "speaker": "^0.5.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CPU.ts: -------------------------------------------------------------------------------- 1 | import settings from "./settings"; 2 | 3 | export default class CPU { 4 | speed = 1; 5 | ticks = 0; // Times for how many instructions to execute before ending the loop. 6 | cyclesTotal = 0; // Relative CPU clocking to speed set, rounded appropriately. 7 | cyclesTotalBase = 0; // Relative CPU clocking to speed set base. 8 | cyclesTotalCurrent = 0; // Relative CPU clocking to speed set, the directly used value. 9 | cyclesTotalRoundoff = 0; // Clocking per iteration rounding catch. 10 | baseCyclesPerIteration = 0; // CPU clocks per iteration at 1x speed. 11 | totalLinesPassed = 0; 12 | clocksPerSecond: number; 13 | stopped: boolean = false; 14 | 15 | constructor() { 16 | this.calculateTimings(); 17 | } 18 | 19 | calculateTimings() { 20 | this.clocksPerSecond = this.speed * 0x400000; 21 | this.baseCyclesPerIteration = this.clocksPerSecond / 1000 * settings.runInterval; 22 | this.cyclesTotalRoundoff = this.baseCyclesPerIteration % 4; 23 | this.cyclesTotalBase = this.cyclesTotal = this.baseCyclesPerIteration - this.cyclesTotalRoundoff | 0; 24 | this.cyclesTotalCurrent = 0; 25 | } 26 | 27 | setSpeed(speed: number) { 28 | this.speed = speed; 29 | this.calculateTimings(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Joypad.ts: -------------------------------------------------------------------------------- 1 | import GameBoy from "./GameBoy"; 2 | import { joypadAddress } from "./memory/Layout"; 3 | 4 | const initialValue = 0xF; 5 | 6 | export default class Joypad { 7 | value: number = 0xFF; 8 | 9 | constructor( 10 | private gameboy: GameBoy 11 | ) { } 12 | 13 | init() { 14 | this.gameboy.memory[joypadAddress] = initialValue; 15 | } 16 | 17 | down(key: number) { 18 | this.value &= 0xff ^ 1 << key; 19 | 20 | if ( 21 | this.gameboy.cartridge && 22 | !this.gameboy.cartridge.useGbcMode && 23 | ( 24 | !this.gameboy.usedBootRom || 25 | !this.gameboy.usedGbcBootRom 26 | ) 27 | ) { 28 | this.gameboy.interruptRequestedFlags |= 1 << 4; // A real GBC doesn't set this! 29 | this.gameboy.remainingClocks = 0; 30 | this.gameboy.checkIrqMatching(); 31 | } 32 | 33 | this.writeMemory(joypadAddress, this.gameboy.memory[joypadAddress]); 34 | 35 | this.gameboy.cpu.stopped = false; 36 | } 37 | 38 | up(key: number) { 39 | this.value |= 1 << key; 40 | this.writeMemory(joypadAddress, this.gameboy.memory[joypadAddress]); 41 | 42 | this.gameboy.cpu.stopped = false; 43 | } 44 | 45 | writeMemory = (address: number, data: number) => { 46 | const switchBits = data & 0b110000; 47 | 48 | const hasDirectionKeys = (data & 0x10) === 0; 49 | const directionNibble = this.value & 0xF; 50 | 51 | const hasButtonKeys = (data & 0x20) === 0; 52 | const buttonNibble = (this.value >> 4) & 0xF; 53 | 54 | this.gameboy.memory[address] = ( 55 | switchBits + 56 | ( 57 | ( 58 | hasButtonKeys ? 59 | buttonNibble : 60 | 0xF 61 | ) & 62 | ( 63 | hasDirectionKeys ? 64 | directionNibble : 65 | 0xF 66 | ) 67 | ) 68 | ); 69 | }; 70 | 71 | readMemory = () => ( 72 | 0b11000000 | 73 | this.gameboy.memoryNew.readDirectly(joypadAddress) 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/LCD.ts: -------------------------------------------------------------------------------- 1 | import * as util from "./util"; 2 | import GameBoy from "./GameBoy"; 3 | 4 | export default class LCD { 5 | context: any; 6 | offscreenContext: any; 7 | offscreenWidth: number; 8 | offscreenHeight: number; 9 | offscreenRgbaCount: number; 10 | width: number; 11 | height: number; 12 | swizzledFrame: Uint8Array; // The secondary gfx buffer that holds the converted RGBA values. 13 | canvasBuffer: any; // imageData handle 14 | newFrameAvailable: boolean; 15 | resizer: any; 16 | drewBlank: number; 17 | colorizedGbPalettes: any; 18 | offscreenCanvas: any; 19 | canvas: any; 20 | offscreenRgbCount: number; 21 | 22 | constructor( 23 | private gameboy: GameBoy, 24 | { 25 | canvas, 26 | context, 27 | offscreenCanvas, 28 | offscreenContext, 29 | width, 30 | height 31 | } 32 | ) { 33 | this.canvas = canvas; 34 | this.context = context; 35 | this.offscreenCanvas = offscreenCanvas; 36 | this.offscreenContext = offscreenContext; 37 | this.gameboy = gameboy; 38 | this.offscreenWidth = 160; 39 | this.offscreenHeight = 144; 40 | this.offscreenRgbCount = this.offscreenWidth * this.offscreenHeight * 3; 41 | this.offscreenRgbaCount = this.offscreenWidth * this.offscreenHeight * 4; 42 | this.width = width || this.offscreenWidth; 43 | this.height = height || this.offscreenHeight; 44 | 45 | if (typeof document !== "undefined") { 46 | if (!this.canvas) this.canvas = document.createElement("canvas"); 47 | if (!this.offscreenCanvas) this.offscreenCanvas = document.createElement("canvas"); 48 | } 49 | 50 | if (this.canvas) { 51 | this.canvas.height = this.height; 52 | this.canvas.width = this.width; 53 | 54 | if (!this.context) this.context = this.canvas.getContext("2d"); 55 | } 56 | 57 | if (this.offscreenCanvas) { 58 | this.offscreenCanvas.height = this.offscreenHeight; 59 | this.offscreenCanvas.width = this.offscreenWidth; 60 | 61 | if (!this.offscreenContext) this.offscreenContext = this.offscreenCanvas.getContext("2d"); 62 | } 63 | 64 | if (!this.context) { 65 | throw new Error("please provide a canvas context in the lcd options"); 66 | } 67 | 68 | if (!this.offscreenContext) { 69 | throw new Error("please provide a canvas offscreen context in the lcd options"); 70 | } 71 | } 72 | 73 | init() { 74 | this.offscreenContext.msImageSmoothingEnabled = false; 75 | this.offscreenContext.mozImageSmoothingEnabled = false; 76 | this.offscreenContext.webkitImageSmoothingEnabled = false; 77 | this.offscreenContext.imageSmoothingEnabled = false; 78 | 79 | this.context.msImageSmoothingEnabled = false; 80 | this.context.mozImageSmoothingEnabled = false; 81 | this.context.webkitImageSmoothingEnabled = false; 82 | this.context.imageSmoothingEnabled = false; 83 | 84 | this.canvasBuffer = this.offscreenContext.createImageData( 85 | this.offscreenWidth, 86 | this.offscreenHeight 87 | ); 88 | 89 | this.swizzledFrame = util.getTypedArray( 90 | this.offscreenRgbCount, 91 | 0xff, 92 | "uint8" 93 | ) as Uint8Array; 94 | 95 | let index = this.offscreenRgbaCount; 96 | while (index > 0) { 97 | index -= 4; 98 | this.canvasBuffer.data[index] = 0xf8; 99 | this.canvasBuffer.data[index + 1] = 0xf8; 100 | this.canvasBuffer.data[index + 2] = 0xf8; 101 | this.canvasBuffer.data[index + 3] = 0xff; // opacity 102 | } 103 | 104 | // Test the draw system and browser vblank latching: 105 | this.newFrameAvailable = true; 106 | this.draw(); 107 | } 108 | 109 | drawToCanvas() { 110 | if ( 111 | this.offscreenWidth === this.width && 112 | this.offscreenHeight === this.height 113 | ) { 114 | this.context.putImageData(this.canvasBuffer, 0, 0); 115 | } else { 116 | this.offscreenContext.putImageData(this.canvasBuffer, 0, 0); 117 | this.context.drawImage( 118 | this.offscreenCanvas, 119 | 0, 120 | 0, 121 | this.width, 122 | this.height 123 | ); 124 | } 125 | } 126 | 127 | draw() { 128 | if ( 129 | !this.newFrameAvailable || 130 | this.offscreenRgbaCount !== 92160 131 | ) return; 132 | 133 | // We actually updated the graphics internally, so copy out: 134 | const canvasData = this.canvasBuffer.data; 135 | let bufferIndex = 0; 136 | let canvasIndex = 0; 137 | 138 | while (canvasIndex < this.offscreenRgbaCount) { 139 | canvasData[canvasIndex++] = this.swizzledFrame[bufferIndex++]; 140 | canvasData[canvasIndex++] = this.swizzledFrame[bufferIndex++]; 141 | canvasData[canvasIndex++] = this.swizzledFrame[bufferIndex++]; 142 | ++canvasIndex; 143 | } 144 | 145 | this.drawToCanvas(); 146 | this.newFrameAvailable = false; 147 | } 148 | 149 | outputFrameBuffer() { 150 | // Convert our dirty 24-bit (24-bit, with internal render flags above it) framebuffer to an 8-bit buffer with separate indices for the RGB channels: 151 | const frameBuffer = this.gameboy.frameBuffer; 152 | const swizzledFrame = this.swizzledFrame; 153 | let bufferIndex = 0; 154 | let canvasIndex = 0; 155 | while (canvasIndex < this.offscreenRgbCount) { 156 | swizzledFrame[canvasIndex++] = frameBuffer[bufferIndex] >> 16 & 0xff; // red 157 | swizzledFrame[canvasIndex++] = frameBuffer[bufferIndex] >> 8 & 0xff; // green 158 | swizzledFrame[canvasIndex++] = frameBuffer[bufferIndex] & 0xff; // blue 159 | ++bufferIndex; 160 | } 161 | this.newFrameAvailable = true; 162 | } 163 | 164 | turnOff() { 165 | if (this.drewBlank === 0) { 166 | // Output a blank screen to the output framebuffer: 167 | this.clearFrameBuffer(); 168 | this.newFrameAvailable = true; 169 | } 170 | this.drewBlank = 2; 171 | } 172 | 173 | clearFrameBuffer() { 174 | const frameBuffer = this.swizzledFrame; 175 | let bufferIndex = 0; 176 | if ( 177 | this.gameboy.cartridge.useGbcMode || 178 | this.colorizedGbPalettes 179 | ) { 180 | while (bufferIndex < this.offscreenRgbCount) { 181 | frameBuffer[bufferIndex++] = 248; 182 | } 183 | } else { 184 | while (bufferIndex < this.offscreenRgbCount) { 185 | frameBuffer[bufferIndex++] = 239; 186 | frameBuffer[bufferIndex++] = 255; 187 | frameBuffer[bufferIndex++] = 222; 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/ROM.ts: -------------------------------------------------------------------------------- 1 | export default class ROM { 2 | data: Uint8Array; 3 | 4 | constructor(data: Uint8Array | ArrayBuffer) { 5 | if (data instanceof Uint8Array) { 6 | this.data = data; 7 | } else { 8 | this.data = new Uint8Array(data); 9 | } 10 | } 11 | 12 | getByte(index: number): number { 13 | return this.data[index]; 14 | } 15 | 16 | getChar(index: number): string { 17 | return String.fromCharCode(this.getByte(index)); 18 | } 19 | 20 | getString(from: number, to: number): string { 21 | let text = ""; 22 | for (let index = from; index <= to; index++) { 23 | text += this.getChar(index); 24 | } 25 | 26 | return text; 27 | } 28 | 29 | get length() { 30 | return this.data.byteLength; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/StateManager.ts: -------------------------------------------------------------------------------- 1 | import GameBoy from "./GameBoy"; 2 | import initialState from "./initialState"; 3 | import { toTypedArray, concatArrayBuffers } from "./util"; 4 | 5 | export default class StateManager { 6 | constructor( 7 | private gameboy: GameBoy 8 | ) { } 9 | 10 | init() { 11 | this.loadOld(initialState.slice(0)); 12 | } 13 | 14 | get() { 15 | const gameboy = this.gameboy; 16 | if (!gameboy.cartridge) return null; 17 | 18 | return concatArrayBuffers( 19 | gameboy.memory.buffer.slice(0), 20 | gameboy.videoRam.buffer.slice(0) 21 | ); 22 | 23 | // return [ 24 | // gameboy.isBootingRom, 25 | // gameboy.registerA, 26 | // gameboy.FZero, 27 | // gameboy.FSubtract, 28 | // gameboy.FHalfCarry, 29 | // gameboy.FCarry, 30 | // gameboy.registerB, 31 | // gameboy.registerC, 32 | // gameboy.registerD, 33 | // gameboy.registerE, 34 | // gameboy.registersHL, 35 | // gameboy.stackPointer, 36 | // gameboy.programCounter, 37 | // gameboy.halt, 38 | // gameboy.IME, 39 | // gameboy.hdmaRunning, 40 | // gameboy.currentInstructionCycleCount, 41 | // gameboy.doubleSpeedShifter, 42 | // // fromTypedArray(gameboy.memory), 43 | // // fromTypedArray(gameboy.videoRam), 44 | // gameboy.currVRAMBank, 45 | // fromTypedArray(gameboy.GBCMemory), 46 | // gameboy.gbcRamBank, 47 | // gameboy.gbcRamBankPosition, 48 | // gameboy.ROMBank1Offset, 49 | // gameboy.cartridge.mbc.currentROMBank, 50 | // gameboy.modeSTAT, 51 | // gameboy.LYCMatchTriggerSTAT, 52 | // gameboy.mode2TriggerSTAT, 53 | // gameboy.mode1TriggerSTAT, 54 | // gameboy.mode0TriggerSTAT, 55 | // gameboy.gpu.lcdEnabled, 56 | // gameboy.gfxWindowCHRBankPosition, 57 | // gameboy.gfxWindowDisplay, 58 | // gameboy.gfxSpriteShow, 59 | // gameboy.gfxSpriteNormalHeight, 60 | // gameboy.gfxBackgroundCHRBankPosition, 61 | // gameboy.gfxBackgroundBankOffset, 62 | // gameboy.TIMAEnabled, 63 | // gameboy.DIVTicks, 64 | // gameboy.LCDTicks, 65 | // gameboy.timerTicks, 66 | // gameboy.TACClocker, 67 | // gameboy.serialTimer, 68 | // gameboy.serialShiftTimer, 69 | // gameboy.serialShiftTimerAllocated, 70 | // gameboy.IRQEnableDelay, 71 | // gameboy.cartridge.hasRTC && 72 | // gameboy.cartridge.mbc3.rtc.lastTime, 73 | // gameboy.drewBlank, 74 | // fromTypedArray(gameboy.frameBuffer), 75 | // gameboy.bgEnabled, 76 | // gameboy.audioController.channel1FrequencyTracker, 77 | // gameboy.channel1FrequencyCounter, 78 | // gameboy.channel1totalLength, 79 | // gameboy.channel1EnvelopeVolume, 80 | // gameboy.channel1EnvelopeType, 81 | // gameboy.channel1EnvelopeSweeps, 82 | // gameboy.channel1EnvelopeSweepsLast, 83 | // gameboy.channel1consecutive, 84 | // gameboy.channel1frequency, 85 | // gameboy.channel1SweepFault, 86 | // gameboy.channel1ShadowFrequency, 87 | // gameboy.channel1timeSweep, 88 | // gameboy.channel1lastTimeSweep, 89 | // gameboy.channel1Swept, 90 | // gameboy.channel1frequencySweepDivider, 91 | // gameboy.channel1decreaseSweep, 92 | // gameboy.channel2FrequencyTracker, 93 | // gameboy.channel2FrequencyCounter, 94 | // gameboy.channel2totalLength, 95 | // gameboy.channel2EnvelopeVolume, 96 | // gameboy.channel2EnvelopeType, 97 | // gameboy.channel2EnvelopeSweeps, 98 | // gameboy.channel2EnvelopeSweepsLast, 99 | // gameboy.channel2consecutive, 100 | // gameboy.channel2frequency, 101 | // gameboy.channel3canPlay, 102 | // gameboy.channel3totalLength, 103 | // gameboy.channel3patternType, 104 | // gameboy.channel3frequency, 105 | // gameboy.channel3consecutive, 106 | // fromTypedArray(gameboy.channel3PCM), 107 | // gameboy.audioController.channel4FrequencyPeriod, 108 | // gameboy.audioController.channel4lastSampleLookup, 109 | // gameboy.channel4totalLength, 110 | // gameboy.channel4EnvelopeVolume, 111 | // gameboy.channel4currentVolume, 112 | // gameboy.channel4EnvelopeType, 113 | // gameboy.channel4EnvelopeSweeps, 114 | // gameboy.channel4EnvelopeSweepsLast, 115 | // gameboy.channel4consecutive, 116 | // gameboy.channel4BitRange, 117 | // gameboy.audioController.enabled, 118 | // gameboy.audioController.VinLeftChannelMasterVolume, 119 | // gameboy.audioController.VinRightChannelMasterVolume, 120 | // gameboy.leftChannel1, 121 | // gameboy.leftChannel2, 122 | // gameboy.leftChannel3, 123 | // gameboy.leftChannel4, 124 | // gameboy.rightChannel1, 125 | // gameboy.rightChannel2, 126 | // gameboy.rightChannel3, 127 | // gameboy.rightChannel4, 128 | // gameboy.channel1currentSampleLeft, 129 | // gameboy.channel1currentSampleRight, 130 | // gameboy.channel2currentSampleLeft, 131 | // gameboy.channel2currentSampleRight, 132 | // gameboy.channel3currentSampleLeft, 133 | // gameboy.channel3currentSampleRight, 134 | // gameboy.channel4currentSampleLeft, 135 | // gameboy.channel4currentSampleRight, 136 | // gameboy.channel1currentSampleLeftSecondary, 137 | // gameboy.channel1currentSampleRightSecondary, 138 | // gameboy.channel2currentSampleLeftSecondary, 139 | // gameboy.channel2currentSampleRightSecondary, 140 | // gameboy.channel3currentSampleLeftSecondary, 141 | // gameboy.channel3currentSampleRightSecondary, 142 | // gameboy.channel4currentSampleLeftSecondary, 143 | // gameboy.channel4currentSampleRightSecondary, 144 | // gameboy.channel1currentSampleLeftTrimary, 145 | // gameboy.channel1currentSampleRightTrimary, 146 | // gameboy.channel2currentSampleLeftTrimary, 147 | // gameboy.channel2currentSampleRightTrimary, 148 | // gameboy.audioController.mixerOutputCache, 149 | // gameboy.audioController.channel1DutyTracker, 150 | // gameboy.audioController.channel1CachedDuty, 151 | // gameboy.audioController.channel2DutyTracker, 152 | // gameboy.audioController.channel2CachedDuty, 153 | // gameboy.audioController.channel1Enabled, 154 | // gameboy.audioController.channel2Enabled, 155 | // gameboy.audioController.channel3Enabled, 156 | // gameboy.audioController.channel4Enabled, 157 | // gameboy.audioController.sequencerClocks, 158 | // gameboy.audioController.sequencePosition, 159 | // gameboy.channel3Counter, 160 | // gameboy.audioController.channel4Counter, 161 | // gameboy.audioController.cachedChannel3Sample, 162 | // gameboy.audioController.cachedChannel4Sample, 163 | // gameboy.channel3FrequencyPeriod, 164 | // gameboy.channel3lastSampleLookup, 165 | // gameboy.actualScanline, 166 | // gameboy.lastUnrenderedLine, 167 | // gameboy.queuedScanlines, 168 | // gameboy.cartridge.hasRTC && 169 | // gameboy.cartridge.mbc3.rtc.RTCisLatched, 170 | // gameboy.cartridge.hasRTC && 171 | // gameboy.cartridge.mbc3.rtc.latchedSeconds, 172 | // gameboy.cartridge.hasRTC && 173 | // gameboy.cartridge.mbc3.rtc.latchedMinutes, 174 | // gameboy.cartridge.hasRTC && 175 | // gameboy.cartridge.mbc3.rtc.latchedHours, 176 | // gameboy.cartridge.hasRTC && 177 | // gameboy.cartridge.mbc3.rtc.latchedLDays, 178 | // gameboy.cartridge.hasRTC && 179 | // gameboy.cartridge.mbc3.rtc.latchedHDays, 180 | // gameboy.cartridge.hasRTC && 181 | // gameboy.cartridge.mbc3.rtc.RTCSeconds, 182 | // gameboy.cartridge.hasRTC && 183 | // gameboy.cartridge.mbc3.rtc.RTCMinutes, 184 | // gameboy.cartridge.hasRTC && 185 | // gameboy.cartridge.mbc3.rtc.RTCHours, 186 | // gameboy.cartridge.hasRTC && 187 | // gameboy.cartridge.mbc3.rtc.RTCDays, 188 | // gameboy.cartridge.hasRTC && 189 | // gameboy.cartridge.mbc3.rtc.RTCDayOverFlow, 190 | // gameboy.cartridge.hasRTC && 191 | // gameboy.cartridge.mbc3.rtc.RTCHALT, 192 | // gameboy.usedBootROM, 193 | // gameboy.skipPCIncrement, 194 | // gameboy.STATTracker, 195 | // gameboy.gbcRamBankPositionECHO, 196 | // gameboy.windowY, 197 | // gameboy.windowX, 198 | // fromTypedArray(gameboy.gbcOBJRawPalette), 199 | // fromTypedArray(gameboy.gbcBGRawPalette), 200 | // fromTypedArray(gameboy.gbOBJPalette), 201 | // fromTypedArray(gameboy.gbBGPalette), 202 | // fromTypedArray(gameboy.gbcOBJPalette), 203 | // fromTypedArray(gameboy.gbcBGPalette), 204 | // fromTypedArray(gameboy.gbBGColorizedPalette), 205 | // fromTypedArray(gameboy.gbOBJColorizedPalette), 206 | // fromTypedArray(gameboy.cachedBGPaletteConversion), 207 | // fromTypedArray(gameboy.cachedOBJPaletteConversion), 208 | // fromTypedArray(gameboy.BGCHRBank1), 209 | // fromTypedArray(gameboy.BGCHRBank2), 210 | // gameboy.haltPostClocks, 211 | // gameboy.interruptRequestedFlags, 212 | // gameboy.interruptEnabledFlags, 213 | // gameboy.remainingClocks, 214 | // gameboy.colorizedGBPalettes, 215 | // gameboy.backgroundY, 216 | // gameboy.backgroundX, 217 | // gameboy.cpu.stopped, 218 | // gameboy.audioController.audioClocksUntilNextEvent, 219 | // gameboy.audioController.audioClocksUntilNextEventCounter 220 | // ]; 221 | } 222 | 223 | load(state) { 224 | let index = 0; 225 | const gameboy = this.gameboy; 226 | 227 | } 228 | 229 | loadOld(state) { 230 | let index = 0; 231 | const gameboy = this.gameboy; 232 | 233 | gameboy.isBootingRom = state[index++]; 234 | gameboy.registerA = state[index++]; 235 | gameboy.FZero = state[index++]; 236 | gameboy.FSubtract = state[index++]; 237 | gameboy.FHalfCarry = state[index++]; 238 | gameboy.FCarry = state[index++]; 239 | gameboy.registerB = state[index++]; 240 | gameboy.registerC = state[index++]; 241 | gameboy.registerD = state[index++]; 242 | gameboy.registerE = state[index++]; 243 | gameboy.registersHL = state[index++]; 244 | gameboy.stackPointer = state[index++]; 245 | gameboy.programCounter = state[index++]; 246 | gameboy.halt = state[index++]; 247 | gameboy.IME = state[index++]; 248 | gameboy.hdmaRunning = state[index++]; 249 | gameboy.currentInstructionCycleCount = state[index++]; 250 | gameboy.doubleSpeedShifter = state[index++]; 251 | gameboy.memory = toTypedArray(state[index++], "uint8"); 252 | gameboy.videoRam = toTypedArray(state[index++], "uint8"); 253 | gameboy.currentVideoRamBank = state[index++]; 254 | gameboy.gbcMemory = toTypedArray(state[index++], "uint8"); 255 | gameboy.gbcRamBank = state[index++]; 256 | gameboy.gbcRamBankPosition = state[index++]; 257 | gameboy.ROMBank1Offset = state[index++]; 258 | if (gameboy.cartridge?.mbc) { 259 | gameboy.cartridge.mbc.currentRomBank = state[index++]; 260 | } else { 261 | index++; 262 | } 263 | gameboy.modeSTAT = state[index++]; 264 | gameboy.LYCMatchTriggerSTAT = state[index++]; 265 | gameboy.mode2TriggerSTAT = state[index++]; 266 | gameboy.mode1TriggerSTAT = state[index++]; 267 | gameboy.mode0TriggerSTAT = state[index++]; 268 | gameboy.gpu.lcdEnabled = state[index++]; 269 | gameboy.gfxWindowCHRBankPosition = state[index++]; 270 | gameboy.gfxWindowDisplay = state[index++]; 271 | gameboy.gfxSpriteShow = state[index++]; 272 | gameboy.gfxSpriteNormalHeight = state[index++]; 273 | gameboy.gfxBackgroundCHRBankPosition = state[index++]; 274 | gameboy.gfxBackgroundBankOffset = state[index++]; 275 | gameboy.TIMAEnabled = state[index++]; 276 | gameboy.DIVTicks = state[index++]; 277 | gameboy.LCDTicks = state[index++]; 278 | gameboy.timerTicks = state[index++]; 279 | gameboy.TACClocker = state[index++]; 280 | gameboy.serialTimer = state[index++]; 281 | gameboy.serialShiftTimer = state[index++]; 282 | gameboy.serialShiftTimerAllocated = state[index++]; 283 | gameboy.IRQEnableDelay = state[index++]; 284 | if (gameboy.cartridge?.hasRtc) { 285 | gameboy.cartridge.mbc3.rtc.lastTime = state[index++]; 286 | } else { 287 | index++; 288 | } 289 | gameboy.drewBlank = state[index++]; 290 | gameboy.frameBuffer = toTypedArray(state[index++], "int32"); 291 | gameboy.backgroundEnabled = state[index++]; 292 | gameboy.audioController.channel1.frequencyTracker = state[index++]; 293 | gameboy.audioController.channel1.frequencyCounter = state[index++]; 294 | gameboy.audioController.channel1.totalLength = state[index++]; 295 | gameboy.audioController.channel1.envelopeVolume = state[index++]; 296 | gameboy.audioController.channel1.envelopeType = state[index++]; 297 | gameboy.audioController.channel1.envelopeSweeps = state[index++]; 298 | gameboy.audioController.channel1.envelopeSweepsLast = state[index++]; 299 | gameboy.audioController.channel1.consecutive = state[index++]; 300 | gameboy.audioController.channel1.frequency = state[index++]; 301 | gameboy.audioController.channel1.sweepFault = state[index++]; 302 | gameboy.audioController.channel1.shadowFrequency = state[index++]; 303 | gameboy.audioController.channel1.timeSweep = state[index++]; 304 | gameboy.audioController.channel1.lastTimeSweep = state[index++]; 305 | gameboy.audioController.channel1.swept = state[index++]; 306 | gameboy.audioController.channel1.frequencySweepDivider = state[index++]; 307 | gameboy.audioController.channel1.decreaseSweep = state[index++]; 308 | gameboy.audioController.channel2.frequencyTracker = state[index++]; 309 | gameboy.audioController.channel2.frequencyCounter = state[index++]; 310 | gameboy.audioController.channel2.totalLength = state[index++]; 311 | gameboy.audioController.channel2.envelopeVolume = state[index++]; 312 | gameboy.audioController.channel2.envelopeType = state[index++]; 313 | gameboy.audioController.channel2.envelopeSweeps = state[index++]; 314 | gameboy.audioController.channel2.envelopeSweepsLast = state[index++]; 315 | gameboy.audioController.channel2.consecutive = state[index++]; 316 | gameboy.audioController.channel2.frequency = state[index++]; 317 | gameboy.audioController.channel3CanPlay = state[index++]; 318 | gameboy.audioController.channel3TotalLength = state[index++]; 319 | gameboy.audioController.channel3PatternType = state[index++]; 320 | gameboy.audioController.channel3frequency = state[index++]; 321 | gameboy.audioController.channel3Consecutive = state[index++]; 322 | gameboy.audioController.channel3PcmData = toTypedArray(state[index++], "int8"); 323 | gameboy.audioController.channel4FrequencyPeriod = state[index++]; 324 | gameboy.audioController.channel4LastSampleLookup = state[index++]; 325 | gameboy.audioController.channel4TotalLength = state[index++]; 326 | gameboy.audioController.channel4EnvelopeVolume = state[index++]; 327 | gameboy.audioController.channel4CurrentVolume = state[index++]; 328 | gameboy.audioController.channel4EnvelopeType = state[index++]; 329 | gameboy.audioController.channel4EnvelopeSweeps = state[index++]; 330 | gameboy.audioController.channel4EnvelopeSweepsLast = state[index++]; 331 | gameboy.audioController.channel4Consecutive = state[index++]; 332 | gameboy.audioController.channel4BitRange = state[index++]; 333 | gameboy.audioController.enabled = state[index++]; 334 | gameboy.audioController.cartridgeLeftChannelInputVolume = state[index++]; 335 | gameboy.audioController.cartridgeRightChannelInputVolume = state[index++]; 336 | gameboy.audioController.channel1.leftChannelEnabled = state[index++]; 337 | gameboy.audioController.channel2.leftChannelEnabled = state[index++]; 338 | gameboy.audioController.leftChannel3 = state[index++]; 339 | gameboy.audioController.leftChannel4 = state[index++]; 340 | gameboy.audioController.channel1.rightChannelEnabled = state[index++]; 341 | gameboy.audioController.channel2.rightChannelEnabled = state[index++]; 342 | gameboy.audioController.rightChannel3 = state[index++]; 343 | gameboy.audioController.rightChannel4 = state[index++]; 344 | gameboy.audioController.channel1.currentSampleLeft = state[index++]; 345 | gameboy.audioController.channel1.currentSampleRight = state[index++]; 346 | gameboy.audioController.channel2.currentSampleLeft = state[index++]; 347 | gameboy.audioController.channel2.currentSampleRight = state[index++]; 348 | gameboy.audioController.channel3CurrentSampleLeft = state[index++]; 349 | gameboy.audioController.channel3CurrentSampleRight = state[index++]; 350 | gameboy.audioController.channel4CurrentSampleLeft = state[index++]; 351 | gameboy.audioController.channel4CurrentSampleRight = state[index++]; 352 | gameboy.audioController.channel1.currentSampleLeftSecondary = state[index++]; 353 | gameboy.audioController.channel1.currentSampleRightSecondary = state[index++]; 354 | gameboy.audioController.channel2.currentSampleLeftSecondary = state[index++]; 355 | gameboy.audioController.channel2.currentSampleRightSecondary = state[index++]; 356 | gameboy.audioController.channel3CurrentSampleLeftSecondary = state[index++]; 357 | gameboy.audioController.channel3CurrentSampleRightSecondary = state[index++]; 358 | gameboy.audioController.channel4CurrentSampleLeftSecondary = state[index++]; 359 | gameboy.audioController.channel4CurrentSampleRightSecondary = state[index++]; 360 | gameboy.audioController.channel1.currentSampleLeftTrimary = state[index++]; 361 | gameboy.audioController.channel1.currentSampleRightTrimary = state[index++]; 362 | gameboy.audioController.channel2.currentSampleLeftTrimary = state[index++]; 363 | gameboy.audioController.channel2.currentSampleRightTrimary = state[index++]; 364 | gameboy.audioController.mixerOutputCache = state[index++]; 365 | gameboy.audioController.channel1.dutyTracker = state[index++]; 366 | gameboy.audioController.channel1.cachedDuty = state[index++]; 367 | gameboy.audioController.channel2.dutyTracker = state[index++]; 368 | gameboy.audioController.channel2.cachedDuty = state[index++]; 369 | gameboy.audioController.channel1.enabled = state[index++]; 370 | gameboy.audioController.channel2.enabled = state[index++]; 371 | gameboy.audioController.channel3Enabled = state[index++]; 372 | gameboy.audioController.channel4Enabled = state[index++]; 373 | gameboy.audioController.sequencerClocks = state[index++]; 374 | gameboy.audioController.sequencePosition = state[index++]; 375 | gameboy.audioController.channel3Counter = state[index++]; 376 | gameboy.audioController.channel4Counter = state[index++]; 377 | gameboy.audioController.cachedChannel3Sample = state[index++]; 378 | gameboy.audioController.cachedChannel4Sample = state[index++]; 379 | gameboy.audioController.channel3FrequencyPeriod = state[index++]; 380 | gameboy.audioController.channel3LastSampleLookup = state[index++]; 381 | gameboy.actualScanline = state[index++]; 382 | gameboy.lastUnrenderedLine = state[index++]; 383 | gameboy.queuedScanlines = state[index++]; 384 | if (gameboy.cartridge?.hasRtc) { 385 | gameboy.cartridge.mbc3.rtc.RTCisLatched = state[index++]; 386 | gameboy.cartridge.mbc3.rtc.latchedSeconds = state[index++]; 387 | gameboy.cartridge.mbc3.rtc.latchedMinutes = state[index++]; 388 | gameboy.cartridge.mbc3.rtc.latchedHours = state[index++]; 389 | gameboy.cartridge.mbc3.rtc.latchedLDays = state[index++]; 390 | gameboy.cartridge.mbc3.rtc.latchedHDays = state[index++]; 391 | gameboy.cartridge.mbc3.rtc.RTCSeconds = state[index++]; 392 | gameboy.cartridge.mbc3.rtc.RTCMinutes = state[index++]; 393 | gameboy.cartridge.mbc3.rtc.RTCHours = state[index++]; 394 | gameboy.cartridge.mbc3.rtc.RTCDays = state[index++]; 395 | gameboy.cartridge.mbc3.rtc.RTCDayOverFlow = state[index++]; 396 | gameboy.cartridge.mbc3.rtc.RTCHalt = state[index++]; 397 | } else { 398 | index += 12; 399 | } 400 | gameboy.usedBootRom = state[index++]; 401 | gameboy.skipPCIncrement = state[index++]; 402 | gameboy.STATTracker = state[index++]; 403 | gameboy.gbcEchoRamBankPosition = state[index++]; 404 | gameboy.windowY = state[index++]; 405 | gameboy.windowX = state[index++]; 406 | gameboy.gbcOBJRawPalette = toTypedArray(state[index++], "uint8"); 407 | gameboy.gbcBGRawPalette = toTypedArray(state[index++], "uint8"); 408 | gameboy.gbOBJPalette = toTypedArray(state[index++], "int32"); 409 | gameboy.gbBGPalette = toTypedArray(state[index++], "int32"); 410 | gameboy.gbcOBJPalette = toTypedArray(state[index++], "int32"); 411 | gameboy.gbcBGPalette = toTypedArray(state[index++], "int32"); 412 | gameboy.gbBGColorizedPalette = toTypedArray(state[index++], "int32"); 413 | gameboy.gbOBJColorizedPalette = toTypedArray(state[index++], "int32"); 414 | gameboy.cachedBGPaletteConversion = toTypedArray( 415 | state[index++], 416 | "int32" 417 | ); 418 | gameboy.cachedOBJPaletteConversion = toTypedArray( 419 | state[index++], 420 | "int32" 421 | ); 422 | gameboy.BGCHRBank1 = toTypedArray(state[index++], "uint8"); 423 | gameboy.BGCHRBank2 = toTypedArray(state[index++], "uint8"); 424 | gameboy.haltPostClocks = state[index++]; 425 | gameboy.interruptRequestedFlags = state[index++]; 426 | gameboy.interruptEnabledFlags = state[index++]; 427 | gameboy.checkIrqMatching(); 428 | gameboy.remainingClocks = state[index++]; 429 | gameboy.colorizedGBPalettes = state[index++]; 430 | gameboy.backgroundY = state[index++]; 431 | gameboy.backgroundX = state[index++]; 432 | gameboy.cpu.stopped = state[index++]; 433 | gameboy.audioController.audioClocksUntilNextEvent = state[index++]; 434 | gameboy.audioController.audioClocksUntilNextEventCounter = state[index]; 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /src/actions.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | export default class Actions extends EventEmitter { 4 | map: { [key: string]: any; } = {}; 5 | 6 | register(action: string) { 7 | this.map[action] = true; 8 | return this; 9 | } 10 | 11 | getAll() { 12 | return Object.keys(this.map); 13 | } 14 | 15 | is(action: string) { 16 | return !!this.map[action]; 17 | } 18 | 19 | down(action: string, options) { 20 | this.emit("Down" + action, options); 21 | } 22 | 23 | change(action: string, options) { 24 | this.emit("Change" + action, options); 25 | } 26 | 27 | up(action: string, options) { 28 | this.emit("Up" + action, options); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/audio/AudioChannel.ts: -------------------------------------------------------------------------------- 1 | import GameBoy from "../GameBoy"; 2 | 3 | export default class AudioChannel { 4 | enabled: boolean = false; 5 | 6 | leftChannelEnabled: boolean = false; 7 | rightChannelEnabled: boolean = false; 8 | 9 | canPlay: boolean = false; 10 | 11 | constructor( 12 | protected gameboy: GameBoy 13 | ) { } 14 | } -------------------------------------------------------------------------------- /src/audio/AudioController.ts: -------------------------------------------------------------------------------- 1 | import CPU from "../CPU"; 2 | import * as util from "../util"; 3 | import GameBoy from "../GameBoy"; 4 | import settings from "../settings"; 5 | import AudioDevice from "./AudioDevice"; 6 | import * as MemoryLayout from "../memory/Layout"; 7 | import SquareAudioChannel from "./SquareAudioChannel"; 8 | 9 | export default class AudioController { 10 | cartridgeLeftChannelInputVolume: number; 11 | cartridgeRightChannelInputVolume: number; 12 | mixerOutputCache: number; 13 | sequencerClocks: number; 14 | sequencePosition: number; 15 | 16 | audioClocksUntilNextEvent: number; 17 | audioClocksUntilNextEventCounter: number; 18 | resamplerFirstPassFactor: number; 19 | 20 | leftChannel3: boolean; 21 | rightChannel3: boolean; 22 | channel3Enabled: boolean; 23 | channel3CanPlay: boolean; 24 | cachedChannel3Sample: number; 25 | channel3CurrentSampleLeft: number; 26 | channel3CurrentSampleRight: number; 27 | channel3CurrentSampleLeftSecondary: number; 28 | channel3CurrentSampleRightSecondary: number; 29 | channel3envelopeVolume: number; 30 | channel3TotalLength: number; 31 | channel3PatternType: number; 32 | channel3frequency: number; 33 | channel3Consecutive: boolean; 34 | channel3Counter: number; 35 | channel3FrequencyPeriod: number; 36 | channel3LastSampleLookup: number; 37 | channel3PcmData: Int8Array; 38 | 39 | leftChannel4: boolean; 40 | rightChannel4: boolean; 41 | channel4Enabled: boolean; 42 | channel4CanPlay: boolean; 43 | cachedChannel4Sample: number; 44 | channel4Counter: number; 45 | channel4CurrentSampleLeft: number; 46 | channel4CurrentSampleRight: number; 47 | channel4CurrentSampleLeftSecondary: number; 48 | channel4CurrentSampleRightSecondary: number; 49 | channel4FrequencyPeriod: number; 50 | channel4TotalLength: number; 51 | channel4EnvelopeVolume: number; 52 | channel4CurrentVolume: number; 53 | channel4EnvelopeType: boolean; 54 | channel4EnvelopeSweeps: number; 55 | channel4EnvelopeSweepsLast: number; 56 | channel4Consecutive: boolean; 57 | channel4BitRange: number; 58 | channel4VolumeShifter: number; 59 | channel4LastSampleLookup: number; 60 | 61 | downSampleInputDivider: number; 62 | device: AudioDevice; 63 | memory: util.TypedArray; 64 | gameboy: GameBoy; 65 | cpu: CPU; 66 | LSFR15Table: Int8Array; 67 | LSFR7Table: Int8Array; 68 | noiseSampleTable: Int8Array; 69 | bufferLength = 0; // Length of the sound buffers 70 | audioTicks = 0; // Used to sample the audio system every x CPU instructions 71 | audioIndex = 0; // Used to keep alignment on audio generation 72 | bufferContainAmount = 0; // Buffer maintenance metric 73 | bufferPosition = 0; // Used to keep alignment on audio generation 74 | downsampleInput = 0; 75 | buffer: Float32Array; 76 | enabled: boolean = true; 77 | 78 | channel1: SquareAudioChannel; 79 | channel2: SquareAudioChannel; 80 | 81 | constructor({ cpu, gameboy }: { cpu: CPU; gameboy: GameBoy; }) { 82 | this.cpu = cpu; 83 | this.gameboy = gameboy; 84 | 85 | this.channel1 = new SquareAudioChannel(this.gameboy, { sweepEnabled: true }); 86 | this.channel2 = new SquareAudioChannel(this.gameboy); 87 | 88 | this.generateWhiteNoise(); 89 | this.setInitialState(); 90 | } 91 | 92 | setMemory(memory: util.TypedArray) { 93 | this.memory = memory; 94 | } 95 | 96 | initMemory() { 97 | this.channel3PcmData = new Int8Array(0x20); 98 | } 99 | 100 | init() { 101 | this.channel1.init(); 102 | this.channel2.init(); 103 | 104 | this.leftChannel3 = false; 105 | this.rightChannel3 = false; 106 | 107 | this.leftChannel4 = false; 108 | this.rightChannel4 = false; 109 | this.channel4Consecutive = false; 110 | 111 | this.cartridgeLeftChannelInputVolume = 8; 112 | this.cartridgeRightChannelInputVolume = 8; 113 | } 114 | 115 | setInitialState() { 116 | this.channel1.setInitialState(); 117 | this.channel2.setInitialState(); 118 | 119 | this.channel3TotalLength = 0; 120 | this.channel3PatternType = 4; 121 | this.channel3frequency = 0; 122 | this.channel3Consecutive = true; 123 | this.channel3Counter = 0x800; 124 | this.channel3FrequencyPeriod = 0x800; 125 | this.channel3LastSampleLookup = 0; 126 | this.cachedChannel3Sample = 0; 127 | this.channel3Enabled = false; 128 | this.channel3CanPlay = false; 129 | 130 | this.channel4FrequencyPeriod = 8; 131 | this.channel4TotalLength = 0; 132 | this.channel4EnvelopeVolume = 0; 133 | this.channel4CurrentVolume = 0; 134 | this.channel4EnvelopeType = false; 135 | this.channel4EnvelopeSweeps = 0; 136 | this.channel4EnvelopeSweepsLast = 0; 137 | this.channel4Consecutive = true; 138 | this.channel4BitRange = 0x7fff; 139 | this.channel4VolumeShifter = 15; 140 | this.channel4LastSampleLookup = 0; 141 | this.channel4FrequencyPeriod = 8; 142 | this.channel4Counter = 8; 143 | this.cachedChannel4Sample = 0; 144 | this.channel4Enabled = false; 145 | this.channel4CanPlay = false; 146 | 147 | this.cartridgeLeftChannelInputVolume = 8; 148 | this.cartridgeRightChannelInputVolume = 8; 149 | this.mixerOutputCache = 0; 150 | this.sequencerClocks = 0x2000; 151 | this.sequencePosition = 0; 152 | this.audioClocksUntilNextEvent = 1; 153 | this.audioClocksUntilNextEventCounter = 1; 154 | 155 | this.channel1.setFirstStageSamples(); 156 | this.channel1.setSecondStageSamples(); 157 | this.channel1.setThirdStageSamples(); 158 | this.cacheMixerOutputLevel(); 159 | 160 | this.channel2.setFirstStageSamples(); 161 | this.channel2.setSecondStageSamples(); 162 | this.channel2.setThirdStageSamples(); 163 | this.cacheMixerOutputLevel(); 164 | 165 | this.cacheChannel3OutputLevel(); 166 | this.cacheChannel4OutputLevel(); 167 | 168 | this.noiseSampleTable = this.LSFR15Table; 169 | } 170 | 171 | setSkippedBootRomState() { 172 | this.channel1.setSkippedBootRomState(); 173 | this.channel2.setSkippedBootRomState(); 174 | 175 | this.leftChannel3 = true; 176 | this.rightChannel3 = false; 177 | this.channel3CanPlay = false; 178 | this.channel3TotalLength = 0; 179 | this.channel3PatternType = 4; 180 | this.channel3frequency = 0; 181 | this.channel3Consecutive = true; 182 | this.channel3Counter = 0x800; 183 | this.channel3FrequencyPeriod = 0x800; 184 | this.channel3LastSampleLookup = 0; 185 | 186 | this.leftChannel4 = true; 187 | this.rightChannel4 = false; 188 | this.channel4FrequencyPeriod = 8; 189 | this.channel4TotalLength = 0; 190 | this.channel4EnvelopeVolume = 0; 191 | this.channel4CurrentVolume = 0; 192 | this.channel4EnvelopeType = false; 193 | this.channel4EnvelopeSweeps = 0; 194 | this.channel4EnvelopeSweepsLast = 0; 195 | this.channel4Consecutive = true; 196 | this.channel4BitRange = 0x7fff; 197 | this.channel4VolumeShifter = 15; 198 | this.channel4LastSampleLookup = 0; 199 | 200 | this.cartridgeLeftChannelInputVolume = 8; 201 | this.cartridgeRightChannelInputVolume = 8; 202 | } 203 | 204 | // Below are the audio generation functions timed against the CPU: 205 | generate(sampleCount: number) { 206 | if ( 207 | this.gameboy.audioController.enabled && 208 | !this.gameboy.cpu.stopped 209 | ) { 210 | for (let clockUpTo = 0; sampleCount > 0;) { 211 | clockUpTo = Math.min(this.audioClocksUntilNextEventCounter, this.sequencerClocks, sampleCount); 212 | this.audioClocksUntilNextEventCounter -= clockUpTo; 213 | this.sequencerClocks -= clockUpTo; 214 | sampleCount -= clockUpTo; 215 | while (clockUpTo > 0) { 216 | const multiplier = Math.min(clockUpTo, this.resamplerFirstPassFactor - this.audioIndex); 217 | clockUpTo -= multiplier; 218 | this.audioIndex += multiplier; 219 | this.downsampleInput += this.mixerOutputCache * multiplier; 220 | if (this.audioIndex === this.resamplerFirstPassFactor) { 221 | this.audioIndex = 0; 222 | this.outputAudio(); 223 | } 224 | } 225 | if (this.sequencerClocks === 0) { 226 | this.audioComputeSequencer(); 227 | this.sequencerClocks = 0x2000; 228 | } 229 | if (this.audioClocksUntilNextEventCounter === 0) { 230 | this.computeChannels(); 231 | } 232 | } 233 | } else { 234 | // SILENT OUTPUT 235 | while (sampleCount > 0) { 236 | const multiplier = Math.min(sampleCount, this.resamplerFirstPassFactor - this.audioIndex); 237 | sampleCount -= multiplier; 238 | this.audioIndex += multiplier; 239 | if (this.audioIndex === this.resamplerFirstPassFactor) { 240 | this.audioIndex = 0; 241 | this.outputAudio(); 242 | } 243 | } 244 | } 245 | } 246 | 247 | enable() { 248 | this.memory[0xff26] = 0x80; 249 | this.enabled = true; 250 | this.setInitialState(); 251 | } 252 | 253 | disable() { 254 | this.memory[0xff26] = 0; 255 | this.enabled = false; 256 | 257 | for (let address = 0xff10; address < 0xff26; address++) { 258 | this.gameboy.writeMemory(address, 0); 259 | } 260 | } 261 | 262 | run() { 263 | this.generate(this.audioTicks); 264 | this.audioTicks = 0; 265 | } 266 | 267 | clockAudioEnvelope() { 268 | this.channel1.envelope(); 269 | this.channel2.envelope(); 270 | 271 | // Channel 4: 272 | if (this.channel4EnvelopeSweepsLast > -1) { 273 | if (this.channel4EnvelopeSweeps > 0) { 274 | --this.channel4EnvelopeSweeps; 275 | } else { 276 | if (!this.channel4EnvelopeType) { 277 | if (this.channel4EnvelopeVolume > 0) { 278 | this.channel4CurrentVolume = --this.channel4EnvelopeVolume << this.channel4VolumeShifter; 279 | this.channel4EnvelopeSweeps = this.channel4EnvelopeSweepsLast; 280 | this.cacheChannel4Update(); 281 | } else { 282 | this.channel4EnvelopeSweepsLast = -1; 283 | } 284 | } else if (this.channel4EnvelopeVolume < 0xf) { 285 | this.channel4CurrentVolume = ++this.channel4EnvelopeVolume << this.channel4VolumeShifter; 286 | this.channel4EnvelopeSweeps = this.channel4EnvelopeSweepsLast; 287 | this.cacheChannel4Update(); 288 | } else { 289 | this.channel4EnvelopeSweepsLast = -1; 290 | } 291 | } 292 | } 293 | } 294 | 295 | performChannel1AudioSweepDummy() { 296 | // Channel 1: 297 | if (this.channel1.frequencySweepDivider > 0) { 298 | if (!this.channel1.decreaseSweep) { 299 | const channel1ShadowFrequency = this.channel1.shadowFrequency + (this.channel1.shadowFrequency >> this.channel1.frequencySweepDivider); 300 | if (channel1ShadowFrequency <= 0x7ff) { 301 | // Run overflow check twice: 302 | if (channel1ShadowFrequency + (channel1ShadowFrequency >> this.channel1.frequencySweepDivider) > 0x7ff) { 303 | this.channel1.sweepFault = true; 304 | this.channel1.checkEnabled(); 305 | this.memory[0xff26] &= 0xfe; // Channel #1 On Flag Off 306 | } 307 | } else { 308 | this.channel1.sweepFault = true; 309 | this.channel1.checkEnabled(); 310 | this.memory[0xff26] &= 0xfe; // Channel #1 On Flag Off 311 | } 312 | } 313 | } 314 | } 315 | 316 | audioComputeSequencer() { 317 | switch (this.sequencePosition++) { 318 | case 0: 319 | this.clockAudioLength(); 320 | break; 321 | case 2: 322 | this.clockAudioLength(); 323 | this.clockAudioSweep(); 324 | break; 325 | case 4: 326 | this.clockAudioLength(); 327 | break; 328 | case 6: 329 | this.clockAudioLength(); 330 | this.clockAudioSweep(); 331 | break; 332 | case 7: 333 | this.clockAudioEnvelope(); 334 | this.sequencePosition = 0; 335 | } 336 | } 337 | 338 | clockAudioLength() { 339 | this.channel1.length(0xfe); 340 | this.channel2.length(0xfd); 341 | 342 | // Channel 3: 343 | if (this.channel3TotalLength > 1) { 344 | --this.channel3TotalLength; 345 | } else if (this.channel3TotalLength === 1) { 346 | this.channel3TotalLength = 0; 347 | this.checkChannel3Enable(); 348 | this.memory[0xff26] &= 0xfb; // Channel #3 On Flag Off 349 | } 350 | 351 | // Channel 4: 352 | if (this.channel4TotalLength > 1) { 353 | --this.channel4TotalLength; 354 | } else if (this.channel4TotalLength === 1) { 355 | this.channel4TotalLength = 0; 356 | this.checkChannel4Enable(); 357 | this.memory[0xff26] &= 0xf7; // Channel #4 On Flag Off 358 | } 359 | } 360 | 361 | clockAudioSweep() { 362 | // Channel 1: 363 | this.channel1.sweep(); 364 | } 365 | 366 | computeChannels() { 367 | // Clock down the four audio channels to the next closest audio event: 368 | this.channel1.frequencyCounter -= this.audioClocksUntilNextEvent; 369 | this.channel2.frequencyCounter -= this.audioClocksUntilNextEvent; 370 | this.channel3Counter -= this.audioClocksUntilNextEvent; 371 | this.channel4Counter -= this.audioClocksUntilNextEvent; 372 | 373 | // Channel 1 counter: 374 | if (this.channel1.frequencyCounter === 0) { 375 | this.channel1.frequencyCounter = this.channel1.frequencyTracker; 376 | this.channel1.dutyTracker = this.channel1.dutyTracker + 1 & 0x7; 377 | this.channel1.setThirdStageSamples(); 378 | this.cacheMixerOutputLevel(); 379 | } 380 | 381 | // Channel 2 counter: 382 | if (this.channel2.frequencyCounter === 0) { 383 | this.channel2.frequencyCounter = this.channel2.frequencyTracker; 384 | this.channel2.dutyTracker = this.channel2.dutyTracker + 1 & 0x7; 385 | this.channel2.setThirdStageSamples(); 386 | this.cacheMixerOutputLevel(); 387 | } 388 | 389 | // Channel 3 counter: 390 | if (this.channel3Counter === 0) { 391 | if (this.channel3CanPlay) { 392 | this.channel3LastSampleLookup = this.channel3LastSampleLookup + 1 & 0x1f; 393 | } 394 | this.channel3Counter = this.channel3FrequencyPeriod; 395 | this.cacheChannel3Update(); 396 | } 397 | 398 | // Channel 4 counter: 399 | if (this.channel4Counter === 0) { 400 | this.channel4LastSampleLookup = this.channel4LastSampleLookup + 1 & this.channel4BitRange; 401 | this.channel4Counter = this.channel4FrequencyPeriod; 402 | this.cacheChannel4Update(); 403 | } 404 | 405 | // Find the number of clocks to next closest counter event: 406 | this.audioClocksUntilNextEventCounter = this.audioClocksUntilNextEvent = Math.min( 407 | this.channel1.frequencyCounter, 408 | this.channel2.frequencyCounter, 409 | this.channel3Counter, 410 | this.channel4Counter 411 | ); 412 | } 413 | 414 | cacheChannel3Update() { 415 | this.cachedChannel3Sample = this.channel3PcmData[this.channel3LastSampleLookup] >> this.channel3PatternType; 416 | this.cacheChannel3OutputLevel(); 417 | } 418 | 419 | checkChannel3Enable() { 420 | this.channel3Enabled = this.channel3Consecutive || this.channel3TotalLength > 0; 421 | this.channel3OutputLevelSecondaryCache(); 422 | } 423 | 424 | cacheChannel3OutputLevel() { 425 | this.channel3CurrentSampleLeft = this.leftChannel3 ? this.cachedChannel3Sample : 0; 426 | this.channel3CurrentSampleRight = this.rightChannel3 ? this.cachedChannel3Sample : 0; 427 | this.channel3OutputLevelSecondaryCache(); 428 | } 429 | 430 | channel3OutputLevelSecondaryCache() { 431 | if ( 432 | this.channel3Enabled && 433 | settings.enabledAudioChannels[2] 434 | ) { 435 | this.channel3CurrentSampleLeftSecondary = this.channel3CurrentSampleLeft; 436 | this.channel3CurrentSampleRightSecondary = this.channel3CurrentSampleRight; 437 | } else { 438 | this.channel3CurrentSampleLeftSecondary = 0; 439 | this.channel3CurrentSampleRightSecondary = 0; 440 | } 441 | this.cacheMixerOutputLevel(); 442 | } 443 | 444 | checkChannel4Enable() { 445 | this.channel4Enabled = (this.channel4Consecutive || this.channel4TotalLength > 0) && this.channel4CanPlay; 446 | this.cacheChannel4OutputLevelSecondary(); 447 | } 448 | 449 | cacheChannel4Update() { 450 | this.cachedChannel4Sample = this.noiseSampleTable[this.channel4CurrentVolume | this.channel4LastSampleLookup]; 451 | this.cacheChannel4OutputLevel(); 452 | } 453 | 454 | cacheChannel4OutputLevel() { 455 | this.channel4CurrentSampleLeft = this.leftChannel4 ? this.cachedChannel4Sample : 0; 456 | this.channel4CurrentSampleRight = this.rightChannel4 ? this.cachedChannel4Sample : 0; 457 | this.cacheChannel4OutputLevelSecondary(); 458 | } 459 | 460 | checkChannel4VolumeEnable() { 461 | this.channel4CanPlay = this.memory[0xff21] > 7; 462 | this.checkChannel4Enable(); 463 | this.cacheChannel4OutputLevelSecondary(); 464 | } 465 | 466 | cacheChannel4OutputLevelSecondary() { 467 | if ( 468 | this.channel4Enabled && 469 | settings.enabledAudioChannels[3] 470 | ) { 471 | this.channel4CurrentSampleLeftSecondary = this.channel4CurrentSampleLeft; 472 | this.channel4CurrentSampleRightSecondary = this.channel4CurrentSampleRight; 473 | } else { 474 | this.channel4CurrentSampleLeftSecondary = 0; 475 | this.channel4CurrentSampleRightSecondary = 0; 476 | } 477 | this.cacheMixerOutputLevel(); 478 | } 479 | 480 | cacheMixerOutputLevel() { 481 | const currentLeftSample = ( 482 | this.channel1.currentSampleLeftTrimary + 483 | this.channel2.currentSampleLeftTrimary + 484 | this.channel3CurrentSampleLeftSecondary + 485 | this.channel4CurrentSampleLeftSecondary 486 | ); 487 | const currentRightSample = ( 488 | this.channel1.currentSampleRightTrimary + 489 | this.channel2.currentSampleRightTrimary + 490 | this.channel3CurrentSampleRightSecondary + 491 | this.channel4CurrentSampleRightSecondary 492 | ); 493 | this.mixerOutputCache = ( 494 | currentLeftSample * this.cartridgeLeftChannelInputVolume << 16 | 495 | currentRightSample * this.cartridgeRightChannelInputVolume 496 | ); 497 | } 498 | 499 | connectDevice(device: AudioDevice) { 500 | this.resamplerFirstPassFactor = Math.max( 501 | Math.min( 502 | Math.floor(this.cpu.clocksPerSecond / 44100), 503 | Math.floor(0xffff / 0x1e0) 504 | ), 505 | 1 506 | ); 507 | this.downSampleInputDivider = 1 / ( 508 | this.resamplerFirstPassFactor * 0xf0 509 | ); 510 | 511 | const sampleRate = this.cpu.clocksPerSecond / this.resamplerFirstPassFactor; 512 | const maxBufferSize = Math.max( 513 | ( 514 | this.cpu.baseCyclesPerIteration * 515 | settings.maxAudioBufferSpanAmountOverXInterpreterIterations / 516 | this.resamplerFirstPassFactor 517 | ), 518 | 8192 519 | ) << 1; 520 | device.setSampleRate(sampleRate); 521 | device.setMaxBufferSize(maxBufferSize); 522 | device.init(); 523 | 524 | this.device = device; 525 | 526 | this.audioIndex = 0; 527 | this.bufferPosition = 0; 528 | this.downsampleInput = 0; 529 | this.bufferContainAmount = Math.max(this.cpu.baseCyclesPerIteration * settings.minAudioBufferSpanAmountOverXInterpreterIterations / this.resamplerFirstPassFactor, 4096) << 1; 530 | this.bufferLength = this.cpu.baseCyclesPerIteration / this.resamplerFirstPassFactor << 1; 531 | this.buffer = new Float32Array(this.bufferLength); 532 | } 533 | 534 | setVolume(volume: number) { 535 | this.device?.setVolume(volume); 536 | } 537 | 538 | adjustUnderrun() { 539 | let underrunAmount = this.device.remainingBuffer(); 540 | if (typeof underrunAmount === "number") { 541 | underrunAmount = this.bufferContainAmount - Math.max(underrunAmount, 0); 542 | if (underrunAmount > 0) { 543 | this.recalculateIterationClockLimitForAudio((underrunAmount >> 1) * this.resamplerFirstPassFactor); 544 | } 545 | } 546 | } 547 | 548 | recalculateIterationClockLimitForAudio(audioClocking) { 549 | this.cpu.cyclesTotal += Math.min(audioClocking >> 2 << 2, this.cpu.cyclesTotalBase << 1); 550 | } 551 | 552 | outputAudio() { 553 | this.fillBuffer(); 554 | if (this.bufferPosition === this.bufferLength) { 555 | this.device.writeAudio(this.buffer); 556 | this.bufferPosition = 0; 557 | } 558 | this.downsampleInput = 0; 559 | } 560 | 561 | fillBuffer() { 562 | this.buffer[this.bufferPosition++] = (this.downsampleInput >>> 16) * this.downSampleInputDivider - 1; 563 | this.buffer[this.bufferPosition++] = (this.downsampleInput & 0xffff) * this.downSampleInputDivider - 1; 564 | } 565 | 566 | generateWhiteNoise() { 567 | // Noise Sample Tables 568 | this.LSFR7Table = this.generateLSFR7Table(); 569 | this.LSFR15Table = this.generateLSFR15Table(); 570 | 571 | // Set the default noise table 572 | this.noiseSampleTable = this.LSFR15Table; 573 | } 574 | 575 | generateLSFR7Table() { 576 | // 7-bit LSFR Cache Generation: 577 | const LSFR7Table = new Int8Array(0x800); 578 | let LSFR = 0x7f; // Seed value has all its bits set. 579 | for (let index = 0; index < 0x80; ++index) { 580 | // Normalize the last LSFR value for usage: 581 | const randomFactor = 1 - (LSFR & 1); // Docs say it's the inverse. 582 | // Cache the different volume level results: 583 | LSFR7Table[0x080 | index] = randomFactor; 584 | LSFR7Table[0x100 | index] = randomFactor * 0x2; 585 | LSFR7Table[0x180 | index] = randomFactor * 0x3; 586 | LSFR7Table[0x200 | index] = randomFactor * 0x4; 587 | LSFR7Table[0x280 | index] = randomFactor * 0x5; 588 | LSFR7Table[0x300 | index] = randomFactor * 0x6; 589 | LSFR7Table[0x380 | index] = randomFactor * 0x7; 590 | LSFR7Table[0x400 | index] = randomFactor * 0x8; 591 | LSFR7Table[0x480 | index] = randomFactor * 0x9; 592 | LSFR7Table[0x500 | index] = randomFactor * 0xa; 593 | LSFR7Table[0x580 | index] = randomFactor * 0xb; 594 | LSFR7Table[0x600 | index] = randomFactor * 0xc; 595 | LSFR7Table[0x680 | index] = randomFactor * 0xd; 596 | LSFR7Table[0x700 | index] = randomFactor * 0xe; 597 | LSFR7Table[0x780 | index] = randomFactor * 0xf; 598 | // Recompute the LSFR algorithm: 599 | const LSFRShifted = LSFR >> 1; 600 | LSFR = LSFRShifted | ((LSFRShifted ^ LSFR) & 0x1) << 6; 601 | } 602 | 603 | return LSFR7Table; 604 | } 605 | 606 | generateLSFR15Table() { 607 | // 15-bit LSFR Cache Generation: 608 | const LSFR15Table = new Int8Array(0x80000); 609 | let LSFR = 0x7fff; // Seed value has all its bits set. 610 | for (let index = 0; index < 0x8000; ++index) { 611 | // Normalize the last LSFR value for usage: 612 | const randomFactor = 1 - (LSFR & 1); // Docs say it's the inverse. 613 | // Cache the different volume level results: 614 | LSFR15Table[0x08000 | index] = randomFactor; 615 | LSFR15Table[0x10000 | index] = randomFactor * 0x2; 616 | LSFR15Table[0x18000 | index] = randomFactor * 0x3; 617 | LSFR15Table[0x20000 | index] = randomFactor * 0x4; 618 | LSFR15Table[0x28000 | index] = randomFactor * 0x5; 619 | LSFR15Table[0x30000 | index] = randomFactor * 0x6; 620 | LSFR15Table[0x38000 | index] = randomFactor * 0x7; 621 | LSFR15Table[0x40000 | index] = randomFactor * 0x8; 622 | LSFR15Table[0x48000 | index] = randomFactor * 0x9; 623 | LSFR15Table[0x50000 | index] = randomFactor * 0xa; 624 | LSFR15Table[0x58000 | index] = randomFactor * 0xb; 625 | LSFR15Table[0x60000 | index] = randomFactor * 0xc; 626 | LSFR15Table[0x68000 | index] = randomFactor * 0xd; 627 | LSFR15Table[0x70000 | index] = randomFactor * 0xe; 628 | LSFR15Table[0x78000 | index] = randomFactor * 0xf; 629 | // Recompute the LSFR algorithm: 630 | const LSFRShifted = LSFR >> 1; 631 | LSFR = LSFRShifted | ((LSFRShifted ^ LSFR) & 0x1) << 14; 632 | } 633 | 634 | return LSFR15Table; 635 | } 636 | 637 | registerMemoryWriters() { 638 | //NR10: 639 | this.gameboy.highMemoryWriter[0x10] = this.gameboy.memoryWriter[0xff10] = (address: number, data: number) => { 640 | if (this.enabled) { 641 | this.run(); 642 | 643 | this.channel1.setSweep(data); 644 | 645 | this.memory[0xff10] = data; 646 | this.channel1.checkEnabled(); 647 | } 648 | }; 649 | //NR11: 650 | this.gameboy.highMemoryWriter[0x11] = this.gameboy.memoryWriter[0xff11] = (address: number, data: number) => { 651 | if (this.enabled || !this.gameboy.cartridge.useGbcMode) { 652 | if (this.enabled) { 653 | this.run(); 654 | } else { 655 | data &= 0x3f; 656 | } 657 | 658 | this.channel1.setDuty(data); 659 | this.channel1.setLength(data); 660 | 661 | this.memory[0xff11] = data; 662 | this.channel1.checkEnabled(); 663 | } 664 | }; 665 | //NR12: 666 | this.gameboy.highMemoryWriter[0x12] = this.gameboy.memoryWriter[0xff12] = (address: number, data: number) => { 667 | if (this.enabled) { 668 | this.run(); 669 | this.channel1.setEnvelopeVolume(0xff12, data); 670 | this.channel1.setEnvelopeType(data); 671 | this.memory[0xff12] = data; 672 | this.channel1.checkVolumeEnabled(); 673 | } 674 | }; 675 | //NR13: 676 | this.gameboy.highMemoryWriter[0x13] = this.gameboy.memoryWriter[0xff13] = (address: number, data: number) => { 677 | if (this.enabled) { 678 | this.run(); 679 | this.channel1.setFrequency(data); 680 | } 681 | }; 682 | //NR14: 683 | this.gameboy.highMemoryWriter[0x14] = this.gameboy.memoryWriter[0xff14] = (address: number, data: number) => { 684 | if (this.enabled) { 685 | this.run(); 686 | this.channel1.consecutive = (data & 0x40) === 0x0; 687 | this.channel1.setHighFrequency(data); 688 | if (data > 0x7f) { 689 | //Reload 0xFF10: 690 | this.channel1.timeSweep = this.channel1.lastTimeSweep; 691 | this.channel1.swept = false; 692 | //Reload 0xFF12: 693 | var nr12 = this.memory[0xff12]; 694 | this.channel1.envelopeVolume = nr12 >> 4; 695 | 696 | this.channel1.setFirstStageSamples(); 697 | this.channel1.setSecondStageSamples(); 698 | this.channel1.setThirdStageSamples(); 699 | this.cacheMixerOutputLevel(); 700 | 701 | this.channel1.envelopeSweepsLast = (nr12 & 0x7) - 1; 702 | if (this.channel1.totalLength === 0) { 703 | this.channel1.totalLength = 0x40; 704 | } 705 | if ( 706 | this.channel1.lastTimeSweep > 0 || 707 | this.channel1.frequencySweepDivider > 0 708 | ) { 709 | this.memory[0xff26] |= 0x1; 710 | } else { 711 | this.memory[0xff26] &= 0xfe; 712 | } 713 | if ((data & 0x40) === 0x40) { 714 | this.memory[0xff26] |= 0x1; 715 | } 716 | this.channel1.shadowFrequency = this.channel1.frequency; 717 | //Reset frequency overflow check + frequency sweep type check: 718 | this.channel1.sweepFault = false; 719 | //Supposed to run immediately: 720 | this.performChannel1AudioSweepDummy(); 721 | } 722 | this.channel1.checkEnabled(); 723 | this.memory[0xff14] = data; 724 | } 725 | }; 726 | //NR20 (Unused I/O): 727 | this.gameboy.highMemoryWriter[0x15] = this.gameboy.memoryWriter[0xff15] = this.gameboy.memoryNew.writeIllegal; 728 | //NR21: 729 | this.gameboy.highMemoryWriter[0x16] = this.gameboy.memoryWriter[0xff16] = (address: number, data: number) => { 730 | if (this.enabled || !this.gameboy.cartridge.useGbcMode) { 731 | if (this.enabled) { 732 | this.run(); 733 | } else { 734 | data &= 0x3f; 735 | } 736 | this.channel2.setDuty(data); 737 | this.channel2.setLength(data); 738 | this.memory[0xff16] = data; 739 | this.channel2.checkEnabled(); 740 | } 741 | }; 742 | //NR22: 743 | this.gameboy.highMemoryWriter[0x17] = this.gameboy.memoryWriter[0xff17] = (address: number, data: number) => { 744 | if (this.enabled) { 745 | this.run(); 746 | this.channel2.setEnvelopeVolume(0xff17, data); 747 | this.channel2.setEnvelopeType(data); 748 | this.memory[0xff17] = data; 749 | this.channel2.checkVolumeEnabled(); 750 | } 751 | }; 752 | //NR23: 753 | this.gameboy.highMemoryWriter[0x18] = this.gameboy.memoryWriter[0xff18] = (address: number, data: number) => { 754 | if (this.enabled) { 755 | this.run(); 756 | this.channel2.setFrequency(data); 757 | } 758 | }; 759 | //NR24: 760 | this.gameboy.highMemoryWriter[0x19] = this.gameboy.memoryWriter[0xff19] = (address: number, data: number) => { 761 | if (this.enabled) { 762 | this.run(); 763 | if (data > 0x7f) { 764 | //Reload 0xFF17: 765 | const nr22 = this.memory[0xff17]; 766 | this.channel2.envelopeVolume = nr22 >> 4; 767 | 768 | this.channel2.setFirstStageSamples(); 769 | this.channel2.setSecondStageSamples(); 770 | this.channel2.setThirdStageSamples(); 771 | this.cacheMixerOutputLevel(); 772 | 773 | this.channel2.envelopeSweepsLast = (nr22 & 0x7) - 1; 774 | if (this.channel2.totalLength === 0) { 775 | this.channel2.totalLength = 0x40; 776 | } 777 | if ((data & 0x40) === 0x40) { 778 | this.memory[0xff26] |= 0x2; 779 | } 780 | } 781 | this.channel2.consecutive = (data & 0x40) === 0x0; 782 | this.channel2.setHighFrequency(data); 783 | this.memory[0xff19] = data; 784 | this.channel2.checkEnabled(); 785 | } 786 | }; 787 | //NR30: 788 | this.gameboy.highMemoryWriter[0x1a] = this.gameboy.memoryWriter[0xff1a] = (address: number, data: number) => { 789 | if (this.enabled) { 790 | this.run(); 791 | if (!this.channel3CanPlay && data >= 0x80) { 792 | this.channel3LastSampleLookup = 0; 793 | this.cacheChannel3Update(); 794 | } 795 | this.channel3CanPlay = data > 0x7f; 796 | if ( 797 | this.channel3CanPlay && 798 | this.memory[0xff1a] > 0x7f && 799 | !this.channel3Consecutive 800 | ) { 801 | this.memory[0xff26] |= 0x4; 802 | } 803 | this.memory[0xff1a] = data; 804 | //this.checkChannel3Enable(); 805 | } 806 | }; 807 | //NR31: 808 | this.gameboy.highMemoryWriter[0x1b] = this.gameboy.memoryWriter[0xff1b] = (address: number, data: number) => { 809 | if (this.enabled || !this.gameboy.cartridge.useGbcMode) { 810 | if (this.enabled) { 811 | this.run(); 812 | } 813 | this.channel3TotalLength = 0x100 - data; 814 | this.checkChannel3Enable(); 815 | } 816 | }; 817 | //NR32: 818 | this.gameboy.highMemoryWriter[0x1c] = this.gameboy.memoryWriter[0xff1c] = (address: number, data: number) => { 819 | if (this.enabled) { 820 | this.run(); 821 | data &= 0x60; 822 | this.memory[0xff1c] = data; 823 | this.channel3PatternType = data === 0 ? 4 : (data >> 5) - 1; 824 | } 825 | }; 826 | //NR33: 827 | this.gameboy.highMemoryWriter[0x1d] = this.gameboy.memoryWriter[0xff1d] = (address: number, data: number) => { 828 | if (this.enabled) { 829 | this.run(); 830 | this.channel3frequency = this.channel3frequency & 0x700 | data; 831 | this.channel3FrequencyPeriod = 0x800 - this.channel3frequency << 1; 832 | } 833 | }; 834 | //NR34: 835 | this.gameboy.highMemoryWriter[0x1e] = this.gameboy.memoryWriter[0xff1e] = (address: number, data: number) => { 836 | if (this.enabled) { 837 | this.run(); 838 | if (data > 0x7f) { 839 | if (this.channel3TotalLength === 0) { 840 | this.channel3TotalLength = 0x100; 841 | } 842 | this.channel3LastSampleLookup = 0; 843 | if ((data & 0x40) === 0x40) { 844 | this.memory[0xff26] |= 0x4; 845 | } 846 | } 847 | this.channel3Consecutive = (data & 0x40) === 0x0; 848 | this.channel3frequency = (data & 0x7) << 8 | this.channel3frequency & 0xff; 849 | this.channel3FrequencyPeriod = 0x800 - this.channel3frequency << 1; 850 | this.memory[0xff1e] = data; 851 | this.checkChannel3Enable(); 852 | } 853 | }; 854 | 855 | //NR40 (Unused I/O): 856 | this.gameboy.highMemoryWriter[0x1f] = this.gameboy.memoryWriter[0xff1f] = this.gameboy.memoryNew.writeIllegal; 857 | //NR41: 858 | this.gameboy.highMemoryWriter[0x20] = this.gameboy.memoryWriter[0xff20] = (address: number, data: number) => { 859 | if (this.enabled || !this.gameboy.cartridge.useGbcMode) { 860 | if (this.enabled) { 861 | this.run(); 862 | } 863 | this.channel4TotalLength = 0x40 - (data & 0x3f); 864 | this.checkChannel4Enable(); 865 | } 866 | }; 867 | //NR42: 868 | this.gameboy.highMemoryWriter[0x21] = this.gameboy.memoryWriter[0xff21] = (address: number, data: number) => { 869 | if (this.enabled) { 870 | this.run(); 871 | if (this.channel4Enabled && this.channel4EnvelopeSweeps === 0) { 872 | //Zombie Volume PAPU Bug: 873 | if (((this.memory[0xff21] ^ data) & 0x8) === 0x8) { 874 | if ((this.memory[0xff21] & 0x8) === 0) { 875 | if ((this.memory[0xff21] & 0x7) === 0x7) { 876 | this.channel4EnvelopeVolume += 2; 877 | } else { 878 | ++this.channel4EnvelopeVolume; 879 | } 880 | } 881 | this.channel4EnvelopeVolume = 16 - this.channel4EnvelopeVolume & 0xf; 882 | } else if ((this.memory[0xff21] & 0xf) === 0x8) { 883 | this.channel4EnvelopeVolume = 1 + this.channel4EnvelopeVolume & 0xf; 884 | } 885 | this.channel4CurrentVolume = this.channel4EnvelopeVolume << this.channel4VolumeShifter; 886 | } 887 | this.channel4EnvelopeType = (data & 0x08) === 0x08; 888 | this.memory[0xff21] = data; 889 | this.cacheChannel4Update(); 890 | this.checkChannel4VolumeEnable(); 891 | } 892 | }; 893 | //NR43: 894 | this.gameboy.highMemoryWriter[0x22] = this.gameboy.memoryWriter[0xff22] = (address: number, data: number) => { 895 | if (this.enabled) { 896 | this.run(); 897 | this.channel4FrequencyPeriod = Math.max((data & 0x7) << 4, 8) << (data >> 4); 898 | var bitWidth = data & 0x8; 899 | if (bitWidth === 0x8 && this.channel4BitRange === 0x7fff || bitWidth === 0 && this.channel4BitRange === 0x7f) { 900 | this.channel4LastSampleLookup = 0; 901 | this.channel4BitRange = bitWidth === 0x8 ? 0x7f : 0x7fff; 902 | this.channel4VolumeShifter = bitWidth === 0x8 ? 7 : 15; 903 | this.channel4CurrentVolume = this.channel4EnvelopeVolume << this.channel4VolumeShifter; 904 | this.noiseSampleTable = bitWidth === 0x8 ? this.LSFR7Table : this.LSFR15Table; 905 | } 906 | this.memory[0xff22] = data; 907 | this.cacheChannel4Update(); 908 | } 909 | }; 910 | //NR44: 911 | this.gameboy.highMemoryWriter[0x23] = this.gameboy.memoryWriter[0xff23] = (address: number, data: number) => { 912 | if (this.enabled) { 913 | this.run(); 914 | this.memory[0xff23] = data; 915 | this.channel4Consecutive = (data & 0x40) === 0x0; 916 | if (data > 0x7f) { 917 | var nr42 = this.memory[0xff21]; 918 | this.channel4EnvelopeVolume = nr42 >> 4; 919 | this.channel4CurrentVolume = this.channel4EnvelopeVolume << this.channel4VolumeShifter; 920 | this.channel4EnvelopeSweepsLast = (nr42 & 0x7) - 1; 921 | if (this.channel4TotalLength === 0) { 922 | this.channel4TotalLength = 0x40; 923 | } 924 | if ((data & 0x40) === 0x40) { 925 | this.memory[0xff26] |= 0x8; 926 | } 927 | } 928 | this.checkChannel4Enable(); 929 | } 930 | }; 931 | 932 | this.gameboy.memoryNew.setWriter(MemoryLayout.soundChannelVolumeControlAddress, this.channelVolumeControlWriter); 933 | this.gameboy.memoryNew.setHighWriter(MemoryLayout.soundChannelVolumeControlAddress, this.channelVolumeControlWriter); 934 | 935 | //NR51: 936 | this.gameboy.highMemoryWriter[0x25] = this.gameboy.memoryWriter[0xff25] = (address: number, data: number) => { 937 | if ( 938 | this.enabled && 939 | this.memory[0xff25] !== data 940 | ) { 941 | this.run(); 942 | this.memory[0xff25] = data; 943 | this.channel1.rightChannelEnabled = (data & 0x01) === 0x01; 944 | this.channel2.rightChannelEnabled = (data & 0x02) === 0x02; 945 | this.rightChannel3 = (data & 0x04) === 0x04; 946 | this.rightChannel4 = (data & 0x08) === 0x08; 947 | this.channel1.leftChannelEnabled = (data & 0x10) === 0x10; 948 | this.channel2.leftChannelEnabled = (data & 0x20) === 0x20; 949 | this.leftChannel3 = (data & 0x40) === 0x40; 950 | this.leftChannel4 = data > 0x7f; 951 | 952 | this.channel1.setFirstStageSamples(); 953 | this.channel1.setSecondStageSamples(); 954 | this.channel1.setThirdStageSamples(); 955 | this.cacheMixerOutputLevel(); 956 | 957 | this.channel2.setFirstStageSamples(); 958 | this.channel2.setSecondStageSamples(); 959 | this.channel2.setThirdStageSamples(); 960 | this.cacheMixerOutputLevel(); 961 | 962 | this.cacheChannel3OutputLevel(); 963 | this.cacheChannel4OutputLevel(); 964 | } 965 | }; 966 | //NR52: 967 | this.gameboy.highMemoryWriter[0x26] = this.gameboy.memoryWriter[0xff26] = (address: number, data: number) => { 968 | this.run(); 969 | 970 | const action = data > 0x7f ? 971 | "Enable" : 972 | "Disable"; 973 | 974 | if (!this.enabled && action === "Enable") { 975 | this.enable(); 976 | } else if (this.enabled && action === "Disable") { 977 | this.disable(); 978 | } 979 | }; 980 | } 981 | 982 | registerWaveformMemoryWriters() { 983 | // WAVE PCM RAM: 984 | this.gameboy.highMemoryWriter[0x30] = this.gameboy.memoryWriter[0xff30] = (address: number, data: number) => { 985 | this.writeWaveformRam(0, data); 986 | }; 987 | this.gameboy.highMemoryWriter[0x31] = this.gameboy.memoryWriter[0xff31] = (address: number, data: number) => { 988 | this.writeWaveformRam(0x1, data); 989 | }; 990 | this.gameboy.highMemoryWriter[0x32] = this.gameboy.memoryWriter[0xff32] = (address: number, data: number) => { 991 | this.writeWaveformRam(0x2, data); 992 | }; 993 | this.gameboy.highMemoryWriter[0x33] = this.gameboy.memoryWriter[0xff33] = (address: number, data: number) => { 994 | this.writeWaveformRam(0x3, data); 995 | }; 996 | this.gameboy.highMemoryWriter[0x34] = this.gameboy.memoryWriter[0xff34] = (address: number, data: number) => { 997 | this.writeWaveformRam(0x4, data); 998 | }; 999 | this.gameboy.highMemoryWriter[0x35] = this.gameboy.memoryWriter[0xff35] = (address: number, data: number) => { 1000 | this.writeWaveformRam(0x5, data); 1001 | }; 1002 | this.gameboy.highMemoryWriter[0x36] = this.gameboy.memoryWriter[0xff36] = (address: number, data: number) => { 1003 | this.writeWaveformRam(0x6, data); 1004 | }; 1005 | this.gameboy.highMemoryWriter[0x37] = this.gameboy.memoryWriter[0xff37] = (address: number, data: number) => { 1006 | this.writeWaveformRam(0x7, data); 1007 | }; 1008 | this.gameboy.highMemoryWriter[0x38] = this.gameboy.memoryWriter[0xff38] = (address: number, data: number) => { 1009 | this.writeWaveformRam(0x8, data); 1010 | }; 1011 | this.gameboy.highMemoryWriter[0x39] = this.gameboy.memoryWriter[0xff39] = (address: number, data: number) => { 1012 | this.writeWaveformRam(0x9, data); 1013 | }; 1014 | this.gameboy.highMemoryWriter[0x3a] = this.gameboy.memoryWriter[0xff3a] = (address: number, data: number) => { 1015 | this.writeWaveformRam(0xa, data); 1016 | }; 1017 | this.gameboy.highMemoryWriter[0x3b] = this.gameboy.memoryWriter[0xff3b] = (address: number, data: number) => { 1018 | this.writeWaveformRam(0xb, data); 1019 | }; 1020 | this.gameboy.highMemoryWriter[0x3c] = this.gameboy.memoryWriter[0xff3c] = (address: number, data: number) => { 1021 | this.writeWaveformRam(0xc, data); 1022 | }; 1023 | this.gameboy.highMemoryWriter[0x3d] = this.gameboy.memoryWriter[0xff3d] = (address: number, data: number) => { 1024 | this.writeWaveformRam(0xd, data); 1025 | }; 1026 | this.gameboy.highMemoryWriter[0x3e] = this.gameboy.memoryWriter[0xff3e] = (address: number, data: number) => { 1027 | this.writeWaveformRam(0xe, data); 1028 | }; 1029 | this.gameboy.highMemoryWriter[0x3f] = this.gameboy.memoryWriter[0xff3f] = (address: number, data: number) => { 1030 | this.writeWaveformRam(0xf, data); 1031 | }; 1032 | } 1033 | 1034 | writeWaveformRam(address: number, data: number) { 1035 | if (this.channel3CanPlay) { 1036 | this.run(); 1037 | } 1038 | this.memory[0xff30 | address] = data; 1039 | address <<= 1; 1040 | this.channel3PcmData[address] = data >> 4; 1041 | this.channel3PcmData[address | 1] = data & 0xf; 1042 | } 1043 | 1044 | channelVolumeControlWriter = (address: number, data: number) => { 1045 | if ( 1046 | !this.enabled || 1047 | this.memory[MemoryLayout.soundChannelVolumeControlAddress] === data 1048 | ) return; 1049 | 1050 | this.run(); 1051 | this.memory[MemoryLayout.soundChannelVolumeControlAddress] = data; 1052 | this.cartridgeLeftChannelInputVolume = (data >> 4 & 0x07) + 1; 1053 | this.cartridgeRightChannelInputVolume = (data & 0x07) + 1; 1054 | 1055 | this.cacheMixerOutputLevel(); 1056 | }; 1057 | } 1058 | -------------------------------------------------------------------------------- /src/audio/AudioDevice.ts: -------------------------------------------------------------------------------- 1 | import Resampler from "./Resampler"; 2 | import { WorkerUrl } from "worker-url"; 3 | 4 | const AudioContextClass = typeof window !== "undefined" ? 5 | (typeof AudioContext !== "undefined" ? AudioContext : (window as any).webkitAudioContext) : 6 | null; 7 | 8 | export default class AudioDevice { 9 | inputBuffer: Float32Array; 10 | inputBufferSize: number; 11 | 12 | resampler: Resampler; 13 | 14 | outputBuffer: Float32Array; 15 | outputBufferSize: number; 16 | outputBufferStart: number; 17 | outputBufferEnd: number; 18 | 19 | volume: number = 1; 20 | context: AudioContext; 21 | audioWorkletSupport: boolean; 22 | samplesPerCallback: number = 2048; // Has to be between 2048 and 4096 (If over, then samples are ignored, if under then silence is added). 23 | channelsAllocated: number; 24 | sampleRate: number; 25 | bufferSize: number; 26 | minBufferSize: number; 27 | maxBufferSize: number; 28 | 29 | gainNode: GainNode; 30 | audioNode: AudioNode; 31 | 32 | constructor({ context, channels, minBufferSize }: any) { 33 | this.context = context; 34 | this.channelsAllocated = Math.max(channels, 1); 35 | this.bufferSize = this.samplesPerCallback * this.channelsAllocated; 36 | this.minBufferSize = minBufferSize || this.bufferSize; 37 | } 38 | 39 | setSampleRate(sampleRate: number) { 40 | this.sampleRate = sampleRate; 41 | } 42 | 43 | setMaxBufferSize(maxBufferSize: number) { 44 | this.maxBufferSize = Math.floor(maxBufferSize) > this.minBufferSize + this.channelsAllocated ? maxBufferSize & -this.channelsAllocated : this.minBufferSize * this.channelsAllocated; 45 | } 46 | 47 | writeAudio(buffer: Float32Array) { 48 | let bufferIndex = 0; 49 | while (bufferIndex < buffer.length && this.inputBufferSize < this.maxBufferSize) { 50 | this.inputBuffer[this.inputBufferSize++] = buffer[bufferIndex++]; 51 | } 52 | } 53 | 54 | remainingBuffer() { 55 | return ( 56 | Math.floor( 57 | this.resampledSamplesLeft() * 58 | this.resampler?.ratioWeight / 59 | this.channelsAllocated 60 | ) * 61 | this.channelsAllocated 62 | ) + this.inputBufferSize; 63 | } 64 | 65 | async init() { 66 | if (!this.context) this.context = new AudioContextClass(); 67 | 68 | if (!this.audioNode) { 69 | this.gainNode = this.context.createGain(); 70 | 71 | if (!this.context?.audioWorklet) { 72 | const workletUrl = new WorkerUrl( 73 | new URL("./noise.worklet.ts", import.meta.url), 74 | { 75 | name: "worklet" 76 | } 77 | ); 78 | await this.context.audioWorklet.addModule(workletUrl); 79 | 80 | this.audioNode = new AudioWorkletNode(this.context, "noise-generator"); 81 | } else { 82 | const scriptProcessorNode = this.audioNode = this.context.createScriptProcessor(this.samplesPerCallback, 0, this.channelsAllocated); 83 | scriptProcessorNode.onaudioprocess = e => this.processAudio(e); 84 | } 85 | 86 | this.audioNode.connect(this.gainNode); 87 | this.gainNode.connect(this.context.destination); 88 | this.resetAudioBuffer(this.context.sampleRate); 89 | } 90 | } 91 | 92 | processAudio(e: AudioProcessingEvent) { 93 | const channels: Float32Array[] = []; 94 | let channel = 0; 95 | 96 | while (channel < this.channelsAllocated) { 97 | channels[channel] = e.outputBuffer.getChannelData(channel); 98 | ++channel; 99 | } 100 | 101 | this.refillResampledBuffer(); 102 | 103 | let index = 0; 104 | while ( 105 | index < this.samplesPerCallback && 106 | this.outputBufferStart !== this.outputBufferEnd 107 | ) { 108 | channel = 0; 109 | while (channel < this.channelsAllocated) { 110 | channels[channel][index] = this.outputBuffer[this.outputBufferStart++]; 111 | 112 | ++channel; 113 | } 114 | 115 | if (this.outputBufferStart === this.outputBufferSize) { 116 | this.outputBufferStart = 0; 117 | } 118 | 119 | ++index; 120 | } 121 | 122 | while (index < this.samplesPerCallback) { 123 | for (channel = 0; channel < this.channelsAllocated; ++channel) { 124 | channels[channel][index] = 0; 125 | } 126 | ++index; 127 | } 128 | } 129 | 130 | setVolume(volume: number) { 131 | this.volume = Math.max(0, Math.min(1, volume)); 132 | this.gainNode.gain.setTargetAtTime(this.volume, this.context.currentTime, 0); 133 | } 134 | 135 | resetAudioBuffer(targetSampleRate: number) { 136 | this.inputBufferSize = this.outputBufferEnd = this.outputBufferStart = 0; 137 | this.initializeResampler(targetSampleRate); 138 | this.outputBuffer = new Float32Array(this.outputBufferSize); 139 | } 140 | 141 | refillResampledBuffer() { 142 | if (this.inputBufferSize > 0) { 143 | const resampleLength = this.resampler.resample(this.getBufferSamples()); 144 | const resampledResult = this.resampler.outputBuffer; 145 | 146 | for (let i = 0; i < resampleLength;) { 147 | this.outputBuffer[this.outputBufferEnd++] = resampledResult[i++]; 148 | 149 | if (this.outputBufferEnd === this.outputBufferSize) { 150 | this.outputBufferEnd = 0; 151 | } 152 | 153 | if (this.outputBufferStart === this.outputBufferEnd) { 154 | this.outputBufferStart += this.channelsAllocated; 155 | 156 | if (this.outputBufferStart === this.outputBufferSize) { 157 | this.outputBufferStart = 0; 158 | } 159 | } 160 | } 161 | this.inputBufferSize = 0; 162 | } 163 | } 164 | 165 | initializeResampler(targetSampleRate: number) { 166 | this.inputBuffer = new Float32Array(this.maxBufferSize); 167 | this.outputBufferSize = Math.max( 168 | this.maxBufferSize * Math.ceil(targetSampleRate / this.sampleRate) + this.channelsAllocated, 169 | this.bufferSize 170 | ); 171 | 172 | this.resampler = new Resampler( 173 | this.sampleRate, 174 | targetSampleRate, 175 | this.channelsAllocated, 176 | this.outputBufferSize 177 | ); 178 | } 179 | 180 | resampledSamplesLeft() { 181 | return ( 182 | this.outputBufferStart <= this.outputBufferEnd ? 183 | 0 : 184 | this.outputBufferSize 185 | ) + this.outputBufferEnd - this.outputBufferStart; 186 | } 187 | 188 | getBufferSamples() { 189 | return this.inputBuffer.subarray(0, this.inputBufferSize); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/audio/Resampler.ts: -------------------------------------------------------------------------------- 1 | export default class Resampler { 2 | ratioWeight: number; 3 | lastWeight: number; 4 | tailExists: boolean; 5 | outputBuffer: Float32Array; 6 | lastOutput: Float32Array; 7 | 8 | constructor( 9 | private fromSampleRate: number, 10 | private toSampleRate: number, 11 | private channels: number, 12 | private outputBufferSize: number 13 | ) { 14 | if ( 15 | this.fromSampleRate <= 0 || 16 | this.toSampleRate <= 0 || 17 | this.channels <= 0 18 | ) throw new Error("Invalid settings specified for the resampler."); 19 | 20 | this.ratioWeight = this.fromSampleRate / this.toSampleRate; 21 | if (this.fromSampleRate < this.toSampleRate) { 22 | this.lastWeight = 1; 23 | } 24 | 25 | this.outputBuffer = new Float32Array(this.outputBufferSize); 26 | this.lastOutput = new Float32Array(this.channels); 27 | } 28 | 29 | resample(buffer: Float32Array) { 30 | let bufferLength = buffer.length; 31 | 32 | if ((bufferLength % this.channels) !== 0) throw new Error("Buffer was of incorrect sample length."); 33 | if (bufferLength === 0) return; 34 | 35 | let weight = this.lastWeight; 36 | let firstWeight = 0; 37 | let secondWeight = 0; 38 | let sourceOffset = 0; 39 | let outputOffset = 0; 40 | 41 | for (; weight < 1; weight += this.ratioWeight) { 42 | secondWeight = weight % 1; 43 | firstWeight = 1 - secondWeight; 44 | 45 | for (let channel = 0; channel < this.channels; channel++) { 46 | this.outputBuffer[outputOffset++] = ( 47 | ( 48 | this.lastOutput[channel] * 49 | firstWeight 50 | ) + 51 | ( 52 | buffer[channel] * 53 | secondWeight 54 | ) 55 | ); 56 | } 57 | } 58 | 59 | weight -= 1; 60 | 61 | for ( 62 | bufferLength -= this.channels, sourceOffset = Math.floor(weight) * this.channels; 63 | outputOffset < this.outputBufferSize && sourceOffset < bufferLength; 64 | ) { 65 | secondWeight = weight % 1; 66 | firstWeight = 1 - secondWeight; 67 | 68 | for (let channel = 0; channel < this.channels; channel++) { 69 | this.outputBuffer[outputOffset++] = ( 70 | ( 71 | buffer[sourceOffset + channel] * 72 | firstWeight 73 | ) + 74 | ( 75 | buffer[sourceOffset + this.channels + channel] * 76 | secondWeight 77 | ) 78 | ); 79 | } 80 | 81 | weight += this.ratioWeight; 82 | sourceOffset = Math.floor(weight) * this.channels; 83 | } 84 | 85 | for (let channel = 0; channel < this.channels; ++channel) { 86 | this.lastOutput[channel] = buffer[sourceOffset++]; 87 | } 88 | 89 | this.lastWeight = weight % 1; 90 | 91 | return outputOffset; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/audio/SquareAudioChannel.ts: -------------------------------------------------------------------------------- 1 | import GameBoy from "../GameBoy"; 2 | import settings from "../settings"; 3 | import dutyLookup from "../dutyLookup"; 4 | import AudioChannel from "./AudioChannel"; 5 | 6 | export default class SquareAudioChannel extends AudioChannel { 7 | currentSampleLeft: number; 8 | currentSampleRight: number; 9 | currentSampleLeftSecondary: number; 10 | currentSampleRightSecondary: number; 11 | currentSampleLeftTrimary: number; 12 | currentSampleRightTrimary: number; 13 | frequencyCounter: number; 14 | frequencyTracker: number; 15 | dutyTracker: number; 16 | cachedDuty: boolean[]; 17 | totalLength: number; 18 | envelopeVolume: number; 19 | envelopeType: boolean; 20 | consecutive: boolean; 21 | frequency: number; 22 | shadowFrequency: number; 23 | envelopeSweeps: number; 24 | envelopeSweepsLast: number; 25 | 26 | sweepEnabled: boolean = false; 27 | sweepFault: boolean; 28 | timeSweep: number; 29 | lastTimeSweep: number; 30 | swept: boolean; 31 | decreaseSweep: boolean; 32 | frequencySweepDivider: number; 33 | 34 | constructor( 35 | protected gameboy: GameBoy, 36 | options: { sweepEnabled?: boolean; } = {} 37 | ) { 38 | super(gameboy); 39 | this.sweepEnabled = !!options.sweepEnabled; 40 | } 41 | 42 | init() { 43 | this.leftChannelEnabled = false; 44 | this.rightChannelEnabled = false; 45 | this.frequency = 0; 46 | this.consecutive = false; 47 | } 48 | 49 | setInitialState() { 50 | this.enabled = false; 51 | this.canPlay = false; 52 | this.frequencyCounter = 0x2000; 53 | this.frequencyTracker = 0x2000; 54 | this.dutyTracker = 0; 55 | this.cachedDuty = dutyLookup[2]; 56 | this.totalLength = 0; 57 | this.envelopeVolume = 0; 58 | this.envelopeType = false; 59 | this.consecutive = true; 60 | this.frequency = 0; 61 | this.shadowFrequency = 0; 62 | this.envelopeSweeps = 0; 63 | this.envelopeSweepsLast = 0; 64 | 65 | if (this.sweepEnabled) { 66 | this.sweepFault = false; 67 | this.timeSweep = 1; 68 | this.lastTimeSweep = 0; 69 | this.swept = false; 70 | this.frequencySweepDivider = 0; 71 | this.decreaseSweep = false; 72 | } 73 | } 74 | 75 | setSkippedBootRomState() { 76 | this.leftChannelEnabled = true; 77 | this.rightChannelEnabled = true; 78 | this.frequencyCounter = 0x200; 79 | this.frequencyTracker = 0x2000; 80 | this.dutyTracker = 0; 81 | this.cachedDuty = dutyLookup[2]; 82 | this.totalLength = 0; 83 | this.envelopeVolume = 0; 84 | this.envelopeType = false; 85 | this.consecutive = true; 86 | this.frequency = 1985; 87 | this.shadowFrequency = 1985; 88 | this.envelopeSweeps = 0; 89 | this.envelopeSweepsLast = 0; 90 | 91 | if (this.sweepEnabled) { 92 | this.swept = false; 93 | this.sweepFault = true; 94 | this.decreaseSweep = false; 95 | this.frequencySweepDivider = 0; 96 | this.timeSweep = 1; 97 | this.lastTimeSweep = 0; 98 | } 99 | } 100 | 101 | envelope() { 102 | if (this.envelopeSweepsLast > -1) { 103 | if (this.envelopeSweeps > 0) { 104 | --this.envelopeSweeps; 105 | } else { 106 | if (!this.envelopeType) { 107 | if (this.envelopeVolume > 0) { 108 | --this.envelopeVolume; 109 | this.envelopeSweeps = this.envelopeSweepsLast; 110 | this.setFirstStageSamples(); 111 | this.setSecondStageSamples(); 112 | this.setThirdStageSamples(); 113 | this.gameboy.audioController.cacheMixerOutputLevel(); 114 | } else { 115 | this.envelopeSweepsLast = -1; 116 | } 117 | } else if (this.envelopeVolume < 0xf) { 118 | ++this.envelopeVolume; 119 | this.envelopeSweeps = this.envelopeSweepsLast; 120 | this.setFirstStageSamples(); 121 | this.setSecondStageSamples(); 122 | this.setThirdStageSamples(); 123 | this.gameboy.audioController.cacheMixerOutputLevel(); 124 | } else { 125 | this.envelopeSweepsLast = -1; 126 | } 127 | } 128 | } 129 | } 130 | 131 | setSweep(data: number) { 132 | if (this.decreaseSweep && (data & 0x08) === 0) { 133 | if (this.swept) { 134 | this.sweepFault = true; 135 | } 136 | } 137 | this.lastTimeSweep = (data & 0x70) >> 4; 138 | this.frequencySweepDivider = data & 0x07; 139 | this.decreaseSweep = (data & 0x08) === 0x08; 140 | } 141 | 142 | setDuty(data: number) { 143 | this.cachedDuty = dutyLookup[data >> 6]; 144 | } 145 | 146 | setLength(data: number) { 147 | this.totalLength = 0x40 - (data & 0x3f); 148 | } 149 | 150 | setEnvelopeVolume(address: number, data: number) { 151 | if ( 152 | this.enabled && 153 | this.envelopeSweeps === 0 154 | ) { 155 | // Zombie Volume PAPU Bug: 156 | if (((this.gameboy.memory[address] ^ data) & 0x8) === 0x8) { 157 | if ((this.gameboy.memory[address] & 0x8) === 0) { 158 | if ((this.gameboy.memory[address] & 0x7) === 0x7) { 159 | this.envelopeVolume += 2; 160 | } else { 161 | ++this.envelopeVolume; 162 | } 163 | } 164 | this.envelopeVolume = 16 - this.envelopeVolume & 0xf; 165 | } else if ((this.gameboy.memory[address] & 0xf) === 0x8) { 166 | this.envelopeVolume = 1 + this.envelopeVolume & 0xf; 167 | } 168 | 169 | this.setFirstStageSamples(); 170 | this.setSecondStageSamples(); 171 | this.setThirdStageSamples(); 172 | this.gameboy.audioController.cacheMixerOutputLevel(); 173 | } 174 | } 175 | 176 | setEnvelopeType(data: number) { 177 | this.envelopeType = (data & 0x08) === 0x08; 178 | } 179 | 180 | setFrequency(data: number) { 181 | this.frequency = this.frequency & 0x700 | data; 182 | this.frequencyTracker = 0x800 - this.frequency << 2; 183 | } 184 | 185 | setHighFrequency(data: number) { 186 | this.frequency = (data & 0x7) << 8 | this.frequency & 0xff; 187 | this.frequencyTracker = 0x800 - this.frequency << 2; 188 | } 189 | 190 | checkEnabled() { 191 | this.enabled = ( 192 | ( 193 | this.consecutive || 194 | this.totalLength > 0 195 | ) && 196 | !this.sweepFault && 197 | this.canPlay 198 | ); 199 | this.setSecondStageSamples(); 200 | this.setThirdStageSamples(); 201 | this.gameboy.audioController.cacheMixerOutputLevel(); 202 | } 203 | 204 | checkVolumeEnabled() { 205 | this.canPlay = this.gameboy.memory[0xff12] > 7; 206 | this.checkEnabled(); 207 | this.setSecondStageSamples(); 208 | this.setThirdStageSamples(); 209 | this.gameboy.audioController.cacheMixerOutputLevel(); 210 | } 211 | 212 | length(value: number) { 213 | if (this.totalLength > 1) { 214 | --this.totalLength; 215 | } else if (this.totalLength === 1) { 216 | this.totalLength = 0; 217 | this.checkEnabled(); 218 | this.gameboy.memory[0xff26] &= value; // set Channel On Flag Off 219 | } 220 | } 221 | 222 | sweep() { 223 | if (!this.sweepFault && this.timeSweep > 0) { 224 | if (--this.timeSweep === 0) { 225 | if (this.lastTimeSweep > 0) { 226 | if (this.frequencySweepDivider > 0) { 227 | this.swept = true; 228 | if (this.decreaseSweep) { 229 | this.shadowFrequency -= this.shadowFrequency >> this.frequencySweepDivider; 230 | this.frequency = this.shadowFrequency & 0x7ff; 231 | this.frequencyTracker = 0x800 - this.frequency << 2; 232 | } else { 233 | this.shadowFrequency += this.shadowFrequency >> this.frequencySweepDivider; 234 | this.frequency = this.shadowFrequency; 235 | if (this.shadowFrequency <= 0x7ff) { 236 | this.frequencyTracker = 0x800 - this.frequency << 2; 237 | //Run overflow check twice: 238 | if (this.shadowFrequency + (this.shadowFrequency >> this.frequencySweepDivider) > 0x7ff) { 239 | this.sweepFault = true; 240 | this.checkEnabled(); 241 | this.gameboy.memory[0xff26] &= 0xfe; // Channel #1 On Flag Off 242 | } 243 | } else { 244 | this.frequency &= 0x7ff; 245 | this.sweepFault = true; 246 | this.checkEnabled(); 247 | this.gameboy.memory[0xff26] &= 0xfe; // Channel #1 On Flag Off 248 | } 249 | } 250 | this.timeSweep = this.lastTimeSweep; 251 | } else { 252 | // Channel has sweep disabled and timer becomes a length counter: 253 | this.sweepFault = true; 254 | this.checkEnabled(); 255 | } 256 | } 257 | } 258 | } 259 | } 260 | 261 | setFirstStageSamples() { 262 | this.currentSampleLeft = this.leftChannelEnabled ? 263 | this.envelopeVolume : 264 | 0; 265 | this.currentSampleRight = this.rightChannelEnabled ? 266 | this.envelopeVolume : 267 | 0; 268 | } 269 | 270 | setSecondStageSamples() { 271 | if (this.enabled) { 272 | this.currentSampleLeftSecondary = this.currentSampleLeft; 273 | this.currentSampleRightSecondary = this.currentSampleRight; 274 | } else { 275 | this.currentSampleLeftSecondary = 0; 276 | this.currentSampleRightSecondary = 0; 277 | } 278 | } 279 | 280 | setThirdStageSamples() { 281 | if ( 282 | this.cachedDuty[this.dutyTracker] && 283 | settings.enabledAudioChannels[0] 284 | ) { 285 | this.currentSampleLeftTrimary = this.currentSampleLeftSecondary; 286 | this.currentSampleRightTrimary = this.currentSampleRightSecondary; 287 | } else { 288 | this.currentSampleLeftTrimary = 0; 289 | this.currentSampleRightTrimary = 0; 290 | } 291 | } 292 | } -------------------------------------------------------------------------------- /src/audio/noise.worklet.ts: -------------------------------------------------------------------------------- 1 | class NoiseGeneratorWorklet extends AudioWorkletProcessor { 2 | static get parameterDescriptors() { 3 | return [ 4 | { name: "amplitude", defaultValue: 0.5, minValue: 0, maxValue: 1 } 5 | ]; 6 | } 7 | 8 | process(inputs, outputs, parameters: Record) { 9 | let output = outputs[0]; 10 | 11 | for (let channelIndex = 0; channelIndex < output.length; ++channelIndex) { 12 | let outputChannel = output[channelIndex]; 13 | 14 | for (let dataIndex = 0; dataIndex < outputChannel.length; ++dataIndex) { 15 | outputChannel[dataIndex] = Math.sin(dataIndex); 16 | } 17 | } 18 | 19 | return true; 20 | } 21 | } 22 | 23 | registerProcessor("noise-generator", NoiseGeneratorWorklet); -------------------------------------------------------------------------------- /src/bitInstructions.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | //RLC B 3 | //#0x00: 4 | function () { 5 | this.FCarry = this.registerB > 0x7f; 6 | this.registerB = this.registerB << 1 & 0xff | (this.FCarry ? 1 : 0); 7 | this.FHalfCarry = this.FSubtract = false; 8 | this.FZero = this.registerB === 0; 9 | }, 10 | //RLC C 11 | //#0x01: 12 | function () { 13 | this.FCarry = this.registerC > 0x7f; 14 | this.registerC = this.registerC << 1 & 0xff | (this.FCarry ? 1 : 0); 15 | this.FHalfCarry = this.FSubtract = false; 16 | this.FZero = this.registerC === 0; 17 | }, 18 | //RLC D 19 | //#0x02: 20 | function () { 21 | this.FCarry = this.registerD > 0x7f; 22 | this.registerD = this.registerD << 1 & 0xff | (this.FCarry ? 1 : 0); 23 | this.FHalfCarry = this.FSubtract = false; 24 | this.FZero = this.registerD === 0; 25 | }, 26 | //RLC E 27 | //#0x03: 28 | function () { 29 | this.FCarry = this.registerE > 0x7f; 30 | this.registerE = this.registerE << 1 & 0xff | (this.FCarry ? 1 : 0); 31 | this.FHalfCarry = this.FSubtract = false; 32 | this.FZero = this.registerE === 0; 33 | }, 34 | //RLC H 35 | //#0x04: 36 | function () { 37 | this.FCarry = this.registersHL > 0x7fff; 38 | this.registersHL = this.registersHL << 1 & 0xfe00 | 39 | (this.FCarry ? 0x100 : 0) | 40 | this.registersHL & 0xff; 41 | this.FHalfCarry = this.FSubtract = false; 42 | this.FZero = this.registersHL < 0x100; 43 | }, 44 | //RLC L 45 | //#0x05: 46 | function () { 47 | this.FCarry = (this.registersHL & 0x80) === 0x80; 48 | this.registersHL = this.registersHL & 0xff00 | 49 | this.registersHL << 1 & 0xff | 50 | (this.FCarry ? 1 : 0); 51 | this.FHalfCarry = this.FSubtract = false; 52 | this.FZero = (this.registersHL & 0xff) === 0; 53 | }, 54 | //RLC (HL) 55 | //#0x06: 56 | function () { 57 | var temp_var = this.readMemory(this.registersHL); 58 | this.FCarry = temp_var > 0x7f; 59 | temp_var = temp_var << 1 & 0xff | (this.FCarry ? 1 : 0); 60 | this.memoryWriter[this.registersHL](this.registersHL, temp_var); 61 | this.FHalfCarry = this.FSubtract = false; 62 | this.FZero = temp_var === 0; 63 | }, 64 | //RLC A 65 | //#0x07: 66 | function () { 67 | this.FCarry = this.registerA > 0x7f; 68 | this.registerA = this.registerA << 1 & 0xff | (this.FCarry ? 1 : 0); 69 | this.FHalfCarry = this.FSubtract = false; 70 | this.FZero = this.registerA === 0; 71 | }, 72 | //RRC B 73 | //#0x08: 74 | function () { 75 | this.FCarry = (this.registerB & 0x01) === 0x01; 76 | this.registerB = (this.FCarry ? 0x80 : 0) | this.registerB >> 1; 77 | this.FHalfCarry = this.FSubtract = false; 78 | this.FZero = this.registerB === 0; 79 | }, 80 | //RRC C 81 | //#0x09: 82 | function () { 83 | this.FCarry = (this.registerC & 0x01) === 0x01; 84 | this.registerC = (this.FCarry ? 0x80 : 0) | this.registerC >> 1; 85 | this.FHalfCarry = this.FSubtract = false; 86 | this.FZero = this.registerC === 0; 87 | }, 88 | //RRC D 89 | //#0x0A: 90 | function () { 91 | this.FCarry = (this.registerD & 0x01) === 0x01; 92 | this.registerD = (this.FCarry ? 0x80 : 0) | this.registerD >> 1; 93 | this.FHalfCarry = this.FSubtract = false; 94 | this.FZero = this.registerD === 0; 95 | }, 96 | //RRC E 97 | //#0x0B: 98 | function () { 99 | this.FCarry = (this.registerE & 0x01) === 0x01; 100 | this.registerE = (this.FCarry ? 0x80 : 0) | this.registerE >> 1; 101 | this.FHalfCarry = this.FSubtract = false; 102 | this.FZero = this.registerE === 0; 103 | }, 104 | //RRC H 105 | //#0x0C: 106 | function () { 107 | this.FCarry = (this.registersHL & 0x0100) === 0x0100; 108 | this.registersHL = (this.FCarry ? 0x8000 : 0) | 109 | this.registersHL >> 1 & 0xff00 | 110 | this.registersHL & 0xff; 111 | this.FHalfCarry = this.FSubtract = false; 112 | this.FZero = this.registersHL < 0x100; 113 | }, 114 | //RRC L 115 | //#0x0D: 116 | function () { 117 | this.FCarry = (this.registersHL & 0x01) === 0x01; 118 | this.registersHL = this.registersHL & 0xff00 | 119 | (this.FCarry ? 0x80 : 0) | 120 | (this.registersHL & 0xff) >> 1; 121 | this.FHalfCarry = this.FSubtract = false; 122 | this.FZero = (this.registersHL & 0xff) === 0; 123 | }, 124 | //RRC (HL) 125 | //#0x0E: 126 | function () { 127 | var temp_var = this.readMemory(this.registersHL); 128 | this.FCarry = (temp_var & 0x01) === 0x01; 129 | temp_var = (this.FCarry ? 0x80 : 0) | temp_var >> 1; 130 | this.memoryWriter[this.registersHL](this.registersHL, temp_var); 131 | this.FHalfCarry = this.FSubtract = false; 132 | this.FZero = temp_var === 0; 133 | }, 134 | //RRC A 135 | //#0x0F: 136 | function () { 137 | this.FCarry = (this.registerA & 0x01) === 0x01; 138 | this.registerA = (this.FCarry ? 0x80 : 0) | this.registerA >> 1; 139 | this.FHalfCarry = this.FSubtract = false; 140 | this.FZero = this.registerA === 0; 141 | }, 142 | //RL B 143 | //#0x10: 144 | function () { 145 | var newFCarry = this.registerB > 0x7f; 146 | this.registerB = this.registerB << 1 & 0xff | (this.FCarry ? 1 : 0); 147 | this.FCarry = newFCarry; 148 | this.FHalfCarry = this.FSubtract = false; 149 | this.FZero = this.registerB === 0; 150 | }, 151 | //RL C 152 | //#0x11: 153 | function () { 154 | var newFCarry = this.registerC > 0x7f; 155 | this.registerC = this.registerC << 1 & 0xff | (this.FCarry ? 1 : 0); 156 | this.FCarry = newFCarry; 157 | this.FHalfCarry = this.FSubtract = false; 158 | this.FZero = this.registerC === 0; 159 | }, 160 | //RL D 161 | //#0x12: 162 | function () { 163 | var newFCarry = this.registerD > 0x7f; 164 | this.registerD = this.registerD << 1 & 0xff | (this.FCarry ? 1 : 0); 165 | this.FCarry = newFCarry; 166 | this.FHalfCarry = this.FSubtract = false; 167 | this.FZero = this.registerD === 0; 168 | }, 169 | //RL E 170 | //#0x13: 171 | function () { 172 | var newFCarry = this.registerE > 0x7f; 173 | this.registerE = this.registerE << 1 & 0xff | (this.FCarry ? 1 : 0); 174 | this.FCarry = newFCarry; 175 | this.FHalfCarry = this.FSubtract = false; 176 | this.FZero = this.registerE === 0; 177 | }, 178 | //RL H 179 | //#0x14: 180 | function () { 181 | var newFCarry = this.registersHL > 0x7fff; 182 | this.registersHL = this.registersHL << 1 & 0xfe00 | 183 | (this.FCarry ? 0x100 : 0) | 184 | this.registersHL & 0xff; 185 | this.FCarry = newFCarry; 186 | this.FHalfCarry = this.FSubtract = false; 187 | this.FZero = this.registersHL < 0x100; 188 | }, 189 | //RL L 190 | //#0x15: 191 | function () { 192 | var newFCarry = (this.registersHL & 0x80) === 0x80; 193 | this.registersHL = this.registersHL & 0xff00 | 194 | this.registersHL << 1 & 0xff | 195 | (this.FCarry ? 1 : 0); 196 | this.FCarry = newFCarry; 197 | this.FHalfCarry = this.FSubtract = false; 198 | this.FZero = (this.registersHL & 0xff) === 0; 199 | }, 200 | //RL (HL) 201 | //#0x16: 202 | function () { 203 | var temp_var = this.readMemory(this.registersHL); 204 | var newFCarry = temp_var > 0x7f; 205 | temp_var = temp_var << 1 & 0xff | (this.FCarry ? 1 : 0); 206 | this.FCarry = newFCarry; 207 | this.memoryWriter[this.registersHL](this.registersHL, temp_var); 208 | this.FHalfCarry = this.FSubtract = false; 209 | this.FZero = temp_var === 0; 210 | }, 211 | //RL A 212 | //#0x17: 213 | function () { 214 | var newFCarry = this.registerA > 0x7f; 215 | this.registerA = this.registerA << 1 & 0xff | (this.FCarry ? 1 : 0); 216 | this.FCarry = newFCarry; 217 | this.FHalfCarry = this.FSubtract = false; 218 | this.FZero = this.registerA === 0; 219 | }, 220 | //RR B 221 | //#0x18: 222 | function () { 223 | var newFCarry = (this.registerB & 0x01) === 0x01; 224 | this.registerB = (this.FCarry ? 0x80 : 0) | this.registerB >> 1; 225 | this.FCarry = newFCarry; 226 | this.FHalfCarry = this.FSubtract = false; 227 | this.FZero = this.registerB === 0; 228 | }, 229 | //RR C 230 | //#0x19: 231 | function () { 232 | var newFCarry = (this.registerC & 0x01) === 0x01; 233 | this.registerC = (this.FCarry ? 0x80 : 0) | this.registerC >> 1; 234 | this.FCarry = newFCarry; 235 | this.FHalfCarry = this.FSubtract = false; 236 | this.FZero = this.registerC === 0; 237 | }, 238 | //RR D 239 | //#0x1A: 240 | function () { 241 | var newFCarry = (this.registerD & 0x01) === 0x01; 242 | this.registerD = (this.FCarry ? 0x80 : 0) | this.registerD >> 1; 243 | this.FCarry = newFCarry; 244 | this.FHalfCarry = this.FSubtract = false; 245 | this.FZero = this.registerD === 0; 246 | }, 247 | //RR E 248 | //#0x1B: 249 | function () { 250 | var newFCarry = (this.registerE & 0x01) === 0x01; 251 | this.registerE = (this.FCarry ? 0x80 : 0) | this.registerE >> 1; 252 | this.FCarry = newFCarry; 253 | this.FHalfCarry = this.FSubtract = false; 254 | this.FZero = this.registerE === 0; 255 | }, 256 | //RR H 257 | //#0x1C: 258 | function () { 259 | var newFCarry = (this.registersHL & 0x0100) === 0x0100; 260 | this.registersHL = (this.FCarry ? 0x8000 : 0) | 261 | this.registersHL >> 1 & 0xff00 | 262 | this.registersHL & 0xff; 263 | this.FCarry = newFCarry; 264 | this.FHalfCarry = this.FSubtract = false; 265 | this.FZero = this.registersHL < 0x100; 266 | }, 267 | //RR L 268 | //#0x1D: 269 | function () { 270 | var newFCarry = (this.registersHL & 0x01) === 0x01; 271 | this.registersHL = this.registersHL & 0xff00 | 272 | (this.FCarry ? 0x80 : 0) | 273 | (this.registersHL & 0xff) >> 1; 274 | this.FCarry = newFCarry; 275 | this.FHalfCarry = this.FSubtract = false; 276 | this.FZero = (this.registersHL & 0xff) === 0; 277 | }, 278 | //RR (HL) 279 | //#0x1E: 280 | function () { 281 | var temp_var = this.readMemory(this.registersHL); 282 | var newFCarry = (temp_var & 0x01) === 0x01; 283 | temp_var = (this.FCarry ? 0x80 : 0) | temp_var >> 1; 284 | this.FCarry = newFCarry; 285 | this.memoryWriter[this.registersHL](this.registersHL, temp_var); 286 | this.FHalfCarry = this.FSubtract = false; 287 | this.FZero = temp_var === 0; 288 | }, 289 | //RR A 290 | //#0x1F: 291 | function () { 292 | var newFCarry = (this.registerA & 0x01) === 0x01; 293 | this.registerA = (this.FCarry ? 0x80 : 0) | this.registerA >> 1; 294 | this.FCarry = newFCarry; 295 | this.FHalfCarry = this.FSubtract = false; 296 | this.FZero = this.registerA === 0; 297 | }, 298 | //SLA B 299 | //#0x20: 300 | function () { 301 | this.FCarry = this.registerB > 0x7f; 302 | this.registerB = this.registerB << 1 & 0xff; 303 | this.FHalfCarry = this.FSubtract = false; 304 | this.FZero = this.registerB === 0; 305 | }, 306 | //SLA C 307 | //#0x21: 308 | function () { 309 | this.FCarry = this.registerC > 0x7f; 310 | this.registerC = this.registerC << 1 & 0xff; 311 | this.FHalfCarry = this.FSubtract = false; 312 | this.FZero = this.registerC === 0; 313 | }, 314 | //SLA D 315 | //#0x22: 316 | function () { 317 | this.FCarry = this.registerD > 0x7f; 318 | this.registerD = this.registerD << 1 & 0xff; 319 | this.FHalfCarry = this.FSubtract = false; 320 | this.FZero = this.registerD === 0; 321 | }, 322 | //SLA E 323 | //#0x23: 324 | function () { 325 | this.FCarry = this.registerE > 0x7f; 326 | this.registerE = this.registerE << 1 & 0xff; 327 | this.FHalfCarry = this.FSubtract = false; 328 | this.FZero = this.registerE === 0; 329 | }, 330 | //SLA H 331 | //#0x24: 332 | function () { 333 | this.FCarry = this.registersHL > 0x7fff; 334 | this.registersHL = this.registersHL << 1 & 0xfe00 | this.registersHL & 0xff; 335 | this.FHalfCarry = this.FSubtract = false; 336 | this.FZero = this.registersHL < 0x100; 337 | }, 338 | //SLA L 339 | //#0x25: 340 | function () { 341 | this.FCarry = (this.registersHL & 0x0080) === 0x0080; 342 | this.registersHL = this.registersHL & 0xff00 | this.registersHL << 1 & 0xff; 343 | this.FHalfCarry = this.FSubtract = false; 344 | this.FZero = (this.registersHL & 0xff) === 0; 345 | }, 346 | //SLA (HL) 347 | //#0x26: 348 | function () { 349 | var temp_var = this.readMemory(this.registersHL); 350 | this.FCarry = temp_var > 0x7f; 351 | temp_var = temp_var << 1 & 0xff; 352 | this.memoryWriter[this.registersHL](this.registersHL, temp_var); 353 | this.FHalfCarry = this.FSubtract = false; 354 | this.FZero = temp_var === 0; 355 | }, 356 | //SLA A 357 | //#0x27: 358 | function () { 359 | this.FCarry = this.registerA > 0x7f; 360 | this.registerA = this.registerA << 1 & 0xff; 361 | this.FHalfCarry = this.FSubtract = false; 362 | this.FZero = this.registerA === 0; 363 | }, 364 | //SRA B 365 | //#0x28: 366 | function () { 367 | this.FCarry = (this.registerB & 0x01) === 0x01; 368 | this.registerB = this.registerB & 0x80 | this.registerB >> 1; 369 | this.FHalfCarry = this.FSubtract = false; 370 | this.FZero = this.registerB === 0; 371 | }, 372 | //SRA C 373 | //#0x29: 374 | function () { 375 | this.FCarry = (this.registerC & 0x01) === 0x01; 376 | this.registerC = this.registerC & 0x80 | this.registerC >> 1; 377 | this.FHalfCarry = this.FSubtract = false; 378 | this.FZero = this.registerC === 0; 379 | }, 380 | //SRA D 381 | //#0x2A: 382 | function () { 383 | this.FCarry = (this.registerD & 0x01) === 0x01; 384 | this.registerD = this.registerD & 0x80 | this.registerD >> 1; 385 | this.FHalfCarry = this.FSubtract = false; 386 | this.FZero = this.registerD === 0; 387 | }, 388 | //SRA E 389 | //#0x2B: 390 | function () { 391 | this.FCarry = (this.registerE & 0x01) === 0x01; 392 | this.registerE = this.registerE & 0x80 | this.registerE >> 1; 393 | this.FHalfCarry = this.FSubtract = false; 394 | this.FZero = this.registerE === 0; 395 | }, 396 | //SRA H 397 | //#0x2C: 398 | function () { 399 | this.FCarry = (this.registersHL & 0x0100) === 0x0100; 400 | this.registersHL = this.registersHL >> 1 & 0xff00 | 401 | this.registersHL & 0x80ff; 402 | this.FHalfCarry = this.FSubtract = false; 403 | this.FZero = this.registersHL < 0x100; 404 | }, 405 | //SRA L 406 | //#0x2D: 407 | function () { 408 | this.FCarry = (this.registersHL & 0x0001) === 0x0001; 409 | this.registersHL = this.registersHL & 0xff80 | 410 | (this.registersHL & 0xff) >> 1; 411 | this.FHalfCarry = this.FSubtract = false; 412 | this.FZero = (this.registersHL & 0xff) === 0; 413 | }, 414 | //SRA (HL) 415 | //#0x2E: 416 | function () { 417 | var temp_var = this.readMemory(this.registersHL); 418 | this.FCarry = (temp_var & 0x01) === 0x01; 419 | temp_var = temp_var & 0x80 | temp_var >> 1; 420 | this.memoryWriter[this.registersHL](this.registersHL, temp_var); 421 | this.FHalfCarry = this.FSubtract = false; 422 | this.FZero = temp_var === 0; 423 | }, 424 | //SRA A 425 | //#0x2F: 426 | function () { 427 | this.FCarry = (this.registerA & 0x01) === 0x01; 428 | this.registerA = this.registerA & 0x80 | this.registerA >> 1; 429 | this.FHalfCarry = this.FSubtract = false; 430 | this.FZero = this.registerA === 0; 431 | }, 432 | //SWAP B 433 | //#0x30: 434 | function () { 435 | this.registerB = (this.registerB & 0xf) << 4 | this.registerB >> 4; 436 | this.FZero = this.registerB === 0; 437 | this.FCarry = this.FHalfCarry = this.FSubtract = false; 438 | }, 439 | //SWAP C 440 | //#0x31: 441 | function () { 442 | this.registerC = (this.registerC & 0xf) << 4 | this.registerC >> 4; 443 | this.FZero = this.registerC === 0; 444 | this.FCarry = this.FHalfCarry = this.FSubtract = false; 445 | }, 446 | //SWAP D 447 | //#0x32: 448 | function () { 449 | this.registerD = (this.registerD & 0xf) << 4 | this.registerD >> 4; 450 | this.FZero = this.registerD === 0; 451 | this.FCarry = this.FHalfCarry = this.FSubtract = false; 452 | }, 453 | //SWAP E 454 | //#0x33: 455 | function () { 456 | this.registerE = (this.registerE & 0xf) << 4 | this.registerE >> 4; 457 | this.FZero = this.registerE === 0; 458 | this.FCarry = this.FHalfCarry = this.FSubtract = false; 459 | }, 460 | //SWAP H 461 | //#0x34: 462 | function () { 463 | this.registersHL = (this.registersHL & 0xf00) << 4 | 464 | (this.registersHL & 0xf000) >> 4 | 465 | this.registersHL & 0xff; 466 | this.FZero = this.registersHL < 0x100; 467 | this.FCarry = this.FHalfCarry = this.FSubtract = false; 468 | }, 469 | //SWAP L 470 | //#0x35: 471 | function () { 472 | this.registersHL = this.registersHL & 0xff00 | 473 | (this.registersHL & 0xf) << 4 | 474 | (this.registersHL & 0xf0) >> 4; 475 | this.FZero = (this.registersHL & 0xff) === 0; 476 | this.FCarry = this.FHalfCarry = this.FSubtract = false; 477 | }, 478 | //SWAP (HL) 479 | //#0x36: 480 | function () { 481 | var temp_var = this.readMemory(this.registersHL); 482 | temp_var = (temp_var & 0xf) << 4 | temp_var >> 4; 483 | this.memoryWriter[this.registersHL](this.registersHL, temp_var); 484 | this.FZero = temp_var === 0; 485 | this.FCarry = this.FHalfCarry = this.FSubtract = false; 486 | }, 487 | //SWAP A 488 | //#0x37: 489 | function () { 490 | this.registerA = (this.registerA & 0xf) << 4 | this.registerA >> 4; 491 | this.FZero = this.registerA === 0; 492 | this.FCarry = this.FHalfCarry = this.FSubtract = false; 493 | }, 494 | //SRL B 495 | //#0x38: 496 | function () { 497 | this.FCarry = (this.registerB & 0x01) === 0x01; 498 | this.registerB >>= 1; 499 | this.FHalfCarry = this.FSubtract = false; 500 | this.FZero = this.registerB === 0; 501 | }, 502 | //SRL C 503 | //#0x39: 504 | function () { 505 | this.FCarry = (this.registerC & 0x01) === 0x01; 506 | this.registerC >>= 1; 507 | this.FHalfCarry = this.FSubtract = false; 508 | this.FZero = this.registerC === 0; 509 | }, 510 | //SRL D 511 | //#0x3A: 512 | function () { 513 | this.FCarry = (this.registerD & 0x01) === 0x01; 514 | this.registerD >>= 1; 515 | this.FHalfCarry = this.FSubtract = false; 516 | this.FZero = this.registerD === 0; 517 | }, 518 | //SRL E 519 | //#0x3B: 520 | function () { 521 | this.FCarry = (this.registerE & 0x01) === 0x01; 522 | this.registerE >>= 1; 523 | this.FHalfCarry = this.FSubtract = false; 524 | this.FZero = this.registerE === 0; 525 | }, 526 | //SRL H 527 | //#0x3C: 528 | function () { 529 | this.FCarry = (this.registersHL & 0x0100) === 0x0100; 530 | this.registersHL = this.registersHL >> 1 & 0xff00 | this.registersHL & 0xff; 531 | this.FHalfCarry = this.FSubtract = false; 532 | this.FZero = this.registersHL < 0x100; 533 | }, 534 | //SRL L 535 | //#0x3D: 536 | function () { 537 | this.FCarry = (this.registersHL & 0x0001) === 0x0001; 538 | this.registersHL = this.registersHL & 0xff00 | 539 | (this.registersHL & 0xff) >> 1; 540 | this.FHalfCarry = this.FSubtract = false; 541 | this.FZero = (this.registersHL & 0xff) === 0; 542 | }, 543 | //SRL (HL) 544 | //#0x3E: 545 | function () { 546 | var temp_var = this.readMemory(this.registersHL); 547 | this.FCarry = (temp_var & 0x01) === 0x01; 548 | this.memoryWriter[this.registersHL](this.registersHL, temp_var >> 1); 549 | this.FHalfCarry = this.FSubtract = false; 550 | this.FZero = temp_var < 2; 551 | }, 552 | //SRL A 553 | //#0x3F: 554 | function () { 555 | this.FCarry = (this.registerA & 0x01) === 0x01; 556 | this.registerA >>= 1; 557 | this.FHalfCarry = this.FSubtract = false; 558 | this.FZero = this.registerA === 0; 559 | }, 560 | //BIT 0, B 561 | //#0x40: 562 | function () { 563 | this.FHalfCarry = true; 564 | this.FSubtract = false; 565 | this.FZero = (this.registerB & 0x01) === 0; 566 | }, 567 | //BIT 0, C 568 | //#0x41: 569 | function () { 570 | this.FHalfCarry = true; 571 | this.FSubtract = false; 572 | this.FZero = (this.registerC & 0x01) === 0; 573 | }, 574 | //BIT 0, D 575 | //#0x42: 576 | function () { 577 | this.FHalfCarry = true; 578 | this.FSubtract = false; 579 | this.FZero = (this.registerD & 0x01) === 0; 580 | }, 581 | //BIT 0, E 582 | //#0x43: 583 | function () { 584 | this.FHalfCarry = true; 585 | this.FSubtract = false; 586 | this.FZero = (this.registerE & 0x01) === 0; 587 | }, 588 | //BIT 0, H 589 | //#0x44: 590 | function () { 591 | this.FHalfCarry = true; 592 | this.FSubtract = false; 593 | this.FZero = (this.registersHL & 0x0100) === 0; 594 | }, 595 | //BIT 0, L 596 | //#0x45: 597 | function () { 598 | this.FHalfCarry = true; 599 | this.FSubtract = false; 600 | this.FZero = (this.registersHL & 0x0001) === 0; 601 | }, 602 | //BIT 0, (HL) 603 | //#0x46: 604 | function () { 605 | this.FHalfCarry = true; 606 | this.FSubtract = false; 607 | this.FZero = (this.readMemory(this.registersHL) & 0x01) === 0; 608 | }, 609 | //BIT 0, A 610 | //#0x47: 611 | function () { 612 | this.FHalfCarry = true; 613 | this.FSubtract = false; 614 | this.FZero = (this.registerA & 0x01) === 0; 615 | }, 616 | //BIT 1, B 617 | //#0x48: 618 | function () { 619 | this.FHalfCarry = true; 620 | this.FSubtract = false; 621 | this.FZero = (this.registerB & 0x02) === 0; 622 | }, 623 | //BIT 1, C 624 | //#0x49: 625 | function () { 626 | this.FHalfCarry = true; 627 | this.FSubtract = false; 628 | this.FZero = (this.registerC & 0x02) === 0; 629 | }, 630 | //BIT 1, D 631 | //#0x4A: 632 | function () { 633 | this.FHalfCarry = true; 634 | this.FSubtract = false; 635 | this.FZero = (this.registerD & 0x02) === 0; 636 | }, 637 | //BIT 1, E 638 | //#0x4B: 639 | function () { 640 | this.FHalfCarry = true; 641 | this.FSubtract = false; 642 | this.FZero = (this.registerE & 0x02) === 0; 643 | }, 644 | //BIT 1, H 645 | //#0x4C: 646 | function () { 647 | this.FHalfCarry = true; 648 | this.FSubtract = false; 649 | this.FZero = (this.registersHL & 0x0200) === 0; 650 | }, 651 | //BIT 1, L 652 | //#0x4D: 653 | function () { 654 | this.FHalfCarry = true; 655 | this.FSubtract = false; 656 | this.FZero = (this.registersHL & 0x0002) === 0; 657 | }, 658 | //BIT 1, (HL) 659 | //#0x4E: 660 | function () { 661 | this.FHalfCarry = true; 662 | this.FSubtract = false; 663 | this.FZero = (this.readMemory(this.registersHL) & 0x02) === 0; 664 | }, 665 | //BIT 1, A 666 | //#0x4F: 667 | function () { 668 | this.FHalfCarry = true; 669 | this.FSubtract = false; 670 | this.FZero = (this.registerA & 0x02) === 0; 671 | }, 672 | //BIT 2, B 673 | //#0x50: 674 | function () { 675 | this.FHalfCarry = true; 676 | this.FSubtract = false; 677 | this.FZero = (this.registerB & 0x04) === 0; 678 | }, 679 | //BIT 2, C 680 | //#0x51: 681 | function () { 682 | this.FHalfCarry = true; 683 | this.FSubtract = false; 684 | this.FZero = (this.registerC & 0x04) === 0; 685 | }, 686 | //BIT 2, D 687 | //#0x52: 688 | function () { 689 | this.FHalfCarry = true; 690 | this.FSubtract = false; 691 | this.FZero = (this.registerD & 0x04) === 0; 692 | }, 693 | //BIT 2, E 694 | //#0x53: 695 | function () { 696 | this.FHalfCarry = true; 697 | this.FSubtract = false; 698 | this.FZero = (this.registerE & 0x04) === 0; 699 | }, 700 | //BIT 2, H 701 | //#0x54: 702 | function () { 703 | this.FHalfCarry = true; 704 | this.FSubtract = false; 705 | this.FZero = (this.registersHL & 0x0400) === 0; 706 | }, 707 | //BIT 2, L 708 | //#0x55: 709 | function () { 710 | this.FHalfCarry = true; 711 | this.FSubtract = false; 712 | this.FZero = (this.registersHL & 0x0004) === 0; 713 | }, 714 | //BIT 2, (HL) 715 | //#0x56: 716 | function () { 717 | this.FHalfCarry = true; 718 | this.FSubtract = false; 719 | this.FZero = (this.readMemory(this.registersHL) & 0x04) === 0; 720 | }, 721 | //BIT 2, A 722 | //#0x57: 723 | function () { 724 | this.FHalfCarry = true; 725 | this.FSubtract = false; 726 | this.FZero = (this.registerA & 0x04) === 0; 727 | }, 728 | //BIT 3, B 729 | //#0x58: 730 | function () { 731 | this.FHalfCarry = true; 732 | this.FSubtract = false; 733 | this.FZero = (this.registerB & 0x08) === 0; 734 | }, 735 | //BIT 3, C 736 | //#0x59: 737 | function () { 738 | this.FHalfCarry = true; 739 | this.FSubtract = false; 740 | this.FZero = (this.registerC & 0x08) === 0; 741 | }, 742 | //BIT 3, D 743 | //#0x5A: 744 | function () { 745 | this.FHalfCarry = true; 746 | this.FSubtract = false; 747 | this.FZero = (this.registerD & 0x08) === 0; 748 | }, 749 | //BIT 3, E 750 | //#0x5B: 751 | function () { 752 | this.FHalfCarry = true; 753 | this.FSubtract = false; 754 | this.FZero = (this.registerE & 0x08) === 0; 755 | }, 756 | //BIT 3, H 757 | //#0x5C: 758 | function () { 759 | this.FHalfCarry = true; 760 | this.FSubtract = false; 761 | this.FZero = (this.registersHL & 0x0800) === 0; 762 | }, 763 | //BIT 3, L 764 | //#0x5D: 765 | function () { 766 | this.FHalfCarry = true; 767 | this.FSubtract = false; 768 | this.FZero = (this.registersHL & 0x0008) === 0; 769 | }, 770 | //BIT 3, (HL) 771 | //#0x5E: 772 | function () { 773 | this.FHalfCarry = true; 774 | this.FSubtract = false; 775 | this.FZero = (this.readMemory(this.registersHL) & 0x08) === 0; 776 | }, 777 | //BIT 3, A 778 | //#0x5F: 779 | function () { 780 | this.FHalfCarry = true; 781 | this.FSubtract = false; 782 | this.FZero = (this.registerA & 0x08) === 0; 783 | }, 784 | //BIT 4, B 785 | //#0x60: 786 | function () { 787 | this.FHalfCarry = true; 788 | this.FSubtract = false; 789 | this.FZero = (this.registerB & 0x10) === 0; 790 | }, 791 | //BIT 4, C 792 | //#0x61: 793 | function () { 794 | this.FHalfCarry = true; 795 | this.FSubtract = false; 796 | this.FZero = (this.registerC & 0x10) === 0; 797 | }, 798 | //BIT 4, D 799 | //#0x62: 800 | function () { 801 | this.FHalfCarry = true; 802 | this.FSubtract = false; 803 | this.FZero = (this.registerD & 0x10) === 0; 804 | }, 805 | //BIT 4, E 806 | //#0x63: 807 | function () { 808 | this.FHalfCarry = true; 809 | this.FSubtract = false; 810 | this.FZero = (this.registerE & 0x10) === 0; 811 | }, 812 | //BIT 4, H 813 | //#0x64: 814 | function () { 815 | this.FHalfCarry = true; 816 | this.FSubtract = false; 817 | this.FZero = (this.registersHL & 0x1000) === 0; 818 | }, 819 | //BIT 4, L 820 | //#0x65: 821 | function () { 822 | this.FHalfCarry = true; 823 | this.FSubtract = false; 824 | this.FZero = (this.registersHL & 0x0010) === 0; 825 | }, 826 | //BIT 4, (HL) 827 | //#0x66: 828 | function () { 829 | this.FHalfCarry = true; 830 | this.FSubtract = false; 831 | this.FZero = (this.readMemory(this.registersHL) & 0x10) === 0; 832 | }, 833 | //BIT 4, A 834 | //#0x67: 835 | function () { 836 | this.FHalfCarry = true; 837 | this.FSubtract = false; 838 | this.FZero = (this.registerA & 0x10) === 0; 839 | }, 840 | //BIT 5, B 841 | //#0x68: 842 | function () { 843 | this.FHalfCarry = true; 844 | this.FSubtract = false; 845 | this.FZero = (this.registerB & 0x20) === 0; 846 | }, 847 | //BIT 5, C 848 | //#0x69: 849 | function () { 850 | this.FHalfCarry = true; 851 | this.FSubtract = false; 852 | this.FZero = (this.registerC & 0x20) === 0; 853 | }, 854 | //BIT 5, D 855 | //#0x6A: 856 | function () { 857 | this.FHalfCarry = true; 858 | this.FSubtract = false; 859 | this.FZero = (this.registerD & 0x20) === 0; 860 | }, 861 | //BIT 5, E 862 | //#0x6B: 863 | function () { 864 | this.FHalfCarry = true; 865 | this.FSubtract = false; 866 | this.FZero = (this.registerE & 0x20) === 0; 867 | }, 868 | //BIT 5, H 869 | //#0x6C: 870 | function () { 871 | this.FHalfCarry = true; 872 | this.FSubtract = false; 873 | this.FZero = (this.registersHL & 0x2000) === 0; 874 | }, 875 | //BIT 5, L 876 | //#0x6D: 877 | function () { 878 | this.FHalfCarry = true; 879 | this.FSubtract = false; 880 | this.FZero = (this.registersHL & 0x0020) === 0; 881 | }, 882 | //BIT 5, (HL) 883 | //#0x6E: 884 | function () { 885 | this.FHalfCarry = true; 886 | this.FSubtract = false; 887 | this.FZero = (this.readMemory(this.registersHL) & 0x20) === 0; 888 | }, 889 | //BIT 5, A 890 | //#0x6F: 891 | function () { 892 | this.FHalfCarry = true; 893 | this.FSubtract = false; 894 | this.FZero = (this.registerA & 0x20) === 0; 895 | }, 896 | //BIT 6, B 897 | //#0x70: 898 | function () { 899 | this.FHalfCarry = true; 900 | this.FSubtract = false; 901 | this.FZero = (this.registerB & 0x40) === 0; 902 | }, 903 | //BIT 6, C 904 | //#0x71: 905 | function () { 906 | this.FHalfCarry = true; 907 | this.FSubtract = false; 908 | this.FZero = (this.registerC & 0x40) === 0; 909 | }, 910 | //BIT 6, D 911 | //#0x72: 912 | function () { 913 | this.FHalfCarry = true; 914 | this.FSubtract = false; 915 | this.FZero = (this.registerD & 0x40) === 0; 916 | }, 917 | //BIT 6, E 918 | //#0x73: 919 | function () { 920 | this.FHalfCarry = true; 921 | this.FSubtract = false; 922 | this.FZero = (this.registerE & 0x40) === 0; 923 | }, 924 | //BIT 6, H 925 | //#0x74: 926 | function () { 927 | this.FHalfCarry = true; 928 | this.FSubtract = false; 929 | this.FZero = (this.registersHL & 0x4000) === 0; 930 | }, 931 | //BIT 6, L 932 | //#0x75: 933 | function () { 934 | this.FHalfCarry = true; 935 | this.FSubtract = false; 936 | this.FZero = (this.registersHL & 0x0040) === 0; 937 | }, 938 | //BIT 6, (HL) 939 | //#0x76: 940 | function () { 941 | this.FHalfCarry = true; 942 | this.FSubtract = false; 943 | this.FZero = (this.readMemory(this.registersHL) & 0x40) === 0; 944 | }, 945 | //BIT 6, A 946 | //#0x77: 947 | function () { 948 | this.FHalfCarry = true; 949 | this.FSubtract = false; 950 | this.FZero = (this.registerA & 0x40) === 0; 951 | }, 952 | //BIT 7, B 953 | //#0x78: 954 | function () { 955 | this.FHalfCarry = true; 956 | this.FSubtract = false; 957 | this.FZero = (this.registerB & 0x80) === 0; 958 | }, 959 | //BIT 7, C 960 | //#0x79: 961 | function () { 962 | this.FHalfCarry = true; 963 | this.FSubtract = false; 964 | this.FZero = (this.registerC & 0x80) === 0; 965 | }, 966 | //BIT 7, D 967 | //#0x7A: 968 | function () { 969 | this.FHalfCarry = true; 970 | this.FSubtract = false; 971 | this.FZero = (this.registerD & 0x80) === 0; 972 | }, 973 | //BIT 7, E 974 | //#0x7B: 975 | function () { 976 | this.FHalfCarry = true; 977 | this.FSubtract = false; 978 | this.FZero = (this.registerE & 0x80) === 0; 979 | }, 980 | //BIT 7, H 981 | //#0x7C: 982 | function () { 983 | this.FHalfCarry = true; 984 | this.FSubtract = false; 985 | this.FZero = (this.registersHL & 0x8000) === 0; 986 | }, 987 | //BIT 7, L 988 | //#0x7D: 989 | function () { 990 | this.FHalfCarry = true; 991 | this.FSubtract = false; 992 | this.FZero = (this.registersHL & 0x0080) === 0; 993 | }, 994 | //BIT 7, (HL) 995 | //#0x7E: 996 | function () { 997 | this.FHalfCarry = true; 998 | this.FSubtract = false; 999 | this.FZero = (this.readMemory(this.registersHL) & 0x80) === 0; 1000 | }, 1001 | //BIT 7, A 1002 | //#0x7F: 1003 | function () { 1004 | this.FHalfCarry = true; 1005 | this.FSubtract = false; 1006 | this.FZero = (this.registerA & 0x80) === 0; 1007 | }, 1008 | //RES 0, B 1009 | //#0x80: 1010 | function () { 1011 | this.registerB &= 0xfe; 1012 | }, 1013 | //RES 0, C 1014 | //#0x81: 1015 | function () { 1016 | this.registerC &= 0xfe; 1017 | }, 1018 | //RES 0, D 1019 | //#0x82: 1020 | function () { 1021 | this.registerD &= 0xfe; 1022 | }, 1023 | //RES 0, E 1024 | //#0x83: 1025 | function () { 1026 | this.registerE &= 0xfe; 1027 | }, 1028 | //RES 0, H 1029 | //#0x84: 1030 | function () { 1031 | this.registersHL &= 0xfeff; 1032 | }, 1033 | //RES 0, L 1034 | //#0x85: 1035 | function () { 1036 | this.registersHL &= 0xfffe; 1037 | }, 1038 | //RES 0, (HL) 1039 | //#0x86: 1040 | function () { 1041 | this.memoryWriter[this.registersHL]( 1042 | this.registersHL, 1043 | this.readMemory(this.registersHL) & 0xfe); 1044 | }, 1045 | //RES 0, A 1046 | //#0x87: 1047 | function () { 1048 | this.registerA &= 0xfe; 1049 | }, 1050 | //RES 1, B 1051 | //#0x88: 1052 | function () { 1053 | this.registerB &= 0xfd; 1054 | }, 1055 | //RES 1, C 1056 | //#0x89: 1057 | function () { 1058 | this.registerC &= 0xfd; 1059 | }, 1060 | //RES 1, D 1061 | //#0x8A: 1062 | function () { 1063 | this.registerD &= 0xfd; 1064 | }, 1065 | //RES 1, E 1066 | //#0x8B: 1067 | function () { 1068 | this.registerE &= 0xfd; 1069 | }, 1070 | //RES 1, H 1071 | //#0x8C: 1072 | function () { 1073 | this.registersHL &= 0xfdff; 1074 | }, 1075 | //RES 1, L 1076 | //#0x8D: 1077 | function () { 1078 | this.registersHL &= 0xfffd; 1079 | }, 1080 | //RES 1, (HL) 1081 | //#0x8E: 1082 | function () { 1083 | this.memoryWriter[this.registersHL]( 1084 | this.registersHL, 1085 | this.readMemory(this.registersHL) & 0xfd 1086 | ); 1087 | }, 1088 | //RES 1, A 1089 | //#0x8F: 1090 | function () { 1091 | this.registerA &= 0xfd; 1092 | }, 1093 | //RES 2, B 1094 | //#0x90: 1095 | function () { 1096 | this.registerB &= 0xfb; 1097 | }, 1098 | //RES 2, C 1099 | //#0x91: 1100 | function () { 1101 | this.registerC &= 0xfb; 1102 | }, 1103 | //RES 2, D 1104 | //#0x92: 1105 | function () { 1106 | this.registerD &= 0xfb; 1107 | }, 1108 | //RES 2, E 1109 | //#0x93: 1110 | function () { 1111 | this.registerE &= 0xfb; 1112 | }, 1113 | //RES 2, H 1114 | //#0x94: 1115 | function () { 1116 | this.registersHL &= 0xfbff; 1117 | }, 1118 | //RES 2, L 1119 | //#0x95: 1120 | function () { 1121 | this.registersHL &= 0xfffb; 1122 | }, 1123 | //RES 2, (HL) 1124 | //#0x96: 1125 | function () { 1126 | this.memoryWriter[this.registersHL]( 1127 | this.registersHL, 1128 | this.readMemory(this.registersHL) & 0xfb 1129 | ); 1130 | }, 1131 | //RES 2, A 1132 | //#0x97: 1133 | function () { 1134 | this.registerA &= 0xfb; 1135 | }, 1136 | //RES 3, B 1137 | //#0x98: 1138 | function () { 1139 | this.registerB &= 0xf7; 1140 | }, 1141 | //RES 3, C 1142 | //#0x99: 1143 | function () { 1144 | this.registerC &= 0xf7; 1145 | }, 1146 | //RES 3, D 1147 | //#0x9A: 1148 | function () { 1149 | this.registerD &= 0xf7; 1150 | }, 1151 | //RES 3, E 1152 | //#0x9B: 1153 | function () { 1154 | this.registerE &= 0xf7; 1155 | }, 1156 | //RES 3, H 1157 | //#0x9C: 1158 | function () { 1159 | this.registersHL &= 0xf7ff; 1160 | }, 1161 | //RES 3, L 1162 | //#0x9D: 1163 | function () { 1164 | this.registersHL &= 0xfff7; 1165 | }, 1166 | //RES 3, (HL) 1167 | //#0x9E: 1168 | function () { 1169 | this.memoryWriter[this.registersHL]( 1170 | this.registersHL, 1171 | this.readMemory(this.registersHL) & 0xf7 1172 | ); 1173 | }, 1174 | //RES 3, A 1175 | //#0x9F: 1176 | function () { 1177 | this.registerA &= 0xf7; 1178 | }, 1179 | //RES 3, B 1180 | //#0xA0: 1181 | function () { 1182 | this.registerB &= 0xef; 1183 | }, 1184 | //RES 4, C 1185 | //#0xA1: 1186 | function () { 1187 | this.registerC &= 0xef; 1188 | }, 1189 | //RES 4, D 1190 | //#0xA2: 1191 | function () { 1192 | this.registerD &= 0xef; 1193 | }, 1194 | //RES 4, E 1195 | //#0xA3: 1196 | function () { 1197 | this.registerE &= 0xef; 1198 | }, 1199 | //RES 4, H 1200 | //#0xA4: 1201 | function () { 1202 | this.registersHL &= 0xefff; 1203 | }, 1204 | //RES 4, L 1205 | //#0xA5: 1206 | function () { 1207 | this.registersHL &= 0xffef; 1208 | }, 1209 | //RES 4, (HL) 1210 | //#0xA6: 1211 | function () { 1212 | this.memoryWriter[this.registersHL]( 1213 | this.registersHL, 1214 | this.readMemory(this.registersHL) & 0xef 1215 | ); 1216 | }, 1217 | //RES 4, A 1218 | //#0xA7: 1219 | function () { 1220 | this.registerA &= 0xef; 1221 | }, 1222 | //RES 5, B 1223 | //#0xA8: 1224 | function () { 1225 | this.registerB &= 0xdf; 1226 | }, 1227 | //RES 5, C 1228 | //#0xA9: 1229 | function () { 1230 | this.registerC &= 0xdf; 1231 | }, 1232 | //RES 5, D 1233 | //#0xAA: 1234 | function () { 1235 | this.registerD &= 0xdf; 1236 | }, 1237 | //RES 5, E 1238 | //#0xAB: 1239 | function () { 1240 | this.registerE &= 0xdf; 1241 | }, 1242 | //RES 5, H 1243 | //#0xAC: 1244 | function () { 1245 | this.registersHL &= 0xdfff; 1246 | }, 1247 | //RES 5, L 1248 | //#0xAD: 1249 | function () { 1250 | this.registersHL &= 0xffdf; 1251 | }, 1252 | //RES 5, (HL) 1253 | //#0xAE: 1254 | function () { 1255 | this.memoryWriter[this.registersHL]( 1256 | this.registersHL, 1257 | this.readMemory(this.registersHL) & 0xdf 1258 | ); 1259 | }, 1260 | //RES 5, A 1261 | //#0xAF: 1262 | function () { 1263 | this.registerA &= 0xdf; 1264 | }, 1265 | //RES 6, B 1266 | //#0xB0: 1267 | function () { 1268 | this.registerB &= 0xbf; 1269 | }, 1270 | //RES 6, C 1271 | //#0xB1: 1272 | function () { 1273 | this.registerC &= 0xbf; 1274 | }, 1275 | //RES 6, D 1276 | //#0xB2: 1277 | function () { 1278 | this.registerD &= 0xbf; 1279 | }, 1280 | //RES 6, E 1281 | //#0xB3: 1282 | function () { 1283 | this.registerE &= 0xbf; 1284 | }, 1285 | //RES 6, H 1286 | //#0xB4: 1287 | function () { 1288 | this.registersHL &= 0xbfff; 1289 | }, 1290 | //RES 6, L 1291 | //#0xB5: 1292 | function () { 1293 | this.registersHL &= 0xffbf; 1294 | }, 1295 | //RES 6, (HL) 1296 | //#0xB6: 1297 | function () { 1298 | this.memoryWriter[this.registersHL]( 1299 | this.registersHL, 1300 | this.readMemory(this.registersHL) & 0xbf 1301 | ); 1302 | }, 1303 | //RES 6, A 1304 | //#0xB7: 1305 | function () { 1306 | this.registerA &= 0xbf; 1307 | }, 1308 | //RES 7, B 1309 | //#0xB8: 1310 | function () { 1311 | this.registerB &= 0x7f; 1312 | }, 1313 | //RES 7, C 1314 | //#0xB9: 1315 | function () { 1316 | this.registerC &= 0x7f; 1317 | }, 1318 | //RES 7, D 1319 | //#0xBA: 1320 | function () { 1321 | this.registerD &= 0x7f; 1322 | }, 1323 | //RES 7, E 1324 | //#0xBB: 1325 | function () { 1326 | this.registerE &= 0x7f; 1327 | }, 1328 | //RES 7, H 1329 | //#0xBC: 1330 | function () { 1331 | this.registersHL &= 0x7fff; 1332 | }, 1333 | //RES 7, L 1334 | //#0xBD: 1335 | function () { 1336 | this.registersHL &= 0xff7f; 1337 | }, 1338 | //RES 7, (HL) 1339 | //#0xBE: 1340 | function () { 1341 | this.memoryWriter[this.registersHL]( 1342 | this.registersHL, 1343 | this.readMemory(this.registersHL) & 0x7f 1344 | ); 1345 | }, 1346 | //RES 7, A 1347 | //#0xBF: 1348 | function () { 1349 | this.registerA &= 0x7f; 1350 | }, 1351 | //SET 0, B 1352 | //#0xC0: 1353 | function () { 1354 | this.registerB |= 0x01; 1355 | }, 1356 | //SET 0, C 1357 | //#0xC1: 1358 | function () { 1359 | this.registerC |= 0x01; 1360 | }, 1361 | //SET 0, D 1362 | //#0xC2: 1363 | function () { 1364 | this.registerD |= 0x01; 1365 | }, 1366 | //SET 0, E 1367 | //#0xC3: 1368 | function () { 1369 | this.registerE |= 0x01; 1370 | }, 1371 | //SET 0, H 1372 | //#0xC4: 1373 | function () { 1374 | this.registersHL |= 0x0100; 1375 | }, 1376 | //SET 0, L 1377 | //#0xC5: 1378 | function () { 1379 | this.registersHL |= 0x01; 1380 | }, 1381 | //SET 0, (HL) 1382 | //#0xC6: 1383 | function () { 1384 | this.memoryWriter[this.registersHL]( 1385 | this.registersHL, 1386 | this.readMemory(this.registersHL) | 0x01 1387 | ); 1388 | }, 1389 | //SET 0, A 1390 | //#0xC7: 1391 | function () { 1392 | this.registerA |= 0x01; 1393 | }, 1394 | //SET 1, B 1395 | //#0xC8: 1396 | function () { 1397 | this.registerB |= 0x02; 1398 | }, 1399 | //SET 1, C 1400 | //#0xC9: 1401 | function () { 1402 | this.registerC |= 0x02; 1403 | }, 1404 | //SET 1, D 1405 | //#0xCA: 1406 | function () { 1407 | this.registerD |= 0x02; 1408 | }, 1409 | //SET 1, E 1410 | //#0xCB: 1411 | function () { 1412 | this.registerE |= 0x02; 1413 | }, 1414 | //SET 1, H 1415 | //#0xCC: 1416 | function () { 1417 | this.registersHL |= 0x0200; 1418 | }, 1419 | //SET 1, L 1420 | //#0xCD: 1421 | function () { 1422 | this.registersHL |= 0x02; 1423 | }, 1424 | //SET 1, (HL) 1425 | //#0xCE: 1426 | function () { 1427 | this.memoryWriter[this.registersHL]( 1428 | this.registersHL, 1429 | this.readMemory(this.registersHL) | 0x02 1430 | ); 1431 | }, 1432 | //SET 1, A 1433 | //#0xCF: 1434 | function () { 1435 | this.registerA |= 0x02; 1436 | }, 1437 | //SET 2, B 1438 | //#0xD0: 1439 | function () { 1440 | this.registerB |= 0x04; 1441 | }, 1442 | //SET 2, C 1443 | //#0xD1: 1444 | function () { 1445 | this.registerC |= 0x04; 1446 | }, 1447 | //SET 2, D 1448 | //#0xD2: 1449 | function () { 1450 | this.registerD |= 0x04; 1451 | }, 1452 | //SET 2, E 1453 | //#0xD3: 1454 | function () { 1455 | this.registerE |= 0x04; 1456 | }, 1457 | //SET 2, H 1458 | //#0xD4: 1459 | function () { 1460 | this.registersHL |= 0x0400; 1461 | }, 1462 | //SET 2, L 1463 | //#0xD5: 1464 | function () { 1465 | this.registersHL |= 0x04; 1466 | }, 1467 | //SET 2, (HL) 1468 | //#0xD6: 1469 | function () { 1470 | this.memoryWriter[this.registersHL]( 1471 | this.registersHL, 1472 | this.readMemory(this.registersHL) | 0x04 1473 | ); 1474 | }, 1475 | //SET 2, A 1476 | //#0xD7: 1477 | function () { 1478 | this.registerA |= 0x04; 1479 | }, 1480 | //SET 3, B 1481 | //#0xD8: 1482 | function () { 1483 | this.registerB |= 0x08; 1484 | }, 1485 | //SET 3, C 1486 | //#0xD9: 1487 | function () { 1488 | this.registerC |= 0x08; 1489 | }, 1490 | //SET 3, D 1491 | //#0xDA: 1492 | function () { 1493 | this.registerD |= 0x08; 1494 | }, 1495 | //SET 3, E 1496 | //#0xDB: 1497 | function () { 1498 | this.registerE |= 0x08; 1499 | }, 1500 | //SET 3, H 1501 | //#0xDC: 1502 | function () { 1503 | this.registersHL |= 0x0800; 1504 | }, 1505 | //SET 3, L 1506 | //#0xDD: 1507 | function () { 1508 | this.registersHL |= 0x08; 1509 | }, 1510 | //SET 3, (HL) 1511 | //#0xDE: 1512 | function () { 1513 | this.memoryWriter[this.registersHL]( 1514 | this.registersHL, 1515 | this.readMemory(this.registersHL) | 0x08 1516 | ); 1517 | }, 1518 | //SET 3, A 1519 | //#0xDF: 1520 | function () { 1521 | this.registerA |= 0x08; 1522 | }, 1523 | //SET 4, B 1524 | //#0xE0: 1525 | function () { 1526 | this.registerB |= 0x10; 1527 | }, 1528 | //SET 4, C 1529 | //#0xE1: 1530 | function () { 1531 | this.registerC |= 0x10; 1532 | }, 1533 | //SET 4, D 1534 | //#0xE2: 1535 | function () { 1536 | this.registerD |= 0x10; 1537 | }, 1538 | //SET 4, E 1539 | //#0xE3: 1540 | function () { 1541 | this.registerE |= 0x10; 1542 | }, 1543 | //SET 4, H 1544 | //#0xE4: 1545 | function () { 1546 | this.registersHL |= 0x1000; 1547 | }, 1548 | //SET 4, L 1549 | //#0xE5: 1550 | function () { 1551 | this.registersHL |= 0x10; 1552 | }, 1553 | //SET 4, (HL) 1554 | //#0xE6: 1555 | function () { 1556 | this.memoryWriter[this.registersHL]( 1557 | this.registersHL, 1558 | this.readMemory(this.registersHL) | 0x10 1559 | ); 1560 | }, 1561 | //SET 4, A 1562 | //#0xE7: 1563 | function () { 1564 | this.registerA |= 0x10; 1565 | }, 1566 | //SET 5, B 1567 | //#0xE8: 1568 | function () { 1569 | this.registerB |= 0x20; 1570 | }, 1571 | //SET 5, C 1572 | //#0xE9: 1573 | function () { 1574 | this.registerC |= 0x20; 1575 | }, 1576 | //SET 5, D 1577 | //#0xEA: 1578 | function () { 1579 | this.registerD |= 0x20; 1580 | }, 1581 | //SET 5, E 1582 | //#0xEB: 1583 | function () { 1584 | this.registerE |= 0x20; 1585 | }, 1586 | //SET 5, H 1587 | //#0xEC: 1588 | function () { 1589 | this.registersHL |= 0x2000; 1590 | }, 1591 | //SET 5, L 1592 | //#0xED: 1593 | function () { 1594 | this.registersHL |= 0x20; 1595 | }, 1596 | //SET 5, (HL) 1597 | //#0xEE: 1598 | function () { 1599 | this.memoryWriter[this.registersHL]( 1600 | this.registersHL, 1601 | this.readMemory(this.registersHL) | 0x20 1602 | ); 1603 | }, 1604 | //SET 5, A 1605 | //#0xEF: 1606 | function () { 1607 | this.registerA |= 0x20; 1608 | }, 1609 | //SET 6, B 1610 | //#0xF0: 1611 | function () { 1612 | this.registerB |= 0x40; 1613 | }, 1614 | //SET 6, C 1615 | //#0xF1: 1616 | function () { 1617 | this.registerC |= 0x40; 1618 | }, 1619 | //SET 6, D 1620 | //#0xF2: 1621 | function () { 1622 | this.registerD |= 0x40; 1623 | }, 1624 | //SET 6, E 1625 | //#0xF3: 1626 | function () { 1627 | this.registerE |= 0x40; 1628 | }, 1629 | //SET 6, H 1630 | //#0xF4: 1631 | function () { 1632 | this.registersHL |= 0x4000; 1633 | }, 1634 | //SET 6, L 1635 | //#0xF5: 1636 | function () { 1637 | this.registersHL |= 0x40; 1638 | }, 1639 | //SET 6, (HL) 1640 | //#0xF6: 1641 | function () { 1642 | this.memoryWriter[this.registersHL]( 1643 | this.registersHL, 1644 | this.readMemory(this.registersHL) | 0x40 1645 | ); 1646 | }, 1647 | //SET 6, A 1648 | //#0xF7: 1649 | function () { 1650 | this.registerA |= 0x40; 1651 | }, 1652 | //SET 7, B 1653 | //#0xF8: 1654 | function () { 1655 | this.registerB |= 0x80; 1656 | }, 1657 | //SET 7, C 1658 | //#0xF9: 1659 | function () { 1660 | this.registerC |= 0x80; 1661 | }, 1662 | //SET 7, D 1663 | //#0xFA: 1664 | function () { 1665 | this.registerD |= 0x80; 1666 | }, 1667 | //SET 7, E 1668 | //#0xFB: 1669 | function () { 1670 | this.registerE |= 0x80; 1671 | }, 1672 | //SET 7, H 1673 | //#0xFC: 1674 | function () { 1675 | this.registersHL |= 0x8000; 1676 | }, 1677 | //SET 7, L 1678 | //#0xFD: 1679 | function () { 1680 | this.registersHL |= 0x80; 1681 | }, 1682 | //SET 7, (HL) 1683 | //#0xFE: 1684 | function () { 1685 | this.memoryWriter[this.registersHL]( 1686 | this.registersHL, 1687 | this.readMemory(this.registersHL) | 0x80 1688 | ); 1689 | }, 1690 | //SET 7, A 1691 | //#0xFF: 1692 | function () { 1693 | this.registerA |= 0x80; 1694 | } 1695 | ]; 1696 | -------------------------------------------------------------------------------- /src/cartridge/Cartridge.ts: -------------------------------------------------------------------------------- 1 | import MBC from "./MBC"; 2 | import ROM from "../ROM"; 3 | import MBC1 from "./MBC1"; 4 | import MBC2 from "./MBC2"; 5 | import MBC3 from "./MBC3"; 6 | import MBC5 from "./MBC5"; 7 | import MBC7 from "./MBC7"; 8 | import RUMBLE from "./RUMBLE"; 9 | import GameBoy from "../GameBoy"; 10 | import settings from "../settings"; 11 | 12 | const gameAndWatchGameCode = "Game and Watch 50"; 13 | 14 | export default class Cartridge { 15 | hasMbc1: boolean = false; // does the cartridge use MBC1? 16 | hasMbc2: boolean = false; // does the cartridge use MBC2? 17 | hasMbc3: boolean = false; // does the cartridge use MBC3? 18 | hasMbc5: boolean = false; // does the cartridge use MBC5? 19 | hasMbc7: boolean = false; // does the cartridge use MBC7? 20 | hasRam: boolean = false; // does the cartridge use save RAM? 21 | hasRumble: boolean = false; // does the cartridge have Rumble addressing? (modified MBC5) 22 | hasCamera: boolean = false; // is the cartridge a GameBoy Camera? 23 | hasTama5: boolean = false; // does the cartridge use TAMA5? (Tamagotchi Cartridge) 24 | hasHuc3: boolean = false; // does the cartridge use HuC3? (Hudson Soft / modified MBC3) 25 | hasHuc1: boolean = false; // does the cartridge use HuC1 (Hudson Soft / modified MBC1)? 26 | hasMmmO1: boolean = false; 27 | hasRtc: boolean = false; // does the cartridge have a RTC? 28 | hasBattery: boolean = false; 29 | 30 | gameboy: GameBoy; 31 | rom: ROM; 32 | useGbcMode: boolean; 33 | 34 | name: string; 35 | gameCode: string; 36 | colorCompatibilityByte: number; 37 | type: number; 38 | typeName: string; 39 | romSizeType: number; 40 | ramSizeType: number; 41 | hasNewLicenseCode: boolean; 42 | licenseCode: number; 43 | 44 | mbc: MBC; 45 | mbc1: MBC1; 46 | mbc2: MBC2; 47 | mbc3: MBC3; 48 | mbc5: MBC5; 49 | mbc7: MBC7; 50 | rumble: RUMBLE; 51 | 52 | constructor(rom: ROM | Uint8Array | ArrayBuffer) { 53 | this.rom = rom instanceof ROM ? rom : new ROM(rom); 54 | } 55 | 56 | connect(gameboy: GameBoy) { 57 | this.gameboy = gameboy; 58 | } 59 | 60 | disconnect() { 61 | this.gameboy = undefined; 62 | } 63 | 64 | interpret() { 65 | this.name = this.rom.getString(0x134, 0x13e); 66 | this.gameCode = this.rom.getString(0x13f, 0x142); 67 | this.colorCompatibilityByte = this.rom.getByte(0x143); 68 | this.type = this.rom.getByte(0x147); 69 | this.setTypeName(); 70 | 71 | if (this.name) { 72 | console.log("Game Title: " + this.name); 73 | } 74 | if (this.gameCode) { 75 | console.log("Game Code: " + this.gameCode); 76 | } 77 | if (this.colorCompatibilityByte) { 78 | console.log("Color Compatibility Byte: " + this.colorCompatibilityByte); 79 | } 80 | if (this.type) { 81 | console.log("Cartridge Type: " + this.type); 82 | } 83 | if (this.typeName) { 84 | console.log("Cartridge Type Name: " + this.typeName); 85 | } 86 | 87 | this.romSizeType = this.rom.getByte(0x148); 88 | this.ramSizeType = this.rom.getByte(0x149); 89 | 90 | // Check the GB/GBC mode byte: 91 | if (!this.gameboy.usedBootRom) { 92 | switch (this.colorCompatibilityByte) { 93 | case 0x00: // GB only 94 | this.useGbcMode = false; 95 | break; 96 | case 0x32: // Exception to the GBC identifying code: 97 | if (!settings.hasGameBoyPriority && this.name + this.gameCode + this.colorCompatibilityByte === gameAndWatchGameCode) { 98 | this.useGbcMode = true; 99 | console.log("Created a boot exception for Game and Watch Gallery 2 (GBC ID byte is wrong on the cartridge)."); 100 | } else { 101 | this.useGbcMode = false; 102 | } 103 | break; 104 | case 0x80: // Both GB + GBC modes 105 | this.useGbcMode = !settings.hasGameBoyPriority; 106 | break; 107 | case 0xc0: // Only GBC mode 108 | this.useGbcMode = true; 109 | break; 110 | default: 111 | this.useGbcMode = false; 112 | console.warn("Unknown GameBoy game type code #" + this.colorCompatibilityByte + ", defaulting to GB mode (Old games don't have a type code)."); 113 | } 114 | } else { 115 | console.log("used boot rom"); 116 | this.useGbcMode = this.gameboy.usedGbcBootRom; // Allow the GBC boot ROM to run in GBC mode... 117 | } 118 | 119 | const oldLicenseCode = this.rom.getByte(0x14b); 120 | const newLicenseCode = this.rom.getByte(0x144) & 0xff00 | this.rom.getByte(0x145) & 0xff; 121 | if (oldLicenseCode !== 0x33) { 122 | this.hasNewLicenseCode = false; 123 | this.licenseCode = oldLicenseCode; 124 | } else { 125 | this.hasNewLicenseCode = true; 126 | this.licenseCode = newLicenseCode; 127 | } 128 | } 129 | 130 | setGbcMode(data: number) { 131 | this.useGbcMode = (data & 0x1) === 0; 132 | // Exception to the GBC identifying code: 133 | if (this.name + this.gameCode + this.colorCompatibilityByte === gameAndWatchGameCode) { 134 | this.useGbcMode = true; 135 | console.log("Created a boot exception for Game and Watch Gallery 2 (GBC ID byte is wrong on the cartridge)."); 136 | } 137 | console.log("Booted to GBC Mode: " + this.useGbcMode); 138 | } 139 | 140 | setTypeName() { 141 | switch (this.type) { 142 | case 0x00: 143 | this.typeName = "ROM"; 144 | break; 145 | case 0x01: 146 | this.hasMbc1 = true; 147 | this.typeName = "MBC1"; 148 | break; 149 | case 0x02: 150 | this.hasMbc1 = true; 151 | this.hasRam = true; 152 | this.typeName = "MBC1 + SRAM"; 153 | break; 154 | case 0x03: 155 | this.hasMbc1 = true; 156 | this.hasRam = true; 157 | this.hasBattery = true; 158 | this.typeName = "MBC1 + SRAM + Battery"; 159 | break; 160 | case 0x05: 161 | this.hasMbc2 = true; 162 | this.typeName = "MBC2"; 163 | break; 164 | case 0x06: 165 | this.hasMbc2 = true; 166 | this.hasBattery = true; 167 | this.typeName = "MBC2 + Battery"; 168 | break; 169 | case 0x08: 170 | this.hasRam = true; 171 | this.typeName = "ROM + SRAM"; 172 | break; 173 | case 0x09: 174 | this.hasRam = true; 175 | this.hasBattery = true; 176 | this.typeName = "ROM + SRAM + Battery"; 177 | break; 178 | case 0x0b: 179 | this.hasMmmO1 = true; 180 | this.typeName = "MMMO1"; 181 | break; 182 | case 0x0c: 183 | this.hasMmmO1 = true; 184 | this.hasRam = true; 185 | this.typeName = "MMMO1 + SRAM"; 186 | break; 187 | case 0x0d: 188 | this.hasMmmO1 = true; 189 | this.hasRam = true; 190 | this.hasBattery = true; 191 | this.typeName = "MMMO1 + SRAM + Battery"; 192 | break; 193 | case 0x0f: 194 | this.hasMbc3 = true; 195 | this.hasRtc = true; 196 | this.hasBattery = true; 197 | this.typeName = "MBC3 + RTC + Battery"; 198 | break; 199 | case 0x10: 200 | this.hasMbc3 = true; 201 | this.hasRtc = true; 202 | this.hasBattery = true; 203 | this.hasRam = true; 204 | this.typeName = "MBC3 + RTC + Battery + SRAM"; 205 | break; 206 | case 0x11: 207 | this.hasMbc3 = true; 208 | this.typeName = "MBC3"; 209 | break; 210 | case 0x12: 211 | this.hasMbc3 = true; 212 | this.hasRam = true; 213 | this.typeName = "MBC3 + SRAM"; 214 | break; 215 | case 0x13: 216 | this.hasMbc3 = true; 217 | this.hasRam = true; 218 | this.hasBattery = true; 219 | this.typeName = "MBC3 + SRAM + Battery"; 220 | break; 221 | case 0x19: 222 | this.hasMbc5 = true; 223 | this.typeName = "MBC5"; 224 | break; 225 | case 0x1a: 226 | this.hasMbc5 = true; 227 | this.hasRam = true; 228 | this.typeName = "MBC5 + SRAM"; 229 | break; 230 | case 0x1b: 231 | this.hasMbc5 = true; 232 | this.hasRam = true; 233 | this.hasBattery = true; 234 | this.typeName = "MBC5 + SRAM + Battery"; 235 | break; 236 | case 0x1c: 237 | this.hasRumble = true; 238 | this.typeName = "RUMBLE"; 239 | break; 240 | case 0x1d: 241 | this.hasRumble = true; 242 | this.hasRam = true; 243 | this.typeName = "RUMBLE + SRAM"; 244 | break; 245 | case 0x1e: 246 | this.hasRumble = true; 247 | this.hasRam = true; 248 | this.hasBattery = true; 249 | this.typeName = "RUMBLE + SRAM + Battery"; 250 | break; 251 | case 0x1f: 252 | this.hasCamera = true; 253 | this.typeName = "GameBoy Camera"; 254 | break; 255 | case 0x22: 256 | this.hasMbc7 = true; 257 | this.hasRam = true; 258 | this.hasBattery = true; 259 | this.typeName = "MBC7 + SRAM + Battery"; 260 | break; 261 | case 0xfd: 262 | this.hasTama5 = true; 263 | this.typeName = "TAMA5"; 264 | break; 265 | case 0xfe: 266 | this.hasHuc3 = true; 267 | this.typeName = "HuC3"; 268 | break; 269 | case 0xff: 270 | this.hasHuc1 = true; 271 | this.typeName = "HuC1"; 272 | break; 273 | default: 274 | throw new Error("Unknown Cartridge Type"); 275 | } 276 | 277 | if (this.hasMbc1) this.mbc1 = new MBC1(this); 278 | if (this.hasMbc2) this.mbc2 = new MBC2(this); 279 | if (this.hasMbc3) this.mbc3 = new MBC3(this); 280 | if (this.hasMbc5) this.mbc5 = new MBC5(this); 281 | if (this.hasMbc7) this.mbc7 = new MBC7(this); 282 | if (this.hasRumble) this.mbc5 = this.rumble = new RUMBLE(this); 283 | 284 | this.mbc = ( 285 | this.mbc1 || 286 | this.mbc2 || 287 | this.mbc3 || 288 | this.mbc5 || 289 | this.mbc7 || 290 | this.rumble || 291 | undefined 292 | ); 293 | } 294 | 295 | setupRAM() { 296 | if (this.mbc) this.mbc.setupRAM(); 297 | 298 | this.gameboy.loadRam(); 299 | this.gameboy.loadRtc(); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/cartridge/MBC.ts: -------------------------------------------------------------------------------- 1 | import RTC from "./RTC"; 2 | import * as util from "../util"; 3 | import Cartridge from "./Cartridge"; 4 | import { EventEmitter } from "events"; 5 | 6 | export default class MBC extends EventEmitter { 7 | currentRomBank: number; 8 | romBank1Offset: number; 9 | ram: Uint8Array; 10 | romBankEdge: number; 11 | currentMbcRamBank: number; 12 | currentRamBankPosition: number; 13 | ramBanksEnabled: boolean; 14 | romSize: number; 15 | ramSize: number; 16 | rtc?: RTC; 17 | 18 | romSizes = [ 19 | 0x00008000, // 32K 20 | 0x00010000, // 64K 21 | 0x00020000, // 128K 22 | 0x00040000, // 256K 23 | 0x00080000, // 512K 24 | 0x00100000, // 1024K 25 | 0x00200000, // 2048K 26 | 0x00400000, // 4096K 27 | 0x00800000 // 8192K 28 | ]; 29 | 30 | ramSizes = [ 31 | 0x00000000, // 0K 32 | 0x00002000, // 2K // Changed to 2000 to avoid problems 33 | 0x00002000, // 8K 34 | 0x00008000, // 32K 35 | 0x00020000, // 128K 36 | 0x00010000 // 64K 37 | ]; 38 | 39 | cartridge: Cartridge; 40 | 41 | constructor(cartridge: Cartridge) { 42 | super(); 43 | this.cartridge = cartridge; 44 | this.ramBanksEnabled = false; // MBC RAM Access Control. 45 | this.currentRamBankPosition = -0xa000; // MBC Position Adder; 46 | this.currentMbcRamBank = 0; // MBC Currently Indexed RAM Bank 47 | this.romBankEdge = Math.floor(cartridge.rom.length / 0x4000); 48 | } 49 | 50 | setupRom() { 51 | this.romSize = this.romSizes[this.cartridge.romSizeType]; 52 | console.log("ROM size 0x" + this.romSize.toString(16)); 53 | } 54 | 55 | setupRAM() { 56 | this.ramSize = this.ramSizes[this.cartridge.ramSizeType]; 57 | console.log("RAM size 0x" + this.ramSize.toString(16)); 58 | this.ram = new Uint8Array(this.ramSize); // Switchable RAM (Used by games for more RAM) for the main memory range 0xA000 - 0xC000. 59 | } 60 | 61 | loadRam(data: Uint8Array) { 62 | if (data.byteLength !== this.ramSize) return; 63 | this.ram = data.slice(0); 64 | } 65 | 66 | getRam() { 67 | return new Uint8Array(this.ram.buffer.slice(0, this.ramSize)); 68 | } 69 | 70 | cutSRAMFromBatteryFileArray(data: ArrayBuffer) { 71 | return new Uint8Array(data.slice(0, this.ramSize)); 72 | } 73 | 74 | saveState() { 75 | // TODO: remove after state refactor 76 | if (!this.cartridge.hasBattery || this.ram.length === 0) return; // No battery backup... 77 | 78 | // return the MBC RAM for backup... 79 | return util.fromTypedArray(this.ram); 80 | } 81 | 82 | readRam = (address: number) => { 83 | if (!this.ramBanksEnabled) return 0xff; 84 | return this.ram[address + this.currentRamBankPosition]; 85 | }; 86 | 87 | writeRam = (address: number, data: number) => { 88 | if (!this.ramBanksEnabled) return; 89 | 90 | this.emit("ramWrite"); 91 | this.ram[address + this.currentRamBankPosition] = data; 92 | }; 93 | 94 | // TODO: for MBC2 & MBC3, compare with other MBCx 95 | setCurrentROMBank() { 96 | // Read the cartridge ROM data from RAM memory: 97 | // Only map bank 0 to bank 1 here (MBC2 is like MBC1, but can only do 16 banks, so only the bank 0 quirk appears for MBC2): 98 | this.currentRomBank = Math.max( 99 | this.romBank1Offset % this.romBankEdge - 1, 100 | 0 101 | ) << 14; 102 | } 103 | 104 | toggle = (address: number, data: number) => { 105 | // MBC RAM Bank Enable/Disable: 106 | this.ramBanksEnabled = (data & 0x0f) === 0x0a; // If lower nibble is 0x0A, then enable, otherwise disable. 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /src/cartridge/MBC1.ts: -------------------------------------------------------------------------------- 1 | import MBC from "./MBC"; 2 | 3 | export default class MBC1 extends MBC { 4 | MBC1Mode: boolean = false; // MBC1 Type (4/32, 16/8) 5 | 6 | writeType = (address: number, data: number) => { 7 | // MBC1 mode setting: 8 | this.MBC1Mode = (data & 0x1) === 0x1; 9 | if (this.MBC1Mode) { 10 | this.romBank1Offset &= 0x1f; 11 | this.setCurrentROMBank(); 12 | } else { 13 | this.currentMbcRamBank = 0; 14 | this.currentRamBankPosition = -0xa000; 15 | } 16 | }; 17 | 18 | writeRomBank = (address: number, data: number) => { 19 | // MBC1 ROM bank switching: 20 | this.romBank1Offset = this.romBank1Offset & 0x60 | data & 0x1f; 21 | this.setCurrentROMBank(); 22 | }; 23 | 24 | writeRamBank = (address: number, data: number) => { 25 | // MBC1 RAM bank switching 26 | if (this.MBC1Mode) { 27 | // 4/32 Mode 28 | this.currentMbcRamBank = data & 0x03; 29 | this.currentRamBankPosition = (this.currentMbcRamBank << 13) - 0xa000; 30 | } else { 31 | // 16/8 Mode 32 | this.romBank1Offset = (data & 0x03) << 5 | this.romBank1Offset & 0x1f; 33 | this.setCurrentROMBank(); 34 | } 35 | }; 36 | 37 | setCurrentROMBank() { 38 | // Read the cartridge ROM data from RAM memory: 39 | switch (this.romBank1Offset) { 40 | case 0x00: 41 | case 0x20: 42 | case 0x40: 43 | case 0x60: 44 | // Bank calls for 0x00, 0x20, 0x40, and 0x60 are really for 0x01, 0x21, 0x41, and 0x61. 45 | this.currentRomBank = this.romBank1Offset % this.romBankEdge << 14; 46 | break; 47 | default: 48 | this.currentRomBank = this.romBank1Offset % this.romBankEdge - 1 << 14; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/cartridge/MBC2.ts: -------------------------------------------------------------------------------- 1 | import MBC from "./MBC"; 2 | 3 | export default class MBC2 extends MBC { 4 | writeRomBank = (address: number, data: number) => { 5 | // MBC2 ROM bank switching: 6 | this.romBank1Offset = data & 0x0f; 7 | this.setCurrentROMBank(); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/cartridge/MBC3.ts: -------------------------------------------------------------------------------- 1 | import RTC from "./RTC"; 2 | import MBC from "./MBC"; 3 | import Cartridge from "./Cartridge"; 4 | 5 | export default class MBC3 extends MBC { 6 | rtc: RTC; 7 | 8 | constructor(cartridge: Cartridge) { 9 | super(cartridge); 10 | 11 | this.rtc = new RTC(this); 12 | } 13 | 14 | writeRomBank = (address: number, data: number) => { 15 | // MBC3 ROM bank switching: 16 | this.romBank1Offset = data & 0x7f; 17 | this.setCurrentROMBank(); 18 | }; 19 | 20 | writeRamBank = (address: number, data: number) => { 21 | this.currentMbcRamBank = data; 22 | if (data < 4) { 23 | // MBC3 RAM bank switching 24 | this.currentRamBankPosition = (this.currentMbcRamBank << 13) - 0xa000; 25 | } 26 | }; 27 | 28 | writeHuc3RamBank = (address: number, data: number) => { 29 | // HuC3 RAM bank switching 30 | this.cartridge.mbc.currentMbcRamBank = data & 0x03; 31 | this.cartridge.mbc.currentRamBankPosition = (this.cartridge.mbc.currentMbcRamBank << 13) - 0xa000; 32 | }; 33 | 34 | writeRam = (address: number, data: number) => { 35 | if (!this.ramBanksEnabled) return; 36 | 37 | switch (this.currentMbcRamBank) { 38 | case 0x00: 39 | case 0x01: 40 | case 0x02: 41 | case 0x03: 42 | this.emit("ramWrite"); 43 | this.ram[address + this.currentRamBankPosition] = data; 44 | break; 45 | case 0x08: 46 | this.rtc?.writeSeconds(data); 47 | break; 48 | case 0x09: 49 | this.rtc?.writeMinutes(data); 50 | break; 51 | case 0x0a: 52 | this.rtc?.writeHours(data); 53 | break; 54 | case 0x0b: 55 | this.rtc?.writeDaysLow(data); 56 | break; 57 | case 0x0c: 58 | this.rtc?.writeDaysHigh(data); 59 | break; 60 | default: 61 | console.log("Invalid MBC3 bank address selected: " + this.currentMbcRamBank); 62 | } 63 | }; 64 | 65 | readRam = (address: number) => { 66 | if (!this.ramBanksEnabled) return 0xFF; 67 | 68 | switch (this.currentMbcRamBank) { 69 | case 0x00: 70 | case 0x01: 71 | case 0x02: 72 | case 0x03: 73 | return this.ram[address + this.currentRamBankPosition]; 74 | case 0x08: 75 | if (this.rtc) return this.rtc.readSeconds(); 76 | break; 77 | case 0x09: 78 | if (this.rtc) return this.rtc.readMinutes(); 79 | break; 80 | case 0x0a: 81 | if (this.rtc) return this.rtc.readHours(); 82 | break; 83 | case 0x0b: 84 | if (this.rtc) return this.rtc.readDaysLow(); 85 | break; 86 | case 0x0c: 87 | if (this.rtc) return this.rtc.readDaysHigh(); 88 | break; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/cartridge/MBC5.ts: -------------------------------------------------------------------------------- 1 | import MBC from "./MBC"; 2 | 3 | export default class MBC5 extends MBC { 4 | setCurrentROMBank() { 5 | // Read the cartridge ROM data from RAM memory: 6 | this.currentRomBank = this.romBank1Offset % this.romBankEdge - 1 << 14; 7 | } 8 | 9 | writRomBank = (address: number, data: number) => { 10 | // MBC5 ROM bank switching: 11 | this.romBank1Offset = this.romBank1Offset & 0x100 | data; 12 | this.setCurrentROMBank(); 13 | }; 14 | 15 | writeHighRomBank = (address: number, data: number) => { 16 | // MBC5 ROM bank switching (by least significant bit): 17 | this.romBank1Offset = (data & 0x01) << 8 | this.romBank1Offset & 0xff; 18 | this.setCurrentROMBank(); 19 | } 20 | 21 | writeRamBank = (address: number, data: number) => { 22 | // MBC5 RAM bank switching 23 | this.currentMbcRamBank = data & 0xf; 24 | this.currentRamBankPosition = (this.currentMbcRamBank << 13) - 0xa000; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/cartridge/MBC7.ts: -------------------------------------------------------------------------------- 1 | import MBC from "./MBC"; 2 | 3 | export default class MBC7 extends MBC { 4 | // Gyro 5 | highX: number = 127; 6 | lowX: number = 127; 7 | highY: number = 127; 8 | lowY: number = 127; 9 | 10 | applyGyroEvent(x: number, y: number) { 11 | x *= -100; 12 | x += 2047; 13 | this.highX = x >> 8; 14 | this.lowX = x & 0xff; 15 | y *= -100; 16 | y += 2047; 17 | this.highY = y >> 8; 18 | this.lowY = y & 0xff; 19 | } 20 | 21 | read(address: number) { 22 | if (!this.ramBanksEnabled) return 0xFF; 23 | 24 | switch (address) { 25 | case 0xa000: 26 | case 0xa060: 27 | case 0xa070: 28 | return 0; 29 | case 0xa080: 30 | // TODO: Gyro Control Register 31 | return 0; 32 | case 0xa050: 33 | // Y High Byte 34 | return this.highY; 35 | case 0xa040: 36 | // Y Low Byte 37 | return this.lowY; 38 | case 0xa030: 39 | // X High Byte 40 | return this.highX; 41 | case 0xa020: 42 | // X Low Byte: 43 | return this.lowX; 44 | default: 45 | return this.ram[address + this.currentRamBankPosition]; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/cartridge/RTC.ts: -------------------------------------------------------------------------------- 1 | import MBC from "./MBC"; 2 | 3 | export default class RTC { 4 | lastTime: number; 5 | latchedLDays: number; 6 | latchedHours: number; 7 | latchedMinutes: number; 8 | latchedSeconds: number; 9 | latchedHDays: number; 10 | RTCDayOverFlow: boolean; 11 | RTCDays: number; 12 | RTCHours: number; 13 | RTCMinutes: number; 14 | RTCSeconds: number; 15 | RTCHalt: boolean; 16 | RTCisLatched: boolean; 17 | 18 | constructor( 19 | private mbc: MBC 20 | ) { } 21 | 22 | // TODO: rename RTC vars 23 | 24 | writeSeconds(data) { 25 | if (data < 60) { 26 | this.RTCSeconds = data; 27 | } else { 28 | console.log( 29 | "(Bank #" + 30 | this.mbc.currentMbcRamBank + 31 | ") RTC write out of range: " + 32 | data 33 | ); 34 | } 35 | } 36 | 37 | writeMinutes(data) { 38 | if (data < 60) { 39 | this.RTCMinutes = data; 40 | } else { 41 | console.log( 42 | "(Bank #" + 43 | this.mbc.currentMbcRamBank + 44 | ") RTC write out of range: " + 45 | data 46 | ); 47 | } 48 | } 49 | 50 | writeDaysLow(data) { 51 | this.RTCDays = data & 0xff | this.RTCDays & 0x100; 52 | } 53 | 54 | writeDaysHigh(data) { 55 | this.RTCDayOverFlow = data > 0x7f; 56 | this.RTCHalt = (data & 0x40) === 0x40; 57 | this.RTCDays = (data & 0x1) << 8 | this.RTCDays & 0xff; 58 | } 59 | 60 | writeHours(data) { 61 | if (data < 24) { 62 | this.RTCHours = data; 63 | } else { 64 | console.log( 65 | "(Bank #" + 66 | this.mbc.currentMbcRamBank + 67 | ") RTC write out of range: " + 68 | data 69 | ); 70 | } 71 | } 72 | 73 | readSeconds() { 74 | return this.latchedSeconds; 75 | } 76 | 77 | readMinutes() { 78 | return this.latchedMinutes; 79 | } 80 | 81 | readHours() { 82 | return this.latchedHours; 83 | } 84 | 85 | readDaysLow() { 86 | return this.latchedLDays; 87 | } 88 | 89 | readDaysHigh() { 90 | return (this.RTCDayOverFlow ? 0x80 : 0) + 91 | (this.RTCHalt ? 0x40 : 0) + 92 | this.latchedHDays; 93 | } 94 | 95 | writeLatch = (address: number, data: number) => { 96 | if (data === 0) { 97 | this.RTCisLatched = false; 98 | } else if (!this.RTCisLatched) { 99 | // Copy over the current RTC time for reading. 100 | this.RTCisLatched = true; 101 | this.latchedSeconds = this.RTCSeconds | 0; 102 | this.latchedMinutes = this.RTCMinutes; 103 | this.latchedHours = this.RTCHours; 104 | this.latchedLDays = this.RTCDays & 0xff; 105 | this.latchedHDays = this.RTCDays >> 8; 106 | } 107 | }; 108 | 109 | get() { 110 | const lastTimeSeconds = Math.round(this.lastTime / 1000); 111 | const lastTimeLow = lastTimeSeconds >> 0 & 0xffff; 112 | const lastTimeHigh = lastTimeSeconds >> 16 & 0xffff; 113 | 114 | const data = new Uint32Array([ 115 | this.RTCSeconds, 116 | this.RTCMinutes, 117 | this.RTCHours, 118 | this.RTCDays, 119 | this.RTCDayOverFlow ? 1 : 0, 120 | this.latchedSeconds, 121 | this.latchedMinutes, 122 | this.latchedHours, 123 | this.latchedLDays, 124 | this.latchedHDays, 125 | lastTimeLow, 126 | lastTimeHigh 127 | ]); 128 | 129 | return data; 130 | } 131 | 132 | load(array) { 133 | const options = this.extract(array); 134 | 135 | this.RTCSeconds = options.seconds; 136 | this.RTCMinutes = options.minutes; 137 | this.RTCHours = options.hours; 138 | this.RTCDays = options.daysLow; 139 | this.RTCDayOverFlow = options.daysHigh; 140 | 141 | this.latchedSeconds = options.latchedSeconds; 142 | this.latchedMinutes = options.latchedMinutes; 143 | this.latchedHours = options.latchedHours; 144 | this.latchedLDays = options.latchedDaysLow; 145 | this.latchedHDays = options.latchedDaysHigh; 146 | 147 | this.lastTime = options.lastTime; 148 | } 149 | 150 | cutBatteryFileArray(data: ArrayBuffer) { 151 | return new Uint32Array(data.slice(this.mbc.ramSize, this.mbc.ramSize + 4 * 12)); 152 | } 153 | 154 | extract(array) { 155 | const seconds = array[0]; 156 | const minutes = array[1]; 157 | const hours = array[2]; 158 | const daysLow = array[3]; 159 | const daysHigh = array[4]; 160 | const latchedSeconds = array[5]; 161 | const latchedMinutes = array[6]; 162 | const latchedHours = array[7]; 163 | const latchedDaysLow = array[8]; 164 | const latchedDaysHigh = array[9]; 165 | const lastTimeLow = array[10]; 166 | const lastTimeHigh = array[11]; 167 | 168 | let lastTimeSeconds = lastTimeLow; 169 | if (lastTimeLow && lastTimeHigh) { 170 | lastTimeSeconds = lastTimeHigh << 16 | lastTimeLow; 171 | } 172 | 173 | return { 174 | seconds, 175 | minutes, 176 | hours, 177 | daysLow, 178 | daysHigh, 179 | latchedSeconds, 180 | latchedMinutes, 181 | latchedHours, 182 | latchedDaysLow, 183 | latchedDaysHigh, 184 | lastTime: lastTimeSeconds * 1000 185 | }; 186 | } 187 | 188 | saveState() { 189 | // TODO: remove after state refactor 190 | // return the MBC RAM for backup... 191 | return [ 192 | this.lastTime, 193 | this.RTCisLatched, 194 | this.latchedSeconds, 195 | this.latchedMinutes, 196 | this.latchedHours, 197 | this.latchedLDays, 198 | this.latchedHDays, 199 | this.RTCSeconds, 200 | this.RTCMinutes, 201 | this.RTCHours, 202 | this.RTCDays, 203 | this.RTCDayOverFlow, 204 | this.RTCHalt 205 | ]; 206 | } 207 | 208 | loadState(data) { 209 | let index = 0; 210 | this.lastTime = data[index++]; 211 | this.RTCisLatched = data[index++]; 212 | this.latchedSeconds = data[index++]; 213 | this.latchedMinutes = data[index++]; 214 | this.latchedHours = data[index++]; 215 | this.latchedLDays = data[index++]; 216 | this.latchedHDays = data[index++]; 217 | this.RTCSeconds = data[index++]; 218 | this.RTCMinutes = data[index++]; 219 | this.RTCHours = data[index++]; 220 | this.RTCDays = data[index++]; 221 | this.RTCDayOverFlow = data[index++]; 222 | this.RTCHalt = data[index]; 223 | } 224 | 225 | updateClock() { 226 | const currentTime = new Date().getTime(); 227 | const elapsedTime = currentTime - this.lastTime; 228 | this.lastTime = currentTime; 229 | 230 | if (!this.RTCHalt) { 231 | //Update the MBC3 RTC: 232 | this.RTCSeconds += elapsedTime / 1000; 233 | while (this.RTCSeconds >= 60) { 234 | // System can stutter, so the seconds difference can get large, thus the "while". 235 | this.RTCSeconds -= 60; 236 | ++this.RTCMinutes; 237 | if (this.RTCMinutes >= 60) { 238 | this.RTCMinutes -= 60; 239 | ++this.RTCHours; 240 | if (this.RTCHours >= 24) { 241 | this.RTCHours -= 24; 242 | ++this.RTCDays; 243 | if (this.RTCDays >= 512) { 244 | this.RTCDays -= 512; 245 | this.RTCDayOverFlow = true; 246 | } 247 | } 248 | } 249 | } 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/cartridge/RUMBLE.ts: -------------------------------------------------------------------------------- 1 | import MBC5 from "./MBC5"; 2 | 3 | export default class RUMBLE extends MBC5 { 4 | writeRamBank = (address: number, data: number) => { 5 | // MBC5 RAM bank switching 6 | if (data & 0x08) this.emit("rumble"); 7 | data &= 0x7; 8 | 9 | super.writeRamBank(address, data); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/dutyLookup.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | // map the duty values given to ones we can work with. 3 | [false, false, false, false, false, false, false, true], 4 | [true, false, false, false, false, false, false, true], 5 | [true, false, false, false, false, true, true, true], 6 | [false, true, true, true, true, true, true, false] 7 | ]; 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "./types"; 2 | import * as util from "./util"; 3 | import GameBoy from "./GameBoy"; 4 | import Storage from "./storages/Storage"; 5 | import LocalStorage from "./storages/LocalStorage"; 6 | import MemoryStorage from "./storages/MemoryStorage"; 7 | 8 | export { GameBoy, Storage, MemoryStorage, LocalStorage, util }; 9 | export default GameBoy; 10 | -------------------------------------------------------------------------------- /src/initialState.ts: -------------------------------------------------------------------------------- 1 | import dutyLookup from "./dutyLookup"; 2 | 3 | export default [ 4 | true, // Whether we're in the GBC boot ROM. 5 | // CPU Registers and Flags: 6 | 0x01, // Register A (Accumulator) 7 | true, // Register F - Result was zero 8 | false, // Register F - Subtraction was executed 9 | true, // Register F - Half carry or half borrow 10 | true, // Register F - Carry or borrow 11 | 0x00, // Register B 12 | 0x13, // Register C 13 | 0x00, // Register D 14 | 0xd8, // Register E 15 | 0x014d, // Registers H and L combined 16 | 0xfffe, // Stack Pointer 17 | 0x0100, // Program Counter 18 | // Some CPU Emulation State Variables: 19 | false, // Has the CPU been suspended until the next interrupt? 20 | true, // Are interrupts enabled? 21 | false, // HDMA Transfer Flag - GBC only 22 | 0, // The number of clock cycles emulated. 23 | 0, // GBC double speed clocking shifter. 24 | [], // Main Core Memory 25 | [], // Extra VRAM bank for GBC. 26 | 0, // Current VRAM bank for GBC. 27 | [], // GBC main RAM Banks 28 | 1, // Currently Switched GameBoy Color ram bank 29 | -0xd000, // GBC RAM offset from address start. 30 | 0, // Offset of the ROM bank switching. 31 | 0, // The parsed current ROM bank selection. 32 | 0, // The scan line mode (for lines 1-144 it's 2-3-0, for 145-154 it's 1) 33 | false, // Should we trigger an interrupt if LY==LYC? 34 | false, // Should we trigger an interrupt if in mode 2? 35 | false, // Should we trigger an interrupt if in mode 1? 36 | false, // Should we trigger an interrupt if in mode 0? 37 | false, // Is the emulated LCD controller on? 38 | 0, // The current bank of the character map the window uses. 39 | false, // Is the windows enabled? 40 | false, // Are sprites enabled? 41 | true, // Are we doing 8x8 or 8x16 sprites? 42 | 0, // The current bank of the character map the background uses. 43 | 0x80, // Fast mapping of the tile numbering/ 44 | false, // Is TIMA enabled? 45 | 56, // DIV Ticks Counter (Invisible lower 8-bit) 46 | 60, // Counter for how many instructions have been executed on a scanline so far. 47 | 0, // Counter for the TIMA timer. 48 | 1024, // Timer Max Ticks 49 | 0, // Serial IRQ Timer 50 | 0, // Serial Transfer Shift Timer 51 | 0, // Serial Transfer Shift Timer Refill 52 | 0, // Are the interrupts on queue to be enabled? 53 | new Date().getTime(), // The last time we iterated the main loop. 54 | 0, // To prevent the repeating of drawing a blank screen. 55 | [], // The internal frame-buffer. 56 | true, // Is the BG enabled? 57 | 0x2000, // channel1FrequencyTracker 58 | 0x200, // channel1FrequencyCounter 59 | 0, // channel1totalLength 60 | 0, // channel1envelopeVolume 61 | false, // channel1envelopeType 62 | 0, // channel1envelopeSweeps 63 | 0, // channel1envelopeSweepsLast 64 | true, // channel1consecutive 65 | 0, // channel1frequency 66 | false, // channel1SweepFault 67 | 0, // channel1ShadowFrequency 68 | 1, // channel1timeSweep 69 | 0, // channel1lastTimeSweep 70 | false, // channel1Swept 71 | 0, // channel1frequencySweepDivider 72 | false, // channel1decreaseSweep 73 | 0x2000, // channel2FrequencyTracker 74 | 0x200, // channel2FrequencyCounter 75 | 0, // channel2totalLength 76 | 0, // channel2envelopeVolume 77 | false, // channel2envelopeType 78 | 0, // channel2envelopeSweeps 79 | 0, // channel2envelopeSweepsLast 80 | true, // channel2consecutive 81 | 0, // channel2frequency 82 | false, // channel3canPlay 83 | 0, // channel3totalLength 84 | 4, // channel3patternType 85 | 0, // channel3frequency 86 | true, // channel3consecutive 87 | null, // Channel 3 adjusted sample buffer. 88 | 8, // channel4FrequencyPeriod 89 | 0, // channel4lastSampleLookup 90 | 0, // channel4totalLength 91 | 0, // channel4envelopeVolume 92 | 0, // channel4currentVolume 93 | false, // channel4envelopeType 94 | 0, // channel4envelopeSweeps 95 | 0, // channel4envelopeSweepsLast 96 | true, // channel4consecutive 97 | 0x7fff, // channel4BitRange 98 | false, // As its name implies 99 | // Vin Shit: 100 | 8, // Computed post-mixing volume. 101 | 8, // Computed post-mixing volume. 102 | // Channel paths enabled: 103 | false, // leftChannel1 104 | false, // leftChannel2 105 | false, // leftChannel3 106 | false, // leftChannel4 107 | false, // rightChannel1 108 | false, // rightChannel2 109 | false, // rightChannel3 110 | false, // rightChannel4 111 | // Channel output level caches: 112 | 0, // channel1currentSampleLeft 113 | 0, // channel1currentSampleRight 114 | 0, // channel2currentSampleLeft 115 | 0, // channel2currentSampleRight 116 | 0, // channel3currentSampleLeft 117 | 0, // channel3currentSampleRight 118 | 0, // channel4currentSampleLeft 119 | 0, // channel4currentSampleRight 120 | 0, // channel1currentSampleLeftSecondary 121 | 0, // channel1currentSampleRightSecondary 122 | 0, // channel2currentSampleLeftSecondary 123 | 0, // channel2currentSampleRightSecondary 124 | 0, // channel3currentSampleLeftSecondary 125 | 0, // channel3currentSampleRightSecondary 126 | 0, // channel4currentSampleLeftSecondary 127 | 0, // channel4currentSampleRightSecondary 128 | 0, // channel1currentSampleLeftTrimary 129 | 0, // channel1currentSampleRightTrimary 130 | 0, // channel2currentSampleLeftTrimary 131 | 0, // channel2currentSampleRightTrimary 132 | 0, // mixerOutputCache 133 | 0, // channel1DutyTracker 134 | dutyLookup[2], // channel1CachedDuty 135 | 0, // channel2DutyTracker 136 | dutyLookup[2], // channel2CachedDuty 137 | false, // channel1Enabled 138 | false, // channel2Enabled 139 | false, // channel3Enabled 140 | false, // channel4Enabled 141 | 0x2000, // sequencerClocks 142 | 0, // sequencePosition 143 | 0x800, // channel3Counter 144 | 8, // channel4Counter 145 | 0, // cachedChannel3Sample 146 | 0, // cachedChannel4Sample 147 | 0x800, // channel3FrequencyPeriod 148 | 0, // channel3lastSampleLookup 149 | 144, // Actual scan line... 150 | 0, // Last rendered scan line... 151 | 0, // queuedScanlines 152 | // RTC (Real Time Clock for MBC3): 153 | false, // RTCisLatched 154 | 0, // RTC latched seconds. 155 | 0, // RTC latched minutes. 156 | 0, // RTC latched hours. 157 | 0, // RTC latched lower 8-bits of the day counter. 158 | 0, // RTC latched high-bit of the day counter. 159 | 0, // RTC seconds counter. 160 | 0, // RTC minutes counter. 161 | 0, // RTC hours counter. 162 | 0, // RTC days counter. 163 | false, // Did the RTC overflow and wrap the day counter? 164 | false, // Is the RTC allowed to clock up? 165 | false, // Updated upon ROM loading... 166 | false, // Did we trip the DMG Halt bug? 167 | 0, // Tracker for STAT triggering. 168 | -0xf000, // GBC RAM (ECHO mirroring) offset from address start. 169 | 0, // Current Y offset of the window. 170 | 0, // Current X offset of the window. 171 | null, // gbcOBJRawPalette 172 | null, // gbcBGRawPalette 173 | null, // gbOBJPalette 174 | null, // gbBGPalette 175 | null, // gbcOBJPalette 176 | null, // gbcBGPalette 177 | null, // gbBGColorizedPalette 178 | null, // gbOBJColorizedPalette 179 | null, // cachedBGPaletteConversion 180 | null, // cachedOBJPaletteConversion 181 | // BG Tile Pointer Caches: 182 | null, // BGCHRBank1 183 | null, // BGCHRBank2 184 | 0, // Post-Halt clocking. 185 | 0, // IF Register 186 | 0, // IE Register 187 | 0, // HALT clocking overrun carry over. 188 | false, // colorizedGBPalettes 189 | 0, // Register SCY (Y-Scroll) 190 | 0, // Register SCX (X-Scroll) 191 | false, // CPU STOP status. 192 | 1, // audioClocksUntilNextEvent 193 | 1 // audioClocksUntilNextEventCounter 194 | ]; 195 | -------------------------------------------------------------------------------- /src/memory/Layout.ts: -------------------------------------------------------------------------------- 1 | // Interrupt service routine addresses 2 | export const VBLANK_ISR_ADDR = 0x40; 3 | export const LCDC_ISR_ADDR = 0x48; 4 | export const TIMER_ISR_ADDR = 0x50; 5 | export const IO_ISR_ADDR = 0x58; 6 | export const HIGH_LOW_ISR_ADDR = 0x60; 7 | 8 | // Memory definitions 9 | 10 | export const INTERRUPT_VECTORS_START = 0x0000; 11 | export const INTERRUPT_VECTORS_END = 0x00FF; 12 | 13 | export const CART_HEADER_START = 0x0100; 14 | export const CART_HEADER_END = 0x014F; 15 | 16 | export const CART_ROM_BANK0_START = 0x0150; 17 | export const CART_ROM_BANK0_END = 0x3FFF; 18 | 19 | export const CART_ROM_SWITCH_BANK_START = 0x4000; 20 | export const CART_ROM_SWITCH_BANK_END = 0x7FFF; 21 | 22 | export const TILE_SET_0_START = 0x8000; 23 | export const TILE_SET_0_END = 0x8FFF; 24 | 25 | export const TILE_SET_1_START = 0x8800; 26 | export const TILE_SET_1_END = 0x97FF; 27 | 28 | export const BG_MAP_DATA0_START = 0x9800; 29 | export const BG_MAP_DATA0_END = 0x9BFF; 30 | 31 | export const BG_MAP_DATA1_START = 0x9C00; 32 | export const BG_MAP_DATA1_END = 0x9FFF; 33 | 34 | export const cartridgeRamStartAddress = 0xA000; 35 | export const cartridgeRamEndAddress = 0xBFFF; 36 | 37 | export const INTERNAL_RAM_BANK0_START = 0xC000; 38 | export const INTERNAL_RAM_BANK0_END = 0xCFFF; 39 | 40 | export const INTERNAL_RAM_SWITCH_BANK_START = 0xD000; 41 | export const INTERNAL_RAM_SWITCH_BANK_END = 0xDFFF; 42 | 43 | export const echoRamStartAddress = 0xE000; 44 | export const echoRamEndAddress = 0xFDFF; 45 | 46 | /*--------------- HIGH MEM-------------- */ 47 | export const SPRITE_ATTRIBUTE_TABLE_START = 0xFE00; 48 | export const SPRITE_ATTRIBUTE_TABLE_END = 0xFE9F; 49 | 50 | export const unusableMemoryStartAddress = 0xFEA0; 51 | export const unusableMemoryEndAddress = 0xFEFF; 52 | 53 | /* -------------IO ports/registers ------------------*/ 54 | export const joypadAddress = 0xFF00; 55 | export const serialDataAddress = 0xFF01; 56 | export const serialControlAddress = 0xFF02; 57 | export const divAddress = 0xFF04; /* Divider register */ 58 | export const TIMA_REG = 0xFF05; /* Timer Counter */ 59 | export const TMA_REG = 0xFF06; /* Timer Modulo */ 60 | export const timerControlAddress = 0xFF07; /* Timer Control */ 61 | export const INTERRUPT_FLAG_REG = 0xFF0F; /* Interrupt Flag */ 62 | 63 | /* Sound Mode 1 registers */ 64 | export const NR_10_REG = 0xFF10; /* Sweep */ 65 | export const NR_11_REG = 0xFF11; /* Length wave pattern*/ 66 | export const NR_12_REG = 0xFF12; /* Envelope */ 67 | export const NR_13_REG = 0xFF12; /* Frequency Lo */ 68 | export const NR_14_REG = 0xFF14; /* Frequency Hi */ 69 | 70 | /* Sound Mode 2 registers */ 71 | export const NR_21_REG = 0xFF16; /* Length wave pattern*/ 72 | export const NR_22_REG = 0xFF17; /* Envelope */ 73 | export const NR_23_REG = 0xFF18; /* Frequency Lo */ 74 | export const NR_24_REG = 0xFF19; /* Frequency Hi */ 75 | 76 | /* Sound Mode 3 registers */ 77 | export const NR_30_REG = 0xFF1A; /* Sound on/off*/ 78 | export const NR_31_REG = 0xFF1B; /* Sound length */ 79 | export const NR_32_REG = 0xFF1C; /* Select output level */ 80 | export const NR_33_REG = 0xFF1D; /* Frequency Lo */ 81 | export const NR_34_REG = 0xFF1E; /* Frequency Hi */ 82 | 83 | /* Sound Mode 4 registers */ 84 | export const NR_41_REG = 0xFF20; /* Sound length */ 85 | export const NR_42_REG = 0xFF21; /* Envelope */ 86 | export const NR_43_REG = 0xFF22; /* Polynomial Counter */ 87 | export const NR_44_REG = 0xFF23; /* Counter/Consecutive; initial */ 88 | 89 | export const soundChannelVolumeControlAddress = 0xFF24; // NR50 90 | export const NR_51_REG = 0xFF25; /* Selection of Sound output terminal */ 91 | export const NR_52_REG = 0xFF26; /* Sound on/off */ 92 | 93 | /* Waveform Storage for Arbitrary Sound data */ 94 | export const WAVE_PATTERN_RAM_START = 0xFF30; 95 | export const WAVE_PATTERN_RAM_END = 0xFF3F; 96 | 97 | /* ------Screen/Graphics register locations------- */ 98 | export const LCDC_REG = 0xFF40; /* LCD Control */ 99 | export const STAT_REG = 0xFF41; /* LCD status */ 100 | export const SCROLL_Y_REG = 0xFF42; /* 8 bit value to scroll BG Y pos */ 101 | export const SCROLL_X_REG = 0xFF43; /* 8 bit value to scroll BG X pos */ 102 | export const LY_REG = 0xFF44; /* LCDC Y-Coordinate */ 103 | export const LYC_REG = 0xFF45; /* LY Compare */ 104 | export const DMA_REG = 0xFF46; /* DMA Transfer and Start Address */ 105 | export const BGP_REF = 0xFF47; /* BG and Window Palette Data */ 106 | export const OBP0_REG = 0xFF48; /* Object Palette 0 Data */ 107 | export const OBP1_REG = 0xFF49; /* Object Palette 1 Data */ 108 | export const WY_REG = 0xFF4A; /* Window Y Position; 0 <= WY <= 143*/ 109 | export const WX_REG = 0xFF4B; /* Window X Position; 0 <= WX <= 166 */ 110 | 111 | export const toggleBootRomControlAddress = 0xFF50; 112 | 113 | /* DMA transfer for Gameboy Color */ 114 | export const HDMA1_REG = 0xFF51; 115 | export const HDMA2_REG = 0xFF52; 116 | export const HDMA3_REG = 0xFF53; 117 | export const HDMA4_REG = 0xFF54; 118 | export const HDMA5_REG = 0xFF55; 119 | 120 | export const KEY1_REG = 0xFF4D; /* Prepare Speed switch for Gameboy Color, used to switch clock speed */ 121 | 122 | export const VBANK_REG = 0xFF4F; /* Select which VRAM bank to use in Gameboy color */ 123 | 124 | export const INFRARED_REG = 0xFF56; /* Infrared Communications Port for Gameboy Color */ 125 | 126 | export const BGPI = 0xFF68; // Background Palette index for Gameboy Color 127 | export const BGPD = 0xFF69; // Background Palette data for Gameboy Color 128 | export const SPPI = 0xFF6A; // Sprite Palette index for Gameboy Color 129 | export const SPPD = 0xFF6B; // Sprite Palette data for Gameboy Color 130 | 131 | // FF6C - Bit 0 (Read/Write) - CGB Mode Only 132 | // Only the least significant bit of this register can be written to. It defaults to 0, so this register's initial value is $FE. 133 | // In non-CGB mode, it isn't writable, and its value is locked at $FF. 134 | export const undocumentedGbcOnlyAddress = 0xFF6C; 135 | 136 | export const SRAM_BANK = 0xFF70; // Register to select internal RAM banks for Gameboy Color 137 | 138 | export const interruptEnableAddress = 0xFFFF; -------------------------------------------------------------------------------- /src/postBootRomState.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | 0x0F, 0x00, 0x7C, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 3 | 0x80, 0xBF, 0xF3, 0xFF, 0xBF, 0xFF, 0x3F, 0x00, 0xFF, 0xBF, 0x7F, 0xFF, 0x9F, 0xFF, 0xBF, 0xFF, 4 | 0xFF, 0x00, 0x00, 0xBF, 0x77, 0xF3, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 5 | 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 6 | 0x91, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7E, 0xFF, 0xFE, 7 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 8 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xC1, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 9 | 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x8F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 10 | 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, 11 | 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 12 | 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, 13 | 0x45, 0xEC, 0x52, 0xFA, 0x08, 0xB7, 0x07, 0x5D, 0x01, 0xFD, 0xC0, 0xFF, 0x08, 0xFC, 0x00, 0xE5, 14 | 0x0B, 0xF8, 0xC2, 0xCE, 0xF4, 0xF9, 0x0F, 0x7F, 0x45, 0x6D, 0x3D, 0xFE, 0x46, 0x97, 0x33, 0x5E, 15 | 0x08, 0xEF, 0xF1, 0xFF, 0x86, 0x83, 0x24, 0x74, 0x12, 0xFC, 0x00, 0x9F, 0xB4, 0xB7, 0x06, 0xD5, 16 | 0xD0, 0x7A, 0x00, 0x9E, 0x04, 0x5F, 0x41, 0x2F, 0x1D, 0x77, 0x36, 0x75, 0x81, 0xAA, 0x70, 0x3A, 17 | 0x98, 0xD1, 0x71, 0x02, 0x4D, 0x01, 0xC1, 0xFF, 0x0D, 0x00, 0xD3, 0x05, 0xF9, 0x00, 0x0B, 0x00 18 | ]; -------------------------------------------------------------------------------- /src/secondaryTickTable.ts: -------------------------------------------------------------------------------- 1 | export default [ // Number of machine cycles for each 0xCBXX instruction: 2 | // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F 3 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // 0 4 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // 1 5 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // 2 6 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // 3 7 | 8 | 8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8, // 4 9 | 8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8, // 5 10 | 8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8, // 6 11 | 8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8, // 7 12 | 13 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // 8 14 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // 9 15 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // A 16 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // B 17 | 18 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // C 19 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // D 20 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8, // E 21 | 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 16, 8 // F 22 | ]; 23 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | hasGameBoyPriority: false, // Give priority to GameBoy mode 3 | colorizeGBMode: true, // Colorize GB mode? 4 | runInterval: 8, // Interval for the emulator loop. 5 | minAudioBufferSpanAmountOverXInterpreterIterations: 10, // Audio buffer minimum span amount over x interpreter iterations. 6 | maxAudioBufferSpanAmountOverXInterpreterIterations: 20, // Audio buffer maximum span amount over x interpreter iterations. 7 | enabledAudioChannels: [true, true, true, true] 8 | }; 9 | -------------------------------------------------------------------------------- /src/storages/LocalStorage.ts: -------------------------------------------------------------------------------- 1 | import Storage from "./Storage"; 2 | 3 | export default class LocalStorage extends Storage { 4 | get(name: string) { 5 | const data = window.localStorage.getItem(name); 6 | return base64ToArrayBuffer(data); 7 | } 8 | 9 | set(name: string, buffer) { 10 | const data = arrayBufferToBase64(buffer); 11 | window.localStorage.setItem(name, data); 12 | } 13 | } 14 | 15 | function base64ToArrayBuffer(data) { 16 | if (!data || data.length <= 0) return null; 17 | 18 | data = atob(data); 19 | const array = new Uint8Array(data.length); 20 | for (let i = 0; i < data.length; i++) { 21 | array[i] = data.charCodeAt(i); 22 | } 23 | return array.buffer; 24 | } 25 | 26 | function arrayBufferToBase64(array) { 27 | if (!array || array.length <= 0) return null; 28 | 29 | array = new Uint8Array(array); 30 | let data = ""; 31 | for (let i = 0; i < array.byteLength; i++) { 32 | data += String.fromCharCode(array[i]); 33 | } 34 | 35 | return btoa(data); 36 | } -------------------------------------------------------------------------------- /src/storages/MemoryStorage.ts: -------------------------------------------------------------------------------- 1 | import Storage from "./Storage"; 2 | 3 | export default class MemoryStorage extends Storage { 4 | memory = {}; 5 | 6 | get(name: string) { 7 | return this.memory[name]; 8 | } 9 | 10 | set(name: string, buffer) { 11 | this.memory[name] = buffer; 12 | } 13 | } -------------------------------------------------------------------------------- /src/storages/Storage.ts: -------------------------------------------------------------------------------- 1 | export default abstract class Storage { 2 | getState(name: string) { 3 | return this.get("state-" + name); 4 | } 5 | 6 | getRam(name: string) { 7 | return this.get("ram-" + name); 8 | } 9 | 10 | getRtc(name: string) { 11 | return this.get("rtc-" + name); 12 | } 13 | 14 | setState(name: string, buffer) { 15 | return this.set("state-" + name, buffer); 16 | } 17 | 18 | setRam(name: string, buffer) { 19 | return this.set("ram-" + name, buffer); 20 | } 21 | 22 | setRtc(name: string, buffer) { 23 | return this.set("rtc-" + name, buffer); 24 | } 25 | 26 | abstract get(key: string): Promise | ArrayBuffer; 27 | abstract set(key: string, buffer: ArrayBuffer): Promise | void; 28 | } -------------------------------------------------------------------------------- /src/tickTable.ts: -------------------------------------------------------------------------------- 1 | export default [ // Number of machine cycles for each instruction: 2 | /* 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F*/ 3 | 4, 12, 8, 8, 4, 4, 8, 4, 20, 8, 8, 8, 4, 4, 8, 4, //0 4 | 4, 12, 8, 8, 4, 4, 8, 4, 12, 8, 8, 8, 4, 4, 8, 4, //1 5 | 8, 12, 8, 8, 4, 4, 8, 4, 8, 8, 8, 8, 4, 4, 8, 4, //2 6 | 8, 12, 8, 8, 12, 12, 12, 4, 8, 8, 8, 8, 4, 4, 8, 4, //3 7 | 8 | 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, //4 9 | 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, //5 10 | 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, //6 11 | 8, 8, 8, 8, 8, 8, 4, 8, 4, 4, 4, 4, 4, 4, 8, 4, //7 12 | 13 | 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, //8 14 | 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, //9 15 | 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, //A 16 | 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4, 4, 8, 4, //B 17 | 18 | 8, 12, 12, 16, 12, 16, 8, 16, 8, 16, 12, 0, 12, 24, 8, 16, //C 19 | 8, 12, 12, 4, 12, 16, 8, 16, 8, 16, 12, 4, 12, 4, 8, 16, //D 20 | 12, 12, 8, 4, 4, 16, 8, 16, 16, 4, 16, 4, 4, 4, 8, 16, //E 21 | 12, 12, 8, 4, 4, 16, 8, 16, 12, 8, 16, 4, 0, 4, 8, 16 //F 22 | ]; 23 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import "./worklet"; -------------------------------------------------------------------------------- /src/types/worklet.ts: -------------------------------------------------------------------------------- 1 | interface AudioWorkletProcessor { 2 | readonly port: MessagePort; 3 | process( 4 | inputs: Float32Array[][], 5 | outputs: Float32Array[][], 6 | parameters: Record 7 | ): boolean; 8 | } 9 | 10 | declare var AudioWorkletProcessor: { 11 | prototype: AudioWorkletProcessor; 12 | new(options?: AudioWorkletNodeOptions): AudioWorkletProcessor; 13 | }; 14 | 15 | declare function registerProcessor( 16 | name: string, 17 | processorCtor: ( 18 | new ( 19 | options?: AudioWorkletNodeOptions 20 | ) => AudioWorkletProcessor 21 | ) 22 | ): undefined; -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import JSZip from "jszip"; 2 | import * as FileSaver from "file-saver"; 3 | 4 | export function toTypedArray(baseArray, memtype) { // TODO: remove 5 | try { 6 | if (!baseArray || !(baseArray.length < 1)) return []; 7 | 8 | var length = baseArray.length; 9 | 10 | let typedArrayTemp; 11 | switch (memtype) { 12 | case "uint8": 13 | typedArrayTemp = new Uint8Array(length); 14 | break; 15 | case "int8": 16 | typedArrayTemp = new Int8Array(length); 17 | break; 18 | case "int32": 19 | typedArrayTemp = new Int32Array(length); 20 | break; 21 | case "float32": 22 | typedArrayTemp = new Float32Array(length); 23 | } 24 | 25 | for (var index = 0; index < length; index++) { 26 | typedArrayTemp[index] = baseArray[index]; 27 | } 28 | 29 | return typedArrayTemp; 30 | } catch (error) { 31 | console.log("Could not convert an array to a typed array: " + error.message, 1); 32 | return baseArray; 33 | } 34 | } 35 | 36 | export function fromTypedArray(baseArray) { // TODO: remove 37 | try { 38 | if (!baseArray || !baseArray.length) { 39 | return []; 40 | } 41 | var arrayTemp = []; 42 | for (var index = 0; index < baseArray.length; ++index) { 43 | arrayTemp[index] = baseArray[index]; 44 | } 45 | return arrayTemp; 46 | } catch (error) { 47 | console.log("Conversion from a typed array failed: " + error.message, 1); 48 | return baseArray; 49 | } 50 | } 51 | 52 | export type TypedArray = Int8Array | Uint8Array | Int32Array | Float32Array; 53 | 54 | export function getTypedArray(length, defaultValue, numberType): TypedArray { // TODO: remove and use fillTypedArray 55 | let arrayHandle; 56 | switch (numberType) { 57 | case "int8": 58 | arrayHandle = new Int8Array(length); 59 | break; 60 | case "uint8": 61 | arrayHandle = new Uint8Array(length); 62 | break; 63 | case "int32": 64 | arrayHandle = new Int32Array(length); 65 | break; 66 | case "float32": 67 | arrayHandle = new Float32Array(length); 68 | break; 69 | default: 70 | break; 71 | } 72 | 73 | if (defaultValue !== 0) { 74 | let index = 0; 75 | while (index < length) { 76 | arrayHandle[index++] = defaultValue; 77 | } 78 | } 79 | 80 | return arrayHandle; 81 | } 82 | 83 | export function stringToArrayBuffer(data) { 84 | const array = new Uint8Array(data.length); 85 | for (let i = 0, strLen = data.length; i < strLen; i++) { 86 | array[i] = data.charCodeAt(i); 87 | } 88 | 89 | return array; 90 | } 91 | 92 | export async function fetchFileAsArrayBuffer(url) { 93 | const res = await fetch(url); 94 | return await res.arrayBuffer(); // Chrome, Opera, Firefox and Edge support only! 95 | } 96 | 97 | export function concatArrayBuffers(...buffers) { 98 | let totalLength = 0; 99 | for (let i = 0; i < buffers.length; i++) { 100 | totalLength += buffers[i].byteLength; 101 | } 102 | 103 | const array = new Uint8Array(totalLength); 104 | 105 | for (let i = 0; i < buffers.length; i++) { 106 | const typedArray = new Uint8Array(buffers[i]); 107 | if (i === 0) { 108 | array.set(typedArray); 109 | } else { 110 | array.set(typedArray, buffers[i - 1].byteLength); 111 | } 112 | } 113 | 114 | return array.buffer; 115 | } 116 | 117 | export function saveAs(file: Blob | ArrayBuffer | Uint8Array, filename?: string) { 118 | if (file instanceof ArrayBuffer) { 119 | file = new Uint8Array(file); 120 | } 121 | 122 | if (file instanceof Uint8Array) { 123 | file = new Blob([file], { type: "application/octet-binary" }); 124 | } 125 | 126 | FileSaver.saveAs(file as Blob, filename); 127 | } 128 | 129 | export type Debounced = (() => any) & { clear?(), flush?() }; 130 | 131 | export async function readBlob(file: Blob): Promise { 132 | return new Promise((resolve, reject) => { 133 | if (file) { 134 | const binaryHandle = new FileReader(); 135 | binaryHandle.addEventListener("load", function () { 136 | if (this.readyState === 2) { 137 | resolve(this.result as ArrayBuffer); 138 | } 139 | }); 140 | binaryHandle.readAsArrayBuffer(file); 141 | } else { 142 | reject(); 143 | } 144 | }); 145 | } 146 | 147 | export async function readFirstMatchingExtension( 148 | blob: Blob, 149 | filename: string = "", 150 | extensions: string[] 151 | ): Promise { 152 | let buffer = await readBlob(blob); 153 | 154 | if ( 155 | !extensions.includes("zip") && 156 | hasExtension(filename, "zip") 157 | ) { 158 | const decodedZip = await JSZip.loadAsync(buffer); 159 | const filenames = Object.keys(decodedZip.files); 160 | const validFilenames = filenames.filter(x => extensions.find(extension => hasExtension(x, extension))); 161 | if (validFilenames.length > 0) { 162 | buffer = await decodedZip.file(validFilenames[0]).async("arraybuffer"); 163 | } else { 164 | buffer = undefined; 165 | } 166 | } 167 | 168 | return buffer; 169 | } 170 | 171 | export function hasExtension(filename: string, extension: string): boolean { 172 | filename = filename.toLowerCase(); 173 | extension = "." + extension.toLowerCase(); 174 | return filename.lastIndexOf(extension) === filename.length - extension.length; 175 | } 176 | 177 | export function debounce(func, wait: number): Debounced { 178 | var timeout, args, context, timestamp, result; 179 | 180 | function later() { 181 | var last = Date.now() - timestamp; 182 | 183 | if (last < wait && last >= 0) { 184 | timeout = setTimeout(later, wait - last); 185 | } else { 186 | timeout = null; 187 | result = func.apply(context, args); 188 | context = args = null; 189 | } 190 | }; 191 | 192 | var debounced: Debounced = function () { 193 | context = this; 194 | args = arguments; 195 | timestamp = Date.now(); 196 | if (!timeout) timeout = setTimeout(later, wait); 197 | 198 | return result; 199 | }; 200 | 201 | debounced.clear = function () { 202 | if (timeout) { 203 | clearTimeout(timeout); 204 | timeout = null; 205 | } 206 | }; 207 | 208 | debounced.flush = function () { 209 | if (timeout) { 210 | result = func.apply(context, args); 211 | context = args = null; 212 | 213 | clearTimeout(timeout); 214 | timeout = null; 215 | } 216 | }; 217 | 218 | return debounced; 219 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "./dist/types", 5 | "noImplicitAny": false, 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "module": "ES2020", 9 | "target": "ES2021" 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const WorkerUrlPlugin = require("worker-url/plugin"); 4 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 5 | 6 | module.exports = { 7 | entry: "./src/index.ts", 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.tsx?$/, 12 | use: "ts-loader", 13 | exclude: /node_modules/ 14 | } 15 | ] 16 | }, 17 | resolve: { 18 | extensions: [".ts", ".js"] 19 | }, 20 | output: { 21 | path: path.resolve(__dirname, "dist"), 22 | globalObject: "typeof self !== 'undefined' ? self : this", 23 | library: "jsgbc-core", 24 | libraryTarget: "umd", 25 | umdNamedDefine: true, 26 | publicPath: "/", 27 | filename: (pathData) => { 28 | return pathData.chunk.name === "main" ? 29 | "jsgbc-core.js" : 30 | pathData.chunk.name + ".js"; 31 | } 32 | }, 33 | plugins: [ 34 | new CleanWebpackPlugin(), 35 | new WorkerUrlPlugin(), 36 | new webpack.DefinePlugin({ 37 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV) 38 | }) 39 | ] 40 | }; -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common"); 3 | 4 | module.exports = merge(common, { 5 | mode: "development", 6 | entry: "./demo/index.ts", 7 | devtool: "inline-source-map", 8 | devServer: { 9 | static: "./" 10 | } 11 | }); -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common"); 3 | const nodeExternals = require("webpack-node-externals"); 4 | 5 | module.exports = merge(common, { 6 | mode: "production", 7 | externals: [nodeExternals()] 8 | }); --------------------------------------------------------------------------------