diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index db1b241a..63564f1b 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -12,6 +12,10 @@ const MTY = { glObj: {}, fds: {}, fdIndex: 0, + jspi: typeof WebAssembly !== 'undefined' && + typeof WebAssembly.Suspending === 'function' && + typeof WebAssembly.promising === 'function', + promising: {}, }; @@ -36,6 +40,50 @@ function mty_dup_c(buf) { return ptr; } +// Wraps the calling function to allow a Promise to be returned (if needed). +// Fallback to a direct call if JSPI is not supported or the export cannot be wrapped. +async function mty_call_export(name, ...args) { + const fn = MTY.exports[name]; + + if (!fn) + throw new Error('Missing export: ' + name); + + if (!MTY.jspi) + return fn(...args); + + if (!MTY.promising[name]) { + try { + MTY.promising[name] = WebAssembly.promising(fn); + + } catch (e) { + console.warn('JSPI export wrap failed for', name, e); + MTY.jspi = false; + return fn(...args); + } + } + + return await MTY.promising[name](...args); +} + +async function mty_web_run_and_yield_async(iter, opaque) { + MTY.exports.mty_app_set_keys(); + + const cfunc = mty_cfunc(iter); + + await new Promise(resolve => { + const step = () => { + if (cfunc(opaque)) { + setTimeout(step, 0); + + } else { + resolve(); + } + }; + + step(); + }); +} + // window.localStorage @@ -313,7 +361,28 @@ function mty_mutex_unlock(mutex, index, notify) { } const MTY_AUDIO_API = { - MTY_AudioCreate: function (format, minBuffer, maxBuffer, deviceID, fallback) { + MTY_AudioCreate: function (formatPtr, minBuffer, maxBuffer, deviceID, fallback) { + if (formatPtr === null || formatPtr === undefined) + return 0; + + let format; + if (typeof formatPtr === 'number') { + const memoryBuffer = new DataView(MTY_MEMORY.buffer); + format = { + channels: memoryBuffer.getUint32(formatPtr, true), + sampleRate: memoryBuffer.getUint32(formatPtr + 4, true), + }; + + } else if (typeof formatPtr === 'object') { + format = { + channels: formatPtr.channels, + sampleRate: formatPtr.sampleRate, + }; + + } else { + throw new Error('Invalid formatPtr type'); + } + MTY.audio = { sampleRate: format.sampleRate, minBuffer, @@ -804,7 +873,10 @@ const MTY_WEB_API = { MTY.app = app; mty_update_window(app, MTY.initWindowInfo); }, + + // Fallback in case the browser does not support JSPI. web_run_and_yield: function (iter, opaque) { + console.warn('JSPI not supported. Fallback to old behavior. This may cause issues with blocking calls.'); MTY.exports.mty_app_set_keys(); const step = () => { @@ -813,7 +885,9 @@ const MTY_WEB_API = { }; setTimeout(step, 0); - throw 'MTY_RunAndYield halted execution'; + // Throw exception to ensure execution does not halt when this function finishes. + // NOTE: This will end up blocking the caller indefinitely. + throw 'run_and_yield halted execution'; }, }; @@ -1090,6 +1164,15 @@ async function mty_instantiate_wasm(wasmBuf, userEnv) { }, } + if (MTY.jspi) { + try { + imports.env.web_run_and_yield = new WebAssembly.Suspending(mty_web_run_and_yield_async); + } catch (e) { + console.warn('JSPI import setup failed, falling back to sync imports', e); + MTY.jspi = false; + } + } + // Add userEnv to imports, run on the main thread for (let x = 0; x < userEnv.length; x++) { const key = userEnv[x]; @@ -1145,17 +1228,18 @@ onmessage = async (ev) => { try { // Additional thread if (msg.startArg) { - MTY.exports.wasi_thread_start(msg.threadId, msg.startArg); + await mty_call_export('wasi_thread_start', msg.threadId, msg.startArg); // Main thread } else { - MTY.exports._start(); + await mty_call_export('_start'); } close(); } catch (e) { - if (e.toString().search('MTY_RunAndYield') == -1) + // Ignore known exception that we throw to continue thread execution + if (e.toString().search('run_and_yield halted execution') == -1) console.error(e); } break; diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 90229262..9f595cb7 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -530,7 +530,7 @@ function mty_set_rgba_cursor(buf, width, height, hot_x, hot_y) { if (buf) { if (!MTY.ccanvas) { MTY.ccanvas = document.createElement('canvas'); - MTY.cctx = MTY.ccanvas.getContext('2d'); + MTY.cctx = MTY.ccanvas.getContext('2d', {"willReadFrequently": true}); } MTY.ccanvas.width = width;