├── .gitignore ├── favicon.ico ├── resources ├── bg.png ├── bios.bin ├── crash.png ├── xhr.js ├── biosbin.js ├── console.css ├── main.css └── console.js ├── .prettierrc ├── bios.S ├── COPYING ├── README.md ├── js ├── sio.js ├── video │ ├── worker.js │ └── proxy.js ├── gpio.js ├── video.js ├── keypad.js ├── savedata.js ├── util.js ├── gba.js ├── audio.js ├── thumb.js ├── io.js └── mmu.js ├── debugger.html ├── console.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Charles-Chrismann/gbajs2/master/favicon.ico -------------------------------------------------------------------------------- /resources/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Charles-Chrismann/gbajs2/master/resources/bg.png -------------------------------------------------------------------------------- /resources/bios.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Charles-Chrismann/gbajs2/master/resources/bios.bin -------------------------------------------------------------------------------- /resources/crash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Charles-Chrismann/gbajs2/master/resources/crash.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": true, 4 | "trailingComma": "none", 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /resources/xhr.js: -------------------------------------------------------------------------------- 1 | function loadRom(url, callback) { 2 | var xhr = new XMLHttpRequest(); 3 | xhr.open('GET', url); 4 | xhr.responseType = 'arraybuffer'; 5 | 6 | xhr.onload = function() { callback(xhr.response) }; 7 | xhr.send(); 8 | } 9 | -------------------------------------------------------------------------------- /resources/biosbin.js: -------------------------------------------------------------------------------- 1 | function _base64ToArrayBuffer(base64) { 2 | // https://stackoverflow.com/questions/21797299/convert-base64-string-to-arraybuffer 3 | var binary_string = window.atob(base64); 4 | var len = binary_string.length; 5 | var bytes = new Uint8Array(len); 6 | for (var i = 0; i < len; i++) { 7 | bytes[i] = binary_string.charCodeAt(i); 8 | } 9 | return bytes.buffer; 10 | } 11 | let biosBin = _base64ToArrayBuffer("BgAA6v7//+oFAADq/v//6v7//+oAAKDhDAAA6v7//+oC86DjAABd4wHToAMg0E0CAEAt6QIAXuUEAFDjCQAACwUAUOMHAAALAEC96A7wsOEPUC3pAQOg4wDgj+IE8BDlD1C96ATwXuIQQC3pBNBN4rAQzeEBQ6DjAkyE4rAA1OGyAM3hsBDd4QEAgOGwAMThAUOg4x8AoOMA8CnhAACg4wEDxOXTAKDjAPAp4bgAVOGwEN3hABAR4AAQIRC4EEQR8///CgFDoOMCTITisgDd4bAAxOEE0I3iEIC96A==\n") -------------------------------------------------------------------------------- /bios.S: -------------------------------------------------------------------------------- 1 | #define nop andeq r0, r0 2 | 3 | .text 4 | 5 | b resetBase 6 | b undefBase 7 | b swiBase 8 | b pabtBase 9 | b dabtBase 10 | nop 11 | b irqBase 12 | b fiqBase 13 | 14 | resetBase: 15 | mov pc, #0x8000000 16 | 17 | swiBase: 18 | cmp sp, #0 19 | moveq sp, #0x04000000 20 | subeq sp, #0x20 21 | stmfd sp!, {lr} 22 | ldrb r0, [lr, #-2] 23 | cmp r0, #4 24 | bleq IntrWait 25 | cmp r0, #5 26 | bleq IntrWait 27 | ldmfd sp!, {lr} 28 | movs pc, lr 29 | 30 | irqBase: 31 | stmfd sp!, {r0-r3, r12, lr} 32 | mov r0, #0x04000000 33 | add lr, pc, #0 34 | ldr pc, [r0, #-4] 35 | ldmfd sp!, {r0-r3, r12, lr} 36 | subs pc, lr, #4 37 | 38 | IntrWait: 39 | stmfd sp!, {r4, lr} 40 | add sp, #-4 41 | strh r1, [sp, #0] 42 | mov r4, #0x04000000 43 | add r4, #0x200 44 | ldrh r0, [r4, #0] 45 | strh r0, [sp, #2] 46 | ldrh r1, [sp, #0] 47 | orr r0, r1 48 | strh r0, [r4, #0x0] 49 | mov r4, #0x04000000 50 | IntrWaitLoop: 51 | mov r0, #0x1F 52 | msr cpsr, r0 53 | mov r0, #0 54 | strb r0, [r4, #0x301] 55 | mov r0, #0xD3 56 | msr cpsr, r0 57 | ldrh r0, [r4, #-8] 58 | ldrh r1, [sp, #0] 59 | ands r1, r0 60 | eorne r1, r0 61 | strneh r1, [r4, #-8] 62 | beq IntrWaitLoop 63 | mov r4, #0x04000000 64 | add r4, #0x200 65 | ldrh r0, [sp, #2] 66 | strh r0, [r4, #0] 67 | add sp, #4 68 | ldmfd sp!, {r4, pc} 69 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright © 2012, Jeffrey Pfau 2 | Copyright © 2020, Andrew Chase 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gbajs2 -- Community Fork 2 | ====== 3 | 4 | gbajs2 is a Game Boy Advance emulator written in Javascript from scratch using HTML5 technologies like Canvas and Web Audio. 5 | It is freely licensed and works in any modern browser without plugins. 6 | 7 | Use it online! 8 | 9 | See the [issues page](https://github.com/andychase/gbajs2/issues) for feature suggestions and ways you can help contribute! 10 | 11 | Mailing list for general discussion or if you want to just be kept in the loop: https://groups.google.com/forum/#!forum/gbajs2 12 | 13 | ## Feature List 14 | 15 | * Playable compatibility, see [compatibility](https://github.com/andychase/gbajs2/wiki/Compatibility-List) 16 | * Acceptable performance on modern browsers 17 | * Pure javascript, allowing easy API access 18 | * Realtime clock gamepad support (Pokemon Ruby) 19 | * Save games 20 | 21 | ## License 22 | Original work by Endrift. Repo: (Archived / No longer maintained) https://github.com/endrift/gbajs 23 | 24 | Copyright © 2012 – 2013, Jeffrey Pfau 25 | Copyright © 2020, Andrew Chase 26 | 27 | All rights reserved. 28 | 29 | Redistribution and use in source and binary forms, with or without 30 | modification, are permitted provided that the following conditions are met: 31 | 32 | * Redistributions of source code must retain the above copyright notice, this 33 | list of conditions and the following disclaimer. 34 | 35 | * Redistributions in binary form must reproduce the above copyright notice, 36 | this list of conditions and the following disclaimer in the documentation 37 | and/or other materials provided with the distribution. 38 | 39 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 40 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 41 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 42 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 43 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 44 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 45 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 46 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 47 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 48 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 49 | POSSIBILITY OF SUCH DAMAGE. 50 | -------------------------------------------------------------------------------- /js/sio.js: -------------------------------------------------------------------------------- 1 | class GameBoyAdvanceSIO { 2 | constructor() { 3 | this.SIO_NORMAL_8 = 0; 4 | this.SIO_NORMAL_32 = 1; 5 | this.SIO_MULTI = 2; 6 | this.SIO_UART = 3; 7 | this.SIO_GPIO = 8; 8 | this.SIO_JOYBUS = 12; 9 | 10 | this.BAUD = [9600, 38400, 57600, 115200]; 11 | } 12 | clear() { 13 | this.mode = this.SIO_GPIO; 14 | this.sd = false; 15 | 16 | this.irq = false; 17 | this.multiplayer = { 18 | baud: 0, 19 | si: 0, 20 | id: 0, 21 | error: 0, 22 | busy: 0, 23 | 24 | states: [0xffff, 0xffff, 0xffff, 0xffff] 25 | }; 26 | 27 | this.linkLayer = null; 28 | } 29 | setMode(mode) { 30 | if (mode & 0x8) { 31 | mode &= 0xc; 32 | } else { 33 | mode &= 0x3; 34 | } 35 | this.mode = mode; 36 | 37 | this.core.INFO("Setting SIO mode to " + hex(mode, 1)); 38 | } 39 | writeRCNT(value) { 40 | if (this.mode != this.SIO_GPIO) { 41 | return; 42 | } 43 | 44 | this.core.STUB("General purpose serial not supported"); 45 | } 46 | writeSIOCNT(value) { 47 | switch (this.mode) { 48 | case this.SIO_NORMAL_8: 49 | this.core.STUB("8-bit transfer unsupported"); 50 | break; 51 | case this.SIO_NORMAL_32: 52 | this.core.STUB("32-bit transfer unsupported"); 53 | break; 54 | case this.SIO_MULTI: 55 | this.multiplayer.baud = value & 0x0003; 56 | if (this.linkLayer) { 57 | this.linkLayer.setBaud(this.BAUD[this.multiplayer.baud]); 58 | } 59 | 60 | if (!this.multiplayer.si) { 61 | this.multiplayer.busy = value & 0x0080; 62 | if (this.linkLayer && this.multiplayer.busy) { 63 | this.linkLayer.startMultiplayerTransfer(); 64 | } 65 | } 66 | this.irq = value & 0x4000; 67 | break; 68 | case this.SIO_UART: 69 | this.core.STUB("UART unsupported"); 70 | break; 71 | case this.SIO_GPIO: 72 | // This register isn't used in general-purpose mode 73 | break; 74 | case this.SIO_JOYBUS: 75 | this.core.STUB("JOY BUS unsupported"); 76 | break; 77 | } 78 | } 79 | readSIOCNT() { 80 | var value = (this.mode << 12) & 0xffff; 81 | switch (this.mode) { 82 | case this.SIO_NORMAL_8: 83 | this.core.STUB("8-bit transfer unsupported"); 84 | break; 85 | case this.SIO_NORMAL_32: 86 | this.core.STUB("32-bit transfer unsupported"); 87 | break; 88 | case this.SIO_MULTI: 89 | value |= this.multiplayer.baud; 90 | value |= this.multiplayer.si; 91 | value |= !!this.sd << 3; 92 | value |= this.multiplayer.id << 4; 93 | value |= this.multiplayer.error; 94 | value |= this.multiplayer.busy; 95 | value |= !!this.multiplayer.irq << 14; 96 | break; 97 | case this.SIO_UART: 98 | this.core.STUB("UART unsupported"); 99 | break; 100 | case this.SIO_GPIO: 101 | // This register isn't used in general-purpose mode 102 | break; 103 | case this.SIO_JOYBUS: 104 | this.core.STUB("JOY BUS unsupported"); 105 | break; 106 | } 107 | return value; 108 | } 109 | read(slot) { 110 | switch (this.mode) { 111 | case this.SIO_NORMAL_32: 112 | this.core.STUB("32-bit transfer unsupported"); 113 | break; 114 | case this.SIO_MULTI: 115 | return this.multiplayer.states[slot]; 116 | case this.SIO_UART: 117 | this.core.STUB("UART unsupported"); 118 | break; 119 | default: 120 | this.core.WARN( 121 | "Reading from transfer register in unsupported mode" 122 | ); 123 | break; 124 | } 125 | return 0; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /js/video/worker.js: -------------------------------------------------------------------------------- 1 | importScripts('software.js'); 2 | 3 | var video = new GameBoyAdvanceSoftwareRenderer(); 4 | var proxyBacking = null; 5 | var currentFrame = 0; 6 | 7 | self.finishDraw = function(pixelData) { 8 | self.postMessage({ type: 'finish', backing: pixelData, frame: currentFrame }); 9 | } 10 | 11 | function receiveDirty(dirty) { 12 | for (var type in dirty) { 13 | switch (type) { 14 | case 'DISPCNT': 15 | video.writeDisplayControl(dirty[type]); 16 | break; 17 | case 'BGCNT': 18 | for (var i in dirty[type]) { 19 | if (typeof(dirty[type][i]) === 'number') { 20 | video.writeBackgroundControl(i, dirty[type][i]); 21 | } 22 | } 23 | break; 24 | case 'BGHOFS': 25 | for (var i in dirty[type]) { 26 | if (typeof(dirty[type][i]) === 'number') { 27 | video.writeBackgroundHOffset(i, dirty[type][i]); 28 | } 29 | } 30 | break; 31 | case 'BGVOFS': 32 | for (var i in dirty[type]) { 33 | if (typeof(dirty[type][i]) === 'number') { 34 | video.writeBackgroundVOffset(i, dirty[type][i]); 35 | } 36 | } 37 | break; 38 | case 'BGX': 39 | for (var i in dirty[type]) { 40 | if (typeof(dirty[type][i]) === 'number') { 41 | video.writeBackgroundRefX(i, dirty[type][i]); 42 | } 43 | } 44 | break; 45 | case 'BGY': 46 | for (var i in dirty[type]) { 47 | if (typeof(dirty[type][i]) === 'number') { 48 | video.writeBackgroundRefY(i, dirty[type][i]); 49 | } 50 | } 51 | break; 52 | case 'BGPA': 53 | for (var i in dirty[type]) { 54 | if (typeof(dirty[type][i]) === 'number') { 55 | video.writeBackgroundParamA(i, dirty[type][i]); 56 | } 57 | } 58 | break; 59 | case 'BGPB': 60 | for (var i in dirty[type]) { 61 | if (typeof(dirty[type][i]) === 'number') { 62 | video.writeBackgroundParamB(i, dirty[type][i]); 63 | } 64 | } 65 | break; 66 | case 'BGPC': 67 | for (var i in dirty[type]) { 68 | if (typeof(dirty[type][i]) === 'number') { 69 | video.writeBackgroundParamC(i, dirty[type][i]); 70 | } 71 | } 72 | break; 73 | case 'BGPD': 74 | for (var i in dirty[type]) { 75 | if (typeof(dirty[type][i]) === 'number') { 76 | video.writeBackgroundParamD(i, dirty[type][i]); 77 | } 78 | } 79 | break; 80 | case 'WIN0H': 81 | video.writeWin0H(dirty[type]); 82 | break; 83 | case 'WIN1H': 84 | video.writeWin1H(dirty[type]); 85 | break; 86 | case 'WIN0V': 87 | video.writeWin0V(dirty[type]); 88 | break; 89 | case 'WIN1V': 90 | video.writeWin1V(dirty[type]); 91 | break; 92 | case 'WININ': 93 | video.writeWinIn(dirty[type]); 94 | break; 95 | case 'WINOUT': 96 | video.writeWinOut(dirty[type]); 97 | break; 98 | case 'BLDCNT': 99 | video.writeBlendControl(dirty[type]); 100 | break; 101 | case 'BLDALPHA': 102 | video.writeBlendAlpha(dirty[type]); 103 | break; 104 | case 'BLDY': 105 | video.writeBlendY(dirty[type]); 106 | break; 107 | case 'MOSAIC': 108 | video.writeMosaic(dirty[type]); 109 | break; 110 | case 'memory': 111 | receiveMemory(dirty.memory); 112 | break; 113 | } 114 | } 115 | } 116 | 117 | function receiveMemory(memory) { 118 | if (memory.palette) { 119 | video.palette.overwrite(new Uint16Array(memory.palette)); 120 | } 121 | if (memory.oam) { 122 | video.oam.overwrite(new Uint16Array(memory.oam)); 123 | } 124 | if (memory.vram) { 125 | for (var i = 0; i < 12; ++i) { 126 | if (memory.vram[i]) { 127 | video.vram.insert(i << 12, new Uint16Array(memory.vram[i])); 128 | } 129 | } 130 | } 131 | } 132 | 133 | var handlers = { 134 | clear: function(data) { 135 | video.clear(data); 136 | }, 137 | 138 | scanline: function(data) { 139 | receiveDirty(data.dirty); 140 | video.drawScanline(data.y, proxyBacking); 141 | }, 142 | 143 | start: function(data) { 144 | proxyBacking = data.backing; 145 | video.setBacking(data.backing); 146 | }, 147 | 148 | finish: function(data) { 149 | currentFrame = data.frame; 150 | var scanline = 0; 151 | for (var i = 0; i < data.scanlines.length; ++i) { 152 | for (var y = scanline; y < data.scanlines[i].y; ++y) { 153 | video.drawScanline(y, proxyBacking); 154 | } 155 | scanline = data.scanlines[i].y + 1; 156 | receiveDirty(data.scanlines[i].dirty); 157 | video.drawScanline(data.scanlines[i].y, proxyBacking); 158 | } 159 | for (var y = scanline; y < 160; ++y) { 160 | video.drawScanline(y, proxyBacking); 161 | } 162 | video.finishDraw(self); 163 | }, 164 | }; 165 | 166 | self.onmessage = function(message) { 167 | handlers[message.data['type']](message.data); 168 | }; 169 | -------------------------------------------------------------------------------- /resources/console.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 'Lucida Sans Typewriter', 'Monaco', 'Andale Mono', monospace; 4 | background-color: #2A2F32; 5 | color: #DADDE1; 6 | font-size: 13px; 7 | } 8 | 9 | h3 { 10 | font-size: 14px; 11 | height: 18px; 12 | margin: 2px 0 -20px 4px; 13 | padding: 0 6px 0 4px; 14 | position: relative; 15 | display: inline-block; 16 | background-color: #2A2F32; 17 | } 18 | 19 | #screen { 20 | background-color: gray; 21 | margin: 40px; 22 | } 23 | 24 | section { 25 | display: -webkit-box; 26 | display: -moz-box; 27 | -webkit-box-pack: center; 28 | -moz-box-pack: center; 29 | } 30 | 31 | #top { 32 | -webkit-box-orient: horizontal; 33 | -moz-box-orient: horizontal; 34 | width: 100%; 35 | height: 100%; 36 | } 37 | 38 | #main { 39 | -webkit-box-orient: vertical; 40 | -moz-box-orient: vertical; 41 | min-width: 242px; 42 | max-width: 410px; 43 | text-align: center; 44 | } 45 | 46 | ul, ol, #registers, #savestates { 47 | padding: 10px; 48 | } 49 | 50 | #devtools, #registers, #savestates, #display { 51 | -webkit-box-orient: vertical; 52 | -moz-box-orient: vertical; 53 | } 54 | 55 | #console { 56 | overflow-y: auto; 57 | white-space: pre-wrap; 58 | max-height: 100%; 59 | margin: 0; 60 | } 61 | 62 | #consoleContainer { 63 | margin: 0; 64 | -webkit-box-flex: 1; 65 | -moz-box-flex: 1; 66 | min-width: 300px; 67 | display: block; 68 | border-right: 1px solid #DADDE1; 69 | } 70 | 71 | #debugViewer { 72 | text-align: center; 73 | margin: 10px; 74 | overflow: auto; 75 | max-height: 50%; 76 | } 77 | 78 | #debugViewSelector { 79 | display: block; 80 | } 81 | 82 | #memory { 83 | white-space: nowrap; 84 | margin: 0; 85 | -webkit-box-flex: 1; 86 | -moz-box-flex: 1; 87 | max-height: 100%; 88 | border-left: 1px solid #DADDE1; 89 | } 90 | 91 | #savestates ol > li { 92 | display: inline; 93 | } 94 | 95 | #gprs { 96 | text-align: center; 97 | } 98 | 99 | #psr { 100 | display: -webkit-box; 101 | display: -moz-box; 102 | -webkit-box-orient: horizontal; 103 | -moz-box-orient: horizontal; 104 | } 105 | 106 | #psr > * { 107 | display: block; 108 | padding: 2px 10px; 109 | } 110 | 111 | #psr > *:last-child { 112 | -webkit-box-flex: 1; 113 | -moz-box-flex: 1; 114 | text-align: right; 115 | } 116 | 117 | #mode { 118 | display: inline-block; 119 | width: 80px; 120 | } 121 | 122 | #registers ol, #savestates ol { 123 | border: 1px solid #DADDE1; 124 | padding: 8px; 125 | } 126 | 127 | #registers ol > li { 128 | margin: 3px; 129 | line-height: 16px; 130 | } 131 | 132 | #gprs > li { 133 | display: inline; 134 | } 135 | 136 | #breakpoints { 137 | -webkit-box-flex: 1; 138 | -moz-box-flex: 1; 139 | border-top: 1px solid #DADDE1; 140 | -webkit-box-orient: vertical; 141 | } 142 | 143 | #breakpointControls { 144 | padding: 2px 4px; 145 | border-bottom: 1px solid #DADDE1; 146 | } 147 | 148 | #memory { 149 | -webkit-box-flex: 1; 150 | -moz-box-flex: 1; 151 | } 152 | 153 | #consoleControls { 154 | border-bottom: 1px solid #DADDE1; 155 | } 156 | 157 | #console li:before { 158 | content: "-"; 159 | color: #5C616F; 160 | padding-right: 1em; 161 | } 162 | 163 | #console li { 164 | list-style-type: none; 165 | } 166 | 167 | #memory { 168 | overflow-y: scroll; 169 | min-width: 400px; 170 | display: -webkit-box; 171 | display: -moz-box; 172 | -webkit-box-orient: vertical; 173 | -moz-box-orient: vertical; 174 | padding: 0; 175 | } 176 | 177 | #memoryControls { 178 | border-bottom: 1px solid #DADDE1; 179 | padding: 2px; 180 | position: fixed; 181 | top: 0; 182 | background-color: #2A2F32; 183 | width: 100%; 184 | } 185 | 186 | #memoryControls ul { 187 | margin: 0 0 0 80px; 188 | padding: 0; 189 | } 190 | 191 | #memoryControls ul > li { 192 | float: left; 193 | margin-left: 16px; 194 | } 195 | 196 | #breakpointView { 197 | margin: 0; 198 | overflow-y: auto; 199 | display: -moz-box; 200 | -moz-box-orient: vertical; 201 | -moz-box-flex: 1; 202 | -webkit-box-flex: 1; 203 | } 204 | 205 | #memoryView { 206 | display: table; 207 | width: 100%; 208 | margin: 50px 0; 209 | border-spacing: 1px 1px; 210 | } 211 | 212 | #memory li { 213 | display: table-row; 214 | list-style-type: none; 215 | } 216 | 217 | #memory .memoryOffset { 218 | margin: 0; 219 | display: table-cell; 220 | } 221 | 222 | #memory .memoryCell { 223 | padding: 0 3px; 224 | display: table-cell; 225 | text-align: center; 226 | } 227 | 228 | #memory .memoryCell.changed { 229 | font-weight: bold; 230 | } 231 | 232 | .disabled { 233 | color: #5C616F; 234 | } 235 | 236 | #saveState { 237 | width: 240px; 238 | margin: auto; 239 | display: block; 240 | } 241 | -------------------------------------------------------------------------------- /js/gpio.js: -------------------------------------------------------------------------------- 1 | class GameBoyAdvanceGPIO { 2 | constructor(core, rom) { 3 | this.core = core; 4 | this.rom = rom; 5 | 6 | this.readWrite = 0; 7 | this.direction = 0; 8 | 9 | this.device = new GameBoyAdvanceRTC(this); // TODO: Support more devices 10 | } 11 | store16(offset, value) { 12 | switch (offset) { 13 | case 0xc4: 14 | this.device.setPins(value & 0xf); 15 | break; 16 | case 0xc6: 17 | this.direction = value & 0xf; 18 | this.device.setDirection(this.direction); 19 | break; 20 | case 0xc8: 21 | this.readWrite = value & 1; 22 | break; 23 | default: 24 | throw new Error( 25 | "BUG: Bad offset passed to GPIO: " + offset.toString(16) 26 | ); 27 | } 28 | if (this.readWrite) { 29 | var old = this.rom.view.getUint16(offset, true); 30 | old &= ~this.direction; 31 | this.rom.view.setUint16( 32 | offset, 33 | old | (value & this.direction), 34 | true 35 | ); 36 | } 37 | } 38 | outputPins(nybble) { 39 | if (this.readWrite) { 40 | var old = this.rom.view.getUint16(0xc4, true); 41 | old &= this.direction; 42 | this.rom.view.setUint16( 43 | 0xc4, 44 | old | (nybble & ~this.direction & 0xf), 45 | true 46 | ); 47 | } 48 | } 49 | } 50 | class GameBoyAdvanceRTC { 51 | constructor(gpio) { 52 | this.gpio = gpio; 53 | 54 | // PINOUT: SCK | SIO | CS | - 55 | this.pins = 0; 56 | this.direction = 0; 57 | 58 | this.totalBytes = [ 59 | 0, // Force reset 60 | 0, // Empty 61 | 7, // Date/Time 62 | 0, // Force IRQ 63 | 1, // Control register 64 | 0, // Empty 65 | 3, // Time 66 | 0 // Empty 67 | ]; 68 | this.bytesRemaining = 0; 69 | 70 | // Transfer sequence: 71 | // == Initiate 72 | // > HI | - | LO | - 73 | // > HI | - | HI | - 74 | // == Transfer bit (x8) 75 | // > LO | x | HI | - 76 | // > HI | - | HI | - 77 | // < ?? | x | ?? | - 78 | // == Terminate 79 | // > - | - | LO | - 80 | this.transferStep = 0; 81 | 82 | this.reading = 0; 83 | this.bitsRead = 0; 84 | this.bits = 0; 85 | this.command = -1; 86 | 87 | this.control = 0x40; 88 | this.time = [ 89 | 0, // Year 90 | 0, // Month 91 | 0, // Day 92 | 0, // Day of week 93 | 0, // Hour 94 | 0, // Minute 95 | 0 // Second 96 | ]; 97 | } 98 | setPins(nybble) { 99 | switch (this.transferStep) { 100 | case 0: 101 | if ((nybble & 5) == 1) { 102 | this.transferStep = 1; 103 | } 104 | break; 105 | case 1: 106 | if (nybble & 4) { 107 | this.transferStep = 2; 108 | } 109 | break; 110 | case 2: 111 | if (!(nybble & 1)) { 112 | this.bits &= ~(1 << this.bitsRead); 113 | this.bits |= ((nybble & 2) >> 1) << this.bitsRead; 114 | } else { 115 | if (nybble & 4) { 116 | // SIO direction should always != this.read 117 | if (this.direction & 2 && !this.read) { 118 | ++this.bitsRead; 119 | if (this.bitsRead == 8) { 120 | this.processByte(); 121 | } 122 | } else { 123 | this.gpio.outputPins( 124 | 5 | (this.sioOutputPin() << 1) 125 | ); 126 | ++this.bitsRead; 127 | if (this.bitsRead == 8) { 128 | --this.bytesRemaining; 129 | if (this.bytesRemaining <= 0) { 130 | this.command = -1; 131 | } 132 | this.bitsRead = 0; 133 | } 134 | } 135 | } else { 136 | this.bitsRead = 0; 137 | this.bytesRemaining = 0; 138 | this.command = -1; 139 | this.transferStep = 0; 140 | } 141 | } 142 | break; 143 | } 144 | 145 | this.pins = nybble & 7; 146 | } 147 | setDirection(direction) { 148 | this.direction = direction; 149 | } 150 | processByte() { 151 | --this.bytesRemaining; 152 | switch (this.command) { 153 | case -1: 154 | if ((this.bits & 0x0f) == 0x06) { 155 | this.command = (this.bits >> 4) & 7; 156 | this.reading = this.bits & 0x80; 157 | 158 | this.bytesRemaining = this.totalBytes[this.command]; 159 | switch (this.command) { 160 | case 0: 161 | this.control = 0; 162 | break; 163 | case 2: 164 | case 6: 165 | this.updateClock(); 166 | break; 167 | } 168 | } else { 169 | this.gpio.core.WARN( 170 | "Invalid RTC command byte: " + this.bits.toString(16) 171 | ); 172 | } 173 | break; 174 | case 4: 175 | // Control 176 | this.control = this.bits & 0x40; 177 | break; 178 | } 179 | this.bits = 0; 180 | this.bitsRead = 0; 181 | if (!this.bytesRemaining) { 182 | this.command = -1; 183 | } 184 | } 185 | sioOutputPin() { 186 | var outputByte = 0; 187 | switch (this.command) { 188 | case 4: 189 | outputByte = this.control; 190 | break; 191 | case 2: 192 | case 6: 193 | outputByte = this.time[7 - this.bytesRemaining]; 194 | break; 195 | } 196 | var output = (outputByte >> this.bitsRead) & 1; 197 | return output; 198 | } 199 | updateClock() { 200 | var date = new Date(); 201 | this.time[0] = this.bcd(date.getFullYear()); 202 | this.time[1] = this.bcd(date.getMonth() + 1); 203 | this.time[2] = this.bcd(date.getDate()); 204 | this.time[3] = date.getDay() - 1; 205 | if (this.time[3] < 0) { 206 | this.time[3] = 6; 207 | } 208 | if (this.control & 0x40) { 209 | // 24 hour 210 | this.time[4] = this.bcd(date.getHours()); 211 | } else { 212 | this.time[4] = this.bcd(date.getHours() % 2); 213 | if (date.getHours() >= 12) { 214 | this.time[4] |= 0x80; 215 | } 216 | } 217 | this.time[5] = this.bcd(date.getMinutes()); 218 | this.time[6] = this.bcd(date.getSeconds()); 219 | } 220 | bcd(binary) { 221 | var counter = binary % 10; 222 | binary /= 10; 223 | counter += binary % 10 << 4; 224 | return counter; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /js/video.js: -------------------------------------------------------------------------------- 1 | class GameBoyAdvanceVideo { 2 | constructor() { 3 | try { 4 | this.renderPath = new GameBoyAdvanceRenderProxy(); 5 | } catch(err) { 6 | console.log("Service worker renderer couldn't load. Save states (not save files) may be glitchy") 7 | this.renderPath = new GameBoyAdvanceSoftwareRenderer(); 8 | } 9 | 10 | 11 | this.CYCLES_PER_PIXEL = 4; 12 | 13 | this.HORIZONTAL_PIXELS = 240; 14 | this.HBLANK_PIXELS = 68; 15 | this.HDRAW_LENGTH = 1006; 16 | this.HBLANK_LENGTH = 226; 17 | this.HORIZONTAL_LENGTH = 1232; 18 | 19 | this.VERTICAL_PIXELS = 160; 20 | this.VBLANK_PIXELS = 68; 21 | this.VERTICAL_TOTAL_PIXELS = 228; 22 | 23 | this.TOTAL_LENGTH = 280896; 24 | 25 | this.drawCallback = function () {}; 26 | this.vblankCallback = function () {}; 27 | } 28 | clear() { 29 | this.renderPath.clear(this.cpu.mmu); 30 | 31 | // DISPSTAT 32 | this.DISPSTAT_MASK = 0xff38; 33 | this.inHblank = false; 34 | this.inVblank = false; 35 | this.vcounter = 0; 36 | this.vblankIRQ = 0; 37 | this.hblankIRQ = 0; 38 | this.vcounterIRQ = 0; 39 | this.vcountSetting = 0; 40 | 41 | // VCOUNT 42 | this.vcount = -1; 43 | 44 | this.lastHblank = 0; 45 | this.nextHblank = this.HDRAW_LENGTH; 46 | this.nextEvent = this.nextHblank; 47 | 48 | this.nextHblankIRQ = 0; 49 | this.nextVblankIRQ = 0; 50 | this.nextVcounterIRQ = 0; 51 | } 52 | freeze() { 53 | return { 54 | inHblank: this.inHblank, 55 | inVblank: this.inVblank, 56 | vcounter: this.vcounter, 57 | vblankIRQ: this.vblankIRQ, 58 | hblankIRQ: this.hblankIRQ, 59 | vcounterIRQ: this.vcounterIRQ, 60 | vcountSetting: this.vcountSetting, 61 | vcount: this.vcount, 62 | lastHblank: this.lastHblank, 63 | nextHblank: this.nextHblank, 64 | nextEvent: this.nextEvent, 65 | nextHblankIRQ: this.nextHblankIRQ, 66 | nextVblankIRQ: this.nextVblankIRQ, 67 | nextVcounterIRQ: this.nextVcounterIRQ, 68 | renderPath: this.renderPath.freeze(this.core.encodeBase64) 69 | }; 70 | } 71 | defrost(frost) { 72 | this.inHblank = frost.inHblank; 73 | this.inVblank = frost.inVblank; 74 | this.vcounter = frost.vcounter; 75 | this.vblankIRQ = frost.vblankIRQ; 76 | this.hblankIRQ = frost.hblankIRQ; 77 | this.vcounterIRQ = frost.vcounterIRQ; 78 | this.vcountSetting = frost.vcountSetting; 79 | this.vcount = frost.vcount; 80 | this.lastHblank = frost.lastHblank; 81 | this.nextHblank = frost.nextHblank; 82 | this.nextEvent = frost.nextEvent; 83 | this.nextHblankIRQ = frost.nextHblankIRQ; 84 | this.nextVblankIRQ = frost.nextVblankIRQ; 85 | this.nextVcounterIRQ = frost.nextVcounterIRQ; 86 | this.renderPath.defrost(frost.renderPath, this.core.decodeBase64); 87 | } 88 | setBacking(backing) { 89 | var pixelData = backing.createImageData( 90 | this.HORIZONTAL_PIXELS, 91 | this.VERTICAL_PIXELS 92 | ); 93 | this.context = backing; 94 | 95 | // Clear backing first 96 | for ( 97 | var offset = 0; 98 | offset < this.HORIZONTAL_PIXELS * this.VERTICAL_PIXELS * 4; 99 | 100 | ) { 101 | pixelData.data[offset++] = 0xff; 102 | pixelData.data[offset++] = 0xff; 103 | pixelData.data[offset++] = 0xff; 104 | pixelData.data[offset++] = 0xff; 105 | } 106 | 107 | this.renderPath.setBacking(pixelData); 108 | } 109 | updateTimers(cpu) { 110 | var cycles = cpu.cycles; 111 | 112 | if (this.nextEvent <= cycles) { 113 | if (this.inHblank) { 114 | // End Hblank 115 | this.inHblank = false; 116 | this.nextEvent = this.nextHblank; 117 | 118 | ++this.vcount; 119 | 120 | switch (this.vcount) { 121 | case this.VERTICAL_PIXELS: 122 | this.inVblank = true; 123 | this.renderPath.finishDraw(this); 124 | this.nextVblankIRQ = this.nextEvent + this.TOTAL_LENGTH; 125 | this.cpu.mmu.runVblankDmas(); 126 | if (this.vblankIRQ) { 127 | this.cpu.irq.raiseIRQ(this.cpu.irq.IRQ_VBLANK); 128 | } 129 | this.vblankCallback(); 130 | break; 131 | case this.VERTICAL_TOTAL_PIXELS - 1: 132 | this.inVblank = false; 133 | break; 134 | case this.VERTICAL_TOTAL_PIXELS: 135 | this.vcount = 0; 136 | this.renderPath.startDraw(); 137 | break; 138 | } 139 | 140 | this.vcounter = this.vcount == this.vcountSetting; 141 | if (this.vcounter && this.vcounterIRQ) { 142 | this.cpu.irq.raiseIRQ(this.cpu.irq.IRQ_VCOUNTER); 143 | this.nextVcounterIRQ += this.TOTAL_LENGTH; 144 | } 145 | 146 | if (this.vcount < this.VERTICAL_PIXELS) { 147 | this.renderPath.drawScanline(this.vcount); 148 | } 149 | } else { 150 | // Begin Hblank 151 | this.inHblank = true; 152 | this.lastHblank = this.nextHblank; 153 | this.nextEvent = this.lastHblank + this.HBLANK_LENGTH; 154 | this.nextHblank = this.nextEvent + this.HDRAW_LENGTH; 155 | this.nextHblankIRQ = this.nextHblank; 156 | 157 | if (this.vcount < this.VERTICAL_PIXELS) { 158 | this.cpu.mmu.runHblankDmas(); 159 | } 160 | if (this.hblankIRQ) { 161 | this.cpu.irq.raiseIRQ(this.cpu.irq.IRQ_HBLANK); 162 | } 163 | } 164 | } 165 | } 166 | writeDisplayStat(value) { 167 | this.vblankIRQ = value & 0x0008; 168 | this.hblankIRQ = value & 0x0010; 169 | this.vcounterIRQ = value & 0x0020; 170 | this.vcountSetting = (value & 0xff00) >> 8; 171 | 172 | if (this.vcounterIRQ) { 173 | // FIXME: this can be too late if we're in the middle of an Hblank 174 | this.nextVcounterIRQ = 175 | this.nextHblank + 176 | this.HBLANK_LENGTH + 177 | (this.vcountSetting - this.vcount) * this.HORIZONTAL_LENGTH; 178 | if (this.nextVcounterIRQ < this.nextEvent) { 179 | this.nextVcounterIRQ += this.TOTAL_LENGTH; 180 | } 181 | } 182 | } 183 | readDisplayStat() { 184 | return this.inVblank | (this.inHblank << 1) | (this.vcounter << 2); 185 | } 186 | finishDraw(pixelData) { 187 | this.context.putImageData(pixelData, 0, 0); 188 | this.drawCallback(); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /debugger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gbajs2 Debugger 5 | 6 | 7 | 8 | 76 | 77 | 78 |
79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 |
87 |
    88 | 89 |
    90 |
    91 |
    92 | 97 |
    98 |
    99 |
    100 | 101 | 102 | 103 | 104 | 105 |
    106 |
    107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
    115 |
    116 |
    117 |

    GPRs

    118 |
      119 |
    1. 0x00000000
    2. 120 |
    3. 0x00000000
    4. 121 |
    5. 0x00000000
    6. 122 |
    7. 0x00000000
    8. 123 |
    9. 0x00000000
    10. 124 |
    11. 0x00000000
    12. 125 |
    13. 0x00000000
    14. 126 |
    15. 0x00000000
    16. 127 |
    17. 0x00000000
    18. 128 |
    19. 0x00000000
    20. 129 |
    21. 0x00000000
    22. 130 |
    23. 0x00000000
    24. 131 |
    25. 0x00000000
    26. 132 |
    27. 0x00000000
    28. 133 |
    29. 0x00000000
    30. 134 |
    31. 0x00000000
    32. 135 |
    136 |
    137 |
    138 |

    Status bits

    139 |
      140 |
    1. N
    2. 141 |
    3. Z
    4. 142 |
    5. C
    6. 143 |
    7. V
    8. 144 |
    9. I
    10. 145 |
    11. T
    12. 146 |
    13. Mode: SYSTEM
    14. 147 |
    148 |
    149 |
    150 |
    151 |
    152 | Add breakpoint: 153 | 154 |
    155 |
      156 |
    157 |
    158 |
    159 |
    160 |
    161 |

    Jump to:

    162 |
      163 |
    • 164 | Region: 165 | 178 |
    • 179 |
    • 180 | Address: 181 | 182 |
    • 183 |
    184 |
    185 |
      186 |
      187 |
      188 | 189 | -------------------------------------------------------------------------------- /js/keypad.js: -------------------------------------------------------------------------------- 1 | class GameBoyAdvanceKeypad { 2 | constructor() { 3 | this.KEYCODE_LEFT = 37; 4 | this.KEYCODE_UP = 38; 5 | this.KEYCODE_RIGHT = 39; 6 | this.KEYCODE_DOWN = 40; 7 | this.KEYCODE_START = 13; 8 | this.KEYCODE_SELECT = 220; 9 | this.KEYCODE_A = 90; 10 | this.KEYCODE_B = 88; 11 | this.KEYCODE_L = 65; 12 | this.KEYCODE_R = 83; 13 | 14 | this.GAMEPAD_LEFT = 14; 15 | this.GAMEPAD_UP = 12; 16 | this.GAMEPAD_RIGHT = 15; 17 | this.GAMEPAD_DOWN = 13; 18 | this.GAMEPAD_START = 9; 19 | this.GAMEPAD_SELECT = 8; 20 | this.GAMEPAD_A = 1; 21 | this.GAMEPAD_B = 0; 22 | this.GAMEPAD_L = 4; 23 | this.GAMEPAD_R = 5; 24 | this.GAMEPAD_THRESHOLD = 0.2; 25 | 26 | this.A = 0; 27 | this.B = 1; 28 | this.SELECT = 2; 29 | this.START = 3; 30 | this.RIGHT = 4; 31 | this.LEFT = 5; 32 | this.UP = 6; 33 | this.DOWN = 7; 34 | this.R = 8; 35 | this.L = 9; 36 | 37 | this.currentDown = 0x03ff; 38 | this.eatInput = false; 39 | 40 | this.gamepads = []; 41 | 42 | this.remappingKeyId = ""; 43 | } 44 | keyboardHandler(e) { 45 | var toggle = 0; 46 | 47 | // Check for a remapping 48 | if (this.remappingKeyId != "") { 49 | this.remapKeycode(this.remappingKeyId, e.keyCode); 50 | this.remappingKeyId = ""; 51 | e.preventDefault(); 52 | return; // Could do an else and wrap the rest of the function in it, but this is cleaner 53 | } 54 | 55 | switch (e.keyCode) { 56 | case this.KEYCODE_START: 57 | toggle = this.START; 58 | break; 59 | case this.KEYCODE_SELECT: 60 | toggle = this.SELECT; 61 | break; 62 | case this.KEYCODE_A: 63 | toggle = this.A; 64 | break; 65 | case this.KEYCODE_B: 66 | toggle = this.B; 67 | break; 68 | case this.KEYCODE_L: 69 | toggle = this.L; 70 | break; 71 | case this.KEYCODE_R: 72 | toggle = this.R; 73 | break; 74 | case this.KEYCODE_UP: 75 | toggle = this.UP; 76 | break; 77 | case this.KEYCODE_RIGHT: 78 | toggle = this.RIGHT; 79 | break; 80 | case this.KEYCODE_DOWN: 81 | toggle = this.DOWN; 82 | break; 83 | case this.KEYCODE_LEFT: 84 | toggle = this.LEFT; 85 | break; 86 | default: 87 | return; 88 | } 89 | 90 | toggle = 1 << toggle; 91 | if (e.type == "keydown") { 92 | this.currentDown &= ~toggle; 93 | } else { 94 | this.currentDown |= toggle; 95 | } 96 | 97 | if (this.eatInput) { 98 | e.preventDefault(); 99 | } 100 | } 101 | gamepadHandler(gamepad) { 102 | var value = 0; 103 | if (gamepad.buttons[this.GAMEPAD_LEFT] > this.GAMEPAD_THRESHOLD) { 104 | value |= 1 << this.LEFT; 105 | } 106 | if (gamepad.buttons[this.GAMEPAD_UP] > this.GAMEPAD_THRESHOLD) { 107 | value |= 1 << this.UP; 108 | } 109 | if (gamepad.buttons[this.GAMEPAD_RIGHT] > this.GAMEPAD_THRESHOLD) { 110 | value |= 1 << this.RIGHT; 111 | } 112 | if (gamepad.buttons[this.GAMEPAD_DOWN] > this.GAMEPAD_THRESHOLD) { 113 | value |= 1 << this.DOWN; 114 | } 115 | if (gamepad.buttons[this.GAMEPAD_START] > this.GAMEPAD_THRESHOLD) { 116 | value |= 1 << this.START; 117 | } 118 | if (gamepad.buttons[this.GAMEPAD_SELECT] > this.GAMEPAD_THRESHOLD) { 119 | value |= 1 << this.SELECT; 120 | } 121 | if (gamepad.buttons[this.GAMEPAD_A] > this.GAMEPAD_THRESHOLD) { 122 | value |= 1 << this.A; 123 | } 124 | if (gamepad.buttons[this.GAMEPAD_B] > this.GAMEPAD_THRESHOLD) { 125 | value |= 1 << this.B; 126 | } 127 | if (gamepad.buttons[this.GAMEPAD_L] > this.GAMEPAD_THRESHOLD) { 128 | value |= 1 << this.L; 129 | } 130 | if (gamepad.buttons[this.GAMEPAD_R] > this.GAMEPAD_THRESHOLD) { 131 | value |= 1 << this.R; 132 | } 133 | 134 | this.currentDown = ~value & 0x3ff; 135 | } 136 | gamepadConnectHandler(gamepad) { 137 | this.gamepads.push(gamepad); 138 | } 139 | gamepadDisconnectHandler(gamepad) { 140 | this.gamepads = self.gamepads.filter(function (other) { 141 | return other != gamepad; 142 | }); 143 | } 144 | pollGamepads() { 145 | var navigatorList = []; 146 | if (navigator.webkitGetGamepads) { 147 | navigatorList = navigator.webkitGetGamepads(); 148 | } else if (navigator.getGamepads) { 149 | navigatorList = navigator.getGamepads(); 150 | } 151 | 152 | // Let's all give a shout out to Chrome for making us get the gamepads EVERY FRAME 153 | /* How big of a performance draw is this? Would it be worth letting users know? */ 154 | if (navigatorList.length) { 155 | this.gamepads = []; 156 | } 157 | for (var i = 0; i < navigatorList.length; ++i) { 158 | if (navigatorList[i]) { 159 | this.gamepads.push(navigatorList[i]); 160 | } 161 | } 162 | if (this.gamepads.length > 0) { 163 | this.gamepadHandler(this.gamepads[0]); 164 | } 165 | } 166 | registerHandlers() { 167 | window.addEventListener( 168 | "keydown", 169 | this.keyboardHandler.bind(this), 170 | true 171 | ); 172 | window.addEventListener("keyup", this.keyboardHandler.bind(this), true); 173 | 174 | window.addEventListener( 175 | "gamepadconnected", 176 | this.gamepadConnectHandler.bind(this), 177 | true 178 | ); 179 | window.addEventListener( 180 | "mozgamepadconnected", 181 | this.gamepadConnectHandler.bind(this), 182 | true 183 | ); 184 | window.addEventListener( 185 | "webkitgamepadconnected", 186 | this.gamepadConnectHandler.bind(this), 187 | true 188 | ); 189 | 190 | window.addEventListener( 191 | "gamepaddisconnected", 192 | this.gamepadDisconnectHandler.bind(this), 193 | true 194 | ); 195 | window.addEventListener( 196 | "mozgamepaddisconnected", 197 | this.gamepadDisconnectHandler.bind(this), 198 | true 199 | ); 200 | window.addEventListener( 201 | "webkitgamepaddisconnected", 202 | this.gamepadDisconnectHandler.bind(this), 203 | true 204 | ); 205 | } 206 | // keyId is ["A", "B", "SELECT", "START", "RIGHT", "LEFT", "UP", "DOWN", "R", "L"] 207 | initKeycodeRemap(keyId) { 208 | // Ensure valid keyId 209 | var validKeyIds = ["A", "B", "SELECT", "START", "RIGHT", "LEFT", "UP", "DOWN", "R", "L"]; 210 | if (validKeyIds.indexOf(keyId) != -1) { 211 | /* If remappingKeyId holds a value, the keydown event above will 212 | wait for the next keypress to assign the keycode */ 213 | this.remappingKeyId = keyId; 214 | } 215 | } 216 | // keyId is ["A", "B", "SELECT", "START", "RIGHT", "LEFT", "UP", "DOWN", "R", "L"] 217 | remapKeycode(keyId, keycode) { 218 | switch (keyId) { 219 | case "A": 220 | this.KEYCODE_A = keycode; 221 | break; 222 | case "B": 223 | this.KEYCODE_B = keycode; 224 | break; 225 | case "SELECT": 226 | this.KEYCODE_SELECT = keycode; 227 | break; 228 | case "START": 229 | this.KEYCODE_START = keycode; 230 | break; 231 | case "RIGHT": 232 | this.KEYCODE_RIGHT = keycode; 233 | break; 234 | case "LEFT": 235 | this.KEYCODE_LEFT = keycode; 236 | break; 237 | case "UP": 238 | this.KEYCODE_UP = keycode; 239 | break; 240 | case "DOWN": 241 | this.KEYCODE_DOWN = keycode; 242 | break; 243 | case "R": 244 | this.KEYCODE_R = keycode; 245 | break; 246 | case "L": 247 | this.KEYCODE_L = keycode; 248 | break; 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 82 | 83 | 84 |
      85 |
      86 |
      87 | 88 | 89 | 90 | 91 | 92 |
      93 |
        94 | 95 |
        96 |
        97 |
        98 | 99 |
        100 |
        101 | 102 | 103 | 104 | 105 | 106 |
        107 |
        108 | Load ROM: 109 | 110 |
        111 |
        112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
        121 |
        122 |
        123 |

        GPRs

        124 |
          125 |
        1. 0x00000000
        2. 126 |
        3. 0x00000000
        4. 127 |
        5. 0x00000000
        6. 128 |
        7. 0x00000000
        8. 129 |
        9. 0x00000000
        10. 130 |
        11. 0x00000000
        12. 131 |
        13. 0x00000000
        14. 132 |
        15. 0x00000000
        16. 133 |
        17. 0x00000000
        18. 134 |
        19. 0x00000000
        20. 135 |
        21. 0x00000000
        22. 136 |
        23. 0x00000000
        24. 137 |
        25. 0x00000000
        26. 138 |
        27. 0x00000000
        28. 139 |
        29. 0x00000000
        30. 140 |
        31. 0x00000000
        32. 141 |
        142 |
        143 |
        144 |

        Status bits

        145 |
          146 |
        1. N
        2. 147 |
        3. Z
        4. 148 |
        5. C
        6. 149 |
        7. V
        8. 150 |
        9. I
        10. 151 |
        11. T
        12. 152 |
        13. Mode: SYSTEM
        14. 153 |
        154 |
        155 |
        156 |
        157 |
        158 | Add breakpoint: 159 | 160 |
        161 |
          162 |
        163 |
        164 |
        165 |
        166 |
        167 |

        Jump to:

        168 |
          169 |
        • 170 | Region: 171 | 184 |
        • 185 |
        • 186 | Address: 187 | 188 |
        • 189 |
        190 |
        191 |
          192 |
          193 |
          194 | 195 | 196 | -------------------------------------------------------------------------------- /resources/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url('bg.png') no-repeat top center, #765490; 3 | text-align: center; 4 | margin: 0; 5 | color: white; 6 | font-family: 'Calibri', 'Verdana', sans-serif; 7 | overflow: hidden; 8 | font-size: 15px; 9 | height: 100%; 10 | } 11 | 12 | button { 13 | font-family: 'Calibri', 'Verdana', sans-serif; 14 | } 15 | 16 | #controls * { 17 | -webkit-font-smoothing: antialiased; /* I'm so sorry */ 18 | } 19 | 20 | #screen { 21 | margin-top: 85px; 22 | margin-bottom: -405px; /* Take up no height */ 23 | } 24 | 25 | #screen[width="240"] { 26 | margin-top: 42.5px; 27 | margin-bottom: -363px; /* Take up no height */ 28 | image-rendering: -webkit-optimize-contrast; 29 | image-rendering: -moz-crisp-edges; 30 | image-rendering: -o-crisp-edges; 31 | zoom: 2; 32 | -o-transform: scale(2); 33 | } 34 | 35 | #crash { 36 | margin-top: 100px; 37 | margin-bottom: -356px; 38 | } 39 | 40 | p:first-child { 41 | margin-top: 0; 42 | } 43 | 44 | a:link, a:visited { 45 | color: #fcb3f0; 46 | } 47 | 48 | #controls { 49 | margin-top: 60px; 50 | margin-right: 50%; 51 | height: 0; 52 | } 53 | 54 | #controls > *, #instructions { 55 | opacity: 1; 56 | -webkit-transition: opacity ease-out 0.6s; 57 | -moz-transition: opacity ease-out 0.6s; 58 | -o-transition: opacity ease-out 0.6s; 59 | transition: opacity ease-out 0.6s; 60 | } 61 | 62 | #controls button, #mapping td, #sound, #controls label { 63 | background: transparent; 64 | border: 0px solid rgba(192, 128, 192, 0.5); 65 | border-top: 2px solid rgba(96, 48, 96, 0.8); 66 | border-bottom: 2px solid rgba(255, 200, 255, 0.5); 67 | border-radius: 30px; 68 | color: white; 69 | font-weight: bold; 70 | display: block; 71 | padding: 6px 15px; 72 | margin-right: 330px; 73 | margin-left: auto; 74 | margin-top: 20px; 75 | font-size: 20px; 76 | text-shadow: 0 2px rgba(96, 48, 96, 0.8), 0 -1px rgba(255, 200, 255, 0.5); 77 | } 78 | 79 | #controls .bigbutton, #mapping td { 80 | font-size: 30px; 81 | padding: 8px 25px; 82 | } 83 | 84 | #moreinfo { 85 | position: absolute; 86 | right: 0; 87 | top: 0; 88 | width: 0px; 89 | bottom: 0px; 90 | overflow: hidden; 91 | padding: 10px 15px; 92 | background: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.2) 12px, rgba(0, 0, 0, 0) 20px), #452e5c; 93 | background: -moz-linear-gradient(left, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.2) 12px, rgba(0, 0, 0, 0) 20px), #452e5c; 94 | background: -ms-linear-gradient(left, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.2) 12px, rgba(0, 0, 0, 0) 20px), #452e5c; 95 | background: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.2) 12px, rgba(0, 0, 0, 0) 20px), #452e5c; 96 | -webkit-transition: width ease-out 0.5s, padding ease-out 0.5s; 97 | -moz-transition: width ease-out 0.5s, padding ease-out 0.5s; 98 | -o-transition: width ease-out 0.5s, padding ease-out 0.5s; 99 | transition: width ease-out 0.5s, padding ease-out 0.5s; 100 | z-index: 15; 101 | } 102 | 103 | #moreinfo:hover { 104 | width: 500px; 105 | padding: 10px 10px; 106 | } 107 | 108 | #moreinfo > * { 109 | width: 480px; 110 | } 111 | 112 | #rollover { 113 | position: absolute; 114 | z-index: 20; 115 | top: 15%; 116 | right: -1px; 117 | width: 6em; 118 | padding: 5px; 119 | background: #765490; 120 | box-shadow: 0 2px 5px -2px rgba(0, 0, 0, 0.6) inset, -4px -1px 5px -3px black; 121 | border-radius: 5px 5px 0 0; 122 | border: 1px solid rgba(0, 0, 0, 0.3); 123 | -webkit-transform-origin: 100% 100%; 124 | -webkit-transform: rotate(-90deg); 125 | -webkit-transition: right ease-out 0.5s; 126 | -moz-transform-origin: 100% 100%; 127 | -moz-transform: rotate(-90deg); 128 | -moz-transition: right ease-out 0.5s; 129 | -o-transform-origin: 100% 100%; 130 | -o-transform: rotate(-90deg); 131 | -o-transition: right ease-out 0.5s; 132 | transform-origin: 100% 100%; 133 | transform: rotate(-90deg); 134 | transition: right ease-out 0.5s; 135 | } 136 | 137 | .hidden { 138 | opacity: 0 !important; 139 | } 140 | 141 | .dead { 142 | display: none; 143 | } 144 | 145 | input[type=file] { 146 | opacity: 0 !important; 147 | width: 0; 148 | height: 0; 149 | margin: 0; 150 | padding: 0; 151 | border: none; 152 | } 153 | 154 | #moreinfo:hover #rollover { 155 | right: -2em; 156 | } 157 | 158 | h1 { 159 | font-size: 40px; 160 | margin-bottom: 10px; 161 | margin-top: 0; 162 | } 163 | 164 | h2 { 165 | font-size: 18px; 166 | font-weight: normal; 167 | font-style: italic; 168 | } 169 | 170 | footer { 171 | font-size: 12px; 172 | padding-top: 8px; 173 | } 174 | 175 | p { 176 | margin-top: 0px; 177 | } 178 | 179 | ul#links { 180 | display: block; 181 | text-align: center; 182 | padding: 0; 183 | } 184 | 185 | ul#links li { 186 | display: inline; 187 | list-style-type: none; 188 | } 189 | 190 | ul#links li:not(:first-child)::before { 191 | content: "•"; 192 | margin: 0 0.5em; 193 | } 194 | 195 | #stats { 196 | text-align: right; 197 | font-size: 12px; 198 | } 199 | 200 | #moreinfo header { 201 | background-color: #27282A; 202 | margin: 20px 0 0 20px; 203 | padding: 20px 30px; 204 | border-radius: 10% 10% 50% 50% / 20% 20% 30% 30%; 205 | border-bottom: 6px solid #D3B2D9; 206 | border-top: 6px solid rgba(20, 22, 24, 0.5); 207 | text-shadow: 0 3px rgba(0, 0, 0, 0.8); 208 | position: relative; 209 | z-index: 10; 210 | width: 400px; 211 | height: 13em; 212 | position: absolute; 213 | top: 0px; 214 | } 215 | 216 | #moreinfo > footer { 217 | position: absolute; 218 | bottom: 15px; 219 | } 220 | 221 | #moreinfo dl { 222 | text-align: left; 223 | overflow-y: scroll; 224 | bottom: 50px; 225 | margin: 0px 50px; 226 | padding-left: 0; 227 | padding-top: 50px; 228 | padding-bottom: 20px; 229 | border-bottom-left-radius: 10px 40px; 230 | border-left: 3px solid #D3B2D9; 231 | position: absolute; 232 | top: 14.5em; 233 | z-index: 9; 234 | width: 390px; 235 | } 236 | 237 | #moreinfo dt { 238 | font-weight: bold; 239 | border-bottom: 1px solid #D3B2D9; 240 | border-bottom-left-radius: 50px 10px; 241 | border-bottom-right-radius: 30px 2px; 242 | padding: 5px 10px 5px 20px; 243 | } 244 | 245 | #moreinfo dd { 246 | margin: 0; 247 | padding: 10px 10px 10px 20px; 248 | font-size: 14px; 249 | } 250 | 251 | #gameinfo { 252 | margin-top: 50px; 253 | margin-bottom: -50px; 254 | margin-left: 50%; 255 | padding-left: 320px; 256 | width: 300px; 257 | height: 0; 258 | } 259 | 260 | #instructions { 261 | margin-top: 140px; 262 | margin-bottom: -70px; 263 | margin-right: 50%; 264 | padding-right: 320px; 265 | margin-left: auto; 266 | padding-left: 20px; 267 | min-width: 300px; 268 | height: 0; 269 | } 270 | 271 | #mapping { 272 | margin: auto; 273 | text-align: center; 274 | border-spacing: 1px 5px; 275 | } 276 | 277 | #mapping td { 278 | padding: 5px 23px; 279 | display: table-cell; 280 | } 281 | 282 | #mapping td:first-child { 283 | border-top-right-radius: 0; 284 | border-bottom-right-radius: 0; 285 | padding-right: 10px; 286 | border-right: 1px solid rgba(96, 48, 96, 0.8); 287 | } 288 | 289 | #mapping td:last-child { 290 | border-top-left-radius: 0; 291 | border-bottom-left-radius: 0; 292 | border-left: 1px solid rgba(96, 48, 96, 0.8); 293 | } 294 | 295 | #sound, #controls label { 296 | float: right; 297 | } 298 | 299 | #sound p { 300 | margin: 0; 301 | } 302 | -------------------------------------------------------------------------------- /js/savedata.js: -------------------------------------------------------------------------------- 1 | class SRAMSavedata extends MemoryView { 2 | constructor(size) { 3 | super(new ArrayBuffer(size), 0); 4 | 5 | this.writePending = false; 6 | } 7 | store8(offset, value) { 8 | this.view.setInt8(offset, value); 9 | this.writePending = true; 10 | } 11 | store16(offset, value) { 12 | this.view.setInt16(offset, value, true); 13 | this.writePending = true; 14 | } 15 | store32(offset, value) { 16 | this.view.setInt32(offset, value, true); 17 | this.writePending = true; 18 | } 19 | } 20 | 21 | class FlashSavedata extends MemoryView { 22 | constructor(size) { 23 | super(new ArrayBuffer(size), 0); 24 | 25 | this.COMMAND_WIPE = 0x10; 26 | this.COMMAND_ERASE_SECTOR = 0x30; 27 | this.COMMAND_ERASE = 0x80; 28 | this.COMMAND_ID = 0x90; 29 | this.COMMAND_WRITE = 0xa0; 30 | this.COMMAND_SWITCH_BANK = 0xb0; 31 | this.COMMAND_TERMINATE_ID = 0xf0; 32 | 33 | this.ID_PANASONIC = 0x1b32; 34 | this.ID_SANYO = 0x1362; 35 | 36 | this.bank0 = new DataView(this.buffer, 0, 0x00010000); 37 | if (size > 0x00010000) { 38 | this.id = this.ID_SANYO; 39 | this.bank1 = new DataView(this.buffer, 0x00010000); 40 | } else { 41 | this.id = this.ID_PANASONIC; 42 | this.bank1 = null; 43 | } 44 | this.bank = this.bank0; 45 | 46 | this.idMode = false; 47 | this.writePending = false; 48 | 49 | this.first = 0; 50 | this.second = 0; 51 | this.command = 0; 52 | this.pendingCommand = 0; 53 | } 54 | load8(offset) { 55 | if (this.idMode && offset < 2) { 56 | return (this.id >> (offset << 3)) & 0xff; 57 | } else if (offset < 0x10000) { 58 | return this.bank.getInt8(offset); 59 | } else { 60 | return 0; 61 | } 62 | } 63 | load16(offset) { 64 | return (this.load8(offset) & 0xff) | (this.load8(offset + 1) << 8); 65 | } 66 | load32(offset) { 67 | return ( 68 | (this.load8(offset) & 0xff) | 69 | (this.load8(offset + 1) << 8) | 70 | (this.load8(offset + 2) << 16) | 71 | (this.load8(offset + 3) << 24) 72 | ); 73 | } 74 | loadU8(offset) { 75 | return this.load8(offset) & 0xff; 76 | } 77 | loadU16(offset) { 78 | return (this.loadU8(offset) & 0xff) | (this.loadU8(offset + 1) << 8); 79 | } 80 | store8(offset, value) { 81 | switch (this.command) { 82 | case 0: 83 | if (offset == 0x5555) { 84 | if (this.second == 0x55) { 85 | switch (value) { 86 | case this.COMMAND_ERASE: 87 | this.pendingCommand = value; 88 | break; 89 | case this.COMMAND_ID: 90 | this.idMode = true; 91 | break; 92 | case this.COMMAND_TERMINATE_ID: 93 | this.idMode = false; 94 | break; 95 | default: 96 | this.command = value; 97 | break; 98 | } 99 | this.second = 0; 100 | this.first = 0; 101 | } else { 102 | this.command = 0; 103 | this.first = value; 104 | this.idMode = false; 105 | } 106 | } else if (offset == 0x2aaa && this.first == 0xaa) { 107 | this.first = 0; 108 | if (this.pendingCommand) { 109 | this.command = this.pendingCommand; 110 | } else { 111 | this.second = value; 112 | } 113 | } 114 | break; 115 | case this.COMMAND_ERASE: 116 | switch (value) { 117 | case this.COMMAND_WIPE: 118 | if (offset == 0x5555) { 119 | for (var i = 0; i < this.view.byteLength; i += 4) { 120 | this.view.setInt32(i, -1); 121 | } 122 | } 123 | break; 124 | case this.COMMAND_ERASE_SECTOR: 125 | if ((offset & 0x0fff) == 0) { 126 | for (var i = offset; i < offset + 0x1000; i += 4) { 127 | this.bank.setInt32(i, -1); 128 | } 129 | } 130 | break; 131 | } 132 | this.pendingCommand = 0; 133 | this.command = 0; 134 | break; 135 | case this.COMMAND_WRITE: 136 | this.bank.setInt8(offset, value); 137 | this.command = 0; 138 | 139 | this.writePending = true; 140 | break; 141 | case this.COMMAND_SWITCH_BANK: 142 | if (this.bank1 && offset == 0) { 143 | if (value == 1) { 144 | this.bank = this.bank1; 145 | } else { 146 | this.bank = this.bank0; 147 | } 148 | } 149 | this.command = 0; 150 | break; 151 | } 152 | } 153 | store16(offset, value) { 154 | throw new Error('Unaligned save to flash!'); 155 | } 156 | store32(offset, value) { 157 | throw new Error('Unaligned save to flash!'); 158 | } 159 | replaceData(memory) { 160 | var bank = this.view === this.bank1; 161 | MemoryView.prototype.replaceData.call(this, memory, 0); 162 | 163 | this.bank0 = new DataView(this.buffer, 0, 0x00010000); 164 | if (memory.byteLength > 0x00010000) { 165 | this.bank1 = new DataView(this.buffer, 0x00010000); 166 | } else { 167 | this.bank1 = null; 168 | } 169 | this.bank = bank ? this.bank1 : this.bank0; 170 | } 171 | } 172 | 173 | class EEPROMSavedata extends MemoryView { 174 | constructor(size, mmu) { 175 | super(new ArrayBuffer(size), 0); 176 | 177 | this.writeAddress = 0; 178 | this.readBitsRemaining = 0; 179 | this.readAddress = 0; 180 | 181 | this.command = 0; 182 | this.commandBitsRemaining = 0; 183 | 184 | this.realSize = 0; 185 | this.addressBits = 0; 186 | this.writePending = false; 187 | 188 | this.dma = mmu.core.irq.dma[3]; 189 | 190 | this.COMMAND_NULL = 0; 191 | this.COMMAND_PENDING = 1; 192 | this.COMMAND_WRITE = 2; 193 | this.COMMAND_READ_PENDING = 3; 194 | this.COMMAND_READ = 4; 195 | } 196 | load8(offset) { 197 | throw new Error('Unsupported 8-bit access!'); 198 | } 199 | load16(offset) { 200 | return this.loadU16(offset); 201 | } 202 | loadU8(offset) { 203 | throw new Error('Unsupported 8-bit access!'); 204 | } 205 | loadU16(offset) { 206 | if (this.command != this.COMMAND_READ || !this.dma.enable) { 207 | return 1; 208 | } 209 | --this.readBitsRemaining; 210 | if (this.readBitsRemaining < 64) { 211 | var step = 63 - this.readBitsRemaining; 212 | var data = 213 | this.view.getUint8((this.readAddress + step) >> 3, false) >> 214 | (0x7 - (step & 0x7)); 215 | if (!this.readBitsRemaining) { 216 | this.command = this.COMMAND_NULL; 217 | } 218 | return data & 0x1; 219 | } 220 | return 0; 221 | } 222 | load32(offset) { 223 | throw new Error('Unsupported 32-bit access!'); 224 | } 225 | store8(offset, value) { 226 | throw new Error('Unsupported 8-bit access!'); 227 | } 228 | store16(offset, value) { 229 | switch (this.command) { 230 | // Read header 231 | case this.COMMAND_NULL: 232 | default: 233 | this.command = value & 0x1; 234 | break; 235 | case this.COMMAND_PENDING: 236 | this.command <<= 1; 237 | this.command |= value & 0x1; 238 | if (this.command == this.COMMAND_WRITE) { 239 | if (!this.realSize) { 240 | var bits = this.dma.count - 67; 241 | this.realSize = 8 << bits; 242 | this.addressBits = bits; 243 | } 244 | this.commandBitsRemaining = this.addressBits + 64 + 1; 245 | this.writeAddress = 0; 246 | } else { 247 | if (!this.realSize) { 248 | var bits = this.dma.count - 3; 249 | this.realSize = 8 << bits; 250 | this.addressBits = bits; 251 | } 252 | this.commandBitsRemaining = this.addressBits + 1; 253 | this.readAddress = 0; 254 | } 255 | break; 256 | // Do commands 257 | case this.COMMAND_WRITE: 258 | // Write 259 | if (--this.commandBitsRemaining > 64) { 260 | this.writeAddress <<= 1; 261 | this.writeAddress |= (value & 0x1) << 6; 262 | } else if (this.commandBitsRemaining <= 0) { 263 | this.command = this.COMMAND_NULL; 264 | this.writePending = true; 265 | } else { 266 | var current = this.view.getUint8(this.writeAddress >> 3); 267 | current &= ~(1 << (0x7 - (this.writeAddress & 0x7))); 268 | current |= 269 | (value & 0x1) << (0x7 - (this.writeAddress & 0x7)); 270 | this.view.setUint8(this.writeAddress >> 3, current); 271 | ++this.writeAddress; 272 | } 273 | break; 274 | case this.COMMAND_READ_PENDING: 275 | // Read 276 | if (--this.commandBitsRemaining > 0) { 277 | this.readAddress <<= 1; 278 | if (value & 0x1) { 279 | this.readAddress |= 0x40; 280 | } 281 | } else { 282 | this.readBitsRemaining = 68; 283 | this.command = this.COMMAND_READ; 284 | } 285 | break; 286 | } 287 | } 288 | store32(offset, value) { 289 | throw new Error('Unsupported 32-bit access!'); 290 | } 291 | replaceData(memory) { 292 | MemoryView.prototype.replaceData.call(this, memory, 0); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /js/util.js: -------------------------------------------------------------------------------- 1 | function hex(number, leading, usePrefix) { 2 | if (typeof usePrefix === "undefined") { 3 | usePrefix = true; 4 | } 5 | if (typeof leading === "undefined") { 6 | leading = 8; 7 | } 8 | var string = (number >>> 0).toString(16).toUpperCase(); 9 | leading -= string.length; 10 | if (leading < 0) return string; 11 | return (usePrefix ? "0x" : "") + new Array(leading + 1).join("0") + string; 12 | } 13 | 14 | class Pointer { 15 | constructor() { 16 | this.index = 0; 17 | this.top = 0; 18 | this.stack = []; 19 | } 20 | advance(amount) { 21 | var index = this.index; 22 | this.index += amount; 23 | return index; 24 | } 25 | mark() { 26 | return this.index - this.top; 27 | } 28 | push() { 29 | this.stack.push(this.top); 30 | this.top = this.index; 31 | } 32 | pop() { 33 | this.top = this.stack.pop(); 34 | } 35 | readString(view) { 36 | var length = view.getUint32(this.advance(4), true); 37 | var bytes = []; 38 | for (var i = 0; i < length; ++i) { 39 | bytes.push(String.fromCharCode(view.getUint8(this.advance(1)))); 40 | } 41 | return bytes.join(""); 42 | } 43 | } 44 | 45 | class SerializerClass { 46 | TAG_INT = 1; 47 | TAG_STRING = 2; 48 | TAG_STRUCT = 3; 49 | TAG_BLOB = 4; 50 | TAG_BOOLEAN = 5; 51 | TYPE = "application/octet-stream"; 52 | 53 | pack(value) { 54 | var object = new DataView(new ArrayBuffer(4)); 55 | object.setUint32(0, value, true); 56 | return object.buffer; 57 | } 58 | 59 | pack8(value) { 60 | var object = new DataView(new ArrayBuffer(1)); 61 | object.setUint8(0, value, true); 62 | return object.buffer; 63 | } 64 | 65 | prefix(value) { 66 | return new Blob( 67 | [ 68 | Serializer.pack(value.size || value.length || value.byteLength), 69 | value 70 | ], 71 | { type: Serializer.TYPE } 72 | ); 73 | } 74 | 75 | serialize(stream) { 76 | var parts = []; 77 | var size = 4; 78 | for (const i of Object.keys(stream)) { 79 | if (stream.hasOwnProperty(i)) { 80 | var tag; 81 | var head = Serializer.prefix(i); 82 | var body; 83 | switch (typeof stream[i]) { 84 | case "number": 85 | tag = Serializer.TAG_INT; 86 | body = Serializer.pack(stream[i]); 87 | break; 88 | case "string": 89 | tag = Serializer.TAG_STRING; 90 | body = Serializer.prefix(stream[i]); 91 | break; 92 | case "object": 93 | if (stream[i].type == Serializer.TYPE) { 94 | tag = Serializer.TAG_BLOB; 95 | body = stream[i]; 96 | } else { 97 | tag = Serializer.TAG_STRUCT; 98 | body = Serializer.serialize(stream[i]); 99 | } 100 | break; 101 | case "boolean": 102 | tag = Serializer.TAG_BOOLEAN; 103 | body = Serializer.pack8(stream[i]); 104 | break; 105 | default: 106 | console.log(stream[i]); 107 | break; 108 | } 109 | size += 110 | 1 + 111 | head.size + 112 | (body.size || body.byteLength || body.length); 113 | parts.push(Serializer.pack8(tag)); 114 | parts.push(head); 115 | parts.push(body); 116 | } 117 | } 118 | parts.unshift(Serializer.pack(size)); 119 | return new Blob(parts); 120 | } 121 | 122 | deserialize(blob, callback) { 123 | var reader = new FileReader(); 124 | reader.onload = function (data) { 125 | callback( 126 | Serializer.deserealizeStream( 127 | new DataView(data.target.result), 128 | new Pointer() 129 | ) 130 | ); 131 | }; 132 | reader.readAsArrayBuffer(blob); 133 | } 134 | 135 | deserealizeStream(view, pointer) { 136 | pointer.push(); 137 | var object = {}; 138 | var remaining = view.getUint32(pointer.advance(4), true); 139 | while (pointer.mark() < remaining) { 140 | var tag = view.getUint8(pointer.advance(1)); 141 | var head = pointer.readString(view); 142 | var body; 143 | switch (tag) { 144 | case Serializer.TAG_INT: 145 | body = view.getUint32(pointer.advance(4), true); 146 | break; 147 | case Serializer.TAG_STRING: 148 | body = pointer.readString(view); 149 | break; 150 | case Serializer.TAG_STRUCT: 151 | body = Serializer.deserealizeStream(view, pointer); 152 | break; 153 | case Serializer.TAG_BLOB: 154 | var size = view.getUint32(pointer.advance(4), true); 155 | body = view.buffer.slice( 156 | pointer.advance(size), 157 | pointer.advance(0) 158 | ); 159 | break; 160 | case Serializer.TAG_BOOLEAN: 161 | body = !!view.getUint8(pointer.advance(1)); 162 | break; 163 | } 164 | object[head] = body; 165 | } 166 | if (pointer.mark() > remaining) { 167 | throw "Size of serialized data exceeded"; 168 | } 169 | pointer.pop(); 170 | return object; 171 | } 172 | 173 | serializePNG(blob, base, callback) { 174 | var canvas = document.createElement("canvas"); 175 | var context = canvas.getContext("2d"); 176 | var pixels = base 177 | .getContext("2d") 178 | .getImageData(0, 0, base.width, base.height); 179 | var transparent = 0; 180 | for (var y = 0; y < base.height; ++y) { 181 | for (var x = 0; x < base.width; ++x) { 182 | if (!pixels.data[(x + y * base.width) * 4 + 3]) { 183 | ++transparent; 184 | } 185 | } 186 | } 187 | var bytesInCanvas = 188 | transparent * 3 + (base.width * base.height - transparent); 189 | for ( 190 | var multiplier = 1; 191 | bytesInCanvas * multiplier * multiplier < blob.size; 192 | ++multiplier 193 | ); 194 | var edges = bytesInCanvas * multiplier * multiplier - blob.size; 195 | var padding = Math.ceil(edges / (base.width * multiplier)); 196 | canvas.setAttribute("width", base.width * multiplier); 197 | canvas.setAttribute("height", base.height * multiplier + padding); 198 | 199 | var reader = new FileReader(); 200 | reader.onload = function (data) { 201 | var view = new Uint8Array(data.target.result); 202 | var pointer = 0; 203 | var pixelPointer = 0; 204 | var newPixels = context.createImageData( 205 | canvas.width, 206 | canvas.height + padding 207 | ); 208 | for (var y = 0; y < canvas.height; ++y) { 209 | for (var x = 0; x < canvas.width; ++x) { 210 | var oldY = (y / multiplier) | 0; 211 | var oldX = (x / multiplier) | 0; 212 | if ( 213 | oldY > base.height || 214 | !pixels.data[(oldX + oldY * base.width) * 4 + 3] 215 | ) { 216 | newPixels.data[pixelPointer++] = view[pointer++]; 217 | newPixels.data[pixelPointer++] = view[pointer++]; 218 | newPixels.data[pixelPointer++] = view[pointer++]; 219 | newPixels.data[pixelPointer++] = 0; 220 | } else { 221 | var byte = view[pointer++]; 222 | newPixels.data[pixelPointer++] = 223 | pixels.data[(oldX + oldY * base.width) * 4 + 0] | 224 | (byte & 7); 225 | newPixels.data[pixelPointer++] = 226 | pixels.data[(oldX + oldY * base.width) * 4 + 1] | 227 | ((byte >> 3) & 7); 228 | newPixels.data[pixelPointer++] = 229 | pixels.data[(oldX + oldY * base.width) * 4 + 2] | 230 | ((byte >> 6) & 7); 231 | newPixels.data[pixelPointer++] = 232 | pixels.data[(oldX + oldY * base.width) * 4 + 3]; 233 | } 234 | } 235 | } 236 | context.putImageData(newPixels, 0, 0); 237 | callback(canvas.toDataURL("image/png")); 238 | }; 239 | reader.readAsArrayBuffer(blob); 240 | return canvas; 241 | } 242 | 243 | deserializePNG(blob, callback) { 244 | var reader = new FileReader(); 245 | reader.onload = function (data) { 246 | var image = document.createElement("img"); 247 | image.setAttribute("src", data.target.result); 248 | var canvas = document.createElement("canvas"); 249 | canvas.setAttribute("height", image.height); 250 | canvas.setAttribute("width", image.width); 251 | var context = canvas.getContext("2d"); 252 | context.drawImage(image, 0, 0); 253 | var pixels = context.getImageData( 254 | 0, 255 | 0, 256 | canvas.width, 257 | canvas.height 258 | ); 259 | var data = []; 260 | for (var y = 0; y < canvas.height; ++y) { 261 | for (var x = 0; x < canvas.width; ++x) { 262 | if (!pixels.data[(x + y * canvas.width) * 4 + 3]) { 263 | data.push(pixels.data[(x + y * canvas.width) * 4 + 0]); 264 | data.push(pixels.data[(x + y * canvas.width) * 4 + 1]); 265 | data.push(pixels.data[(x + y * canvas.width) * 4 + 2]); 266 | } else { 267 | var byte = 0; 268 | byte |= pixels.data[(x + y * canvas.width) * 4 + 0] & 7; 269 | byte |= 270 | (pixels.data[(x + y * canvas.width) * 4 + 1] & 7) << 271 | 3; 272 | byte |= 273 | (pixels.data[(x + y * canvas.width) * 4 + 2] & 7) << 274 | 6; 275 | data.push(byte); 276 | } 277 | } 278 | } 279 | newBlob = new Blob( 280 | data.map(function (byte) { 281 | var array = new Uint8Array(1); 282 | array[0] = byte; 283 | return array; 284 | }), 285 | { type: Serializer.TYPE } 286 | ); 287 | Serializer.deserialize(newBlob, callback); 288 | }; 289 | reader.readAsDataURL(blob); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /js/video/proxy.js: -------------------------------------------------------------------------------- 1 | class MemoryProxy { 2 | constructor(owner, size, blockSize) { 3 | this.owner = owner; 4 | this.blocks = []; 5 | this.blockSize = blockSize; 6 | this.mask = (1 << blockSize) - 1; 7 | this.size = size; 8 | if (blockSize) { 9 | for (var i = 0; i < (size >> blockSize); ++i) { 10 | this.blocks.push(new MemoryView(new ArrayBuffer(1 << blockSize))); 11 | } 12 | } 13 | else { 14 | this.blockSize = 31; 15 | this.mask = -1; 16 | this.blocks[0] = new MemoryView(new ArrayBuffer(size)); 17 | } 18 | } 19 | combine() { 20 | if (this.blocks.length > 1) { 21 | var combined = new Uint8Array(this.size); 22 | for (var i = 0; i < this.blocks.length; ++i) { 23 | combined.set(new Uint8Array(this.blocks[i].buffer), i << this.blockSize); 24 | } 25 | return combined.buffer; 26 | } 27 | else { 28 | return this.blocks[0].buffer; 29 | } 30 | } 31 | replace(buffer) { 32 | for (var i = 0; i < this.blocks.length; ++i) { 33 | this.blocks[i] = new MemoryView(buffer.slice(i << this.blockSize, (i << this.blockSize) + this.blocks[i].buffer.byteLength)); 34 | } 35 | } 36 | load8(offset) { 37 | return this.blocks[offset >> this.blockSize].load8(offset & this.mask); 38 | } 39 | load16(offset) { 40 | return this.blocks[offset >> this.blockSize].load16(offset & this.mask); 41 | } 42 | loadU8(offset) { 43 | return this.blocks[offset >> this.blockSize].loadU8(offset & this.mask); 44 | } 45 | loadU16(offset) { 46 | return this.blocks[offset >> this.blockSize].loadU16(offset & this.mask); 47 | } 48 | load32(offset) { 49 | return this.blocks[offset >> this.blockSize].load32(offset & this.mask); 50 | } 51 | store8(offset, value) { 52 | if (offset >= this.size) { 53 | return; 54 | } 55 | this.owner.memoryDirtied(this, offset >> this.blockSize); 56 | this.blocks[offset >> this.blockSize].store8(offset & this.mask, value); 57 | this.blocks[offset >> this.blockSize].store8((offset & this.mask) ^ 1, value); 58 | } 59 | store16(offset, value) { 60 | if (offset >= this.size) { 61 | return; 62 | } 63 | this.owner.memoryDirtied(this, offset >> this.blockSize); 64 | return this.blocks[offset >> this.blockSize].store16(offset & this.mask, value); 65 | } 66 | store32(offset, value) { 67 | if (offset >= this.size) { 68 | return; 69 | } 70 | this.owner.memoryDirtied(this, offset >> this.blockSize); 71 | return this.blocks[offset >> this.blockSize].store32(offset & this.mask, value); 72 | } 73 | invalidatePage(address) { } 74 | }; 75 | 76 | class GameBoyAdvanceRenderProxy { 77 | constructor() { 78 | this.worker = new Worker('js/video/worker.js'); 79 | 80 | this.currentFrame = 0; 81 | this.delay = 0; 82 | this.skipFrame = false; 83 | 84 | this.dirty = null; 85 | var self = this; 86 | var handlers = { 87 | finish: function (data) { 88 | self.backing = data.backing; 89 | self.caller.finishDraw(self.backing); 90 | --self.delay; 91 | } 92 | }; 93 | this.worker.onmessage = function (message) { 94 | handlers[message.data['type']](message.data); 95 | }; 96 | } 97 | memoryDirtied(mem, block) { 98 | this.dirty = this.dirty || {}; 99 | this.dirty.memory = this.dirty.memory || {}; 100 | if (mem === this.palette) { 101 | this.dirty.memory.palette = mem.blocks[0].buffer; 102 | } 103 | if (mem === this.oam) { 104 | this.dirty.memory.oam = mem.blocks[0].buffer; 105 | } 106 | if (mem === this.vram) { 107 | this.dirty.memory.vram = this.dirty.memory.vram || []; 108 | this.dirty.memory.vram[block] = mem.blocks[block].buffer; 109 | } 110 | } 111 | clear(mmu) { 112 | this.palette = new MemoryProxy(this, mmu.SIZE_PALETTE_RAM, 0); 113 | this.vram = new MemoryProxy(this, mmu.SIZE_VRAM, 13); 114 | this.oam = new MemoryProxy(this, mmu.SIZE_OAM, 0); 115 | 116 | this.dirty = null; 117 | this.scanlineQueue = []; 118 | 119 | this.worker.postMessage({ type: 'clear', SIZE_VRAM: mmu.SIZE_VRAM, SIZE_OAM: mmu.SIZE_OAM }); 120 | } 121 | freeze(encodeBase64) { 122 | return { 123 | 'palette': Serializer.prefix(this.palette.combine()), 124 | 'vram': Serializer.prefix(this.vram.combine()), 125 | 'oam': Serializer.prefix(this.oam.combine()) 126 | }; 127 | } 128 | defrost(frost, decodeBase64) { 129 | this.palette.replace(frost.palette); 130 | this.memoryDirtied(this.palette, 0); 131 | this.vram.replace(frost.vram); 132 | for (var i = 0; i < this.vram.blocks.length; ++i) { 133 | this.memoryDirtied(this.vram, i); 134 | } 135 | this.oam.replace(frost.oam); 136 | this.memoryDirtied(this.oam, 0); 137 | } 138 | writeDisplayControl(value) { 139 | this.dirty = this.dirty || {}; 140 | this.dirty.DISPCNT = value; 141 | } 142 | writeBackgroundControl(bg, value) { 143 | this.dirty = this.dirty || {}; 144 | this.dirty.BGCNT = this.dirty.BGCNT || []; 145 | this.dirty.BGCNT[bg] = value; 146 | } 147 | writeBackgroundHOffset(bg, value) { 148 | this.dirty = this.dirty || {}; 149 | this.dirty.BGHOFS = this.dirty.BGHOFS || []; 150 | this.dirty.BGHOFS[bg] = value; 151 | } 152 | writeBackgroundVOffset(bg, value) { 153 | this.dirty = this.dirty || {}; 154 | this.dirty.BGVOFS = this.dirty.BGVOFS || []; 155 | this.dirty.BGVOFS[bg] = value; 156 | } 157 | writeBackgroundRefX(bg, value) { 158 | this.dirty = this.dirty || {}; 159 | this.dirty.BGX = this.dirty.BGX || []; 160 | this.dirty.BGX[bg] = value; 161 | } 162 | writeBackgroundRefY(bg, value) { 163 | this.dirty = this.dirty || {}; 164 | this.dirty.BGY = this.dirty.BGY || []; 165 | this.dirty.BGY[bg] = value; 166 | } 167 | writeBackgroundParamA(bg, value) { 168 | this.dirty = this.dirty || {}; 169 | this.dirty.BGPA = this.dirty.BGPA || []; 170 | this.dirty.BGPA[bg] = value; 171 | } 172 | writeBackgroundParamB(bg, value) { 173 | this.dirty = this.dirty || {}; 174 | this.dirty.BGPB = this.dirty.BGPB || []; 175 | this.dirty.BGPB[bg] = value; 176 | } 177 | writeBackgroundParamC(bg, value) { 178 | this.dirty = this.dirty || {}; 179 | this.dirty.BGPC = this.dirty.BGPC || []; 180 | this.dirty.BGPC[bg] = value; 181 | } 182 | writeBackgroundParamD(bg, value) { 183 | this.dirty = this.dirty || {}; 184 | this.dirty.BGPD = this.dirty.BGPD || []; 185 | this.dirty.BGPD[bg] = value; 186 | } 187 | writeWin0H(value) { 188 | this.dirty = this.dirty || {}; 189 | this.dirty.WIN0H = value; 190 | } 191 | writeWin1H(value) { 192 | this.dirty = this.dirty || {}; 193 | this.dirty.WIN1H = value; 194 | } 195 | writeWin0V(value) { 196 | this.dirty = this.dirty || {}; 197 | this.dirty.WIN0V = value; 198 | } 199 | writeWin1V(value) { 200 | this.dirty = this.dirty || {}; 201 | this.dirty.WIN1V = value; 202 | } 203 | writeWinIn(value) { 204 | this.dirty = this.dirty || {}; 205 | this.dirty.WININ = value; 206 | } 207 | writeWinOut(value) { 208 | this.dirty = this.dirty || {}; 209 | this.dirty.WINOUT = value; 210 | } 211 | writeBlendControl(value) { 212 | this.dirty = this.dirty || {}; 213 | this.dirty.BLDCNT = value; 214 | } 215 | writeBlendAlpha(value) { 216 | this.dirty = this.dirty || {}; 217 | this.dirty.BLDALPHA = value; 218 | } 219 | writeBlendY(value) { 220 | this.dirty = this.dirty || {}; 221 | this.dirty.BLDY = value; 222 | } 223 | writeMosaic(value) { 224 | this.dirty = this.dirty || {}; 225 | this.dirty.MOSAIC = value; 226 | } 227 | clearSubsets(mmu, regions) { 228 | this.dirty = this.dirty || {}; 229 | if (regions & 0x04) { 230 | this.palette = new MemoryProxy(this, mmu.SIZE_PALETTE_RAM, 0); 231 | mmu.mmap(mmu.REGION_PALETTE_RAM, this.palette); 232 | this.memoryDirtied(this.palette, 0); 233 | } 234 | if (regions & 0x08) { 235 | this.vram = new MemoryProxy(this, mmu.SIZE_VRAM, 13); 236 | mmu.mmap(mmu.REGION_VRAM, this.vram); 237 | for (var i = 0; i < this.vram.blocks.length; ++i) { 238 | this.memoryDirtied(this.vram, i); 239 | } 240 | } 241 | if (regions & 0x10) { 242 | this.oam = new MemoryProxy(this, mmu.SIZE_OAM, 0); 243 | mmu.mmap(mmu.REGION_OAM, this.oam); 244 | this.memoryDirtied(this.oam, 0); 245 | } 246 | } 247 | setBacking(backing) { 248 | this.backing = backing; 249 | this.worker.postMessage({ type: 'start', backing: this.backing }); 250 | } 251 | drawScanline(y) { 252 | if (!this.skipFrame) { 253 | if (this.dirty) { 254 | if (this.dirty.memory) { 255 | if (this.dirty.memory.palette) { 256 | this.dirty.memory.palette = this.dirty.memory.palette.slice(0); 257 | } 258 | if (this.dirty.memory.oam) { 259 | this.dirty.memory.oam = this.dirty.memory.oam.slice(0); 260 | } 261 | if (this.dirty.memory.vram) { 262 | for (var i = 0; i < 12; ++i) { 263 | if (this.dirty.memory.vram[i]) { 264 | this.dirty.memory.vram[i] = this.dirty.memory.vram[i].slice(0); 265 | } 266 | } 267 | } 268 | } 269 | this.scanlineQueue.push({ y: y, dirty: this.dirty }); 270 | this.dirty = null; 271 | } 272 | } 273 | } 274 | startDraw() { 275 | ++this.currentFrame; 276 | if (this.delay <= 0) { 277 | this.skipFrame = false; 278 | } 279 | if (!this.skipFrame) { 280 | ++this.delay; 281 | } 282 | } 283 | finishDraw(caller) { 284 | this.caller = caller; 285 | if (!this.skipFrame) { 286 | this.worker.postMessage({ type: 'finish', scanlines: this.scanlineQueue, frame: this.currentFrame }); 287 | this.scanlineQueue = []; 288 | if (this.delay > 2) { 289 | this.skipFrame = true; 290 | } 291 | } 292 | } 293 | }; 294 | -------------------------------------------------------------------------------- /js/gba.js: -------------------------------------------------------------------------------- 1 | class GameBoyAdvance { 2 | constructor() { 3 | this.LOG_ERROR = 1; 4 | this.LOG_WARN = 2; 5 | this.LOG_STUB = 4; 6 | this.LOG_INFO = 8; 7 | this.LOG_DEBUG = 16; 8 | 9 | this.SYS_ID = "com.endrift.gbajs"; 10 | 11 | this.logLevel = this.LOG_ERROR | this.LOG_WARN; 12 | 13 | this.rom = null; 14 | 15 | this.cpu = new ARMCore(); 16 | this.mmu = new GameBoyAdvanceMMU(); 17 | this.irq = new GameBoyAdvanceInterruptHandler(); 18 | this.io = new GameBoyAdvanceIO(); 19 | this.audio = new GameBoyAdvanceAudio(); 20 | this.video = new GameBoyAdvanceVideo(); 21 | this.keypad = new GameBoyAdvanceKeypad(); 22 | this.sio = new GameBoyAdvanceSIO(); 23 | 24 | // TODO: simplify this graph 25 | this.cpu.mmu = this.mmu; 26 | this.cpu.irq = this.irq; 27 | 28 | this.mmu.cpu = this.cpu; 29 | this.mmu.core = this; 30 | 31 | this.irq.cpu = this.cpu; 32 | this.irq.io = this.io; 33 | this.irq.audio = this.audio; 34 | this.irq.video = this.video; 35 | this.irq.core = this; 36 | 37 | this.io.cpu = this.cpu; 38 | this.io.audio = this.audio; 39 | this.io.video = this.video; 40 | this.io.keypad = this.keypad; 41 | this.io.sio = this.sio; 42 | this.io.core = this; 43 | 44 | this.audio.cpu = this.cpu; 45 | this.audio.core = this; 46 | 47 | this.video.cpu = this.cpu; 48 | this.video.core = this; 49 | 50 | this.keypad.core = this; 51 | 52 | this.sio.core = this; 53 | 54 | this.keypad.registerHandlers(); 55 | this.doStep = this.waitFrame; 56 | this.paused = false; 57 | 58 | this.seenFrame = false; 59 | this.seenSave = false; 60 | this.lastVblank = 0; 61 | 62 | this.queue = null; 63 | this.reportFPS = null; 64 | this.throttle = 16; // This is rough, but the 2/3ms difference gives us a good overhead 65 | 66 | var self = this; 67 | window.queueFrame = function (f) { 68 | self.queue = window.setTimeout(f, self.throttle); 69 | }; 70 | 71 | window.URL = window.URL || window.webkitURL; 72 | 73 | this.video.vblankCallback = function () { 74 | self.seenFrame = true; 75 | }; 76 | } 77 | setCanvas(canvas) { 78 | if (canvas.offsetWidth != 240 || canvas.offsetHeight != 160) { 79 | var self = this; 80 | this.indirectCanvas = document.createElement("canvas"); 81 | this.indirectCanvas.setAttribute("height", "160"); 82 | this.indirectCanvas.setAttribute("width", "240"); 83 | this.targetCanvas = canvas; 84 | this.setCanvasDirect(this.indirectCanvas); 85 | var targetContext = canvas.getContext("2d"); 86 | this.video.drawCallback = function () { 87 | targetContext.drawImage( 88 | self.indirectCanvas, 89 | 0, 90 | 0, 91 | canvas.width, 92 | canvas.height 93 | ); 94 | }; 95 | } else { 96 | this.setCanvasDirect(canvas); 97 | var self = this; 98 | } 99 | } 100 | setCanvasDirect(canvas) { 101 | this.context = canvas.getContext("2d"); 102 | this.video.setBacking(this.context); 103 | } 104 | setBios(bios, real) { 105 | this.mmu.loadBios(bios, real); 106 | } 107 | setRom(rom) { 108 | this.reset(); 109 | 110 | this.rom = this.mmu.loadRom(rom, true); 111 | if (!this.rom) { 112 | return false; 113 | } 114 | this.retrieveSavedata(); 115 | return true; 116 | } 117 | hasRom() { 118 | return !!this.rom; 119 | } 120 | loadRomFromFile(romFile, callback) { 121 | var reader = new FileReader(); 122 | var self = this; 123 | reader.onload = function (e) { 124 | var result = self.setRom(e.target.result); 125 | if (callback) { 126 | callback(result); 127 | } 128 | }; 129 | reader.readAsArrayBuffer(romFile); 130 | } 131 | reset() { 132 | this.audio.pause(true); 133 | 134 | this.mmu.clear(); 135 | this.io.clear(); 136 | this.audio.clear(); 137 | this.video.clear(); 138 | this.sio.clear(); 139 | 140 | this.mmu.mmap(this.mmu.REGION_IO, this.io); 141 | this.mmu.mmap( 142 | this.mmu.REGION_PALETTE_RAM, 143 | this.video.renderPath.palette 144 | ); 145 | this.mmu.mmap(this.mmu.REGION_VRAM, this.video.renderPath.vram); 146 | this.mmu.mmap(this.mmu.REGION_OAM, this.video.renderPath.oam); 147 | 148 | this.cpu.resetCPU(0); 149 | } 150 | step() { 151 | while (this.doStep()) { 152 | this.cpu.step(); 153 | } 154 | } 155 | waitFrame() { 156 | var seen = this.seenFrame; 157 | this.seenFrame = false; 158 | return !seen; 159 | } 160 | pause() { 161 | this.paused = true; 162 | this.audio.pause(true); 163 | if (this.queue) { 164 | clearTimeout(this.queue); 165 | this.queue = null; 166 | } 167 | } 168 | advanceFrame() { 169 | this.step(); 170 | if (this.seenSave) { 171 | if (!this.mmu.saveNeedsFlush()) { 172 | this.storeSavedata(); 173 | this.seenSave = false; 174 | } else { 175 | this.mmu.flushSave(); 176 | } 177 | } else if (this.mmu.saveNeedsFlush()) { 178 | this.seenSave = true; 179 | this.mmu.flushSave(); 180 | } 181 | } 182 | runStable() { 183 | if (this.interval) { 184 | return; // Already running 185 | } 186 | var self = this; 187 | var timer = 0; 188 | var frames = 0; 189 | var runFunc; 190 | var start = Date.now(); 191 | this.paused = false; 192 | this.audio.pause(false); 193 | 194 | if (this.reportFPS) { 195 | runFunc = function () { 196 | try { 197 | timer += Date.now() - start; 198 | if (self.paused) { 199 | return; 200 | } else { 201 | queueFrame(runFunc); 202 | } 203 | start = Date.now(); 204 | self.advanceFrame(); 205 | ++frames; 206 | if (frames == 60) { 207 | self.reportFPS((frames * 1000) / timer); 208 | frames = 0; 209 | timer = 0; 210 | } 211 | } catch (exception) { 212 | self.ERROR(exception); 213 | if (exception.stack) { 214 | self.logStackTrace(exception.stack.split("\n")); 215 | } 216 | throw exception; 217 | } 218 | }; 219 | } else { 220 | runFunc = function () { 221 | try { 222 | if (self.paused) { 223 | return; 224 | } else { 225 | queueFrame(runFunc); 226 | } 227 | self.advanceFrame(); 228 | } catch (exception) { 229 | self.ERROR(exception); 230 | if (exception.stack) { 231 | self.logStackTrace(exception.stack.split("\n")); 232 | } 233 | throw exception; 234 | } 235 | }; 236 | } 237 | queueFrame(runFunc); 238 | } 239 | setSavedata(data) { 240 | this.mmu.loadSavedata(data); 241 | } 242 | loadSavedataFromFile(saveFile) { 243 | var reader = new FileReader(); 244 | var self = this; 245 | reader.onload = function (e) { 246 | self.setSavedata(e.target.result); 247 | }; 248 | reader.readAsArrayBuffer(saveFile); 249 | } 250 | decodeSavedata(string) { 251 | this.setSavedata(this.decodeBase64(string)); 252 | } 253 | decodeBase64(string) { 254 | var length = (string.length * 3) / 4; 255 | if (string[string.length - 2] == "=") { 256 | length -= 2; 257 | } else if (string[string.length - 1] == "=") { 258 | length -= 1; 259 | } 260 | var buffer = new ArrayBuffer(length); 261 | var view = new Uint8Array(buffer); 262 | var bits = string.match(/..../g); 263 | for (var i = 0; i + 2 < length; i += 3) { 264 | var s = atob(bits.shift()); 265 | view[i] = s.charCodeAt(0); 266 | view[i + 1] = s.charCodeAt(1); 267 | view[i + 2] = s.charCodeAt(2); 268 | } 269 | if (i < length) { 270 | var s = atob(bits.shift()); 271 | view[i++] = s.charCodeAt(0); 272 | if (s.length > 1) { 273 | view[i++] = s.charCodeAt(1); 274 | } 275 | } 276 | 277 | return buffer; 278 | } 279 | encodeBase64(view) { 280 | var data = []; 281 | var b; 282 | var wordstring = []; 283 | var triplet; 284 | for (var i = 0; i < view.byteLength; ++i) { 285 | b = view.getUint8(i, true); 286 | wordstring.push(String.fromCharCode(b)); 287 | while (wordstring.length >= 3) { 288 | triplet = wordstring.splice(0, 3); 289 | data.push(btoa(triplet.join(""))); 290 | } 291 | } 292 | if (wordstring.length) { 293 | data.push(btoa(wordstring.join(""))); 294 | } 295 | return data.join(""); 296 | } 297 | downloadSavedata() { 298 | var sram = this.mmu.save; 299 | if (!sram) { 300 | this.WARN("No save data available"); 301 | return null; 302 | } 303 | if (window.URL) { 304 | var url = window.URL.createObjectURL( 305 | new Blob([sram.buffer], { type: "application/octet-stream" }) 306 | ); 307 | window.open(url); 308 | } else { 309 | var data = this.encodeBase64(sram.view); 310 | window.open( 311 | "data:application/octet-stream;base64," + data, 312 | this.rom.code + ".sav" 313 | ); 314 | } 315 | } 316 | storeSavedata() { 317 | var sram = this.mmu.save; 318 | try { 319 | var storage = window.localStorage; 320 | storage[this.SYS_ID + "." + this.mmu.cart.code] = this.encodeBase64( 321 | sram.view 322 | ); 323 | } catch (e) { 324 | this.WARN("Could not store savedata! " + e); 325 | } 326 | } 327 | retrieveSavedata() { 328 | try { 329 | var storage = window.localStorage; 330 | var data = storage[this.SYS_ID + "." + this.mmu.cart.code]; 331 | if (data) { 332 | this.decodeSavedata(data); 333 | return true; 334 | } 335 | } catch (e) { 336 | this.WARN("Could not retrieve savedata! " + e); 337 | } 338 | return false; 339 | } 340 | freeze() { 341 | return { 342 | cpu: this.cpu.freeze(), 343 | mmu: this.mmu.freeze(), 344 | irq: this.irq.freeze(), 345 | io: this.io.freeze(), 346 | audio: this.audio.freeze(), 347 | video: this.video.freeze() 348 | }; 349 | } 350 | defrost(frost) { 351 | this.cpu.defrost(frost.cpu); 352 | this.mmu.defrost(frost.mmu); 353 | this.audio.defrost(frost.audio); 354 | this.video.defrost(frost.video); 355 | this.irq.defrost(frost.irq); 356 | this.io.defrost(frost.io); 357 | } 358 | log(level, message) {} 359 | setLogger(logger) { 360 | this.log = logger; 361 | } 362 | logStackTrace(stack) { 363 | var overflow = stack.length - 32; 364 | this.ERROR("Stack trace follows:"); 365 | if (overflow > 0) { 366 | this.log(-1, "> (Too many frames)"); 367 | } 368 | for (var i = Math.max(overflow, 0); i < stack.length; ++i) { 369 | this.log(-1, "> " + stack[i]); 370 | } 371 | } 372 | ERROR(error) { 373 | if (this.logLevel & this.LOG_ERROR) { 374 | this.log(this.LOG_ERROR, error); 375 | } 376 | } 377 | WARN(warn) { 378 | if (this.logLevel & this.LOG_WARN) { 379 | this.log(this.LOG_WARN, warn); 380 | } 381 | } 382 | STUB(func) { 383 | if (this.logLevel & this.LOG_STUB) { 384 | this.log(this.LOG_STUB, func); 385 | } 386 | } 387 | INFO(info) { 388 | if (this.logLevel & this.LOG_INFO) { 389 | this.log(this.LOG_INFO, info); 390 | } 391 | } 392 | DEBUG(info) { 393 | if (this.logLevel & this.LOG_DEBUG) { 394 | this.log(this.LOG_DEBUG, info); 395 | } 396 | } 397 | ASSERT_UNREACHED(err) { 398 | throw new Error("Should be unreached: " + err); 399 | } 400 | ASSERT(test, err) { 401 | if (!test) { 402 | throw new Error("Assertion failed: " + err); 403 | } 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /resources/console.js: -------------------------------------------------------------------------------- 1 | function Console(gba) { 2 | this.cpu = gba.cpu; 3 | this.gba = gba; 4 | this.ul = document.getElementById('console'); 5 | this.gprs = document.getElementById('gprs'); 6 | this.memory = new Memory(gba.mmu); 7 | this.breakpoints = []; 8 | this.logQueue = []; 9 | 10 | this.activeView = null; 11 | this.paletteView = new PaletteViewer(gba.video.renderPath.palette); 12 | this.tileView = new TileViewer(gba.video.renderPath.vram, gba.video.renderPath.palette); 13 | this.update(); 14 | 15 | var self = this; 16 | gba.setLogger(function (level, message) { self.log(level, message) }); 17 | this.gba.doStep = function () { return self.testBreakpoints() }; 18 | } 19 | 20 | Console.prototype.updateGPRs = function() { 21 | for (var i = 0; i < 16; ++i) { 22 | this.gprs.children[i].textContent = hex(this.cpu.gprs[i]); 23 | } 24 | } 25 | 26 | Console.prototype.updateCPSR = function() { 27 | var cpu = this.cpu; 28 | var bit = function(psr, member) { 29 | var element = document.getElementById(psr); 30 | if (cpu[member]) { 31 | element.removeAttribute('class'); 32 | } else { 33 | element.setAttribute('class', 'disabled'); 34 | } 35 | } 36 | bit('cpsrN', 'cpsrN'); 37 | bit('cpsrZ', 'cpsrZ'); 38 | bit('cpsrC', 'cpsrC'); 39 | bit('cpsrV', 'cpsrV'); 40 | bit('cpsrI', 'cpsrI'); 41 | bit('cpsrT', 'execMode'); 42 | 43 | var mode = document.getElementById('mode'); 44 | switch (cpu.mode) { 45 | case cpu.MODE_USER: 46 | mode.textContent = 'USER'; 47 | break; 48 | case cpu.MODE_IRQ: 49 | mode.textContent = 'IRQ'; 50 | break; 51 | case cpu.MODE_FIQ: 52 | mode.textContent = 'FIQ'; 53 | break; 54 | case cpu.MODE_SUPERVISOR: 55 | mode.textContent = 'SVC'; 56 | break; 57 | case cpu.MODE_ABORT: 58 | mode.textContent = 'ABORT'; 59 | break; 60 | case cpu.MODE_UNDEFINED: 61 | mode.textContent = 'UNDEFINED'; 62 | break; 63 | case cpu.MODE_SYSTEM: 64 | mode.textContent = 'SYSTEM'; 65 | break; 66 | default: 67 | mode.textContent = '???'; 68 | break; 69 | } 70 | } 71 | 72 | Console.prototype.log = function(level, message) { 73 | switch (level) { 74 | case this.gba.LOG_ERROR: 75 | message = '[ERROR] ' + message; 76 | break; 77 | case this.gba.LOG_WARN: 78 | message = '[WARN] ' + message; 79 | break; 80 | case this.gba.LOG_STUB: 81 | message = '[STUB] ' + message; 82 | break; 83 | case this.gba.LOG_INFO: 84 | message = '[INFO] ' + message; 85 | break; 86 | case this.gba.LOG_DEBUG: 87 | message = '[DEBUG] ' + message; 88 | break; 89 | } 90 | this.logQueue.push(message); 91 | if (level == this.gba.LOG_ERROR) { 92 | this.pause(); 93 | } 94 | if (!this.stillRunning) { 95 | this.flushLog(); 96 | } 97 | } 98 | 99 | Console.prototype.flushLog = function() { 100 | var doScroll = this.ul.scrollTop == this.ul.scrollHeight - this.ul.offsetHeight; 101 | while (this.logQueue.length) { 102 | var entry = document.createElement('li'); 103 | entry.textContent = this.logQueue.shift(); 104 | this.ul.appendChild(entry); 105 | } 106 | if (doScroll) { 107 | var ul = this.ul; 108 | var last = ul.scrollTop; 109 | var scrollUp = function() { 110 | if (ul.scrollTop == last) { 111 | ul.scrollTop = (ul.scrollHeight - ul.offsetHeight) * 0.2 + last * 0.8; 112 | last = ul.scrollTop; 113 | if (last != ul.scrollHeight - ul.offsetHeight) { 114 | setTimeout(scrollUp, 25); 115 | } 116 | } 117 | } 118 | setTimeout(scrollUp, 25); 119 | } 120 | 121 | } 122 | 123 | Console.prototype.update = function() { 124 | this.updateGPRs(); 125 | this.updateCPSR(); 126 | this.memory.refreshAll(); 127 | if (this.activeView) { 128 | this.activeView.redraw(); 129 | } 130 | } 131 | 132 | Console.prototype.setView = function(view) { 133 | var container = document.getElementById('debugViewer'); 134 | while (container.hasChildNodes()) { 135 | container.removeChild(container.lastChild); 136 | } 137 | if (view) { 138 | view.insertChildren(container); 139 | view.redraw(); 140 | } 141 | this.activeView = view; 142 | } 143 | 144 | Console.prototype.step = function() { 145 | try { 146 | this.cpu.step(); 147 | this.update(); 148 | } catch (exception) { 149 | this.log(this.gba.LOG_DEBUG, exception); 150 | throw exception; 151 | } 152 | } 153 | 154 | Console.prototype.runVisible = function() { 155 | if (this.stillRunning) { 156 | return; 157 | } 158 | 159 | this.stillRunning = true; 160 | var self = this; 161 | run = function() { 162 | if (self.stillRunning) { 163 | try { 164 | self.step(); 165 | if (self.breakpoints.length && self.breakpoints[self.cpu.gprs[self.cpu.PC]]) { 166 | self.breakpointHit(); 167 | return; 168 | } 169 | self.flushLog(); 170 | setTimeout(run, 0); 171 | } catch (exception) { 172 | self.log(this.gba.LOG_DEBUG, exception); 173 | self.pause(); 174 | throw exception; 175 | } 176 | } 177 | } 178 | setTimeout(run, 0); 179 | } 180 | 181 | Console.prototype.run = function() { 182 | if (this.stillRunning) { 183 | return; 184 | } 185 | 186 | this.stillRunning = true; 187 | var regs = document.getElementById('registers'); 188 | var mem = document.getElementById('memory'); 189 | var start = Date.now(); 190 | regs.setAttribute('class', 'disabled'); 191 | mem.setAttribute('class', 'disabled'); 192 | var self = this; 193 | this.gba.runStable(); 194 | } 195 | 196 | Console.prototype.runFrame = function() { 197 | if (this.stillRunning) { 198 | return; 199 | } 200 | 201 | this.stillRunning = true; 202 | var regs = document.getElementById('registers'); 203 | var mem = document.getElementById('memory'); 204 | var start = Date.now(); 205 | regs.setAttribute('class', 'disabled'); 206 | mem.setAttribute('class', 'disabled'); 207 | var self = this; 208 | run = function() { 209 | self.gba.step(); 210 | self.pause(); 211 | } 212 | setTimeout(run, 0); 213 | } 214 | 215 | Console.prototype.pause = function() { 216 | this.stillRunning = false; 217 | this.gba.pause(); 218 | var regs = document.getElementById('registers'); 219 | var mem = document.getElementById('memory'); 220 | mem.removeAttribute('class'); 221 | regs.removeAttribute('class'); 222 | this.update(); 223 | this.flushLog(); 224 | } 225 | 226 | Console.prototype.breakpointHit = function() { 227 | this.pause(); 228 | this.log(this.gba.LOG_DEBUG, 'Hit breakpoint at ' + hex(this.cpu.gprs[this.cpu.PC])); 229 | } 230 | 231 | Console.prototype.addBreakpoint = function(addr) { 232 | this.breakpoints[addr] = true; 233 | var bpLi = document.getElementById('bp' + addr); 234 | if (!bpLi) { 235 | bpLi = document.createElement('li'); 236 | bpLi.address = addr; 237 | var cb = document.createElement('input'); 238 | cb.setAttribute('type', 'checkbox'); 239 | cb.setAttribute('checked', 'checked'); 240 | var self = this; 241 | cb.addEventListener('click', function() { 242 | self.breakpoints[addr] = cb.checked; 243 | }, false); 244 | bpLi.appendChild(cb); 245 | bpLi.appendChild(document.createTextNode(hex(addr))); 246 | document.getElementById('breakpointView').appendChild(bpLi); 247 | } 248 | } 249 | 250 | Console.prototype.testBreakpoints = function() { 251 | if (this.breakpoints.length && this.breakpoints[this.cpu.gprs[this.cpu.PC]]) { 252 | this.breakpointHit(); 253 | return false; 254 | } 255 | return this.gba.waitFrame(); 256 | }; 257 | 258 | Memory = function(mmu) { 259 | this.mmu = mmu; 260 | this.ul = document.getElementById('memoryView'); 261 | row = this.createRow(0); 262 | this.ul.appendChild(row); 263 | this.rowHeight = row.offsetHeight; 264 | this.numberRows = this.ul.parentNode.offsetHeight / this.rowHeight + 2; 265 | this.ul.removeChild(row); 266 | this.scrollTop = 50 - this.ul.parentElement.firstElementChild.offsetHeight; 267 | 268 | for (var i = 0; i < this.numberRows; ++i) { 269 | this.ul.appendChild(this.createRow(i << 4)); 270 | } 271 | this.ul.parentElement.scrollTop = this.scrollTop; 272 | 273 | var self = this; 274 | this.ul.parentElement.addEventListener('scroll', function(e) { self.scroll(e) }, true); 275 | window.addEventListener('resize', function(e) { self.resize() }, true); 276 | } 277 | 278 | Memory.prototype.scroll = function(e) { 279 | while (this.ul.parentElement.scrollTop - this.scrollTop < this.rowHeight) { 280 | if (this.ul.firstChild.offset == 0) { 281 | break; 282 | } 283 | var victim = this.ul.lastChild; 284 | this.ul.removeChild(victim); 285 | victim.offset = this.ul.firstChild.offset - 16; 286 | this.refresh(victim); 287 | this.ul.insertBefore(victim, this.ul.firstChild); 288 | this.ul.parentElement.scrollTop += this.rowHeight; 289 | } 290 | while (this.ul.parentElement.scrollTop - this.scrollTop > this.rowHeight * 2) { 291 | var victim = this.ul.firstChild; 292 | this.ul.removeChild(victim); 293 | victim.offset = this.ul.lastChild.offset + 16; 294 | this.refresh(victim); 295 | this.ul.appendChild(victim); 296 | this.ul.parentElement.scrollTop -= this.rowHeight; 297 | } 298 | if (this.ul.parentElement.scrollTop < this.scrollTop) { 299 | this.ul.parentElement.scrollTop = this.scrollTop; 300 | e.preventDefault(); 301 | } 302 | } 303 | 304 | Memory.prototype.resize = function() { 305 | this.numberRows = this.ul.parentNode.offsetHeight / this.rowHeight + 2; 306 | if (this.numberRows > this.ul.children.length) { 307 | var offset = this.ul.lastChild.offset + 16; 308 | for (var i = 0; i < this.numberRows - this.ul.children.length; ++i) { 309 | var row = this.createRow(offset); 310 | this.refresh(row); 311 | this.ul.appendChild(row); 312 | offset += 16; 313 | } 314 | } else { 315 | for (var i = 0; i < this.ul.children.length - this.numberRows; ++i) { 316 | this.ul.removeChild(this.ul.lastChild); 317 | } 318 | } 319 | } 320 | 321 | Memory.prototype.refresh = function(row) { 322 | var showChanged; 323 | var newValue; 324 | var child; 325 | row.firstChild.textContent = hex(row.offset); 326 | if (row.oldOffset == row.offset) { 327 | showChanged = true; 328 | } else { 329 | row.oldOffset = row.offset; 330 | showChanged = false; 331 | } 332 | for (var i = 0; i < 16; ++i) { 333 | child = row.children[i + 1]; 334 | try { 335 | newValue = this.mmu.loadU8(row.offset + i); 336 | if (newValue >= 0) { 337 | newValue = hex(newValue, 2, false); 338 | if (child.textContent == newValue) { 339 | child.setAttribute('class', 'memoryCell'); 340 | } else if (showChanged) { 341 | child.setAttribute('class', 'memoryCell changed'); 342 | child.textContent = newValue; 343 | } else { 344 | child.setAttribute('class', 'memoryCell'); 345 | child.textContent = newValue; 346 | } 347 | } else { 348 | child.setAttribute('class', 'memoryCell'); 349 | child.textContent = '--'; 350 | } 351 | } catch (exception) { 352 | child.setAttribute('class', 'memoryCell'); 353 | child.textContent = '--'; 354 | } 355 | } 356 | } 357 | 358 | Memory.prototype.refreshAll = function() { 359 | for (var i = 0; i < this.ul.children.length; ++i) { 360 | this.refresh(this.ul.children[i]); 361 | } 362 | } 363 | 364 | Memory.prototype.createRow = function(startOffset) { 365 | var li = document.createElement('li'); 366 | var offset = document.createElement('span'); 367 | offset.setAttribute('class', 'memoryOffset'); 368 | offset.textContent = hex(startOffset); 369 | li.appendChild(offset); 370 | 371 | for (var i = 0; i < 16; ++i) { 372 | var b = document.createElement('span'); 373 | b.textContent = '00'; 374 | b.setAttribute('class', 'memoryCell'); 375 | li.appendChild(b); 376 | } 377 | li.offset = startOffset; 378 | li.oldOffset = startOffset; 379 | return li; 380 | } 381 | 382 | Memory.prototype.scrollTo = function(offset) { 383 | offset &= 0xFFFFFFF0; 384 | if (offset) { 385 | for (var i = 0; i < this.ul.children.length; ++i) { 386 | var child = this.ul.children[i]; 387 | child.offset = offset + (i - 1) * 16; 388 | this.refresh(child); 389 | } 390 | this.ul.parentElement.scrollTop = this.scrollTop + this.rowHeight; 391 | } else { 392 | for (var i = 0; i < this.ul.children.length; ++i) { 393 | var child = this.ul.children[i]; 394 | child.offset = offset + i * 16; 395 | this.refresh(child); 396 | } 397 | this.ul.parentElement.scrollTop = this.scrollTop; 398 | } 399 | } 400 | 401 | function PaletteViewer(palette) { 402 | this.palette = palette; 403 | this.view = document.createElement('canvas'); 404 | this.view.setAttribute('class', 'paletteView'); 405 | this.view.setAttribute('width', '240'); 406 | this.view.setAttribute('height', '500'); 407 | } 408 | 409 | PaletteViewer.prototype.insertChildren = function(container) { 410 | container.appendChild(this.view); 411 | } 412 | 413 | PaletteViewer.prototype.redraw = function() { 414 | var context = this.view.getContext('2d'); 415 | context.clearRect(0, 0, this.view.width, this.view.height); 416 | for (var p = 0; p < 2; ++p) { 417 | for (var y = 0; y < 16; ++y) { 418 | for (var x = 0; x < 16; ++x) { 419 | var color = this.palette.loadU16((p * 256 + y * 16 + x) * 2); 420 | var r = (color & 0x001F) << 3; 421 | var g = (color & 0x03E0) >> 2; 422 | var b = (color & 0x7C00) >> 7; 423 | context.fillStyle = '#' + hex(r, 2, false) + hex(g, 2, false) + hex(b, 2, false); 424 | context.fillRect(x * 15 + 1, y * 15 + p * 255 + 1, 13, 13); 425 | } 426 | } 427 | } 428 | } 429 | 430 | function TileViewer(vram, palette) { 431 | this.BG_MAP_WIDTH = 256; 432 | this.vram = vram; 433 | this.palette = palette; 434 | 435 | this.view = document.createElement('canvas'); 436 | this.view.setAttribute('class', 'tileView'); 437 | this.view.setAttribute('width', '256'); 438 | this.view.setAttribute('height', '512'); 439 | 440 | this.activePalette = 0; 441 | } 442 | 443 | TileViewer.prototype.insertChildren = function(container) { 444 | container.appendChild(this.view); 445 | }; 446 | 447 | TileViewer.prototype.redraw = function() { 448 | var context = this.view.getContext('2d'); 449 | var data = context.createImageData(this.BG_MAP_WIDTH, 512); 450 | var t = 0; 451 | for (var y = 0; y < 512; y += 8) { 452 | for (var x = 0; x < this.BG_MAP_WIDTH; x += 8) { 453 | this.drawTile(data.data, t, this.activePalette, x + y * this.BG_MAP_WIDTH, this.BG_MAP_WIDTH); 454 | ++t; 455 | } 456 | } 457 | context.putImageData(data, 0, 0); 458 | }; 459 | 460 | TileViewer.prototype.drawTile = function(data, tile, palette, offset, stride) { 461 | for (var j = 0; j < 8; ++j) { 462 | var memOffset = tile << 5; 463 | memOffset |= j << 2; 464 | 465 | var row = this.vram.load32(memOffset); 466 | for (var i = 0; i < 8; ++i) { 467 | var index = (row >> (i << 2)) & 0xF; 468 | var color = this.palette.loadU16((index << 1) + (palette << 5)); 469 | var r = (color & 0x001F) << 3; 470 | var g = (color & 0x03E0) >> 2; 471 | var b = (color & 0x7C00) >> 7; 472 | data[(offset + i + stride * j) * 4 + 0] = r; 473 | data[(offset + i + stride * j) * 4 + 1] = g; 474 | data[(offset + i + stride * j) * 4 + 2] = b; 475 | data[(offset + i + stride * j) * 4 + 3] = 255; 476 | } 477 | } 478 | }; 479 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gbajs2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 351 | 352 | 353 | 354 |
          355 |

          The Game Boy Advance's buttons are mapped as follows:

          356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 |
          UpUp
          DownDown
          LeftLeft
          RightRight
          AZ
          BX
          LA
          RS
          StartEnter
          Select\
          398 |
          399 |
          400 |
          401 | 408 | 414 | 417 | 422 |
          423 | 460 |
          461 | 475 | 478 | 479 | 483 | 484 | -------------------------------------------------------------------------------- /js/audio.js: -------------------------------------------------------------------------------- 1 | class GameBoyAdvanceAudio { 2 | constructor() { 3 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 4 | if (window.AudioContext) { 5 | var self = this; 6 | this.context = new AudioContext(); 7 | this.bufferSize = 0; 8 | this.bufferSize = 4096; 9 | this.maxSamples = this.bufferSize << 2; 10 | this.buffers = [ 11 | new Float32Array(this.maxSamples), 12 | new Float32Array(this.maxSamples) 13 | ]; 14 | this.sampleMask = this.maxSamples - 1; 15 | if (this.context.createScriptProcessor) { 16 | this.jsAudio = this.context.createScriptProcessor( 17 | this.bufferSize 18 | ); 19 | } else { 20 | this.jsAudio = this.context.createJavaScriptNode( 21 | this.bufferSize 22 | ); 23 | } 24 | this.jsAudio.onaudioprocess = function (e) { 25 | self.audioProcess(e); 26 | }; 27 | } else { 28 | this.context = null; 29 | } 30 | 31 | this.masterEnable = true; 32 | this.masterVolume = 1.0; 33 | 34 | this.SOUND_MAX = 0x400; 35 | this.FIFO_MAX = 0x200; 36 | this.PSG_MAX = 0x080; 37 | } 38 | clear() { 39 | this.fifoA = []; 40 | this.fifoB = []; 41 | this.fifoASample = 0; 42 | this.fifoBSample = 0; 43 | 44 | this.enabled = false; 45 | if (this.context) { 46 | try { 47 | this.jsAudio.disconnect(this.context.destination); 48 | } catch (e) {} 49 | } 50 | 51 | this.enableChannel3 = false; 52 | this.enableChannel4 = false; 53 | this.enableChannelA = false; 54 | this.enableChannelB = false; 55 | this.enableRightChannelA = false; 56 | this.enableLeftChannelA = false; 57 | this.enableRightChannelB = false; 58 | this.enableLeftChannelB = false; 59 | 60 | this.playingChannel3 = false; 61 | this.playingChannel4 = false; 62 | 63 | this.volumeLeft = 0; 64 | this.volumeRight = 0; 65 | this.ratioChannelA = 1; 66 | this.ratioChannelB = 1; 67 | this.enabledLeft = 0; 68 | this.enabledRight = 0; 69 | 70 | this.dmaA = -1; 71 | this.dmaB = -1; 72 | this.soundTimerA = 0; 73 | this.soundTimerB = 0; 74 | 75 | this.soundRatio = 1; 76 | this.soundBias = 0x200; 77 | 78 | this.squareChannels = new Array(); 79 | for (var i = 0; i < 2; ++i) { 80 | this.squareChannels[i] = { 81 | enabled: false, 82 | playing: false, 83 | sample: 0, 84 | duty: 0.5, 85 | increment: 0, 86 | step: 0, 87 | initialVolume: 0, 88 | volume: 0, 89 | frequency: 0, 90 | interval: 0, 91 | sweepSteps: 0, 92 | sweepIncrement: 0, 93 | sweepInterval: 0, 94 | doSweep: false, 95 | raise: 0, 96 | lower: 0, 97 | nextStep: 0, 98 | timed: false, 99 | length: 0, 100 | end: 0 101 | }; 102 | } 103 | 104 | this.waveData = new Uint8Array(32); 105 | this.channel3Dimension = 0; 106 | this.channel3Bank = 0; 107 | this.channel3Volume = 0; 108 | this.channel3Interval = 0; 109 | this.channel3Next = 0; 110 | this.channel3Length = 0; 111 | this.channel3Timed = false; 112 | this.channel3End = 0; 113 | this.channel3Pointer = 0; 114 | this.channel3Sample = 0; 115 | 116 | this.cpuFrequency = this.core.irq.FREQUENCY; 117 | 118 | this.channel4 = { 119 | sample: 0, 120 | lfsr: 0, 121 | width: 15, 122 | interval: this.cpuFrequency / 524288, 123 | increment: 0, 124 | step: 0, 125 | initialVolume: 0, 126 | volume: 0, 127 | nextStep: 0, 128 | timed: false, 129 | length: 0, 130 | end: 0 131 | }; 132 | 133 | this.nextEvent = 0; 134 | 135 | this.nextSample = 0; 136 | this.outputPointer = 0; 137 | this.samplePointer = 0; 138 | 139 | this.backup = 0; 140 | this.totalSamples = 0; 141 | 142 | this.sampleRate = 32768; 143 | this.sampleInterval = this.cpuFrequency / this.sampleRate; 144 | this.resampleRatio = 1; 145 | if (this.context) { 146 | this.resampleRatio = this.sampleRate / this.context.sampleRate; 147 | } 148 | 149 | this.writeSquareChannelFC(0, 0); 150 | this.writeSquareChannelFC(1, 0); 151 | this.writeChannel4FC(0); 152 | } 153 | freeze() { 154 | return { 155 | nextSample: this.nextSample 156 | }; 157 | } 158 | defrost(frost) { 159 | this.nextSample = frost.nextSample; 160 | } 161 | pause(paused) { 162 | if (this.context) { 163 | if (paused) { 164 | try { 165 | this.jsAudio.disconnect(this.context.destination); 166 | } catch (e) { 167 | // Sigh 168 | } 169 | } else if (this.enabled) { 170 | this.jsAudio.connect(this.context.destination); 171 | } 172 | } 173 | } 174 | updateTimers() { 175 | var cycles = this.cpu.cycles; 176 | if ( 177 | !this.enabled || 178 | (cycles < this.nextEvent && cycles < this.nextSample) 179 | ) { 180 | return; 181 | } 182 | 183 | if (cycles >= this.nextEvent) { 184 | var channel = this.squareChannels[0]; 185 | this.nextEvent = Infinity; 186 | if (channel.playing) { 187 | this.updateSquareChannel(channel, cycles); 188 | } 189 | 190 | channel = this.squareChannels[1]; 191 | if (channel.playing) { 192 | this.updateSquareChannel(channel, cycles); 193 | } 194 | 195 | if (this.enableChannel3 && this.playingChannel3) { 196 | if (cycles >= this.channel3Next) { 197 | if (this.channel3Write) { 198 | var sample = this.waveData[this.channel3Pointer >> 1]; 199 | this.channel3Sample = (((sample >> ((this.channel3Pointer & 1) << 2)) & 0xF) - 0x8) / 8; 200 | this.channel3Pointer = (this.channel3Pointer + 1); 201 | if ( 202 | this.channel3Dimension && 203 | this.channel3Pointer >= 64 204 | ) { 205 | this.channel3Pointer -= 64; 206 | } else if ( 207 | !this.channel3Bank && 208 | this.channel3Pointer >= 32 209 | ) { 210 | this.channel3Pointer -= 32; 211 | } else if (this.channel3Pointer >= 64) { 212 | this.channel3Pointer -= 32; 213 | } 214 | } 215 | this.channel3Next += this.channel3Interval; 216 | if ( 217 | this.channel3Interval && 218 | this.nextEvent > this.channel3Next 219 | ) { 220 | this.nextEvent = this.channel3Next; 221 | } 222 | } 223 | if (this.channel3Timed && cycles >= this.channel3End) { 224 | this.playingChannel3 = false; 225 | } 226 | } 227 | 228 | if (this.enableChannel4 && this.playingChannel4) { 229 | if (this.channel4.timed && cycles >= this.channel4.end) { 230 | this.playingChannel4 = false; 231 | } else { 232 | if (cycles >= this.channel4.next) { 233 | this.channel4.lfsr >>= 1; 234 | var sample = this.channel4.lfsr & 1; 235 | this.channel4.lfsr |= 236 | (((this.channel4.lfsr >> 1) & 1) ^ sample) << 237 | (this.channel4.width - 1); 238 | this.channel4.next += this.channel4.interval; 239 | this.channel4.sample = 240 | (sample - 0.5) * 2 * this.channel4.volume; 241 | } 242 | this.updateEnvelope(this.channel4, cycles); 243 | if (this.nextEvent > this.channel4.next) { 244 | this.nextEvent = this.channel4.next; 245 | } 246 | if ( 247 | this.channel4.timed && 248 | this.nextEvent > this.channel4.end 249 | ) { 250 | this.nextEvent = this.channel4.end; 251 | } 252 | } 253 | } 254 | } 255 | 256 | if (cycles >= this.nextSample) { 257 | this.sample(); 258 | this.nextSample += this.sampleInterval; 259 | } 260 | 261 | this.nextEvent = Math.ceil(this.nextEvent); 262 | if (this.nextEvent < cycles || this.nextSample < cycles) { 263 | // STM instructions may take a long time 264 | this.updateTimers(); 265 | } 266 | } 267 | writeEnable(value) { 268 | this.enabled = !!value; 269 | this.nextEvent = this.cpu.cycles; 270 | this.nextSample = this.nextEvent; 271 | this.updateTimers(); 272 | this.core.irq.pollNextEvent(); 273 | if (this.context) { 274 | if (value) { 275 | this.jsAudio.connect(this.context.destination); 276 | } else { 277 | try { 278 | this.jsAudio.disconnect(this.context.destination); 279 | } catch (e) {} 280 | } 281 | } 282 | } 283 | writeSoundControlLo(value) { 284 | this.masterVolumeLeft = value & 0x7; 285 | this.masterVolumeRight = (value >> 4) & 0x7; 286 | this.enabledLeft = (value >> 8) & 0xf; 287 | this.enabledRight = (value >> 12) & 0xf; 288 | 289 | this.setSquareChannelEnabled( 290 | this.squareChannels[0], 291 | (this.enabledLeft | this.enabledRight) & 0x1 292 | ); 293 | this.setSquareChannelEnabled( 294 | this.squareChannels[1], 295 | (this.enabledLeft | this.enabledRight) & 0x2 296 | ); 297 | this.enableChannel3 = (this.enabledLeft | this.enabledRight) & 0x4; 298 | this.setChannel4Enabled((this.enabledLeft | this.enabledRight) & 0x8); 299 | 300 | this.updateTimers(); 301 | this.core.irq.pollNextEvent(); 302 | } 303 | writeSoundControlHi(value) { 304 | switch (value & 0x0003) { 305 | case 0: 306 | this.soundRatio = 0.25; 307 | break; 308 | case 1: 309 | this.soundRatio = 0.50; 310 | break; 311 | case 2: 312 | this.soundRatio = 1; 313 | break; 314 | } 315 | this.ratioChannelA = (((value & 0x0004) >> 2) + 1) * 0.5; 316 | this.ratioChannelB = (((value & 0x0008) >> 3) + 1) * 0.5; 317 | 318 | this.enableRightChannelA = value & 0x0100; 319 | this.enableLeftChannelA = value & 0x0200; 320 | this.enableChannelA = value & 0x0300; 321 | this.soundTimerA = value & 0x0400; 322 | if (value & 0x0800) { 323 | this.fifoA = []; 324 | } 325 | this.enableRightChannelB = value & 0x1000; 326 | this.enableLeftChannelB = value & 0x2000; 327 | this.enableChannelB = value & 0x3000; 328 | this.soundTimerB = value & 0x4000; 329 | if (value & 0x8000) { 330 | this.fifoB = []; 331 | } 332 | } 333 | resetSquareChannel(channel) { 334 | if (channel.step) { 335 | channel.nextStep = this.cpu.cycles + channel.step; 336 | } 337 | if (channel.enabled && !channel.playing) { 338 | channel.raise = this.cpu.cycles; 339 | channel.lower = channel.raise + channel.duty * channel.interval; 340 | channel.end = this.cpu.cycles + channel.length; 341 | this.nextEvent = this.cpu.cycles; 342 | } 343 | channel.playing = channel.enabled; 344 | this.updateTimers(); 345 | this.core.irq.pollNextEvent(); 346 | } 347 | setSquareChannelEnabled(channel, enable) { 348 | if (!(channel.enabled && channel.playing) && enable) { 349 | channel.enabled = !!enable; 350 | this.updateTimers(); 351 | this.core.irq.pollNextEvent(); 352 | } else { 353 | channel.enabled = !!enable; 354 | } 355 | } 356 | writeSquareChannelSweep(channelId, value) { 357 | var channel = this.squareChannels[channelId]; 358 | channel.sweepSteps = value & 0x07; 359 | channel.sweepIncrement = (value & 0x08) ? -1 : 1; 360 | channel.sweepInterval = ((value >> 4) & 0x7) * this.cpuFrequency / 128; 361 | channel.doSweep = !!channel.sweepInterval; 362 | channel.nextSweep = this.cpu.cycles + channel.sweepInterval; 363 | this.resetSquareChannel(channel); 364 | } 365 | writeSquareChannelDLE(channelId, value) { 366 | var channel = this.squareChannels[channelId]; 367 | var duty = (value >> 6) & 0x3; 368 | switch (duty) { 369 | case 0: 370 | channel.duty = 0.125; 371 | break; 372 | case 1: 373 | channel.duty = 0.25; 374 | break; 375 | case 2: 376 | channel.duty = 0.5; 377 | break; 378 | case 3: 379 | channel.duty = 0.75; 380 | break; 381 | } 382 | this.writeChannelLE(channel, value); 383 | this.resetSquareChannel(channel); 384 | } 385 | writeSquareChannelFC(channelId, value) { 386 | var channel = this.squareChannels[channelId]; 387 | var frequency = value & 2047; 388 | channel.frequency = frequency; 389 | channel.interval = (this.cpuFrequency * (2048 - frequency)) / 131072; 390 | channel.timed = !!(value & 0x4000); 391 | 392 | if (value & 0x8000) { 393 | this.resetSquareChannel(channel); 394 | channel.volume = channel.initialVolume; 395 | } 396 | } 397 | updateSquareChannel(channel, cycles) { 398 | if (channel.timed && cycles >= channel.end) { 399 | channel.playing = false; 400 | return; 401 | } 402 | 403 | if (channel.doSweep && cycles >= channel.nextSweep) { 404 | channel.frequency += 405 | channel.sweepIncrement * 406 | (channel.frequency >> channel.sweepSteps); 407 | if (channel.frequency < 0) { 408 | channel.frequency = 0; 409 | } else if (channel.frequency > 2047) { 410 | channel.frequency = 2047; 411 | channel.playing = false; 412 | return; 413 | } 414 | channel.interval = 415 | (this.cpuFrequency * (2048 - channel.frequency)) / 131072; 416 | channel.nextSweep += channel.sweepInterval; 417 | } 418 | 419 | if (cycles >= channel.raise) { 420 | channel.sample = channel.volume; 421 | channel.lower = channel.raise + channel.duty * channel.interval; 422 | channel.raise += channel.interval; 423 | } else if (cycles >= channel.lower) { 424 | channel.sample = -channel.volume; 425 | channel.lower += channel.interval; 426 | } 427 | 428 | this.updateEnvelope(channel, cycles); 429 | 430 | if (this.nextEvent > channel.raise) { 431 | this.nextEvent = channel.raise; 432 | } 433 | if (this.nextEvent > channel.lower) { 434 | this.nextEvent = channel.lower; 435 | } 436 | if (channel.timed && this.nextEvent > channel.end) { 437 | this.nextEvent = channel.end; 438 | } 439 | if (channel.doSweep && this.nextEvent > channel.nextSweep) { 440 | this.nextEvent = channel.nextSweep; 441 | } 442 | } 443 | writeChannel3Lo(value) { 444 | this.channel3Dimension = value & 0x20; 445 | this.channel3Bank = value & 0x40; 446 | var enable = value & 0x80; 447 | if (!this.channel3Write && enable) { 448 | this.channel3Write = enable; 449 | this.resetChannel3(); 450 | } else { 451 | this.channel3Write = enable; 452 | } 453 | } 454 | writeChannel3Hi(value) { 455 | this.channel3Length = 456 | (this.cpuFrequency * (0x100 - (value & 0xff))) / 256; 457 | var volume = (value >> 13) & 0x7; 458 | switch (volume) { 459 | case 0: 460 | this.channel3Volume = 0; 461 | break; 462 | case 1: 463 | this.channel3Volume = 1; 464 | break; 465 | case 2: 466 | this.channel3Volume = 0.5; 467 | break; 468 | case 3: 469 | this.channel3Volume = 0.25; 470 | break; 471 | default: 472 | this.channel3Volume = 0.75; 473 | } 474 | } 475 | writeChannel3X(value) { 476 | this.channel3Interval = 477 | (this.cpuFrequency * (2048 - (value & 0x7ff))) / 2097152; 478 | this.channel3Timed = !!(value & 0x4000); 479 | if (this.channel3Write) { 480 | this.resetChannel3(); 481 | } 482 | } 483 | resetChannel3() { 484 | this.channel3Next = this.cpu.cycles; 485 | this.nextEvent = this.channel3Next; 486 | this.channel3End = this.cpu.cycles + this.channel3Length; 487 | this.playingChannel3 = this.channel3Write; 488 | this.updateTimers(); 489 | this.core.irq.pollNextEvent(); 490 | } 491 | writeWaveData(offset, data, width) { 492 | if (!this.channel3Bank) { 493 | offset += 16; 494 | } 495 | if (width == 2) { 496 | this.waveData[offset] = data & 0xff; 497 | data >>= 8; 498 | ++offset; 499 | } 500 | this.waveData[offset] = data & 0xff; 501 | } 502 | setChannel4Enabled(enable) { 503 | if (!this.enableChannel4 && enable) { 504 | this.channel4.next = this.cpu.cycles; 505 | this.channel4.end = this.cpu.cycles + this.channel4.length; 506 | this.enableChannel4 = true; 507 | this.playingChannel4 = true; 508 | this.nextEvent = this.cpu.cycles; 509 | this.updateEnvelope(this.channel4); 510 | this.updateTimers(); 511 | this.core.irq.pollNextEvent(); 512 | } else { 513 | this.enableChannel4 = enable; 514 | } 515 | } 516 | writeChannel4LE(value) { 517 | this.writeChannelLE(this.channel4, value); 518 | this.resetChannel4(); 519 | } 520 | writeChannel4FC(value) { 521 | this.channel4.timed = !!(value & 0x4000); 522 | 523 | var r = value & 0x7; 524 | if (!r) { 525 | r = 0.5; 526 | } 527 | var s = (value >> 4) & 0xf; 528 | var interval = (this.cpuFrequency * (r * (2 << s))) / 524288; 529 | if (interval != this.channel4.interval) { 530 | this.channel4.interval = interval; 531 | this.resetChannel4(); 532 | } 533 | 534 | var width = value & 0x8 ? 7 : 15; 535 | if (width != this.channel4.width) { 536 | this.channel4.width = width; 537 | this.resetChannel4(); 538 | } 539 | 540 | if (value & 0x8000) { 541 | this.resetChannel4(); 542 | } 543 | } 544 | resetChannel4() { 545 | if (this.channel4.width == 15) { 546 | this.channel4.lfsr = 0x4000; 547 | } else { 548 | this.channel4.lfsr = 0x40; 549 | } 550 | this.channel4.volume = this.channel4.initialVolume; 551 | if (this.channel4.step) { 552 | this.channel4.nextStep = this.cpu.cycles + this.channel4.step; 553 | } 554 | this.channel4.end = this.cpu.cycles + this.channel4.length; 555 | this.channel4.next = this.cpu.cycles; 556 | this.nextEvent = this.channel4.next; 557 | 558 | this.playingChannel4 = this.enableChannel4; 559 | this.updateTimers(); 560 | this.core.irq.pollNextEvent(); 561 | } 562 | writeChannelLE(channel, value) { 563 | channel.length = this.cpuFrequency * ((0x40 - (value & 0x3f)) / 256); 564 | 565 | if (value & 0x0800) { 566 | channel.increment = 1 / 16; 567 | } else { 568 | channel.increment = -1 / 16; 569 | } 570 | channel.initialVolume = ((value >> 12) & 0xf) / 16; 571 | 572 | channel.step = this.cpuFrequency * (((value >> 8) & 0x7) / 64); 573 | } 574 | updateEnvelope(channel, cycles) { 575 | if (channel.step) { 576 | if (cycles >= channel.nextStep) { 577 | channel.volume += channel.increment; 578 | if (channel.volume > 1) { 579 | channel.volume = 1; 580 | } else if (channel.volume < 0) { 581 | channel.volume = 0; 582 | } 583 | channel.nextStep += channel.step; 584 | } 585 | 586 | if (this.nextEvent > channel.nextStep) { 587 | this.nextEvent = channel.nextStep; 588 | } 589 | } 590 | } 591 | appendToFifoA(value) { 592 | var b; 593 | if (this.fifoA.length > 28) { 594 | this.fifoA = this.fifoA.slice(-28); 595 | } 596 | for (var i = 0; i < 4; ++i) { 597 | b = (value & 0xff) << 24; 598 | value >>= 8; 599 | this.fifoA.push(b / 0x80000000); 600 | } 601 | } 602 | appendToFifoB(value) { 603 | var b; 604 | if (this.fifoB.length > 28) { 605 | this.fifoB = this.fifoB.slice(-28); 606 | } 607 | for (var i = 0; i < 4; ++i) { 608 | b = (value & 0xff) << 24; 609 | value >>= 8; 610 | this.fifoB.push(b / 0x80000000); 611 | } 612 | } 613 | sampleFifoA() { 614 | if (this.fifoA.length <= 16) { 615 | var dma = this.core.irq.dma[this.dmaA]; 616 | dma.nextCount = 4; 617 | this.core.mmu.serviceDma(this.dmaA, dma); 618 | } 619 | this.fifoASample = this.fifoA.shift(); 620 | } 621 | sampleFifoB() { 622 | if (this.fifoB.length <= 16) { 623 | var dma = this.core.irq.dma[this.dmaB]; 624 | dma.nextCount = 4; 625 | this.core.mmu.serviceDma(this.dmaB, dma); 626 | } 627 | this.fifoBSample = this.fifoB.shift(); 628 | } 629 | scheduleFIFODma(number, info) { 630 | switch (info.dest) { 631 | case this.cpu.mmu.BASE_IO | this.cpu.irq.io.FIFO_A_LO: 632 | // FIXME: is this needed or a hack? 633 | info.dstControl = 2; 634 | this.dmaA = number; 635 | break; 636 | case this.cpu.mmu.BASE_IO | this.cpu.irq.io.FIFO_B_LO: 637 | info.dstControl = 2; 638 | this.dmaB = number; 639 | break; 640 | default: 641 | this.core.WARN( 642 | "Tried to schedule FIFO DMA for non-FIFO destination" 643 | ); 644 | break; 645 | } 646 | } 647 | sample() { 648 | var sampleLeft = 0; 649 | var sampleRight = 0; 650 | var sample; 651 | var channel; 652 | 653 | channel = this.squareChannels[0]; 654 | if (channel.playing) { 655 | sample = channel.sample * this.soundRatio * this.PSG_MAX; 656 | if (this.enabledLeft & 0x1) { 657 | sampleLeft += sample; 658 | } 659 | if (this.enabledRight & 0x1) { 660 | sampleRight += sample; 661 | } 662 | } 663 | 664 | channel = this.squareChannels[1]; 665 | if (channel.playing) { 666 | sample = channel.sample * this.soundRatio * this.PSG_MAX; 667 | if (this.enabledLeft & 0x2) { 668 | sampleLeft += sample; 669 | } 670 | if (this.enabledRight & 0x2) { 671 | sampleRight += sample; 672 | } 673 | } 674 | 675 | if (this.playingChannel3) { 676 | sample = 677 | this.channel3Sample * 678 | this.soundRatio * 679 | this.channel3Volume * 680 | this.PSG_MAX; 681 | if (this.enabledLeft & 0x4) { 682 | sampleLeft += sample; 683 | } 684 | if (this.enabledRight & 0x4) { 685 | sampleRight += sample; 686 | } 687 | } 688 | 689 | if (this.playingChannel4) { 690 | sample = this.channel4.sample * this.soundRatio * this.PSG_MAX; 691 | if (this.enabledLeft & 0x8) { 692 | sampleLeft += sample; 693 | } 694 | if (this.enabledRight & 0x8) { 695 | sampleRight += sample; 696 | } 697 | } 698 | 699 | if (this.enableChannelA) { 700 | sample = this.fifoASample * this.FIFO_MAX * this.ratioChannelA; 701 | if (this.enableLeftChannelA) { 702 | sampleLeft += sample; 703 | } 704 | if (this.enableRightChannelA) { 705 | sampleRight += sample; 706 | } 707 | } 708 | 709 | if (this.enableChannelB) { 710 | sample = this.fifoBSample * this.FIFO_MAX * this.ratioChannelB; 711 | if (this.enableLeftChannelB) { 712 | sampleLeft += sample; 713 | } 714 | if (this.enableRightChannelB) { 715 | sampleRight += sample; 716 | } 717 | } 718 | 719 | var samplePointer = this.samplePointer; 720 | sampleLeft *= this.masterVolume / this.SOUND_MAX; 721 | sampleLeft = Math.max(Math.min(sampleLeft, 1), -1); 722 | sampleRight *= this.masterVolume / this.SOUND_MAX; 723 | sampleRight = Math.max(Math.min(sampleRight, 1), -1); 724 | if (this.buffers) { 725 | this.buffers[0][samplePointer] = sampleLeft; 726 | this.buffers[1][samplePointer] = sampleRight; 727 | } 728 | this.samplePointer = (samplePointer + 1) & this.sampleMask; 729 | } 730 | audioProcess(audioProcessingEvent) { 731 | var left = audioProcessingEvent.outputBuffer.getChannelData(0); 732 | var right = audioProcessingEvent.outputBuffer.getChannelData(1); 733 | if (this.masterEnable) { 734 | var i; 735 | var o = this.outputPointer; 736 | for (i = 0; i < this.bufferSize; ++i, o += this.resampleRatio) { 737 | if (o >= this.maxSamples) { 738 | o -= this.maxSamples; 739 | } 740 | if ((o | 0) == this.samplePointer) { 741 | ++this.backup; 742 | break; 743 | } 744 | left[i] = this.buffers[0][o | 0]; 745 | right[i] = this.buffers[1][o | 0]; 746 | } 747 | for (; i < this.bufferSize; ++i) { 748 | left[i] = 0; 749 | right[i] = 0; 750 | } 751 | this.outputPointer = o; 752 | ++this.totalSamples; 753 | } else { 754 | for (i = 0; i < this.bufferSize; ++i) { 755 | left[i] = 0; 756 | right[i] = 0; 757 | } 758 | } 759 | } 760 | } 761 | -------------------------------------------------------------------------------- /js/thumb.js: -------------------------------------------------------------------------------- 1 | class ARMCoreThumb { 2 | constructor(cpu) { 3 | this.cpu = cpu; 4 | } 5 | constructADC(rd, rm) { 6 | var cpu = this.cpu; 7 | var gprs = cpu.gprs; 8 | return function () { 9 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 10 | var m = (gprs[rm] >>> 0) + !!cpu.cpsrC; 11 | var oldD = gprs[rd]; 12 | var d = (oldD >>> 0) + m; 13 | var oldDn = oldD >> 31; 14 | var dn = d >> 31; 15 | var mn = m >> 31; 16 | cpu.cpsrN = dn; 17 | cpu.cpsrZ = !(d & 0xffffffff); 18 | cpu.cpsrC = d > 0xffffffff; 19 | cpu.cpsrV = oldDn == mn && oldDn != dn && mn != dn; 20 | gprs[rd] = d; 21 | }; 22 | } 23 | constructADD1(rd, rn, immediate) { 24 | var cpu = this.cpu; 25 | var gprs = cpu.gprs; 26 | return function () { 27 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 28 | var d = (gprs[rn] >>> 0) + immediate; 29 | cpu.cpsrN = d >> 31; 30 | cpu.cpsrZ = !(d & 0xffffffff); 31 | cpu.cpsrC = d > 0xffffffff; 32 | cpu.cpsrV = 33 | !(gprs[rn] >> 31) && ((gprs[rn] >> 31) ^ d) >> 31 && d >> 31; 34 | gprs[rd] = d; 35 | }; 36 | } 37 | constructADD2(rn, immediate) { 38 | var cpu = this.cpu; 39 | var gprs = cpu.gprs; 40 | return function () { 41 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 42 | var d = (gprs[rn] >>> 0) + immediate; 43 | cpu.cpsrN = d >> 31; 44 | cpu.cpsrZ = !(d & 0xffffffff); 45 | cpu.cpsrC = d > 0xffffffff; 46 | cpu.cpsrV = 47 | !(gprs[rn] >> 31) && 48 | (gprs[rn] ^ d) >> 31 && 49 | (immediate ^ d) >> 31; 50 | gprs[rn] = d; 51 | }; 52 | } 53 | constructADD3(rd, rn, rm) { 54 | var cpu = this.cpu; 55 | var gprs = cpu.gprs; 56 | return function () { 57 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 58 | var d = (gprs[rn] >>> 0) + (gprs[rm] >>> 0); 59 | cpu.cpsrN = d >> 31; 60 | cpu.cpsrZ = !(d & 0xffffffff); 61 | cpu.cpsrC = d > 0xffffffff; 62 | cpu.cpsrV = 63 | !((gprs[rn] ^ gprs[rm]) >> 31) && 64 | (gprs[rn] ^ d) >> 31 && 65 | (gprs[rm] ^ d) >> 31; 66 | gprs[rd] = d; 67 | }; 68 | } 69 | constructADD4(rd, rm) { 70 | var cpu = this.cpu; 71 | var gprs = cpu.gprs; 72 | return function () { 73 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 74 | gprs[rd] += gprs[rm]; 75 | }; 76 | } 77 | constructADD5(rd, immediate) { 78 | var cpu = this.cpu; 79 | var gprs = cpu.gprs; 80 | return function () { 81 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 82 | gprs[rd] = (gprs[cpu.PC] & 0xfffffffc) + immediate; 83 | }; 84 | } 85 | constructADD6(rd, immediate) { 86 | var cpu = this.cpu; 87 | var gprs = cpu.gprs; 88 | return function () { 89 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 90 | gprs[rd] = gprs[cpu.SP] + immediate; 91 | }; 92 | } 93 | constructADD7(immediate) { 94 | var cpu = this.cpu; 95 | var gprs = cpu.gprs; 96 | return function () { 97 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 98 | gprs[cpu.SP] += immediate; 99 | }; 100 | } 101 | constructAND(rd, rm) { 102 | var cpu = this.cpu; 103 | var gprs = cpu.gprs; 104 | return function () { 105 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 106 | gprs[rd] = gprs[rd] & gprs[rm]; 107 | cpu.cpsrN = gprs[rd] >> 31; 108 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 109 | }; 110 | } 111 | constructASR1(rd, rm, immediate) { 112 | var cpu = this.cpu; 113 | var gprs = cpu.gprs; 114 | return function () { 115 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 116 | if (immediate == 0) { 117 | cpu.cpsrC = gprs[rm] >> 31; 118 | if (cpu.cpsrC) { 119 | gprs[rd] = 0xffffffff; 120 | } else { 121 | gprs[rd] = 0; 122 | } 123 | } else { 124 | cpu.cpsrC = gprs[rm] & (1 << (immediate - 1)); 125 | gprs[rd] = gprs[rm] >> immediate; 126 | } 127 | cpu.cpsrN = gprs[rd] >> 31; 128 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 129 | }; 130 | } 131 | constructASR2(rd, rm) { 132 | var cpu = this.cpu; 133 | var gprs = cpu.gprs; 134 | return function () { 135 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 136 | var rs = gprs[rm] & 0xff; 137 | if (rs) { 138 | if (rs < 32) { 139 | cpu.cpsrC = gprs[rd] & (1 << (rs - 1)); 140 | gprs[rd] >>= rs; 141 | } else { 142 | cpu.cpsrC = gprs[rd] >> 31; 143 | if (cpu.cpsrC) { 144 | gprs[rd] = 0xffffffff; 145 | } else { 146 | gprs[rd] = 0; 147 | } 148 | } 149 | } 150 | cpu.cpsrN = gprs[rd] >> 31; 151 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 152 | }; 153 | } 154 | constructB1(immediate, condOp) { 155 | var cpu = this.cpu; 156 | var gprs = cpu.gprs; 157 | return function () { 158 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 159 | if (condOp()) { 160 | gprs[cpu.PC] += immediate; 161 | } 162 | }; 163 | } 164 | constructB2(immediate) { 165 | var cpu = this.cpu; 166 | var gprs = cpu.gprs; 167 | return function () { 168 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 169 | gprs[cpu.PC] += immediate; 170 | }; 171 | } 172 | constructBIC(rd, rm) { 173 | var cpu = this.cpu; 174 | var gprs = cpu.gprs; 175 | return function () { 176 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 177 | gprs[rd] = gprs[rd] & ~gprs[rm]; 178 | cpu.cpsrN = gprs[rd] >> 31; 179 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 180 | }; 181 | } 182 | constructBL1(immediate) { 183 | var cpu = this.cpu; 184 | var gprs = cpu.gprs; 185 | return function () { 186 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 187 | gprs[cpu.LR] = gprs[cpu.PC] + immediate; 188 | }; 189 | } 190 | constructBL2(immediate) { 191 | var cpu = this.cpu; 192 | var gprs = cpu.gprs; 193 | return function () { 194 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 195 | var pc = gprs[cpu.PC]; 196 | gprs[cpu.PC] = gprs[cpu.LR] + (immediate << 1); 197 | gprs[cpu.LR] = pc - 1; 198 | }; 199 | } 200 | constructBX(rd, rm) { 201 | var cpu = this.cpu; 202 | var gprs = cpu.gprs; 203 | return function () { 204 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 205 | cpu.switchExecMode(gprs[rm] & 0x00000001); 206 | var misalign = 0; 207 | if (rm == 15) { 208 | misalign = gprs[rm] & 0x00000002; 209 | } 210 | gprs[cpu.PC] = gprs[rm] & (0xfffffffe - misalign); 211 | }; 212 | } 213 | constructCMN(rd, rm) { 214 | var cpu = this.cpu; 215 | var gprs = cpu.gprs; 216 | return function () { 217 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 218 | var aluOut = (gprs[rd] >>> 0) + (gprs[rm] >>> 0); 219 | cpu.cpsrN = aluOut >> 31; 220 | cpu.cpsrZ = !(aluOut & 0xffffffff); 221 | cpu.cpsrC = aluOut > 0xffffffff; 222 | cpu.cpsrV = 223 | gprs[rd] >> 31 == gprs[rm] >> 31 && 224 | gprs[rd] >> 31 != aluOut >> 31 && 225 | gprs[rm] >> 31 != aluOut >> 31; 226 | }; 227 | } 228 | constructCMP1(rn, immediate) { 229 | var cpu = this.cpu; 230 | var gprs = cpu.gprs; 231 | return function () { 232 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 233 | var aluOut = gprs[rn] - immediate; 234 | cpu.cpsrN = aluOut >> 31; 235 | cpu.cpsrZ = !(aluOut & 0xffffffff); 236 | cpu.cpsrC = gprs[rn] >>> 0 >= immediate; 237 | cpu.cpsrV = gprs[rn] >> 31 && (gprs[rn] ^ aluOut) >> 31; 238 | }; 239 | } 240 | constructCMP2(rd, rm) { 241 | var cpu = this.cpu; 242 | var gprs = cpu.gprs; 243 | return function () { 244 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 245 | var d = gprs[rd]; 246 | var m = gprs[rm]; 247 | var aluOut = d - m; 248 | var an = aluOut >> 31; 249 | var dn = d >> 31; 250 | cpu.cpsrN = an; 251 | cpu.cpsrZ = !(aluOut & 0xffffffff); 252 | cpu.cpsrC = d >>> 0 >= m >>> 0; 253 | cpu.cpsrV = dn != m >> 31 && dn != an; 254 | }; 255 | } 256 | constructCMP3(rd, rm) { 257 | var cpu = this.cpu; 258 | var gprs = cpu.gprs; 259 | return function () { 260 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 261 | var aluOut = gprs[rd] - gprs[rm]; 262 | cpu.cpsrN = aluOut >> 31; 263 | cpu.cpsrZ = !(aluOut & 0xffffffff); 264 | cpu.cpsrC = gprs[rd] >>> 0 >= gprs[rm] >>> 0; 265 | cpu.cpsrV = 266 | (gprs[rd] ^ gprs[rm]) >> 31 && (gprs[rd] ^ aluOut) >> 31; 267 | }; 268 | } 269 | constructEOR(rd, rm) { 270 | var cpu = this.cpu; 271 | var gprs = cpu.gprs; 272 | return function () { 273 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 274 | gprs[rd] = gprs[rd] ^ gprs[rm]; 275 | cpu.cpsrN = gprs[rd] >> 31; 276 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 277 | }; 278 | } 279 | constructLDMIA(rn, rs) { 280 | var cpu = this.cpu; 281 | var gprs = cpu.gprs; 282 | return function () { 283 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 284 | var address = gprs[rn]; 285 | var total = 0; 286 | var m, i; 287 | for (m = 0x01, i = 0; i < 8; m <<= 1, ++i) { 288 | if (rs & m) { 289 | gprs[i] = cpu.mmu.load32(address); 290 | address += 4; 291 | ++total; 292 | } 293 | } 294 | cpu.mmu.waitMulti32(address, total); 295 | if (!((1 << rn) & rs)) { 296 | gprs[rn] = address; 297 | } 298 | }; 299 | } 300 | constructLDR1(rd, rn, immediate) { 301 | var cpu = this.cpu; 302 | var gprs = cpu.gprs; 303 | return function () { 304 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 305 | var n = gprs[rn] + immediate; 306 | gprs[rd] = cpu.mmu.load32(n); 307 | cpu.mmu.wait32(n); 308 | ++cpu.cycles; 309 | }; 310 | } 311 | constructLDR2(rd, rn, rm) { 312 | var cpu = this.cpu; 313 | var gprs = cpu.gprs; 314 | return function () { 315 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 316 | gprs[rd] = cpu.mmu.load32(gprs[rn] + gprs[rm]); 317 | cpu.mmu.wait32(gprs[rn] + gprs[rm]); 318 | ++cpu.cycles; 319 | }; 320 | } 321 | constructLDR3(rd, immediate) { 322 | var cpu = this.cpu; 323 | var gprs = cpu.gprs; 324 | return function () { 325 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 326 | gprs[rd] = cpu.mmu.load32((gprs[cpu.PC] & 0xfffffffc) + immediate); 327 | cpu.mmu.wait32(gprs[cpu.PC]); 328 | ++cpu.cycles; 329 | }; 330 | } 331 | constructLDR4(rd, immediate) { 332 | var cpu = this.cpu; 333 | var gprs = cpu.gprs; 334 | return function () { 335 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 336 | gprs[rd] = cpu.mmu.load32(gprs[cpu.SP] + immediate); 337 | cpu.mmu.wait32(gprs[cpu.SP] + immediate); 338 | ++cpu.cycles; 339 | }; 340 | } 341 | constructLDRB1(rd, rn, immediate) { 342 | var cpu = this.cpu; 343 | var gprs = cpu.gprs; 344 | return function () { 345 | var n = gprs[rn] + immediate; 346 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 347 | gprs[rd] = cpu.mmu.loadU8(n); 348 | cpu.mmu.wait(n); 349 | ++cpu.cycles; 350 | }; 351 | } 352 | constructLDRB2(rd, rn, rm) { 353 | var cpu = this.cpu; 354 | var gprs = cpu.gprs; 355 | return function () { 356 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 357 | gprs[rd] = cpu.mmu.loadU8(gprs[rn] + gprs[rm]); 358 | cpu.mmu.wait(gprs[rn] + gprs[rm]); 359 | ++cpu.cycles; 360 | }; 361 | } 362 | constructLDRH1(rd, rn, immediate) { 363 | var cpu = this.cpu; 364 | var gprs = cpu.gprs; 365 | return function () { 366 | var n = gprs[rn] + immediate; 367 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 368 | gprs[rd] = cpu.mmu.loadU16(n); 369 | cpu.mmu.wait(n); 370 | ++cpu.cycles; 371 | }; 372 | } 373 | constructLDRH2(rd, rn, rm) { 374 | var cpu = this.cpu; 375 | var gprs = cpu.gprs; 376 | return function () { 377 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 378 | gprs[rd] = cpu.mmu.loadU16(gprs[rn] + gprs[rm]); 379 | cpu.mmu.wait(gprs[rn] + gprs[rm]); 380 | ++cpu.cycles; 381 | }; 382 | } 383 | constructLDRSB(rd, rn, rm) { 384 | var cpu = this.cpu; 385 | var gprs = cpu.gprs; 386 | return function () { 387 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 388 | gprs[rd] = cpu.mmu.load8(gprs[rn] + gprs[rm]); 389 | cpu.mmu.wait(gprs[rn] + gprs[rm]); 390 | ++cpu.cycles; 391 | }; 392 | } 393 | constructLDRSH(rd, rn, rm) { 394 | var cpu = this.cpu; 395 | var gprs = cpu.gprs; 396 | return function () { 397 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 398 | gprs[rd] = cpu.mmu.load16(gprs[rn] + gprs[rm]); 399 | cpu.mmu.wait(gprs[rn] + gprs[rm]); 400 | ++cpu.cycles; 401 | }; 402 | } 403 | constructLSL1(rd, rm, immediate) { 404 | var cpu = this.cpu; 405 | var gprs = cpu.gprs; 406 | return function () { 407 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 408 | if (immediate == 0) { 409 | gprs[rd] = gprs[rm]; 410 | } else { 411 | cpu.cpsrC = gprs[rm] & (1 << (32 - immediate)); 412 | gprs[rd] = gprs[rm] << immediate; 413 | } 414 | cpu.cpsrN = gprs[rd] >> 31; 415 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 416 | }; 417 | } 418 | constructLSL2(rd, rm) { 419 | var cpu = this.cpu; 420 | var gprs = cpu.gprs; 421 | return function () { 422 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 423 | var rs = gprs[rm] & 0xff; 424 | if (rs) { 425 | if (rs < 32) { 426 | cpu.cpsrC = gprs[rd] & (1 << (32 - rs)); 427 | gprs[rd] <<= rs; 428 | } else { 429 | if (rs > 32) { 430 | cpu.cpsrC = 0; 431 | } else { 432 | cpu.cpsrC = gprs[rd] & 0x00000001; 433 | } 434 | gprs[rd] = 0; 435 | } 436 | } 437 | cpu.cpsrN = gprs[rd] >> 31; 438 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 439 | }; 440 | } 441 | constructLSR1(rd, rm, immediate) { 442 | var cpu = this.cpu; 443 | var gprs = cpu.gprs; 444 | return function () { 445 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 446 | if (immediate == 0) { 447 | cpu.cpsrC = gprs[rm] >> 31; 448 | gprs[rd] = 0; 449 | } else { 450 | cpu.cpsrC = gprs[rm] & (1 << (immediate - 1)); 451 | gprs[rd] = gprs[rm] >>> immediate; 452 | } 453 | cpu.cpsrN = 0; 454 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 455 | }; 456 | } 457 | constructLSR2(rd, rm) { 458 | var cpu = this.cpu; 459 | var gprs = cpu.gprs; 460 | return function () { 461 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 462 | var rs = gprs[rm] & 0xff; 463 | if (rs) { 464 | if (rs < 32) { 465 | cpu.cpsrC = gprs[rd] & (1 << (rs - 1)); 466 | gprs[rd] >>>= rs; 467 | } else { 468 | if (rs > 32) { 469 | cpu.cpsrC = 0; 470 | } else { 471 | cpu.cpsrC = gprs[rd] >> 31; 472 | } 473 | gprs[rd] = 0; 474 | } 475 | } 476 | cpu.cpsrN = gprs[rd] >> 31; 477 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 478 | }; 479 | } 480 | constructMOV1(rn, immediate) { 481 | var cpu = this.cpu; 482 | var gprs = cpu.gprs; 483 | return function () { 484 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 485 | gprs[rn] = immediate; 486 | cpu.cpsrN = immediate >> 31; 487 | cpu.cpsrZ = !(immediate & 0xffffffff); 488 | }; 489 | } 490 | constructMOV2(rd, rn, rm) { 491 | var cpu = this.cpu; 492 | var gprs = cpu.gprs; 493 | return function () { 494 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 495 | var d = gprs[rn]; 496 | cpu.cpsrN = d >> 31; 497 | cpu.cpsrZ = !(d & 0xffffffff); 498 | cpu.cpsrC = 0; 499 | cpu.cpsrV = 0; 500 | gprs[rd] = d; 501 | }; 502 | } 503 | constructMOV3(rd, rm) { 504 | var cpu = this.cpu; 505 | var gprs = cpu.gprs; 506 | return function () { 507 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 508 | gprs[rd] = gprs[rm]; 509 | }; 510 | } 511 | constructMUL(rd, rm) { 512 | var cpu = this.cpu; 513 | var gprs = cpu.gprs; 514 | return function () { 515 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 516 | cpu.mmu.waitMul(gprs[rm]); 517 | if (gprs[rm] & 0xffff0000 && gprs[rd] & 0xffff0000) { 518 | // Our data type is a double--we'll lose bits if we do it all at once! 519 | var hi = ((gprs[rd] & 0xffff0000) * gprs[rm]) & 0xffffffff; 520 | var lo = ((gprs[rd] & 0x0000ffff) * gprs[rm]) & 0xffffffff; 521 | gprs[rd] = (hi + lo) & 0xffffffff; 522 | } else { 523 | gprs[rd] *= gprs[rm]; 524 | } 525 | cpu.cpsrN = gprs[rd] >> 31; 526 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 527 | }; 528 | } 529 | constructMVN(rd, rm) { 530 | var cpu = this.cpu; 531 | var gprs = cpu.gprs; 532 | return function () { 533 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 534 | gprs[rd] = ~gprs[rm]; 535 | cpu.cpsrN = gprs[rd] >> 31; 536 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 537 | }; 538 | } 539 | constructNEG(rd, rm) { 540 | var cpu = this.cpu; 541 | var gprs = cpu.gprs; 542 | return function () { 543 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 544 | var d = -gprs[rm]; 545 | cpu.cpsrN = d >> 31; 546 | cpu.cpsrZ = !(d & 0xffffffff); 547 | cpu.cpsrC = 0 >= d >>> 0; 548 | cpu.cpsrV = gprs[rm] >> 31 && d >> 31; 549 | gprs[rd] = d; 550 | }; 551 | } 552 | constructORR(rd, rm) { 553 | var cpu = this.cpu; 554 | var gprs = cpu.gprs; 555 | return function () { 556 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 557 | gprs[rd] = gprs[rd] | gprs[rm]; 558 | cpu.cpsrN = gprs[rd] >> 31; 559 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 560 | }; 561 | } 562 | constructPOP(rs, r) { 563 | var cpu = this.cpu; 564 | var gprs = cpu.gprs; 565 | return function () { 566 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 567 | ++cpu.cycles; 568 | var address = gprs[cpu.SP]; 569 | var total = 0; 570 | var m, i; 571 | for (m = 0x01, i = 0; i < 8; m <<= 1, ++i) { 572 | if (rs & m) { 573 | cpu.mmu.waitSeq32(address); 574 | gprs[i] = cpu.mmu.load32(address); 575 | address += 4; 576 | ++total; 577 | } 578 | } 579 | if (r) { 580 | gprs[cpu.PC] = cpu.mmu.load32(address) & 0xfffffffe; 581 | address += 4; 582 | ++total; 583 | } 584 | cpu.mmu.waitMulti32(address, total); 585 | gprs[cpu.SP] = address; 586 | }; 587 | } 588 | constructPUSH(rs, r) { 589 | var cpu = this.cpu; 590 | var gprs = cpu.gprs; 591 | return function () { 592 | var address = gprs[cpu.SP] - 4; 593 | var total = 0; 594 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 595 | if (r) { 596 | cpu.mmu.store32(address, gprs[cpu.LR]); 597 | address -= 4; 598 | ++total; 599 | } 600 | var m, i; 601 | for (m = 0x80, i = 7; m; m >>= 1, --i) { 602 | if (rs & m) { 603 | cpu.mmu.store32(address, gprs[i]); 604 | address -= 4; 605 | ++total; 606 | break; 607 | } 608 | } 609 | for (m >>= 1, --i; m; m >>= 1, --i) { 610 | if (rs & m) { 611 | cpu.mmu.store32(address, gprs[i]); 612 | address -= 4; 613 | ++total; 614 | } 615 | } 616 | cpu.mmu.waitMulti32(address, total); 617 | gprs[cpu.SP] = address + 4; 618 | }; 619 | } 620 | constructROR(rd, rm) { 621 | var cpu = this.cpu; 622 | var gprs = cpu.gprs; 623 | return function () { 624 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 625 | var rs = gprs[rm] & 0xff; 626 | if (rs) { 627 | var r4 = rs & 0x1f; 628 | if (r4 > 0) { 629 | cpu.cpsrC = gprs[rd] & (1 << (r4 - 1)); 630 | gprs[rd] = (gprs[rd] >>> r4) | (gprs[rd] << (32 - r4)); 631 | } else { 632 | cpu.cpsrC = gprs[rd] >> 31; 633 | } 634 | } 635 | cpu.cpsrN = gprs[rd] >> 31; 636 | cpu.cpsrZ = !(gprs[rd] & 0xffffffff); 637 | }; 638 | } 639 | constructSBC(rd, rm) { 640 | var cpu = this.cpu; 641 | var gprs = cpu.gprs; 642 | return function () { 643 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 644 | var m = (gprs[rm] >>> 0) + !cpu.cpsrC; 645 | var d = (gprs[rd] >>> 0) - m; 646 | cpu.cpsrN = d >> 31; 647 | cpu.cpsrZ = !(d & 0xffffffff); 648 | cpu.cpsrC = gprs[rd] >>> 0 >= d >>> 0; 649 | cpu.cpsrV = (gprs[rd] ^ m) >> 31 && (gprs[rd] ^ d) >> 31; 650 | gprs[rd] = d; 651 | }; 652 | } 653 | constructSTMIA(rn, rs) { 654 | var cpu = this.cpu; 655 | var gprs = cpu.gprs; 656 | return function () { 657 | cpu.mmu.wait(gprs[cpu.PC]); 658 | var address = gprs[rn]; 659 | var total = 0; 660 | var m, i; 661 | for (m = 0x01, i = 0; i < 8; m <<= 1, ++i) { 662 | if (rs & m) { 663 | cpu.mmu.store32(address, gprs[i]); 664 | address += 4; 665 | ++total; 666 | break; 667 | } 668 | } 669 | for (m <<= 1, ++i; i < 8; m <<= 1, ++i) { 670 | if (rs & m) { 671 | cpu.mmu.store32(address, gprs[i]); 672 | address += 4; 673 | ++total; 674 | } 675 | } 676 | cpu.mmu.waitMulti32(address, total); 677 | gprs[rn] = address; 678 | }; 679 | } 680 | constructSTR1(rd, rn, immediate) { 681 | var cpu = this.cpu; 682 | var gprs = cpu.gprs; 683 | return function () { 684 | var n = gprs[rn] + immediate; 685 | cpu.mmu.store32(n, gprs[rd]); 686 | cpu.mmu.wait(gprs[cpu.PC]); 687 | cpu.mmu.wait32(n); 688 | }; 689 | } 690 | constructSTR2(rd, rn, rm) { 691 | var cpu = this.cpu; 692 | var gprs = cpu.gprs; 693 | return function () { 694 | cpu.mmu.store32(gprs[rn] + gprs[rm], gprs[rd]); 695 | cpu.mmu.wait(gprs[cpu.PC]); 696 | cpu.mmu.wait32(gprs[rn] + gprs[rm]); 697 | }; 698 | } 699 | constructSTR3(rd, immediate) { 700 | var cpu = this.cpu; 701 | var gprs = cpu.gprs; 702 | return function () { 703 | cpu.mmu.store32(gprs[cpu.SP] + immediate, gprs[rd]); 704 | cpu.mmu.wait(gprs[cpu.PC]); 705 | cpu.mmu.wait32(gprs[cpu.SP] + immediate); 706 | }; 707 | } 708 | constructSTRB1(rd, rn, immediate) { 709 | var cpu = this.cpu; 710 | var gprs = cpu.gprs; 711 | return function () { 712 | var n = gprs[rn] + immediate; 713 | cpu.mmu.store8(n, gprs[rd]); 714 | cpu.mmu.wait(gprs[cpu.PC]); 715 | cpu.mmu.wait(n); 716 | }; 717 | } 718 | constructSTRB2(rd, rn, rm) { 719 | var cpu = this.cpu; 720 | var gprs = cpu.gprs; 721 | return function () { 722 | cpu.mmu.store8(gprs[rn] + gprs[rm], gprs[rd]); 723 | cpu.mmu.wait(gprs[cpu.PC]); 724 | cpu.mmu.wait(gprs[rn] + gprs[rm]); 725 | }; 726 | } 727 | constructSTRH1(rd, rn, immediate) { 728 | var cpu = this.cpu; 729 | var gprs = cpu.gprs; 730 | return function () { 731 | var n = gprs[rn] + immediate; 732 | cpu.mmu.store16(n, gprs[rd]); 733 | cpu.mmu.wait(gprs[cpu.PC]); 734 | cpu.mmu.wait(n); 735 | }; 736 | } 737 | constructSTRH2(rd, rn, rm) { 738 | var cpu = this.cpu; 739 | var gprs = cpu.gprs; 740 | return function () { 741 | cpu.mmu.store16(gprs[rn] + gprs[rm], gprs[rd]); 742 | cpu.mmu.wait(gprs[cpu.PC]); 743 | cpu.mmu.wait(gprs[rn] + gprs[rm]); 744 | }; 745 | } 746 | constructSUB1(rd, rn, immediate) { 747 | var cpu = this.cpu; 748 | var gprs = cpu.gprs; 749 | return function () { 750 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 751 | var d = gprs[rn] - immediate; 752 | cpu.cpsrN = d >> 31; 753 | cpu.cpsrZ = !(d & 0xffffffff); 754 | cpu.cpsrC = gprs[rn] >>> 0 >= immediate; 755 | cpu.cpsrV = gprs[rn] >> 31 && (gprs[rn] ^ d) >> 31; 756 | gprs[rd] = d; 757 | }; 758 | } 759 | constructSUB2(rn, immediate) { 760 | var cpu = this.cpu; 761 | var gprs = cpu.gprs; 762 | return function () { 763 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 764 | var d = gprs[rn] - immediate; 765 | cpu.cpsrN = d >> 31; 766 | cpu.cpsrZ = !(d & 0xffffffff); 767 | cpu.cpsrC = gprs[rn] >>> 0 >= immediate; 768 | cpu.cpsrV = gprs[rn] >> 31 && (gprs[rn] ^ d) >> 31; 769 | gprs[rn] = d; 770 | }; 771 | } 772 | constructSUB3(rd, rn, rm) { 773 | var cpu = this.cpu; 774 | var gprs = cpu.gprs; 775 | return function () { 776 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 777 | var d = gprs[rn] - gprs[rm]; 778 | cpu.cpsrN = d >> 31; 779 | cpu.cpsrZ = !(d & 0xffffffff); 780 | cpu.cpsrC = gprs[rn] >>> 0 >= gprs[rm] >>> 0; 781 | cpu.cpsrV = 782 | gprs[rn] >> 31 != gprs[rm] >> 31 && gprs[rn] >> 31 != d >> 31; 783 | gprs[rd] = d; 784 | }; 785 | } 786 | constructSWI(immediate) { 787 | var cpu = this.cpu; 788 | var gprs = cpu.gprs; 789 | return function () { 790 | cpu.irq.swi(immediate); 791 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 792 | }; 793 | } 794 | constructTST(rd, rm) { 795 | var cpu = this.cpu; 796 | var gprs = cpu.gprs; 797 | return function () { 798 | cpu.mmu.waitPrefetch(gprs[cpu.PC]); 799 | var aluOut = gprs[rd] & gprs[rm]; 800 | cpu.cpsrN = aluOut >> 31; 801 | cpu.cpsrZ = !(aluOut & 0xffffffff); 802 | }; 803 | } 804 | } 805 | -------------------------------------------------------------------------------- /js/io.js: -------------------------------------------------------------------------------- 1 | class GameBoyAdvanceIO { 2 | constructor() { 3 | // Video 4 | this.DISPCNT = 0x000; 5 | this.GREENSWP = 0x002; 6 | this.DISPSTAT = 0x004; 7 | this.VCOUNT = 0x006; 8 | this.BG0CNT = 0x008; 9 | this.BG1CNT = 0x00a; 10 | this.BG2CNT = 0x00c; 11 | this.BG3CNT = 0x00e; 12 | this.BG0HOFS = 0x010; 13 | this.BG0VOFS = 0x012; 14 | this.BG1HOFS = 0x014; 15 | this.BG1VOFS = 0x016; 16 | this.BG2HOFS = 0x018; 17 | this.BG2VOFS = 0x01a; 18 | this.BG3HOFS = 0x01c; 19 | this.BG3VOFS = 0x01e; 20 | this.BG2PA = 0x020; 21 | this.BG2PB = 0x022; 22 | this.BG2PC = 0x024; 23 | this.BG2PD = 0x026; 24 | this.BG2X_LO = 0x028; 25 | this.BG2X_HI = 0x02a; 26 | this.BG2Y_LO = 0x02c; 27 | this.BG2Y_HI = 0x02e; 28 | this.BG3PA = 0x030; 29 | this.BG3PB = 0x032; 30 | this.BG3PC = 0x034; 31 | this.BG3PD = 0x036; 32 | this.BG3X_LO = 0x038; 33 | this.BG3X_HI = 0x03a; 34 | this.BG3Y_LO = 0x03c; 35 | this.BG3Y_HI = 0x03e; 36 | this.WIN0H = 0x040; 37 | this.WIN1H = 0x042; 38 | this.WIN0V = 0x044; 39 | this.WIN1V = 0x046; 40 | this.WININ = 0x048; 41 | this.WINOUT = 0x04a; 42 | this.MOSAIC = 0x04c; 43 | this.BLDCNT = 0x050; 44 | this.BLDALPHA = 0x052; 45 | this.BLDY = 0x054; 46 | 47 | // Sound 48 | this.SOUND1CNT_LO = 0x060; 49 | this.SOUND1CNT_HI = 0x062; 50 | this.SOUND1CNT_X = 0x064; 51 | this.SOUND2CNT_LO = 0x068; 52 | this.SOUND2CNT_HI = 0x06c; 53 | this.SOUND3CNT_LO = 0x070; 54 | this.SOUND3CNT_HI = 0x072; 55 | this.SOUND3CNT_X = 0x074; 56 | this.SOUND4CNT_LO = 0x078; 57 | this.SOUND4CNT_HI = 0x07c; 58 | this.SOUNDCNT_LO = 0x080; 59 | this.SOUNDCNT_HI = 0x082; 60 | this.SOUNDCNT_X = 0x084; 61 | this.SOUNDBIAS = 0x088; 62 | this.WAVE_RAM0_LO = 0x090; 63 | this.WAVE_RAM0_HI = 0x092; 64 | this.WAVE_RAM1_LO = 0x094; 65 | this.WAVE_RAM1_HI = 0x096; 66 | this.WAVE_RAM2_LO = 0x098; 67 | this.WAVE_RAM2_HI = 0x09a; 68 | this.WAVE_RAM3_LO = 0x09c; 69 | this.WAVE_RAM3_HI = 0x09e; 70 | this.FIFO_A_LO = 0x0a0; 71 | this.FIFO_A_HI = 0x0a2; 72 | this.FIFO_B_LO = 0x0a4; 73 | this.FIFO_B_HI = 0x0a6; 74 | 75 | // DMA 76 | this.DMA0SAD_LO = 0x0b0; 77 | this.DMA0SAD_HI = 0x0b2; 78 | this.DMA0DAD_LO = 0x0b4; 79 | this.DMA0DAD_HI = 0x0b6; 80 | this.DMA0CNT_LO = 0x0b8; 81 | this.DMA0CNT_HI = 0x0ba; 82 | this.DMA1SAD_LO = 0x0bc; 83 | this.DMA1SAD_HI = 0x0be; 84 | this.DMA1DAD_LO = 0x0c0; 85 | this.DMA1DAD_HI = 0x0c2; 86 | this.DMA1CNT_LO = 0x0c4; 87 | this.DMA1CNT_HI = 0x0c6; 88 | this.DMA2SAD_LO = 0x0c8; 89 | this.DMA2SAD_HI = 0x0ca; 90 | this.DMA2DAD_LO = 0x0cc; 91 | this.DMA2DAD_HI = 0x0ce; 92 | this.DMA2CNT_LO = 0x0d0; 93 | this.DMA2CNT_HI = 0x0d2; 94 | this.DMA3SAD_LO = 0x0d4; 95 | this.DMA3SAD_HI = 0x0d6; 96 | this.DMA3DAD_LO = 0x0d8; 97 | this.DMA3DAD_HI = 0x0da; 98 | this.DMA3CNT_LO = 0x0dc; 99 | this.DMA3CNT_HI = 0x0de; 100 | 101 | // Timers 102 | this.TM0CNT_LO = 0x100; 103 | this.TM0CNT_HI = 0x102; 104 | this.TM1CNT_LO = 0x104; 105 | this.TM1CNT_HI = 0x106; 106 | this.TM2CNT_LO = 0x108; 107 | this.TM2CNT_HI = 0x10a; 108 | this.TM3CNT_LO = 0x10c; 109 | this.TM3CNT_HI = 0x10e; 110 | 111 | // SIO (note: some of these are repeated) 112 | this.SIODATA32_LO = 0x120; 113 | this.SIOMULTI0 = 0x120; 114 | this.SIODATA32_HI = 0x122; 115 | this.SIOMULTI1 = 0x122; 116 | this.SIOMULTI2 = 0x124; 117 | this.SIOMULTI3 = 0x126; 118 | this.SIOCNT = 0x128; 119 | this.SIOMLT_SEND = 0x12a; 120 | this.SIODATA8 = 0x12a; 121 | this.RCNT = 0x134; 122 | this.JOYCNT = 0x140; 123 | this.JOY_RECV = 0x150; 124 | this.JOY_TRANS = 0x154; 125 | this.JOYSTAT = 0x158; 126 | 127 | // Keypad 128 | this.KEYINPUT = 0x130; 129 | this.KEYCNT = 0x132; 130 | 131 | // Interrupts, etc 132 | this.IE = 0x200; 133 | this.IF = 0x202; 134 | this.WAITCNT = 0x204; 135 | this.IME = 0x208; 136 | 137 | this.POSTFLG = 0x300; 138 | this.HALTCNT = 0x301; 139 | 140 | this.DEFAULT_DISPCNT = 0x0080; 141 | this.DEFAULT_SOUNDBIAS = 0x200; 142 | this.DEFAULT_BGPA = 1; 143 | this.DEFAULT_BGPD = 1; 144 | this.DEFAULT_RCNT = 0x8000; 145 | } 146 | clear() { 147 | this.registers = new Uint16Array(this.cpu.mmu.SIZE_IO); 148 | 149 | this.registers[this.DISPCNT >> 1] = this.DEFAULT_DISPCNT; 150 | this.registers[this.SOUNDBIAS >> 1] = this.DEFAULT_SOUNDBIAS; 151 | this.registers[this.BG2PA >> 1] = this.DEFAULT_BGPA; 152 | this.registers[this.BG2PD >> 1] = this.DEFAULT_BGPD; 153 | this.registers[this.BG3PA >> 1] = this.DEFAULT_BGPA; 154 | this.registers[this.BG3PD >> 1] = this.DEFAULT_BGPD; 155 | this.registers[this.RCNT >> 1] = this.DEFAULT_RCNT; 156 | } 157 | freeze() { 158 | return { 159 | registers: Serializer.prefix(this.registers.buffer) 160 | }; 161 | } 162 | defrost(frost) { 163 | this.registers = new Uint16Array(frost.registers); 164 | // Video registers don't serialize themselves 165 | for (var i = 0; i <= this.BLDY; i += 2) { 166 | this.store16(i, this.registers[i >> 1]); 167 | } 168 | } 169 | load8(offset) { 170 | throw "Unimplmeneted unaligned I/O access"; 171 | } 172 | load16(offset) { 173 | return (this.loadU16(offset) << 16) >> 16; 174 | } 175 | load32(offset) { 176 | offset &= 0xfffffffc; 177 | switch (offset) { 178 | case this.DMA0CNT_LO: 179 | case this.DMA1CNT_LO: 180 | case this.DMA2CNT_LO: 181 | case this.DMA3CNT_LO: 182 | return this.loadU16(offset | 2) << 16; 183 | case this.IME: 184 | return this.loadU16(offset) & 0xffff; 185 | case this.JOY_RECV: 186 | case this.JOY_TRANS: 187 | this.core.STUB( 188 | "Unimplemented JOY register read: 0x" + offset.toString(16) 189 | ); 190 | return 0; 191 | } 192 | 193 | return this.loadU16(offset) | (this.loadU16(offset | 2) << 16); 194 | } 195 | loadU8(offset) { 196 | var odd = offset & 0x0001; 197 | var value = this.loadU16(offset & 0xfffe); 198 | return (value >>> (odd << 3)) & 0xff; 199 | } 200 | loadU16(offset) { 201 | switch (offset) { 202 | case this.DISPCNT: 203 | case this.BG0CNT: 204 | case this.BG1CNT: 205 | case this.BG2CNT: 206 | case this.BG3CNT: 207 | case this.WININ: 208 | case this.WINOUT: 209 | case this.SOUND1CNT_LO: 210 | case this.SOUND3CNT_LO: 211 | case this.SOUNDCNT_LO: 212 | case this.SOUNDCNT_HI: 213 | case this.SOUNDBIAS: 214 | case this.BLDCNT: 215 | case this.BLDALPHA: 216 | 217 | case this.TM0CNT_HI: 218 | case this.TM1CNT_HI: 219 | case this.TM2CNT_HI: 220 | case this.TM3CNT_HI: 221 | case this.DMA0CNT_HI: 222 | case this.DMA1CNT_HI: 223 | case this.DMA2CNT_HI: 224 | case this.DMA3CNT_HI: 225 | case this.RCNT: 226 | case this.WAITCNT: 227 | case this.IE: 228 | case this.IF: 229 | case this.IME: 230 | case this.POSTFLG: 231 | // Handled transparently by the written registers 232 | break; 233 | 234 | // Video 235 | case this.DISPSTAT: 236 | return ( 237 | this.registers[offset >> 1] | this.video.readDisplayStat() 238 | ); 239 | case this.VCOUNT: 240 | return this.video.vcount; 241 | 242 | // Sound 243 | case this.SOUND1CNT_HI: 244 | case this.SOUND2CNT_LO: 245 | return this.registers[offset >> 1] & 0xffc0; 246 | case this.SOUND1CNT_X: 247 | case this.SOUND2CNT_HI: 248 | case this.SOUND3CNT_X: 249 | return this.registers[offset >> 1] & 0x4000; 250 | case this.SOUND3CNT_HI: 251 | return this.registers[offset >> 1] & 0xe000; 252 | case this.SOUND4CNT_LO: 253 | return this.registers[offset >> 1] & 0xff00; 254 | case this.SOUND4CNT_HI: 255 | return this.registers[offset >> 1] & 0x40ff; 256 | case this.SOUNDCNT_X: 257 | this.core.STUB("Unimplemented sound register read: SOUNDCNT_X"); 258 | return this.registers[offset >> 1] | 0x0000; 259 | 260 | // Timers 261 | case this.TM0CNT_LO: 262 | return this.cpu.irq.timerRead(0); 263 | case this.TM1CNT_LO: 264 | return this.cpu.irq.timerRead(1); 265 | case this.TM2CNT_LO: 266 | return this.cpu.irq.timerRead(2); 267 | case this.TM3CNT_LO: 268 | return this.cpu.irq.timerRead(3); 269 | 270 | // SIO 271 | case this.SIOCNT: 272 | return this.sio.readSIOCNT(); 273 | 274 | case this.KEYINPUT: 275 | this.keypad.pollGamepads(); 276 | return this.keypad.currentDown; 277 | case this.KEYCNT: 278 | this.core.STUB("Unimplemented I/O register read: KEYCNT"); 279 | return 0; 280 | 281 | case this.BG0HOFS: 282 | case this.BG0VOFS: 283 | case this.BG1HOFS: 284 | case this.BG1VOFS: 285 | case this.BG2HOFS: 286 | case this.BG2VOFS: 287 | case this.BG3HOFS: 288 | case this.BG3VOFS: 289 | case this.BG2PA: 290 | case this.BG2PB: 291 | case this.BG2PC: 292 | case this.BG2PD: 293 | case this.BG3PA: 294 | case this.BG3PB: 295 | case this.BG3PC: 296 | case this.BG3PD: 297 | case this.BG2X_LO: 298 | case this.BG2X_HI: 299 | case this.BG2Y_LO: 300 | case this.BG2Y_HI: 301 | case this.BG3X_LO: 302 | case this.BG3X_HI: 303 | case this.BG3Y_LO: 304 | case this.BG3Y_HI: 305 | case this.WIN0H: 306 | case this.WIN1H: 307 | case this.WIN0V: 308 | case this.WIN1V: 309 | case this.BLDY: 310 | case this.DMA0SAD_LO: 311 | case this.DMA0SAD_HI: 312 | case this.DMA0DAD_LO: 313 | case this.DMA0DAD_HI: 314 | case this.DMA0CNT_LO: 315 | case this.DMA1SAD_LO: 316 | case this.DMA1SAD_HI: 317 | case this.DMA1DAD_LO: 318 | case this.DMA1DAD_HI: 319 | case this.DMA1CNT_LO: 320 | case this.DMA2SAD_LO: 321 | case this.DMA2SAD_HI: 322 | case this.DMA2DAD_LO: 323 | case this.DMA2DAD_HI: 324 | case this.DMA2CNT_LO: 325 | case this.DMA3SAD_LO: 326 | case this.DMA3SAD_HI: 327 | case this.DMA3DAD_LO: 328 | case this.DMA3DAD_HI: 329 | case this.DMA3CNT_LO: 330 | case this.FIFO_A_LO: 331 | case this.FIFO_A_HI: 332 | case this.FIFO_B_LO: 333 | case this.FIFO_B_HI: 334 | this.core.WARN( 335 | "Read for write-only register: 0x" + offset.toString(16) 336 | ); 337 | return this.core.mmu.badMemory.loadU16(0); 338 | 339 | case this.MOSAIC: 340 | this.core.WARN( 341 | "Read for write-only register: 0x" + offset.toString(16) 342 | ); 343 | return 0; 344 | 345 | case this.SIOMULTI0: 346 | case this.SIOMULTI1: 347 | case this.SIOMULTI2: 348 | case this.SIOMULTI3: 349 | return this.sio.read((offset - this.SIOMULTI0) >> 1); 350 | 351 | case this.SIODATA8: 352 | this.core.STUB( 353 | "Unimplemented SIO register read: 0x" + offset.toString(16) 354 | ); 355 | return 0; 356 | case this.JOYCNT: 357 | case this.JOYSTAT: 358 | this.core.STUB( 359 | "Unimplemented JOY register read: 0x" + offset.toString(16) 360 | ); 361 | return 0; 362 | 363 | default: 364 | this.core.WARN( 365 | "Bad I/O register read: 0x" + offset.toString(16) 366 | ); 367 | return this.core.mmu.badMemory.loadU16(0); 368 | } 369 | return this.registers[offset >> 1]; 370 | } 371 | store8(offset, value) { 372 | switch (offset) { 373 | case this.WININ: 374 | this.value & 0x3f; 375 | break; 376 | case this.WININ | 1: 377 | this.value & 0x3f; 378 | break; 379 | case this.WINOUT: 380 | this.value & 0x3f; 381 | break; 382 | case this.WINOUT | 1: 383 | this.value & 0x3f; 384 | break; 385 | case this.SOUND1CNT_LO: 386 | case this.SOUND1CNT_LO | 1: 387 | case this.SOUND1CNT_HI: 388 | case this.SOUND1CNT_HI | 1: 389 | case this.SOUND1CNT_X: 390 | case this.SOUND1CNT_X | 1: 391 | case this.SOUND2CNT_LO: 392 | case this.SOUND2CNT_LO | 1: 393 | case this.SOUND2CNT_HI: 394 | case this.SOUND2CNT_HI | 1: 395 | case this.SOUND3CNT_LO: 396 | case this.SOUND3CNT_LO | 1: 397 | case this.SOUND3CNT_HI: 398 | case this.SOUND3CNT_HI | 1: 399 | case this.SOUND3CNT_X: 400 | case this.SOUND3CNT_X | 1: 401 | case this.SOUND4CNT_LO: 402 | case this.SOUND4CNT_LO | 1: 403 | case this.SOUND4CNT_HI: 404 | case this.SOUND4CNT_HI | 1: 405 | case this.SOUNDCNT_LO: 406 | case this.SOUNDCNT_LO | 1: 407 | case this.SOUNDCNT_X: 408 | case this.IF: 409 | case this.IME: 410 | break; 411 | case this.SOUNDBIAS | 1: 412 | this.STUB_REG("sound", offset); 413 | break; 414 | case this.HALTCNT: 415 | value &= 0x80; 416 | if (!value) { 417 | this.core.irq.halt(); 418 | } else { 419 | this.core.STUB("Stop"); 420 | } 421 | return; 422 | default: 423 | this.STUB_REG("8-bit I/O", offset); 424 | break; 425 | } 426 | 427 | if (offset & 1) { 428 | value <<= 8; 429 | value |= this.registers[offset >> 1] & 0x00ff; 430 | } else { 431 | value &= 0x00ff; 432 | value |= this.registers[offset >> 1] & 0xff00; 433 | } 434 | this.store16(offset & 0xffffffe, value); 435 | } 436 | store16(offset, value) { 437 | switch (offset) { 438 | // Video 439 | case this.DISPCNT: 440 | this.video.renderPath.writeDisplayControl(value); 441 | break; 442 | case this.DISPSTAT: 443 | value &= this.video.DISPSTAT_MASK; 444 | this.video.writeDisplayStat(value); 445 | break; 446 | case this.BG0CNT: 447 | this.video.renderPath.writeBackgroundControl(0, value); 448 | break; 449 | case this.BG1CNT: 450 | this.video.renderPath.writeBackgroundControl(1, value); 451 | break; 452 | case this.BG2CNT: 453 | this.video.renderPath.writeBackgroundControl(2, value); 454 | break; 455 | case this.BG3CNT: 456 | this.video.renderPath.writeBackgroundControl(3, value); 457 | break; 458 | case this.BG0HOFS: 459 | this.video.renderPath.writeBackgroundHOffset(0, value); 460 | break; 461 | case this.BG0VOFS: 462 | this.video.renderPath.writeBackgroundVOffset(0, value); 463 | break; 464 | case this.BG1HOFS: 465 | this.video.renderPath.writeBackgroundHOffset(1, value); 466 | break; 467 | case this.BG1VOFS: 468 | this.video.renderPath.writeBackgroundVOffset(1, value); 469 | break; 470 | case this.BG2HOFS: 471 | this.video.renderPath.writeBackgroundHOffset(2, value); 472 | break; 473 | case this.BG2VOFS: 474 | this.video.renderPath.writeBackgroundVOffset(2, value); 475 | break; 476 | case this.BG3HOFS: 477 | this.video.renderPath.writeBackgroundHOffset(3, value); 478 | break; 479 | case this.BG3VOFS: 480 | this.video.renderPath.writeBackgroundVOffset(3, value); 481 | break; 482 | case this.BG2X_LO: 483 | this.video.renderPath.writeBackgroundRefX( 484 | 2, 485 | (this.registers[(offset >> 1) | 1] << 16) | value 486 | ); 487 | break; 488 | case this.BG2X_HI: 489 | this.video.renderPath.writeBackgroundRefX( 490 | 2, 491 | this.registers[(offset >> 1) ^ 1] | (value << 16) 492 | ); 493 | break; 494 | case this.BG2Y_LO: 495 | this.video.renderPath.writeBackgroundRefY( 496 | 2, 497 | (this.registers[(offset >> 1) | 1] << 16) | value 498 | ); 499 | break; 500 | case this.BG2Y_HI: 501 | this.video.renderPath.writeBackgroundRefY( 502 | 2, 503 | this.registers[(offset >> 1) ^ 1] | (value << 16) 504 | ); 505 | break; 506 | case this.BG2PA: 507 | this.video.renderPath.writeBackgroundParamA(2, value); 508 | break; 509 | case this.BG2PB: 510 | this.video.renderPath.writeBackgroundParamB(2, value); 511 | break; 512 | case this.BG2PC: 513 | this.video.renderPath.writeBackgroundParamC(2, value); 514 | break; 515 | case this.BG2PD: 516 | this.video.renderPath.writeBackgroundParamD(2, value); 517 | break; 518 | case this.BG3X_LO: 519 | this.video.renderPath.writeBackgroundRefX( 520 | 3, 521 | (this.registers[(offset >> 1) | 1] << 16) | value 522 | ); 523 | break; 524 | case this.BG3X_HI: 525 | this.video.renderPath.writeBackgroundRefX( 526 | 3, 527 | this.registers[(offset >> 1) ^ 1] | (value << 16) 528 | ); 529 | break; 530 | case this.BG3Y_LO: 531 | this.video.renderPath.writeBackgroundRefY( 532 | 3, 533 | (this.registers[(offset >> 1) | 1] << 16) | value 534 | ); 535 | break; 536 | case this.BG3Y_HI: 537 | this.video.renderPath.writeBackgroundRefY( 538 | 3, 539 | this.registers[(offset >> 1) ^ 1] | (value << 16) 540 | ); 541 | break; 542 | case this.BG3PA: 543 | this.video.renderPath.writeBackgroundParamA(3, value); 544 | break; 545 | case this.BG3PB: 546 | this.video.renderPath.writeBackgroundParamB(3, value); 547 | break; 548 | case this.BG3PC: 549 | this.video.renderPath.writeBackgroundParamC(3, value); 550 | break; 551 | case this.BG3PD: 552 | this.video.renderPath.writeBackgroundParamD(3, value); 553 | break; 554 | case this.WIN0H: 555 | this.video.renderPath.writeWin0H(value); 556 | break; 557 | case this.WIN1H: 558 | this.video.renderPath.writeWin1H(value); 559 | break; 560 | case this.WIN0V: 561 | this.video.renderPath.writeWin0V(value); 562 | break; 563 | case this.WIN1V: 564 | this.video.renderPath.writeWin1V(value); 565 | break; 566 | case this.WININ: 567 | value &= 0x3f3f; 568 | this.video.renderPath.writeWinIn(value); 569 | break; 570 | case this.WINOUT: 571 | value &= 0x3f3f; 572 | this.video.renderPath.writeWinOut(value); 573 | break; 574 | case this.BLDCNT: 575 | value &= 0x7fff; 576 | this.video.renderPath.writeBlendControl(value); 577 | break; 578 | case this.BLDALPHA: 579 | value &= 0x1f1f; 580 | this.video.renderPath.writeBlendAlpha(value); 581 | break; 582 | case this.BLDY: 583 | value &= 0x001f; 584 | this.video.renderPath.writeBlendY(value); 585 | break; 586 | case this.MOSAIC: 587 | this.video.renderPath.writeMosaic(value); 588 | break; 589 | 590 | // Sound 591 | case this.SOUND1CNT_LO: 592 | value &= 0x007f; 593 | this.audio.writeSquareChannelSweep(0, value); 594 | break; 595 | case this.SOUND1CNT_HI: 596 | this.audio.writeSquareChannelDLE(0, value); 597 | break; 598 | case this.SOUND1CNT_X: 599 | value &= 0xc7ff; 600 | this.audio.writeSquareChannelFC(0, value); 601 | value &= ~0x8000; 602 | break; 603 | case this.SOUND2CNT_LO: 604 | this.audio.writeSquareChannelDLE(1, value); 605 | break; 606 | case this.SOUND2CNT_HI: 607 | value &= 0xc7ff; 608 | this.audio.writeSquareChannelFC(1, value); 609 | value &= ~0x8000; 610 | break; 611 | case this.SOUND3CNT_LO: 612 | value &= 0x00e0; 613 | this.audio.writeChannel3Lo(value); 614 | break; 615 | case this.SOUND3CNT_HI: 616 | value &= 0xe0ff; 617 | this.audio.writeChannel3Hi(value); 618 | break; 619 | case this.SOUND3CNT_X: 620 | value &= 0xc7ff; 621 | this.audio.writeChannel3X(value); 622 | value &= ~0x8000; 623 | break; 624 | case this.SOUND4CNT_LO: 625 | value &= 0xff3f; 626 | this.audio.writeChannel4LE(value); 627 | break; 628 | case this.SOUND4CNT_HI: 629 | value &= 0xc0ff; 630 | this.audio.writeChannel4FC(value); 631 | value &= ~0x8000; 632 | break; 633 | case this.SOUNDCNT_LO: 634 | value &= 0xff77; 635 | this.audio.writeSoundControlLo(value); 636 | break; 637 | case this.SOUNDCNT_HI: 638 | value &= 0xff0f; 639 | this.audio.writeSoundControlHi(value); 640 | break; 641 | case this.SOUNDCNT_X: 642 | value &= 0x0080; 643 | this.audio.writeEnable(value); 644 | break; 645 | case this.WAVE_RAM0_LO: 646 | case this.WAVE_RAM0_HI: 647 | case this.WAVE_RAM1_LO: 648 | case this.WAVE_RAM1_HI: 649 | case this.WAVE_RAM2_LO: 650 | case this.WAVE_RAM2_HI: 651 | case this.WAVE_RAM3_LO: 652 | case this.WAVE_RAM3_HI: 653 | this.audio.writeWaveData(offset - this.WAVE_RAM0_LO, value, 2); 654 | break; 655 | 656 | // DMA 657 | case this.DMA0SAD_LO: 658 | case this.DMA0DAD_LO: 659 | case this.DMA1SAD_LO: 660 | case this.DMA1DAD_LO: 661 | case this.DMA2SAD_LO: 662 | case this.DMA2DAD_LO: 663 | case this.DMA3SAD_LO: 664 | case this.DMA3DAD_LO: 665 | this.store32( 666 | offset, 667 | (this.registers[(offset >> 1) + 1] << 16) | value 668 | ); 669 | return; 670 | 671 | case this.DMA0SAD_HI: 672 | case this.DMA0DAD_HI: 673 | case this.DMA1SAD_HI: 674 | case this.DMA1DAD_HI: 675 | case this.DMA2SAD_HI: 676 | case this.DMA2DAD_HI: 677 | case this.DMA3SAD_HI: 678 | case this.DMA3DAD_HI: 679 | this.store32( 680 | offset - 2, 681 | this.registers[(offset >> 1) - 1] | (value << 16) 682 | ); 683 | return; 684 | 685 | case this.DMA0CNT_LO: 686 | this.cpu.irq.dmaSetWordCount(0, value); 687 | break; 688 | case this.DMA0CNT_HI: 689 | // The DMA registers need to set the values before writing the control, as writing the 690 | // control can synchronously trigger a DMA transfer 691 | this.registers[offset >> 1] = value & 0xffe0; 692 | this.cpu.irq.dmaWriteControl(0, value); 693 | return; 694 | case this.DMA1CNT_LO: 695 | this.cpu.irq.dmaSetWordCount(1, value); 696 | break; 697 | case this.DMA1CNT_HI: 698 | this.registers[offset >> 1] = value & 0xffe0; 699 | this.cpu.irq.dmaWriteControl(1, value); 700 | return; 701 | case this.DMA2CNT_LO: 702 | this.cpu.irq.dmaSetWordCount(2, value); 703 | break; 704 | case this.DMA2CNT_HI: 705 | this.registers[offset >> 1] = value & 0xffe0; 706 | this.cpu.irq.dmaWriteControl(2, value); 707 | return; 708 | case this.DMA3CNT_LO: 709 | this.cpu.irq.dmaSetWordCount(3, value); 710 | break; 711 | case this.DMA3CNT_HI: 712 | this.registers[offset >> 1] = value & 0xffe0; 713 | this.cpu.irq.dmaWriteControl(3, value); 714 | return; 715 | 716 | // Timers 717 | case this.TM0CNT_LO: 718 | this.cpu.irq.timerSetReload(0, value); 719 | return; 720 | case this.TM1CNT_LO: 721 | this.cpu.irq.timerSetReload(1, value); 722 | return; 723 | case this.TM2CNT_LO: 724 | this.cpu.irq.timerSetReload(2, value); 725 | return; 726 | case this.TM3CNT_LO: 727 | this.cpu.irq.timerSetReload(3, value); 728 | return; 729 | 730 | case this.TM0CNT_HI: 731 | value &= 0x00c7; 732 | this.cpu.irq.timerWriteControl(0, value); 733 | break; 734 | case this.TM1CNT_HI: 735 | value &= 0x00c7; 736 | this.cpu.irq.timerWriteControl(1, value); 737 | break; 738 | case this.TM2CNT_HI: 739 | value &= 0x00c7; 740 | this.cpu.irq.timerWriteControl(2, value); 741 | break; 742 | case this.TM3CNT_HI: 743 | value &= 0x00c7; 744 | this.cpu.irq.timerWriteControl(3, value); 745 | break; 746 | 747 | // SIO 748 | case this.SIOMULTI0: 749 | case this.SIOMULTI1: 750 | case this.SIOMULTI2: 751 | case this.SIOMULTI3: 752 | case this.SIODATA8: 753 | this.STUB_REG("SIO", offset); 754 | break; 755 | case this.RCNT: 756 | this.sio.setMode( 757 | ((value >> 12) & 0xc) | 758 | ((this.registers[this.SIOCNT >> 1] >> 12) & 0x3) 759 | ); 760 | this.sio.writeRCNT(value); 761 | break; 762 | case this.SIOCNT: 763 | this.sio.setMode( 764 | ((value >> 12) & 0x3) | 765 | ((this.registers[this.RCNT >> 1] >> 12) & 0xc) 766 | ); 767 | this.sio.writeSIOCNT(value); 768 | return; 769 | case this.JOYCNT: 770 | case this.JOYSTAT: 771 | this.STUB_REG("JOY", offset); 772 | break; 773 | 774 | // Misc 775 | case this.IE: 776 | value &= 0x3fff; 777 | this.cpu.irq.setInterruptsEnabled(value); 778 | break; 779 | case this.IF: 780 | this.cpu.irq.dismissIRQs(value); 781 | return; 782 | case this.WAITCNT: 783 | value &= 0xdfff; 784 | this.cpu.mmu.adjustTimings(value); 785 | break; 786 | case this.IME: 787 | value &= 0x0001; 788 | this.cpu.irq.masterEnable(value); 789 | break; 790 | default: 791 | this.STUB_REG("I/O", offset); 792 | } 793 | this.registers[offset >> 1] = value; 794 | } 795 | store32(offset, value) { 796 | switch (offset) { 797 | case this.BG2X_LO: 798 | value &= 0x0fffffff; 799 | this.video.renderPath.writeBackgroundRefX(2, value); 800 | break; 801 | case this.BG2Y_LO: 802 | value &= 0x0fffffff; 803 | this.video.renderPath.writeBackgroundRefY(2, value); 804 | break; 805 | case this.BG3X_LO: 806 | value &= 0x0fffffff; 807 | this.video.renderPath.writeBackgroundRefX(3, value); 808 | break; 809 | case this.BG3Y_LO: 810 | value &= 0x0fffffff; 811 | this.video.renderPath.writeBackgroundRefY(3, value); 812 | break; 813 | case this.DMA0SAD_LO: 814 | this.cpu.irq.dmaSetSourceAddress(0, value); 815 | break; 816 | case this.DMA0DAD_LO: 817 | this.cpu.irq.dmaSetDestAddress(0, value); 818 | break; 819 | case this.DMA1SAD_LO: 820 | this.cpu.irq.dmaSetSourceAddress(1, value); 821 | break; 822 | case this.DMA1DAD_LO: 823 | this.cpu.irq.dmaSetDestAddress(1, value); 824 | break; 825 | case this.DMA2SAD_LO: 826 | this.cpu.irq.dmaSetSourceAddress(2, value); 827 | break; 828 | case this.DMA2DAD_LO: 829 | this.cpu.irq.dmaSetDestAddress(2, value); 830 | break; 831 | case this.DMA3SAD_LO: 832 | this.cpu.irq.dmaSetSourceAddress(3, value); 833 | break; 834 | case this.DMA3DAD_LO: 835 | this.cpu.irq.dmaSetDestAddress(3, value); 836 | break; 837 | case this.FIFO_A_LO: 838 | this.audio.appendToFifoA(value); 839 | return; 840 | case this.FIFO_B_LO: 841 | this.audio.appendToFifoB(value); 842 | return; 843 | 844 | // High bits of this write should be ignored 845 | case this.IME: 846 | this.store16(offset, value & 0xffff); 847 | return; 848 | case this.JOY_RECV: 849 | case this.JOY_TRANS: 850 | this.STUB_REG("JOY", offset); 851 | return; 852 | default: 853 | this.store16(offset, value & 0xffff); 854 | this.store16(offset | 2, value >>> 16); 855 | return; 856 | } 857 | 858 | this.registers[offset >> 1] = value & 0xffff; 859 | this.registers[(offset >> 1) + 1] = value >>> 16; 860 | } 861 | invalidatePage(address) {} 862 | STUB_REG(type, offset) { 863 | this.core.STUB( 864 | "Unimplemented " + type + " register write: " + offset.toString(16) 865 | ); 866 | } 867 | } 868 | -------------------------------------------------------------------------------- /js/mmu.js: -------------------------------------------------------------------------------- 1 | class MemoryView { 2 | constructor(memory, offset) { 3 | // this.inherit(); 4 | this.buffer = memory; 5 | this.view = new DataView( 6 | this.buffer, 7 | typeof offset === "number" ? offset : 0 8 | ); 9 | this.mask = memory.byteLength - 1; 10 | this.resetMask(); 11 | } 12 | resetMask() { 13 | this.mask8 = this.mask & 0xffffffff; 14 | this.mask16 = this.mask & 0xfffffffe; 15 | this.mask32 = this.mask & 0xfffffffc; 16 | } 17 | load8(offset) { 18 | return this.view.getInt8(offset & this.mask8); 19 | } 20 | load16(offset) { 21 | // Unaligned 16-bit loads are unpredictable...let's just pretend they work 22 | return this.view.getInt16(offset & this.mask, true); 23 | } 24 | loadU8(offset) { 25 | return this.view.getUint8(offset & this.mask8); 26 | } 27 | loadU16(offset) { 28 | // Unaligned 16-bit loads are unpredictable...let's just pretend they work 29 | return this.view.getUint16(offset & this.mask, true); 30 | } 31 | load32(offset) { 32 | // Unaligned 32-bit loads are "rotated" so they make some semblance of sense 33 | var rotate = (offset & 3) << 3; 34 | var mem = this.view.getInt32(offset & this.mask32, true); 35 | return (mem >>> rotate) | (mem << (32 - rotate)); 36 | } 37 | store8(offset, value) { 38 | this.view.setInt8(offset & this.mask8, value); 39 | } 40 | store16(offset, value) { 41 | this.view.setInt16(offset & this.mask16, value, true); 42 | } 43 | store32(offset, value) { 44 | this.view.setInt32(offset & this.mask32, value, true); 45 | } 46 | invalidatePage(address) {} 47 | replaceData(memory, offset) { 48 | this.buffer = memory; 49 | this.view = new DataView( 50 | this.buffer, 51 | typeof offset === "number" ? offset : 0 52 | ); 53 | if (this.icache) { 54 | this.icache = new Array(this.icache.length); 55 | } 56 | } 57 | } 58 | class MemoryBlock extends MemoryView { 59 | constructor(size, cacheBits) { 60 | super(new ArrayBuffer(size)); 61 | this.ICACHE_PAGE_BITS = cacheBits; 62 | this.PAGE_MASK = (2 << this.ICACHE_PAGE_BITS) - 1; 63 | this.icache = new Array(size >> (this.ICACHE_PAGE_BITS + 1)); 64 | } 65 | invalidatePage(address) { 66 | var page = this.icache[(address & this.mask) >> this.ICACHE_PAGE_BITS]; 67 | if (page) { 68 | page.invalid = true; 69 | } 70 | } 71 | } 72 | 73 | class ROMView extends MemoryView { 74 | constructor(rom, offset) { 75 | super(rom, offset); 76 | this.ICACHE_PAGE_BITS = 10; 77 | this.PAGE_MASK = (2 << this.ICACHE_PAGE_BITS) - 1; 78 | this.icache = new Array(rom.byteLength >> (this.ICACHE_PAGE_BITS + 1)); 79 | this.mask = 0x01ffffff; 80 | this.resetMask(); 81 | } 82 | store8(offset, value) {} 83 | store16(offset, value) { 84 | if (offset < 0xca && offset >= 0xc4) { 85 | if (!this.gpio) { 86 | this.gpio = this.mmu.allocGPIO(this); 87 | } 88 | this.gpio.store16(offset, value); 89 | } 90 | } 91 | store32(offset, value) { 92 | if (offset < 0xca && offset >= 0xc4) { 93 | if (!this.gpio) { 94 | this.gpio = this.mmu.allocGPIO(this); 95 | } 96 | this.gpio.store32(offset, value); 97 | } 98 | } 99 | } 100 | 101 | class BIOSView extends MemoryView { 102 | constructor(rom, offset) { 103 | super(rom, offset); 104 | 105 | this.ICACHE_PAGE_BITS = 16; 106 | this.PAGE_MASK = (2 << this.ICACHE_PAGE_BITS) - 1; 107 | this.icache = new Array(1); 108 | } 109 | load8(offset) { 110 | if (offset >= this.buffer.byteLength) { 111 | return -1; 112 | } 113 | return this.view.getInt8(offset); 114 | } 115 | load16(offset) { 116 | if (offset >= this.buffer.byteLength) { 117 | return -1; 118 | } 119 | return this.view.getInt16(offset, true); 120 | } 121 | loadU8(offset) { 122 | if (offset >= this.buffer.byteLength) { 123 | return -1; 124 | } 125 | return this.view.getUint8(offset); 126 | } 127 | loadU16(offset) { 128 | if (offset >= this.buffer.byteLength) { 129 | return -1; 130 | } 131 | return this.view.getUint16(offset, true); 132 | } 133 | load32(offset) { 134 | if (offset >= this.buffer.byteLength) { 135 | return -1; 136 | } 137 | return this.view.getInt32(offset, true); 138 | } 139 | store8(offset, value) {} 140 | store16(offset, value) {} 141 | store32(offset, value) {} 142 | } 143 | 144 | class BadMemory { 145 | constructor(mmu, cpu) { 146 | // this.inherit(); 147 | this.cpu = cpu; 148 | this.mmu = mmu; 149 | } 150 | load8(offset) { 151 | return this.mmu.load8( 152 | this.cpu.gprs[this.cpu.PC] - 153 | this.cpu.instructionWidth + 154 | (offset & 0x3) 155 | ); 156 | } 157 | load16(offset) { 158 | return this.mmu.load16( 159 | this.cpu.gprs[this.cpu.PC] - 160 | this.cpu.instructionWidth + 161 | (offset & 0x2) 162 | ); 163 | } 164 | loadU8(offset) { 165 | return this.mmu.loadU8( 166 | this.cpu.gprs[this.cpu.PC] - 167 | this.cpu.instructionWidth + 168 | (offset & 0x3) 169 | ); 170 | } 171 | loadU16(offset) { 172 | return this.mmu.loadU16( 173 | this.cpu.gprs[this.cpu.PC] - 174 | this.cpu.instructionWidth + 175 | (offset & 0x2) 176 | ); 177 | } 178 | load32(offset) { 179 | if (this.cpu.execMode == this.cpu.MODE_ARM) { 180 | return this.mmu.load32( 181 | this.cpu.gprs[this.cpu.gprs.PC] - this.cpu.instructionWidth 182 | ); 183 | } else { 184 | var halfword = this.mmu.loadU16( 185 | this.cpu.gprs[this.cpu.PC] - this.cpu.instructionWidth 186 | ); 187 | return halfword | (halfword << 16); 188 | } 189 | } 190 | store8(offset, value) {} 191 | store16(offset, value) {} 192 | store32(offset, value) {} 193 | invalidatePage(address) {} 194 | } 195 | 196 | class GameBoyAdvanceMMU { 197 | constructor() { 198 | // this.inherit(); 199 | this.REGION_BIOS = 0x0; 200 | this.REGION_WORKING_RAM = 0x2; 201 | this.REGION_WORKING_IRAM = 0x3; 202 | this.REGION_IO = 0x4; 203 | this.REGION_PALETTE_RAM = 0x5; 204 | this.REGION_VRAM = 0x6; 205 | this.REGION_OAM = 0x7; 206 | this.REGION_CART0 = 0x8; 207 | this.REGION_CART1 = 0xa; 208 | this.REGION_CART2 = 0xc; 209 | this.REGION_CART_SRAM = 0xe; 210 | 211 | this.BASE_BIOS = 0x00000000; 212 | this.BASE_WORKING_RAM = 0x02000000; 213 | this.BASE_WORKING_IRAM = 0x03000000; 214 | this.BASE_IO = 0x04000000; 215 | this.BASE_PALETTE_RAM = 0x05000000; 216 | this.BASE_VRAM = 0x06000000; 217 | this.BASE_OAM = 0x07000000; 218 | this.BASE_CART0 = 0x08000000; 219 | this.BASE_CART1 = 0x0a000000; 220 | this.BASE_CART2 = 0x0c000000; 221 | this.BASE_CART_SRAM = 0x0e000000; 222 | 223 | this.BASE_MASK = 0x0f000000; 224 | this.BASE_OFFSET = 24; 225 | this.OFFSET_MASK = 0x00ffffff; 226 | 227 | this.SIZE_BIOS = 0x00004000; 228 | this.SIZE_WORKING_RAM = 0x00040000; 229 | this.SIZE_WORKING_IRAM = 0x00008000; 230 | this.SIZE_IO = 0x00000400; 231 | this.SIZE_PALETTE_RAM = 0x00000400; 232 | this.SIZE_VRAM = 0x00018000; 233 | this.SIZE_OAM = 0x00000400; 234 | this.SIZE_CART0 = 0x02000000; 235 | this.SIZE_CART1 = 0x02000000; 236 | this.SIZE_CART2 = 0x02000000; 237 | this.SIZE_CART_SRAM = 0x00008000; 238 | this.SIZE_CART_FLASH512 = 0x00010000; 239 | this.SIZE_CART_FLASH1M = 0x00020000; 240 | this.SIZE_CART_EEPROM = 0x00002000; 241 | 242 | this.DMA_TIMING_NOW = 0; 243 | this.DMA_TIMING_VBLANK = 1; 244 | this.DMA_TIMING_HBLANK = 2; 245 | this.DMA_TIMING_CUSTOM = 3; 246 | 247 | this.DMA_INCREMENT = 0; 248 | this.DMA_DECREMENT = 1; 249 | this.DMA_FIXED = 2; 250 | this.DMA_INCREMENT_RELOAD = 3; 251 | 252 | this.DMA_OFFSET = [1, -1, 0, 1]; 253 | 254 | this.WAITSTATES = [0, 0, 2, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4]; 255 | this.WAITSTATES_32 = [0, 0, 5, 0, 0, 1, 0, 1, 7, 7, 9, 9, 13, 13, 8]; 256 | this.WAITSTATES_SEQ = [0, 0, 2, 0, 0, 0, 0, 0, 2, 2, 4, 4, 8, 8, 4]; 257 | this.WAITSTATES_SEQ_32 = [ 258 | 0, 259 | 0, 260 | 5, 261 | 0, 262 | 0, 263 | 1, 264 | 0, 265 | 1, 266 | 5, 267 | 5, 268 | 9, 269 | 9, 270 | 17, 271 | 17, 272 | 8 273 | ]; 274 | this.NULLWAIT = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 275 | 276 | for (var i = 15; i < 256; ++i) { 277 | this.WAITSTATES[i] = 0; 278 | this.WAITSTATES_32[i] = 0; 279 | this.WAITSTATES_SEQ[i] = 0; 280 | this.WAITSTATES_SEQ_32[i] = 0; 281 | this.NULLWAIT[i] = 0; 282 | } 283 | 284 | this.ROM_WS = [4, 3, 2, 8]; 285 | this.ROM_WS_SEQ = [ 286 | [2, 1], 287 | [4, 1], 288 | [8, 1] 289 | ]; 290 | 291 | this.ICACHE_PAGE_BITS = 8; 292 | this.PAGE_MASK = (2 << this.ICACHE_PAGE_BITS) - 1; 293 | 294 | this.bios = null; 295 | } 296 | mmap(region, object) { 297 | this.memory[region] = object; 298 | } 299 | clear() { 300 | this.badMemory = new BadMemory(this, this.cpu); 301 | this.memory = [ 302 | this.bios, 303 | this.badMemory, 304 | new MemoryBlock(this.SIZE_WORKING_RAM, 9), 305 | new MemoryBlock(this.SIZE_WORKING_IRAM, 7), 306 | null, // This is owned by GameBoyAdvanceIO 307 | null, // This is owned by GameBoyAdvancePalette 308 | null, // This is owned by GameBoyAdvanceVRAM 309 | null, // This is owned by GameBoyAdvanceOAM 310 | this.badMemory, 311 | this.badMemory, 312 | this.badMemory, 313 | this.badMemory, 314 | this.badMemory, 315 | this.badMemory, 316 | this.badMemory, 317 | this.badMemory // Unused 318 | ]; 319 | for (var i = 16; i < 256; ++i) { 320 | this.memory[i] = this.badMemory; 321 | } 322 | 323 | this.waitstates = this.WAITSTATES.slice(0); 324 | this.waitstatesSeq = this.WAITSTATES_SEQ.slice(0); 325 | this.waitstates32 = this.WAITSTATES_32.slice(0); 326 | this.waitstatesSeq32 = this.WAITSTATES_SEQ_32.slice(0); 327 | this.waitstatesPrefetch = this.WAITSTATES_SEQ.slice(0); 328 | this.waitstatesPrefetch32 = this.WAITSTATES_SEQ_32.slice(0); 329 | 330 | this.cart = null; 331 | this.save = null; 332 | 333 | this.DMA_REGISTER = [ 334 | this.core.io.DMA0CNT_HI >> 1, 335 | this.core.io.DMA1CNT_HI >> 1, 336 | this.core.io.DMA2CNT_HI >> 1, 337 | this.core.io.DMA3CNT_HI >> 1 338 | ]; 339 | } 340 | freeze() { 341 | return { 342 | ram: Serializer.prefix(this.memory[this.REGION_WORKING_RAM].buffer), 343 | iram: Serializer.prefix( 344 | this.memory[this.REGION_WORKING_IRAM].buffer 345 | ) 346 | }; 347 | } 348 | defrost(frost) { 349 | this.memory[this.REGION_WORKING_RAM].replaceData(frost.ram); 350 | this.memory[this.REGION_WORKING_IRAM].replaceData(frost.iram); 351 | } 352 | loadBios(bios, real) { 353 | this.bios = new BIOSView(bios); 354 | this.bios.real = !!real; 355 | } 356 | loadRom(rom, process) { 357 | var cart = { 358 | title: null, 359 | code: null, 360 | maker: null, 361 | memory: rom, 362 | saveType: null 363 | }; 364 | 365 | var lo = new ROMView(rom); 366 | if (lo.view.getUint8(0xb2) != 0x96) { 367 | // Not a valid ROM 368 | return null; 369 | } 370 | lo.mmu = this; // Needed for GPIO 371 | this.memory[this.REGION_CART0] = lo; 372 | this.memory[this.REGION_CART1] = lo; 373 | this.memory[this.REGION_CART2] = lo; 374 | 375 | if (rom.byteLength > 0x01000000) { 376 | var hi = new ROMView(rom, 0x01000000); 377 | this.memory[this.REGION_CART0 + 1] = hi; 378 | this.memory[this.REGION_CART1 + 1] = hi; 379 | this.memory[this.REGION_CART2 + 1] = hi; 380 | } 381 | 382 | if (process) { 383 | var name = ""; 384 | for (var i = 0; i < 12; ++i) { 385 | var c = lo.loadU8(i + 0xa0); 386 | if (!c) { 387 | break; 388 | } 389 | name += String.fromCharCode(c); 390 | } 391 | cart.title = name; 392 | 393 | var code = ""; 394 | for (var i = 0; i < 4; ++i) { 395 | var c = lo.loadU8(i + 0xac); 396 | if (!c) { 397 | break; 398 | } 399 | code += String.fromCharCode(c); 400 | } 401 | cart.code = code; 402 | 403 | var maker = ""; 404 | for (var i = 0; i < 2; ++i) { 405 | var c = lo.loadU8(i + 0xb0); 406 | if (!c) { 407 | break; 408 | } 409 | maker += String.fromCharCode(c); 410 | } 411 | cart.maker = maker; 412 | 413 | // Find savedata type 414 | var state = ""; 415 | var next; 416 | var terminal = false; 417 | for (var i = 0xe4; i < rom.byteLength && !terminal; ++i) { 418 | next = String.fromCharCode(lo.loadU8(i)); 419 | state += next; 420 | switch (state) { 421 | case "F": 422 | case "FL": 423 | case "FLA": 424 | case "FLAS": 425 | case "FLASH": 426 | case "FLASH_": 427 | case "FLASH5": 428 | case "FLASH51": 429 | case "FLASH512": 430 | case "FLASH512_": 431 | case "FLASH1": 432 | case "FLASH1M": 433 | case "FLASH1M_": 434 | case "S": 435 | case "SR": 436 | case "SRA": 437 | case "SRAM": 438 | case "SRAM_": 439 | case "E": 440 | case "EE": 441 | case "EEP": 442 | case "EEPR": 443 | case "EEPRO": 444 | case "EEPROM": 445 | case "EEPROM_": 446 | break; 447 | case "FLASH_V": 448 | case "FLASH512_V": 449 | case "FLASH1M_V": 450 | case "SRAM_V": 451 | case "EEPROM_V": 452 | terminal = true; 453 | break; 454 | default: 455 | state = next; 456 | break; 457 | } 458 | } 459 | if (terminal) { 460 | cart.saveType = state; 461 | switch (state) { 462 | case "FLASH_V": 463 | case "FLASH512_V": 464 | this.save = this.memory[ 465 | this.REGION_CART_SRAM 466 | ] = new FlashSavedata(this.SIZE_CART_FLASH512); 467 | break; 468 | case "FLASH1M_V": 469 | this.save = this.memory[ 470 | this.REGION_CART_SRAM 471 | ] = new FlashSavedata(this.SIZE_CART_FLASH1M); 472 | break; 473 | case "SRAM_V": 474 | this.save = this.memory[ 475 | this.REGION_CART_SRAM 476 | ] = new SRAMSavedata(this.SIZE_CART_SRAM); 477 | break; 478 | case "EEPROM_V": 479 | this.save = this.memory[ 480 | this.REGION_CART2 + 1 481 | ] = new EEPROMSavedata(this.SIZE_CART_EEPROM, this); 482 | break; 483 | } 484 | } 485 | if (!this.save) { 486 | // Assume we have SRAM 487 | this.save = this.memory[ 488 | this.REGION_CART_SRAM 489 | ] = new SRAMSavedata(this.SIZE_CART_SRAM); 490 | } 491 | } 492 | 493 | this.cart = cart; 494 | return cart; 495 | } 496 | loadSavedata(save) { 497 | this.save.replaceData(save); 498 | } 499 | load8(offset) { 500 | return this.memory[offset >>> this.BASE_OFFSET].load8( 501 | offset & 0x00ffffff 502 | ); 503 | } 504 | load16(offset) { 505 | return this.memory[offset >>> this.BASE_OFFSET].load16( 506 | offset & 0x00ffffff 507 | ); 508 | } 509 | load32(offset) { 510 | return this.memory[offset >>> this.BASE_OFFSET].load32( 511 | offset & 0x00ffffff 512 | ); 513 | } 514 | loadU8(offset) { 515 | return this.memory[offset >>> this.BASE_OFFSET].loadU8( 516 | offset & 0x00ffffff 517 | ); 518 | } 519 | loadU16(offset) { 520 | return this.memory[offset >>> this.BASE_OFFSET].loadU16( 521 | offset & 0x00ffffff 522 | ); 523 | } 524 | store8(offset, value) { 525 | var maskedOffset = offset & 0x00ffffff; 526 | var memory = this.memory[offset >>> this.BASE_OFFSET]; 527 | memory.store8(maskedOffset, value); 528 | memory.invalidatePage(maskedOffset); 529 | } 530 | store16(offset, value) { 531 | var maskedOffset = offset & 0x00fffffe; 532 | var memory = this.memory[offset >>> this.BASE_OFFSET]; 533 | memory.store16(maskedOffset, value); 534 | memory.invalidatePage(maskedOffset); 535 | } 536 | store32(offset, value) { 537 | var maskedOffset = offset & 0x00fffffc; 538 | var memory = this.memory[offset >>> this.BASE_OFFSET]; 539 | memory.store32(maskedOffset, value); 540 | memory.invalidatePage(maskedOffset); 541 | memory.invalidatePage(maskedOffset + 2); 542 | } 543 | waitPrefetch(memory) { 544 | this.cpu.cycles += 545 | 1 + this.waitstatesPrefetch[memory >>> this.BASE_OFFSET]; 546 | } 547 | waitPrefetch32(memory) { 548 | this.cpu.cycles += 549 | 1 + this.waitstatesPrefetch32[memory >>> this.BASE_OFFSET]; 550 | } 551 | wait(memory) { 552 | this.cpu.cycles += 1 + this.waitstates[memory >>> this.BASE_OFFSET]; 553 | } 554 | wait32(memory) { 555 | this.cpu.cycles += 1 + this.waitstates32[memory >>> this.BASE_OFFSET]; 556 | } 557 | waitSeq(memory) { 558 | this.cpu.cycles += 1 + this.waitstatesSeq[memory >>> this.BASE_OFFSET]; 559 | } 560 | waitSeq32(memory) { 561 | this.cpu.cycles += 562 | 1 + this.waitstatesSeq32[memory >>> this.BASE_OFFSET]; 563 | } 564 | waitMul(rs) { 565 | if (rs & (0xffffff00 == 0xffffff00) || !(rs & 0xffffff00)) { 566 | this.cpu.cycles += 1; 567 | } else if (rs & (0xffff0000 == 0xffff0000) || !(rs & 0xffff0000)) { 568 | this.cpu.cycles += 2; 569 | } else if (rs & (0xff000000 == 0xff000000) || !(rs & 0xff000000)) { 570 | this.cpu.cycles += 3; 571 | } else { 572 | this.cpu.cycles += 4; 573 | } 574 | } 575 | waitMulti32(memory, seq) { 576 | this.cpu.cycles += 1 + this.waitstates32[memory >>> this.BASE_OFFSET]; 577 | this.cpu.cycles += 578 | (1 + this.waitstatesSeq32[memory >>> this.BASE_OFFSET]) * (seq - 1); 579 | } 580 | addressToPage(region, address) { 581 | return address >> this.memory[region].ICACHE_PAGE_BITS; 582 | } 583 | accessPage(region, pageId) { 584 | var memory = this.memory[region]; 585 | var page = memory.icache[pageId]; 586 | if (!page || page.invalid) { 587 | page = { 588 | thumb: new Array(1 << memory.ICACHE_PAGE_BITS), 589 | arm: new Array(1 << (memory.ICACHE_PAGE_BITS - 1)), 590 | invalid: false 591 | }; 592 | memory.icache[pageId] = page; 593 | } 594 | return page; 595 | } 596 | scheduleDma(number, info) { 597 | switch (info.timing) { 598 | case this.DMA_TIMING_NOW: 599 | this.serviceDma(number, info); 600 | break; 601 | case this.DMA_TIMING_HBLANK: 602 | // Handled implicitly 603 | break; 604 | case this.DMA_TIMING_VBLANK: 605 | // Handled implicitly 606 | break; 607 | case this.DMA_TIMING_CUSTOM: 608 | switch (number) { 609 | case 0: 610 | this.core.WARN("Discarding invalid DMA0 scheduling"); 611 | break; 612 | case 1: 613 | case 2: 614 | this.cpu.irq.audio.scheduleFIFODma(number, info); 615 | break; 616 | case 3: 617 | this.cpu.irq.video.scheduleVCaptureDma(dma, info); 618 | break; 619 | } 620 | } 621 | } 622 | runHblankDmas() { 623 | var dma; 624 | for (var i = 0; i < this.cpu.irq.dma.length; ++i) { 625 | dma = this.cpu.irq.dma[i]; 626 | if (dma.enable && dma.timing == this.DMA_TIMING_HBLANK) { 627 | this.serviceDma(i, dma); 628 | } 629 | } 630 | } 631 | runVblankDmas() { 632 | var dma; 633 | for (var i = 0; i < this.cpu.irq.dma.length; ++i) { 634 | dma = this.cpu.irq.dma[i]; 635 | if (dma.enable && dma.timing == this.DMA_TIMING_VBLANK) { 636 | this.serviceDma(i, dma); 637 | } 638 | } 639 | } 640 | serviceDma(number, info) { 641 | if (!info.enable) { 642 | // There was a DMA scheduled that got canceled 643 | return; 644 | } 645 | 646 | var width = info.width; 647 | var sourceOffset = this.DMA_OFFSET[info.srcControl] * width; 648 | var destOffset = this.DMA_OFFSET[info.dstControl] * width; 649 | var wordsRemaining = info.nextCount; 650 | var source = info.nextSource & this.OFFSET_MASK; 651 | var dest = info.nextDest & this.OFFSET_MASK; 652 | var sourceRegion = info.nextSource >>> this.BASE_OFFSET; 653 | var destRegion = info.nextDest >>> this.BASE_OFFSET; 654 | var sourceBlock = this.memory[sourceRegion]; 655 | var destBlock = this.memory[destRegion]; 656 | var sourceView = null; 657 | var destView = null; 658 | var sourceMask = 0xffffffff; 659 | var destMask = 0xffffffff; 660 | var word; 661 | 662 | if (destBlock.ICACHE_PAGE_BITS) { 663 | var endPage = 664 | (dest + wordsRemaining * width) >> destBlock.ICACHE_PAGE_BITS; 665 | for ( 666 | var i = dest >> destBlock.ICACHE_PAGE_BITS; 667 | i <= endPage; 668 | ++i 669 | ) { 670 | destBlock.invalidatePage(i << destBlock.ICACHE_PAGE_BITS); 671 | } 672 | } 673 | 674 | if ( 675 | destRegion == this.REGION_WORKING_RAM || 676 | destRegion == this.REGION_WORKING_IRAM 677 | ) { 678 | destView = destBlock.view; 679 | destMask = destBlock.mask; 680 | } 681 | 682 | if ( 683 | sourceRegion == this.REGION_WORKING_RAM || 684 | sourceRegion == this.REGION_WORKING_IRAM || 685 | sourceRegion == this.REGION_CART0 || 686 | sourceRegion == this.REGION_CART1 687 | ) { 688 | sourceView = sourceBlock.view; 689 | sourceMask = sourceBlock.mask; 690 | } 691 | 692 | if (sourceBlock && destBlock) { 693 | if (sourceView && destView) { 694 | if (width == 4) { 695 | source &= 0xfffffffc; 696 | dest &= 0xfffffffc; 697 | while (wordsRemaining--) { 698 | word = sourceView.getInt32(source & sourceMask); 699 | destView.setInt32(dest & destMask, word); 700 | source += sourceOffset; 701 | dest += destOffset; 702 | } 703 | } else { 704 | while (wordsRemaining--) { 705 | word = sourceView.getUint16(source & sourceMask); 706 | destView.setUint16(dest & destMask, word); 707 | source += sourceOffset; 708 | dest += destOffset; 709 | } 710 | } 711 | } else if (sourceView) { 712 | if (width == 4) { 713 | source &= 0xfffffffc; 714 | dest &= 0xfffffffc; 715 | while (wordsRemaining--) { 716 | word = sourceView.getInt32(source & sourceMask, true); 717 | destBlock.store32(dest, word); 718 | source += sourceOffset; 719 | dest += destOffset; 720 | } 721 | } else { 722 | while (wordsRemaining--) { 723 | word = sourceView.getUint16(source & sourceMask, true); 724 | destBlock.store16(dest, word); 725 | source += sourceOffset; 726 | dest += destOffset; 727 | } 728 | } 729 | } else { 730 | if (width == 4) { 731 | source &= 0xfffffffc; 732 | dest &= 0xfffffffc; 733 | while (wordsRemaining--) { 734 | word = sourceBlock.load32(source); 735 | destBlock.store32(dest, word); 736 | source += sourceOffset; 737 | dest += destOffset; 738 | } 739 | } else { 740 | while (wordsRemaining--) { 741 | word = sourceBlock.loadU16(source); 742 | destBlock.store16(dest, word); 743 | source += sourceOffset; 744 | dest += destOffset; 745 | } 746 | } 747 | } 748 | } else { 749 | this.core.WARN("Invalid DMA"); 750 | } 751 | 752 | if (info.doIrq) { 753 | info.nextIRQ = this.cpu.cycles + 2; 754 | info.nextIRQ += 755 | width == 4 756 | ? this.waitstates32[sourceRegion] + 757 | this.waitstates32[destRegion] 758 | : this.waitstates[sourceRegion] + 759 | this.waitstates[destRegion]; 760 | info.nextIRQ += 761 | (info.count - 1) * 762 | (width == 4 763 | ? this.waitstatesSeq32[sourceRegion] + 764 | this.waitstatesSeq32[destRegion] 765 | : this.waitstatesSeq[sourceRegion] + 766 | this.waitstatesSeq[destRegion]); 767 | } 768 | 769 | info.nextSource = source | (sourceRegion << this.BASE_OFFSET); 770 | info.nextDest = dest | (destRegion << this.BASE_OFFSET); 771 | info.nextCount = wordsRemaining; 772 | 773 | if (!info.repeat) { 774 | info.enable = false; 775 | 776 | // Clear the enable bit in memory 777 | var io = this.memory[this.REGION_IO]; 778 | io.registers[this.DMA_REGISTER[number]] &= 0x7fe0; 779 | } else { 780 | info.nextCount = info.count; 781 | if (info.dstControl == this.DMA_INCREMENT_RELOAD) { 782 | info.nextDest = info.dest; 783 | } 784 | this.scheduleDma(number, info); 785 | } 786 | } 787 | adjustTimings(word) { 788 | var sram = word & 0x0003; 789 | var ws0 = (word & 0x000c) >> 2; 790 | var ws0seq = (word & 0x0010) >> 4; 791 | var ws1 = (word & 0x0060) >> 5; 792 | var ws1seq = (word & 0x0080) >> 7; 793 | var ws2 = (word & 0x0300) >> 8; 794 | var ws2seq = (word & 0x0400) >> 10; 795 | var prefetch = word & 0x4000; 796 | 797 | this.waitstates[this.REGION_CART_SRAM] = this.ROM_WS[sram]; 798 | this.waitstatesSeq[this.REGION_CART_SRAM] = this.ROM_WS[sram]; 799 | this.waitstates32[this.REGION_CART_SRAM] = this.ROM_WS[sram]; 800 | this.waitstatesSeq32[this.REGION_CART_SRAM] = this.ROM_WS[sram]; 801 | 802 | this.waitstates[this.REGION_CART0] = this.waitstates[ 803 | this.REGION_CART0 + 1 804 | ] = this.ROM_WS[ws0]; 805 | this.waitstates[this.REGION_CART1] = this.waitstates[ 806 | this.REGION_CART1 + 1 807 | ] = this.ROM_WS[ws1]; 808 | this.waitstates[this.REGION_CART2] = this.waitstates[ 809 | this.REGION_CART2 + 1 810 | ] = this.ROM_WS[ws2]; 811 | 812 | this.waitstatesSeq[this.REGION_CART0] = this.waitstatesSeq[ 813 | this.REGION_CART0 + 1 814 | ] = this.ROM_WS_SEQ[0][ws0seq]; 815 | this.waitstatesSeq[this.REGION_CART1] = this.waitstatesSeq[ 816 | this.REGION_CART1 + 1 817 | ] = this.ROM_WS_SEQ[1][ws1seq]; 818 | this.waitstatesSeq[this.REGION_CART2] = this.waitstatesSeq[ 819 | this.REGION_CART2 + 1 820 | ] = this.ROM_WS_SEQ[2][ws2seq]; 821 | 822 | this.waitstates32[this.REGION_CART0] = this.waitstates32[ 823 | this.REGION_CART0 + 1 824 | ] = 825 | this.waitstates[this.REGION_CART0] + 826 | 1 + 827 | this.waitstatesSeq[this.REGION_CART0]; 828 | this.waitstates32[this.REGION_CART1] = this.waitstates32[ 829 | this.REGION_CART1 + 1 830 | ] = 831 | this.waitstates[this.REGION_CART1] + 832 | 1 + 833 | this.waitstatesSeq[this.REGION_CART1]; 834 | this.waitstates32[this.REGION_CART2] = this.waitstates32[ 835 | this.REGION_CART2 + 1 836 | ] = 837 | this.waitstates[this.REGION_CART2] + 838 | 1 + 839 | this.waitstatesSeq[this.REGION_CART2]; 840 | 841 | this.waitstatesSeq32[this.REGION_CART0] = this.waitstatesSeq32[ 842 | this.REGION_CART0 + 1 843 | ] = 2 * this.waitstatesSeq[this.REGION_CART0] + 1; 844 | this.waitstatesSeq32[this.REGION_CART1] = this.waitstatesSeq32[ 845 | this.REGION_CART1 + 1 846 | ] = 2 * this.waitstatesSeq[this.REGION_CART1] + 1; 847 | this.waitstatesSeq32[this.REGION_CART2] = this.waitstatesSeq32[ 848 | this.REGION_CART2 + 1 849 | ] = 2 * this.waitstatesSeq[this.REGION_CART2] + 1; 850 | 851 | if (prefetch) { 852 | this.waitstatesPrefetch[ 853 | this.REGION_CART0 854 | ] = this.waitstatesPrefetch[this.REGION_CART0 + 1] = 0; 855 | this.waitstatesPrefetch[ 856 | this.REGION_CART1 857 | ] = this.waitstatesPrefetch[this.REGION_CART1 + 1] = 0; 858 | this.waitstatesPrefetch[ 859 | this.REGION_CART2 860 | ] = this.waitstatesPrefetch[this.REGION_CART2 + 1] = 0; 861 | 862 | this.waitstatesPrefetch32[ 863 | this.REGION_CART0 864 | ] = this.waitstatesPrefetch32[this.REGION_CART0 + 1] = 0; 865 | this.waitstatesPrefetch32[ 866 | this.REGION_CART1 867 | ] = this.waitstatesPrefetch32[this.REGION_CART1 + 1] = 0; 868 | this.waitstatesPrefetch32[ 869 | this.REGION_CART2 870 | ] = this.waitstatesPrefetch32[this.REGION_CART2 + 1] = 0; 871 | } else { 872 | this.waitstatesPrefetch[ 873 | this.REGION_CART0 874 | ] = this.waitstatesPrefetch[ 875 | this.REGION_CART0 + 1 876 | ] = this.waitstatesSeq[this.REGION_CART0]; 877 | this.waitstatesPrefetch[ 878 | this.REGION_CART1 879 | ] = this.waitstatesPrefetch[ 880 | this.REGION_CART1 + 1 881 | ] = this.waitstatesSeq[this.REGION_CART1]; 882 | this.waitstatesPrefetch[ 883 | this.REGION_CART2 884 | ] = this.waitstatesPrefetch[ 885 | this.REGION_CART2 + 1 886 | ] = this.waitstatesSeq[this.REGION_CART2]; 887 | 888 | this.waitstatesPrefetch32[ 889 | this.REGION_CART0 890 | ] = this.waitstatesPrefetch32[ 891 | this.REGION_CART0 + 1 892 | ] = this.waitstatesSeq32[this.REGION_CART0]; 893 | this.waitstatesPrefetch32[ 894 | this.REGION_CART1 895 | ] = this.waitstatesPrefetch32[ 896 | this.REGION_CART1 + 1 897 | ] = this.waitstatesSeq32[this.REGION_CART1]; 898 | this.waitstatesPrefetch32[ 899 | this.REGION_CART2 900 | ] = this.waitstatesPrefetch32[ 901 | this.REGION_CART2 + 1 902 | ] = this.waitstatesSeq32[this.REGION_CART2]; 903 | } 904 | } 905 | saveNeedsFlush() { 906 | return this.save.writePending; 907 | } 908 | flushSave() { 909 | this.save.writePending = false; 910 | } 911 | allocGPIO(rom) { 912 | return new GameBoyAdvanceGPIO(this.core, rom); 913 | } 914 | } 915 | --------------------------------------------------------------------------------