@soapbox.pub/wasmboy
Version:
Soapbox fork of Wasmboy.
281 lines (241 loc) • 9.72 kB
JavaScript
// Start our update and render process
// Can't time by raf, as raf is not garunteed to be 60fps
// Need to run like a web game, where updates to the state of the core are done a 60 fps
// but we can render whenever the user would actually see the changes browser side in a raf
// https://developer.mozilla.org/en-US/docs/Games/Anatomy
// Imports
import { postMessage } from '../../worker/workerapi';
import { getSmartWorkerMessage } from '../../worker/smartworker';
import { WORKER_MESSAGE_TYPE, MEMORY_TYPE } from '../../worker/constants';
// Memory
import { getCartridgeRam } from './memory/ram.js';
import { getGameBoyMemory } from './memory/gameboymemory.js';
import { getPaletteMemory } from './memory/palettememory.js';
import { getInternalState } from './memory/internalstate.js';
// Timestamps
import { getPerformanceTimestamp } from '../../common/common';
import { addTimeStamp, waitForTimeStampsForFrameRate } from './timestamp';
// Transferring
import { transferGraphics } from './graphics/transfer';
// Some variables to help with Audio Latency
// 0.25 (quarter of a second), just felt right from testing :)
const MAX_AUDIO_LATENCY = 0.25;
// Pass over samples once we have enough worth playing:
// https://www.reddit.com/r/EmuDev/comments/5gkwi5/gb_apu_sound_emulation/
const AUDIO_BUFFER_SIZE = 1024;
// FPS measuring
let currentHighResTime;
let currentFps;
let gameboyFrameRateWithSpeed;
// interval to set timeout
let intervalRate;
function scheduleNextUpdate(libWorker) {
// Get our high res time
const highResTime = getPerformanceTimestamp();
// Find how long it has been since the last timestamp
const timeSinceLastTimestamp = highResTime - libWorker.fpsTimeStamps[libWorker.fpsTimeStamps.length - 1];
// Get the next time we should update using our interval rate
let nextUpdateTime = intervalRate - timeSinceLastTimestamp;
if (nextUpdateTime < 0) {
nextUpdateTime = 0;
}
// Lastly, increase by our lib worker speed
if (libWorker.speed && libWorker.speed > 0) {
nextUpdateTime = nextUpdateTime / libWorker.speed;
}
libWorker.updateId = setTimeout(() => {
update(libWorker);
}, Math.floor(nextUpdateTime));
}
// Function to run an update on the emulator itself
export function update(libWorker, passedIntervalRate) {
// Don't run if paused
if (libWorker.paused) {
return true;
}
// Set the intervalRate if it was passed
if (passedIntervalRate !== undefined) {
intervalRate = passedIntervalRate;
}
// Set a timestamp for this moment
// And make sure we are on track for FPS
currentFps = libWorker.getFPS();
gameboyFrameRateWithSpeed = libWorker.options.gameboyFrameRate + 1;
if (libWorker.speed && libWorker.speed > 0) {
gameboyFrameRateWithSpeed = gameboyFrameRateWithSpeed * libWorker.speed;
}
if (currentFps > gameboyFrameRateWithSpeed) {
// Pop a timestamp off of the front
// This is to avoid infinite loop here on loadstate
libWorker.fpsTimeStamps.shift();
scheduleNextUpdate(libWorker);
return true;
} else {
currentHighResTime = addTimeStamp(libWorker);
}
// Check if we are outputting audio
const shouldCheckAudio = !libWorker.options.headless && !libWorker.pauseFpsThrottle && libWorker.options.isAudioEnabled;
// Execute
// Wrapped in promise to better handle audio slowdowns and things of that sort
const executePromise = new Promise(resolve => {
// Update (Execute a frame)
let response;
if (shouldCheckAudio) {
executeAndCheckAudio(libWorker, resolve);
} else {
response = libWorker.wasmInstance.exports.executeFrame();
resolve(response);
}
});
executePromise.then(response => {
// Handle our update() response
if (response >= 0) {
// Pass messages to everyone
postMessage(
getSmartWorkerMessage({
type: WORKER_MESSAGE_TYPE.UPDATED,
fps: currentFps
})
);
// Check if we have frameskip
let shouldSkipRenderingFrame = false;
if (libWorker.options.frameSkip && libWorker.options.frameSkip > 0) {
libWorker.frameSkipCounter++;
if (libWorker.frameSkipCounter <= libWorker.options.frameSkip) {
shouldSkipRenderingFrame = true;
} else {
libWorker.frameSkipCounter = 0;
}
}
// Transfer Graphics
if (!shouldSkipRenderingFrame) {
transferGraphics(libWorker);
}
// Transfer Memory for things like save states
const memoryObject = {
type: WORKER_MESSAGE_TYPE.UPDATED
};
memoryObject[MEMORY_TYPE.CARTRIDGE_RAM] = getCartridgeRam(libWorker).buffer;
memoryObject[MEMORY_TYPE.GAMEBOY_MEMORY] = getGameBoyMemory(libWorker).buffer;
memoryObject[MEMORY_TYPE.PALETTE_MEMORY] = getPaletteMemory(libWorker).buffer;
memoryObject[MEMORY_TYPE.INTERNAL_STATE] = getInternalState(libWorker).buffer;
// Check for any undefined values
Object.keys(memoryObject).forEach(key => {
if (memoryObject[key] === undefined) {
memoryObject[key] = new Uint8Array().buffer;
}
});
libWorker.memoryWorkerPort.postMessage(getSmartWorkerMessage(memoryObject), [
memoryObject[MEMORY_TYPE.CARTRIDGE_RAM],
memoryObject[MEMORY_TYPE.GAMEBOY_MEMORY],
memoryObject[MEMORY_TYPE.PALETTE_MEMORY],
memoryObject[MEMORY_TYPE.INTERNAL_STATE]
]);
// Check if we hit a breakpoint
if (response === 2) {
postMessage(
getSmartWorkerMessage({
type: WORKER_MESSAGE_TYPE.BREAKPOINT
})
);
} else {
scheduleNextUpdate(libWorker);
}
} else {
postMessage(
getSmartWorkerMessage({
type: WORKER_MESSAGE_TYPE.CRASHED
})
);
libWorker.paused = true;
}
});
}
// If audio is enabled, sync by audio
// Audio will pass us its forward latency, and if it is too far ahead,
// Then we can wait a little bit to let audio catch up
// 0.25 (quarter of a second), just felt right from testing :)
function executeAndCheckAudio(libWorker, resolve) {
// Get our response
let response = -1;
response = libWorker.wasmInstance.exports.executeFrameAndCheckAudio(AUDIO_BUFFER_SIZE);
// If our response is not 1, simply resolve
if (response !== 1) {
resolve(response);
}
// Do some audio magic
if (response === 1) {
// Get our audioQueueIndex
const audioQueueIndex = libWorker.wasmInstance.exports.getNumberOfSamplesInAudioBuffer();
// Check if we are sending too much audio
const isTooMuchLatency = libWorker.currentAudioLatencyInSeconds > MAX_AUDIO_LATENCY;
const isRunningFullSpeed = currentFps >= gameboyFrameRateWithSpeed;
if (isTooMuchLatency && isRunningFullSpeed) {
sendAudio(libWorker, audioQueueIndex);
// Wait, Set a timeout for when we would like to
// Continue executing. * 1000 for seconds -> milli
// Wait for half the difference, since it may take time to execute, and things
const latencyDifferenceInSeconds = libWorker.currentAudioLatencyInSeconds - MAX_AUDIO_LATENCY;
const latencyDifferenceInMilli = Math.floor(latencyDifferenceInSeconds * 1000);
setTimeout(() => {
waitForTimeStampsForFrameRate(libWorker);
executeAndCheckAudio(libWorker, resolve);
}, Math.floor(latencyDifferenceInMilli / 10));
} else {
sendAudio(libWorker, audioQueueIndex);
executeAndCheckAudio(libWorker, resolve);
}
}
}
function sendAudio(libWorker, audioQueueIndex) {
// Send out our audio
// audioQueueIndex * 2, because audio Queue index represents 1 sample,
// for left AND right channel. Therefore the end index is, twice
// of the audioQueueIndex
// Build our message bits
const audioBuffer = libWorker.wasmByteMemory.slice(
libWorker.WASMBOY_SOUND_OUTPUT_LOCATION,
libWorker.WASMBOY_SOUND_OUTPUT_LOCATION + audioQueueIndex * 2
).buffer;
const message = {
type: WORKER_MESSAGE_TYPE.UPDATED,
audioBuffer,
numberOfSamples: audioQueueIndex,
fps: currentFps,
allowFastSpeedStretching: libWorker.options.gameboyFrameRate > 60
};
const messageTransferrables = [audioBuffer];
// If audio debugging is enabled, we gotta send a lot more
if (libWorker.options && libWorker.options.enableAudioDebugging) {
// Channel 1
const channel1Buffer = libWorker.wasmByteMemory.slice(
libWorker.WASMBOY_CHANNEL_1_OUTPUT_LOCATION,
libWorker.WASMBOY_CHANNEL_1_OUTPUT_LOCATION + audioQueueIndex * 2
).buffer;
message.channel1Buffer = channel1Buffer;
messageTransferrables.push(channel1Buffer);
// Channel 2
const channel2Buffer = libWorker.wasmByteMemory.slice(
libWorker.WASMBOY_CHANNEL_2_OUTPUT_LOCATION,
libWorker.WASMBOY_CHANNEL_2_OUTPUT_LOCATION + audioQueueIndex * 2
).buffer;
message.channel2Buffer = channel2Buffer;
messageTransferrables.push(channel2Buffer);
// Channel 3
const channel3Buffer = libWorker.wasmByteMemory.slice(
libWorker.WASMBOY_CHANNEL_3_OUTPUT_LOCATION,
libWorker.WASMBOY_CHANNEL_3_OUTPUT_LOCATION + audioQueueIndex * 2
).buffer;
message.channel3Buffer = channel3Buffer;
messageTransferrables.push(channel3Buffer);
// Channel 4
const channel4Buffer = libWorker.wasmByteMemory.slice(
libWorker.WASMBOY_CHANNEL_4_OUTPUT_LOCATION,
libWorker.WASMBOY_CHANNEL_4_OUTPUT_LOCATION + audioQueueIndex * 2
).buffer;
message.channel4Buffer = channel4Buffer;
messageTransferrables.push(channel4Buffer);
}
libWorker.audioWorkerPort.postMessage(getSmartWorkerMessage(message), messageTransferrables);
libWorker.wasmInstance.exports.clearAudioBuffer();
}