Split core web runtime into separate js

This commit is contained in:
2022-02-17 23:27:57 +01:00
parent 5d41733142
commit f1493ebded
2 changed files with 328 additions and 302 deletions

View File

@@ -1,5 +1,4 @@
import loaderUrl from "data-url:../../platform/bin/loader.wasm"; import MicroW8 from './microw8.js';
import platformUrl from "data-url:../../platform/bin/platform.uw8";
function setMessage(size, error) { function setMessage(size, error) {
let html = size ? `${size} bytes` : 'Insert cart'; let html = size ? `${size} bytes` : 'Insert cart';
@@ -9,314 +8,27 @@ function setMessage(size, error) {
document.getElementById('message').innerHTML = html; document.getElementById('message').innerHTML = html;
} }
let screen = document.getElementById('screen'); let uw8 = MicroW8(document.getElementById('screen'), {
let canvasCtx = screen.getContext('2d'); setMessage,
let imageData = canvasCtx.createImageData(320, 240); keyboardElement: window,
timerElement: document.getElementById("timer"),
let devkitMode;
let cancelFunction;
let currentData;
let U8 = (d) => new Uint8Array(d);
let U32 = (d) => new Uint32Array(d);
let pad = 0;
let keyHandler = (e) => {
let isKeyDown = e.type == 'keydown';
let mask;
switch (e.code) {
case 'ArrowUp':
mask = 1;
break;
case 'ArrowDown':
mask = 2;
break;
case 'ArrowLeft':
mask = 4;
break;
case 'ArrowRight':
mask = 8;
break;
case 'KeyZ':
mask = 16;
break;
case 'KeyX':
mask = 32;
break;
case 'KeyA':
mask = 64;
break;
case 'KeyS':
mask = 128;
break;
case 'KeyR':
if (isKeyDown) {
runModule(currentData, true);
}
break;
case 'F9':
if(isKeyDown) {
screen.toBlob(blob => {
downloadBlob(blob, '.png');
}); });
}
e.preventDefault();
break;
case 'F10':
if(isKeyDown) {
recordVideo();
}
e.preventDefault();
break;
}
if (isKeyDown) {
pad |= mask;
} else {
pad &= ~mask;
}
};
window.onkeydown = keyHandler;
window.onkeyup = keyHandler;
async function runModule(data, keepUrl) {
if (cancelFunction) {
cancelFunction();
cancelFunction = null;
}
let cartridgeSize = data.byteLength;
setMessage(cartridgeSize);
if (cartridgeSize == 0) {
return;
}
currentData = data;
let newURL = window.location.pathname;
if (cartridgeSize <= 1024 && !keepUrl) {
let dataString = '';
for (let byte of U8(data)) {
dataString += String.fromCharCode(byte);
}
newURL += '#' + btoa(dataString);
if (newURL != window.location.pathname + window.location.hash) {
history.pushState(null, null, newURL);
}
}
screen.width = screen.width;
try {
let memSize = { initial: 4 };
if(!devkitMode) {
memSize.maximum = 4;
}
let memory = new WebAssembly.Memory({ initial: 4, maximum: devkitMode ? 16 : 4 });
let memU8 = U8(memory.buffer);
let importObject = {
env: {
memory
},
};
let loader;
let loadModuleData = (data) => {
if (loader && (!devkitMode || U8(data)[0] != 0)) {
memU8.set(U8(data));
let length = loader.exports.load_uw8(data.byteLength);
data = new ArrayBuffer(length);
U8(data).set(memU8.slice(0, length));
}
return data;
}
let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance;
let loadModuleURL = async (url) => instantiate(loadModuleData(await (await fetch(url)).arrayBuffer()));
loader = await loadModuleURL(loaderUrl);
for (let n of ['acos', 'asin', 'atan', 'atan2', 'cos', 'exp', 'log', 'sin', 'tan', 'pow']) {
importObject.env[n] = Math[n];
}
for (let i = 9; i < 64; ++i) {
importObject.env['reserved' + i] = () => { };
}
for (let i = 0; i < 16; ++i) {
importObject.env['g_reserved' + i] = 0;
}
data = loadModuleData(data);
let platform_instance = await loadModuleURL(platformUrl);
for (let name in platform_instance.exports) {
importObject.env[name] = platform_instance.exports[name]
}
let instance = await instantiate(data);
let buffer = U32(imageData.data.buffer);
let startTime = Date.now();
let keepRunning = true;
cancelFunction = () => keepRunning = false;
const timePerFrame = 1000 / 60;
let nextFrame = startTime;
function mainloop() {
if (!keepRunning) {
return;
}
try {
let now = Date.now();
let restart = false;
if (now >= nextFrame) {
let gamepads = navigator.getGamepads();
let gamepad = 0;
for (let i = 0; i < 4; ++i) {
let pad = gamepads[i];
if (!pad) {
continue;
}
for (let j = 0; j < 8; ++j) {
let buttonIdx = (j + 12) % 16;
if (pad.buttons.length > buttonIdx && pad.buttons[buttonIdx].pressed) {
gamepad |= 1 << (i * 8 + j);
}
}
if (pad.axes.length > 1) {
for (let j = 0; j < 4; ++j) {
let v = pad.axes[1 - (j >> 1)];
if (((j & 1) ? v : -v) > 0.5) {
gamepad |= 1 << (i * 8 + j);
}
}
}
if (pad.buttons.length > 9 && pad.buttons[9].pressed) {
restart = true;
}
}
let u32Mem = U32(memory.buffer);
u32Mem[16] = now - startTime;
u32Mem[17] = pad | gamepad;
instance.exports.upd();
platform_instance.exports.endFrame();
let palette = U32(memory.buffer.slice(0x13000, 0x13000 + 1024));
for (let i = 0; i < 320 * 240; ++i) {
buffer[i] = palette[memU8[i + 120]] | 0xff000000;
}
canvasCtx.putImageData(imageData, 0, 0);
nextFrame = Math.max(nextFrame + timePerFrame, now);
}
if (restart) {
runModule(currentData);
} else {
window.requestAnimationFrame(mainloop);
}
} catch (err) {
setMessage(cartridgeSize, err.toString());
}
}
mainloop();
} catch (err) {
setMessage(cartridgeSize, err.toString());
}
}
function downloadBlob(blob, ext) {
let a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'microw8_' + new Date().toISOString() + ext;
a.click();
URL.revokeObjectURL(a.href);
}
let videoRecorder;
let videoStartTime;
function recordVideo() {
if(videoRecorder) {
videoRecorder.stop();
videoRecorder = null;
return;
}
videoRecorder = new MediaRecorder(screen.captureStream(), {
mimeType: 'video/webm',
videoBitsPerSecond: 25000000
});
let chunks = [];
videoRecorder.ondataavailable = e => {
chunks.push(e.data);
};
let timer = document.getElementById("timer");
timer.hidden = false;
timer.innerText = "00:00";
videoRecorder.onstop = () => {
timer.hidden = true;
downloadBlob(new Blob(chunks, {type: 'video/webm'}), '.webm');
};
videoRecorder.start();
videoStartTime = Date.now();
function updateTimer() {
if(!videoStartTime) {
return;
}
let duration = Math.floor((Date.now() - videoStartTime) / 1000);
timer.innerText = Math.floor(duration / 60).toString().padStart(2, '0') + ':' + (duration % 60).toString().padStart(2, '0');
setTimeout(updateTimer, 1000);
}
setTimeout(updateTimer, 1000);
}
async function runModuleFromURL(url, keepUrl) {
let response = await fetch(url);
let type = response.headers.get('Content-Type');
if(type && type.includes('html')) {
throw false;
}
runModule(await response.arrayBuffer(), keepUrl || devkitMode);
}
function runModuleFromHash() { function runModuleFromHash() {
let hash = window.location.hash.slice(1); let hash = window.location.hash.slice(1);
if(hash == 'devkit') { if(hash == 'devkit') {
devkitMode = true; uw8.setDevkitMode(true);
return; return;
} }
devkitMode = false; uw8.setDevkitMode(false);
if (hash.length > 0) { if (hash.length > 0) {
if (hash.startsWith("url=")) { if (hash.startsWith("url=")) {
runModuleFromURL(hash.slice(4), true); uw8.runModuleFromURL(hash.slice(4), true);
} else { } else {
runModuleFromURL('data:;base64,' + hash); uw8.runModuleFromURL('data:;base64,' + hash);
} }
} else { } else {
runModule(new ArrayBuffer(0)); uw8.runModule(new ArrayBuffer(0));
} }
} }
@@ -331,7 +43,7 @@ let setupLoad = () => {
fileInput.accept = '.wasm,.uw8,application/wasm'; fileInput.accept = '.wasm,.uw8,application/wasm';
fileInput.onchange = () => { fileInput.onchange = () => {
if (fileInput.files.length > 0) { if (fileInput.files.length > 0) {
runModuleFromURL(URL.createObjectURL(fileInput.files[0])); uw8.runModuleFromURL(URL.createObjectURL(fileInput.files[0]));
} }
}; };
fileInput.click(); fileInput.click();
@@ -345,7 +57,7 @@ let setupLoad = () => {
let files = e.dataTransfer && e.dataTransfer.files; let files = e.dataTransfer && e.dataTransfer.files;
if(files && files.length == 1) { if(files && files.length == 1) {
e.preventDefault(); e.preventDefault();
runModuleFromURL(URL.createObjectURL(e.dataTransfer.files[0])); uw8.runModuleFromURL(URL.createObjectURL(e.dataTransfer.files[0]));
} }
} }
@@ -367,7 +79,7 @@ if(location.hash.length != 0) {
url += 'cart.uw8'; url += 'cart.uw8';
} }
try { try {
await runModuleFromURL(url, true); await uw8.runModuleFromURL(url, true);
} catch(e) { } catch(e) {
setupLoad(); setupLoad();
} }

314
web/src/microw8.js Normal file
View File

@@ -0,0 +1,314 @@
import loaderUrl from "data-url:../../platform/bin/loader.wasm";
import platformUrl from "data-url:../../platform/bin/platform.uw8";
export default function MicroW8(screen, config) {
let canvasCtx = screen.getContext('2d');
let imageData = canvasCtx.createImageData(320, 240);
let devkitMode = config.devkitMode;
let cancelFunction;
let currentData;
let U8 = (d) => new Uint8Array(d);
let U32 = (d) => new Uint32Array(d);
let pad = 0;
let keyboardElement = config.keyboardElement == undefined ? screen : config.keyboardElement;
if(keyboardElement) {
let keyHandler = (e) => {
let isKeyDown = e.type == 'keydown';
let mask;
switch (e.code) {
case 'ArrowUp':
mask = 1;
break;
case 'ArrowDown':
mask = 2;
break;
case 'ArrowLeft':
mask = 4;
break;
case 'ArrowRight':
mask = 8;
break;
case 'KeyZ':
mask = 16;
break;
case 'KeyX':
mask = 32;
break;
case 'KeyA':
mask = 64;
break;
case 'KeyS':
mask = 128;
break;
case 'KeyR':
if (isKeyDown) {
runModule(currentData, true);
}
break;
case 'F9':
if(isKeyDown) {
screen.toBlob(blob => {
downloadBlob(blob, '.png');
});
}
e.preventDefault();
break;
case 'F10':
if(isKeyDown) {
recordVideo();
}
e.preventDefault();
break;
}
if (isKeyDown) {
pad |= mask;
} else {
pad &= ~mask;
}
};
keyboardElement.onkeydown = keyHandler;
keyboardElement.onkeyup = keyHandler;
}
async function runModule(data, keepUrl) {
if (cancelFunction) {
cancelFunction();
cancelFunction = null;
}
let cartridgeSize = data.byteLength;
config.setMessage(cartridgeSize);
if (cartridgeSize == 0) {
return;
}
currentData = data;
let newURL = window.location.pathname;
if (cartridgeSize <= 1024 && !keepUrl) {
let dataString = '';
for (let byte of U8(data)) {
dataString += String.fromCharCode(byte);
}
newURL += '#' + btoa(dataString);
if (newURL != window.location.pathname + window.location.hash) {
history.pushState(null, null, newURL);
}
}
screen.width = screen.width;
try {
let memSize = { initial: 4 };
if(!devkitMode) {
memSize.maximum = 4;
}
let memory = new WebAssembly.Memory({ initial: 4, maximum: devkitMode ? 16 : 4 });
let memU8 = U8(memory.buffer);
let importObject = {
env: {
memory
},
};
let loader;
let loadModuleData = (data) => {
if (loader && (!devkitMode || U8(data)[0] != 0)) {
memU8.set(U8(data));
let length = loader.exports.load_uw8(data.byteLength);
data = new ArrayBuffer(length);
U8(data).set(memU8.slice(0, length));
}
return data;
}
let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance;
let loadModuleURL = async (url) => instantiate(loadModuleData(await (await fetch(url)).arrayBuffer()));
loader = await loadModuleURL(loaderUrl);
for (let n of ['acos', 'asin', 'atan', 'atan2', 'cos', 'exp', 'log', 'sin', 'tan', 'pow']) {
importObject.env[n] = Math[n];
}
for (let i = 9; i < 64; ++i) {
importObject.env['reserved' + i] = () => { };
}
for (let i = 0; i < 16; ++i) {
importObject.env['g_reserved' + i] = 0;
}
data = loadModuleData(data);
let platform_instance = await loadModuleURL(platformUrl);
for (let name in platform_instance.exports) {
importObject.env[name] = platform_instance.exports[name]
}
let instance = await instantiate(data);
let buffer = U32(imageData.data.buffer);
let startTime = Date.now();
let keepRunning = true;
cancelFunction = () => keepRunning = false;
const timePerFrame = 1000 / 60;
let nextFrame = startTime;
function mainloop() {
if (!keepRunning) {
return;
}
try {
let now = Date.now();
let restart = false;
if (now >= nextFrame) {
let gamepads = navigator.getGamepads();
let gamepad = 0;
for (let i = 0; i < 4; ++i) {
let pad = gamepads[i];
if (!pad) {
continue;
}
for (let j = 0; j < 8; ++j) {
let buttonIdx = (j + 12) % 16;
if (pad.buttons.length > buttonIdx && pad.buttons[buttonIdx].pressed) {
gamepad |= 1 << (i * 8 + j);
}
}
if (pad.axes.length > 1) {
for (let j = 0; j < 4; ++j) {
let v = pad.axes[1 - (j >> 1)];
if (((j & 1) ? v : -v) > 0.5) {
gamepad |= 1 << (i * 8 + j);
}
}
}
if (pad.buttons.length > 9 && pad.buttons[9].pressed) {
restart = true;
}
}
let u32Mem = U32(memory.buffer);
u32Mem[16] = now - startTime;
u32Mem[17] = pad | gamepad;
instance.exports.upd();
platform_instance.exports.endFrame();
let palette = U32(memory.buffer.slice(0x13000, 0x13000 + 1024));
for (let i = 0; i < 320 * 240; ++i) {
buffer[i] = palette[memU8[i + 120]] | 0xff000000;
}
canvasCtx.putImageData(imageData, 0, 0);
nextFrame = Math.max(nextFrame + timePerFrame, now);
}
if (restart) {
runModule(currentData);
} else {
window.requestAnimationFrame(mainloop);
}
} catch (err) {
config.setMessage(cartridgeSize, err.toString());
}
}
mainloop();
} catch (err) {
config.setMessage(cartridgeSize, err.toString());
}
}
function downloadBlob(blob, ext) {
let a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'microw8_' + new Date().toISOString() + ext;
a.click();
URL.revokeObjectURL(a.href);
}
let videoRecorder;
let videoStartTime;
function recordVideo() {
if(videoRecorder) {
videoRecorder.stop();
videoRecorder = null;
return;
}
videoRecorder = new MediaRecorder(screen.captureStream(), {
mimeType: 'video/webm',
videoBitsPerSecond: 25000000
});
let chunks = [];
videoRecorder.ondataavailable = e => {
chunks.push(e.data);
};
let timer = config.timerElement;
if(timer) {
timer.hidden = false;
timer.innerText = "00:00";
}
videoRecorder.onstop = () => {
if(timer) {
timer.hidden = true;
}
downloadBlob(new Blob(chunks, {type: 'video/webm'}), '.webm');
};
videoRecorder.start();
videoStartTime = Date.now();
function updateTimer() {
if(!videoStartTime) {
return;
}
if(timer) {
let duration = Math.floor((Date.now() - videoStartTime) / 1000);
timer.innerText = Math.floor(duration / 60).toString().padStart(2, '0') + ':' + (duration % 60).toString().padStart(2, '0');
}
setTimeout(updateTimer, 1000);
}
setTimeout(updateTimer, 1000);
}
async function runModuleFromURL(url, keepUrl) {
let response = await fetch(url);
let type = response.headers.get('Content-Type');
if(type && type.includes('html')) {
throw false;
}
runModule(await response.arrayBuffer(), keepUrl || devkitMode);
}
return {
runModule,
runModuleFromURL,
setDevkitMode: (m) => devkitMode = m,
};
}
this.uw8 = MicroW8;