├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── .npmignore ├── AUTHORS.md ├── LICENSE ├── README.md ├── example ├── InterglacticTransmissing.nes ├── README.md ├── nes-embed.html └── nes-embed.js ├── package.json ├── roms ├── croom │ ├── README.html │ └── croom.nes └── lj65 │ ├── README.txt │ └── lj65.nes ├── src ├── controller.js ├── cpu.js ├── index.js ├── mappers.js ├── nes.js ├── papu.js ├── ppu.js ├── rom.js ├── tile.js └── utils.js ├── test └── nes.spec.js ├── webpack.config.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "commonjs": true 7 | }, 8 | "extends": ["eslint:recommended", "prettier"], 9 | "rules": { 10 | "eqeqeq": ["error", "always"], 11 | "no-alert": "error" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: webpack 10 | versions: 11 | - "< 5" 12 | - ">= 4.0.a" 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: '12.x' 16 | - run: yarn 17 | - run: yarn build 18 | - run: yarn test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ._* 2 | .DS_Store 3 | /dist 4 | /local-roms 5 | /node_modules 6 | /tmp 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /local-roms 2 | /tmp 3 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | * Ben Firshman 5 | 6 | Thanks to: 7 | 8 | * Jamie Sanders for vNES, the Java emulator that JSNES owes so much to. 9 | * Matt Westcott for JSSpeccy, the original inspiration for JSNES. 10 | * Connor Dunn for a patch that dramatically increased performance on Chrome. 11 | * Jens Lindstrom for some optimisations. 12 | * Rafal Chlodnicki for an Opera fix. 13 | * Ecin Krispie for fixing player 2 controls. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2020 Ben Firshman 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSNES 2 | 3 | A JavaScript NES emulator. 4 | 5 | It's a library that works in both the browser and Node.js. The browser UI is available at [https://github.com/bfirsh/jsnes-web](https://github.com/bfirsh/jsnes-web). 6 | 7 | ## Installation 8 | 9 | For Node.js or Webpack: 10 | 11 | $ npm install jsnes 12 | 13 | (Or `yarn add jsnes`.) 14 | 15 | In the browser, you can use [unpkg](https://unpkg.com): 16 | 17 | ```html 18 | <script type="text/javascript" src="https://unpkg.com/jsnes/dist/jsnes.min.js"></script> 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```javascript 24 | // Initialize and set up outputs 25 | var nes = new jsnes.NES({ 26 | onFrame: function(frameBuffer) { 27 | // ... write frameBuffer to screen 28 | }, 29 | onAudioSample: function(left, right) { 30 | // ... play audio sample 31 | } 32 | }); 33 | 34 | // Read ROM data from disk (using Node.js APIs, for the sake of this example) 35 | const fs = require('fs'); 36 | var romData = fs.readFileSync('path/to/rom.nes', {encoding: 'binary'}); 37 | 38 | // Load ROM data as a string or byte array 39 | nes.loadROM(romData); 40 | 41 | // Run frames at 60 fps, or as fast as you can. 42 | // You are responsible for reliable timing as best you can on your platform. 43 | nes.frame(); 44 | nes.frame(); 45 | // ... 46 | 47 | // Hook up whatever input device you have to the controller. 48 | nes.buttonDown(1, jsnes.Controller.BUTTON_A); 49 | nes.frame(); 50 | nes.buttonUp(1, jsnes.Controller.BUTTON_A); 51 | nes.frame(); 52 | // ... 53 | ``` 54 | 55 | ## Build 56 | 57 | To build a distribution: 58 | 59 | $ yarn run build 60 | 61 | This will create `dist/jsnes.min.js`. 62 | 63 | ## Running tests 64 | 65 | $ yarn test 66 | 67 | ## Embedding JSNES in a web page 68 | 69 | You can use JSNES to embed a playable version of a ROM in a web page. This is handy if you are a homebrew ROM developer and want to put a playable version of your ROM on its web page. 70 | 71 | The best implementation is [jsnes-web](https://github.com/bfirsh/jsnes-web) but unfortunately it is not trivial to reuse the code. You'll have to copy and paste the code from that repository, the use the [`<Emulator>`](https://github.com/bfirsh/jsnes-web/blob/master/src/Emulator.js) React component. [Here is a usage example.](https://github.com/bfirsh/jsnes-web/blob/d3c35eec11986412626cbd08668dbac700e08751/src/RunPage.js#L119-L125). 72 | 73 | A project for potential contributors (hello!): jsnes-web should be reusable and on NPM! It just needs compiling and bundling. 74 | 75 | A more basic example is in the `example/` directory of this repository. Unfortunately this is known to be flawed, and doesn't do timing and sound as well as jsnes-web. 76 | 77 | ## Formatting code 78 | 79 | All code must conform to [Prettier](https://prettier.io/) formatting. The test suite won't pass unless it does. 80 | 81 | To automatically format all your code, run: 82 | 83 | $ yarn run format 84 | 85 | ## Maintainers 86 | 87 | - [Ben Firshman](http://github.com/bfirsh) 88 | - [Ben Jones](https://github.com/BenShelton) 89 | - [Stephen Hicks](https://github.com/shicks) 90 | - [Alison Saia](https://github.com/allie) 91 | 92 | JSNES is based on [James Sanders' vNES](https://github.com/bfirsh/vNES), and owes an awful lot to it. It also wouldn't have happened without [Matt Wescott's JSSpeccy](http://jsspeccy.zxdemo.org/), which sparked the original idea. (Ben, circa 2008: "Hmm, I wonder what else could run in a browser?!") 93 | -------------------------------------------------------------------------------- /example/InterglacticTransmissing.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfirsh/jsnes/d8021d0336cb5c1cf924cd660ecf816bec15c11a/example/InterglacticTransmissing.nes -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | An example app to demonstrate a simple way to embed JSNES. 2 | 3 | ROM is by @slembcke: https://github.com/slembcke/InterglacticTransmissing 4 | -------------------------------------------------------------------------------- /example/nes-embed.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | 3 | <html> 4 | <head> 5 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 6 | <title>Embedding Example</title> 7 | 8 | <script type="text/javascript" src="https://unpkg.com/jsnes/dist/jsnes.min.js"></script> 9 | <script type="text/javascript" src="nes-embed.js"></script> 10 | <script>window.onload = function(){nes_load_url("nes-canvas", "InterglacticTransmissing.nes");}</script> 11 | </head> 12 | <body> 13 | <div style="margin: auto; width: 75%;"> 14 | <canvas id="nes-canvas" width="256" height="240" style="width: 100%"/> 15 | </div> 16 | <p>DPad: Arrow keys<br/>Start: Return, Select: Tab<br/>A Button: A, B Button: S</p> 17 | </body> 18 | </html> 19 | -------------------------------------------------------------------------------- /example/nes-embed.js: -------------------------------------------------------------------------------- 1 | var SCREEN_WIDTH = 256; 2 | var SCREEN_HEIGHT = 240; 3 | var FRAMEBUFFER_SIZE = SCREEN_WIDTH*SCREEN_HEIGHT; 4 | 5 | var canvas_ctx, image; 6 | var framebuffer_u8, framebuffer_u32; 7 | 8 | var AUDIO_BUFFERING = 512; 9 | var SAMPLE_COUNT = 4*1024; 10 | var SAMPLE_MASK = SAMPLE_COUNT - 1; 11 | var audio_samples_L = new Float32Array(SAMPLE_COUNT); 12 | var audio_samples_R = new Float32Array(SAMPLE_COUNT); 13 | var audio_write_cursor = 0, audio_read_cursor = 0; 14 | 15 | var nes = new jsnes.NES({ 16 | onFrame: function(framebuffer_24){ 17 | for(var i = 0; i < FRAMEBUFFER_SIZE; i++) framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i]; 18 | }, 19 | onAudioSample: function(l, r){ 20 | audio_samples_L[audio_write_cursor] = l; 21 | audio_samples_R[audio_write_cursor] = r; 22 | audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK; 23 | }, 24 | }); 25 | 26 | function onAnimationFrame(){ 27 | window.requestAnimationFrame(onAnimationFrame); 28 | 29 | image.data.set(framebuffer_u8); 30 | canvas_ctx.putImageData(image, 0, 0); 31 | } 32 | 33 | function audio_remain(){ 34 | return (audio_write_cursor - audio_read_cursor) & SAMPLE_MASK; 35 | } 36 | 37 | function audio_callback(event){ 38 | var dst = event.outputBuffer; 39 | var len = dst.length; 40 | 41 | // Attempt to avoid buffer underruns. 42 | if(audio_remain() < AUDIO_BUFFERING) nes.frame(); 43 | 44 | var dst_l = dst.getChannelData(0); 45 | var dst_r = dst.getChannelData(1); 46 | for(var i = 0; i < len; i++){ 47 | var src_idx = (audio_read_cursor + i) & SAMPLE_MASK; 48 | dst_l[i] = audio_samples_L[src_idx]; 49 | dst_r[i] = audio_samples_R[src_idx]; 50 | } 51 | 52 | audio_read_cursor = (audio_read_cursor + len) & SAMPLE_MASK; 53 | } 54 | 55 | function keyboard(callback, event){ 56 | var player = 1; 57 | switch(event.keyCode){ 58 | case 38: // UP 59 | callback(player, jsnes.Controller.BUTTON_UP); break; 60 | case 40: // Down 61 | callback(player, jsnes.Controller.BUTTON_DOWN); break; 62 | case 37: // Left 63 | callback(player, jsnes.Controller.BUTTON_LEFT); break; 64 | case 39: // Right 65 | callback(player, jsnes.Controller.BUTTON_RIGHT); break; 66 | case 65: // 'a' - qwerty, dvorak 67 | case 81: // 'q' - azerty 68 | callback(player, jsnes.Controller.BUTTON_A); break; 69 | case 83: // 's' - qwerty, azerty 70 | case 79: // 'o' - dvorak 71 | callback(player, jsnes.Controller.BUTTON_B); break; 72 | case 9: // Tab 73 | callback(player, jsnes.Controller.BUTTON_SELECT); break; 74 | case 13: // Return 75 | callback(player, jsnes.Controller.BUTTON_START); break; 76 | default: break; 77 | } 78 | } 79 | 80 | function nes_init(canvas_id){ 81 | var canvas = document.getElementById(canvas_id); 82 | canvas_ctx = canvas.getContext("2d"); 83 | image = canvas_ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); 84 | 85 | canvas_ctx.fillStyle = "black"; 86 | canvas_ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); 87 | 88 | // Allocate framebuffer array. 89 | var buffer = new ArrayBuffer(image.data.length); 90 | framebuffer_u8 = new Uint8ClampedArray(buffer); 91 | framebuffer_u32 = new Uint32Array(buffer); 92 | 93 | // Setup audio. 94 | var audio_ctx = new window.AudioContext(); 95 | var script_processor = audio_ctx.createScriptProcessor(AUDIO_BUFFERING, 0, 2); 96 | script_processor.onaudioprocess = audio_callback; 97 | script_processor.connect(audio_ctx.destination); 98 | } 99 | 100 | function nes_boot(rom_data){ 101 | nes.loadROM(rom_data); 102 | window.requestAnimationFrame(onAnimationFrame); 103 | } 104 | 105 | function nes_load_data(canvas_id, rom_data){ 106 | nes_init(canvas_id); 107 | nes_boot(rom_data); 108 | } 109 | 110 | function nes_load_url(canvas_id, path){ 111 | nes_init(canvas_id); 112 | 113 | var req = new XMLHttpRequest(); 114 | req.open("GET", path); 115 | req.overrideMimeType("text/plain; charset=x-user-defined"); 116 | req.onerror = () => console.log(`Error loading ${path}: ${req.statusText}`); 117 | 118 | req.onload = function() { 119 | if (this.status === 200) { 120 | nes_boot(this.responseText); 121 | } else if (this.status === 0) { 122 | // Aborted, so ignore error 123 | } else { 124 | req.onerror(); 125 | } 126 | }; 127 | 128 | req.send(); 129 | } 130 | 131 | document.addEventListener('keydown', (event) => {keyboard(nes.buttonDown, event)}); 132 | document.addEventListener('keyup', (event) => {keyboard(nes.buttonUp, event)}); 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsnes", 3 | "version": "1.2.1", 4 | "description": "A JavaScript NES emulator", 5 | "homepage": "https://github.com/bfirsh/jsnes", 6 | "author": "Ben Firshman <ben@firshman.co.uk> (https://fir.sh)", 7 | "main": "src/index.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/bfirsh/jsnes.git" 11 | }, 12 | "license": "Apache-2.0", 13 | "scripts": { 14 | "build": "webpack", 15 | "test": "prettier-check src/**/*.js && mocha ./test/*.spec.js", 16 | "test:watch": "mocha -w ./test/*.spec.js", 17 | "prepublish": "npm run build", 18 | "format": "prettier --write src/**/*.js" 19 | }, 20 | "devDependencies": { 21 | "chai": "^4.1.2", 22 | "eslint": "^6.8.0", 23 | "eslint-config-prettier": "^6.10.1", 24 | "eslint-loader": "^2.0.0", 25 | "mocha": "^9.1.1", 26 | "prettier": "^2.0.5", 27 | "prettier-check": "^2.0.0", 28 | "sinon": "^9.0.1", 29 | "uglifyjs-webpack-plugin": "^1.3.0", 30 | "webpack": "^3.9.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /roms/croom/README.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE HTML><html><head> 2 | <title>Concentration Room</title> 3 | <link rel="stylesheet" type="text/css" href="docs/croom.css"> 4 | </head><body> 5 | <div id="pgheader"><div class="thereisnosuchthingaspaddingauto"> 6 | <h1><img alt="Concentration Room" width="320" height="128" src="docs/croomlogo320.png"></h1> 7 | <ul id="headerlist"> 8 | <li><strong>About</strong> 9 | <ul> 10 | <li><a href="#overview">Overview</a> 11 | <li><a href="#requirements">Requirements</a> 12 | <li><a href="#modes">Modes</a> 13 | <li><a href="#faq">FAQ</a> 14 | </ul> 15 | <li><a href="http://pineight.com/croom/dl">Download</a> 16 | </ul> 17 | 18 | </div></div><div id="pgbody"><div class="thereisnosuchthingaspaddingauto"> 19 | 20 | <h2><a name="overview">Overview</a></h2> 21 | <img src="docs/croom_screenshot01.png" style="float:right; margin: 0 0 1em 1em"> 22 | <p> 23 | An accident at the biochemical lab has released a neurotoxin, 24 | and you've been quarantined after exposure. Maintain your 25 | sanity by playing a card-matching game. 26 | </p><p> 27 | The table is littered with 10, 20, 36, 52, or 72 face-down cards. 28 | Flip two cards, and if they show the same emblem, you keep them. 29 | If they don't, flip them back. 30 | </p> 31 | 32 | <h2><a name="requirements">System Requirements</a></h2> 33 | <p> 34 | Concentration Room is designed for your Nintendo Entertainment System. This version is an NROM-128 (16 KiB PRG, 8 KiB CHR), and it has been tested on a <a title="CompactFlash to NES adapter" href="http://www.retrousb.com/index.php?cPath=24">PowerPak</a>. It also works in PC-based emulators such as <a href="http://nestopia.sourceforge.net/">Nestopia</a> and <a href="http://fceux.com/web/home.html">FCE Ultra</a>. 35 | </p> 36 | 37 | <h2><a name="modes">Modes</a></h2> 38 | <dl> 39 | <dt>1 Player Story<dd> 40 | Play solitaire to start to work the toxin out of your system. Then defeat other contaminated technicians and children one on one. 41 | <dt>1 Player Solitaire<dd> 42 | Select a difficulty level, then try to clear the table without having to turn back more than 99 non-matching pairs. 43 | <dt>2 Players<dd> 44 | Two players take turns turning over cards. They can pass one controller back and forth or use one controller each. If a pair doesn't match, the other player presses the A and B Buttons and takes a turn. The first player to take half the pairs wins. 45 | <dt>Vs. CPU<dd> 46 | Like 2 Players, except the second player is controlled by the NES. 47 | </dl> 48 | 49 | <h2><a name="faq">FAQ (Fully Anticipated Questions)</a></h2> 50 | <dl> 51 | <dt>How long have you been working on this?<dd> 52 | This is actually my third try. The logo and the earliest background sketch date back to 2000. It got held up because I lacked artistic skill on the 16x16 pixel canvas. The second try in 2007 finalized the appearance of the game, and I did some work on the "emblem designer" that will show up in a future release. In late November 2009, I discovered <a title="Review of Dian Shi Mali on waluigious.com" href="http://www.waluigious.com/2008/09/in-which-dian-shi-ma-li.html"><i>Dian Shi Mali</i></a>, a <a title="Dian Shi Mali article on Wikipedia" href="http://en.wikipedia.org/wiki/Dian_Shi_Mali">gambling simulator</a> for the Famicom (Asian version of the NES) that also uses 16x16 pixel emblems. After a few hours of <a title="Video of Dian Shi Mali play" href="http://www.youtube.com/watch?v=4s1mAPISOzw">pushing Start to rich</a>, I was inspired to create a set of 36 emblems. By then, I was ready to code most of the game in spare time during December 2009. 53 | <dt>Why are you still making games that don't scroll? You're better than that, as I saw in the <a title="Video of a homebrew sidescroller engine" href="http://www.youtube.com/watch?v=GY693NxC9xU">President video</a>.<dd> 54 | I saw it as something simple that I could finish fairly quickly in order to push falling block games off <a href="http://www.pineight.com/">the front page of my web site</a>. 55 | <dt>GameTek already made two other Concentration games on the NES. Why did you make this one?<dd> 56 | The controls in <i>I Can Remember</i> nor <i>Classic Concentration</i> are clunky. Neither of them features a full 72-card deck. And of course, they're not <a title="Free Software Definition (free speech, not free beer)" href="http://www.gnu.org/philosophy/free-sw.html">free software</a>. 57 | <dt>In vs. modes, why end the game at half the cards matched instead of one more than half?<dd> 58 | Pairs early in a game require more skill to clear, and the last pair requires absolutely no skill. For example, a 20-card game tied at 4-4 will always end up 6-4. And at 5-3, the player in the lead likely got more early matches. So if we award no points for the last pair, the first player to reach half always wins. 59 | <dt>What's that font?<dd> 60 | The font in the game's logo is called <a href="http://www.windowfonts.com/fonts/wasted-collection.html">Wasted Collection</a>. The font in <a title="Launcher for small programs for Game Boy Advance" href="http://www.pineight.com/gba/#mbmenu">Multiboot Menu</a> was based on it. The monospace font for menu text originally appeared in the "Who's Cuter" demo and is based on <a href="http://en.wikipedia.org/wiki/Chicago_%28typeface%29">Apple Chicago by Susan Kare</a>. (Another fun font is on <a href="http://www.angelfire.com/stars5/tkcpics2/wildworld/#downloads">this page</a>.) 61 | <dt>Are you a Nazi?<dd> 62 | No, and that's why this game is called Concentration <em>Room,</em> not <a title="National Lampoon video" href="http://www.youtube.com/watch?v=cXeHn9k27Iw">Concentration Camp</a>. 63 | </dl> 64 | <h2>Legal</h2> 65 | <p> 66 | Copyright © 2010 Damian Yerrick <croom@pineight.com> 67 | </p><p> 68 | Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. 69 | </p><p> 70 | The accompanying program is free software: you can redistribute it and/or modify it under the terms of the <a href="http://www.gnu.org/copyleft/gpl.html">GNU General Public License</a>, version 3 or later. As a special exception, you may copy and distribute exact copies of the program, as published by Damian Yerrick, in iNES or UNIF executable form without source code. 71 | </p><p> 72 | This product is not sponsored or endorsed by Nintendo, Ravensburger, Hasbro, Mattel, Quaker Oats, NBC Universal, GameTek, or Apple. 73 | </p> 74 | </div></div> 75 | </body></html> 76 | -------------------------------------------------------------------------------- /roms/croom/croom.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfirsh/jsnes/d8021d0336cb5c1cf924cd660ecf816bec15c11a/roms/croom/croom.nes -------------------------------------------------------------------------------- /roms/lj65/README.txt: -------------------------------------------------------------------------------- 1 | _ _ __ ___ 2 | | | (_) / / / __| 3 | | | _ / /_ | /__ 4 | | | | | | _ \ |___ \ 5 | | |_ | | | (_) | .___) | 6 | \__|_| | \___/ \___/ 7 | |__/ 8 | 9 | LJ65 10 | an NES game 11 | by Damian Yerrick 12 | 13 | See the legal section below. 14 | 15 | _____________________________________________________________________ 16 | Introduction 17 | 18 | LJ65 is an action puzzle game for NES comparable to the popular 19 | game Tetris(R), except distributed as free software and with more 20 | responsive movement controls. 21 | 22 | _____________________________________________________________________ 23 | Installing 24 | 25 | LJ65 is designed to run on Nintendo Entertainment System (called 26 | Family Computer in Japan) and accurate NES emulators. It is 27 | distributed as source code and an iNES format binary, using mapper 28 | 0 (NROM). Separate binaries for NTSC and PAL systems are provided. 29 | 30 | This program has been tested on NES using a PowerPak. It also works 31 | on the current versions of Nintendulator, Nestopia, and FCE Ultra. 32 | (Do not use the outdated Nesticle emulator anymore.) 33 | 34 | To run LJ65 on an NES without buying a PowerPak, you'll need to 35 | solder together an NES cartridge with at least 16 KB of PRG space 36 | and 4 KB of CHR space. A modded NROM-128 or CNROM board should be 37 | fine. Chris Covell has put together instructions on how to replace 38 | NES Game Paks' mask ROM chips with writable EEPROMs. 39 | http://www.zyx.com/chrisc/solarwarscart.html 40 | 41 | To build LJ65 from source code, you will need 42 | * CC65 (from http://www.cc65.org/ but you don't need the 43 | non-free C compiler) 44 | * GNU Make and Coreutils (included with most Linux distributions; 45 | Windows users can use MSYS from http://www.devkitpro.org/) 46 | 47 | Modify the makefile to point to where you have CC65 installed. 48 | Then run make. (Windows users can run mk.bat instead, which runs 49 | make in the correct folder.) On a desktop PC from late 2000 with 50 | a Pentium III 866 MHz, recompiling the whole thing takes about one 51 | second. To build some data conversion tools, you'll need a GNU C 52 | compiler such as MinGW; I have included Windows binaries of the 53 | conversion tools for those who want to quickly get into hacking 54 | on LJ65. 55 | 56 | _____________________________________________________________________ 57 | Game controls 58 | 59 | Title screen: 60 | Start: Show playfields. 61 | Game over: 62 | A+B: Join game. 63 | Menu: 64 | Control Pad up, down: Move cursor. 65 | Control Pad left, right: Change option at cursor. 66 | A: Start game. 67 | Game: 68 | Control Pad left, right, down: Move piece. 69 | Control Pad up: Move piece to floor. 70 | Control Pad up, down once landed: Lock piece into place. 71 | A: Rotate piece clockwise. 72 | B: Rotate piece anticlockwise. 73 | Start: Pause game. 74 | 75 | _____________________________________________________________________ 76 | Play 77 | 78 | At first, press Start to skip past each of the informational screens. 79 | Then press Start at the title screen to display the playfields. 80 | At this point, either player can press the A and B buttons at the 81 | same time to begin playing. 82 | 83 | The pieces in LJ65 are called tetrominoes. (The word comes from 84 | tetra-, a Greek prefix meaning four, and -omino, as in domino or 85 | pentomino.) Each of the seven tetrominoes is made of four square 86 | blocks and named after a letter of the Latin alphabet that it 87 | resembles: 88 | _ _ ___ ___ _ ___ 89 | _______ | |___ ___| | | | _| _| _| |_ |_ |_ 90 | |_______| |_____| |_____| |___| |___| |_____| |___| 91 | I J L O S T Z 92 | 93 | When you start the game, a tetromino will begin to fall slowly into 94 | the bin. You can move it with the Control Pad and rotate it with 95 | the A or B button. 96 | 97 | The goal of LJ65 is to make complete horizontal lines by 98 | packing the pieces into the bin with no holes. If you complete 99 | a line, everything above it will move down a row. If you complete 100 | more than one line with a piece, you get more points. 101 | 102 | As you play, the pieces will gradually fall faster, making the game 103 | more difficult. At some point, the pieces will fall so fast that 104 | they appear immediately at the bottom row of the playfield. If you 105 | fill the bin to the top, to the point where more pieces cannot enter, 106 | you "top out" and the game ends. 107 | 108 | If you have an overhang in the blocks, you can slide another 109 | piece under it by holding Left or Right as the new piece passes 110 | by the overhang: 111 | _ 112 | | | 113 | _| | 114 | |___| 115 | _ _ _ _ _ 116 | _| | => _| | | | => _| | | 117 | | _| | _|_| | | _| | 118 | |_| |_| |___| |_|___| 119 | 120 | Or in some cases, you can rotate pieces into very tight spaces: 121 | _ 122 | _| | 123 | |_ | 124 | |_| 125 | _ ___ _ _ ___ _ ___ 126 | | | |_ | => | |_| |_ | => | |___|_ | 127 | | |_ _| | | |_ |_| | | |_ _| | 128 | |___| |___| |___|_|___| |___|_|___| 129 | 130 | _____________________________________________________________________ 131 | Rotation systems 132 | 133 | LJ65 supports two rotation systems, which it calls "Center" and 134 | "Bottom". Center implements rules more familiar to Western players, 135 | while Bottom pleases fans of the Japanese arcade tradition. 136 | 137 | In Center, pieces start out with their flat side down, and they 138 | rotate around the center of an imaginary 3x3 or 4x4 cell bounding 139 | box. If this is blocked, try one square to the right, one square to 140 | the left, and finally one square up. 141 | Up locks a piece into place immediately, and down waits for another 142 | press of up or down before locking the piece. 143 | After a piece locks, the next one comes out immediately, but after 144 | the pieces have sped up enough, the next piece waits a bit. 145 | Colors match the so-called Guideline: I is turquoise. 146 | 147 | . []. . []. . . . . []. . [][] . []. . . . []. . 148 | [][][] . [][] [][][] [][]. [][]. . [][] . [][] [][]. 149 | . . . . []. . []. . []. . . . . . [] [][]. . []. 150 | Figure: T and S rotation in Center 151 | 152 | In Bottom, the J, L, S, T, and Z pieces start out with their flat 153 | side up, and they rotate to stay in contact with the bottom of an 154 | imaginary 3x3 cell box. S and Z pieces also keep a block in the 155 | bottom center of this box. If this is blocked by a wall or a block 156 | outside the piece's central column, then try one square to the right, 157 | one square to the left, and finally (in the case of T) one square up. 158 | Down locks on contact, and up waits for another press of up or down 159 | to lock. After a piece locks, the next one waits a bit to come out. 160 | Colors match those from a game with a monkey: I is red. 161 | 162 | . . . . []. . . . . []. . . . []. . . . . []. . 163 | [][][] [][]. . []. . [][] . [][] [][]. . [][] [][]. 164 | . []. . []. [][][] . []. [][]. . []. [][]. . []. 165 | Figure: T and S rotation in Bottom 166 | 167 | _____________________________________________________________________ 168 | Scoring 169 | 170 | Use up or down on the Control Pad to drop pieces, and you'll get 171 | one point per row that the piece moves down. 172 | 173 | You also get points for clearing lines. Clearing more lines 174 | with a single piece is worth more points: 175 | 176 | SINGLE (1 line with any piece) 1 * 1 * 100 = 100 points 177 | DOUBLE (2 lines with any piece) 2 * 2 * 100 = 400 points 178 | TRIPLE (3 lines with I, J, or L) 3 * 3 * 100 = 900 points 179 | HOME RUN (4 lines with I only) 4 * 4 * 100 = 1600 points 180 | 181 | Making lines with consecutive pieces is called a combo and is 182 | worth even more points. In general, the score for a line clear 183 | is the number of lines cleared with this piece, times the number 184 | of lines cleared so far in this combo, times 100. For example, 185 | a double-triple-single combo is worth a total of 2300 points: 186 | 187 | 2 lines 2 * 2 * 100 = 200 points 188 | 3 lines 3 * 5 * 100 = 1500 points 189 | 1 line 1 * 6 * 100 = 600 points 190 | 191 | When you start clearing lines, the game shows how many lines you 192 | made in this combo. If you leave a 2-block-wide hole at the side 193 | of the bin, you might manage to make a combo of 12 lines or more. 194 | But then you have to weigh this against keeping your stack low 195 | and earning more drop bonus. 196 | 197 | There are some grandmasters who can get millions of points in 198 | some puzzle games. There exists a known corner case in this 199 | game's score computation, and scoring is expected to fail beyond 200 | 6,553,000 points. 201 | 202 | If two players are playing, and you have GARBAGE turned on in the 203 | menu, and you complete more than one line with a piece, the other 204 | player's field rises by one or more rows: 205 | 206 | DOUBLE: 1 line 207 | TRIPLE: 2 lines 208 | HOME RUN: 4 lines 209 | 210 | This is not affected by combos. 211 | 212 | _____________________________________________________________________ 213 | Keypress codes 214 | 215 | Some of the lesser-used features of the game are hidden so that 216 | players interested in the most common features don't become confused. 217 | 218 | At title screen: 219 | * B + Left hides the ghost piece. 220 | 221 | _____________________________________________________________________ 222 | Questions 223 | 224 | Q: Isn't this a copy of Tetris? 225 | 226 | Yes, in part, but we don't believe it infringes Tetris Holding's 227 | copyright. It was developed by people who had not read the source 228 | code of Tetris. We disagree with Tetris Holding's claim of broad 229 | patent-like rights over the game. Any similarity between LJ65 and 230 | Tetris is a consequence of common methods of operation, which are 231 | excluded from U.S. copyright (17 USC 102(b)). 232 | 233 | Q: Where's (feature that has appeared in another game)? 234 | 235 | If it's mentioned in the "future" list at the bottom of CHANGES.txt, 236 | I know about it, and you may see some of those issues resolved in 237 | the next version. Otherwise, I'd be glad to take suggestions, 238 | provided that they aren't "network play with no lag" or "make the 239 | game just like that Japanese game I saw on YouTube". 240 | 241 | Q: Why aren't the blocks square on my TV? 242 | 243 | In NTSC, a square pixel is 7/24 of a color subcarrier period wide 244 | in 480i mode or 7/12 of a period in the so-called "240p" mode. 245 | But like the video chipsets in most 8-bit and 16-bit computing 246 | platforms, the NES PPU generates pixels that are not square: 247 | 8/12 of a period instead of 7/12. Games for PC, Apple II, or any 248 | other platform with frame buffer video could correct for this by 249 | drawing differently sized tiles, but games for NES are limited to 250 | an 8x8 pixel tile grid. PAL video and widescreen televisions make 251 | the problem even more pronounced. 252 | 253 | Q: Why do some pieces change color subtly when they land? 254 | 255 | The NES's tile size is 8x8 pixels, but the "attribute table" 256 | assigns palettes to 16x16 pixel areas, or clusters of 2x2 tiles. 257 | Only three colors plus the backdrop color can appear in each 258 | color area. So the game approximates the color of each piece as a 259 | combination of blue, orange, and green throughout the screen. 260 | 261 | The MMC5 mapper has ExGrafix, which allows 8x8 pixel color areas. 262 | But the only source of MMC5 hardware is used copies of Castlevania 263 | III: Dracula's Curse and Koei's war sims, unlike the discrete mapper 264 | boards that retrousb.com sells. 265 | 266 | Q: Who is the fellow on How to Play, and where are his legs? 267 | 268 | Who are you, and where is your tail? ;-) 269 | 270 | _____________________________________________________________________ 271 | Credits 272 | 273 | Program and graphics by Damian Yerrick 274 | Original game design by Alexey Pajitnov 275 | NES assembler toolchain by Ullrich von Bassewitz 276 | NES emulators by Xodnizel, Martin Freij, and Quietust 277 | NES documentation by contributors to http://nesdevwiki.org/ 278 | 279 | Music: 280 | TEMP is "Tetris New Melody (OCRemoved)" by Am.Fm.GM 281 | K.231 is "Leck mich im Arsch" by Wolfgang A. Mozart 282 | 283 | _____________________________________________________________________ 284 | Legal 285 | 286 | Copyright (c) 2009 Damian Yerrick 287 | 288 | This manual is under the following license: 289 | 290 | This work is provided 'as-is', without any express or implied 291 | warranty. In no event will the authors be held liable for any 292 | damages arising from the use of this work. 293 | 294 | Permission is granted to anyone to use this work for any 295 | purpose, including commercial applications, and to alter it and 296 | redistribute it freely, subject to the following restrictions: 297 | 298 | 1. The origin of this work must not be misrepresented; you 299 | must not claim that you wrote the original work. If you use 300 | this work in a product, an acknowledgment in the product 301 | documentation would be appreciated but is not required. 302 | 2. Altered source versions must be plainly marked as such, 303 | and must not be misrepresented as being the original work. 304 | 3. This notice may not be removed or altered from any 305 | source distribution. 306 | 307 | The term "source" refers to the preferred form of a work for making 308 | changes to it. 309 | 310 | The LJ65 software described by this manual is distributed under 311 | the GNU General Public License, version 2 or later, with ABSOLUTELY 312 | NO WARRANTY. See GPL.txt for details. 313 | 314 | LJ65 is not a Tetris product and is not endorsed by Tetris Holding. 315 | -------------------------------------------------------------------------------- /roms/lj65/lj65.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfirsh/jsnes/d8021d0336cb5c1cf924cd660ecf816bec15c11a/roms/lj65/lj65.nes -------------------------------------------------------------------------------- /src/controller.js: -------------------------------------------------------------------------------- 1 | var Controller = function () { 2 | this.state = new Array(8); 3 | for (var i = 0; i < this.state.length; i++) { 4 | this.state[i] = 0x40; 5 | } 6 | }; 7 | 8 | Controller.BUTTON_A = 0; 9 | Controller.BUTTON_B = 1; 10 | Controller.BUTTON_SELECT = 2; 11 | Controller.BUTTON_START = 3; 12 | Controller.BUTTON_UP = 4; 13 | Controller.BUTTON_DOWN = 5; 14 | Controller.BUTTON_LEFT = 6; 15 | Controller.BUTTON_RIGHT = 7; 16 | 17 | Controller.prototype = { 18 | buttonDown: function (key) { 19 | this.state[key] = 0x41; 20 | }, 21 | 22 | buttonUp: function (key) { 23 | this.state[key] = 0x40; 24 | }, 25 | }; 26 | 27 | module.exports = Controller; 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Controller: require("./controller"), 3 | NES: require("./nes"), 4 | }; 5 | -------------------------------------------------------------------------------- /src/mappers.js: -------------------------------------------------------------------------------- 1 | var utils = require("./utils"); 2 | 3 | var Mappers = {}; 4 | 5 | Mappers[0] = function (nes) { 6 | this.nes = nes; 7 | }; 8 | 9 | Mappers[0].prototype = { 10 | reset: function () { 11 | this.joy1StrobeState = 0; 12 | this.joy2StrobeState = 0; 13 | this.joypadLastWrite = 0; 14 | 15 | this.zapperFired = false; 16 | this.zapperX = null; 17 | this.zapperY = null; 18 | }, 19 | 20 | write: function (address, value) { 21 | if (address < 0x2000) { 22 | // Mirroring of RAM: 23 | this.nes.cpu.mem[address & 0x7ff] = value; 24 | } else if (address > 0x4017) { 25 | this.nes.cpu.mem[address] = value; 26 | if (address >= 0x6000 && address < 0x8000) { 27 | // Write to persistent RAM 28 | this.nes.opts.onBatteryRamWrite(address, value); 29 | } 30 | } else if (address > 0x2007 && address < 0x4000) { 31 | this.regWrite(0x2000 + (address & 0x7), value); 32 | } else { 33 | this.regWrite(address, value); 34 | } 35 | }, 36 | 37 | writelow: function (address, value) { 38 | if (address < 0x2000) { 39 | // Mirroring of RAM: 40 | this.nes.cpu.mem[address & 0x7ff] = value; 41 | } else if (address > 0x4017) { 42 | this.nes.cpu.mem[address] = value; 43 | } else if (address > 0x2007 && address < 0x4000) { 44 | this.regWrite(0x2000 + (address & 0x7), value); 45 | } else { 46 | this.regWrite(address, value); 47 | } 48 | }, 49 | 50 | load: function (address) { 51 | // Wrap around: 52 | address &= 0xffff; 53 | 54 | // Check address range: 55 | if (address > 0x4017) { 56 | // ROM: 57 | return this.nes.cpu.mem[address]; 58 | } else if (address >= 0x2000) { 59 | // I/O Ports. 60 | return this.regLoad(address); 61 | } else { 62 | // RAM (mirrored) 63 | return this.nes.cpu.mem[address & 0x7ff]; 64 | } 65 | }, 66 | 67 | regLoad: function (address) { 68 | switch ( 69 | address >> 12 // use fourth nibble (0xF000) 70 | ) { 71 | case 0: 72 | break; 73 | 74 | case 1: 75 | break; 76 | 77 | case 2: 78 | // Fall through to case 3 79 | case 3: 80 | // PPU Registers 81 | switch (address & 0x7) { 82 | case 0x0: 83 | // 0x2000: 84 | // PPU Control Register 1. 85 | // (the value is stored both 86 | // in main memory and in the 87 | // PPU as flags): 88 | // (not in the real NES) 89 | return this.nes.cpu.mem[0x2000]; 90 | 91 | case 0x1: 92 | // 0x2001: 93 | // PPU Control Register 2. 94 | // (the value is stored both 95 | // in main memory and in the 96 | // PPU as flags): 97 | // (not in the real NES) 98 | return this.nes.cpu.mem[0x2001]; 99 | 100 | case 0x2: 101 | // 0x2002: 102 | // PPU Status Register. 103 | // The value is stored in 104 | // main memory in addition 105 | // to as flags in the PPU. 106 | // (not in the real NES) 107 | return this.nes.ppu.readStatusRegister(); 108 | 109 | case 0x3: 110 | return 0; 111 | 112 | case 0x4: 113 | // 0x2004: 114 | // Sprite Memory read. 115 | return this.nes.ppu.sramLoad(); 116 | case 0x5: 117 | return 0; 118 | 119 | case 0x6: 120 | return 0; 121 | 122 | case 0x7: 123 | // 0x2007: 124 | // VRAM read: 125 | return this.nes.ppu.vramLoad(); 126 | } 127 | break; 128 | case 4: 129 | // Sound+Joypad registers 130 | switch (address - 0x4015) { 131 | case 0: 132 | // 0x4015: 133 | // Sound channel enable, DMC Status 134 | return this.nes.papu.readReg(address); 135 | 136 | case 1: 137 | // 0x4016: 138 | // Joystick 1 + Strobe 139 | return this.joy1Read(); 140 | 141 | case 2: 142 | // 0x4017: 143 | // Joystick 2 + Strobe 144 | // https://wiki.nesdev.com/w/index.php/Zapper 145 | var w; 146 | 147 | if ( 148 | this.zapperX !== null && 149 | this.zapperY !== null && 150 | this.nes.ppu.isPixelWhite(this.zapperX, this.zapperY) 151 | ) { 152 | w = 0; 153 | } else { 154 | w = 0x1 << 3; 155 | } 156 | 157 | if (this.zapperFired) { 158 | w |= 0x1 << 4; 159 | } 160 | return (this.joy2Read() | w) & 0xffff; 161 | } 162 | break; 163 | } 164 | return 0; 165 | }, 166 | 167 | regWrite: function (address, value) { 168 | switch (address) { 169 | case 0x2000: 170 | // PPU Control register 1 171 | this.nes.cpu.mem[address] = value; 172 | this.nes.ppu.updateControlReg1(value); 173 | break; 174 | 175 | case 0x2001: 176 | // PPU Control register 2 177 | this.nes.cpu.mem[address] = value; 178 | this.nes.ppu.updateControlReg2(value); 179 | break; 180 | 181 | case 0x2003: 182 | // Set Sprite RAM address: 183 | this.nes.ppu.writeSRAMAddress(value); 184 | break; 185 | 186 | case 0x2004: 187 | // Write to Sprite RAM: 188 | this.nes.ppu.sramWrite(value); 189 | break; 190 | 191 | case 0x2005: 192 | // Screen Scroll offsets: 193 | this.nes.ppu.scrollWrite(value); 194 | break; 195 | 196 | case 0x2006: 197 | // Set VRAM address: 198 | this.nes.ppu.writeVRAMAddress(value); 199 | break; 200 | 201 | case 0x2007: 202 | // Write to VRAM: 203 | this.nes.ppu.vramWrite(value); 204 | break; 205 | 206 | case 0x4014: 207 | // Sprite Memory DMA Access 208 | this.nes.ppu.sramDMA(value); 209 | break; 210 | 211 | case 0x4015: 212 | // Sound Channel Switch, DMC Status 213 | this.nes.papu.writeReg(address, value); 214 | break; 215 | 216 | case 0x4016: 217 | // Joystick 1 + Strobe 218 | if ((value & 1) === 0 && (this.joypadLastWrite & 1) === 1) { 219 | this.joy1StrobeState = 0; 220 | this.joy2StrobeState = 0; 221 | } 222 | this.joypadLastWrite = value; 223 | break; 224 | 225 | case 0x4017: 226 | // Sound channel frame sequencer: 227 | this.nes.papu.writeReg(address, value); 228 | break; 229 | 230 | default: 231 | // Sound registers 232 | // console.log("write to sound reg"); 233 | if (address >= 0x4000 && address <= 0x4017) { 234 | this.nes.papu.writeReg(address, value); 235 | } 236 | } 237 | }, 238 | 239 | joy1Read: function () { 240 | var ret; 241 | 242 | switch (this.joy1StrobeState) { 243 | case 0: 244 | case 1: 245 | case 2: 246 | case 3: 247 | case 4: 248 | case 5: 249 | case 6: 250 | case 7: 251 | ret = this.nes.controllers[1].state[this.joy1StrobeState]; 252 | break; 253 | case 8: 254 | case 9: 255 | case 10: 256 | case 11: 257 | case 12: 258 | case 13: 259 | case 14: 260 | case 15: 261 | case 16: 262 | case 17: 263 | case 18: 264 | ret = 0; 265 | break; 266 | case 19: 267 | ret = 1; 268 | break; 269 | default: 270 | ret = 0; 271 | } 272 | 273 | this.joy1StrobeState++; 274 | if (this.joy1StrobeState === 24) { 275 | this.joy1StrobeState = 0; 276 | } 277 | 278 | return ret; 279 | }, 280 | 281 | joy2Read: function () { 282 | var ret; 283 | 284 | switch (this.joy2StrobeState) { 285 | case 0: 286 | case 1: 287 | case 2: 288 | case 3: 289 | case 4: 290 | case 5: 291 | case 6: 292 | case 7: 293 | ret = this.nes.controllers[2].state[this.joy2StrobeState]; 294 | break; 295 | case 8: 296 | case 9: 297 | case 10: 298 | case 11: 299 | case 12: 300 | case 13: 301 | case 14: 302 | case 15: 303 | case 16: 304 | case 17: 305 | case 18: 306 | ret = 0; 307 | break; 308 | case 19: 309 | ret = 1; 310 | break; 311 | default: 312 | ret = 0; 313 | } 314 | 315 | this.joy2StrobeState++; 316 | if (this.joy2StrobeState === 24) { 317 | this.joy2StrobeState = 0; 318 | } 319 | 320 | return ret; 321 | }, 322 | 323 | loadROM: function () { 324 | if (!this.nes.rom.valid || this.nes.rom.romCount < 1) { 325 | throw new Error("NoMapper: Invalid ROM! Unable to load."); 326 | } 327 | 328 | // Load ROM into memory: 329 | this.loadPRGROM(); 330 | 331 | // Load CHR-ROM: 332 | this.loadCHRROM(); 333 | 334 | // Load Battery RAM (if present): 335 | this.loadBatteryRam(); 336 | 337 | // Reset IRQ: 338 | //nes.getCpu().doResetInterrupt(); 339 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 340 | }, 341 | 342 | loadPRGROM: function () { 343 | if (this.nes.rom.romCount > 1) { 344 | // Load the two first banks into memory. 345 | this.loadRomBank(0, 0x8000); 346 | this.loadRomBank(1, 0xc000); 347 | } else { 348 | // Load the one bank into both memory locations: 349 | this.loadRomBank(0, 0x8000); 350 | this.loadRomBank(0, 0xc000); 351 | } 352 | }, 353 | 354 | loadCHRROM: function () { 355 | // console.log("Loading CHR ROM.."); 356 | if (this.nes.rom.vromCount > 0) { 357 | if (this.nes.rom.vromCount === 1) { 358 | this.loadVromBank(0, 0x0000); 359 | this.loadVromBank(0, 0x1000); 360 | } else { 361 | this.loadVromBank(0, 0x0000); 362 | this.loadVromBank(1, 0x1000); 363 | } 364 | } else { 365 | //System.out.println("There aren't any CHR-ROM banks.."); 366 | } 367 | }, 368 | 369 | loadBatteryRam: function () { 370 | if (this.nes.rom.batteryRam) { 371 | var ram = this.nes.rom.batteryRam; 372 | if (ram !== null && ram.length === 0x2000) { 373 | // Load Battery RAM into memory: 374 | utils.copyArrayElements(ram, 0, this.nes.cpu.mem, 0x6000, 0x2000); 375 | } 376 | } 377 | }, 378 | 379 | loadRomBank: function (bank, address) { 380 | // Loads a ROM bank into the specified address. 381 | bank %= this.nes.rom.romCount; 382 | //var data = this.nes.rom.rom[bank]; 383 | //cpuMem.write(address,data,data.length); 384 | utils.copyArrayElements( 385 | this.nes.rom.rom[bank], 386 | 0, 387 | this.nes.cpu.mem, 388 | address, 389 | 16384 390 | ); 391 | }, 392 | 393 | loadVromBank: function (bank, address) { 394 | if (this.nes.rom.vromCount === 0) { 395 | return; 396 | } 397 | this.nes.ppu.triggerRendering(); 398 | 399 | utils.copyArrayElements( 400 | this.nes.rom.vrom[bank % this.nes.rom.vromCount], 401 | 0, 402 | this.nes.ppu.vramMem, 403 | address, 404 | 4096 405 | ); 406 | 407 | var vromTile = this.nes.rom.vromTile[bank % this.nes.rom.vromCount]; 408 | utils.copyArrayElements( 409 | vromTile, 410 | 0, 411 | this.nes.ppu.ptTile, 412 | address >> 4, 413 | 256 414 | ); 415 | }, 416 | 417 | load32kRomBank: function (bank, address) { 418 | this.loadRomBank((bank * 2) % this.nes.rom.romCount, address); 419 | this.loadRomBank((bank * 2 + 1) % this.nes.rom.romCount, address + 16384); 420 | }, 421 | 422 | load8kVromBank: function (bank4kStart, address) { 423 | if (this.nes.rom.vromCount === 0) { 424 | return; 425 | } 426 | this.nes.ppu.triggerRendering(); 427 | 428 | this.loadVromBank(bank4kStart % this.nes.rom.vromCount, address); 429 | this.loadVromBank( 430 | (bank4kStart + 1) % this.nes.rom.vromCount, 431 | address + 4096 432 | ); 433 | }, 434 | 435 | load1kVromBank: function (bank1k, address) { 436 | if (this.nes.rom.vromCount === 0) { 437 | return; 438 | } 439 | this.nes.ppu.triggerRendering(); 440 | 441 | var bank4k = Math.floor(bank1k / 4) % this.nes.rom.vromCount; 442 | var bankoffset = (bank1k % 4) * 1024; 443 | utils.copyArrayElements( 444 | this.nes.rom.vrom[bank4k], 445 | bankoffset, 446 | this.nes.ppu.vramMem, 447 | address, 448 | 1024 449 | ); 450 | 451 | // Update tiles: 452 | var vromTile = this.nes.rom.vromTile[bank4k]; 453 | var baseIndex = address >> 4; 454 | for (var i = 0; i < 64; i++) { 455 | this.nes.ppu.ptTile[baseIndex + i] = vromTile[(bank1k % 4 << 6) + i]; 456 | } 457 | }, 458 | 459 | load2kVromBank: function (bank2k, address) { 460 | if (this.nes.rom.vromCount === 0) { 461 | return; 462 | } 463 | this.nes.ppu.triggerRendering(); 464 | 465 | var bank4k = Math.floor(bank2k / 2) % this.nes.rom.vromCount; 466 | var bankoffset = (bank2k % 2) * 2048; 467 | utils.copyArrayElements( 468 | this.nes.rom.vrom[bank4k], 469 | bankoffset, 470 | this.nes.ppu.vramMem, 471 | address, 472 | 2048 473 | ); 474 | 475 | // Update tiles: 476 | var vromTile = this.nes.rom.vromTile[bank4k]; 477 | var baseIndex = address >> 4; 478 | for (var i = 0; i < 128; i++) { 479 | this.nes.ppu.ptTile[baseIndex + i] = vromTile[(bank2k % 2 << 7) + i]; 480 | } 481 | }, 482 | 483 | load8kRomBank: function (bank8k, address) { 484 | var bank16k = Math.floor(bank8k / 2) % this.nes.rom.romCount; 485 | var offset = (bank8k % 2) * 8192; 486 | 487 | //this.nes.cpu.mem.write(address,this.nes.rom.rom[bank16k],offset,8192); 488 | utils.copyArrayElements( 489 | this.nes.rom.rom[bank16k], 490 | offset, 491 | this.nes.cpu.mem, 492 | address, 493 | 8192 494 | ); 495 | }, 496 | 497 | clockIrqCounter: function () { 498 | // Does nothing. This is used by the MMC3 mapper. 499 | }, 500 | 501 | // eslint-disable-next-line no-unused-vars 502 | latchAccess: function (address) { 503 | // Does nothing. This is used by MMC2. 504 | }, 505 | 506 | toJSON: function () { 507 | return { 508 | joy1StrobeState: this.joy1StrobeState, 509 | joy2StrobeState: this.joy2StrobeState, 510 | joypadLastWrite: this.joypadLastWrite, 511 | }; 512 | }, 513 | 514 | fromJSON: function (s) { 515 | this.joy1StrobeState = s.joy1StrobeState; 516 | this.joy2StrobeState = s.joy2StrobeState; 517 | this.joypadLastWrite = s.joypadLastWrite; 518 | }, 519 | }; 520 | 521 | Mappers[1] = function (nes) { 522 | this.nes = nes; 523 | }; 524 | 525 | Mappers[1].prototype = new Mappers[0](); 526 | 527 | Mappers[1].prototype.reset = function () { 528 | Mappers[0].prototype.reset.apply(this); 529 | 530 | // 5-bit buffer: 531 | this.regBuffer = 0; 532 | this.regBufferCounter = 0; 533 | 534 | // Register 0: 535 | this.mirroring = 0; 536 | this.oneScreenMirroring = 0; 537 | this.prgSwitchingArea = 1; 538 | this.prgSwitchingSize = 1; 539 | this.vromSwitchingSize = 0; 540 | 541 | // Register 1: 542 | this.romSelectionReg0 = 0; 543 | 544 | // Register 2: 545 | this.romSelectionReg1 = 0; 546 | 547 | // Register 3: 548 | this.romBankSelect = 0; 549 | }; 550 | 551 | Mappers[1].prototype.write = function (address, value) { 552 | // Writes to addresses other than MMC registers are handled by NoMapper. 553 | if (address < 0x8000) { 554 | Mappers[0].prototype.write.apply(this, arguments); 555 | return; 556 | } 557 | 558 | // See what should be done with the written value: 559 | if ((value & 128) !== 0) { 560 | // Reset buffering: 561 | this.regBufferCounter = 0; 562 | this.regBuffer = 0; 563 | 564 | // Reset register: 565 | if (this.getRegNumber(address) === 0) { 566 | this.prgSwitchingArea = 1; 567 | this.prgSwitchingSize = 1; 568 | } 569 | } else { 570 | // Continue buffering: 571 | //regBuffer = (regBuffer & (0xFF-(1<<regBufferCounter))) | ((value & (1<<regBufferCounter))<<regBufferCounter); 572 | this.regBuffer = 573 | (this.regBuffer & (0xff - (1 << this.regBufferCounter))) | 574 | ((value & 1) << this.regBufferCounter); 575 | this.regBufferCounter++; 576 | 577 | if (this.regBufferCounter === 5) { 578 | // Use the buffered value: 579 | this.setReg(this.getRegNumber(address), this.regBuffer); 580 | 581 | // Reset buffer: 582 | this.regBuffer = 0; 583 | this.regBufferCounter = 0; 584 | } 585 | } 586 | }; 587 | 588 | Mappers[1].prototype.setReg = function (reg, value) { 589 | var tmp; 590 | 591 | switch (reg) { 592 | case 0: 593 | // Mirroring: 594 | tmp = value & 3; 595 | if (tmp !== this.mirroring) { 596 | // Set mirroring: 597 | this.mirroring = tmp; 598 | if ((this.mirroring & 2) === 0) { 599 | // SingleScreen mirroring overrides the other setting: 600 | this.nes.ppu.setMirroring(this.nes.rom.SINGLESCREEN_MIRRORING); 601 | } else if ((this.mirroring & 1) !== 0) { 602 | // Not overridden by SingleScreen mirroring. 603 | this.nes.ppu.setMirroring(this.nes.rom.HORIZONTAL_MIRRORING); 604 | } else { 605 | this.nes.ppu.setMirroring(this.nes.rom.VERTICAL_MIRRORING); 606 | } 607 | } 608 | 609 | // PRG Switching Area; 610 | this.prgSwitchingArea = (value >> 2) & 1; 611 | 612 | // PRG Switching Size: 613 | this.prgSwitchingSize = (value >> 3) & 1; 614 | 615 | // VROM Switching Size: 616 | this.vromSwitchingSize = (value >> 4) & 1; 617 | 618 | break; 619 | 620 | case 1: 621 | // ROM selection: 622 | this.romSelectionReg0 = (value >> 4) & 1; 623 | 624 | // Check whether the cart has VROM: 625 | if (this.nes.rom.vromCount > 0) { 626 | // Select VROM bank at 0x0000: 627 | if (this.vromSwitchingSize === 0) { 628 | // Swap 8kB VROM: 629 | if (this.romSelectionReg0 === 0) { 630 | this.load8kVromBank(value & 0xf, 0x0000); 631 | } else { 632 | this.load8kVromBank( 633 | Math.floor(this.nes.rom.vromCount / 2) + (value & 0xf), 634 | 0x0000 635 | ); 636 | } 637 | } else { 638 | // Swap 4kB VROM: 639 | if (this.romSelectionReg0 === 0) { 640 | this.loadVromBank(value & 0xf, 0x0000); 641 | } else { 642 | this.loadVromBank( 643 | Math.floor(this.nes.rom.vromCount / 2) + (value & 0xf), 644 | 0x0000 645 | ); 646 | } 647 | } 648 | } 649 | 650 | break; 651 | 652 | case 2: 653 | // ROM selection: 654 | this.romSelectionReg1 = (value >> 4) & 1; 655 | 656 | // Check whether the cart has VROM: 657 | if (this.nes.rom.vromCount > 0) { 658 | // Select VROM bank at 0x1000: 659 | if (this.vromSwitchingSize === 1) { 660 | // Swap 4kB of VROM: 661 | if (this.romSelectionReg1 === 0) { 662 | this.loadVromBank(value & 0xf, 0x1000); 663 | } else { 664 | this.loadVromBank( 665 | Math.floor(this.nes.rom.vromCount / 2) + (value & 0xf), 666 | 0x1000 667 | ); 668 | } 669 | } 670 | } 671 | break; 672 | 673 | default: 674 | // Select ROM bank: 675 | // ------------------------- 676 | tmp = value & 0xf; 677 | var bank; 678 | var baseBank = 0; 679 | 680 | if (this.nes.rom.romCount >= 32) { 681 | // 1024 kB cart 682 | if (this.vromSwitchingSize === 0) { 683 | if (this.romSelectionReg0 === 1) { 684 | baseBank = 16; 685 | } 686 | } else { 687 | baseBank = 688 | (this.romSelectionReg0 | (this.romSelectionReg1 << 1)) << 3; 689 | } 690 | } else if (this.nes.rom.romCount >= 16) { 691 | // 512 kB cart 692 | if (this.romSelectionReg0 === 1) { 693 | baseBank = 8; 694 | } 695 | } 696 | 697 | if (this.prgSwitchingSize === 0) { 698 | // 32kB 699 | bank = baseBank + (value & 0xf); 700 | this.load32kRomBank(bank, 0x8000); 701 | } else { 702 | // 16kB 703 | bank = baseBank * 2 + (value & 0xf); 704 | if (this.prgSwitchingArea === 0) { 705 | this.loadRomBank(bank, 0xc000); 706 | } else { 707 | this.loadRomBank(bank, 0x8000); 708 | } 709 | } 710 | } 711 | }; 712 | 713 | // Returns the register number from the address written to: 714 | Mappers[1].prototype.getRegNumber = function (address) { 715 | if (address >= 0x8000 && address <= 0x9fff) { 716 | return 0; 717 | } else if (address >= 0xa000 && address <= 0xbfff) { 718 | return 1; 719 | } else if (address >= 0xc000 && address <= 0xdfff) { 720 | return 2; 721 | } else { 722 | return 3; 723 | } 724 | }; 725 | 726 | Mappers[1].prototype.loadROM = function () { 727 | if (!this.nes.rom.valid) { 728 | throw new Error("MMC1: Invalid ROM! Unable to load."); 729 | } 730 | 731 | // Load PRG-ROM: 732 | this.loadRomBank(0, 0x8000); // First ROM bank.. 733 | this.loadRomBank(this.nes.rom.romCount - 1, 0xc000); // ..and last ROM bank. 734 | 735 | // Load CHR-ROM: 736 | this.loadCHRROM(); 737 | 738 | // Load Battery RAM (if present): 739 | this.loadBatteryRam(); 740 | 741 | // Do Reset-Interrupt: 742 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 743 | }; 744 | 745 | // eslint-disable-next-line no-unused-vars 746 | Mappers[1].prototype.switchLowHighPrgRom = function (oldSetting) { 747 | // not yet. 748 | }; 749 | 750 | Mappers[1].prototype.switch16to32 = function () { 751 | // not yet. 752 | }; 753 | 754 | Mappers[1].prototype.switch32to16 = function () { 755 | // not yet. 756 | }; 757 | 758 | Mappers[1].prototype.toJSON = function () { 759 | var s = Mappers[0].prototype.toJSON.apply(this); 760 | s.mirroring = this.mirroring; 761 | s.oneScreenMirroring = this.oneScreenMirroring; 762 | s.prgSwitchingArea = this.prgSwitchingArea; 763 | s.prgSwitchingSize = this.prgSwitchingSize; 764 | s.vromSwitchingSize = this.vromSwitchingSize; 765 | s.romSelectionReg0 = this.romSelectionReg0; 766 | s.romSelectionReg1 = this.romSelectionReg1; 767 | s.romBankSelect = this.romBankSelect; 768 | s.regBuffer = this.regBuffer; 769 | s.regBufferCounter = this.regBufferCounter; 770 | return s; 771 | }; 772 | 773 | Mappers[1].prototype.fromJSON = function (s) { 774 | Mappers[0].prototype.fromJSON.apply(this, arguments); 775 | this.mirroring = s.mirroring; 776 | this.oneScreenMirroring = s.oneScreenMirroring; 777 | this.prgSwitchingArea = s.prgSwitchingArea; 778 | this.prgSwitchingSize = s.prgSwitchingSize; 779 | this.vromSwitchingSize = s.vromSwitchingSize; 780 | this.romSelectionReg0 = s.romSelectionReg0; 781 | this.romSelectionReg1 = s.romSelectionReg1; 782 | this.romBankSelect = s.romBankSelect; 783 | this.regBuffer = s.regBuffer; 784 | this.regBufferCounter = s.regBufferCounter; 785 | }; 786 | 787 | Mappers[2] = function (nes) { 788 | this.nes = nes; 789 | }; 790 | 791 | Mappers[2].prototype = new Mappers[0](); 792 | 793 | Mappers[2].prototype.write = function (address, value) { 794 | // Writes to addresses other than MMC registers are handled by NoMapper. 795 | if (address < 0x8000) { 796 | Mappers[0].prototype.write.apply(this, arguments); 797 | return; 798 | } else { 799 | // This is a ROM bank select command. 800 | // Swap in the given ROM bank at 0x8000: 801 | this.loadRomBank(value, 0x8000); 802 | } 803 | }; 804 | 805 | Mappers[2].prototype.loadROM = function () { 806 | if (!this.nes.rom.valid) { 807 | throw new Error("UNROM: Invalid ROM! Unable to load."); 808 | } 809 | 810 | // Load PRG-ROM: 811 | this.loadRomBank(0, 0x8000); 812 | this.loadRomBank(this.nes.rom.romCount - 1, 0xc000); 813 | 814 | // Load CHR-ROM: 815 | this.loadCHRROM(); 816 | 817 | // Do Reset-Interrupt: 818 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 819 | }; 820 | 821 | /** 822 | * Mapper 003 (CNROM) 823 | * 824 | * @constructor 825 | * @example Solomon's Key, Arkanoid, Arkista's Ring, Bump 'n' Jump, Cybernoid 826 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_003 827 | */ 828 | Mappers[3] = function (nes) { 829 | this.nes = nes; 830 | }; 831 | 832 | Mappers[3].prototype = new Mappers[0](); 833 | 834 | Mappers[3].prototype.write = function (address, value) { 835 | // Writes to addresses other than MMC registers are handled by NoMapper. 836 | if (address < 0x8000) { 837 | Mappers[0].prototype.write.apply(this, arguments); 838 | return; 839 | } else { 840 | // This is a ROM bank select command. 841 | // Swap in the given ROM bank at 0x8000: 842 | // This is a VROM bank select command. 843 | // Swap in the given VROM bank at 0x0000: 844 | var bank = (value % (this.nes.rom.vromCount / 2)) * 2; 845 | this.loadVromBank(bank, 0x0000); 846 | this.loadVromBank(bank + 1, 0x1000); 847 | this.load8kVromBank(value * 2, 0x0000); 848 | } 849 | }; 850 | 851 | Mappers[4] = function (nes) { 852 | this.nes = nes; 853 | 854 | this.CMD_SEL_2_1K_VROM_0000 = 0; 855 | this.CMD_SEL_2_1K_VROM_0800 = 1; 856 | this.CMD_SEL_1K_VROM_1000 = 2; 857 | this.CMD_SEL_1K_VROM_1400 = 3; 858 | this.CMD_SEL_1K_VROM_1800 = 4; 859 | this.CMD_SEL_1K_VROM_1C00 = 5; 860 | this.CMD_SEL_ROM_PAGE1 = 6; 861 | this.CMD_SEL_ROM_PAGE2 = 7; 862 | 863 | this.command = null; 864 | this.prgAddressSelect = null; 865 | this.chrAddressSelect = null; 866 | this.pageNumber = null; 867 | this.irqCounter = null; 868 | this.irqLatchValue = null; 869 | this.irqEnable = null; 870 | this.prgAddressChanged = false; 871 | }; 872 | 873 | Mappers[4].prototype = new Mappers[0](); 874 | 875 | Mappers[4].prototype.write = function (address, value) { 876 | // Writes to addresses other than MMC registers are handled by NoMapper. 877 | if (address < 0x8000) { 878 | Mappers[0].prototype.write.apply(this, arguments); 879 | return; 880 | } 881 | 882 | switch (address) { 883 | case 0x8000: 884 | // Command/Address Select register 885 | this.command = value & 7; 886 | var tmp = (value >> 6) & 1; 887 | if (tmp !== this.prgAddressSelect) { 888 | this.prgAddressChanged = true; 889 | } 890 | this.prgAddressSelect = tmp; 891 | this.chrAddressSelect = (value >> 7) & 1; 892 | break; 893 | 894 | case 0x8001: 895 | // Page number for command 896 | this.executeCommand(this.command, value); 897 | break; 898 | 899 | case 0xa000: 900 | // Mirroring select 901 | if ((value & 1) !== 0) { 902 | this.nes.ppu.setMirroring(this.nes.rom.HORIZONTAL_MIRRORING); 903 | } else { 904 | this.nes.ppu.setMirroring(this.nes.rom.VERTICAL_MIRRORING); 905 | } 906 | break; 907 | 908 | case 0xa001: 909 | // SaveRAM Toggle 910 | // TODO 911 | //nes.getRom().setSaveState((value&1)!=0); 912 | break; 913 | 914 | case 0xc000: 915 | // IRQ Counter register 916 | this.irqCounter = value; 917 | //nes.ppu.mapperIrqCounter = 0; 918 | break; 919 | 920 | case 0xc001: 921 | // IRQ Latch register 922 | this.irqLatchValue = value; 923 | break; 924 | 925 | case 0xe000: 926 | // IRQ Control Reg 0 (disable) 927 | //irqCounter = irqLatchValue; 928 | this.irqEnable = 0; 929 | break; 930 | 931 | case 0xe001: 932 | // IRQ Control Reg 1 (enable) 933 | this.irqEnable = 1; 934 | break; 935 | 936 | default: 937 | // Not a MMC3 register. 938 | // The game has probably crashed, 939 | // since it tries to write to ROM.. 940 | // IGNORE. 941 | } 942 | }; 943 | 944 | Mappers[4].prototype.executeCommand = function (cmd, arg) { 945 | switch (cmd) { 946 | case this.CMD_SEL_2_1K_VROM_0000: 947 | // Select 2 1KB VROM pages at 0x0000: 948 | if (this.chrAddressSelect === 0) { 949 | this.load1kVromBank(arg, 0x0000); 950 | this.load1kVromBank(arg + 1, 0x0400); 951 | } else { 952 | this.load1kVromBank(arg, 0x1000); 953 | this.load1kVromBank(arg + 1, 0x1400); 954 | } 955 | break; 956 | 957 | case this.CMD_SEL_2_1K_VROM_0800: 958 | // Select 2 1KB VROM pages at 0x0800: 959 | if (this.chrAddressSelect === 0) { 960 | this.load1kVromBank(arg, 0x0800); 961 | this.load1kVromBank(arg + 1, 0x0c00); 962 | } else { 963 | this.load1kVromBank(arg, 0x1800); 964 | this.load1kVromBank(arg + 1, 0x1c00); 965 | } 966 | break; 967 | 968 | case this.CMD_SEL_1K_VROM_1000: 969 | // Select 1K VROM Page at 0x1000: 970 | if (this.chrAddressSelect === 0) { 971 | this.load1kVromBank(arg, 0x1000); 972 | } else { 973 | this.load1kVromBank(arg, 0x0000); 974 | } 975 | break; 976 | 977 | case this.CMD_SEL_1K_VROM_1400: 978 | // Select 1K VROM Page at 0x1400: 979 | if (this.chrAddressSelect === 0) { 980 | this.load1kVromBank(arg, 0x1400); 981 | } else { 982 | this.load1kVromBank(arg, 0x0400); 983 | } 984 | break; 985 | 986 | case this.CMD_SEL_1K_VROM_1800: 987 | // Select 1K VROM Page at 0x1800: 988 | if (this.chrAddressSelect === 0) { 989 | this.load1kVromBank(arg, 0x1800); 990 | } else { 991 | this.load1kVromBank(arg, 0x0800); 992 | } 993 | break; 994 | 995 | case this.CMD_SEL_1K_VROM_1C00: 996 | // Select 1K VROM Page at 0x1C00: 997 | if (this.chrAddressSelect === 0) { 998 | this.load1kVromBank(arg, 0x1c00); 999 | } else { 1000 | this.load1kVromBank(arg, 0x0c00); 1001 | } 1002 | break; 1003 | 1004 | case this.CMD_SEL_ROM_PAGE1: 1005 | if (this.prgAddressChanged) { 1006 | // Load the two hardwired banks: 1007 | if (this.prgAddressSelect === 0) { 1008 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0xc000); 1009 | } else { 1010 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0x8000); 1011 | } 1012 | this.prgAddressChanged = false; 1013 | } 1014 | 1015 | // Select first switchable ROM page: 1016 | if (this.prgAddressSelect === 0) { 1017 | this.load8kRomBank(arg, 0x8000); 1018 | } else { 1019 | this.load8kRomBank(arg, 0xc000); 1020 | } 1021 | break; 1022 | 1023 | case this.CMD_SEL_ROM_PAGE2: 1024 | // Select second switchable ROM page: 1025 | this.load8kRomBank(arg, 0xa000); 1026 | 1027 | // hardwire appropriate bank: 1028 | if (this.prgAddressChanged) { 1029 | // Load the two hardwired banks: 1030 | if (this.prgAddressSelect === 0) { 1031 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0xc000); 1032 | } else { 1033 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0x8000); 1034 | } 1035 | this.prgAddressChanged = false; 1036 | } 1037 | } 1038 | }; 1039 | 1040 | Mappers[4].prototype.loadROM = function () { 1041 | if (!this.nes.rom.valid) { 1042 | throw new Error("MMC3: Invalid ROM! Unable to load."); 1043 | } 1044 | 1045 | // Load hardwired PRG banks (0xC000 and 0xE000): 1046 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2, 0xc000); 1047 | this.load8kRomBank((this.nes.rom.romCount - 1) * 2 + 1, 0xe000); 1048 | 1049 | // Load swappable PRG banks (0x8000 and 0xA000): 1050 | this.load8kRomBank(0, 0x8000); 1051 | this.load8kRomBank(1, 0xa000); 1052 | 1053 | // Load CHR-ROM: 1054 | this.loadCHRROM(); 1055 | 1056 | // Load Battery RAM (if present): 1057 | this.loadBatteryRam(); 1058 | 1059 | // Do Reset-Interrupt: 1060 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1061 | }; 1062 | 1063 | Mappers[4].prototype.clockIrqCounter = function () { 1064 | if (this.irqEnable === 1) { 1065 | this.irqCounter--; 1066 | if (this.irqCounter < 0) { 1067 | // Trigger IRQ: 1068 | //nes.getCpu().doIrq(); 1069 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NORMAL); 1070 | this.irqCounter = this.irqLatchValue; 1071 | } 1072 | } 1073 | }; 1074 | 1075 | Mappers[4].prototype.toJSON = function () { 1076 | var s = Mappers[0].prototype.toJSON.apply(this); 1077 | s.command = this.command; 1078 | s.prgAddressSelect = this.prgAddressSelect; 1079 | s.chrAddressSelect = this.chrAddressSelect; 1080 | s.pageNumber = this.pageNumber; 1081 | s.irqCounter = this.irqCounter; 1082 | s.irqLatchValue = this.irqLatchValue; 1083 | s.irqEnable = this.irqEnable; 1084 | s.prgAddressChanged = this.prgAddressChanged; 1085 | return s; 1086 | }; 1087 | 1088 | Mappers[4].prototype.fromJSON = function (s) { 1089 | Mappers[0].prototype.fromJSON.apply(this, arguments); 1090 | this.command = s.command; 1091 | this.prgAddressSelect = s.prgAddressSelect; 1092 | this.chrAddressSelect = s.chrAddressSelect; 1093 | this.pageNumber = s.pageNumber; 1094 | this.irqCounter = s.irqCounter; 1095 | this.irqLatchValue = s.irqLatchValue; 1096 | this.irqEnable = s.irqEnable; 1097 | this.prgAddressChanged = s.prgAddressChanged; 1098 | }; 1099 | 1100 | /** 1101 | * Mapper005 (MMC5,ExROM) 1102 | * 1103 | * @example Castlevania 3, Just Breed, Uncharted Waters, Romance of the 3 Kingdoms 2, Laser Invasion, Metal Slader Glory, Uchuu Keibitai SDF, Shin 4 Nin Uchi Mahjong - Yakuman Tengoku 1104 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_005 1105 | * @constructor 1106 | */ 1107 | Mappers[5] = function (nes) { 1108 | this.nes = nes; 1109 | }; 1110 | 1111 | Mappers[5].prototype = new Mappers[0](); 1112 | 1113 | Mappers[5].prototype.write = function (address, value) { 1114 | // Writes to addresses other than MMC registers are handled by NoMapper. 1115 | if (address < 0x8000) { 1116 | Mappers[0].prototype.write.apply(this, arguments); 1117 | } else { 1118 | this.load8kVromBank(value, 0x0000); 1119 | } 1120 | }; 1121 | 1122 | Mappers[5].prototype.write = function (address, value) { 1123 | // Writes to addresses other than MMC registers are handled by NoMapper. 1124 | if (address < 0x5000) { 1125 | Mappers[0].prototype.write.apply(this, arguments); 1126 | return; 1127 | } 1128 | 1129 | switch (address) { 1130 | case 0x5100: 1131 | this.prg_size = value & 3; 1132 | break; 1133 | case 0x5101: 1134 | this.chr_size = value & 3; 1135 | break; 1136 | case 0x5102: 1137 | this.sram_we_a = value & 3; 1138 | break; 1139 | case 0x5103: 1140 | this.sram_we_b = value & 3; 1141 | break; 1142 | case 0x5104: 1143 | this.graphic_mode = value & 3; 1144 | break; 1145 | case 0x5105: 1146 | this.nametable_mode = value; 1147 | this.nametable_type[0] = value & 3; 1148 | this.load1kVromBank(value & 3, 0x2000); 1149 | value >>= 2; 1150 | this.nametable_type[1] = value & 3; 1151 | this.load1kVromBank(value & 3, 0x2400); 1152 | value >>= 2; 1153 | this.nametable_type[2] = value & 3; 1154 | this.load1kVromBank(value & 3, 0x2800); 1155 | value >>= 2; 1156 | this.nametable_type[3] = value & 3; 1157 | this.load1kVromBank(value & 3, 0x2c00); 1158 | break; 1159 | case 0x5106: 1160 | this.fill_chr = value; 1161 | break; 1162 | case 0x5107: 1163 | this.fill_pal = value & 3; 1164 | break; 1165 | case 0x5113: 1166 | this.SetBank_SRAM(3, value & 3); 1167 | break; 1168 | case 0x5114: 1169 | case 0x5115: 1170 | case 0x5116: 1171 | case 0x5117: 1172 | this.SetBank_CPU(address, value); 1173 | break; 1174 | case 0x5120: 1175 | case 0x5121: 1176 | case 0x5122: 1177 | case 0x5123: 1178 | case 0x5124: 1179 | case 0x5125: 1180 | case 0x5126: 1181 | case 0x5127: 1182 | this.chr_mode = 0; 1183 | this.chr_page[0][address & 7] = value; 1184 | this.SetBank_PPU(); 1185 | break; 1186 | case 0x5128: 1187 | case 0x5129: 1188 | case 0x512a: 1189 | case 0x512b: 1190 | this.chr_mode = 1; 1191 | this.chr_page[1][(address & 3) + 0] = value; 1192 | this.chr_page[1][(address & 3) + 4] = value; 1193 | this.SetBank_PPU(); 1194 | break; 1195 | case 0x5200: 1196 | this.split_control = value; 1197 | break; 1198 | case 0x5201: 1199 | this.split_scroll = value; 1200 | break; 1201 | case 0x5202: 1202 | this.split_page = value & 0x3f; 1203 | break; 1204 | case 0x5203: 1205 | this.irq_line = value; 1206 | this.nes.cpu.ClearIRQ(); 1207 | break; 1208 | case 0x5204: 1209 | this.irq_enable = value; 1210 | this.nes.cpu.ClearIRQ(); 1211 | break; 1212 | case 0x5205: 1213 | this.mult_a = value; 1214 | break; 1215 | case 0x5206: 1216 | this.mult_b = value; 1217 | break; 1218 | default: 1219 | if (address >= 0x5000 && address <= 0x5015) { 1220 | this.nes.papu.exWrite(address, value); 1221 | } else if (address >= 0x5c00 && address <= 0x5fff) { 1222 | if (this.graphic_mode === 2) { 1223 | // ExRAM 1224 | // vram write 1225 | } else if (this.graphic_mode !== 3) { 1226 | // Split,ExGraphic 1227 | if (this.irq_status & 0x40) { 1228 | // vram write 1229 | } else { 1230 | // vram write 1231 | } 1232 | } 1233 | } else if (address >= 0x6000 && address <= 0x7fff) { 1234 | if (this.sram_we_a === 2 && this.sram_we_b === 1) { 1235 | // additional ram write 1236 | } 1237 | } 1238 | break; 1239 | } 1240 | }; 1241 | 1242 | Mappers[5].prototype.loadROM = function () { 1243 | if (!this.nes.rom.valid) { 1244 | throw new Error("UNROM: Invalid ROM! Unable to load."); 1245 | } 1246 | 1247 | // Load PRG-ROM: 1248 | this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0x8000); 1249 | this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0xa000); 1250 | this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0xc000); 1251 | this.load8kRomBank(this.nes.rom.romCount * 2 - 1, 0xe000); 1252 | 1253 | // Load CHR-ROM: 1254 | this.loadCHRROM(); 1255 | 1256 | // Do Reset-Interrupt: 1257 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1258 | }; 1259 | 1260 | /** 1261 | * Mapper007 (AxROM) 1262 | * @example Battletoads, Time Lord, Marble Madness 1263 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_007 1264 | * @constructor 1265 | */ 1266 | Mappers[7] = function (nes) { 1267 | this.nes = nes; 1268 | }; 1269 | 1270 | Mappers[7].prototype = new Mappers[0](); 1271 | 1272 | Mappers[7].prototype.write = function (address, value) { 1273 | // Writes to addresses other than MMC registers are handled by NoMapper. 1274 | if (address < 0x8000) { 1275 | Mappers[0].prototype.write.apply(this, arguments); 1276 | } else { 1277 | this.load32kRomBank(value & 0x7, 0x8000); 1278 | if (value & 0x10) { 1279 | this.nes.ppu.setMirroring(this.nes.rom.SINGLESCREEN_MIRRORING2); 1280 | } else { 1281 | this.nes.ppu.setMirroring(this.nes.rom.SINGLESCREEN_MIRRORING); 1282 | } 1283 | } 1284 | }; 1285 | 1286 | Mappers[7].prototype.loadROM = function () { 1287 | if (!this.nes.rom.valid) { 1288 | throw new Error("AOROM: Invalid ROM! Unable to load."); 1289 | } 1290 | 1291 | // Load PRG-ROM: 1292 | this.loadPRGROM(); 1293 | 1294 | // Load CHR-ROM: 1295 | this.loadCHRROM(); 1296 | 1297 | // Do Reset-Interrupt: 1298 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1299 | }; 1300 | 1301 | /** 1302 | * Mapper 011 (Color Dreams) 1303 | * 1304 | * @description http://wiki.nesdev.com/w/index.php/Color_Dreams 1305 | * @example Crystal Mines, Metal Fighter 1306 | * @constructor 1307 | */ 1308 | Mappers[11] = function (nes) { 1309 | this.nes = nes; 1310 | }; 1311 | 1312 | Mappers[11].prototype = new Mappers[0](); 1313 | 1314 | Mappers[11].prototype.write = function (address, value) { 1315 | if (address < 0x8000) { 1316 | Mappers[0].prototype.write.apply(this, arguments); 1317 | return; 1318 | } else { 1319 | // Swap in the given PRG-ROM bank: 1320 | var prgbank1 = ((value & 0xf) * 2) % this.nes.rom.romCount; 1321 | var prgbank2 = ((value & 0xf) * 2 + 1) % this.nes.rom.romCount; 1322 | 1323 | this.loadRomBank(prgbank1, 0x8000); 1324 | this.loadRomBank(prgbank2, 0xc000); 1325 | 1326 | if (this.nes.rom.vromCount > 0) { 1327 | // Swap in the given VROM bank at 0x0000: 1328 | var bank = ((value >> 4) * 2) % this.nes.rom.vromCount; 1329 | this.loadVromBank(bank, 0x0000); 1330 | this.loadVromBank(bank + 1, 0x1000); 1331 | } 1332 | } 1333 | }; 1334 | 1335 | /** 1336 | * Mapper 034 (BNROM, NINA-01) 1337 | * 1338 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_034 1339 | * @example Darkseed, Mashou, Mission Impossible 2 1340 | * @constructor 1341 | */ 1342 | Mappers[34] = function (nes) { 1343 | this.nes = nes; 1344 | }; 1345 | 1346 | Mappers[34].prototype = new Mappers[0](); 1347 | 1348 | Mappers[34].prototype.write = function (address, value) { 1349 | if (address < 0x8000) { 1350 | Mappers[0].prototype.write.apply(this, arguments); 1351 | return; 1352 | } else { 1353 | this.load32kRomBank(value, 0x8000); 1354 | } 1355 | }; 1356 | 1357 | /** 1358 | * Mapper 038 1359 | * 1360 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_038 1361 | * @example Crime Busters 1362 | * @constructor 1363 | */ 1364 | Mappers[38] = function (nes) { 1365 | this.nes = nes; 1366 | }; 1367 | 1368 | Mappers[38].prototype = new Mappers[0](); 1369 | 1370 | Mappers[38].prototype.write = function (address, value) { 1371 | if (address < 0x7000 || address > 0x7fff) { 1372 | Mappers[0].prototype.write.apply(this, arguments); 1373 | return; 1374 | } else { 1375 | // Swap in the given PRG-ROM bank at 0x8000: 1376 | this.load32kRomBank(value & 3, 0x8000); 1377 | 1378 | // Swap in the given VROM bank at 0x0000: 1379 | this.load8kVromBank(((value >> 2) & 3) * 2, 0x0000); 1380 | } 1381 | }; 1382 | 1383 | /** 1384 | * Mapper 066 (GxROM) 1385 | * 1386 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_066 1387 | * @example Doraemon, Dragon Power, Gumshoe, Thunder & Lightning, 1388 | * Super Mario Bros. + Duck Hunt 1389 | * @constructor 1390 | */ 1391 | Mappers[66] = function (nes) { 1392 | this.nes = nes; 1393 | }; 1394 | 1395 | Mappers[66].prototype = new Mappers[0](); 1396 | 1397 | Mappers[66].prototype.write = function (address, value) { 1398 | if (address < 0x8000) { 1399 | Mappers[0].prototype.write.apply(this, arguments); 1400 | return; 1401 | } else { 1402 | // Swap in the given PRG-ROM bank at 0x8000: 1403 | this.load32kRomBank((value >> 4) & 3, 0x8000); 1404 | 1405 | // Swap in the given VROM bank at 0x0000: 1406 | this.load8kVromBank((value & 3) * 2, 0x0000); 1407 | } 1408 | }; 1409 | 1410 | /** 1411 | * Mapper 094 (UN1ROM) 1412 | * 1413 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_094 1414 | * @example Senjou no Ookami 1415 | * @constructor 1416 | */ 1417 | Mappers[94] = function (nes) { 1418 | this.nes = nes; 1419 | }; 1420 | 1421 | Mappers[94].prototype = new Mappers[0](); 1422 | 1423 | Mappers[94].prototype.write = function (address, value) { 1424 | // Writes to addresses other than MMC registers are handled by NoMapper. 1425 | if (address < 0x8000) { 1426 | Mappers[0].prototype.write.apply(this, arguments); 1427 | return; 1428 | } else { 1429 | // This is a ROM bank select command. 1430 | // Swap in the given ROM bank at 0x8000: 1431 | this.loadRomBank(value >> 2, 0x8000); 1432 | } 1433 | }; 1434 | 1435 | Mappers[94].prototype.loadROM = function () { 1436 | if (!this.nes.rom.valid) { 1437 | throw new Error("UN1ROM: Invalid ROM! Unable to load."); 1438 | } 1439 | 1440 | // Load PRG-ROM: 1441 | this.loadRomBank(0, 0x8000); 1442 | this.loadRomBank(this.nes.rom.romCount - 1, 0xc000); 1443 | 1444 | // Load CHR-ROM: 1445 | this.loadCHRROM(); 1446 | 1447 | // Do Reset-Interrupt: 1448 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1449 | }; 1450 | 1451 | /** 1452 | * Mapper 140 1453 | * 1454 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_140 1455 | * @example Bio Senshi Dan - Increaser Tono Tatakai 1456 | * @constructor 1457 | */ 1458 | Mappers[140] = function (nes) { 1459 | this.nes = nes; 1460 | }; 1461 | 1462 | Mappers[140].prototype = new Mappers[0](); 1463 | 1464 | Mappers[140].prototype.write = function (address, value) { 1465 | if (address < 0x6000 || address > 0x7fff) { 1466 | Mappers[0].prototype.write.apply(this, arguments); 1467 | return; 1468 | } else { 1469 | // Swap in the given PRG-ROM bank at 0x8000: 1470 | this.load32kRomBank((value >> 4) & 3, 0x8000); 1471 | 1472 | // Swap in the given VROM bank at 0x0000: 1473 | this.load8kVromBank((value & 0xf) * 2, 0x0000); 1474 | } 1475 | }; 1476 | 1477 | /** 1478 | * Mapper 180 1479 | * 1480 | * @description http://wiki.nesdev.com/w/index.php/INES_Mapper_180 1481 | * @example Crazy Climber 1482 | * @constructor 1483 | */ 1484 | Mappers[180] = function (nes) { 1485 | this.nes = nes; 1486 | }; 1487 | 1488 | Mappers[180].prototype = new Mappers[0](); 1489 | 1490 | Mappers[180].prototype.write = function (address, value) { 1491 | // Writes to addresses other than MMC registers are handled by NoMapper. 1492 | if (address < 0x8000) { 1493 | Mappers[0].prototype.write.apply(this, arguments); 1494 | return; 1495 | } else { 1496 | // This is a ROM bank select command. 1497 | // Swap in the given ROM bank at 0xc000: 1498 | this.loadRomBank(value, 0xc000); 1499 | } 1500 | }; 1501 | 1502 | Mappers[180].prototype.loadROM = function () { 1503 | if (!this.nes.rom.valid) { 1504 | throw new Error("Mapper 180: Invalid ROM! Unable to load."); 1505 | } 1506 | 1507 | // Load PRG-ROM: 1508 | this.loadRomBank(0, 0x8000); 1509 | this.loadRomBank(this.nes.rom.romCount - 1, 0xc000); 1510 | 1511 | // Load CHR-ROM: 1512 | this.loadCHRROM(); 1513 | 1514 | // Do Reset-Interrupt: 1515 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_RESET); 1516 | }; 1517 | 1518 | module.exports = Mappers; 1519 | -------------------------------------------------------------------------------- /src/nes.js: -------------------------------------------------------------------------------- 1 | var CPU = require("./cpu"); 2 | var Controller = require("./controller"); 3 | var PPU = require("./ppu"); 4 | var PAPU = require("./papu"); 5 | var ROM = require("./rom"); 6 | 7 | var NES = function (opts) { 8 | this.opts = { 9 | onFrame: function () {}, 10 | onAudioSample: null, 11 | onStatusUpdate: function () {}, 12 | onBatteryRamWrite: function () {}, 13 | 14 | // FIXME: not actually used except for in PAPU 15 | preferredFrameRate: 60, 16 | 17 | emulateSound: true, 18 | sampleRate: 48000, // Sound sample rate in hz 19 | }; 20 | if (typeof opts !== "undefined") { 21 | var key; 22 | for (key in this.opts) { 23 | if (typeof opts[key] !== "undefined") { 24 | this.opts[key] = opts[key]; 25 | } 26 | } 27 | } 28 | 29 | this.frameTime = 1000 / this.opts.preferredFrameRate; 30 | 31 | this.ui = { 32 | writeFrame: this.opts.onFrame, 33 | updateStatus: this.opts.onStatusUpdate, 34 | }; 35 | this.cpu = new CPU(this); 36 | this.ppu = new PPU(this); 37 | this.papu = new PAPU(this); 38 | this.mmap = null; // set in loadROM() 39 | this.controllers = { 40 | 1: new Controller(), 41 | 2: new Controller(), 42 | }; 43 | 44 | this.ui.updateStatus("Ready to load a ROM."); 45 | 46 | this.frame = this.frame.bind(this); 47 | this.buttonDown = this.buttonDown.bind(this); 48 | this.buttonUp = this.buttonUp.bind(this); 49 | this.zapperMove = this.zapperMove.bind(this); 50 | this.zapperFireDown = this.zapperFireDown.bind(this); 51 | this.zapperFireUp = this.zapperFireUp.bind(this); 52 | }; 53 | 54 | NES.prototype = { 55 | fpsFrameCount: 0, 56 | romData: null, 57 | break: false, 58 | 59 | // Set break to true to stop frame loop. 60 | stop: function () { 61 | this.break = true; 62 | }, 63 | 64 | // Resets the system 65 | reset: function () { 66 | if (this.mmap !== null) { 67 | this.mmap.reset(); 68 | } 69 | 70 | this.cpu.reset(); 71 | this.ppu.reset(); 72 | this.papu.reset(); 73 | 74 | this.lastFpsTime = null; 75 | this.fpsFrameCount = 0; 76 | 77 | this.break = false; 78 | }, 79 | 80 | frame: function () { 81 | this.ppu.startFrame(); 82 | var cycles = 0; 83 | var emulateSound = this.opts.emulateSound; 84 | var cpu = this.cpu; 85 | var ppu = this.ppu; 86 | var papu = this.papu; 87 | FRAMELOOP: for (;;) { 88 | if (this.break) break; 89 | if (cpu.cyclesToHalt === 0) { 90 | // Execute a CPU instruction 91 | cycles = cpu.emulate(); 92 | if (emulateSound) { 93 | papu.clockFrameCounter(cycles); 94 | } 95 | cycles *= 3; 96 | } else { 97 | if (cpu.cyclesToHalt > 8) { 98 | cycles = 24; 99 | if (emulateSound) { 100 | papu.clockFrameCounter(8); 101 | } 102 | cpu.cyclesToHalt -= 8; 103 | } else { 104 | cycles = cpu.cyclesToHalt * 3; 105 | if (emulateSound) { 106 | papu.clockFrameCounter(cpu.cyclesToHalt); 107 | } 108 | cpu.cyclesToHalt = 0; 109 | } 110 | } 111 | 112 | for (; cycles > 0; cycles--) { 113 | if ( 114 | ppu.curX === ppu.spr0HitX && 115 | ppu.f_spVisibility === 1 && 116 | ppu.scanline - 21 === ppu.spr0HitY 117 | ) { 118 | // Set sprite 0 hit flag: 119 | ppu.setStatusFlag(ppu.STATUS_SPRITE0HIT, true); 120 | } 121 | 122 | if (ppu.requestEndFrame) { 123 | ppu.nmiCounter--; 124 | if (ppu.nmiCounter === 0) { 125 | ppu.requestEndFrame = false; 126 | ppu.startVBlank(); 127 | break FRAMELOOP; 128 | } 129 | } 130 | 131 | ppu.curX++; 132 | if (ppu.curX === 341) { 133 | ppu.curX = 0; 134 | ppu.endScanline(); 135 | } 136 | } 137 | } 138 | this.fpsFrameCount++; 139 | }, 140 | 141 | buttonDown: function (controller, button) { 142 | this.controllers[controller].buttonDown(button); 143 | }, 144 | 145 | buttonUp: function (controller, button) { 146 | this.controllers[controller].buttonUp(button); 147 | }, 148 | 149 | zapperMove: function (x, y) { 150 | if (!this.mmap) return; 151 | this.mmap.zapperX = x; 152 | this.mmap.zapperY = y; 153 | }, 154 | 155 | zapperFireDown: function () { 156 | if (!this.mmap) return; 157 | this.mmap.zapperFired = true; 158 | }, 159 | 160 | zapperFireUp: function () { 161 | if (!this.mmap) return; 162 | this.mmap.zapperFired = false; 163 | }, 164 | 165 | getFPS: function () { 166 | var now = +new Date(); 167 | var fps = null; 168 | if (this.lastFpsTime) { 169 | fps = this.fpsFrameCount / ((now - this.lastFpsTime) / 1000); 170 | } 171 | this.fpsFrameCount = 0; 172 | this.lastFpsTime = now; 173 | return fps; 174 | }, 175 | 176 | reloadROM: function () { 177 | if (this.romData !== null) { 178 | this.loadROM(this.romData); 179 | } 180 | }, 181 | 182 | // Loads a ROM file into the CPU and PPU. 183 | // The ROM file is validated first. 184 | loadROM: function (data) { 185 | // Load ROM file: 186 | this.rom = new ROM(this); 187 | this.rom.load(data); 188 | 189 | this.reset(); 190 | this.mmap = this.rom.createMapper(); 191 | this.mmap.loadROM(); 192 | this.ppu.setMirroring(this.rom.getMirroringType()); 193 | this.romData = data; 194 | }, 195 | 196 | setFramerate: function (rate) { 197 | this.opts.preferredFrameRate = rate; 198 | this.frameTime = 1000 / rate; 199 | this.papu.setSampleRate(this.opts.sampleRate, false); 200 | }, 201 | 202 | toJSON: function () { 203 | return { 204 | // romData: this.romData, 205 | cpu: this.cpu.toJSON(), 206 | mmap: this.mmap.toJSON(), 207 | ppu: this.ppu.toJSON(), 208 | papu: this.papu.toJSON(), 209 | }; 210 | }, 211 | 212 | fromJSON: function (s) { 213 | this.reset(); 214 | // this.romData = s.romData; 215 | this.cpu.fromJSON(s.cpu); 216 | this.mmap.fromJSON(s.mmap); 217 | this.ppu.fromJSON(s.ppu); 218 | this.papu.fromJSON(s.papu); 219 | }, 220 | }; 221 | 222 | module.exports = NES; 223 | -------------------------------------------------------------------------------- /src/papu.js: -------------------------------------------------------------------------------- 1 | var utils = require("./utils"); 2 | 3 | var CPU_FREQ_NTSC = 1789772.5; //1789772.72727272d; 4 | // var CPU_FREQ_PAL = 1773447.4; 5 | 6 | var PAPU = function (nes) { 7 | this.nes = nes; 8 | 9 | this.square1 = new ChannelSquare(this, true); 10 | this.square2 = new ChannelSquare(this, false); 11 | this.triangle = new ChannelTriangle(this); 12 | this.noise = new ChannelNoise(this); 13 | this.dmc = new ChannelDM(this); 14 | 15 | this.frameIrqCounter = null; 16 | this.frameIrqCounterMax = 4; 17 | this.initCounter = 2048; 18 | this.channelEnableValue = null; 19 | 20 | this.sampleRate = 44100; 21 | 22 | this.lengthLookup = null; 23 | this.dmcFreqLookup = null; 24 | this.noiseWavelengthLookup = null; 25 | this.square_table = null; 26 | this.tnd_table = null; 27 | 28 | this.frameIrqEnabled = false; 29 | this.frameIrqActive = null; 30 | this.frameClockNow = null; 31 | this.startedPlaying = false; 32 | this.recordOutput = false; 33 | this.initingHardware = false; 34 | 35 | this.masterFrameCounter = null; 36 | this.derivedFrameCounter = null; 37 | this.countSequence = null; 38 | this.sampleTimer = null; 39 | this.frameTime = null; 40 | this.sampleTimerMax = null; 41 | this.sampleCount = null; 42 | this.triValue = 0; 43 | 44 | this.smpSquare1 = null; 45 | this.smpSquare2 = null; 46 | this.smpTriangle = null; 47 | this.smpDmc = null; 48 | this.accCount = null; 49 | 50 | // DC removal vars: 51 | this.prevSampleL = 0; 52 | this.prevSampleR = 0; 53 | this.smpAccumL = 0; 54 | this.smpAccumR = 0; 55 | 56 | // DAC range: 57 | this.dacRange = 0; 58 | this.dcValue = 0; 59 | 60 | // Master volume: 61 | this.masterVolume = 256; 62 | 63 | // Stereo positioning: 64 | this.stereoPosLSquare1 = null; 65 | this.stereoPosLSquare2 = null; 66 | this.stereoPosLTriangle = null; 67 | this.stereoPosLNoise = null; 68 | this.stereoPosLDMC = null; 69 | this.stereoPosRSquare1 = null; 70 | this.stereoPosRSquare2 = null; 71 | this.stereoPosRTriangle = null; 72 | this.stereoPosRNoise = null; 73 | this.stereoPosRDMC = null; 74 | 75 | this.extraCycles = null; 76 | 77 | this.maxSample = null; 78 | this.minSample = null; 79 | 80 | // Panning: 81 | this.panning = [80, 170, 100, 150, 128]; 82 | this.setPanning(this.panning); 83 | 84 | // Initialize lookup tables: 85 | this.initLengthLookup(); 86 | this.initDmcFrequencyLookup(); 87 | this.initNoiseWavelengthLookup(); 88 | this.initDACtables(); 89 | 90 | // Init sound registers: 91 | for (var i = 0; i < 0x14; i++) { 92 | if (i === 0x10) { 93 | this.writeReg(0x4010, 0x10); 94 | } else { 95 | this.writeReg(0x4000 + i, 0); 96 | } 97 | } 98 | 99 | this.reset(); 100 | }; 101 | 102 | PAPU.prototype = { 103 | reset: function () { 104 | this.sampleRate = this.nes.opts.sampleRate; 105 | this.sampleTimerMax = Math.floor( 106 | (1024.0 * CPU_FREQ_NTSC * this.nes.opts.preferredFrameRate) / 107 | (this.sampleRate * 60.0) 108 | ); 109 | 110 | this.frameTime = Math.floor( 111 | (14915.0 * this.nes.opts.preferredFrameRate) / 60.0 112 | ); 113 | 114 | this.sampleTimer = 0; 115 | 116 | this.updateChannelEnable(0); 117 | this.masterFrameCounter = 0; 118 | this.derivedFrameCounter = 0; 119 | this.countSequence = 0; 120 | this.sampleCount = 0; 121 | this.initCounter = 2048; 122 | this.frameIrqEnabled = false; 123 | this.initingHardware = false; 124 | 125 | this.resetCounter(); 126 | 127 | this.square1.reset(); 128 | this.square2.reset(); 129 | this.triangle.reset(); 130 | this.noise.reset(); 131 | this.dmc.reset(); 132 | 133 | this.accCount = 0; 134 | this.smpSquare1 = 0; 135 | this.smpSquare2 = 0; 136 | this.smpTriangle = 0; 137 | this.smpDmc = 0; 138 | 139 | this.frameIrqEnabled = false; 140 | this.frameIrqCounterMax = 4; 141 | 142 | this.channelEnableValue = 0xff; 143 | this.startedPlaying = false; 144 | this.prevSampleL = 0; 145 | this.prevSampleR = 0; 146 | this.smpAccumL = 0; 147 | this.smpAccumR = 0; 148 | 149 | this.maxSample = -500000; 150 | this.minSample = 500000; 151 | }, 152 | 153 | // eslint-disable-next-line no-unused-vars 154 | readReg: function (address) { 155 | // Read 0x4015: 156 | var tmp = 0; 157 | tmp |= this.square1.getLengthStatus(); 158 | tmp |= this.square2.getLengthStatus() << 1; 159 | tmp |= this.triangle.getLengthStatus() << 2; 160 | tmp |= this.noise.getLengthStatus() << 3; 161 | tmp |= this.dmc.getLengthStatus() << 4; 162 | tmp |= (this.frameIrqActive && this.frameIrqEnabled ? 1 : 0) << 6; 163 | tmp |= this.dmc.getIrqStatus() << 7; 164 | 165 | this.frameIrqActive = false; 166 | this.dmc.irqGenerated = false; 167 | 168 | return tmp & 0xffff; 169 | }, 170 | 171 | writeReg: function (address, value) { 172 | if (address >= 0x4000 && address < 0x4004) { 173 | // Square Wave 1 Control 174 | this.square1.writeReg(address, value); 175 | // console.log("Square Write"); 176 | } else if (address >= 0x4004 && address < 0x4008) { 177 | // Square 2 Control 178 | this.square2.writeReg(address, value); 179 | } else if (address >= 0x4008 && address < 0x400c) { 180 | // Triangle Control 181 | this.triangle.writeReg(address, value); 182 | } else if (address >= 0x400c && address <= 0x400f) { 183 | // Noise Control 184 | this.noise.writeReg(address, value); 185 | } else if (address === 0x4010) { 186 | // DMC Play mode & DMA frequency 187 | this.dmc.writeReg(address, value); 188 | } else if (address === 0x4011) { 189 | // DMC Delta Counter 190 | this.dmc.writeReg(address, value); 191 | } else if (address === 0x4012) { 192 | // DMC Play code starting address 193 | this.dmc.writeReg(address, value); 194 | } else if (address === 0x4013) { 195 | // DMC Play code length 196 | this.dmc.writeReg(address, value); 197 | } else if (address === 0x4015) { 198 | // Channel enable 199 | this.updateChannelEnable(value); 200 | 201 | if (value !== 0 && this.initCounter > 0) { 202 | // Start hardware initialization 203 | this.initingHardware = true; 204 | } 205 | 206 | // DMC/IRQ Status 207 | this.dmc.writeReg(address, value); 208 | } else if (address === 0x4017) { 209 | // Frame counter control 210 | this.countSequence = (value >> 7) & 1; 211 | this.masterFrameCounter = 0; 212 | this.frameIrqActive = false; 213 | 214 | if (((value >> 6) & 0x1) === 0) { 215 | this.frameIrqEnabled = true; 216 | } else { 217 | this.frameIrqEnabled = false; 218 | } 219 | 220 | if (this.countSequence === 0) { 221 | // NTSC: 222 | this.frameIrqCounterMax = 4; 223 | this.derivedFrameCounter = 4; 224 | } else { 225 | // PAL: 226 | this.frameIrqCounterMax = 5; 227 | this.derivedFrameCounter = 0; 228 | this.frameCounterTick(); 229 | } 230 | } 231 | }, 232 | 233 | resetCounter: function () { 234 | if (this.countSequence === 0) { 235 | this.derivedFrameCounter = 4; 236 | } else { 237 | this.derivedFrameCounter = 0; 238 | } 239 | }, 240 | 241 | // Updates channel enable status. 242 | // This is done on writes to the 243 | // channel enable register (0x4015), 244 | // and when the user enables/disables channels 245 | // in the GUI. 246 | updateChannelEnable: function (value) { 247 | this.channelEnableValue = value & 0xffff; 248 | this.square1.setEnabled((value & 1) !== 0); 249 | this.square2.setEnabled((value & 2) !== 0); 250 | this.triangle.setEnabled((value & 4) !== 0); 251 | this.noise.setEnabled((value & 8) !== 0); 252 | this.dmc.setEnabled((value & 16) !== 0); 253 | }, 254 | 255 | // Clocks the frame counter. It should be clocked at 256 | // twice the cpu speed, so the cycles will be 257 | // divided by 2 for those counters that are 258 | // clocked at cpu speed. 259 | clockFrameCounter: function (nCycles) { 260 | if (this.initCounter > 0) { 261 | if (this.initingHardware) { 262 | this.initCounter -= nCycles; 263 | if (this.initCounter <= 0) { 264 | this.initingHardware = false; 265 | } 266 | return; 267 | } 268 | } 269 | 270 | // Don't process ticks beyond next sampling: 271 | nCycles += this.extraCycles; 272 | var maxCycles = this.sampleTimerMax - this.sampleTimer; 273 | if (nCycles << 10 > maxCycles) { 274 | this.extraCycles = ((nCycles << 10) - maxCycles) >> 10; 275 | nCycles -= this.extraCycles; 276 | } else { 277 | this.extraCycles = 0; 278 | } 279 | 280 | var dmc = this.dmc; 281 | var triangle = this.triangle; 282 | var square1 = this.square1; 283 | var square2 = this.square2; 284 | var noise = this.noise; 285 | 286 | // Clock DMC: 287 | if (dmc.isEnabled) { 288 | dmc.shiftCounter -= nCycles << 3; 289 | while (dmc.shiftCounter <= 0 && dmc.dmaFrequency > 0) { 290 | dmc.shiftCounter += dmc.dmaFrequency; 291 | dmc.clockDmc(); 292 | } 293 | } 294 | 295 | // Clock Triangle channel Prog timer: 296 | if (triangle.progTimerMax > 0) { 297 | triangle.progTimerCount -= nCycles; 298 | while (triangle.progTimerCount <= 0) { 299 | triangle.progTimerCount += triangle.progTimerMax + 1; 300 | if (triangle.linearCounter > 0 && triangle.lengthCounter > 0) { 301 | triangle.triangleCounter++; 302 | triangle.triangleCounter &= 0x1f; 303 | 304 | if (triangle.isEnabled) { 305 | if (triangle.triangleCounter >= 0x10) { 306 | // Normal value. 307 | triangle.sampleValue = triangle.triangleCounter & 0xf; 308 | } else { 309 | // Inverted value. 310 | triangle.sampleValue = 0xf - (triangle.triangleCounter & 0xf); 311 | } 312 | triangle.sampleValue <<= 4; 313 | } 314 | } 315 | } 316 | } 317 | 318 | // Clock Square channel 1 Prog timer: 319 | square1.progTimerCount -= nCycles; 320 | if (square1.progTimerCount <= 0) { 321 | square1.progTimerCount += (square1.progTimerMax + 1) << 1; 322 | 323 | square1.squareCounter++; 324 | square1.squareCounter &= 0x7; 325 | square1.updateSampleValue(); 326 | } 327 | 328 | // Clock Square channel 2 Prog timer: 329 | square2.progTimerCount -= nCycles; 330 | if (square2.progTimerCount <= 0) { 331 | square2.progTimerCount += (square2.progTimerMax + 1) << 1; 332 | 333 | square2.squareCounter++; 334 | square2.squareCounter &= 0x7; 335 | square2.updateSampleValue(); 336 | } 337 | 338 | // Clock noise channel Prog timer: 339 | var acc_c = nCycles; 340 | if (noise.progTimerCount - acc_c > 0) { 341 | // Do all cycles at once: 342 | noise.progTimerCount -= acc_c; 343 | noise.accCount += acc_c; 344 | noise.accValue += acc_c * noise.sampleValue; 345 | } else { 346 | // Slow-step: 347 | while (acc_c-- > 0) { 348 | if (--noise.progTimerCount <= 0 && noise.progTimerMax > 0) { 349 | // Update noise shift register: 350 | noise.shiftReg <<= 1; 351 | noise.tmp = 352 | ((noise.shiftReg << (noise.randomMode === 0 ? 1 : 6)) ^ 353 | noise.shiftReg) & 354 | 0x8000; 355 | if (noise.tmp !== 0) { 356 | // Sample value must be 0. 357 | noise.shiftReg |= 0x01; 358 | noise.randomBit = 0; 359 | noise.sampleValue = 0; 360 | } else { 361 | // Find sample value: 362 | noise.randomBit = 1; 363 | if (noise.isEnabled && noise.lengthCounter > 0) { 364 | noise.sampleValue = noise.masterVolume; 365 | } else { 366 | noise.sampleValue = 0; 367 | } 368 | } 369 | 370 | noise.progTimerCount += noise.progTimerMax; 371 | } 372 | 373 | noise.accValue += noise.sampleValue; 374 | noise.accCount++; 375 | } 376 | } 377 | 378 | // Frame IRQ handling: 379 | if (this.frameIrqEnabled && this.frameIrqActive) { 380 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NORMAL); 381 | } 382 | 383 | // Clock frame counter at double CPU speed: 384 | this.masterFrameCounter += nCycles << 1; 385 | if (this.masterFrameCounter >= this.frameTime) { 386 | // 240Hz tick: 387 | this.masterFrameCounter -= this.frameTime; 388 | this.frameCounterTick(); 389 | } 390 | 391 | // Accumulate sample value: 392 | this.accSample(nCycles); 393 | 394 | // Clock sample timer: 395 | this.sampleTimer += nCycles << 10; 396 | if (this.sampleTimer >= this.sampleTimerMax) { 397 | // Sample channels: 398 | this.sample(); 399 | this.sampleTimer -= this.sampleTimerMax; 400 | } 401 | }, 402 | 403 | accSample: function (cycles) { 404 | // Special treatment for triangle channel - need to interpolate. 405 | if (this.triangle.sampleCondition) { 406 | this.triValue = Math.floor( 407 | (this.triangle.progTimerCount << 4) / (this.triangle.progTimerMax + 1) 408 | ); 409 | if (this.triValue > 16) { 410 | this.triValue = 16; 411 | } 412 | if (this.triangle.triangleCounter >= 16) { 413 | this.triValue = 16 - this.triValue; 414 | } 415 | 416 | // Add non-interpolated sample value: 417 | this.triValue += this.triangle.sampleValue; 418 | } 419 | 420 | // Now sample normally: 421 | if (cycles === 2) { 422 | this.smpTriangle += this.triValue << 1; 423 | this.smpDmc += this.dmc.sample << 1; 424 | this.smpSquare1 += this.square1.sampleValue << 1; 425 | this.smpSquare2 += this.square2.sampleValue << 1; 426 | this.accCount += 2; 427 | } else if (cycles === 4) { 428 | this.smpTriangle += this.triValue << 2; 429 | this.smpDmc += this.dmc.sample << 2; 430 | this.smpSquare1 += this.square1.sampleValue << 2; 431 | this.smpSquare2 += this.square2.sampleValue << 2; 432 | this.accCount += 4; 433 | } else { 434 | this.smpTriangle += cycles * this.triValue; 435 | this.smpDmc += cycles * this.dmc.sample; 436 | this.smpSquare1 += cycles * this.square1.sampleValue; 437 | this.smpSquare2 += cycles * this.square2.sampleValue; 438 | this.accCount += cycles; 439 | } 440 | }, 441 | 442 | frameCounterTick: function () { 443 | this.derivedFrameCounter++; 444 | if (this.derivedFrameCounter >= this.frameIrqCounterMax) { 445 | this.derivedFrameCounter = 0; 446 | } 447 | 448 | if (this.derivedFrameCounter === 1 || this.derivedFrameCounter === 3) { 449 | // Clock length & sweep: 450 | this.triangle.clockLengthCounter(); 451 | this.square1.clockLengthCounter(); 452 | this.square2.clockLengthCounter(); 453 | this.noise.clockLengthCounter(); 454 | this.square1.clockSweep(); 455 | this.square2.clockSweep(); 456 | } 457 | 458 | if (this.derivedFrameCounter >= 0 && this.derivedFrameCounter < 4) { 459 | // Clock linear & decay: 460 | this.square1.clockEnvDecay(); 461 | this.square2.clockEnvDecay(); 462 | this.noise.clockEnvDecay(); 463 | this.triangle.clockLinearCounter(); 464 | } 465 | 466 | if (this.derivedFrameCounter === 3 && this.countSequence === 0) { 467 | // Enable IRQ: 468 | this.frameIrqActive = true; 469 | } 470 | 471 | // End of 240Hz tick 472 | }, 473 | 474 | // Samples the channels, mixes the output together, then writes to buffer. 475 | sample: function () { 476 | var sq_index, tnd_index; 477 | 478 | if (this.accCount > 0) { 479 | this.smpSquare1 <<= 4; 480 | this.smpSquare1 = Math.floor(this.smpSquare1 / this.accCount); 481 | 482 | this.smpSquare2 <<= 4; 483 | this.smpSquare2 = Math.floor(this.smpSquare2 / this.accCount); 484 | 485 | this.smpTriangle = Math.floor(this.smpTriangle / this.accCount); 486 | 487 | this.smpDmc <<= 4; 488 | this.smpDmc = Math.floor(this.smpDmc / this.accCount); 489 | 490 | this.accCount = 0; 491 | } else { 492 | this.smpSquare1 = this.square1.sampleValue << 4; 493 | this.smpSquare2 = this.square2.sampleValue << 4; 494 | this.smpTriangle = this.triangle.sampleValue; 495 | this.smpDmc = this.dmc.sample << 4; 496 | } 497 | 498 | var smpNoise = Math.floor((this.noise.accValue << 4) / this.noise.accCount); 499 | this.noise.accValue = smpNoise >> 4; 500 | this.noise.accCount = 1; 501 | 502 | // Stereo sound. 503 | 504 | // Left channel: 505 | sq_index = 506 | (this.smpSquare1 * this.stereoPosLSquare1 + 507 | this.smpSquare2 * this.stereoPosLSquare2) >> 508 | 8; 509 | tnd_index = 510 | (3 * this.smpTriangle * this.stereoPosLTriangle + 511 | (smpNoise << 1) * this.stereoPosLNoise + 512 | this.smpDmc * this.stereoPosLDMC) >> 513 | 8; 514 | if (sq_index >= this.square_table.length) { 515 | sq_index = this.square_table.length - 1; 516 | } 517 | if (tnd_index >= this.tnd_table.length) { 518 | tnd_index = this.tnd_table.length - 1; 519 | } 520 | var sampleValueL = 521 | this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue; 522 | 523 | // Right channel: 524 | sq_index = 525 | (this.smpSquare1 * this.stereoPosRSquare1 + 526 | this.smpSquare2 * this.stereoPosRSquare2) >> 527 | 8; 528 | tnd_index = 529 | (3 * this.smpTriangle * this.stereoPosRTriangle + 530 | (smpNoise << 1) * this.stereoPosRNoise + 531 | this.smpDmc * this.stereoPosRDMC) >> 532 | 8; 533 | if (sq_index >= this.square_table.length) { 534 | sq_index = this.square_table.length - 1; 535 | } 536 | if (tnd_index >= this.tnd_table.length) { 537 | tnd_index = this.tnd_table.length - 1; 538 | } 539 | var sampleValueR = 540 | this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue; 541 | 542 | // Remove DC from left channel: 543 | var smpDiffL = sampleValueL - this.prevSampleL; 544 | this.prevSampleL += smpDiffL; 545 | this.smpAccumL += smpDiffL - (this.smpAccumL >> 10); 546 | sampleValueL = this.smpAccumL; 547 | 548 | // Remove DC from right channel: 549 | var smpDiffR = sampleValueR - this.prevSampleR; 550 | this.prevSampleR += smpDiffR; 551 | this.smpAccumR += smpDiffR - (this.smpAccumR >> 10); 552 | sampleValueR = this.smpAccumR; 553 | 554 | // Write: 555 | if (sampleValueL > this.maxSample) { 556 | this.maxSample = sampleValueL; 557 | } 558 | if (sampleValueL < this.minSample) { 559 | this.minSample = sampleValueL; 560 | } 561 | 562 | if (this.nes.opts.onAudioSample) { 563 | this.nes.opts.onAudioSample(sampleValueL / 32768, sampleValueR / 32768); 564 | } 565 | 566 | // Reset sampled values: 567 | this.smpSquare1 = 0; 568 | this.smpSquare2 = 0; 569 | this.smpTriangle = 0; 570 | this.smpDmc = 0; 571 | }, 572 | 573 | getLengthMax: function (value) { 574 | return this.lengthLookup[value >> 3]; 575 | }, 576 | 577 | getDmcFrequency: function (value) { 578 | if (value >= 0 && value < 0x10) { 579 | return this.dmcFreqLookup[value]; 580 | } 581 | return 0; 582 | }, 583 | 584 | getNoiseWaveLength: function (value) { 585 | if (value >= 0 && value < 0x10) { 586 | return this.noiseWavelengthLookup[value]; 587 | } 588 | return 0; 589 | }, 590 | 591 | setPanning: function (pos) { 592 | for (var i = 0; i < 5; i++) { 593 | this.panning[i] = pos[i]; 594 | } 595 | this.updateStereoPos(); 596 | }, 597 | 598 | setMasterVolume: function (value) { 599 | if (value < 0) { 600 | value = 0; 601 | } 602 | if (value > 256) { 603 | value = 256; 604 | } 605 | this.masterVolume = value; 606 | this.updateStereoPos(); 607 | }, 608 | 609 | updateStereoPos: function () { 610 | this.stereoPosLSquare1 = (this.panning[0] * this.masterVolume) >> 8; 611 | this.stereoPosLSquare2 = (this.panning[1] * this.masterVolume) >> 8; 612 | this.stereoPosLTriangle = (this.panning[2] * this.masterVolume) >> 8; 613 | this.stereoPosLNoise = (this.panning[3] * this.masterVolume) >> 8; 614 | this.stereoPosLDMC = (this.panning[4] * this.masterVolume) >> 8; 615 | 616 | this.stereoPosRSquare1 = this.masterVolume - this.stereoPosLSquare1; 617 | this.stereoPosRSquare2 = this.masterVolume - this.stereoPosLSquare2; 618 | this.stereoPosRTriangle = this.masterVolume - this.stereoPosLTriangle; 619 | this.stereoPosRNoise = this.masterVolume - this.stereoPosLNoise; 620 | this.stereoPosRDMC = this.masterVolume - this.stereoPosLDMC; 621 | }, 622 | 623 | initLengthLookup: function () { 624 | // prettier-ignore 625 | this.lengthLookup = [ 626 | 0x0A, 0xFE, 627 | 0x14, 0x02, 628 | 0x28, 0x04, 629 | 0x50, 0x06, 630 | 0xA0, 0x08, 631 | 0x3C, 0x0A, 632 | 0x0E, 0x0C, 633 | 0x1A, 0x0E, 634 | 0x0C, 0x10, 635 | 0x18, 0x12, 636 | 0x30, 0x14, 637 | 0x60, 0x16, 638 | 0xC0, 0x18, 639 | 0x48, 0x1A, 640 | 0x10, 0x1C, 641 | 0x20, 0x1E 642 | ]; 643 | }, 644 | 645 | initDmcFrequencyLookup: function () { 646 | this.dmcFreqLookup = new Array(16); 647 | 648 | this.dmcFreqLookup[0x0] = 0xd60; 649 | this.dmcFreqLookup[0x1] = 0xbe0; 650 | this.dmcFreqLookup[0x2] = 0xaa0; 651 | this.dmcFreqLookup[0x3] = 0xa00; 652 | this.dmcFreqLookup[0x4] = 0x8f0; 653 | this.dmcFreqLookup[0x5] = 0x7f0; 654 | this.dmcFreqLookup[0x6] = 0x710; 655 | this.dmcFreqLookup[0x7] = 0x6b0; 656 | this.dmcFreqLookup[0x8] = 0x5f0; 657 | this.dmcFreqLookup[0x9] = 0x500; 658 | this.dmcFreqLookup[0xa] = 0x470; 659 | this.dmcFreqLookup[0xb] = 0x400; 660 | this.dmcFreqLookup[0xc] = 0x350; 661 | this.dmcFreqLookup[0xd] = 0x2a0; 662 | this.dmcFreqLookup[0xe] = 0x240; 663 | this.dmcFreqLookup[0xf] = 0x1b0; 664 | //for(int i=0;i<16;i++)dmcFreqLookup[i]/=8; 665 | }, 666 | 667 | initNoiseWavelengthLookup: function () { 668 | this.noiseWavelengthLookup = new Array(16); 669 | 670 | this.noiseWavelengthLookup[0x0] = 0x004; 671 | this.noiseWavelengthLookup[0x1] = 0x008; 672 | this.noiseWavelengthLookup[0x2] = 0x010; 673 | this.noiseWavelengthLookup[0x3] = 0x020; 674 | this.noiseWavelengthLookup[0x4] = 0x040; 675 | this.noiseWavelengthLookup[0x5] = 0x060; 676 | this.noiseWavelengthLookup[0x6] = 0x080; 677 | this.noiseWavelengthLookup[0x7] = 0x0a0; 678 | this.noiseWavelengthLookup[0x8] = 0x0ca; 679 | this.noiseWavelengthLookup[0x9] = 0x0fe; 680 | this.noiseWavelengthLookup[0xa] = 0x17c; 681 | this.noiseWavelengthLookup[0xb] = 0x1fc; 682 | this.noiseWavelengthLookup[0xc] = 0x2fa; 683 | this.noiseWavelengthLookup[0xd] = 0x3f8; 684 | this.noiseWavelengthLookup[0xe] = 0x7f2; 685 | this.noiseWavelengthLookup[0xf] = 0xfe4; 686 | }, 687 | 688 | initDACtables: function () { 689 | var value, ival, i; 690 | var max_sqr = 0; 691 | var max_tnd = 0; 692 | 693 | this.square_table = new Array(32 * 16); 694 | this.tnd_table = new Array(204 * 16); 695 | 696 | for (i = 0; i < 32 * 16; i++) { 697 | value = 95.52 / (8128.0 / (i / 16.0) + 100.0); 698 | value *= 0.98411; 699 | value *= 50000.0; 700 | ival = Math.floor(value); 701 | 702 | this.square_table[i] = ival; 703 | if (ival > max_sqr) { 704 | max_sqr = ival; 705 | } 706 | } 707 | 708 | for (i = 0; i < 204 * 16; i++) { 709 | value = 163.67 / (24329.0 / (i / 16.0) + 100.0); 710 | value *= 0.98411; 711 | value *= 50000.0; 712 | ival = Math.floor(value); 713 | 714 | this.tnd_table[i] = ival; 715 | if (ival > max_tnd) { 716 | max_tnd = ival; 717 | } 718 | } 719 | 720 | this.dacRange = max_sqr + max_tnd; 721 | this.dcValue = this.dacRange / 2; 722 | }, 723 | 724 | JSON_PROPERTIES: [ 725 | "frameIrqCounter", 726 | "frameIrqCounterMax", 727 | "initCounter", 728 | "channelEnableValue", 729 | "sampleRate", 730 | "frameIrqEnabled", 731 | "frameIrqActive", 732 | "frameClockNow", 733 | "startedPlaying", 734 | "recordOutput", 735 | "initingHardware", 736 | "masterFrameCounter", 737 | "derivedFrameCounter", 738 | "countSequence", 739 | "sampleTimer", 740 | "frameTime", 741 | "sampleTimerMax", 742 | "sampleCount", 743 | "triValue", 744 | "smpSquare1", 745 | "smpSquare2", 746 | "smpTriangle", 747 | "smpDmc", 748 | "accCount", 749 | "prevSampleL", 750 | "prevSampleR", 751 | "smpAccumL", 752 | "smpAccumR", 753 | "masterVolume", 754 | "stereoPosLSquare1", 755 | "stereoPosLSquare2", 756 | "stereoPosLTriangle", 757 | "stereoPosLNoise", 758 | "stereoPosLDMC", 759 | "stereoPosRSquare1", 760 | "stereoPosRSquare2", 761 | "stereoPosRTriangle", 762 | "stereoPosRNoise", 763 | "stereoPosRDMC", 764 | "extraCycles", 765 | "maxSample", 766 | "minSample", 767 | "panning", 768 | ], 769 | 770 | toJSON: function () { 771 | let obj = utils.toJSON(this); 772 | obj.dmc = this.dmc.toJSON(); 773 | obj.noise = this.noise.toJSON(); 774 | obj.square1 = this.square1.toJSON(); 775 | obj.square2 = this.square2.toJSON(); 776 | obj.triangle = this.triangle.toJSON(); 777 | return obj; 778 | }, 779 | 780 | fromJSON: function (s) { 781 | utils.fromJSON(this, s); 782 | this.dmc.fromJSON(s.dmc); 783 | this.noise.fromJSON(s.noise); 784 | this.square1.fromJSON(s.square1); 785 | this.square2.fromJSON(s.square2); 786 | this.triangle.fromJSON(s.triangle); 787 | }, 788 | }; 789 | 790 | var ChannelDM = function (papu) { 791 | this.papu = papu; 792 | 793 | this.MODE_NORMAL = 0; 794 | this.MODE_LOOP = 1; 795 | this.MODE_IRQ = 2; 796 | 797 | this.isEnabled = null; 798 | this.hasSample = null; 799 | this.irqGenerated = false; 800 | 801 | this.playMode = null; 802 | this.dmaFrequency = null; 803 | this.dmaCounter = null; 804 | this.deltaCounter = null; 805 | this.playStartAddress = null; 806 | this.playAddress = null; 807 | this.playLength = null; 808 | this.playLengthCounter = null; 809 | this.shiftCounter = null; 810 | this.reg4012 = null; 811 | this.reg4013 = null; 812 | this.sample = null; 813 | this.dacLsb = null; 814 | this.data = null; 815 | 816 | this.reset(); 817 | }; 818 | 819 | ChannelDM.prototype = { 820 | clockDmc: function () { 821 | // Only alter DAC value if the sample buffer has data: 822 | if (this.hasSample) { 823 | if ((this.data & 1) === 0) { 824 | // Decrement delta: 825 | if (this.deltaCounter > 0) { 826 | this.deltaCounter--; 827 | } 828 | } else { 829 | // Increment delta: 830 | if (this.deltaCounter < 63) { 831 | this.deltaCounter++; 832 | } 833 | } 834 | 835 | // Update sample value: 836 | this.sample = this.isEnabled ? (this.deltaCounter << 1) + this.dacLsb : 0; 837 | 838 | // Update shift register: 839 | this.data >>= 1; 840 | } 841 | 842 | this.dmaCounter--; 843 | if (this.dmaCounter <= 0) { 844 | // No more sample bits. 845 | this.hasSample = false; 846 | this.endOfSample(); 847 | this.dmaCounter = 8; 848 | } 849 | 850 | if (this.irqGenerated) { 851 | this.papu.nes.cpu.requestIrq(this.papu.nes.cpu.IRQ_NORMAL); 852 | } 853 | }, 854 | 855 | endOfSample: function () { 856 | if (this.playLengthCounter === 0 && this.playMode === this.MODE_LOOP) { 857 | // Start from beginning of sample: 858 | this.playAddress = this.playStartAddress; 859 | this.playLengthCounter = this.playLength; 860 | } 861 | 862 | if (this.playLengthCounter > 0) { 863 | // Fetch next sample: 864 | this.nextSample(); 865 | 866 | if (this.playLengthCounter === 0) { 867 | // Last byte of sample fetched, generate IRQ: 868 | if (this.playMode === this.MODE_IRQ) { 869 | // Generate IRQ: 870 | this.irqGenerated = true; 871 | } 872 | } 873 | } 874 | }, 875 | 876 | nextSample: function () { 877 | // Fetch byte: 878 | this.data = this.papu.nes.mmap.load(this.playAddress); 879 | this.papu.nes.cpu.haltCycles(4); 880 | 881 | this.playLengthCounter--; 882 | this.playAddress++; 883 | if (this.playAddress > 0xffff) { 884 | this.playAddress = 0x8000; 885 | } 886 | 887 | this.hasSample = true; 888 | }, 889 | 890 | writeReg: function (address, value) { 891 | if (address === 0x4010) { 892 | // Play mode, DMA Frequency 893 | if (value >> 6 === 0) { 894 | this.playMode = this.MODE_NORMAL; 895 | } else if (((value >> 6) & 1) === 1) { 896 | this.playMode = this.MODE_LOOP; 897 | } else if (value >> 6 === 2) { 898 | this.playMode = this.MODE_IRQ; 899 | } 900 | 901 | if ((value & 0x80) === 0) { 902 | this.irqGenerated = false; 903 | } 904 | 905 | this.dmaFrequency = this.papu.getDmcFrequency(value & 0xf); 906 | } else if (address === 0x4011) { 907 | // Delta counter load register: 908 | this.deltaCounter = (value >> 1) & 63; 909 | this.dacLsb = value & 1; 910 | this.sample = (this.deltaCounter << 1) + this.dacLsb; // update sample value 911 | } else if (address === 0x4012) { 912 | // DMA address load register 913 | this.playStartAddress = (value << 6) | 0x0c000; 914 | this.playAddress = this.playStartAddress; 915 | this.reg4012 = value; 916 | } else if (address === 0x4013) { 917 | // Length of play code 918 | this.playLength = (value << 4) + 1; 919 | this.playLengthCounter = this.playLength; 920 | this.reg4013 = value; 921 | } else if (address === 0x4015) { 922 | // DMC/IRQ Status 923 | if (((value >> 4) & 1) === 0) { 924 | // Disable: 925 | this.playLengthCounter = 0; 926 | } else { 927 | // Restart: 928 | this.playAddress = this.playStartAddress; 929 | this.playLengthCounter = this.playLength; 930 | } 931 | this.irqGenerated = false; 932 | } 933 | }, 934 | 935 | setEnabled: function (value) { 936 | if (!this.isEnabled && value) { 937 | this.playLengthCounter = this.playLength; 938 | } 939 | this.isEnabled = value; 940 | }, 941 | 942 | getLengthStatus: function () { 943 | return this.playLengthCounter === 0 || !this.isEnabled ? 0 : 1; 944 | }, 945 | 946 | getIrqStatus: function () { 947 | return this.irqGenerated ? 1 : 0; 948 | }, 949 | 950 | reset: function () { 951 | this.isEnabled = false; 952 | this.irqGenerated = false; 953 | this.playMode = this.MODE_NORMAL; 954 | this.dmaFrequency = 0; 955 | this.dmaCounter = 0; 956 | this.deltaCounter = 0; 957 | this.playStartAddress = 0; 958 | this.playAddress = 0; 959 | this.playLength = 0; 960 | this.playLengthCounter = 0; 961 | this.sample = 0; 962 | this.dacLsb = 0; 963 | this.shiftCounter = 0; 964 | this.reg4012 = 0; 965 | this.reg4013 = 0; 966 | this.data = 0; 967 | }, 968 | 969 | JSON_PROPERTIES: [ 970 | "MODE_NORMAL", 971 | "MODE_LOOP", 972 | "MODE_IRQ", 973 | "isEnabled", 974 | "hasSample", 975 | "irqGenerated", 976 | "playMode", 977 | "dmaFrequency", 978 | "dmaCounter", 979 | "deltaCounter", 980 | "playStartAddress", 981 | "playAddress", 982 | "playLength", 983 | "playLengthCounter", 984 | "shiftCounter", 985 | "reg4012", 986 | "reg4013", 987 | "sample", 988 | "dacLsb", 989 | "data", 990 | ], 991 | 992 | toJSON: function () { 993 | return utils.toJSON(this); 994 | }, 995 | 996 | fromJSON: function (s) { 997 | utils.fromJSON(this, s); 998 | }, 999 | }; 1000 | 1001 | var ChannelNoise = function (papu) { 1002 | this.papu = papu; 1003 | 1004 | this.isEnabled = null; 1005 | this.envDecayDisable = null; 1006 | this.envDecayLoopEnable = null; 1007 | this.lengthCounterEnable = null; 1008 | this.envReset = null; 1009 | this.shiftNow = null; 1010 | 1011 | this.lengthCounter = null; 1012 | this.progTimerCount = null; 1013 | this.progTimerMax = null; 1014 | this.envDecayRate = null; 1015 | this.envDecayCounter = null; 1016 | this.envVolume = null; 1017 | this.masterVolume = null; 1018 | this.shiftReg = 1 << 14; 1019 | this.randomBit = null; 1020 | this.randomMode = null; 1021 | this.sampleValue = null; 1022 | this.accValue = 0; 1023 | this.accCount = 1; 1024 | this.tmp = null; 1025 | 1026 | this.reset(); 1027 | }; 1028 | 1029 | ChannelNoise.prototype = { 1030 | reset: function () { 1031 | this.progTimerCount = 0; 1032 | this.progTimerMax = 0; 1033 | this.isEnabled = false; 1034 | this.lengthCounter = 0; 1035 | this.lengthCounterEnable = false; 1036 | this.envDecayDisable = false; 1037 | this.envDecayLoopEnable = false; 1038 | this.shiftNow = false; 1039 | this.envDecayRate = 0; 1040 | this.envDecayCounter = 0; 1041 | this.envVolume = 0; 1042 | this.masterVolume = 0; 1043 | this.shiftReg = 1; 1044 | this.randomBit = 0; 1045 | this.randomMode = 0; 1046 | this.sampleValue = 0; 1047 | this.tmp = 0; 1048 | }, 1049 | 1050 | clockLengthCounter: function () { 1051 | if (this.lengthCounterEnable && this.lengthCounter > 0) { 1052 | this.lengthCounter--; 1053 | if (this.lengthCounter === 0) { 1054 | this.updateSampleValue(); 1055 | } 1056 | } 1057 | }, 1058 | 1059 | clockEnvDecay: function () { 1060 | if (this.envReset) { 1061 | // Reset envelope: 1062 | this.envReset = false; 1063 | this.envDecayCounter = this.envDecayRate + 1; 1064 | this.envVolume = 0xf; 1065 | } else if (--this.envDecayCounter <= 0) { 1066 | // Normal handling: 1067 | this.envDecayCounter = this.envDecayRate + 1; 1068 | if (this.envVolume > 0) { 1069 | this.envVolume--; 1070 | } else { 1071 | this.envVolume = this.envDecayLoopEnable ? 0xf : 0; 1072 | } 1073 | } 1074 | if (this.envDecayDisable) { 1075 | this.masterVolume = this.envDecayRate; 1076 | } else { 1077 | this.masterVolume = this.envVolume; 1078 | } 1079 | this.updateSampleValue(); 1080 | }, 1081 | 1082 | updateSampleValue: function () { 1083 | if (this.isEnabled && this.lengthCounter > 0) { 1084 | this.sampleValue = this.randomBit * this.masterVolume; 1085 | } 1086 | }, 1087 | 1088 | writeReg: function (address, value) { 1089 | if (address === 0x400c) { 1090 | // Volume/Envelope decay: 1091 | this.envDecayDisable = (value & 0x10) !== 0; 1092 | this.envDecayRate = value & 0xf; 1093 | this.envDecayLoopEnable = (value & 0x20) !== 0; 1094 | this.lengthCounterEnable = (value & 0x20) === 0; 1095 | if (this.envDecayDisable) { 1096 | this.masterVolume = this.envDecayRate; 1097 | } else { 1098 | this.masterVolume = this.envVolume; 1099 | } 1100 | } else if (address === 0x400e) { 1101 | // Programmable timer: 1102 | this.progTimerMax = this.papu.getNoiseWaveLength(value & 0xf); 1103 | this.randomMode = value >> 7; 1104 | } else if (address === 0x400f) { 1105 | // Length counter 1106 | this.lengthCounter = this.papu.getLengthMax(value & 248); 1107 | this.envReset = true; 1108 | } 1109 | // Update: 1110 | //updateSampleValue(); 1111 | }, 1112 | 1113 | setEnabled: function (value) { 1114 | this.isEnabled = value; 1115 | if (!value) { 1116 | this.lengthCounter = 0; 1117 | } 1118 | this.updateSampleValue(); 1119 | }, 1120 | 1121 | getLengthStatus: function () { 1122 | return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1; 1123 | }, 1124 | 1125 | JSON_PROPERTIES: [ 1126 | "isEnabled", 1127 | "envDecayDisable", 1128 | "envDecayLoopEnable", 1129 | "lengthCounterEnable", 1130 | "envReset", 1131 | "shiftNow", 1132 | "lengthCounter", 1133 | "progTimerCount", 1134 | "progTimerMax", 1135 | "envDecayRate", 1136 | "envDecayCounter", 1137 | "envVolume", 1138 | "masterVolume", 1139 | "shiftReg", 1140 | "randomBit", 1141 | "randomMode", 1142 | "sampleValue", 1143 | "accValue", 1144 | "accCount", 1145 | "tmp", 1146 | ], 1147 | 1148 | toJSON: function () { 1149 | return utils.toJSON(this); 1150 | }, 1151 | 1152 | fromJSON: function (s) { 1153 | utils.fromJSON(this, s); 1154 | }, 1155 | }; 1156 | 1157 | var ChannelSquare = function (papu, square1) { 1158 | this.papu = papu; 1159 | 1160 | // prettier-ignore 1161 | this.dutyLookup = [ 1162 | 0, 1, 0, 0, 0, 0, 0, 0, 1163 | 0, 1, 1, 0, 0, 0, 0, 0, 1164 | 0, 1, 1, 1, 1, 0, 0, 0, 1165 | 1, 0, 0, 1, 1, 1, 1, 1 1166 | ]; 1167 | // prettier-ignore 1168 | this.impLookup = [ 1169 | 1,-1, 0, 0, 0, 0, 0, 0, 1170 | 1, 0,-1, 0, 0, 0, 0, 0, 1171 | 1, 0, 0, 0,-1, 0, 0, 0, 1172 | -1, 0, 1, 0, 0, 0, 0, 0 1173 | ]; 1174 | 1175 | this.sqr1 = square1; 1176 | this.isEnabled = null; 1177 | this.lengthCounterEnable = null; 1178 | this.sweepActive = null; 1179 | this.envDecayDisable = null; 1180 | this.envDecayLoopEnable = null; 1181 | this.envReset = null; 1182 | this.sweepCarry = null; 1183 | this.updateSweepPeriod = null; 1184 | 1185 | this.progTimerCount = null; 1186 | this.progTimerMax = null; 1187 | this.lengthCounter = null; 1188 | this.squareCounter = null; 1189 | this.sweepCounter = null; 1190 | this.sweepCounterMax = null; 1191 | this.sweepMode = null; 1192 | this.sweepShiftAmount = null; 1193 | this.envDecayRate = null; 1194 | this.envDecayCounter = null; 1195 | this.envVolume = null; 1196 | this.masterVolume = null; 1197 | this.dutyMode = null; 1198 | this.sweepResult = null; 1199 | this.sampleValue = null; 1200 | this.vol = null; 1201 | 1202 | this.reset(); 1203 | }; 1204 | 1205 | ChannelSquare.prototype = { 1206 | reset: function () { 1207 | this.progTimerCount = 0; 1208 | this.progTimerMax = 0; 1209 | this.lengthCounter = 0; 1210 | this.squareCounter = 0; 1211 | this.sweepCounter = 0; 1212 | this.sweepCounterMax = 0; 1213 | this.sweepMode = 0; 1214 | this.sweepShiftAmount = 0; 1215 | this.envDecayRate = 0; 1216 | this.envDecayCounter = 0; 1217 | this.envVolume = 0; 1218 | this.masterVolume = 0; 1219 | this.dutyMode = 0; 1220 | this.vol = 0; 1221 | 1222 | this.isEnabled = false; 1223 | this.lengthCounterEnable = false; 1224 | this.sweepActive = false; 1225 | this.sweepCarry = false; 1226 | this.envDecayDisable = false; 1227 | this.envDecayLoopEnable = false; 1228 | }, 1229 | 1230 | clockLengthCounter: function () { 1231 | if (this.lengthCounterEnable && this.lengthCounter > 0) { 1232 | this.lengthCounter--; 1233 | if (this.lengthCounter === 0) { 1234 | this.updateSampleValue(); 1235 | } 1236 | } 1237 | }, 1238 | 1239 | clockEnvDecay: function () { 1240 | if (this.envReset) { 1241 | // Reset envelope: 1242 | this.envReset = false; 1243 | this.envDecayCounter = this.envDecayRate + 1; 1244 | this.envVolume = 0xf; 1245 | } else if (--this.envDecayCounter <= 0) { 1246 | // Normal handling: 1247 | this.envDecayCounter = this.envDecayRate + 1; 1248 | if (this.envVolume > 0) { 1249 | this.envVolume--; 1250 | } else { 1251 | this.envVolume = this.envDecayLoopEnable ? 0xf : 0; 1252 | } 1253 | } 1254 | 1255 | if (this.envDecayDisable) { 1256 | this.masterVolume = this.envDecayRate; 1257 | } else { 1258 | this.masterVolume = this.envVolume; 1259 | } 1260 | this.updateSampleValue(); 1261 | }, 1262 | 1263 | clockSweep: function () { 1264 | if (--this.sweepCounter <= 0) { 1265 | this.sweepCounter = this.sweepCounterMax + 1; 1266 | if ( 1267 | this.sweepActive && 1268 | this.sweepShiftAmount > 0 && 1269 | this.progTimerMax > 7 1270 | ) { 1271 | // Calculate result from shifter: 1272 | this.sweepCarry = false; 1273 | if (this.sweepMode === 0) { 1274 | this.progTimerMax += this.progTimerMax >> this.sweepShiftAmount; 1275 | if (this.progTimerMax > 4095) { 1276 | this.progTimerMax = 4095; 1277 | this.sweepCarry = true; 1278 | } 1279 | } else { 1280 | this.progTimerMax = 1281 | this.progTimerMax - 1282 | ((this.progTimerMax >> this.sweepShiftAmount) - 1283 | (this.sqr1 ? 1 : 0)); 1284 | } 1285 | } 1286 | } 1287 | 1288 | if (this.updateSweepPeriod) { 1289 | this.updateSweepPeriod = false; 1290 | this.sweepCounter = this.sweepCounterMax + 1; 1291 | } 1292 | }, 1293 | 1294 | updateSampleValue: function () { 1295 | if (this.isEnabled && this.lengthCounter > 0 && this.progTimerMax > 7) { 1296 | if ( 1297 | this.sweepMode === 0 && 1298 | this.progTimerMax + (this.progTimerMax >> this.sweepShiftAmount) > 4095 1299 | ) { 1300 | //if (this.sweepCarry) { 1301 | this.sampleValue = 0; 1302 | } else { 1303 | this.sampleValue = 1304 | this.masterVolume * 1305 | this.dutyLookup[(this.dutyMode << 3) + this.squareCounter]; 1306 | } 1307 | } else { 1308 | this.sampleValue = 0; 1309 | } 1310 | }, 1311 | 1312 | writeReg: function (address, value) { 1313 | var addrAdd = this.sqr1 ? 0 : 4; 1314 | if (address === 0x4000 + addrAdd) { 1315 | // Volume/Envelope decay: 1316 | this.envDecayDisable = (value & 0x10) !== 0; 1317 | this.envDecayRate = value & 0xf; 1318 | this.envDecayLoopEnable = (value & 0x20) !== 0; 1319 | this.dutyMode = (value >> 6) & 0x3; 1320 | this.lengthCounterEnable = (value & 0x20) === 0; 1321 | if (this.envDecayDisable) { 1322 | this.masterVolume = this.envDecayRate; 1323 | } else { 1324 | this.masterVolume = this.envVolume; 1325 | } 1326 | this.updateSampleValue(); 1327 | } else if (address === 0x4001 + addrAdd) { 1328 | // Sweep: 1329 | this.sweepActive = (value & 0x80) !== 0; 1330 | this.sweepCounterMax = (value >> 4) & 7; 1331 | this.sweepMode = (value >> 3) & 1; 1332 | this.sweepShiftAmount = value & 7; 1333 | this.updateSweepPeriod = true; 1334 | } else if (address === 0x4002 + addrAdd) { 1335 | // Programmable timer: 1336 | this.progTimerMax &= 0x700; 1337 | this.progTimerMax |= value; 1338 | } else if (address === 0x4003 + addrAdd) { 1339 | // Programmable timer, length counter 1340 | this.progTimerMax &= 0xff; 1341 | this.progTimerMax |= (value & 0x7) << 8; 1342 | 1343 | if (this.isEnabled) { 1344 | this.lengthCounter = this.papu.getLengthMax(value & 0xf8); 1345 | } 1346 | 1347 | this.envReset = true; 1348 | } 1349 | }, 1350 | 1351 | setEnabled: function (value) { 1352 | this.isEnabled = value; 1353 | if (!value) { 1354 | this.lengthCounter = 0; 1355 | } 1356 | this.updateSampleValue(); 1357 | }, 1358 | 1359 | getLengthStatus: function () { 1360 | return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1; 1361 | }, 1362 | 1363 | JSON_PROPERTIES: [ 1364 | "isEnabled", 1365 | "lengthCounterEnable", 1366 | "sweepActive", 1367 | "envDecayDisable", 1368 | "envDecayLoopEnable", 1369 | "envReset", 1370 | "sweepCarry", 1371 | "updateSweepPeriod", 1372 | "progTimerCount", 1373 | "progTimerMax", 1374 | "lengthCounter", 1375 | "squareCounter", 1376 | "sweepCounter", 1377 | "sweepCounterMax", 1378 | "sweepMode", 1379 | "sweepShiftAmount", 1380 | "envDecayRate", 1381 | "envDecayCounter", 1382 | "envVolume", 1383 | "masterVolume", 1384 | "dutyMode", 1385 | "sweepResult", 1386 | "sampleValue", 1387 | "vol", 1388 | ], 1389 | 1390 | toJSON: function () { 1391 | return utils.toJSON(this); 1392 | }, 1393 | 1394 | fromJSON: function (s) { 1395 | utils.fromJSON(this, s); 1396 | }, 1397 | }; 1398 | 1399 | var ChannelTriangle = function (papu) { 1400 | this.papu = papu; 1401 | 1402 | this.isEnabled = null; 1403 | this.sampleCondition = null; 1404 | this.lengthCounterEnable = null; 1405 | this.lcHalt = null; 1406 | this.lcControl = null; 1407 | 1408 | this.progTimerCount = null; 1409 | this.progTimerMax = null; 1410 | this.triangleCounter = null; 1411 | this.lengthCounter = null; 1412 | this.linearCounter = null; 1413 | this.lcLoadValue = null; 1414 | this.sampleValue = null; 1415 | this.tmp = null; 1416 | 1417 | this.reset(); 1418 | }; 1419 | 1420 | ChannelTriangle.prototype = { 1421 | reset: function () { 1422 | this.progTimerCount = 0; 1423 | this.progTimerMax = 0; 1424 | this.triangleCounter = 0; 1425 | this.isEnabled = false; 1426 | this.sampleCondition = false; 1427 | this.lengthCounter = 0; 1428 | this.lengthCounterEnable = false; 1429 | this.linearCounter = 0; 1430 | this.lcLoadValue = 0; 1431 | this.lcHalt = true; 1432 | this.lcControl = false; 1433 | this.tmp = 0; 1434 | this.sampleValue = 0xf; 1435 | }, 1436 | 1437 | clockLengthCounter: function () { 1438 | if (this.lengthCounterEnable && this.lengthCounter > 0) { 1439 | this.lengthCounter--; 1440 | if (this.lengthCounter === 0) { 1441 | this.updateSampleCondition(); 1442 | } 1443 | } 1444 | }, 1445 | 1446 | clockLinearCounter: function () { 1447 | if (this.lcHalt) { 1448 | // Load: 1449 | this.linearCounter = this.lcLoadValue; 1450 | this.updateSampleCondition(); 1451 | } else if (this.linearCounter > 0) { 1452 | // Decrement: 1453 | this.linearCounter--; 1454 | this.updateSampleCondition(); 1455 | } 1456 | if (!this.lcControl) { 1457 | // Clear halt flag: 1458 | this.lcHalt = false; 1459 | } 1460 | }, 1461 | 1462 | getLengthStatus: function () { 1463 | return this.lengthCounter === 0 || !this.isEnabled ? 0 : 1; 1464 | }, 1465 | 1466 | // eslint-disable-next-line no-unused-vars 1467 | readReg: function (address) { 1468 | return 0; 1469 | }, 1470 | 1471 | writeReg: function (address, value) { 1472 | if (address === 0x4008) { 1473 | // New values for linear counter: 1474 | this.lcControl = (value & 0x80) !== 0; 1475 | this.lcLoadValue = value & 0x7f; 1476 | 1477 | // Length counter enable: 1478 | this.lengthCounterEnable = !this.lcControl; 1479 | } else if (address === 0x400a) { 1480 | // Programmable timer: 1481 | this.progTimerMax &= 0x700; 1482 | this.progTimerMax |= value; 1483 | } else if (address === 0x400b) { 1484 | // Programmable timer, length counter 1485 | this.progTimerMax &= 0xff; 1486 | this.progTimerMax |= (value & 0x07) << 8; 1487 | this.lengthCounter = this.papu.getLengthMax(value & 0xf8); 1488 | this.lcHalt = true; 1489 | } 1490 | 1491 | this.updateSampleCondition(); 1492 | }, 1493 | 1494 | clockProgrammableTimer: function (nCycles) { 1495 | if (this.progTimerMax > 0) { 1496 | this.progTimerCount += nCycles; 1497 | while ( 1498 | this.progTimerMax > 0 && 1499 | this.progTimerCount >= this.progTimerMax 1500 | ) { 1501 | this.progTimerCount -= this.progTimerMax; 1502 | if ( 1503 | this.isEnabled && 1504 | this.lengthCounter > 0 && 1505 | this.linearCounter > 0 1506 | ) { 1507 | this.clockTriangleGenerator(); 1508 | } 1509 | } 1510 | } 1511 | }, 1512 | 1513 | clockTriangleGenerator: function () { 1514 | this.triangleCounter++; 1515 | this.triangleCounter &= 0x1f; 1516 | }, 1517 | 1518 | setEnabled: function (value) { 1519 | this.isEnabled = value; 1520 | if (!value) { 1521 | this.lengthCounter = 0; 1522 | } 1523 | this.updateSampleCondition(); 1524 | }, 1525 | 1526 | updateSampleCondition: function () { 1527 | this.sampleCondition = 1528 | this.isEnabled && 1529 | this.progTimerMax > 7 && 1530 | this.linearCounter > 0 && 1531 | this.lengthCounter > 0; 1532 | }, 1533 | 1534 | JSON_PROPERTIES: [ 1535 | "isEnabled", 1536 | "sampleCondition", 1537 | "lengthCounterEnable", 1538 | "lcHalt", 1539 | "lcControl", 1540 | "progTimerCount", 1541 | "progTimerMax", 1542 | "triangleCounter", 1543 | "lengthCounter", 1544 | "linearCounter", 1545 | "lcLoadValue", 1546 | "sampleValue", 1547 | "tmp", 1548 | ], 1549 | 1550 | toJSON: function () { 1551 | return utils.toJSON(this); 1552 | }, 1553 | 1554 | fromJSON: function (s) { 1555 | utils.fromJSON(this, s); 1556 | }, 1557 | }; 1558 | 1559 | module.exports = PAPU; 1560 | -------------------------------------------------------------------------------- /src/ppu.js: -------------------------------------------------------------------------------- 1 | var Tile = require("./tile"); 2 | var utils = require("./utils"); 3 | 4 | var PPU = function (nes) { 5 | this.nes = nes; 6 | 7 | // Keep Chrome happy 8 | this.vramMem = null; 9 | this.spriteMem = null; 10 | this.vramAddress = null; 11 | this.vramTmpAddress = null; 12 | this.vramBufferedReadValue = null; 13 | this.firstWrite = null; 14 | this.sramAddress = null; 15 | this.currentMirroring = null; 16 | this.requestEndFrame = null; 17 | this.nmiOk = null; 18 | this.dummyCycleToggle = null; 19 | this.validTileData = null; 20 | this.nmiCounter = null; 21 | this.scanlineAlreadyRendered = null; 22 | this.f_nmiOnVblank = null; 23 | this.f_spriteSize = null; 24 | this.f_bgPatternTable = null; 25 | this.f_spPatternTable = null; 26 | this.f_addrInc = null; 27 | this.f_nTblAddress = null; 28 | this.f_color = null; 29 | this.f_spVisibility = null; 30 | this.f_bgVisibility = null; 31 | this.f_spClipping = null; 32 | this.f_bgClipping = null; 33 | this.f_dispType = null; 34 | this.cntFV = null; 35 | this.cntV = null; 36 | this.cntH = null; 37 | this.cntVT = null; 38 | this.cntHT = null; 39 | this.regFV = null; 40 | this.regV = null; 41 | this.regH = null; 42 | this.regVT = null; 43 | this.regHT = null; 44 | this.regFH = null; 45 | this.regS = null; 46 | this.curNt = null; 47 | this.attrib = null; 48 | this.buffer = null; 49 | this.bgbuffer = null; 50 | this.pixrendered = null; 51 | 52 | this.validTileData = null; 53 | this.scantile = null; 54 | this.scanline = null; 55 | this.lastRenderedScanline = null; 56 | this.curX = null; 57 | this.sprX = null; 58 | this.sprY = null; 59 | this.sprTile = null; 60 | this.sprCol = null; 61 | this.vertFlip = null; 62 | this.horiFlip = null; 63 | this.bgPriority = null; 64 | this.spr0HitX = null; 65 | this.spr0HitY = null; 66 | this.hitSpr0 = null; 67 | this.sprPalette = null; 68 | this.imgPalette = null; 69 | this.ptTile = null; 70 | this.ntable1 = null; 71 | this.currentMirroring = null; 72 | this.nameTable = null; 73 | this.vramMirrorTable = null; 74 | this.palTable = null; 75 | 76 | // Rendering Options: 77 | this.showSpr0Hit = false; 78 | this.clipToTvSize = true; 79 | 80 | this.reset(); 81 | }; 82 | 83 | PPU.prototype = { 84 | // Status flags: 85 | STATUS_VRAMWRITE: 4, 86 | STATUS_SLSPRITECOUNT: 5, 87 | STATUS_SPRITE0HIT: 6, 88 | STATUS_VBLANK: 7, 89 | 90 | reset: function () { 91 | var i; 92 | 93 | // Memory 94 | this.vramMem = new Array(0x8000); 95 | this.spriteMem = new Array(0x100); 96 | for (i = 0; i < this.vramMem.length; i++) { 97 | this.vramMem[i] = 0; 98 | } 99 | for (i = 0; i < this.spriteMem.length; i++) { 100 | this.spriteMem[i] = 0; 101 | } 102 | 103 | // VRAM I/O: 104 | this.vramAddress = null; 105 | this.vramTmpAddress = null; 106 | this.vramBufferedReadValue = 0; 107 | this.firstWrite = true; // VRAM/Scroll Hi/Lo latch 108 | 109 | // SPR-RAM I/O: 110 | this.sramAddress = 0; // 8-bit only. 111 | 112 | this.currentMirroring = -1; 113 | this.requestEndFrame = false; 114 | this.nmiOk = false; 115 | this.dummyCycleToggle = false; 116 | this.validTileData = false; 117 | this.nmiCounter = 0; 118 | this.scanlineAlreadyRendered = null; 119 | 120 | // Control Flags Register 1: 121 | this.f_nmiOnVblank = 0; // NMI on VBlank. 0=disable, 1=enable 122 | this.f_spriteSize = 0; // Sprite size. 0=8x8, 1=8x16 123 | this.f_bgPatternTable = 0; // Background Pattern Table address. 0=0x0000,1=0x1000 124 | this.f_spPatternTable = 0; // Sprite Pattern Table address. 0=0x0000,1=0x1000 125 | this.f_addrInc = 0; // PPU Address Increment. 0=1,1=32 126 | this.f_nTblAddress = 0; // Name Table Address. 0=0x2000,1=0x2400,2=0x2800,3=0x2C00 127 | 128 | // Control Flags Register 2: 129 | this.f_color = 0; // Background color. 0=black, 1=blue, 2=green, 4=red 130 | this.f_spVisibility = 0; // Sprite visibility. 0=not displayed,1=displayed 131 | this.f_bgVisibility = 0; // Background visibility. 0=Not Displayed,1=displayed 132 | this.f_spClipping = 0; // Sprite clipping. 0=Sprites invisible in left 8-pixel column,1=No clipping 133 | this.f_bgClipping = 0; // Background clipping. 0=BG invisible in left 8-pixel column, 1=No clipping 134 | this.f_dispType = 0; // Display type. 0=color, 1=monochrome 135 | 136 | // Counters: 137 | this.cntFV = 0; 138 | this.cntV = 0; 139 | this.cntH = 0; 140 | this.cntVT = 0; 141 | this.cntHT = 0; 142 | 143 | // Registers: 144 | this.regFV = 0; 145 | this.regV = 0; 146 | this.regH = 0; 147 | this.regVT = 0; 148 | this.regHT = 0; 149 | this.regFH = 0; 150 | this.regS = 0; 151 | 152 | // These are temporary variables used in rendering and sound procedures. 153 | // Their states outside of those procedures can be ignored. 154 | // TODO: the use of this is a bit weird, investigate 155 | this.curNt = null; 156 | 157 | // Variables used when rendering: 158 | this.attrib = new Array(32); 159 | this.buffer = new Array(256 * 240); 160 | this.bgbuffer = new Array(256 * 240); 161 | this.pixrendered = new Array(256 * 240); 162 | 163 | this.validTileData = null; 164 | 165 | this.scantile = new Array(32); 166 | 167 | // Initialize misc vars: 168 | this.scanline = 0; 169 | this.lastRenderedScanline = -1; 170 | this.curX = 0; 171 | 172 | // Sprite data: 173 | this.sprX = new Array(64); // X coordinate 174 | this.sprY = new Array(64); // Y coordinate 175 | this.sprTile = new Array(64); // Tile Index (into pattern table) 176 | this.sprCol = new Array(64); // Upper two bits of color 177 | this.vertFlip = new Array(64); // Vertical Flip 178 | this.horiFlip = new Array(64); // Horizontal Flip 179 | this.bgPriority = new Array(64); // Background priority 180 | this.spr0HitX = 0; // Sprite #0 hit X coordinate 181 | this.spr0HitY = 0; // Sprite #0 hit Y coordinate 182 | this.hitSpr0 = false; 183 | 184 | // Palette data: 185 | this.sprPalette = new Array(16); 186 | this.imgPalette = new Array(16); 187 | 188 | // Create pattern table tile buffers: 189 | this.ptTile = new Array(512); 190 | for (i = 0; i < 512; i++) { 191 | this.ptTile[i] = new Tile(); 192 | } 193 | 194 | // Create nametable buffers: 195 | // Name table data: 196 | this.ntable1 = new Array(4); 197 | this.currentMirroring = -1; 198 | this.nameTable = new Array(4); 199 | for (i = 0; i < 4; i++) { 200 | this.nameTable[i] = new NameTable(32, 32, "Nt" + i); 201 | } 202 | 203 | // Initialize mirroring lookup table: 204 | this.vramMirrorTable = new Array(0x8000); 205 | for (i = 0; i < 0x8000; i++) { 206 | this.vramMirrorTable[i] = i; 207 | } 208 | 209 | this.palTable = new PaletteTable(); 210 | this.palTable.loadNTSCPalette(); 211 | //this.palTable.loadDefaultPalette(); 212 | 213 | this.updateControlReg1(0); 214 | this.updateControlReg2(0); 215 | }, 216 | 217 | // Sets Nametable mirroring. 218 | setMirroring: function (mirroring) { 219 | if (mirroring === this.currentMirroring) { 220 | return; 221 | } 222 | 223 | this.currentMirroring = mirroring; 224 | this.triggerRendering(); 225 | 226 | // Remove mirroring: 227 | if (this.vramMirrorTable === null) { 228 | this.vramMirrorTable = new Array(0x8000); 229 | } 230 | for (var i = 0; i < 0x8000; i++) { 231 | this.vramMirrorTable[i] = i; 232 | } 233 | 234 | // Palette mirroring: 235 | this.defineMirrorRegion(0x3f20, 0x3f00, 0x20); 236 | this.defineMirrorRegion(0x3f40, 0x3f00, 0x20); 237 | this.defineMirrorRegion(0x3f80, 0x3f00, 0x20); 238 | this.defineMirrorRegion(0x3fc0, 0x3f00, 0x20); 239 | 240 | // Additional mirroring: 241 | this.defineMirrorRegion(0x3000, 0x2000, 0xf00); 242 | this.defineMirrorRegion(0x4000, 0x0000, 0x4000); 243 | 244 | if (mirroring === this.nes.rom.HORIZONTAL_MIRRORING) { 245 | // Horizontal mirroring. 246 | 247 | this.ntable1[0] = 0; 248 | this.ntable1[1] = 0; 249 | this.ntable1[2] = 1; 250 | this.ntable1[3] = 1; 251 | 252 | this.defineMirrorRegion(0x2400, 0x2000, 0x400); 253 | this.defineMirrorRegion(0x2c00, 0x2800, 0x400); 254 | } else if (mirroring === this.nes.rom.VERTICAL_MIRRORING) { 255 | // Vertical mirroring. 256 | 257 | this.ntable1[0] = 0; 258 | this.ntable1[1] = 1; 259 | this.ntable1[2] = 0; 260 | this.ntable1[3] = 1; 261 | 262 | this.defineMirrorRegion(0x2800, 0x2000, 0x400); 263 | this.defineMirrorRegion(0x2c00, 0x2400, 0x400); 264 | } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING) { 265 | // Single Screen mirroring 266 | 267 | this.ntable1[0] = 0; 268 | this.ntable1[1] = 0; 269 | this.ntable1[2] = 0; 270 | this.ntable1[3] = 0; 271 | 272 | this.defineMirrorRegion(0x2400, 0x2000, 0x400); 273 | this.defineMirrorRegion(0x2800, 0x2000, 0x400); 274 | this.defineMirrorRegion(0x2c00, 0x2000, 0x400); 275 | } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING2) { 276 | this.ntable1[0] = 1; 277 | this.ntable1[1] = 1; 278 | this.ntable1[2] = 1; 279 | this.ntable1[3] = 1; 280 | 281 | this.defineMirrorRegion(0x2400, 0x2400, 0x400); 282 | this.defineMirrorRegion(0x2800, 0x2400, 0x400); 283 | this.defineMirrorRegion(0x2c00, 0x2400, 0x400); 284 | } else { 285 | // Assume Four-screen mirroring. 286 | 287 | this.ntable1[0] = 0; 288 | this.ntable1[1] = 1; 289 | this.ntable1[2] = 2; 290 | this.ntable1[3] = 3; 291 | } 292 | }, 293 | 294 | // Define a mirrored area in the address lookup table. 295 | // Assumes the regions don't overlap. 296 | // The 'to' region is the region that is physically in memory. 297 | defineMirrorRegion: function (fromStart, toStart, size) { 298 | for (var i = 0; i < size; i++) { 299 | this.vramMirrorTable[fromStart + i] = toStart + i; 300 | } 301 | }, 302 | 303 | startVBlank: function () { 304 | // Do NMI: 305 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI); 306 | 307 | // Make sure everything is rendered: 308 | if (this.lastRenderedScanline < 239) { 309 | this.renderFramePartially( 310 | this.lastRenderedScanline + 1, 311 | 240 - this.lastRenderedScanline 312 | ); 313 | } 314 | 315 | // End frame: 316 | this.endFrame(); 317 | 318 | // Reset scanline counter: 319 | this.lastRenderedScanline = -1; 320 | }, 321 | 322 | endScanline: function () { 323 | switch (this.scanline) { 324 | case 19: 325 | // Dummy scanline. 326 | // May be variable length: 327 | if (this.dummyCycleToggle) { 328 | // Remove dead cycle at end of scanline, 329 | // for next scanline: 330 | this.curX = 1; 331 | this.dummyCycleToggle = !this.dummyCycleToggle; 332 | } 333 | break; 334 | 335 | case 20: 336 | // Clear VBlank flag: 337 | this.setStatusFlag(this.STATUS_VBLANK, false); 338 | 339 | // Clear Sprite #0 hit flag: 340 | this.setStatusFlag(this.STATUS_SPRITE0HIT, false); 341 | this.hitSpr0 = false; 342 | this.spr0HitX = -1; 343 | this.spr0HitY = -1; 344 | 345 | if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) { 346 | // Update counters: 347 | this.cntFV = this.regFV; 348 | this.cntV = this.regV; 349 | this.cntH = this.regH; 350 | this.cntVT = this.regVT; 351 | this.cntHT = this.regHT; 352 | 353 | if (this.f_bgVisibility === 1) { 354 | // Render dummy scanline: 355 | this.renderBgScanline(false, 0); 356 | } 357 | } 358 | 359 | if (this.f_bgVisibility === 1 && this.f_spVisibility === 1) { 360 | // Check sprite 0 hit for first scanline: 361 | this.checkSprite0(0); 362 | } 363 | 364 | if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) { 365 | // Clock mapper IRQ Counter: 366 | this.nes.mmap.clockIrqCounter(); 367 | } 368 | break; 369 | 370 | case 261: 371 | // Dead scanline, no rendering. 372 | // Set VINT: 373 | this.setStatusFlag(this.STATUS_VBLANK, true); 374 | this.requestEndFrame = true; 375 | this.nmiCounter = 9; 376 | 377 | // Wrap around: 378 | this.scanline = -1; // will be incremented to 0 379 | 380 | break; 381 | 382 | default: 383 | if (this.scanline >= 21 && this.scanline <= 260) { 384 | // Render normally: 385 | if (this.f_bgVisibility === 1) { 386 | if (!this.scanlineAlreadyRendered) { 387 | // update scroll: 388 | this.cntHT = this.regHT; 389 | this.cntH = this.regH; 390 | this.renderBgScanline(true, this.scanline + 1 - 21); 391 | } 392 | this.scanlineAlreadyRendered = false; 393 | 394 | // Check for sprite 0 (next scanline): 395 | if (!this.hitSpr0 && this.f_spVisibility === 1) { 396 | if ( 397 | this.sprX[0] >= -7 && 398 | this.sprX[0] < 256 && 399 | this.sprY[0] + 1 <= this.scanline - 20 && 400 | this.sprY[0] + 1 + (this.f_spriteSize === 0 ? 8 : 16) >= 401 | this.scanline - 20 402 | ) { 403 | if (this.checkSprite0(this.scanline - 20)) { 404 | this.hitSpr0 = true; 405 | } 406 | } 407 | } 408 | } 409 | 410 | if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) { 411 | // Clock mapper IRQ Counter: 412 | this.nes.mmap.clockIrqCounter(); 413 | } 414 | } 415 | } 416 | 417 | this.scanline++; 418 | this.regsToAddress(); 419 | this.cntsToAddress(); 420 | }, 421 | 422 | startFrame: function () { 423 | // Set background color: 424 | var bgColor = 0; 425 | 426 | if (this.f_dispType === 0) { 427 | // Color display. 428 | // f_color determines color emphasis. 429 | // Use first entry of image palette as BG color. 430 | bgColor = this.imgPalette[0]; 431 | } else { 432 | // Monochrome display. 433 | // f_color determines the bg color. 434 | switch (this.f_color) { 435 | case 0: 436 | // Black 437 | bgColor = 0x00000; 438 | break; 439 | case 1: 440 | // Green 441 | bgColor = 0x00ff00; 442 | break; 443 | case 2: 444 | // Blue 445 | bgColor = 0xff0000; 446 | break; 447 | case 3: 448 | // Invalid. Use black. 449 | bgColor = 0x000000; 450 | break; 451 | case 4: 452 | // Red 453 | bgColor = 0x0000ff; 454 | break; 455 | default: 456 | // Invalid. Use black. 457 | bgColor = 0x0; 458 | } 459 | } 460 | 461 | var buffer = this.buffer; 462 | var i; 463 | for (i = 0; i < 256 * 240; i++) { 464 | buffer[i] = bgColor; 465 | } 466 | var pixrendered = this.pixrendered; 467 | for (i = 0; i < pixrendered.length; i++) { 468 | pixrendered[i] = 65; 469 | } 470 | }, 471 | 472 | endFrame: function () { 473 | var i, x, y; 474 | var buffer = this.buffer; 475 | 476 | // Draw spr#0 hit coordinates: 477 | if (this.showSpr0Hit) { 478 | // Spr 0 position: 479 | if ( 480 | this.sprX[0] >= 0 && 481 | this.sprX[0] < 256 && 482 | this.sprY[0] >= 0 && 483 | this.sprY[0] < 240 484 | ) { 485 | for (i = 0; i < 256; i++) { 486 | buffer[(this.sprY[0] << 8) + i] = 0xff5555; 487 | } 488 | for (i = 0; i < 240; i++) { 489 | buffer[(i << 8) + this.sprX[0]] = 0xff5555; 490 | } 491 | } 492 | // Hit position: 493 | if ( 494 | this.spr0HitX >= 0 && 495 | this.spr0HitX < 256 && 496 | this.spr0HitY >= 0 && 497 | this.spr0HitY < 240 498 | ) { 499 | for (i = 0; i < 256; i++) { 500 | buffer[(this.spr0HitY << 8) + i] = 0x55ff55; 501 | } 502 | for (i = 0; i < 240; i++) { 503 | buffer[(i << 8) + this.spr0HitX] = 0x55ff55; 504 | } 505 | } 506 | } 507 | 508 | // This is a bit lazy.. 509 | // if either the sprites or the background should be clipped, 510 | // both are clipped after rendering is finished. 511 | if ( 512 | this.clipToTvSize || 513 | this.f_bgClipping === 0 || 514 | this.f_spClipping === 0 515 | ) { 516 | // Clip left 8-pixels column: 517 | for (y = 0; y < 240; y++) { 518 | for (x = 0; x < 8; x++) { 519 | buffer[(y << 8) + x] = 0; 520 | } 521 | } 522 | } 523 | 524 | if (this.clipToTvSize) { 525 | // Clip right 8-pixels column too: 526 | for (y = 0; y < 240; y++) { 527 | for (x = 0; x < 8; x++) { 528 | buffer[(y << 8) + 255 - x] = 0; 529 | } 530 | } 531 | } 532 | 533 | // Clip top and bottom 8 pixels: 534 | if (this.clipToTvSize) { 535 | for (y = 0; y < 8; y++) { 536 | for (x = 0; x < 256; x++) { 537 | buffer[(y << 8) + x] = 0; 538 | buffer[((239 - y) << 8) + x] = 0; 539 | } 540 | } 541 | } 542 | 543 | this.nes.ui.writeFrame(buffer); 544 | }, 545 | 546 | updateControlReg1: function (value) { 547 | this.triggerRendering(); 548 | 549 | this.f_nmiOnVblank = (value >> 7) & 1; 550 | this.f_spriteSize = (value >> 5) & 1; 551 | this.f_bgPatternTable = (value >> 4) & 1; 552 | this.f_spPatternTable = (value >> 3) & 1; 553 | this.f_addrInc = (value >> 2) & 1; 554 | this.f_nTblAddress = value & 3; 555 | 556 | this.regV = (value >> 1) & 1; 557 | this.regH = value & 1; 558 | this.regS = (value >> 4) & 1; 559 | }, 560 | 561 | updateControlReg2: function (value) { 562 | this.triggerRendering(); 563 | 564 | this.f_color = (value >> 5) & 7; 565 | this.f_spVisibility = (value >> 4) & 1; 566 | this.f_bgVisibility = (value >> 3) & 1; 567 | this.f_spClipping = (value >> 2) & 1; 568 | this.f_bgClipping = (value >> 1) & 1; 569 | this.f_dispType = value & 1; 570 | 571 | if (this.f_dispType === 0) { 572 | this.palTable.setEmphasis(this.f_color); 573 | } 574 | this.updatePalettes(); 575 | }, 576 | 577 | setStatusFlag: function (flag, value) { 578 | var n = 1 << flag; 579 | this.nes.cpu.mem[0x2002] = 580 | (this.nes.cpu.mem[0x2002] & (255 - n)) | (value ? n : 0); 581 | }, 582 | 583 | // CPU Register $2002: 584 | // Read the Status Register. 585 | readStatusRegister: function () { 586 | var tmp = this.nes.cpu.mem[0x2002]; 587 | 588 | // Reset scroll & VRAM Address toggle: 589 | this.firstWrite = true; 590 | 591 | // Clear VBlank flag: 592 | this.setStatusFlag(this.STATUS_VBLANK, false); 593 | 594 | // Fetch status data: 595 | return tmp; 596 | }, 597 | 598 | // CPU Register $2003: 599 | // Write the SPR-RAM address that is used for sramWrite (Register 0x2004 in CPU memory map) 600 | writeSRAMAddress: function (address) { 601 | this.sramAddress = address; 602 | }, 603 | 604 | // CPU Register $2004 (R): 605 | // Read from SPR-RAM (Sprite RAM). 606 | // The address should be set first. 607 | sramLoad: function () { 608 | /*short tmp = sprMem.load(sramAddress); 609 | sramAddress++; // Increment address 610 | sramAddress%=0x100; 611 | return tmp;*/ 612 | return this.spriteMem[this.sramAddress]; 613 | }, 614 | 615 | // CPU Register $2004 (W): 616 | // Write to SPR-RAM (Sprite RAM). 617 | // The address should be set first. 618 | sramWrite: function (value) { 619 | this.spriteMem[this.sramAddress] = value; 620 | this.spriteRamWriteUpdate(this.sramAddress, value); 621 | this.sramAddress++; // Increment address 622 | this.sramAddress %= 0x100; 623 | }, 624 | 625 | // CPU Register $2005: 626 | // Write to scroll registers. 627 | // The first write is the vertical offset, the second is the 628 | // horizontal offset: 629 | scrollWrite: function (value) { 630 | this.triggerRendering(); 631 | 632 | if (this.firstWrite) { 633 | // First write, horizontal scroll: 634 | this.regHT = (value >> 3) & 31; 635 | this.regFH = value & 7; 636 | } else { 637 | // Second write, vertical scroll: 638 | this.regFV = value & 7; 639 | this.regVT = (value >> 3) & 31; 640 | } 641 | this.firstWrite = !this.firstWrite; 642 | }, 643 | 644 | // CPU Register $2006: 645 | // Sets the adress used when reading/writing from/to VRAM. 646 | // The first write sets the high byte, the second the low byte. 647 | writeVRAMAddress: function (address) { 648 | if (this.firstWrite) { 649 | this.regFV = (address >> 4) & 3; 650 | this.regV = (address >> 3) & 1; 651 | this.regH = (address >> 2) & 1; 652 | this.regVT = (this.regVT & 7) | ((address & 3) << 3); 653 | } else { 654 | this.triggerRendering(); 655 | 656 | this.regVT = (this.regVT & 24) | ((address >> 5) & 7); 657 | this.regHT = address & 31; 658 | 659 | this.cntFV = this.regFV; 660 | this.cntV = this.regV; 661 | this.cntH = this.regH; 662 | this.cntVT = this.regVT; 663 | this.cntHT = this.regHT; 664 | 665 | this.checkSprite0(this.scanline - 20); 666 | } 667 | 668 | this.firstWrite = !this.firstWrite; 669 | 670 | // Invoke mapper latch: 671 | this.cntsToAddress(); 672 | if (this.vramAddress < 0x2000) { 673 | this.nes.mmap.latchAccess(this.vramAddress); 674 | } 675 | }, 676 | 677 | // CPU Register $2007(R): 678 | // Read from PPU memory. The address should be set first. 679 | vramLoad: function () { 680 | var tmp; 681 | 682 | this.cntsToAddress(); 683 | this.regsToAddress(); 684 | 685 | // If address is in range 0x0000-0x3EFF, return buffered values: 686 | if (this.vramAddress <= 0x3eff) { 687 | tmp = this.vramBufferedReadValue; 688 | 689 | // Update buffered value: 690 | if (this.vramAddress < 0x2000) { 691 | this.vramBufferedReadValue = this.vramMem[this.vramAddress]; 692 | } else { 693 | this.vramBufferedReadValue = this.mirroredLoad(this.vramAddress); 694 | } 695 | 696 | // Mapper latch access: 697 | if (this.vramAddress < 0x2000) { 698 | this.nes.mmap.latchAccess(this.vramAddress); 699 | } 700 | 701 | // Increment by either 1 or 32, depending on d2 of Control Register 1: 702 | this.vramAddress += this.f_addrInc === 1 ? 32 : 1; 703 | 704 | this.cntsFromAddress(); 705 | this.regsFromAddress(); 706 | 707 | return tmp; // Return the previous buffered value. 708 | } 709 | 710 | // No buffering in this mem range. Read normally. 711 | tmp = this.mirroredLoad(this.vramAddress); 712 | 713 | // Increment by either 1 or 32, depending on d2 of Control Register 1: 714 | this.vramAddress += this.f_addrInc === 1 ? 32 : 1; 715 | 716 | this.cntsFromAddress(); 717 | this.regsFromAddress(); 718 | 719 | return tmp; 720 | }, 721 | 722 | // CPU Register $2007(W): 723 | // Write to PPU memory. The address should be set first. 724 | vramWrite: function (value) { 725 | this.triggerRendering(); 726 | this.cntsToAddress(); 727 | this.regsToAddress(); 728 | 729 | if (this.vramAddress >= 0x2000) { 730 | // Mirroring is used. 731 | this.mirroredWrite(this.vramAddress, value); 732 | } else { 733 | // Write normally. 734 | this.writeMem(this.vramAddress, value); 735 | 736 | // Invoke mapper latch: 737 | this.nes.mmap.latchAccess(this.vramAddress); 738 | } 739 | 740 | // Increment by either 1 or 32, depending on d2 of Control Register 1: 741 | this.vramAddress += this.f_addrInc === 1 ? 32 : 1; 742 | this.regsFromAddress(); 743 | this.cntsFromAddress(); 744 | }, 745 | 746 | // CPU Register $4014: 747 | // Write 256 bytes of main memory 748 | // into Sprite RAM. 749 | sramDMA: function (value) { 750 | var baseAddress = value * 0x100; 751 | var data; 752 | for (var i = this.sramAddress; i < 256; i++) { 753 | data = this.nes.cpu.mem[baseAddress + i]; 754 | this.spriteMem[i] = data; 755 | this.spriteRamWriteUpdate(i, data); 756 | } 757 | 758 | this.nes.cpu.haltCycles(513); 759 | }, 760 | 761 | // Updates the scroll registers from a new VRAM address. 762 | regsFromAddress: function () { 763 | var address = (this.vramTmpAddress >> 8) & 0xff; 764 | this.regFV = (address >> 4) & 7; 765 | this.regV = (address >> 3) & 1; 766 | this.regH = (address >> 2) & 1; 767 | this.regVT = (this.regVT & 7) | ((address & 3) << 3); 768 | 769 | address = this.vramTmpAddress & 0xff; 770 | this.regVT = (this.regVT & 24) | ((address >> 5) & 7); 771 | this.regHT = address & 31; 772 | }, 773 | 774 | // Updates the scroll registers from a new VRAM address. 775 | cntsFromAddress: function () { 776 | var address = (this.vramAddress >> 8) & 0xff; 777 | this.cntFV = (address >> 4) & 3; 778 | this.cntV = (address >> 3) & 1; 779 | this.cntH = (address >> 2) & 1; 780 | this.cntVT = (this.cntVT & 7) | ((address & 3) << 3); 781 | 782 | address = this.vramAddress & 0xff; 783 | this.cntVT = (this.cntVT & 24) | ((address >> 5) & 7); 784 | this.cntHT = address & 31; 785 | }, 786 | 787 | regsToAddress: function () { 788 | var b1 = (this.regFV & 7) << 4; 789 | b1 |= (this.regV & 1) << 3; 790 | b1 |= (this.regH & 1) << 2; 791 | b1 |= (this.regVT >> 3) & 3; 792 | 793 | var b2 = (this.regVT & 7) << 5; 794 | b2 |= this.regHT & 31; 795 | 796 | this.vramTmpAddress = ((b1 << 8) | b2) & 0x7fff; 797 | }, 798 | 799 | cntsToAddress: function () { 800 | var b1 = (this.cntFV & 7) << 4; 801 | b1 |= (this.cntV & 1) << 3; 802 | b1 |= (this.cntH & 1) << 2; 803 | b1 |= (this.cntVT >> 3) & 3; 804 | 805 | var b2 = (this.cntVT & 7) << 5; 806 | b2 |= this.cntHT & 31; 807 | 808 | this.vramAddress = ((b1 << 8) | b2) & 0x7fff; 809 | }, 810 | 811 | incTileCounter: function (count) { 812 | for (var i = count; i !== 0; i--) { 813 | this.cntHT++; 814 | if (this.cntHT === 32) { 815 | this.cntHT = 0; 816 | this.cntVT++; 817 | if (this.cntVT >= 30) { 818 | this.cntH++; 819 | if (this.cntH === 2) { 820 | this.cntH = 0; 821 | this.cntV++; 822 | if (this.cntV === 2) { 823 | this.cntV = 0; 824 | this.cntFV++; 825 | this.cntFV &= 0x7; 826 | } 827 | } 828 | } 829 | } 830 | } 831 | }, 832 | 833 | // Reads from memory, taking into account 834 | // mirroring/mapping of address ranges. 835 | mirroredLoad: function (address) { 836 | return this.vramMem[this.vramMirrorTable[address]]; 837 | }, 838 | 839 | // Writes to memory, taking into account 840 | // mirroring/mapping of address ranges. 841 | mirroredWrite: function (address, value) { 842 | if (address >= 0x3f00 && address < 0x3f20) { 843 | // Palette write mirroring. 844 | if (address === 0x3f00 || address === 0x3f10) { 845 | this.writeMem(0x3f00, value); 846 | this.writeMem(0x3f10, value); 847 | } else if (address === 0x3f04 || address === 0x3f14) { 848 | this.writeMem(0x3f04, value); 849 | this.writeMem(0x3f14, value); 850 | } else if (address === 0x3f08 || address === 0x3f18) { 851 | this.writeMem(0x3f08, value); 852 | this.writeMem(0x3f18, value); 853 | } else if (address === 0x3f0c || address === 0x3f1c) { 854 | this.writeMem(0x3f0c, value); 855 | this.writeMem(0x3f1c, value); 856 | } else { 857 | this.writeMem(address, value); 858 | } 859 | } else { 860 | // Use lookup table for mirrored address: 861 | if (address < this.vramMirrorTable.length) { 862 | this.writeMem(this.vramMirrorTable[address], value); 863 | } else { 864 | throw new Error("Invalid VRAM address: " + address.toString(16)); 865 | } 866 | } 867 | }, 868 | 869 | triggerRendering: function () { 870 | if (this.scanline >= 21 && this.scanline <= 260) { 871 | // Render sprites, and combine: 872 | this.renderFramePartially( 873 | this.lastRenderedScanline + 1, 874 | this.scanline - 21 - this.lastRenderedScanline 875 | ); 876 | 877 | // Set last rendered scanline: 878 | this.lastRenderedScanline = this.scanline - 21; 879 | } 880 | }, 881 | 882 | renderFramePartially: function (startScan, scanCount) { 883 | if (this.f_spVisibility === 1) { 884 | this.renderSpritesPartially(startScan, scanCount, true); 885 | } 886 | 887 | if (this.f_bgVisibility === 1) { 888 | var si = startScan << 8; 889 | var ei = (startScan + scanCount) << 8; 890 | if (ei > 0xf000) { 891 | ei = 0xf000; 892 | } 893 | var buffer = this.buffer; 894 | var bgbuffer = this.bgbuffer; 895 | var pixrendered = this.pixrendered; 896 | for (var destIndex = si; destIndex < ei; destIndex++) { 897 | if (pixrendered[destIndex] > 0xff) { 898 | buffer[destIndex] = bgbuffer[destIndex]; 899 | } 900 | } 901 | } 902 | 903 | if (this.f_spVisibility === 1) { 904 | this.renderSpritesPartially(startScan, scanCount, false); 905 | } 906 | 907 | this.validTileData = false; 908 | }, 909 | 910 | renderBgScanline: function (bgbuffer, scan) { 911 | var baseTile = this.regS === 0 ? 0 : 256; 912 | var destIndex = (scan << 8) - this.regFH; 913 | 914 | this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH]; 915 | 916 | this.cntHT = this.regHT; 917 | this.cntH = this.regH; 918 | this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH]; 919 | 920 | if (scan < 240 && scan - this.cntFV >= 0) { 921 | var tscanoffset = this.cntFV << 3; 922 | var scantile = this.scantile; 923 | var attrib = this.attrib; 924 | var ptTile = this.ptTile; 925 | var nameTable = this.nameTable; 926 | var imgPalette = this.imgPalette; 927 | var pixrendered = this.pixrendered; 928 | var targetBuffer = bgbuffer ? this.bgbuffer : this.buffer; 929 | 930 | var t, tpix, att, col; 931 | 932 | for (var tile = 0; tile < 32; tile++) { 933 | if (scan >= 0) { 934 | // Fetch tile & attrib data: 935 | if (this.validTileData) { 936 | // Get data from array: 937 | t = scantile[tile]; 938 | if (typeof t === "undefined") { 939 | continue; 940 | } 941 | tpix = t.pix; 942 | att = attrib[tile]; 943 | } else { 944 | // Fetch data: 945 | t = 946 | ptTile[ 947 | baseTile + 948 | nameTable[this.curNt].getTileIndex(this.cntHT, this.cntVT) 949 | ]; 950 | if (typeof t === "undefined") { 951 | continue; 952 | } 953 | tpix = t.pix; 954 | att = nameTable[this.curNt].getAttrib(this.cntHT, this.cntVT); 955 | scantile[tile] = t; 956 | attrib[tile] = att; 957 | } 958 | 959 | // Render tile scanline: 960 | var sx = 0; 961 | var x = (tile << 3) - this.regFH; 962 | 963 | if (x > -8) { 964 | if (x < 0) { 965 | destIndex -= x; 966 | sx = -x; 967 | } 968 | if (t.opaque[this.cntFV]) { 969 | for (; sx < 8; sx++) { 970 | targetBuffer[destIndex] = 971 | imgPalette[tpix[tscanoffset + sx] + att]; 972 | pixrendered[destIndex] |= 256; 973 | destIndex++; 974 | } 975 | } else { 976 | for (; sx < 8; sx++) { 977 | col = tpix[tscanoffset + sx]; 978 | if (col !== 0) { 979 | targetBuffer[destIndex] = imgPalette[col + att]; 980 | pixrendered[destIndex] |= 256; 981 | } 982 | destIndex++; 983 | } 984 | } 985 | } 986 | } 987 | 988 | // Increase Horizontal Tile Counter: 989 | if (++this.cntHT === 32) { 990 | this.cntHT = 0; 991 | this.cntH++; 992 | this.cntH %= 2; 993 | this.curNt = this.ntable1[(this.cntV << 1) + this.cntH]; 994 | } 995 | } 996 | 997 | // Tile data for one row should now have been fetched, 998 | // so the data in the array is valid. 999 | this.validTileData = true; 1000 | } 1001 | 1002 | // update vertical scroll: 1003 | this.cntFV++; 1004 | if (this.cntFV === 8) { 1005 | this.cntFV = 0; 1006 | this.cntVT++; 1007 | if (this.cntVT === 30) { 1008 | this.cntVT = 0; 1009 | this.cntV++; 1010 | this.cntV %= 2; 1011 | this.curNt = this.ntable1[(this.cntV << 1) + this.cntH]; 1012 | } else if (this.cntVT === 32) { 1013 | this.cntVT = 0; 1014 | } 1015 | 1016 | // Invalidate fetched data: 1017 | this.validTileData = false; 1018 | } 1019 | }, 1020 | 1021 | renderSpritesPartially: function (startscan, scancount, bgPri) { 1022 | if (this.f_spVisibility === 1) { 1023 | for (var i = 0; i < 64; i++) { 1024 | if ( 1025 | this.bgPriority[i] === bgPri && 1026 | this.sprX[i] >= 0 && 1027 | this.sprX[i] < 256 && 1028 | this.sprY[i] + 8 >= startscan && 1029 | this.sprY[i] < startscan + scancount 1030 | ) { 1031 | // Show sprite. 1032 | if (this.f_spriteSize === 0) { 1033 | // 8x8 sprites 1034 | 1035 | this.srcy1 = 0; 1036 | this.srcy2 = 8; 1037 | 1038 | if (this.sprY[i] < startscan) { 1039 | this.srcy1 = startscan - this.sprY[i] - 1; 1040 | } 1041 | 1042 | if (this.sprY[i] + 8 > startscan + scancount) { 1043 | this.srcy2 = startscan + scancount - this.sprY[i] + 1; 1044 | } 1045 | 1046 | if (this.f_spPatternTable === 0) { 1047 | this.ptTile[this.sprTile[i]].render( 1048 | this.buffer, 1049 | 0, 1050 | this.srcy1, 1051 | 8, 1052 | this.srcy2, 1053 | this.sprX[i], 1054 | this.sprY[i] + 1, 1055 | this.sprCol[i], 1056 | this.sprPalette, 1057 | this.horiFlip[i], 1058 | this.vertFlip[i], 1059 | i, 1060 | this.pixrendered 1061 | ); 1062 | } else { 1063 | this.ptTile[this.sprTile[i] + 256].render( 1064 | this.buffer, 1065 | 0, 1066 | this.srcy1, 1067 | 8, 1068 | this.srcy2, 1069 | this.sprX[i], 1070 | this.sprY[i] + 1, 1071 | this.sprCol[i], 1072 | this.sprPalette, 1073 | this.horiFlip[i], 1074 | this.vertFlip[i], 1075 | i, 1076 | this.pixrendered 1077 | ); 1078 | } 1079 | } else { 1080 | // 8x16 sprites 1081 | var top = this.sprTile[i]; 1082 | if ((top & 1) !== 0) { 1083 | top = this.sprTile[i] - 1 + 256; 1084 | } 1085 | 1086 | var srcy1 = 0; 1087 | var srcy2 = 8; 1088 | 1089 | if (this.sprY[i] < startscan) { 1090 | srcy1 = startscan - this.sprY[i] - 1; 1091 | } 1092 | 1093 | if (this.sprY[i] + 8 > startscan + scancount) { 1094 | srcy2 = startscan + scancount - this.sprY[i]; 1095 | } 1096 | 1097 | this.ptTile[top + (this.vertFlip[i] ? 1 : 0)].render( 1098 | this.buffer, 1099 | 0, 1100 | srcy1, 1101 | 8, 1102 | srcy2, 1103 | this.sprX[i], 1104 | this.sprY[i] + 1, 1105 | this.sprCol[i], 1106 | this.sprPalette, 1107 | this.horiFlip[i], 1108 | this.vertFlip[i], 1109 | i, 1110 | this.pixrendered 1111 | ); 1112 | 1113 | srcy1 = 0; 1114 | srcy2 = 8; 1115 | 1116 | if (this.sprY[i] + 8 < startscan) { 1117 | srcy1 = startscan - (this.sprY[i] + 8 + 1); 1118 | } 1119 | 1120 | if (this.sprY[i] + 16 > startscan + scancount) { 1121 | srcy2 = startscan + scancount - (this.sprY[i] + 8); 1122 | } 1123 | 1124 | this.ptTile[top + (this.vertFlip[i] ? 0 : 1)].render( 1125 | this.buffer, 1126 | 0, 1127 | srcy1, 1128 | 8, 1129 | srcy2, 1130 | this.sprX[i], 1131 | this.sprY[i] + 1 + 8, 1132 | this.sprCol[i], 1133 | this.sprPalette, 1134 | this.horiFlip[i], 1135 | this.vertFlip[i], 1136 | i, 1137 | this.pixrendered 1138 | ); 1139 | } 1140 | } 1141 | } 1142 | } 1143 | }, 1144 | 1145 | checkSprite0: function (scan) { 1146 | this.spr0HitX = -1; 1147 | this.spr0HitY = -1; 1148 | 1149 | var toffset; 1150 | var tIndexAdd = this.f_spPatternTable === 0 ? 0 : 256; 1151 | var x, y, t, i; 1152 | var bufferIndex; 1153 | 1154 | x = this.sprX[0]; 1155 | y = this.sprY[0] + 1; 1156 | 1157 | if (this.f_spriteSize === 0) { 1158 | // 8x8 sprites. 1159 | 1160 | // Check range: 1161 | if (y <= scan && y + 8 > scan && x >= -7 && x < 256) { 1162 | // Sprite is in range. 1163 | // Draw scanline: 1164 | t = this.ptTile[this.sprTile[0] + tIndexAdd]; 1165 | 1166 | if (this.vertFlip[0]) { 1167 | toffset = 7 - (scan - y); 1168 | } else { 1169 | toffset = scan - y; 1170 | } 1171 | toffset *= 8; 1172 | 1173 | bufferIndex = scan * 256 + x; 1174 | if (this.horiFlip[0]) { 1175 | for (i = 7; i >= 0; i--) { 1176 | if (x >= 0 && x < 256) { 1177 | if ( 1178 | bufferIndex >= 0 && 1179 | bufferIndex < 61440 && 1180 | this.pixrendered[bufferIndex] !== 0 1181 | ) { 1182 | if (t.pix[toffset + i] !== 0) { 1183 | this.spr0HitX = bufferIndex % 256; 1184 | this.spr0HitY = scan; 1185 | return true; 1186 | } 1187 | } 1188 | } 1189 | x++; 1190 | bufferIndex++; 1191 | } 1192 | } else { 1193 | for (i = 0; i < 8; i++) { 1194 | if (x >= 0 && x < 256) { 1195 | if ( 1196 | bufferIndex >= 0 && 1197 | bufferIndex < 61440 && 1198 | this.pixrendered[bufferIndex] !== 0 1199 | ) { 1200 | if (t.pix[toffset + i] !== 0) { 1201 | this.spr0HitX = bufferIndex % 256; 1202 | this.spr0HitY = scan; 1203 | return true; 1204 | } 1205 | } 1206 | } 1207 | x++; 1208 | bufferIndex++; 1209 | } 1210 | } 1211 | } 1212 | } else { 1213 | // 8x16 sprites: 1214 | 1215 | // Check range: 1216 | if (y <= scan && y + 16 > scan && x >= -7 && x < 256) { 1217 | // Sprite is in range. 1218 | // Draw scanline: 1219 | 1220 | if (this.vertFlip[0]) { 1221 | toffset = 15 - (scan - y); 1222 | } else { 1223 | toffset = scan - y; 1224 | } 1225 | 1226 | if (toffset < 8) { 1227 | // first half of sprite. 1228 | t = this.ptTile[ 1229 | this.sprTile[0] + 1230 | (this.vertFlip[0] ? 1 : 0) + 1231 | ((this.sprTile[0] & 1) !== 0 ? 255 : 0) 1232 | ]; 1233 | } else { 1234 | // second half of sprite. 1235 | t = this.ptTile[ 1236 | this.sprTile[0] + 1237 | (this.vertFlip[0] ? 0 : 1) + 1238 | ((this.sprTile[0] & 1) !== 0 ? 255 : 0) 1239 | ]; 1240 | if (this.vertFlip[0]) { 1241 | toffset = 15 - toffset; 1242 | } else { 1243 | toffset -= 8; 1244 | } 1245 | } 1246 | toffset *= 8; 1247 | 1248 | bufferIndex = scan * 256 + x; 1249 | if (this.horiFlip[0]) { 1250 | for (i = 7; i >= 0; i--) { 1251 | if (x >= 0 && x < 256) { 1252 | if ( 1253 | bufferIndex >= 0 && 1254 | bufferIndex < 61440 && 1255 | this.pixrendered[bufferIndex] !== 0 1256 | ) { 1257 | if (t.pix[toffset + i] !== 0) { 1258 | this.spr0HitX = bufferIndex % 256; 1259 | this.spr0HitY = scan; 1260 | return true; 1261 | } 1262 | } 1263 | } 1264 | x++; 1265 | bufferIndex++; 1266 | } 1267 | } else { 1268 | for (i = 0; i < 8; i++) { 1269 | if (x >= 0 && x < 256) { 1270 | if ( 1271 | bufferIndex >= 0 && 1272 | bufferIndex < 61440 && 1273 | this.pixrendered[bufferIndex] !== 0 1274 | ) { 1275 | if (t.pix[toffset + i] !== 0) { 1276 | this.spr0HitX = bufferIndex % 256; 1277 | this.spr0HitY = scan; 1278 | return true; 1279 | } 1280 | } 1281 | } 1282 | x++; 1283 | bufferIndex++; 1284 | } 1285 | } 1286 | } 1287 | } 1288 | 1289 | return false; 1290 | }, 1291 | 1292 | // This will write to PPU memory, and 1293 | // update internally buffered data 1294 | // appropriately. 1295 | writeMem: function (address, value) { 1296 | this.vramMem[address] = value; 1297 | 1298 | // Update internally buffered data: 1299 | if (address < 0x2000) { 1300 | this.vramMem[address] = value; 1301 | this.patternWrite(address, value); 1302 | } else if (address >= 0x2000 && address < 0x23c0) { 1303 | this.nameTableWrite(this.ntable1[0], address - 0x2000, value); 1304 | } else if (address >= 0x23c0 && address < 0x2400) { 1305 | this.attribTableWrite(this.ntable1[0], address - 0x23c0, value); 1306 | } else if (address >= 0x2400 && address < 0x27c0) { 1307 | this.nameTableWrite(this.ntable1[1], address - 0x2400, value); 1308 | } else if (address >= 0x27c0 && address < 0x2800) { 1309 | this.attribTableWrite(this.ntable1[1], address - 0x27c0, value); 1310 | } else if (address >= 0x2800 && address < 0x2bc0) { 1311 | this.nameTableWrite(this.ntable1[2], address - 0x2800, value); 1312 | } else if (address >= 0x2bc0 && address < 0x2c00) { 1313 | this.attribTableWrite(this.ntable1[2], address - 0x2bc0, value); 1314 | } else if (address >= 0x2c00 && address < 0x2fc0) { 1315 | this.nameTableWrite(this.ntable1[3], address - 0x2c00, value); 1316 | } else if (address >= 0x2fc0 && address < 0x3000) { 1317 | this.attribTableWrite(this.ntable1[3], address - 0x2fc0, value); 1318 | } else if (address >= 0x3f00 && address < 0x3f20) { 1319 | this.updatePalettes(); 1320 | } 1321 | }, 1322 | 1323 | // Reads data from $3f00 to $f20 1324 | // into the two buffered palettes. 1325 | updatePalettes: function () { 1326 | var i; 1327 | 1328 | for (i = 0; i < 16; i++) { 1329 | if (this.f_dispType === 0) { 1330 | this.imgPalette[i] = this.palTable.getEntry( 1331 | this.vramMem[0x3f00 + i] & 63 1332 | ); 1333 | } else { 1334 | this.imgPalette[i] = this.palTable.getEntry( 1335 | this.vramMem[0x3f00 + i] & 32 1336 | ); 1337 | } 1338 | } 1339 | for (i = 0; i < 16; i++) { 1340 | if (this.f_dispType === 0) { 1341 | this.sprPalette[i] = this.palTable.getEntry( 1342 | this.vramMem[0x3f10 + i] & 63 1343 | ); 1344 | } else { 1345 | this.sprPalette[i] = this.palTable.getEntry( 1346 | this.vramMem[0x3f10 + i] & 32 1347 | ); 1348 | } 1349 | } 1350 | }, 1351 | 1352 | // Updates the internal pattern 1353 | // table buffers with this new byte. 1354 | // In vNES, there is a version of this with 4 arguments which isn't used. 1355 | patternWrite: function (address, value) { 1356 | var tileIndex = Math.floor(address / 16); 1357 | var leftOver = address % 16; 1358 | if (leftOver < 8) { 1359 | this.ptTile[tileIndex].setScanline( 1360 | leftOver, 1361 | value, 1362 | this.vramMem[address + 8] 1363 | ); 1364 | } else { 1365 | this.ptTile[tileIndex].setScanline( 1366 | leftOver - 8, 1367 | this.vramMem[address - 8], 1368 | value 1369 | ); 1370 | } 1371 | }, 1372 | 1373 | // Updates the internal name table buffers 1374 | // with this new byte. 1375 | nameTableWrite: function (index, address, value) { 1376 | this.nameTable[index].tile[address] = value; 1377 | 1378 | // Update Sprite #0 hit: 1379 | //updateSpr0Hit(); 1380 | this.checkSprite0(this.scanline - 20); 1381 | }, 1382 | 1383 | // Updates the internal pattern 1384 | // table buffers with this new attribute 1385 | // table byte. 1386 | attribTableWrite: function (index, address, value) { 1387 | this.nameTable[index].writeAttrib(address, value); 1388 | }, 1389 | 1390 | // Updates the internally buffered sprite 1391 | // data with this new byte of info. 1392 | spriteRamWriteUpdate: function (address, value) { 1393 | var tIndex = Math.floor(address / 4); 1394 | 1395 | if (tIndex === 0) { 1396 | //updateSpr0Hit(); 1397 | this.checkSprite0(this.scanline - 20); 1398 | } 1399 | 1400 | if (address % 4 === 0) { 1401 | // Y coordinate 1402 | this.sprY[tIndex] = value; 1403 | } else if (address % 4 === 1) { 1404 | // Tile index 1405 | this.sprTile[tIndex] = value; 1406 | } else if (address % 4 === 2) { 1407 | // Attributes 1408 | this.vertFlip[tIndex] = (value & 0x80) !== 0; 1409 | this.horiFlip[tIndex] = (value & 0x40) !== 0; 1410 | this.bgPriority[tIndex] = (value & 0x20) !== 0; 1411 | this.sprCol[tIndex] = (value & 3) << 2; 1412 | } else if (address % 4 === 3) { 1413 | // X coordinate 1414 | this.sprX[tIndex] = value; 1415 | } 1416 | }, 1417 | 1418 | doNMI: function () { 1419 | // Set VBlank flag: 1420 | this.setStatusFlag(this.STATUS_VBLANK, true); 1421 | //nes.getCpu().doNonMaskableInterrupt(); 1422 | this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI); 1423 | }, 1424 | 1425 | isPixelWhite: function (x, y) { 1426 | this.triggerRendering(); 1427 | return this.nes.ppu.buffer[(y << 8) + x] === 0xffffff; 1428 | }, 1429 | 1430 | JSON_PROPERTIES: [ 1431 | // Memory 1432 | "vramMem", 1433 | "spriteMem", 1434 | // Counters 1435 | "cntFV", 1436 | "cntV", 1437 | "cntH", 1438 | "cntVT", 1439 | "cntHT", 1440 | // Registers 1441 | "regFV", 1442 | "regV", 1443 | "regH", 1444 | "regVT", 1445 | "regHT", 1446 | "regFH", 1447 | "regS", 1448 | // VRAM addr 1449 | "vramAddress", 1450 | "vramTmpAddress", 1451 | // Control/Status registers 1452 | "f_nmiOnVblank", 1453 | "f_spriteSize", 1454 | "f_bgPatternTable", 1455 | "f_spPatternTable", 1456 | "f_addrInc", 1457 | "f_nTblAddress", 1458 | "f_color", 1459 | "f_spVisibility", 1460 | "f_bgVisibility", 1461 | "f_spClipping", 1462 | "f_bgClipping", 1463 | "f_dispType", 1464 | // VRAM I/O 1465 | "vramBufferedReadValue", 1466 | "firstWrite", 1467 | // Mirroring 1468 | "currentMirroring", 1469 | "vramMirrorTable", 1470 | "ntable1", 1471 | // SPR-RAM I/O 1472 | "sramAddress", 1473 | // Sprites. Most sprite data is rebuilt from spriteMem 1474 | "hitSpr0", 1475 | // Palettes 1476 | "sprPalette", 1477 | "imgPalette", 1478 | // Rendering progression 1479 | "curX", 1480 | "scanline", 1481 | "lastRenderedScanline", 1482 | "curNt", 1483 | "scantile", 1484 | // Used during rendering 1485 | "attrib", 1486 | "buffer", 1487 | "bgbuffer", 1488 | "pixrendered", 1489 | // Misc 1490 | "requestEndFrame", 1491 | "nmiOk", 1492 | "dummyCycleToggle", 1493 | "nmiCounter", 1494 | "validTileData", 1495 | "scanlineAlreadyRendered", 1496 | ], 1497 | 1498 | toJSON: function () { 1499 | var i; 1500 | var state = utils.toJSON(this); 1501 | 1502 | state.nameTable = []; 1503 | for (i = 0; i < this.nameTable.length; i++) { 1504 | state.nameTable[i] = this.nameTable[i].toJSON(); 1505 | } 1506 | 1507 | state.ptTile = []; 1508 | for (i = 0; i < this.ptTile.length; i++) { 1509 | state.ptTile[i] = this.ptTile[i].toJSON(); 1510 | } 1511 | 1512 | return state; 1513 | }, 1514 | 1515 | fromJSON: function (state) { 1516 | var i; 1517 | 1518 | utils.fromJSON(this, state); 1519 | 1520 | for (i = 0; i < this.nameTable.length; i++) { 1521 | this.nameTable[i].fromJSON(state.nameTable[i]); 1522 | } 1523 | 1524 | for (i = 0; i < this.ptTile.length; i++) { 1525 | this.ptTile[i].fromJSON(state.ptTile[i]); 1526 | } 1527 | 1528 | // Sprite data: 1529 | for (i = 0; i < this.spriteMem.length; i++) { 1530 | this.spriteRamWriteUpdate(i, this.spriteMem[i]); 1531 | } 1532 | }, 1533 | }; 1534 | 1535 | var NameTable = function (width, height, name) { 1536 | this.width = width; 1537 | this.height = height; 1538 | this.name = name; 1539 | 1540 | this.tile = new Array(width * height); 1541 | this.attrib = new Array(width * height); 1542 | for (var i = 0; i < width * height; i++) { 1543 | this.tile[i] = 0; 1544 | this.attrib[i] = 0; 1545 | } 1546 | }; 1547 | 1548 | NameTable.prototype = { 1549 | getTileIndex: function (x, y) { 1550 | return this.tile[y * this.width + x]; 1551 | }, 1552 | 1553 | getAttrib: function (x, y) { 1554 | return this.attrib[y * this.width + x]; 1555 | }, 1556 | 1557 | writeAttrib: function (index, value) { 1558 | var basex = (index % 8) * 4; 1559 | var basey = Math.floor(index / 8) * 4; 1560 | var add; 1561 | var tx, ty; 1562 | var attindex; 1563 | 1564 | for (var sqy = 0; sqy < 2; sqy++) { 1565 | for (var sqx = 0; sqx < 2; sqx++) { 1566 | add = (value >> (2 * (sqy * 2 + sqx))) & 3; 1567 | for (var y = 0; y < 2; y++) { 1568 | for (var x = 0; x < 2; x++) { 1569 | tx = basex + sqx * 2 + x; 1570 | ty = basey + sqy * 2 + y; 1571 | attindex = ty * this.width + tx; 1572 | this.attrib[attindex] = (add << 2) & 12; 1573 | } 1574 | } 1575 | } 1576 | } 1577 | }, 1578 | 1579 | toJSON: function () { 1580 | return { 1581 | tile: this.tile, 1582 | attrib: this.attrib, 1583 | }; 1584 | }, 1585 | 1586 | fromJSON: function (s) { 1587 | this.tile = s.tile; 1588 | this.attrib = s.attrib; 1589 | }, 1590 | }; 1591 | 1592 | var PaletteTable = function () { 1593 | this.curTable = new Array(64); 1594 | this.emphTable = new Array(8); 1595 | this.currentEmph = -1; 1596 | }; 1597 | 1598 | PaletteTable.prototype = { 1599 | reset: function () { 1600 | this.setEmphasis(0); 1601 | }, 1602 | 1603 | loadNTSCPalette: function () { 1604 | // prettier-ignore 1605 | this.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000]; 1606 | this.makeTables(); 1607 | this.setEmphasis(0); 1608 | }, 1609 | 1610 | loadPALPalette: function () { 1611 | // prettier-ignore 1612 | this.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000]; 1613 | this.makeTables(); 1614 | this.setEmphasis(0); 1615 | }, 1616 | 1617 | makeTables: function () { 1618 | var r, g, b, col, i, rFactor, gFactor, bFactor; 1619 | 1620 | // Calculate a table for each possible emphasis setting: 1621 | for (var emph = 0; emph < 8; emph++) { 1622 | // Determine color component factors: 1623 | rFactor = 1.0; 1624 | gFactor = 1.0; 1625 | bFactor = 1.0; 1626 | 1627 | if ((emph & 1) !== 0) { 1628 | rFactor = 0.75; 1629 | bFactor = 0.75; 1630 | } 1631 | if ((emph & 2) !== 0) { 1632 | rFactor = 0.75; 1633 | gFactor = 0.75; 1634 | } 1635 | if ((emph & 4) !== 0) { 1636 | gFactor = 0.75; 1637 | bFactor = 0.75; 1638 | } 1639 | 1640 | this.emphTable[emph] = new Array(64); 1641 | 1642 | // Calculate table: 1643 | for (i = 0; i < 64; i++) { 1644 | col = this.curTable[i]; 1645 | r = Math.floor(this.getRed(col) * rFactor); 1646 | g = Math.floor(this.getGreen(col) * gFactor); 1647 | b = Math.floor(this.getBlue(col) * bFactor); 1648 | this.emphTable[emph][i] = this.getRgb(r, g, b); 1649 | } 1650 | } 1651 | }, 1652 | 1653 | setEmphasis: function (emph) { 1654 | if (emph !== this.currentEmph) { 1655 | this.currentEmph = emph; 1656 | for (var i = 0; i < 64; i++) { 1657 | this.curTable[i] = this.emphTable[emph][i]; 1658 | } 1659 | } 1660 | }, 1661 | 1662 | getEntry: function (yiq) { 1663 | return this.curTable[yiq]; 1664 | }, 1665 | 1666 | getRed: function (rgb) { 1667 | return (rgb >> 16) & 0xff; 1668 | }, 1669 | 1670 | getGreen: function (rgb) { 1671 | return (rgb >> 8) & 0xff; 1672 | }, 1673 | 1674 | getBlue: function (rgb) { 1675 | return rgb & 0xff; 1676 | }, 1677 | 1678 | getRgb: function (r, g, b) { 1679 | return (r << 16) | (g << 8) | b; 1680 | }, 1681 | 1682 | loadDefaultPalette: function () { 1683 | this.curTable[0] = this.getRgb(117, 117, 117); 1684 | this.curTable[1] = this.getRgb(39, 27, 143); 1685 | this.curTable[2] = this.getRgb(0, 0, 171); 1686 | this.curTable[3] = this.getRgb(71, 0, 159); 1687 | this.curTable[4] = this.getRgb(143, 0, 119); 1688 | this.curTable[5] = this.getRgb(171, 0, 19); 1689 | this.curTable[6] = this.getRgb(167, 0, 0); 1690 | this.curTable[7] = this.getRgb(127, 11, 0); 1691 | this.curTable[8] = this.getRgb(67, 47, 0); 1692 | this.curTable[9] = this.getRgb(0, 71, 0); 1693 | this.curTable[10] = this.getRgb(0, 81, 0); 1694 | this.curTable[11] = this.getRgb(0, 63, 23); 1695 | this.curTable[12] = this.getRgb(27, 63, 95); 1696 | this.curTable[13] = this.getRgb(0, 0, 0); 1697 | this.curTable[14] = this.getRgb(0, 0, 0); 1698 | this.curTable[15] = this.getRgb(0, 0, 0); 1699 | this.curTable[16] = this.getRgb(188, 188, 188); 1700 | this.curTable[17] = this.getRgb(0, 115, 239); 1701 | this.curTable[18] = this.getRgb(35, 59, 239); 1702 | this.curTable[19] = this.getRgb(131, 0, 243); 1703 | this.curTable[20] = this.getRgb(191, 0, 191); 1704 | this.curTable[21] = this.getRgb(231, 0, 91); 1705 | this.curTable[22] = this.getRgb(219, 43, 0); 1706 | this.curTable[23] = this.getRgb(203, 79, 15); 1707 | this.curTable[24] = this.getRgb(139, 115, 0); 1708 | this.curTable[25] = this.getRgb(0, 151, 0); 1709 | this.curTable[26] = this.getRgb(0, 171, 0); 1710 | this.curTable[27] = this.getRgb(0, 147, 59); 1711 | this.curTable[28] = this.getRgb(0, 131, 139); 1712 | this.curTable[29] = this.getRgb(0, 0, 0); 1713 | this.curTable[30] = this.getRgb(0, 0, 0); 1714 | this.curTable[31] = this.getRgb(0, 0, 0); 1715 | this.curTable[32] = this.getRgb(255, 255, 255); 1716 | this.curTable[33] = this.getRgb(63, 191, 255); 1717 | this.curTable[34] = this.getRgb(95, 151, 255); 1718 | this.curTable[35] = this.getRgb(167, 139, 253); 1719 | this.curTable[36] = this.getRgb(247, 123, 255); 1720 | this.curTable[37] = this.getRgb(255, 119, 183); 1721 | this.curTable[38] = this.getRgb(255, 119, 99); 1722 | this.curTable[39] = this.getRgb(255, 155, 59); 1723 | this.curTable[40] = this.getRgb(243, 191, 63); 1724 | this.curTable[41] = this.getRgb(131, 211, 19); 1725 | this.curTable[42] = this.getRgb(79, 223, 75); 1726 | this.curTable[43] = this.getRgb(88, 248, 152); 1727 | this.curTable[44] = this.getRgb(0, 235, 219); 1728 | this.curTable[45] = this.getRgb(0, 0, 0); 1729 | this.curTable[46] = this.getRgb(0, 0, 0); 1730 | this.curTable[47] = this.getRgb(0, 0, 0); 1731 | this.curTable[48] = this.getRgb(255, 255, 255); 1732 | this.curTable[49] = this.getRgb(171, 231, 255); 1733 | this.curTable[50] = this.getRgb(199, 215, 255); 1734 | this.curTable[51] = this.getRgb(215, 203, 255); 1735 | this.curTable[52] = this.getRgb(255, 199, 255); 1736 | this.curTable[53] = this.getRgb(255, 199, 219); 1737 | this.curTable[54] = this.getRgb(255, 191, 179); 1738 | this.curTable[55] = this.getRgb(255, 219, 171); 1739 | this.curTable[56] = this.getRgb(255, 231, 163); 1740 | this.curTable[57] = this.getRgb(227, 255, 163); 1741 | this.curTable[58] = this.getRgb(171, 243, 191); 1742 | this.curTable[59] = this.getRgb(179, 255, 207); 1743 | this.curTable[60] = this.getRgb(159, 255, 243); 1744 | this.curTable[61] = this.getRgb(0, 0, 0); 1745 | this.curTable[62] = this.getRgb(0, 0, 0); 1746 | this.curTable[63] = this.getRgb(0, 0, 0); 1747 | 1748 | this.makeTables(); 1749 | this.setEmphasis(0); 1750 | }, 1751 | }; 1752 | 1753 | module.exports = PPU; 1754 | -------------------------------------------------------------------------------- /src/rom.js: -------------------------------------------------------------------------------- 1 | var Mappers = require("./mappers"); 2 | var Tile = require("./tile"); 3 | 4 | var ROM = function (nes) { 5 | this.nes = nes; 6 | 7 | this.mapperName = new Array(92); 8 | 9 | for (var i = 0; i < 92; i++) { 10 | this.mapperName[i] = "Unknown Mapper"; 11 | } 12 | this.mapperName[0] = "Direct Access"; 13 | this.mapperName[1] = "Nintendo MMC1"; 14 | this.mapperName[2] = "UNROM"; 15 | this.mapperName[3] = "CNROM"; 16 | this.mapperName[4] = "Nintendo MMC3"; 17 | this.mapperName[5] = "Nintendo MMC5"; 18 | this.mapperName[6] = "FFE F4xxx"; 19 | this.mapperName[7] = "AOROM"; 20 | this.mapperName[8] = "FFE F3xxx"; 21 | this.mapperName[9] = "Nintendo MMC2"; 22 | this.mapperName[10] = "Nintendo MMC4"; 23 | this.mapperName[11] = "Color Dreams Chip"; 24 | this.mapperName[12] = "FFE F6xxx"; 25 | this.mapperName[15] = "100-in-1 switch"; 26 | this.mapperName[16] = "Bandai chip"; 27 | this.mapperName[17] = "FFE F8xxx"; 28 | this.mapperName[18] = "Jaleco SS8806 chip"; 29 | this.mapperName[19] = "Namcot 106 chip"; 30 | this.mapperName[20] = "Famicom Disk System"; 31 | this.mapperName[21] = "Konami VRC4a"; 32 | this.mapperName[22] = "Konami VRC2a"; 33 | this.mapperName[23] = "Konami VRC2a"; 34 | this.mapperName[24] = "Konami VRC6"; 35 | this.mapperName[25] = "Konami VRC4b"; 36 | this.mapperName[32] = "Irem G-101 chip"; 37 | this.mapperName[33] = "Taito TC0190/TC0350"; 38 | this.mapperName[34] = "32kB ROM switch"; 39 | 40 | this.mapperName[64] = "Tengen RAMBO-1 chip"; 41 | this.mapperName[65] = "Irem H-3001 chip"; 42 | this.mapperName[66] = "GNROM switch"; 43 | this.mapperName[67] = "SunSoft3 chip"; 44 | this.mapperName[68] = "SunSoft4 chip"; 45 | this.mapperName[69] = "SunSoft5 FME-7 chip"; 46 | this.mapperName[71] = "Camerica chip"; 47 | this.mapperName[78] = "Irem 74HC161/32-based"; 48 | this.mapperName[91] = "Pirate HK-SF3 chip"; 49 | }; 50 | 51 | ROM.prototype = { 52 | // Mirroring types: 53 | VERTICAL_MIRRORING: 0, 54 | HORIZONTAL_MIRRORING: 1, 55 | FOURSCREEN_MIRRORING: 2, 56 | SINGLESCREEN_MIRRORING: 3, 57 | SINGLESCREEN_MIRRORING2: 4, 58 | SINGLESCREEN_MIRRORING3: 5, 59 | SINGLESCREEN_MIRRORING4: 6, 60 | CHRROM_MIRRORING: 7, 61 | 62 | header: null, 63 | rom: null, 64 | vrom: null, 65 | vromTile: null, 66 | 67 | romCount: null, 68 | vromCount: null, 69 | mirroring: null, 70 | batteryRam: null, 71 | trainer: null, 72 | fourScreen: null, 73 | mapperType: null, 74 | valid: false, 75 | 76 | load: function (data) { 77 | var i, j, v; 78 | 79 | if (data.indexOf("NES\x1a") === -1) { 80 | throw new Error("Not a valid NES ROM."); 81 | } 82 | this.header = new Array(16); 83 | for (i = 0; i < 16; i++) { 84 | this.header[i] = data.charCodeAt(i) & 0xff; 85 | } 86 | this.romCount = this.header[4]; 87 | this.vromCount = this.header[5] * 2; // Get the number of 4kB banks, not 8kB 88 | this.mirroring = (this.header[6] & 1) !== 0 ? 1 : 0; 89 | this.batteryRam = (this.header[6] & 2) !== 0; 90 | this.trainer = (this.header[6] & 4) !== 0; 91 | this.fourScreen = (this.header[6] & 8) !== 0; 92 | this.mapperType = (this.header[6] >> 4) | (this.header[7] & 0xf0); 93 | /* TODO 94 | if (this.batteryRam) 95 | this.loadBatteryRam();*/ 96 | // Check whether byte 8-15 are zero's: 97 | var foundError = false; 98 | for (i = 8; i < 16; i++) { 99 | if (this.header[i] !== 0) { 100 | foundError = true; 101 | break; 102 | } 103 | } 104 | if (foundError) { 105 | this.mapperType &= 0xf; // Ignore byte 7 106 | } 107 | // Load PRG-ROM banks: 108 | this.rom = new Array(this.romCount); 109 | var offset = 16; 110 | for (i = 0; i < this.romCount; i++) { 111 | this.rom[i] = new Array(16384); 112 | for (j = 0; j < 16384; j++) { 113 | if (offset + j >= data.length) { 114 | break; 115 | } 116 | this.rom[i][j] = data.charCodeAt(offset + j) & 0xff; 117 | } 118 | offset += 16384; 119 | } 120 | // Load CHR-ROM banks: 121 | this.vrom = new Array(this.vromCount); 122 | for (i = 0; i < this.vromCount; i++) { 123 | this.vrom[i] = new Array(4096); 124 | for (j = 0; j < 4096; j++) { 125 | if (offset + j >= data.length) { 126 | break; 127 | } 128 | this.vrom[i][j] = data.charCodeAt(offset + j) & 0xff; 129 | } 130 | offset += 4096; 131 | } 132 | 133 | // Create VROM tiles: 134 | this.vromTile = new Array(this.vromCount); 135 | for (i = 0; i < this.vromCount; i++) { 136 | this.vromTile[i] = new Array(256); 137 | for (j = 0; j < 256; j++) { 138 | this.vromTile[i][j] = new Tile(); 139 | } 140 | } 141 | 142 | // Convert CHR-ROM banks to tiles: 143 | var tileIndex; 144 | var leftOver; 145 | for (v = 0; v < this.vromCount; v++) { 146 | for (i = 0; i < 4096; i++) { 147 | tileIndex = i >> 4; 148 | leftOver = i % 16; 149 | if (leftOver < 8) { 150 | this.vromTile[v][tileIndex].setScanline( 151 | leftOver, 152 | this.vrom[v][i], 153 | this.vrom[v][i + 8] 154 | ); 155 | } else { 156 | this.vromTile[v][tileIndex].setScanline( 157 | leftOver - 8, 158 | this.vrom[v][i - 8], 159 | this.vrom[v][i] 160 | ); 161 | } 162 | } 163 | } 164 | 165 | this.valid = true; 166 | }, 167 | 168 | getMirroringType: function () { 169 | if (this.fourScreen) { 170 | return this.FOURSCREEN_MIRRORING; 171 | } 172 | if (this.mirroring === 0) { 173 | return this.HORIZONTAL_MIRRORING; 174 | } 175 | return this.VERTICAL_MIRRORING; 176 | }, 177 | 178 | getMapperName: function () { 179 | if (this.mapperType >= 0 && this.mapperType < this.mapperName.length) { 180 | return this.mapperName[this.mapperType]; 181 | } 182 | return "Unknown Mapper, " + this.mapperType; 183 | }, 184 | 185 | mapperSupported: function () { 186 | return typeof Mappers[this.mapperType] !== "undefined"; 187 | }, 188 | 189 | createMapper: function () { 190 | if (this.mapperSupported()) { 191 | return new Mappers[this.mapperType](this.nes); 192 | } else { 193 | throw new Error( 194 | "This ROM uses a mapper not supported by JSNES: " + 195 | this.getMapperName() + 196 | "(" + 197 | this.mapperType + 198 | ")" 199 | ); 200 | } 201 | }, 202 | }; 203 | 204 | module.exports = ROM; 205 | -------------------------------------------------------------------------------- /src/tile.js: -------------------------------------------------------------------------------- 1 | var Tile = function () { 2 | // Tile data: 3 | this.pix = new Array(64); 4 | 5 | this.fbIndex = null; 6 | this.tIndex = null; 7 | this.x = null; 8 | this.y = null; 9 | this.w = null; 10 | this.h = null; 11 | this.incX = null; 12 | this.incY = null; 13 | this.palIndex = null; 14 | this.tpri = null; 15 | this.c = null; 16 | this.initialized = false; 17 | this.opaque = new Array(8); 18 | }; 19 | 20 | Tile.prototype = { 21 | setBuffer: function (scanline) { 22 | for (this.y = 0; this.y < 8; this.y++) { 23 | this.setScanline(this.y, scanline[this.y], scanline[this.y + 8]); 24 | } 25 | }, 26 | 27 | setScanline: function (sline, b1, b2) { 28 | this.initialized = true; 29 | this.tIndex = sline << 3; 30 | for (this.x = 0; this.x < 8; this.x++) { 31 | this.pix[this.tIndex + this.x] = 32 | ((b1 >> (7 - this.x)) & 1) + (((b2 >> (7 - this.x)) & 1) << 1); 33 | if (this.pix[this.tIndex + this.x] === 0) { 34 | this.opaque[sline] = false; 35 | } 36 | } 37 | }, 38 | 39 | render: function ( 40 | buffer, 41 | srcx1, 42 | srcy1, 43 | srcx2, 44 | srcy2, 45 | dx, 46 | dy, 47 | palAdd, 48 | palette, 49 | flipHorizontal, 50 | flipVertical, 51 | pri, 52 | priTable 53 | ) { 54 | if (dx < -7 || dx >= 256 || dy < -7 || dy >= 240) { 55 | return; 56 | } 57 | 58 | this.w = srcx2 - srcx1; 59 | this.h = srcy2 - srcy1; 60 | 61 | if (dx < 0) { 62 | srcx1 -= dx; 63 | } 64 | if (dx + srcx2 >= 256) { 65 | srcx2 = 256 - dx; 66 | } 67 | 68 | if (dy < 0) { 69 | srcy1 -= dy; 70 | } 71 | if (dy + srcy2 >= 240) { 72 | srcy2 = 240 - dy; 73 | } 74 | 75 | if (!flipHorizontal && !flipVertical) { 76 | this.fbIndex = (dy << 8) + dx; 77 | this.tIndex = 0; 78 | for (this.y = 0; this.y < 8; this.y++) { 79 | for (this.x = 0; this.x < 8; this.x++) { 80 | if ( 81 | this.x >= srcx1 && 82 | this.x < srcx2 && 83 | this.y >= srcy1 && 84 | this.y < srcy2 85 | ) { 86 | this.palIndex = this.pix[this.tIndex]; 87 | this.tpri = priTable[this.fbIndex]; 88 | if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) { 89 | //console.log("Rendering upright tile to buffer"); 90 | buffer[this.fbIndex] = palette[this.palIndex + palAdd]; 91 | this.tpri = (this.tpri & 0xf00) | pri; 92 | priTable[this.fbIndex] = this.tpri; 93 | } 94 | } 95 | this.fbIndex++; 96 | this.tIndex++; 97 | } 98 | this.fbIndex -= 8; 99 | this.fbIndex += 256; 100 | } 101 | } else if (flipHorizontal && !flipVertical) { 102 | this.fbIndex = (dy << 8) + dx; 103 | this.tIndex = 7; 104 | for (this.y = 0; this.y < 8; this.y++) { 105 | for (this.x = 0; this.x < 8; this.x++) { 106 | if ( 107 | this.x >= srcx1 && 108 | this.x < srcx2 && 109 | this.y >= srcy1 && 110 | this.y < srcy2 111 | ) { 112 | this.palIndex = this.pix[this.tIndex]; 113 | this.tpri = priTable[this.fbIndex]; 114 | if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) { 115 | buffer[this.fbIndex] = palette[this.palIndex + palAdd]; 116 | this.tpri = (this.tpri & 0xf00) | pri; 117 | priTable[this.fbIndex] = this.tpri; 118 | } 119 | } 120 | this.fbIndex++; 121 | this.tIndex--; 122 | } 123 | this.fbIndex -= 8; 124 | this.fbIndex += 256; 125 | this.tIndex += 16; 126 | } 127 | } else if (flipVertical && !flipHorizontal) { 128 | this.fbIndex = (dy << 8) + dx; 129 | this.tIndex = 56; 130 | for (this.y = 0; this.y < 8; this.y++) { 131 | for (this.x = 0; this.x < 8; this.x++) { 132 | if ( 133 | this.x >= srcx1 && 134 | this.x < srcx2 && 135 | this.y >= srcy1 && 136 | this.y < srcy2 137 | ) { 138 | this.palIndex = this.pix[this.tIndex]; 139 | this.tpri = priTable[this.fbIndex]; 140 | if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) { 141 | buffer[this.fbIndex] = palette[this.palIndex + palAdd]; 142 | this.tpri = (this.tpri & 0xf00) | pri; 143 | priTable[this.fbIndex] = this.tpri; 144 | } 145 | } 146 | this.fbIndex++; 147 | this.tIndex++; 148 | } 149 | this.fbIndex -= 8; 150 | this.fbIndex += 256; 151 | this.tIndex -= 16; 152 | } 153 | } else { 154 | this.fbIndex = (dy << 8) + dx; 155 | this.tIndex = 63; 156 | for (this.y = 0; this.y < 8; this.y++) { 157 | for (this.x = 0; this.x < 8; this.x++) { 158 | if ( 159 | this.x >= srcx1 && 160 | this.x < srcx2 && 161 | this.y >= srcy1 && 162 | this.y < srcy2 163 | ) { 164 | this.palIndex = this.pix[this.tIndex]; 165 | this.tpri = priTable[this.fbIndex]; 166 | if (this.palIndex !== 0 && pri <= (this.tpri & 0xff)) { 167 | buffer[this.fbIndex] = palette[this.palIndex + palAdd]; 168 | this.tpri = (this.tpri & 0xf00) | pri; 169 | priTable[this.fbIndex] = this.tpri; 170 | } 171 | } 172 | this.fbIndex++; 173 | this.tIndex--; 174 | } 175 | this.fbIndex -= 8; 176 | this.fbIndex += 256; 177 | } 178 | } 179 | }, 180 | 181 | isTransparent: function (x, y) { 182 | return this.pix[(y << 3) + x] === 0; 183 | }, 184 | 185 | toJSON: function () { 186 | return { 187 | opaque: this.opaque, 188 | pix: this.pix, 189 | }; 190 | }, 191 | 192 | fromJSON: function (s) { 193 | this.opaque = s.opaque; 194 | this.pix = s.pix; 195 | }, 196 | }; 197 | 198 | module.exports = Tile; 199 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | copyArrayElements: function (src, srcPos, dest, destPos, length) { 3 | for (var i = 0; i < length; ++i) { 4 | dest[destPos + i] = src[srcPos + i]; 5 | } 6 | }, 7 | 8 | copyArray: function (src) { 9 | return src.slice(0); 10 | }, 11 | 12 | fromJSON: function (obj, state) { 13 | for (var i = 0; i < obj.JSON_PROPERTIES.length; i++) { 14 | obj[obj.JSON_PROPERTIES[i]] = state[obj.JSON_PROPERTIES[i]]; 15 | } 16 | }, 17 | 18 | toJSON: function (obj) { 19 | var state = {}; 20 | for (var i = 0; i < obj.JSON_PROPERTIES.length; i++) { 21 | state[obj.JSON_PROPERTIES[i]] = obj[obj.JSON_PROPERTIES[i]]; 22 | } 23 | return state; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /test/nes.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var fs = require("fs"); 3 | var NES = require("../src/nes"); 4 | var sinon = require("sinon"); 5 | 6 | describe("NES", function() { 7 | it("can be initialized", function() { 8 | var nes = new NES(); 9 | }); 10 | 11 | it("loads a ROM and runs a frame", function(done) { 12 | var onFrame = sinon.spy(); 13 | var nes = new NES({ onFrame: onFrame }); 14 | fs.readFile("roms/croom/croom.nes", function(err, data) { 15 | if (err) return done(err); 16 | nes.loadROM(data.toString("binary")); 17 | nes.frame(); 18 | assert(onFrame.calledOnce); 19 | assert.isArray(onFrame.args[0][0]); 20 | assert.lengthOf(onFrame.args[0][0], 256 * 240); 21 | done(); 22 | }); 23 | }); 24 | 25 | it("generates the correct frame buffer", function(done) { 26 | var onFrame = sinon.spy(); 27 | var nes = new NES({ onFrame: onFrame }); 28 | fs.readFile("roms/croom/croom.nes", function(err, data) { 29 | if (err) return done(err); 30 | nes.loadROM(data.toString("binary")); 31 | // Check the first index of a white pixel on the first 6 frames of 32 | // output. Croom only uses 2 colors on the initial screen which makes 33 | // it easy to detect. Comparing full snapshots of each frame takes too 34 | // long. 35 | var expectedIndexes = [-1, -1, -1, 2056, 4104, 4104]; 36 | for (var i = 0; i < 6; i++) { 37 | nes.frame(); 38 | assert.equal(onFrame.lastCall.args[0].indexOf(0xFFFFFF), expectedIndexes[i]); 39 | } 40 | done(); 41 | }); 42 | }); 43 | 44 | describe("#loadROM()", function() { 45 | it("throws an error given an invalid ROM", function() { 46 | var nes = new NES(); 47 | assert.throws(function() { 48 | nes.loadROM("foo"); 49 | }, "Not a valid NES ROM."); 50 | }); 51 | }); 52 | 53 | describe("#getFPS()", function() { 54 | var nes = new NES(); 55 | before(function(done) { 56 | fs.readFile("roms/croom/croom.nes", function(err, data) { 57 | if (err) return done(err); 58 | nes.loadROM(data.toString("binary")); 59 | done(); 60 | }); 61 | }); 62 | 63 | it("returns an FPS count when frames have been run", function() { 64 | assert.isNull(nes.getFPS()); 65 | nes.frame(); 66 | nes.frame(); 67 | var fps = nes.getFPS(); 68 | assert.isNumber(fps); 69 | assert.isAbove(fps, 0); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: { 6 | jsnes: "./src/index.js", 7 | "jsnes.min": "./src/index.js", 8 | }, 9 | devtool: "source-map", 10 | output: { 11 | path: path.resolve(__dirname, "dist"), 12 | filename: "[name].js", 13 | library: "jsnes", 14 | libraryTarget: "umd", 15 | umdNamedDefine: true, 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | enforce: "pre", 22 | exclude: /node_modules/, 23 | use: [ 24 | { 25 | loader: "eslint-loader", 26 | }, 27 | ], 28 | }, 29 | ], 30 | }, 31 | plugins: [ 32 | new UglifyJsPlugin({ 33 | include: /\.min\.js$/, 34 | sourceMap: true, 35 | }), 36 | ], 37 | }; 38 | --------------------------------------------------------------------------------