From 4c43ae0ba715d2938953b23733d97311442748c9 Mon Sep 17 00:00:00 2001 From: Grant Galitz Date: Thu, 19 Nov 2015 06:34:17 -0500 Subject: [PATCH 1/3] preliminary web worker support Emulator run in a web worker while A/V output stuff is run in the main thread. --- IodineGBA/core/Emulator.js | 81 +++++----- index.html | 1 + user_scripts/IodineGBACoreGlueCode.js | 25 ++- user_scripts/IodineGBAGraphicsGlueCode.js | 6 +- user_scripts/IodineGBASavesGlueCode.js | 7 +- user_scripts/IodineGBAWorkerGlueCode.js | 209 ++++++++++++++++++++++++++ user_scripts/IodineGBAWorkerGlueCodeWorker.js | 209 ++++++++++++++++++++++++++ 7 files changed, 491 insertions(+), 47 deletions(-) create mode 100644 user_scripts/IodineGBAWorkerGlueCode.js create mode 100644 user_scripts/IodineGBAWorkerGlueCodeWorker.js diff --git a/IodineGBA/core/Emulator.js b/IodineGBA/core/Emulator.js index 9fb2793..3c5afb9 100644 --- a/IodineGBA/core/Emulator.js +++ b/IodineGBA/core/Emulator.js @@ -19,9 +19,7 @@ function GameBoyAdvanceEmulator() { "dynamicSpeed":false //Whether to actively change the target speed for best user experience. }; this.audioFound = 0; //Do we have audio output sink found yet? - this.loaded = false; //Did we initialize IodineGBA? - this.faultFound = false; //Did we run into a fatal error? - this.paused = true; //Are we paused? + this.emulatorStatus = 0x10; //{paused, saves loaded, fault found, loaded} this.offscreenWidth = 240; //Width of the GBA screen. this.offscreenHeight = 160; //Height of the GBA screen. this.BIOS = []; //Initialize BIOS as not existing. @@ -56,11 +54,11 @@ GameBoyAdvanceEmulator.prototype.generateCoreExposed = function () { } } GameBoyAdvanceEmulator.prototype.play = function () { - if (this.paused) { - this.paused = false; - if (!this.loaded && this.BIOS && this.ROM) { + if ((this.emulatorStatus | 0) >= 0x10) { + this.emulatorStatus = this.emulatorStatus & 0xF; + if ((this.emulatorStatus & 0x1) == 0 && this.BIOS && this.ROM) { this.initializeCore(); - this.loaded = true; + this.emulatorStatus = this.emulatorStatus | 0x1; this.importSave(); } this.invalidateMetrics(); @@ -68,20 +66,19 @@ GameBoyAdvanceEmulator.prototype.play = function () { } } GameBoyAdvanceEmulator.prototype.pause = function () { - if (!this.paused) { + if ((this.emulatorStatus | 0) < 0x10) { this.exportSave(); - this.paused = true; + this.emulatorStatus = this.emulatorStatus | 0x10; } } GameBoyAdvanceEmulator.prototype.stop = function () { - this.faultFound = false; - this.loaded = false; + this.emulatorStatus = this.emulatorStatus & 0x1C; this.audioUpdateState = 1; this.pause(); } GameBoyAdvanceEmulator.prototype.restart = function () { - if (this.loaded) { - this.faultFound = false; + if ((this.emulatorStatus & 0x1) == 0x1) { + this.emulatorStatus = this.emulatorStatus & 0x1D; this.exportSave(); this.initializeCore(); this.importSave(); @@ -93,25 +90,23 @@ GameBoyAdvanceEmulator.prototype.restart = function () { GameBoyAdvanceEmulator.prototype.timerCallback = function (lastTimestamp) { //Callback passes us a reference timestamp: this.lastTimestamp = +lastTimestamp; - if (!this.paused) { - if (!this.faultFound && this.loaded) { //Any error pending or no ROM loaded is a show-stopper! - this.iterationStartSequence(); //Run start of iteration stuff. - this.IOCore.enter(this.CPUCyclesTotal | 0); //Step through the emulation core loop. - this.iterationEndSequence(); //Run end of iteration stuff. - } - else { - this.pause(); //Some pending error is preventing execution, so pause. - } + if ((this.emulatorStatus | 0) == 0x5) { //Any error pending or no ROM loaded is a show-stopper! + this.iterationStartSequence(); //Run start of iteration stuff. + this.IOCore.enter(this.CPUCyclesTotal | 0); //Step through the emulation core loop. + this.iterationEndSequence(); //Run end of iteration stuff. + } + else { + this.pause(); //Some pending error is preventing execution, so pause. } } GameBoyAdvanceEmulator.prototype.iterationStartSequence = function () { this.calculateSpeedPercentage(); //Calculate the emulator realtime run speed heuristics. - this.faultFound = true; //If the end routine doesn't unset this, then we are marked as having crashed. + this.emulatorStatus = this.emulatorStatus | 0x2; //If the end routine doesn't unset this, then we are marked as having crashed. this.audioUnderrunAdjustment(); //If audio is enabled, look to see how much we should overclock by to maintain the audio buffer. this.audioPushNewState(); //Check to see if we need to update the audio core for any output changes. } GameBoyAdvanceEmulator.prototype.iterationEndSequence = function () { - this.faultFound = false; //If core did not throw while running, unset the fatal error flag. + this.emulatorStatus = this.emulatorStatus & 0x1D; //If core did not throw while running, unset the fatal error flag. this.clockCyclesSinceStart = ((this.clockCyclesSinceStart | 0) + (this.CPUCyclesTotal | 0)) | 0; //Accumulate tracking. } GameBoyAdvanceEmulator.prototype.attachROM = function (ROM) { @@ -123,7 +118,7 @@ GameBoyAdvanceEmulator.prototype.attachBIOS = function (BIOS) { this.BIOS = BIOS; } GameBoyAdvanceEmulator.prototype.getGameName = function () { - if (!this.faultFound && this.loaded) { + if ((this.emulatorStatus & 0x3) == 0x1) { return this.IOCore.cartridge.name; } else { @@ -149,23 +144,31 @@ GameBoyAdvanceEmulator.prototype.importSave = function () { if (this.saveImportHandler) { var name = this.getGameName(); if (name != "") { - var save = this.saveImportHandler(name); - var saveType = this.saveImportHandler("TYPE_" + name); - if (save && saveType && !this.faultFound && this.loaded) { - var length = save.length | 0; - var convertedSave = getUint8Array(length | 0); - if ((length | 0) > 0) { - for (var index = 0; (index | 0) < (length | 0); index = ((index | 0) + 1) | 0) { - convertedSave[index | 0] = save[index | 0] & 0xFF; + var parentObj = this; + this.emulatorStatus = this.emulatorStatus & 0x1B; + this.saveImportHandler(name, function (save) { + parentObj.emulatorStatus = parentObj.emulatorStatus & 0x1B; + parentObj.saveImportHandler("TYPE_" + name, function (saveType) { + if (save && saveType && (parentObj.emulatorStatus & 0x3) == 0x1) { + var length = save.length | 0; + var convertedSave = getUint8Array(length | 0); + if ((length | 0) > 0) { + for (var index = 0; (index | 0) < (length | 0); index = ((index | 0) + 1) | 0) { + convertedSave[index | 0] = save[index | 0] & 0xFF; + } + parentObj.IOCore.saves.importSave(convertedSave, saveType | 0); + parentObj.emulatorStatus = parentObj.emulatorStatus | 0x4; + } } - this.IOCore.saves.importSave(convertedSave, saveType | 0); - } - } + }, function (){parentObj.emulatorStatus = parentObj.emulatorStatus | 0x4;}); + }, function (){parentObj.emulatorStatus = parentObj.emulatorStatus | 0x4;}); + return; } } + this.emulatorStatus = this.emulatorStatus | 0x4; } GameBoyAdvanceEmulator.prototype.exportSave = function () { - if (this.saveExportHandler && !this.faultFound && this.loaded) { + if (this.saveExportHandler && (this.emulatorStatus & 0x3) == 0x1) { var save = this.IOCore.saves.exportSave(); var saveType = this.IOCore.saves.exportSaveType(); if (save != null && saveType != null) { @@ -254,13 +257,13 @@ GameBoyAdvanceEmulator.prototype.initializeCore = function () { } GameBoyAdvanceEmulator.prototype.keyDown = function (keyPressed) { keyPressed = keyPressed | 0; - if (!this.paused && (keyPressed | 0) >= 0 && (keyPressed | 0) <= 9) { + if ((this.emulatorStatus | 0) < 0x10 && (keyPressed | 0) >= 0 && (keyPressed | 0) <= 9) { this.IOCore.joypad.keyPress(keyPressed | 0); } } GameBoyAdvanceEmulator.prototype.keyUp = function (keyReleased) { keyReleased = keyReleased | 0; - if (!this.paused && (keyReleased | 0) >= 0 && (keyReleased | 0) <= 9) { + if ((this.emulatorStatus | 0) < 0x10 && (keyReleased | 0) >= 0 && (keyReleased | 0) <= 9) { this.IOCore.joypad.keyRelease(keyReleased | 0); } } diff --git a/index.html b/index.html index dcf511a..ead1bbd 100644 --- a/index.html +++ b/index.html @@ -57,6 +57,7 @@ + diff --git a/user_scripts/IodineGBACoreGlueCode.js b/user_scripts/IodineGBACoreGlueCode.js index 6ec6b53..20564bc 100644 --- a/user_scripts/IodineGBACoreGlueCode.js +++ b/user_scripts/IodineGBACoreGlueCode.js @@ -46,7 +46,7 @@ var IodineGUI = { }; window.onload = function () { //Initialize Iodine: - IodineGUI.Iodine = new GameBoyAdvanceEmulator(); + registerIodineHandler(); //Initialize the timer: registerTimerHandler(); //Initialize the graphics: @@ -60,6 +60,27 @@ window.onload = function () { //Register GUI settings. registerGUISettings(); } +function registerIodineHandler() { + try { + //Try starting Iodine in a webworker: + IodineGUI.Iodine = new IodineGBAWorkerShim(); + addEvent("onbeforeunload", window, registerBeforeUnloadHandler); + } + catch (e) { + //Otherwise just run on-thread: + IodineGUI.Iodine = new GameBoyAdvanceEmulator(); + } +} +function registerBeforeUnloadHandler(e) { + IodineGUI.Iodine.pause(); + this.style.display = "none"; + document.getElementById("play").style.display = "inline"; + if (e.preventDefault) { + e.preventDefault(); + } + removeEvent("onbeforeunload", window, registerBeforeUnloadHandler); + return "IodineGBA needs to process your save data, leaving now may result in not saving current data."; +} function registerTimerHandler() { var rate = 4; IodineGUI.Iodine.setIntervalRate(rate | 0); @@ -77,7 +98,7 @@ function registerTimerHandler() { }, rate | 0); } function registerBlitterHandler() { - IodineGUI.Blitter = new GlueCodeGfx(); + IodineGUI.Blitter = new GlueCodeGfx(240, 160); IodineGUI.Blitter.attachCanvas(document.getElementById("emulator_target")); IodineGUI.Iodine.attachGraphicsFrameHandler(function (buffer) {IodineGUI.Blitter.copyBuffer(buffer);}); } diff --git a/user_scripts/IodineGBAGraphicsGlueCode.js b/user_scripts/IodineGBAGraphicsGlueCode.js index 39b5f71..930f1f0 100644 --- a/user_scripts/IodineGBAGraphicsGlueCode.js +++ b/user_scripts/IodineGBAGraphicsGlueCode.js @@ -8,11 +8,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -function GlueCodeGfx() { +function GlueCodeGfx(width, height) { this.didRAF = false; //Set when rAF has been used. this.graphicsFound = 0; //Do we have graphics output sink found yet? - this.offscreenWidth = 240; //Width of the GBA screen. - this.offscreenHeight = 160; //Height of the GBA screen. + this.offscreenWidth = width; //Width of the GBA screen. + this.offscreenHeight = height; //Height of the GBA screen. this.doSmoothing = true; //Cache some frame buffer lengths: var offscreenRGBCount = this.offscreenWidth * this.offscreenHeight * 3; diff --git a/user_scripts/IodineGBASavesGlueCode.js b/user_scripts/IodineGBASavesGlueCode.js index ed54688..c532ff3 100644 --- a/user_scripts/IodineGBASavesGlueCode.js +++ b/user_scripts/IodineGBASavesGlueCode.js @@ -8,18 +8,19 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -function ImportSaveCallback(name) { +function ImportSaveCallback(name, callbackFunc, callbackFuncNoSave) { try { var save = findValue("SAVE_" + name); if (save != null) { writeRedTemporaryText("Loaded save."); - return base64ToArray(save); + callbackFunc(base64ToArray(save)); + return; } } catch (error) { writeRedTemporaryText("Could not read save: " + error.message); } - return null; + callbackFuncNoSave(); } function ExportSave() { IodineGUI.Iodine.exportSave(); diff --git a/user_scripts/IodineGBAWorkerGlueCode.js b/user_scripts/IodineGBAWorkerGlueCode.js new file mode 100644 index 0000000..b1d749c --- /dev/null +++ b/user_scripts/IodineGBAWorkerGlueCode.js @@ -0,0 +1,209 @@ +"use strict"; +/* + Copyright (C) 2012-2015 Grant Galitz + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +function IodineGBAWorkerShim() { + this.gfx = null; + this.audio = null; + this.speed = null; + this.saveExport = null; + this.saveImport = null; + this.worker = null; + this.initialize(); +} +var tempvar = document.getElementsByTagName("script"); +IodineGBAWorkerShim.prototype.filepath = tempvar[tempvar.length-1].src; +IodineGBAWorkerShim.prototype.initialize = function () { + var parentObj = this; + this.worker = new Worker(this.filepath.substring(0, (this.filepath.length | 0) - 3) + "Worker.js"); + this.worker.onmessage = function (event) { + parentObj.decodeMessage(event.data); + } +} +IodineGBAWorkerShim.prototype.sendMessageSingle = function (eventCode) { + eventCode = eventCode | 0; + this.worker.postMessage({messageID:eventCode}); +} +IodineGBAWorkerShim.prototype.sendMessageDouble = function (eventCode, eventData) { + eventCode = eventCode | 0; + this.worker.postMessage({messageID:eventCode, payload:eventData}); +} +IodineGBAWorkerShim.prototype.sendBufferBack = function (eventCode, eventData) { + eventCode = eventCode | 0; + this.worker.postMessage({messageID:eventCode, payload:eventData}, [eventData.buffer]); +} +IodineGBAWorkerShim.prototype.play = function () { + this.sendMessageSingle(0); +} +IodineGBAWorkerShim.prototype.pause = function () { + this.sendMessageSingle(1); +} +IodineGBAWorkerShim.prototype.restart = function () { + this.sendMessageSingle(2); +} +IodineGBAWorkerShim.prototype.setIntervalRate = function (rate) { + rate = rate | 0; + this.sendMessageDouble(3, rate | 0); +} +IodineGBAWorkerShim.prototype.timerCallback = function (timestamp) { + timestamp = +timestamp; + this.sendMessageDouble(23, this.audio.remainingBuffer()); + this.sendMessageDouble(4, +timestamp); +} +IodineGBAWorkerShim.prototype.attachGraphicsFrameHandler = function (gfx) { + this.gfx = gfx; + this.sendMessageSingle(5); +} +IodineGBAWorkerShim.prototype.attachAudioHandler = function (audio) { + this.audio = audio; + this.sendMessageSingle(6); +} +IodineGBAWorkerShim.prototype.enableAudio = function () { + if (this.audio) { + this.sendMessageSingle(7); + } +} +IodineGBAWorkerShim.prototype.disableAudio = function () { + if (this.audio) { + this.sendMessageSingle(8); + } +} +IodineGBAWorkerShim.prototype.toggleSkipBootROM = function (doEnable) { + doEnable = doEnable | 0; + this.sendMessageDouble(9, doEnable | 0); +} +IodineGBAWorkerShim.prototype.toggleDynamicSpeed = function (doEnable) { + doEnable = doEnable | 0; + this.sendMessageDouble(10, doEnable | 0); +} +IodineGBAWorkerShim.prototype.attachSpeedHandler = function (speed) { + this.speed = speed; + this.sendMessageSingle(11); +} +IodineGBAWorkerShim.prototype.keyDown = function (keyCode) { + keyCode = keyCode | 0; + this.sendMessageDouble(12, keyCode | 0); +} +IodineGBAWorkerShim.prototype.keyUp = function (keyCode) { + keyCode = keyCode | 0; + this.sendMessageDouble(13, keyCode | 0); +} +IodineGBAWorkerShim.prototype.incrementSpeed = function (newSpeed) { + newSpeed = +newSpeed; + this.sendMessageDouble(14, +newSpeed); +} +IodineGBAWorkerShim.prototype.attachBIOS = function (BIOS) { + this.sendMessageDouble(15, BIOS); +} +IodineGBAWorkerShim.prototype.attachROM = function (ROM) { + this.sendMessageDouble(16, ROM); +} +IodineGBAWorkerShim.prototype.exportSave = function () { + this.sendMessageSingle(17); +} +IodineGBAWorkerShim.prototype.attachSaveExportHandler = function (saveExport) { + this.saveExport = saveExport; + this.sendMessageSingle(18); +} +IodineGBAWorkerShim.prototype.attachSaveImportHandler = function (saveImport) { + this.saveImport = saveImport; + this.sendMessageSingle(19); +} +IodineGBAWorkerShim.prototype.decodeMessage = function (data) { + switch (data.messageID) { + case 0: + this.audioInitialize(data.channels | 0, +data.sampleRate, data.bufferLimit | 0); + break; + case 1: + this.audioRegister(); + break; + case 2: + this.audioSetBufferSpace(data.audioBufferContainAmount | 0); + break; + case 3: + this.audioPush(data.audioBuffer, data.audioNumSamplesTotal | 0); + break; + case 4: + this.graphicsPush(data.graphicsBuffer); + break; + case 5: + this.speedPush(+data.speed); + break; + case 6: + this.saveImportRequest(data.saveID); + break; + case 7: + this.saveExportRequest(data.saveID, data.saveData); + break; + case 8: + this.audioUnregister(); + } +} +IodineGBAWorkerShim.prototype.audioInitialize = function (channels, sampleRate, bufferLimit) { + channels = channels | 0; + sampleRate = +sampleRate; + bufferLimit = bufferLimit | 0; + if (this.audio) { + this.audio.initialize(channels | 0, +sampleRate, bufferLimit | 0, function () { + //Disable audio in the callback here: + parentObj.disableAudio(); + }); + } +} +IodineGBAWorkerShim.prototype.audioRegister = function () { + if (this.audio) { + this.audio.register(); + } +} +IodineGBAWorkerShim.prototype.audioUnregister = function () { + if (this.audio) { + this.audio.unregister(); + } +} +IodineGBAWorkerShim.prototype.audioSetBufferSpace = function (bufferSpace) { + bufferSpace = bufferSpace | 0; + if (this.audio) { + this.audio.setBufferSpace(bufferSpace | 0); + } +} +IodineGBAWorkerShim.prototype.audioPush = function (audioBuffer, audioNumSamplesTotal) { + audioNumSamplesTotal = audioNumSamplesTotal | 0; + if (this.audio) { + this.audio.push(audioBuffer, audioNumSamplesTotal | 0); + this.sendBufferBack(21, audioBuffer); + } +} +IodineGBAWorkerShim.prototype.graphicsPush = function (graphicsBuffer) { + if (this.gfx) { + this.gfx(graphicsBuffer); + this.sendBufferBack(22, graphicsBuffer); + } +} +IodineGBAWorkerShim.prototype.speedPush = function (speed) { + speed = +speed; + if (this.speed) { + this.speed(+speed); + } +} +IodineGBAWorkerShim.prototype.saveImportRequest = function (saveID) { + if (this.saveImport) { + var parentObj = this; + this.saveImport(saveID, function (saveData) { + parentObj.sendMessageDouble(20, saveData); + }, + function () { + parentObj.sendMessageSingle(24); + }); + } +} +IodineGBAWorkerShim.prototype.saveExportRequest = function (saveID, saveData) { + if (this.saveExport) { + this.saveExport(saveID, saveData); + } +} \ No newline at end of file diff --git a/user_scripts/IodineGBAWorkerGlueCodeWorker.js b/user_scripts/IodineGBAWorkerGlueCodeWorker.js new file mode 100644 index 0000000..0822e42 --- /dev/null +++ b/user_scripts/IodineGBAWorkerGlueCodeWorker.js @@ -0,0 +1,209 @@ +"use strict"; +/* + Copyright (C) 2012-2015 Grant Galitz + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +importScripts("../IodineGBA/includes/TypedArrayShim.js"); +importScripts("../IodineGBA/core/Cartridge.js"); +importScripts("../IodineGBA/core/DMA.js"); +importScripts("../IodineGBA/core/Emulator.js"); +importScripts("../IodineGBA/core/Graphics.js"); +importScripts("../IodineGBA/core/RunLoop.js"); +importScripts("../IodineGBA/core/Memory.js"); +importScripts("../IodineGBA/core/IRQ.js"); +importScripts("../IodineGBA/core/JoyPad.js"); +importScripts("../IodineGBA/core/Serial.js"); +importScripts("../IodineGBA/core/Sound.js"); +importScripts("../IodineGBA/core/Timer.js"); +importScripts("../IodineGBA/core/Wait.js"); +importScripts("../IodineGBA/core/CPU.js"); +importScripts("../IodineGBA/core/Saves.js"); +importScripts("../IodineGBA/core/sound/FIFO.js"); +importScripts("../IodineGBA/core/sound/Channel1.js"); +importScripts("../IodineGBA/core/sound/Channel2.js"); +importScripts("../IodineGBA/core/sound/Channel3.js"); +importScripts("../IodineGBA/core/sound/Channel4.js"); +importScripts("../IodineGBA/core/CPU/ARM.js"); +importScripts("../IodineGBA/core/CPU/THUMB.js"); +importScripts("../IodineGBA/core/CPU/CPSR.js"); +importScripts("../IodineGBA/core/graphics/Renderer.js"); +importScripts("../IodineGBA/core/graphics/RendererProxy.js"); +importScripts("../IodineGBA/core/graphics/BGTEXT.js"); +importScripts("../IodineGBA/core/graphics/BG2FrameBuffer.js"); +importScripts("../IodineGBA/core/graphics/BGMatrix.js"); +importScripts("../IodineGBA/core/graphics/AffineBG.js"); +importScripts("../IodineGBA/core/graphics/ColorEffects.js"); +importScripts("../IodineGBA/core/graphics/Mosaic.js"); +importScripts("../IodineGBA/core/graphics/OBJ.js"); +importScripts("../IodineGBA/core/graphics/OBJWindow.js"); +importScripts("../IodineGBA/core/graphics/Window.js"); +importScripts("../IodineGBA/core/graphics/Compositor.js"); +importScripts("../IodineGBA/core/memory/DMA0.js"); +importScripts("../IodineGBA/core/memory/DMA1.js"); +importScripts("../IodineGBA/core/memory/DMA2.js"); +importScripts("../IodineGBA/core/memory/DMA3.js"); +importScripts("../IodineGBA/core/cartridge/SaveDeterminer.js"); +importScripts("../IodineGBA/core/cartridge/SRAM.js"); +importScripts("../IodineGBA/core/cartridge/FLASH.js"); +importScripts("../IodineGBA/core/cartridge/EEPROM.js"); +var Iodine = new GameBoyAdvanceEmulator(); +//Spare audio buffers: +var audioBufferPool = []; +//Spare graphics buffers: +var graphicsBufferPool = []; +//Save callbacks waiting to be satisfied: +var saveImportPool = []; +//Cached timestamp: +var timestamp = 0; +//Event decoding: +self.onmessage = function (event) { + var data = event.data; + switch (data.messageID | 0) { + case 0: + Iodine.play(); + break; + case 1: + Iodine.pause(); + break; + case 2: + Iodine.restart(); + break; + case 3: + Iodine.setIntervalRate(data.payload | 0); + setInterval(function() {Iodine.timerCallback(+timestamp);}, data.payload | 0); + break; + case 4: + timestamp = +data.payload; + break; + case 5: + Iodine.attachGraphicsFrameHandler(graphicsFrameHandler); + break; + case 6: + Iodine.attachAudioHandler(audioHandler); + break; + case 7: + Iodine.enableAudio(); + break; + case 8: + Iodine.disableAudio(); + break; + case 9: + Iodine.toggleSkipBootROM(!!data.payload); + break; + case 10: + Iodine.toggleDynamicSpeed(!!data.payload); + break; + case 11: + Iodine.attachSpeedHandler(speedHandler); + break; + case 12: + Iodine.keyDown(data.payload | 0); + break; + case 13: + Iodine.keyUp(data.payload | 0); + break; + case 14: + Iodine.incrementSpeed(+data.payload); + break; + case 15: + Iodine.attachBIOS(data.payload); + break; + case 16: + Iodine.attachROM(data.payload); + break; + case 17: + Iodine.exportSave(); + break; + case 18: + Iodine.attachSaveExportHandler(saveExportHandler); + break; + case 19: + Iodine.attachSaveImportHandler(saveImportHandler); + break; + case 20: + processSaveImportSuccess(data.payload); + break; + case 21: + repoolAudioBuffer(data.payload); + break; + case 22: + repoolGraphicsBuffer(data.payload); + break; + case 23: + audioHandler.remainingBufferCache = data.payload; + break; + case 24: + processSaveImportFail(); + } +} +function graphicsFrameHandler(swizzledFrame) { + var buffer = getFreeGraphicsBuffer(swizzledFrame.length); + buffer.set(swizzledFrame); + postMessage({messageID:4, graphicsBuffer:buffer}, [buffer.buffer]); +} +//Shim for our audio api: +var audioHandler = { + push:function (audioBuffer, amountToSend) { + var buffer = getFreeAudioBuffer(amountToSend | 0); + buffer.set(audioBuffer); + postMessage({messageID:3, audioBuffer:buffer, audioNumSamplesTotal:amountToSend | 0}, [buffer.buffer]); + }, + register:function () { + postMessage({messageID:1}); + }, + unregister:function () { + postMessage({messageID:8}); + }, + setBufferSpace:function (spaceContain) { + postMessage({messageID:2, audioBufferContainAmount:spaceContain}); + }, + initialize:function (channels, sampleRate, bufferLimit) { + postMessage({messageID:0, channels:channels, sampleRate:sampleRate, bufferLimit:bufferLimit}); + }, + remainingBuffer:function () { + return this.remainingBufferCache; + }, + remainingBufferCache:0 +}; +function speedHandler(speed) { + postMessage({messageID:5, speed:speed}); +} +function saveExportHandler(saveID, saveData) { + postMessage({messageID:7, saveID:saveID, saveData:saveData}); +} +function saveImportHandler(saveID, saveCallback, noSaveCallback) { + postMessage({messageID:6, saveID:saveID}); + saveImportPool.push([saveCallback, noSaveCallback]); +} +function processSaveImportSuccess(saveData) { + saveImportPool.shift()[0](saveData); +} +function processSaveImportFail() { + saveImportPool.shift()[1](); +} +function repoolAudioBuffer(buffer) { + audioBufferPool.push(buffer); +} +function repoolGraphicsBuffer(buffer) { + graphicsBufferPool.push(buffer); +} +function getFreeGraphicsBuffer(amountToSend) { + if (graphicsBufferPool.length == 0) { + return new getUint8Array(amountToSend); + } + return graphicsBufferPool.shift(); +} +function getFreeAudioBuffer(amountToSend) { + while (audioBufferPool.length > 0) { + var buffer = audioBufferPool.shift(); + if (buffer.length >= amountToSend) { + return buffer; + } + } + return new getFloat32Array(amountToSend); +} \ No newline at end of file From d6efa2131a95f7f5c707ad83512d88e8fee6a3af Mon Sep 17 00:00:00 2001 From: Grant Galitz Date: Fri, 20 Nov 2015 02:01:32 -0500 Subject: [PATCH 2/3] require sharedarraybuffer support We use it for sharing low latency buffer metrics. --- user_scripts/IodineGBACoreGlueCode.js | 10 ++++++--- user_scripts/IodineGBAWorkerGlueCode.js | 16 +++++++++++++- user_scripts/IodineGBAWorkerGlueCodeWorker.js | 32 ++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/user_scripts/IodineGBACoreGlueCode.js b/user_scripts/IodineGBACoreGlueCode.js index 20564bc..cf33407 100644 --- a/user_scripts/IodineGBACoreGlueCode.js +++ b/user_scripts/IodineGBACoreGlueCode.js @@ -62,9 +62,13 @@ window.onload = function () { } function registerIodineHandler() { try { + if (!window.SharedInt32Array) { + //Audio synchronization is much better with shared array memory: + throw null; + } //Try starting Iodine in a webworker: IodineGUI.Iodine = new IodineGBAWorkerShim(); - addEvent("onbeforeunload", window, registerBeforeUnloadHandler); + addEvent("beforeunload", window, registerBeforeUnloadHandler); } catch (e) { //Otherwise just run on-thread: @@ -73,12 +77,12 @@ function registerIodineHandler() { } function registerBeforeUnloadHandler(e) { IodineGUI.Iodine.pause(); - this.style.display = "none"; + document.getElementById("play").style.display = "none"; document.getElementById("play").style.display = "inline"; if (e.preventDefault) { e.preventDefault(); } - removeEvent("onbeforeunload", window, registerBeforeUnloadHandler); + removeEvent("beforeunload", window, registerBeforeUnloadHandler); return "IodineGBA needs to process your save data, leaving now may result in not saving current data."; } function registerTimerHandler() { diff --git a/user_scripts/IodineGBAWorkerGlueCode.js b/user_scripts/IodineGBAWorkerGlueCode.js index b1d749c..ee56e1e 100644 --- a/user_scripts/IodineGBAWorkerGlueCode.js +++ b/user_scripts/IodineGBAWorkerGlueCode.js @@ -11,10 +11,12 @@ function IodineGBAWorkerShim() { this.gfx = null; this.audio = null; + this.audioInitialized = false; this.speed = null; this.saveExport = null; this.saveImport = null; this.worker = null; + this.shared = null; this.initialize(); } var tempvar = document.getElementsByTagName("script"); @@ -25,6 +27,10 @@ IodineGBAWorkerShim.prototype.initialize = function () { this.worker.onmessage = function (event) { parentObj.decodeMessage(event.data); } + if (window.SharedInt32Array) { + this.shared = new SharedInt32Array(1); + this.sendBufferBack(25, this.shared); + } } IodineGBAWorkerShim.prototype.sendMessageSingle = function (eventCode) { eventCode = eventCode | 0; @@ -53,7 +59,14 @@ IodineGBAWorkerShim.prototype.setIntervalRate = function (rate) { } IodineGBAWorkerShim.prototype.timerCallback = function (timestamp) { timestamp = +timestamp; - this.sendMessageDouble(23, this.audio.remainingBuffer()); + if (this.audio && this.audioInitialized) { + if (this.shared) { + this.shared[0] = this.audio.remainingBuffer() | 0; + } + else { + this.sendMessageDouble(23, this.audio.remainingBuffer() | 0); + } + } this.sendMessageDouble(4, +timestamp); } IodineGBAWorkerShim.prototype.attachGraphicsFrameHandler = function (gfx) { @@ -154,6 +167,7 @@ IodineGBAWorkerShim.prototype.audioInitialize = function (channels, sampleRate, //Disable audio in the callback here: parentObj.disableAudio(); }); + this.audioInitialized = true; } } IodineGBAWorkerShim.prototype.audioRegister = function () { diff --git a/user_scripts/IodineGBAWorkerGlueCodeWorker.js b/user_scripts/IodineGBAWorkerGlueCodeWorker.js index 0822e42..dfa0083 100644 --- a/user_scripts/IodineGBAWorkerGlueCodeWorker.js +++ b/user_scripts/IodineGBAWorkerGlueCodeWorker.js @@ -54,8 +54,10 @@ importScripts("../IodineGBA/core/cartridge/EEPROM.js"); var Iodine = new GameBoyAdvanceEmulator(); //Spare audio buffers: var audioBufferPool = []; +var audioBufferPassCount = 0; //Spare graphics buffers: var graphicsBufferPool = []; +var graphicsBufferPassCount = 0; //Save callbacks waiting to be satisfied: var saveImportPool = []; //Cached timestamp: @@ -139,19 +141,29 @@ self.onmessage = function (event) { break; case 24: processSaveImportFail(); + break; + case 25: + attachAudioMetricHook(data.payload); } } function graphicsFrameHandler(swizzledFrame) { - var buffer = getFreeGraphicsBuffer(swizzledFrame.length); - buffer.set(swizzledFrame); - postMessage({messageID:4, graphicsBuffer:buffer}, [buffer.buffer]); + if ((graphicsBufferPassCount | 0) < 2) { + var buffer = getFreeGraphicsBuffer(swizzledFrame.length); + buffer.set(swizzledFrame); + graphicsBufferPassCount = ((graphicsBufferPassCount | 0) + 1) | 0; + postMessage({messageID:4, graphicsBuffer:buffer}, [buffer.buffer]); + } } //Shim for our audio api: var audioHandler = { + shared:null, push:function (audioBuffer, amountToSend) { - var buffer = getFreeAudioBuffer(amountToSend | 0); - buffer.set(audioBuffer); - postMessage({messageID:3, audioBuffer:buffer, audioNumSamplesTotal:amountToSend | 0}, [buffer.buffer]); + if ((audioBufferPassCount | 0) < 10) { + var buffer = getFreeAudioBuffer(amountToSend | 0); + buffer.set(audioBuffer); + audioBufferPassCount = ((audioBufferPassCount | 0) + 1) | 0; + postMessage({messageID:3, audioBuffer:buffer, audioNumSamplesTotal:amountToSend | 0}, [buffer.buffer]); + } }, register:function () { postMessage({messageID:1}); @@ -166,10 +178,16 @@ var audioHandler = { postMessage({messageID:0, channels:channels, sampleRate:sampleRate, bufferLimit:bufferLimit}); }, remainingBuffer:function () { + if (this.shared) { + return this.shared[0]; + } return this.remainingBufferCache; }, remainingBufferCache:0 }; +function attachAudioMetricHook(buffer) { + audioHandler.shared = new SharedInt32Array(buffer.buffer); +} function speedHandler(speed) { postMessage({messageID:5, speed:speed}); } @@ -187,9 +205,11 @@ function processSaveImportFail() { saveImportPool.shift()[1](); } function repoolAudioBuffer(buffer) { + audioBufferPassCount = ((audioBufferPassCount | 0) - 1) | 0; audioBufferPool.push(buffer); } function repoolGraphicsBuffer(buffer) { + graphicsBufferPassCount = ((graphicsBufferPassCount | 0) - 1) | 0; graphicsBufferPool.push(buffer); } function getFreeGraphicsBuffer(amountToSend) { From 3c6bcf910881c25074e6619c3337d1400634e0c7 Mon Sep 17 00:00:00 2001 From: Grant Galitz Date: Fri, 20 Nov 2015 02:10:38 -0500 Subject: [PATCH 3/3] ehh --- user_scripts/IodineGBACoreGlueCode.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_scripts/IodineGBACoreGlueCode.js b/user_scripts/IodineGBACoreGlueCode.js index cf33407..8a523c5 100644 --- a/user_scripts/IodineGBACoreGlueCode.js +++ b/user_scripts/IodineGBACoreGlueCode.js @@ -62,8 +62,8 @@ window.onload = function () { } function registerIodineHandler() { try { - if (!window.SharedInt32Array) { - //Audio synchronization is much better with shared array memory: + //Will run like shit if missing some of this for the webworker copy: + if (!Math.imul || !window.Int32Array /*|| !window.SharedInt32Array*/) { throw null; } //Try starting Iodine in a webworker: