From 9fe89b70060d4caf928bc9529f0bdc6ddf3fb519 Mon Sep 17 00:00:00 2001 From: Julien Loir <6706489+Namaneo@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:13:18 +0100 Subject: [PATCH 01/30] Feature - [WASM] Webview support --- src/unix/web/matoya-worker.js | 30 +++++++++ src/unix/web/matoya.js | 46 +++++++++++++ src/unix/web/webview.c | 123 ++++++++++++++++++++++++++++++++-- 3 files changed, 195 insertions(+), 4 deletions(-) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 1dff330e..cfe64bdb 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -815,6 +815,31 @@ const MTY_WEB_API = { setTimeout(step, 0); throw 'MTY_RunAndYield halted execution'; }, + web_webview_create: function(ctx) { + postMessage({type: 'wv-create', ctx}); + }, + web_webview_destroy: function() { + postMessage({type: 'wv-destroy'}); + }, + web_webview_navigate: function(csource, url) { + const source = mty_str_to_js(csource); + postMessage({type: 'wv-navigate', source, url}); + }, + web_webview_show: function(show) { + postMessage({type: 'wv-show', show}); + }, + web_webview_is_visible: function() { + postMessage({type: 'wv-is-visible', sync: MTY.sync, sab: MTY.sab}); + mty_wait(MTY.sync); + return MTY.sab[0] != 0; + }, + web_webview_send_text: function(cmessage) { + const message = mty_str_to_js(cmessage); + postMessage({type: 'wv-send-text', message}); + }, + web_webview_reload: function() { + postMessage({type: 'wv-reload'}); + }, }; @@ -1235,6 +1260,11 @@ onmessage = async (ev) => { mty_free(cmem); break; } + case 'wv-event': + const buf = mty_alloc(1, msg.message.length + 1); + mty_str_to_c(msg.message, buf, msg.message.length + 1); + MTY.exports.mty_webview_handle_event(msg.ctx, buf); + break; } }; diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 90229262..3c802287 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -1082,5 +1082,51 @@ async function mty_thread_message(ev) { mty_signal(msg.sync); break; + case 'wv-create': + MTY.webview = document.createElement('iframe'); + + MTY.webview.style.visibility = 'hidden'; + MTY.webview.style.position = 'fixed'; + MTY.webview.style.border = 'none'; + MTY.webview.style.width = '100%'; + MTY.webview.style.height = '100%'; + MTY.webview.style.inset = '0'; + + MTY.webview.onload = () => { + setTimeout(() => MTY.webview.style.visibility = 'visible', 250); + }; + + window.addEventListener('message', function (message) { + MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message.data}); + }); + + document.body.appendChild(MTY.webview); + break; + case 'wv-destroy': + document.removeChild(MTY.webview); + delete MTY.webview; + break; + case 'wv-navigate': + if (msg.url) { + MTY.webview.src = msg.source; + + } else { + const blob = new Blob([msg.source], { type: 'text/html' }); + MTY.webview.src = URL.createObjectURL(blob); + } + break; + case 'wv-show': + MTY.webview.style.visibility = msg.show ? 'visible' : 'hidden'; + break; + case 'wv-is-visible': + msg.sab[0] = MTY.webview.style.visibility != 'visible'; + mty_signal(msg.sync); + break; + case 'wv-send-text': + MTY.webview.contentWindow.postMessage(msg.message, '*'); + break; + case 'wv-reload': + MTY.webview.contentWindow.location.reload(); + break; } } diff --git a/src/unix/web/webview.c b/src/unix/web/webview.c index 34bde07a..8c20fbde 100644 --- a/src/unix/web/webview.c +++ b/src/unix/web/webview.c @@ -4,39 +4,98 @@ #include "webview.h" +#include +#include + +#include "matoya.h" +#include "web/keymap.h" + +struct webview { + MTY_App *app; + MTY_Window window; + WEBVIEW_READY ready_func; + WEBVIEW_TEXT text_func; + WEBVIEW_KEY key_func; + MTY_Hash *keys; + MTY_Queue *pushq; + bool ready; + bool passthrough; +}; + +void web_webview_create(struct webview *ctx); +void web_webview_destroy(); +void web_webview_navigate(const char *source, bool url); +void web_webview_show(bool show); +bool web_webview_is_visible(); +void web_webview_send_text(const char *message); +void web_webview_reload(); + struct webview *mty_webview_create(MTY_App *app, MTY_Window window, const char *dir, bool debug, WEBVIEW_READY ready_func, WEBVIEW_TEXT text_func, WEBVIEW_KEY key_func) { - return NULL; + struct webview *ctx = MTY_Alloc(1, sizeof(struct webview)); + + ctx->app = app; + ctx->window = window; + ctx->ready_func = ready_func; + ctx->text_func = text_func; + ctx->key_func = key_func; + + ctx->keys = web_keymap_hash(); + ctx->pushq = MTY_QueueCreate(50, 0); + + web_webview_create(ctx); + + return ctx; } void mty_webview_destroy(struct webview **webview) { + if (!webview || !*webview) + return; + + struct webview *ctx = *webview; + *webview = NULL; + + web_webview_destroy(); + + if (ctx->pushq) + MTY_QueueFlush(ctx->pushq, MTY_Free); + + MTY_QueueDestroy(&ctx->pushq); + MTY_HashDestroy(&ctx->keys, NULL); + + MTY_Free(ctx); } void mty_webview_navigate(struct webview *ctx, const char *source, bool url) { + web_webview_navigate(source, url); } void mty_webview_show(struct webview *ctx, bool show) { + web_webview_show(show); } bool mty_webview_is_visible(struct webview *ctx) { - return false; + return web_webview_is_visible(); } void mty_webview_send_text(struct webview *ctx, const char *msg) { + web_webview_send_text(msg); } void mty_webview_reload(struct webview *ctx) { + web_webview_reload(); } void mty_webview_set_input_passthrough(struct webview *ctx, bool passthrough) { + ctx->passthrough = passthrough; } bool mty_webview_event(struct webview *ctx, MTY_Event *evt) @@ -54,7 +113,7 @@ void mty_webview_render(struct webview *ctx) bool mty_webview_is_focussed(struct webview *ctx) { - return false; + return true; } bool mty_webview_is_steam(void) @@ -64,5 +123,61 @@ bool mty_webview_is_steam(void) bool mty_webview_is_available(void) { - return false; + return true; +} + +__attribute__((export_name("mty_webview_handle_event"))) +void mty_webview_handle_event(struct webview *ctx, char *str) +{ + MTY_JSON *j = NULL; + + switch (str[0]) { + // MTY_EVENT_WEBVIEW_READY + case 'R': + ctx->ready = true; + + // Send any queued messages before the WebView became ready + for (char *msg = NULL; MTY_QueuePopPtr(ctx->pushq, 0, (void **) &msg, NULL);) { + mty_webview_send_text(ctx, msg); + MTY_Free(msg); + } + + ctx->ready_func(ctx->app, ctx->window); + break; + + // MTY_EVENT_WEBVIEW_TEXT + case 'T': + ctx->text_func(ctx->app, ctx->window, str + 1); + break; + + // MTY_EVENT_KEY + case 'D': + case 'U': + if (!ctx->passthrough) + break; + + j = MTY_JSONParse(str + 1); + if (!j) + break; + + const char *code = MTY_JSONObjGetStringPtr(j, "code"); + if (!code) + break; + + uint32_t jmods = 0; + if (!MTY_JSONObjGetInt(j, "mods", (int32_t *) &jmods)) + break; + + MTY_Key key = (MTY_Key) (uintptr_t) MTY_HashGet(ctx->keys, code) & 0xFFFF; + if (key == MTY_KEY_NONE) + break; + + MTY_Mod mods = web_keymap_mods(jmods); + + ctx->key_func(ctx->app, ctx->window, str[0] == 'D', key, mods); + break; + } + + MTY_JSONDestroy(&j); + MTY_Free(str); } From 64fe12bf722169e065b81f5107bb52ed4110c83e Mon Sep 17 00:00:00 2001 From: Samuel Tranchet Date: Fri, 5 Dec 2025 17:53:28 +0100 Subject: [PATCH 02/30] ignore vscode stuff here --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 53d76920..28468049 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.vscode/* + *.o *.so *.obj From 676756f51866a6f96e69afac6fe34ceb63b7a565 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Wed, 17 Dec 2025 13:02:04 -0700 Subject: [PATCH 03/30] wip -- stuff is working --- src/swebview.c | 2 +- src/unix/apple/ws.m | 52 +++++++++++++++---- src/unix/web/matoya-worker.js | 5 ++ src/unix/web/matoya.js | 95 +++++++++++++++++++++++++++++++++-- src/unix/web/webview.c | 3 ++ 5 files changed, 141 insertions(+), 16 deletions(-) diff --git a/src/swebview.c b/src/swebview.c index eb9b9b26..253ce1f3 100644 --- a/src/swebview.c +++ b/src/swebview.c @@ -188,7 +188,7 @@ static void finished_request_run0(void *This, void *pvParam) "};" "window.MTY_NativeSendText = text => {" - "alert('T' + text);" + "alert('T' + text);alert('I AM HERE!!!');" "};" "alert('R');" diff --git a/src/unix/apple/ws.m b/src/unix/apple/ws.m index e0d6b79a..8104b9a7 100644 --- a/src/unix/apple/ws.m +++ b/src/unix/apple/ws.m @@ -33,16 +33,16 @@ static void websocket_URLSession_task_didCompleteWithError(id self, SEL _cmd, NS if (!error) return; - MTY_Log("'URLSession:task:didCompleteWithError' fired with code %ld", error.code); + MTY_Log("'URLSession:task:didCompleteWithError' fired with code %ld. Not closing the session though!", error.code); MTY_WebSocket *ctx = OBJC_CTX(); if (!ctx) return; - ctx->closed = true; + // ctx->closed = true; - if (ctx->conn) - MTY_WaitableSignal(ctx->conn); + // if (ctx->conn) + // MTY_WaitableSignal(ctx->conn); } static void websocket_URLSession_webSocketTask_didOpenWithProtocol(id self, SEL _cmd, NSURLSession *session, @@ -52,6 +52,8 @@ static void websocket_URLSession_webSocketTask_didOpenWithProtocol(id self, SEL if (ctx && ctx->conn) MTY_WaitableSignal(ctx->conn); + + MTY_Log("didOpenWithProtocol ---- %s", [protocol UTF8String]); } static void websocket_URLSession_webSocketTask_didCloseWithCode_reason(id self, SEL _cmd, NSURLSession *session, @@ -63,6 +65,17 @@ static void websocket_URLSession_webSocketTask_didCloseWithCode_reason(id self, ctx->closed = true; + // Convert reason data to a UTF-8 string if available + const char *reasonCString = NULL; + if (reason != nil && reason.length > 0) { + NSString *reasonString = [[NSString alloc] initWithData:reason encoding:NSUTF8StringEncoding]; + reasonCString = reasonString ? [reasonString UTF8String] : "(non-UTF8 data)"; + } else { + reasonCString = "(no reason)"; + } + + MTY_Log("didCloseWithCode_reason ---- %d --- %s", (int)closeCode, reasonCString); + if (closeCode != NSURLSessionWebSocketCloseCodeNormalClosure) MTY_Log("'URLSession:webSocketTask:didCloseWithCode' fired with closeCode %ld\n", closeCode); } @@ -73,6 +86,8 @@ static void websocket_URLSession_didBecomeInvalidWithError(id self, SEL _cmd, NS if (ctx && ctx->conn) MTY_WaitableSignal(ctx->conn); + + MTY_Log("didBecomeInvalidWithError ---- %s", error ? [error.localizedDescription UTF8String] : "(no error)"); } static Class websocket_class(void) @@ -119,6 +134,9 @@ static Class websocket_class(void) // Session configuration NSURLSessionConfiguration *cfg = net_configuration(proxy); + printf("OLD NETWORK SERVICE TYPE: %d %s\n", (int) cfg.networkServiceType, url); + cfg.networkServiceType = NSURLNetworkServiceTypeResponsiveData; + printf("NEW NETWORK SERVICE TYPE: %d\n", (int) cfg.networkServiceType); // Connect ctx->session = [NSURLSession sessionWithConfiguration:cfg @@ -133,6 +151,8 @@ static Class websocket_class(void) // Upgrade status NSHTTPURLResponse *response = (NSHTTPURLResponse *) ctx->task.response; + MTY_Log("SETTING UP NEW WEBSOCKET CONNECTION ---- %u", response.statusCode); + if (response) *upgradeStatus = response.statusCode; @@ -149,8 +169,11 @@ void MTY_WebSocketDestroy(MTY_WebSocket **webSocket) MTY_WebSocket *ctx = *webSocket; - if (ctx->task) - [ctx->task cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure reason:nil]; + if (ctx->task) { + MTY_Log("Sending client cancel with close code"); + NSData *reasonData = [@"client initiated disconnect" dataUsingEncoding:NSUTF8StringEncoding]; + [ctx->task cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure reason:reasonData]; + } if (ctx->session) { id delegate = [ctx->session delegate]; @@ -173,6 +196,8 @@ void MTY_WebSocketDestroy(MTY_WebSocket **webSocket) MTY_Free(ctx->msg); MTY_Free(ctx); *webSocket = NULL; + + MTY_Log("Calling MTY_WebSocketDestroy"); } MTY_Async MTY_WebSocketRead(MTY_WebSocket *ctx, uint32_t timeout, char *msg, size_t size) @@ -182,11 +207,13 @@ MTY_Async MTY_WebSocketRead(MTY_WebSocket *ctx, uint32_t timeout, char *msg, siz if (MTY_TimeDiff(ctx->last_ping, now) > WS_PING_INTERVAL) { [ctx->task sendPingWithPongReceiveHandler:^(NSError *e) { + MTY_Log("I SENT THE PING ---- %u %lld", time(NULL), now); if (e) { - MTY_Log("NSURLSessionWebSocketTask:sendPingWithPongReceiveHandler failed: %s", - [e.localizedDescription UTF8String]); + MTY_Log("NSURLSessionWebSocketTask:sendPingWithPongReceiveHandler failed: %s. Code is %d", + [e.localizedDescription UTF8String], (int) e.code); } else { + MTY_Log("I GOT THE PONG ---- %u %lld", time(NULL), now); ctx->last_pong = MTY_GetTime(); } }]; @@ -195,12 +222,17 @@ MTY_Async MTY_WebSocketRead(MTY_WebSocket *ctx, uint32_t timeout, char *msg, siz } // If we haven't gotten a pong within WS_PONG_TO, error - if (MTY_TimeDiff(ctx->last_pong, now) > WS_PONG_TO) + if (MTY_TimeDiff(ctx->last_pong, now) > WS_PONG_TO) { + MTY_Log("WebSocket pong timeout %lld %lld", now, ctx->last_pong); return MTY_ASYNC_ERROR; + } // WebSocket is already closed - if (ctx->closed || ctx->task.closeCode != NSURLSessionWebSocketCloseCodeInvalid) + if (ctx->closed || ctx->task.closeCode != NSURLSessionWebSocketCloseCodeInvalid) { + MTY_Log("WebSocket is closed %d", (int)ctx->task.closeCode); return MTY_ASYNC_DONE; + } + // Set completion handler and sempaphore if (!ctx->read_started) { diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 5f429479..22a1c8c9 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -816,6 +816,7 @@ const MTY_WEB_API = { throw 'MTY_RunAndYield halted execution'; }, web_webview_create: function(ctx) { + console.log("I AM ABOUT TO CREATE THE WEBVIEW!!!"); postMessage({type: 'wv-create', ctx}); }, web_webview_destroy: function() { @@ -1142,6 +1143,9 @@ async function mty_instantiate_wasm(wasmBuf, userEnv) { } onmessage = async (ev) => { + // if (ev.data.type != 'window-update') { + // console.log("I HAVE RECEIVED THIS MESSAGE:", ev.data, ev.data.type); + // } const msg = ev.data; switch (msg.type) { @@ -1263,6 +1267,7 @@ onmessage = async (ev) => { case 'wv-event': const buf = mty_alloc(1, msg.message.length + 1); mty_str_to_c(msg.message, buf, msg.message.length + 1); + console.log("WebView Event:", buf, msg.message); MTY.exports.mty_webview_handle_event(msg.ctx, buf); break; } diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 3c802287..1878c3ad 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -893,6 +893,9 @@ async function MTY_Start(bin, container, userEnv) { // Add input events mty_add_input_events(MTY.mainThread); + console.log('MTY Started - posting ready message now!'); + window.postMessage('R'); + return true; } @@ -1096,10 +1099,41 @@ async function mty_thread_message(ev) { setTimeout(() => MTY.webview.style.visibility = 'visible', 250); }; - window.addEventListener('message', function (message) { - MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message.data}); + + window.addEventListener('message', (event) => { + console.log("RECEIVED MESSAGE FROM WEBVIEW IN THE IFRAME: ", msg.ctx, event.data); + MTY.webview.contentWindow.MTY_NativeListener(event.data); }); + window.postWVMessage = (message) => { + MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message}); + } + + + // window.addEventListener('message', function (message) { + // console.log("RECEIVED MESSAGE FROM WEBVIEW: ", msg.ctx, message.data); + // MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message.data}); + // }); + + + + + + // console.log(MTY.webview.srcdoc); + + // MTY.webview.sandbox = 'allow-scripts allow-same-origin'; + // MTY.webview.srcdoc = ` + // `); + } else { + html = `${baseTag}${html}`; + } + iframe.srcdoc = html; +} \ No newline at end of file diff --git a/src/unix/web/webview.c b/src/unix/web/webview.c index 8c20fbde..df7d6bdc 100644 --- a/src/unix/web/webview.c +++ b/src/unix/web/webview.c @@ -35,6 +35,8 @@ struct webview *mty_webview_create(MTY_App *app, MTY_Window window, const char * { struct webview *ctx = MTY_Alloc(1, sizeof(struct webview)); + printf("Creating UNIX webview\n"); + ctx->app = app; ctx->window = window; ctx->ready_func = ready_func; @@ -130,6 +132,7 @@ __attribute__((export_name("mty_webview_handle_event"))) void mty_webview_handle_event(struct webview *ctx, char *str) { MTY_JSON *j = NULL; + printf("RECEIVED WEBVIEW EVENT: %s\n", str); switch (str[0]) { // MTY_EVENT_WEBVIEW_READY From d72f7f8edb3eeafa189c7185b03fc0acbe6f8f70 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Wed, 17 Dec 2025 13:07:47 -0700 Subject: [PATCH 04/30] cleanup several event listeners --- src/unix/web/matoya.js | 50 ++++-------------------------------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 1878c3ad..6c68b99a 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -1099,41 +1099,10 @@ async function mty_thread_message(ev) { setTimeout(() => MTY.webview.style.visibility = 'visible', 250); }; - - window.addEventListener('message', (event) => { - console.log("RECEIVED MESSAGE FROM WEBVIEW IN THE IFRAME: ", msg.ctx, event.data); - MTY.webview.contentWindow.MTY_NativeListener(event.data); + window.addEventListener('message', function (message) { + MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message.data}); }); - window.postWVMessage = (message) => { - MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message}); - } - - - // window.addEventListener('message', function (message) { - // console.log("RECEIVED MESSAGE FROM WEBVIEW: ", msg.ctx, message.data); - // MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message.data}); - // }); - - - - - - // console.log(MTY.webview.srcdoc); - - // MTY.webview.sandbox = 'allow-scripts allow-same-origin'; - // MTY.webview.srcdoc = ` - // `); + html = html.replace(/]*)>/i, (m, attrs) => `${baseTag}`); } else { html = `${baseTag}${html}`; } From a04c48c43efb67d5b3ba0be5571a131fda1a79ec Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Wed, 17 Dec 2025 13:13:35 -0700 Subject: [PATCH 05/30] remove log statements --- src/unix/web/matoya-worker.js | 1 - src/unix/web/matoya.js | 14 +++----------- src/unix/web/webview.c | 1 - 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 22a1c8c9..cae7a703 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -1267,7 +1267,6 @@ onmessage = async (ev) => { case 'wv-event': const buf = mty_alloc(1, msg.message.length + 1); mty_str_to_c(msg.message, buf, msg.message.length + 1); - console.log("WebView Event:", buf, msg.message); MTY.exports.mty_webview_handle_event(msg.ctx, buf); break; } diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 6c68b99a..65f1ebe6 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -893,9 +893,6 @@ async function MTY_Start(bin, container, userEnv) { // Add input events mty_add_input_events(MTY.mainThread); - console.log('MTY Started - posting ready message now!'); - window.postMessage('R'); - return true; } @@ -1112,11 +1109,7 @@ async function mty_thread_message(ev) { case 'wv-navigate': if (msg.url) { // MTY.webview.src = msg.source; - loadIframeFromUrlSrcdoc(MTY.webview, msg.source).then(() => { - console.log("I AM IN wv-navigate after loadIframeFromUrlSrcdoc"); - // MTY.webview.contentWindow.MTY_NativeSendText = MTY_NativeSendText; - - }); + loadIframeFromUrlSrcdoc(MTY.webview, msg.source) } else { const blob = new Blob([msg.source], { type: 'text/html' }); MTY.webview.src = URL.createObjectURL(blob); @@ -1130,9 +1123,8 @@ async function mty_thread_message(ev) { mty_signal(msg.sync); break; case 'wv-send-text': - // console.log("I AM IN wv-send-text", msg.message); + // wv-send-text sends native app messages back to the running UI MTY.webview.contentWindow.MTY_NativeListener(msg.message); - // MTY.webview.contentWindow.postMessage(msg.message); break; case 'wv-reload': MTY.webview.contentWindow.location.reload(); @@ -1167,7 +1159,7 @@ async function loadIframeFromUrlSrcdoc(iframe, url, fetchOpts = {}) { // Ensure a so relative URLs inside the HTML resolve to the original URL const baseTag = ``; if (/]/i.test(html)) { - html = html.replace(/]*)>/i, (m, attrs) => `${baseTag}`); + html = html.replace(/]*)>/i, (m, attrs) => `${baseTag}`); } else { html = `${baseTag}${html}`; } diff --git a/src/unix/web/webview.c b/src/unix/web/webview.c index df7d6bdc..0a7ac4c9 100644 --- a/src/unix/web/webview.c +++ b/src/unix/web/webview.c @@ -132,7 +132,6 @@ __attribute__((export_name("mty_webview_handle_event"))) void mty_webview_handle_event(struct webview *ctx, char *str) { MTY_JSON *j = NULL; - printf("RECEIVED WEBVIEW EVENT: %s\n", str); switch (str[0]) { // MTY_EVENT_WEBVIEW_READY From cd8e44f3ae8bd1428158b15c827288fcc896bd4d Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Wed, 17 Dec 2025 13:14:27 -0700 Subject: [PATCH 06/30] more cleanup --- src/swebview.c | 2 +- src/unix/apple/ws.m | 52 +++++++++------------------------------------ 2 files changed, 11 insertions(+), 43 deletions(-) diff --git a/src/swebview.c b/src/swebview.c index 253ce1f3..eb9b9b26 100644 --- a/src/swebview.c +++ b/src/swebview.c @@ -188,7 +188,7 @@ static void finished_request_run0(void *This, void *pvParam) "};" "window.MTY_NativeSendText = text => {" - "alert('T' + text);alert('I AM HERE!!!');" + "alert('T' + text);" "};" "alert('R');" diff --git a/src/unix/apple/ws.m b/src/unix/apple/ws.m index 8104b9a7..e0d6b79a 100644 --- a/src/unix/apple/ws.m +++ b/src/unix/apple/ws.m @@ -33,16 +33,16 @@ static void websocket_URLSession_task_didCompleteWithError(id self, SEL _cmd, NS if (!error) return; - MTY_Log("'URLSession:task:didCompleteWithError' fired with code %ld. Not closing the session though!", error.code); + MTY_Log("'URLSession:task:didCompleteWithError' fired with code %ld", error.code); MTY_WebSocket *ctx = OBJC_CTX(); if (!ctx) return; - // ctx->closed = true; + ctx->closed = true; - // if (ctx->conn) - // MTY_WaitableSignal(ctx->conn); + if (ctx->conn) + MTY_WaitableSignal(ctx->conn); } static void websocket_URLSession_webSocketTask_didOpenWithProtocol(id self, SEL _cmd, NSURLSession *session, @@ -52,8 +52,6 @@ static void websocket_URLSession_webSocketTask_didOpenWithProtocol(id self, SEL if (ctx && ctx->conn) MTY_WaitableSignal(ctx->conn); - - MTY_Log("didOpenWithProtocol ---- %s", [protocol UTF8String]); } static void websocket_URLSession_webSocketTask_didCloseWithCode_reason(id self, SEL _cmd, NSURLSession *session, @@ -65,17 +63,6 @@ static void websocket_URLSession_webSocketTask_didCloseWithCode_reason(id self, ctx->closed = true; - // Convert reason data to a UTF-8 string if available - const char *reasonCString = NULL; - if (reason != nil && reason.length > 0) { - NSString *reasonString = [[NSString alloc] initWithData:reason encoding:NSUTF8StringEncoding]; - reasonCString = reasonString ? [reasonString UTF8String] : "(non-UTF8 data)"; - } else { - reasonCString = "(no reason)"; - } - - MTY_Log("didCloseWithCode_reason ---- %d --- %s", (int)closeCode, reasonCString); - if (closeCode != NSURLSessionWebSocketCloseCodeNormalClosure) MTY_Log("'URLSession:webSocketTask:didCloseWithCode' fired with closeCode %ld\n", closeCode); } @@ -86,8 +73,6 @@ static void websocket_URLSession_didBecomeInvalidWithError(id self, SEL _cmd, NS if (ctx && ctx->conn) MTY_WaitableSignal(ctx->conn); - - MTY_Log("didBecomeInvalidWithError ---- %s", error ? [error.localizedDescription UTF8String] : "(no error)"); } static Class websocket_class(void) @@ -134,9 +119,6 @@ static Class websocket_class(void) // Session configuration NSURLSessionConfiguration *cfg = net_configuration(proxy); - printf("OLD NETWORK SERVICE TYPE: %d %s\n", (int) cfg.networkServiceType, url); - cfg.networkServiceType = NSURLNetworkServiceTypeResponsiveData; - printf("NEW NETWORK SERVICE TYPE: %d\n", (int) cfg.networkServiceType); // Connect ctx->session = [NSURLSession sessionWithConfiguration:cfg @@ -151,8 +133,6 @@ static Class websocket_class(void) // Upgrade status NSHTTPURLResponse *response = (NSHTTPURLResponse *) ctx->task.response; - MTY_Log("SETTING UP NEW WEBSOCKET CONNECTION ---- %u", response.statusCode); - if (response) *upgradeStatus = response.statusCode; @@ -169,11 +149,8 @@ void MTY_WebSocketDestroy(MTY_WebSocket **webSocket) MTY_WebSocket *ctx = *webSocket; - if (ctx->task) { - MTY_Log("Sending client cancel with close code"); - NSData *reasonData = [@"client initiated disconnect" dataUsingEncoding:NSUTF8StringEncoding]; - [ctx->task cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure reason:reasonData]; - } + if (ctx->task) + [ctx->task cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure reason:nil]; if (ctx->session) { id delegate = [ctx->session delegate]; @@ -196,8 +173,6 @@ void MTY_WebSocketDestroy(MTY_WebSocket **webSocket) MTY_Free(ctx->msg); MTY_Free(ctx); *webSocket = NULL; - - MTY_Log("Calling MTY_WebSocketDestroy"); } MTY_Async MTY_WebSocketRead(MTY_WebSocket *ctx, uint32_t timeout, char *msg, size_t size) @@ -207,13 +182,11 @@ MTY_Async MTY_WebSocketRead(MTY_WebSocket *ctx, uint32_t timeout, char *msg, siz if (MTY_TimeDiff(ctx->last_ping, now) > WS_PING_INTERVAL) { [ctx->task sendPingWithPongReceiveHandler:^(NSError *e) { - MTY_Log("I SENT THE PING ---- %u %lld", time(NULL), now); if (e) { - MTY_Log("NSURLSessionWebSocketTask:sendPingWithPongReceiveHandler failed: %s. Code is %d", - [e.localizedDescription UTF8String], (int) e.code); + MTY_Log("NSURLSessionWebSocketTask:sendPingWithPongReceiveHandler failed: %s", + [e.localizedDescription UTF8String]); } else { - MTY_Log("I GOT THE PONG ---- %u %lld", time(NULL), now); ctx->last_pong = MTY_GetTime(); } }]; @@ -222,17 +195,12 @@ MTY_Async MTY_WebSocketRead(MTY_WebSocket *ctx, uint32_t timeout, char *msg, siz } // If we haven't gotten a pong within WS_PONG_TO, error - if (MTY_TimeDiff(ctx->last_pong, now) > WS_PONG_TO) { - MTY_Log("WebSocket pong timeout %lld %lld", now, ctx->last_pong); + if (MTY_TimeDiff(ctx->last_pong, now) > WS_PONG_TO) return MTY_ASYNC_ERROR; - } // WebSocket is already closed - if (ctx->closed || ctx->task.closeCode != NSURLSessionWebSocketCloseCodeInvalid) { - MTY_Log("WebSocket is closed %d", (int)ctx->task.closeCode); + if (ctx->closed || ctx->task.closeCode != NSURLSessionWebSocketCloseCodeInvalid) return MTY_ASYNC_DONE; - } - // Set completion handler and sempaphore if (!ctx->read_started) { From 15bcb7823d95eefed62da8f41ee885ac66de7b2e Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Wed, 17 Dec 2025 13:17:17 -0700 Subject: [PATCH 07/30] final cleanup --- src/unix/web/matoya-worker.js | 4 ---- src/unix/web/matoya.js | 19 ------------------- src/unix/web/webview.c | 2 -- 3 files changed, 25 deletions(-) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index cae7a703..5f429479 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -816,7 +816,6 @@ const MTY_WEB_API = { throw 'MTY_RunAndYield halted execution'; }, web_webview_create: function(ctx) { - console.log("I AM ABOUT TO CREATE THE WEBVIEW!!!"); postMessage({type: 'wv-create', ctx}); }, web_webview_destroy: function() { @@ -1143,9 +1142,6 @@ async function mty_instantiate_wasm(wasmBuf, userEnv) { } onmessage = async (ev) => { - // if (ev.data.type != 'window-update') { - // console.log("I HAVE RECEIVED THIS MESSAGE:", ev.data, ev.data.type); - // } const msg = ev.data; switch (msg.type) { diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 65f1ebe6..38d398f1 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -1132,25 +1132,6 @@ async function mty_thread_message(ev) { } } -function MTY_NativeSendText(text) { - console.log("HI SAM!"); - window.postMessage('T' + text); -} - -async function createBlobUrlFrom(url) { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`Fetch failed: ${res.status} ${res.statusText}`); - } - const blob = await res.blob(); - const objectUrl = URL.createObjectURL(blob); - return { - objectUrl, - blob, - revoke: () => URL.revokeObjectURL(objectUrl), - }; -} - // HACKY FIX TO RESOLVE THE DIFFERENT ORIGIN ISSUE (UI is on :3000, app is on :8000) async function loadIframeFromUrlSrcdoc(iframe, url, fetchOpts = {}) { const res = await fetch(url, fetchOpts); diff --git a/src/unix/web/webview.c b/src/unix/web/webview.c index 0a7ac4c9..8c20fbde 100644 --- a/src/unix/web/webview.c +++ b/src/unix/web/webview.c @@ -35,8 +35,6 @@ struct webview *mty_webview_create(MTY_App *app, MTY_Window window, const char * { struct webview *ctx = MTY_Alloc(1, sizeof(struct webview)); - printf("Creating UNIX webview\n"); - ctx->app = app; ctx->window = window; ctx->ready_func = ready_func; From fe39763712e568567b6d7b00020ace3224410600 Mon Sep 17 00:00:00 2001 From: Callum Watson Date: Wed, 11 Feb 2026 13:14:34 +0000 Subject: [PATCH 08/30] Fix use of struct from WASM in JS for audio format --- src/unix/web/matoya-worker.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 5f429479..36f74946 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -313,7 +313,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, From ee24069b1284d70b78d17dbc3ff09f0c07774ae4 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Fri, 20 Feb 2026 14:09:21 -0700 Subject: [PATCH 09/30] Update flow to enable iframe via src instead of loading the entire document --- src/unix/web/matoya.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 38d398f1..c4c4c716 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -1092,10 +1092,6 @@ async function mty_thread_message(ev) { MTY.webview.style.height = '100%'; MTY.webview.style.inset = '0'; - MTY.webview.onload = () => { - setTimeout(() => MTY.webview.style.visibility = 'visible', 250); - }; - window.addEventListener('message', function (message) { MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message.data}); }); @@ -1103,13 +1099,25 @@ async function mty_thread_message(ev) { document.body.appendChild(MTY.webview); break; case 'wv-destroy': - document.removeChild(MTY.webview); + document.body.removeChild(MTY.webview); delete MTY.webview; break; case 'wv-navigate': if (msg.url) { - // MTY.webview.src = msg.source; - loadIframeFromUrlSrcdoc(MTY.webview, msg.source) + MTY.webview.src = msg.source; + + MTY.webview.onload = () => { + try { + const script = MTY.webview.contentWindow.document.createElement('script'); + script.textContent = "window.parent.postMessage('R');window.MTY_NativeSendText = (text) => { window.parent.postMessage('T' + text); }"; + MTY.webview.contentWindow.document.head.appendChild(script); + setTimeout(() => MTY.webview.style.visibility = 'visible', 250); + } catch (e) { + console.error('Failed to inject script into iframe (cross-origin restriction):', e); + setTimeout(() => MTY.webview.style.visibility = 'visible', 250); + } + }; + // loadIframeFromUrlSrcdoc(MTY.webview, msg.source) } else { const blob = new Blob([msg.source], { type: 'text/html' }); MTY.webview.src = URL.createObjectURL(blob); From 1437d6d1f50a6e4940875b9572989f4438936984 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Mon, 23 Feb 2026 10:28:05 -0700 Subject: [PATCH 10/30] fix focus event --- src/unix/web/matoya.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 38d398f1..0cd5d064 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -324,17 +324,10 @@ function mty_add_input_events(thread) { ev.preventDefault(); }); - window.addEventListener('blur', (ev) => { + document.addEventListener('visibilitychange', (ev) => { thread.postMessage({ type: 'focus', - focus: false, - }); - }); - - window.addEventListener('focus', (ev) => { - thread.postMessage({ - type: 'focus', - focus: true, + focus: document.visibilityState == 'visible', }); }); From 250870b892f55df1a062a0ff1abfa045c71318b8 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Wed, 4 Mar 2026 16:04:37 -0700 Subject: [PATCH 11/30] add new method to run loops on main thread --- src/unix/web/app.c | 6 ++++-- src/unix/web/matoya-worker.js | 11 ++++++++++- src/unix/web/web.h | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/unix/web/app.c b/src/unix/web/app.c index 2b5a9e7f..4af4a4c1 100644 --- a/src/unix/web/app.c +++ b/src/unix/web/app.c @@ -335,7 +335,8 @@ void MTY_AppDestroy(MTY_App **app) void MTY_AppRun(MTY_App *ctx) { - web_run_and_yield(ctx->app_func, ctx->opaque); + // NOTE: This function will never complete / return. + web_run_main_thread(ctx->app_func, ctx->opaque); } void MTY_AppSetTimeout(MTY_App *ctx, uint32_t timeout) @@ -707,5 +708,6 @@ void *MTY_GLGetProcAddress(const char *name) void MTY_RunAndYield(MTY_IterFunc iter, void *opaque) { - web_run_and_yield(iter, opaque); + while(iter(opaque)); + // web_run_and_yield(iter, opaque); } diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 36f74946..f2d7dcfe 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -826,6 +826,14 @@ const MTY_WEB_API = { mty_update_window(app, MTY.initWindowInfo); }, web_run_and_yield: function (iter, opaque) { + // Must run on a non-main thread since a tight loop on the main thread + // would block the event loop and prevent yielding + while (mty_cfunc(iter)(opaque)); + }, + web_run_main_thread: function (iter, opaque) { + // Cannot use web_run_and_yield on the main thread + // since the tight loop would block the event loop and prevent yielding, + // so we use setTimeout to yield after each iteration MTY.exports.mty_app_set_keys(); const step = () => { @@ -834,7 +842,8 @@ const MTY_WEB_API = { }; setTimeout(step, 0); - throw 'MTY_RunAndYield halted execution'; + // Throw exception to ensure execution does not halt when this function finishes. + throw 'run_main_thread halted execution'; }, web_webview_create: function(ctx) { postMessage({type: 'wv-create', ctx}); diff --git a/src/unix/web/web.h b/src/unix/web/web.h index 3fa0adca..e41c8518 100644 --- a/src/unix/web/web.h +++ b/src/unix/web/web.h @@ -26,6 +26,7 @@ char *web_get_clipboard(void); void web_set_clipboard(const char *text); void web_set_title(const char *title); void web_run_and_yield(MTY_IterFunc iter, void *opaque); +void web_run_main_thread(MTY_IterFunc iter, void *opaque); void web_gl_flush(void); void web_set_gfx(void); void web_set_canvas_size(uint32_t width, uint32_t height); From 4a43240de32297026b7404747839093c27d1a769 Mon Sep 17 00:00:00 2001 From: Callum Watson Date: Wed, 11 Feb 2026 13:14:34 +0000 Subject: [PATCH 12/30] Fix use of struct from WASM in JS for audio format --- src/unix/web/matoya-worker.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index db1b241a..94ccc54d 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -313,7 +313,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, From c790b6a5cfe224e6a49ed830f1ce30aa72f686af Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Wed, 4 Mar 2026 16:04:37 -0700 Subject: [PATCH 13/30] add new method to run loops on main thread --- src/unix/web/app.c | 6 ++++-- src/unix/web/matoya-worker.js | 11 ++++++++++- src/unix/web/web.h | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/unix/web/app.c b/src/unix/web/app.c index 2b5a9e7f..4af4a4c1 100644 --- a/src/unix/web/app.c +++ b/src/unix/web/app.c @@ -335,7 +335,8 @@ void MTY_AppDestroy(MTY_App **app) void MTY_AppRun(MTY_App *ctx) { - web_run_and_yield(ctx->app_func, ctx->opaque); + // NOTE: This function will never complete / return. + web_run_main_thread(ctx->app_func, ctx->opaque); } void MTY_AppSetTimeout(MTY_App *ctx, uint32_t timeout) @@ -707,5 +708,6 @@ void *MTY_GLGetProcAddress(const char *name) void MTY_RunAndYield(MTY_IterFunc iter, void *opaque) { - web_run_and_yield(iter, opaque); + while(iter(opaque)); + // web_run_and_yield(iter, opaque); } diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 94ccc54d..306c2d01 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -826,6 +826,14 @@ const MTY_WEB_API = { mty_update_window(app, MTY.initWindowInfo); }, web_run_and_yield: function (iter, opaque) { + // Must run on a non-main thread since a tight loop on the main thread + // would block the event loop and prevent yielding + while (mty_cfunc(iter)(opaque)); + }, + web_run_main_thread: function (iter, opaque) { + // Cannot use web_run_and_yield on the main thread + // since the tight loop would block the event loop and prevent yielding, + // so we use setTimeout to yield after each iteration MTY.exports.mty_app_set_keys(); const step = () => { @@ -834,7 +842,8 @@ const MTY_WEB_API = { }; setTimeout(step, 0); - throw 'MTY_RunAndYield halted execution'; + // Throw exception to ensure execution does not halt when this function finishes. + throw 'run_main_thread halted execution'; }, }; diff --git a/src/unix/web/web.h b/src/unix/web/web.h index 3fa0adca..e41c8518 100644 --- a/src/unix/web/web.h +++ b/src/unix/web/web.h @@ -26,6 +26,7 @@ char *web_get_clipboard(void); void web_set_clipboard(const char *text); void web_set_title(const char *title); void web_run_and_yield(MTY_IterFunc iter, void *opaque); +void web_run_main_thread(MTY_IterFunc iter, void *opaque); void web_gl_flush(void); void web_set_gfx(void); void web_set_canvas_size(uint32_t width, uint32_t height); From ade1e6d6c0d16f9e5d55a12e2bc3627ba95166f3 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Wed, 4 Mar 2026 16:24:43 -0700 Subject: [PATCH 14/30] cleanup --- src/unix/web/app.c | 2 +- src/unix/web/matoya-worker.js | 5 ----- src/unix/web/web.h | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/unix/web/app.c b/src/unix/web/app.c index 4af4a4c1..d6cb9e0e 100644 --- a/src/unix/web/app.c +++ b/src/unix/web/app.c @@ -706,8 +706,8 @@ void *MTY_GLGetProcAddress(const char *name) return NULL; } +// Cannot be called on the main thread since the tight loop would block the event loop and prevent yielding. void MTY_RunAndYield(MTY_IterFunc iter, void *opaque) { while(iter(opaque)); - // web_run_and_yield(iter, opaque); } diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 306c2d01..ac3808ed 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -825,11 +825,6 @@ const MTY_WEB_API = { MTY.app = app; mty_update_window(app, MTY.initWindowInfo); }, - web_run_and_yield: function (iter, opaque) { - // Must run on a non-main thread since a tight loop on the main thread - // would block the event loop and prevent yielding - while (mty_cfunc(iter)(opaque)); - }, web_run_main_thread: function (iter, opaque) { // Cannot use web_run_and_yield on the main thread // since the tight loop would block the event loop and prevent yielding, diff --git a/src/unix/web/web.h b/src/unix/web/web.h index e41c8518..3ffc16ac 100644 --- a/src/unix/web/web.h +++ b/src/unix/web/web.h @@ -25,7 +25,6 @@ char *web_get_hostname(void); char *web_get_clipboard(void); void web_set_clipboard(const char *text); void web_set_title(const char *title); -void web_run_and_yield(MTY_IterFunc iter, void *opaque); void web_run_main_thread(MTY_IterFunc iter, void *opaque); void web_gl_flush(void); void web_set_gfx(void); From 4d8c9810d777e00f14003c5482b79ad65c5d780c Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Thu, 5 Mar 2026 09:38:27 -0700 Subject: [PATCH 15/30] fix merge issue --- src/unix/web/matoya-worker.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 15849f3d..1f755fed 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -840,6 +840,31 @@ const MTY_WEB_API = { // Throw exception to ensure execution does not halt when this function finishes. throw 'run_main_thread halted execution'; }, + web_webview_create: function(ctx) { + postMessage({type: 'wv-create', ctx}); + }, + web_webview_destroy: function() { + postMessage({type: 'wv-destroy'}); + }, + web_webview_navigate: function(csource, url) { + const source = mty_str_to_js(csource); + postMessage({type: 'wv-navigate', source, url}); + }, + web_webview_show: function(show) { + postMessage({type: 'wv-show', show}); + }, + web_webview_is_visible: function() { + postMessage({type: 'wv-is-visible', sync: MTY.sync, sab: MTY.sab}); + mty_wait(MTY.sync); + return MTY.sab[0] != 0; + }, + web_webview_send_text: function(cmessage) { + const message = mty_str_to_js(cmessage); + postMessage({type: 'wv-send-text', message}); + }, + web_webview_reload: function() { + postMessage({type: 'wv-reload'}); + }, }; From 4150fc1a2ba3bd986ac8c8873b4a267b0ef974c0 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Thu, 5 Mar 2026 11:39:24 -0700 Subject: [PATCH 16/30] remove hackytime fix --- src/unix/web/matoya.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 687075f7..1053bd6e 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -1130,19 +1130,4 @@ async function mty_thread_message(ev) { MTY.webview.contentWindow.location.reload(); break; } -} - -// HACKY FIX TO RESOLVE THE DIFFERENT ORIGIN ISSUE (UI is on :3000, app is on :8000) -async function loadIframeFromUrlSrcdoc(iframe, url, fetchOpts = {}) { - const res = await fetch(url, fetchOpts); - if (!res.ok) throw new Error(`Fetch failed: ${res.status}`); - let html = await res.text(); - // Ensure a so relative URLs inside the HTML resolve to the original URL - const baseTag = ``; - if (/]/i.test(html)) { - html = html.replace(/]*)>/i, (m, attrs) => `${baseTag}`); - } else { - html = `${baseTag}${html}`; - } - iframe.srcdoc = html; } \ No newline at end of file From d6f9430e284e4165aec56ead904d0f41dd19bdc1 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Fri, 6 Mar 2026 15:40:46 -0700 Subject: [PATCH 17/30] fix exception suppression --- src/unix/web/matoya-worker.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index ac3808ed..b1d66984 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -1180,7 +1180,8 @@ onmessage = async (ev) => { close(); } catch (e) { - if (e.toString().search('MTY_RunAndYield') == -1) + // Ignore known exception that we throw to continue main thread execution + if (e.toString().search('run_main_thread halted execution') == -1) console.error(e); } break; From beb6b5d8e2c8053162a17285f6f29a7eed138ab5 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Mon, 9 Mar 2026 15:42:52 -0600 Subject: [PATCH 18/30] Add new window method to set the web build If we just use user-agent, we can't tell if we're on the WebApp or running the webview within the native app. --- src/unix/web/matoya.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 1053bd6e..173f4601 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -1104,6 +1104,9 @@ async function mty_thread_message(ev) { const script = MTY.webview.contentWindow.document.createElement('script'); script.textContent = "window.parent.postMessage('R');window.MTY_NativeSendText = (text) => { window.parent.postMessage('T' + text); }"; MTY.webview.contentWindow.document.head.appendChild(script); + const userAgentScript = MTY.webview.contentWindow.document.createElement('script'); + userAgentScript.textContent = "Object.defineProperty(window, 'MTY_GetPlatform', { value: () => 'web'});"; + MTY.webview.contentWindow.document.head.appendChild(userAgentScript); setTimeout(() => MTY.webview.style.visibility = 'visible', 250); } catch (e) { console.error('Failed to inject script into iframe (cross-origin restriction):', e); From 33e417108f56a6d3826819892adbd8b8e0567509 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Mon, 9 Mar 2026 15:43:31 -0600 Subject: [PATCH 19/30] Fix opening of URLs on webview Need to translate the JS string from C before passing it to the worker. --- src/unix/web/matoya-worker.js | 2 +- src/unix/web/matoya.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 1f755fed..baf231e5 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -680,7 +680,7 @@ const MTY_CRYPTO_API = { const MTY_SYSTEM_API = { MTY_HandleProtocol: function (uri, token) { - postMessage({type: 'uri', uri}); + postMessage({type: 'uri', uri: mty_str_to_js(uri)}); }, }; diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 173f4601..3304ff44 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -990,7 +990,7 @@ async function mty_thread_message(ev) { break; case 'uri': mty_set_action(() => { - window.open(mty_str_to_js(msg.uri), '_blank'); + window.open(msg.uri, '_blank'); }); break; case 'http': { From 02d7fed54ad9368f1534435dc26fd7f8be39502a Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Tue, 10 Mar 2026 06:57:23 -0600 Subject: [PATCH 20/30] implement file deletion Delete is implemented by path_unlink_file. Which had a stub implementation. --- src/unix/web/matoya-worker.js | 10 ++++++++++ src/unix/web/matoya.js | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index baf231e5..3e181e79 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -979,6 +979,16 @@ const MTY_WASI_SNAPSHOT_PREVIEW1_API = { return __WASI_ERRNO_SUCCESS; }, path_unlink_file: function (fd, path) { + const jpath = mty_str_to_js(path); + + postMessage({ + type: 'remove-ls', + key: jpath, + sync: MTY.sync, + }); + + mty_wait(MTY.sync); + return __WASI_ERRNO_SUCCESS; }, path_readlink: function (fd, path, buf, buf_len, retptr0) { diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 3304ff44..94bfbaf0 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -944,6 +944,10 @@ async function mty_thread_message(ev) { window.localStorage[msg.key] = mty_buf_to_b64(msg.val); mty_signal(msg.sync); break; + case 'remove-ls': + window.localStorage.removeItem(msg.key); + mty_signal(msg.sync); + break; case 'alert': mty_alert(msg.title, msg.msg); break; From a8ce4d4c20ea6001ec3fcac3a6a45e7e740c722d Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Thu, 12 Mar 2026 08:30:04 -0600 Subject: [PATCH 21/30] wip - willRead --- src/unix/web/matoya.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 90229262..2f4456bc 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; @@ -665,7 +665,7 @@ async function mty_decode_image(input) { const height = img.naturalHeight; const canvas = new OffscreenCanvas(width, height); - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext('2d', {"willReadFrequently": true}); ctx.drawImage(img, 0, 0, width, height); return ctx.getImageData(0, 0, width, height); From 8ea5f100973c61d8f7029762d5f473dd0f6330ef Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Thu, 12 Mar 2026 14:58:57 -0600 Subject: [PATCH 22/30] wip - direct codex need to cleanup --- src/unix/web/app.c | 2 +- src/unix/web/matoya-worker.js | 94 ++++++++++++++++++++++++++++++++++- src/unix/web/matoya.js | 17 +++++++ src/unix/web/web.h | 1 + 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/src/unix/web/app.c b/src/unix/web/app.c index d6cb9e0e..d68378d6 100644 --- a/src/unix/web/app.c +++ b/src/unix/web/app.c @@ -709,5 +709,5 @@ void *MTY_GLGetProcAddress(const char *name) // Cannot be called on the main thread since the tight loop would block the event loop and prevent yielding. void MTY_RunAndYield(MTY_IterFunc iter, void *opaque) { - while(iter(opaque)); + web_run_and_yield(iter, opaque); } diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index b1d66984..481f4544 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -12,6 +12,8 @@ const MTY = { glObj: {}, fds: {}, fdIndex: 0, + jspi: false, + promising: {}, }; @@ -36,6 +38,64 @@ function mty_dup_c(buf) { return ptr; } +function mty_schedule(func) { + if (typeof scheduler != 'undefined' && typeof scheduler.postTask == 'function') { + scheduler.postTask(func); + + } else { + setTimeout(func, 0); + } +} + +function mty_run_until_false(cfunc, opaque) { + return new Promise(resolve => { + const step = () => { + if (cfunc(opaque)) { + mty_schedule(step); + + } else { + resolve(); + } + }; + + step(); + }); +} + +function mty_supports_jspi() { + return typeof WebAssembly.Suspending == 'function' && typeof WebAssembly.promising == 'function'; +} + +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 mty_run_until_false(cfunc, opaque); +} + // window.localStorage @@ -825,6 +885,19 @@ const MTY_WEB_API = { MTY.app = app; mty_update_window(app, MTY.initWindowInfo); }, + // web_run_and_yield: function (iter, opaque) { + // // Cannot be called on the main thread since the tight loop would block the event loop and prevent yielding. + // // MTY.exports.mty_app_set_keys(); + + // const cfunc = mty_cfunc(iter); + // let count = 0; + + // while (cfunc(opaque)) { + // // Periodically block for 1 ms to reduce allocator/GC pressure in long-running loops. + // if ((++count & 0x3FF) == 0) + // Atomics.wait(MTY.sleeper, 0, 0, 1); + // } + // }, web_run_main_thread: function (iter, opaque) { // Cannot use web_run_and_yield on the main thread // since the tight loop would block the event loop and prevent yielding, @@ -1115,6 +1188,22 @@ async function mty_instantiate_wasm(wasmBuf, userEnv) { }, } + if (MTY.jspi) { + if (mty_supports_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; + } + + } else { + console.warn('JSPI requested but not supported by this runtime, falling back to sync imports'); + MTY.jspi = false; + } + } + // Add userEnv to imports, run on the main thread for (let x = 0; x < userEnv.length; x++) { const key = userEnv[x]; @@ -1158,6 +1247,7 @@ onmessage = async (ev) => { MTY.psync = msg.psync; MTY.audioObjs = msg.audioObjs; MTY.initWindowInfo = msg.windowInfo; + MTY.jspi = msg.jspi === true; MTY.sync = new Int32Array(new SharedArrayBuffer(4)); MTY.sleeper = new Int32Array(new SharedArrayBuffer(4)); MTY.module = await mty_instantiate_wasm(msg.wasmBuf, msg.userEnv); @@ -1170,11 +1260,11 @@ 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(); diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 2f4456bc..c8b1102c 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -783,6 +783,21 @@ function mty_supports_web_gl() { return false; } +function mty_use_jspi() { + if (typeof WebAssembly == 'undefined') + return false; + + const params = new URLSearchParams(window.location.search); + + if (params.get('mty-jspi') == '1') + return true; + + if (params.get('mty-jspi') == '0') + return false; + + return typeof WebAssembly.Suspending == 'function' && typeof WebAssembly.promising == 'function'; +} + function mty_update_interval(thread) { // Poll gamepads if (document.hasFocus()) @@ -826,6 +841,7 @@ function mty_thread_start(threadId, bin, wasmBuf, memory, startArg, userEnv, kbM threadId: threadId, memory: memory, audioObjs, + jspi: MTY.jspi, }); return worker; @@ -837,6 +853,7 @@ async function MTY_Start(bin, container, userEnv) { MTY.bin = bin; MTY.userEnv = userEnv; + MTY.jspi = mty_use_jspi(); MTY.psync = new Int32Array(new SharedArrayBuffer(4)); MTY.audioObjs = { buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)), diff --git a/src/unix/web/web.h b/src/unix/web/web.h index 3ffc16ac..e41c8518 100644 --- a/src/unix/web/web.h +++ b/src/unix/web/web.h @@ -25,6 +25,7 @@ char *web_get_hostname(void); char *web_get_clipboard(void); void web_set_clipboard(const char *text); void web_set_title(const char *title); +void web_run_and_yield(MTY_IterFunc iter, void *opaque); void web_run_main_thread(MTY_IterFunc iter, void *opaque); void web_gl_flush(void); void web_set_gfx(void); From 4322b59a69561b07c053617cc1e04c5f38422cc0 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Thu, 12 Mar 2026 15:30:35 -0600 Subject: [PATCH 23/30] Cleanup code --- src/unix/web/app.c | 3 +- src/unix/web/matoya-worker.js | 57 ++++++++--------------------------- src/unix/web/matoya.js | 22 ++++---------- src/unix/web/web.h | 1 - 4 files changed, 20 insertions(+), 63 deletions(-) diff --git a/src/unix/web/app.c b/src/unix/web/app.c index d68378d6..a0540823 100644 --- a/src/unix/web/app.c +++ b/src/unix/web/app.c @@ -335,8 +335,7 @@ void MTY_AppDestroy(MTY_App **app) void MTY_AppRun(MTY_App *ctx) { - // NOTE: This function will never complete / return. - web_run_main_thread(ctx->app_func, ctx->opaque); + web_run_and_yield(ctx->app_func, ctx->opaque); } void MTY_AppSetTimeout(MTY_App *ctx, uint32_t timeout) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 481f4544..1e5e6d25 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -38,20 +38,11 @@ function mty_dup_c(buf) { return ptr; } -function mty_schedule(func) { - if (typeof scheduler != 'undefined' && typeof scheduler.postTask == 'function') { - scheduler.postTask(func); - - } else { - setTimeout(func, 0); - } -} - function mty_run_until_false(cfunc, opaque) { return new Promise(resolve => { const step = () => { if (cfunc(opaque)) { - mty_schedule(step); + setTimeout(step, 0); } else { resolve(); @@ -62,10 +53,8 @@ function mty_run_until_false(cfunc, opaque) { }); } -function mty_supports_jspi() { - return typeof WebAssembly.Suspending == 'function' && typeof WebAssembly.promising == 'function'; -} - +// 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]; @@ -885,23 +874,9 @@ const MTY_WEB_API = { MTY.app = app; mty_update_window(app, MTY.initWindowInfo); }, - // web_run_and_yield: function (iter, opaque) { - // // Cannot be called on the main thread since the tight loop would block the event loop and prevent yielding. - // // MTY.exports.mty_app_set_keys(); - - // const cfunc = mty_cfunc(iter); - // let count = 0; - - // while (cfunc(opaque)) { - // // Periodically block for 1 ms to reduce allocator/GC pressure in long-running loops. - // if ((++count & 0x3FF) == 0) - // Atomics.wait(MTY.sleeper, 0, 0, 1); - // } - // }, - web_run_main_thread: function (iter, opaque) { - // Cannot use web_run_and_yield on the main thread - // since the tight loop would block the event loop and prevent yielding, - // so we use setTimeout to yield after each iteration + + // Fallback in case the browser does not support JSPI. + web_run_and_yield: function (iter, opaque) { MTY.exports.mty_app_set_keys(); const step = () => { @@ -911,7 +886,8 @@ const MTY_WEB_API = { setTimeout(step, 0); // Throw exception to ensure execution does not halt when this function finishes. - throw 'run_main_thread halted execution'; + // NOTE: This will end up blocking the caller indefinitely. + throw 'run_and_yield halted execution'; }, }; @@ -1189,17 +1165,10 @@ async function mty_instantiate_wasm(wasmBuf, userEnv) { } if (MTY.jspi) { - if (mty_supports_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; - } - - } else { - console.warn('JSPI requested but not supported by this runtime, falling back to sync imports'); + 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; } } @@ -1271,7 +1240,7 @@ onmessage = async (ev) => { } catch (e) { // Ignore known exception that we throw to continue main thread execution - if (e.toString().search('run_main_thread halted execution') == -1) + 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 c8b1102c..61e5cda9 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -783,21 +783,6 @@ function mty_supports_web_gl() { return false; } -function mty_use_jspi() { - if (typeof WebAssembly == 'undefined') - return false; - - const params = new URLSearchParams(window.location.search); - - if (params.get('mty-jspi') == '1') - return true; - - if (params.get('mty-jspi') == '0') - return false; - - return typeof WebAssembly.Suspending == 'function' && typeof WebAssembly.promising == 'function'; -} - function mty_update_interval(thread) { // Poll gamepads if (document.hasFocus()) @@ -853,7 +838,12 @@ async function MTY_Start(bin, container, userEnv) { MTY.bin = bin; MTY.userEnv = userEnv; - MTY.jspi = mty_use_jspi(); + + // For now, we'll allow execution if the client does not support JSPI + // by falling back to non-promise based functions + MTY.jspi = typeof WebAssembly !== 'undefined' && + typeof WebAssembly.Suspending === 'function' && + typeof WebAssembly.promising === 'function'; MTY.psync = new Int32Array(new SharedArrayBuffer(4)); MTY.audioObjs = { buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)), diff --git a/src/unix/web/web.h b/src/unix/web/web.h index e41c8518..3fa0adca 100644 --- a/src/unix/web/web.h +++ b/src/unix/web/web.h @@ -26,7 +26,6 @@ char *web_get_clipboard(void); void web_set_clipboard(const char *text); void web_set_title(const char *title); void web_run_and_yield(MTY_IterFunc iter, void *opaque); -void web_run_main_thread(MTY_IterFunc iter, void *opaque); void web_gl_flush(void); void web_set_gfx(void); void web_set_canvas_size(uint32_t width, uint32_t height); From 29a7c82d1bc9236eaf73ed0927fa7e24a82e5016 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Thu, 12 Mar 2026 15:32:12 -0600 Subject: [PATCH 24/30] Cleanup canvas --- src/unix/web/matoya.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 61e5cda9..c7372ddc 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -665,7 +665,7 @@ async function mty_decode_image(input) { const height = img.naturalHeight; const canvas = new OffscreenCanvas(width, height); - const ctx = canvas.getContext('2d', {"willReadFrequently": true}); + const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); return ctx.getImageData(0, 0, width, height); From 01af3e8500ff84b5d2f5179bd767305cc0ce4b4c Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Thu, 12 Mar 2026 15:40:06 -0600 Subject: [PATCH 25/30] More cleanup of JSPI state --- src/unix/web/matoya-worker.js | 6 ++++-- src/unix/web/matoya.js | 6 ------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 1e5e6d25..c82291c7 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -12,7 +12,9 @@ const MTY = { glObj: {}, fds: {}, fdIndex: 0, - jspi: false, + jspi: typeof WebAssembly !== 'undefined' && + typeof WebAssembly.Suspending === 'function' && + typeof WebAssembly.promising === 'function', promising: {}, }; @@ -877,6 +879,7 @@ const MTY_WEB_API = { // 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 = () => { @@ -1216,7 +1219,6 @@ onmessage = async (ev) => { MTY.psync = msg.psync; MTY.audioObjs = msg.audioObjs; MTY.initWindowInfo = msg.windowInfo; - MTY.jspi = msg.jspi === true; MTY.sync = new Int32Array(new SharedArrayBuffer(4)); MTY.sleeper = new Int32Array(new SharedArrayBuffer(4)); MTY.module = await mty_instantiate_wasm(msg.wasmBuf, msg.userEnv); diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index c7372ddc..9ed5b481 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -826,7 +826,6 @@ function mty_thread_start(threadId, bin, wasmBuf, memory, startArg, userEnv, kbM threadId: threadId, memory: memory, audioObjs, - jspi: MTY.jspi, }); return worker; @@ -839,11 +838,6 @@ async function MTY_Start(bin, container, userEnv) { MTY.bin = bin; MTY.userEnv = userEnv; - // For now, we'll allow execution if the client does not support JSPI - // by falling back to non-promise based functions - MTY.jspi = typeof WebAssembly !== 'undefined' && - typeof WebAssembly.Suspending === 'function' && - typeof WebAssembly.promising === 'function'; MTY.psync = new Int32Array(new SharedArrayBuffer(4)); MTY.audioObjs = { buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)), From b266b9c0b8dc93a2ed06c128d5409e1f41d18f55 Mon Sep 17 00:00:00 2001 From: Martin Trang Date: Thu, 12 Mar 2026 15:49:14 -0600 Subject: [PATCH 26/30] mo betta --- src/unix/web/app.c | 1 - src/unix/web/matoya-worker.js | 31 ++++++++++++++----------------- src/unix/web/matoya.js | 1 - 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/unix/web/app.c b/src/unix/web/app.c index a0540823..2b5a9e7f 100644 --- a/src/unix/web/app.c +++ b/src/unix/web/app.c @@ -705,7 +705,6 @@ void *MTY_GLGetProcAddress(const char *name) return NULL; } -// Cannot be called on the main thread since the tight loop would block the event loop and prevent yielding. void MTY_RunAndYield(MTY_IterFunc iter, void *opaque) { web_run_and_yield(iter, opaque); diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index c82291c7..63564f1b 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -40,21 +40,6 @@ function mty_dup_c(buf) { return ptr; } -function mty_run_until_false(cfunc, opaque) { - return new Promise(resolve => { - const step = () => { - if (cfunc(opaque)) { - setTimeout(step, 0); - - } else { - resolve(); - } - }; - - step(); - }); -} - // 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) { @@ -84,7 +69,19 @@ async function mty_web_run_and_yield_async(iter, opaque) { MTY.exports.mty_app_set_keys(); const cfunc = mty_cfunc(iter); - await mty_run_until_false(cfunc, opaque); + + await new Promise(resolve => { + const step = () => { + if (cfunc(opaque)) { + setTimeout(step, 0); + + } else { + resolve(); + } + }; + + step(); + }); } @@ -1241,7 +1238,7 @@ onmessage = async (ev) => { close(); } catch (e) { - // Ignore known exception that we throw to continue main thread execution + // Ignore known exception that we throw to continue thread execution if (e.toString().search('run_and_yield halted execution') == -1) console.error(e); } diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 9ed5b481..9f595cb7 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -837,7 +837,6 @@ async function MTY_Start(bin, container, userEnv) { MTY.bin = bin; MTY.userEnv = userEnv; - MTY.psync = new Int32Array(new SharedArrayBuffer(4)); MTY.audioObjs = { buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)), From f39f6c4a76e107232281646e93960ffd93eb0e9f Mon Sep 17 00:00:00 2001 From: margaux Date: Tue, 2 Jun 2026 15:34:33 +0200 Subject: [PATCH 27/30] add title on iframe that render the webview --- src/unix/web/matoya.js | 1845 ++++++++++++++++++++-------------------- 1 file changed, 940 insertions(+), 905 deletions(-) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 22c38739..6c329281 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -2,7 +2,6 @@ // If a copy of the MIT License was not distributed with this file, // You can obtain one at https://spdx.org/licenses/MIT.html. - // Global State let MTY_MEMORY; @@ -10,1131 +9,1167 @@ let MTY_CURRENT_SCRIPT; // Worker if (typeof importScripts == 'function') { - MTY_CURRENT_SCRIPT = location; + MTY_CURRENT_SCRIPT = location; -// Main thread + // Main thread } else { - MTY_CURRENT_SCRIPT = new URL(document.currentScript.src); - - window.MTY = { - wsIndex: 1, - wsObj: {}, - cursorId: 0, - threadId: 1, - cursorCache: {}, - cursorClass: '', - defaultCursor: false, - synthesizeEsc: true, - relative: false, - gps: [false, false, false, false], - }; + MTY_CURRENT_SCRIPT = new URL(document.currentScript.src); + + window.MTY = { + wsIndex: 1, + wsObj: {}, + cursorId: 0, + threadId: 1, + cursorCache: {}, + cursorClass: '', + defaultCursor: false, + synthesizeEsc: true, + relative: false, + gps: [false, false, false, false], + }; } - // Memory function mty_encode(str) { - return new TextEncoder().encode(str); + return new TextEncoder().encode(str); } function mty_decode(buf) { - return new TextDecoder().decode(buf); + return new TextDecoder().decode(buf); } function mty_strlen(buf) { - let len = 0; - for (; buf[len] != 0; len++); + let len = 0; + for (; buf[len] != 0; len++); - return len; + return len; } function mty_memcpy(ptr, buf) { - new Uint8Array(MTY_MEMORY.buffer, ptr, buf.byteLength).set(buf); + new Uint8Array(MTY_MEMORY.buffer, ptr, buf.byteLength).set(buf); } function mty_strcpy(ptr, buf) { - mty_memcpy(ptr, buf); - mty_set_int8(ptr + buf.byteLength, 0); + mty_memcpy(ptr, buf); + mty_set_int8(ptr + buf.byteLength, 0); } function mty_dup(ptr, size) { - return new Uint8Array(MTY_MEMORY.buffer, ptr).slice(0, size); + return new Uint8Array(MTY_MEMORY.buffer, ptr).slice(0, size); } function mty_str_to_js(ptr) { - const buf = new Uint8Array(MTY_MEMORY.buffer, ptr); + const buf = new Uint8Array(MTY_MEMORY.buffer, ptr); - return mty_decode(buf.slice(0, mty_strlen(buf))); + return mty_decode(buf.slice(0, mty_strlen(buf))); } function mty_str_to_c(str, ptr, size) { - const buf = mty_encode(str); + const buf = mty_encode(str); - if (buf.byteLength >= size) - throw 'mty_str_to_c overflow' + if (buf.byteLength >= size) throw 'mty_str_to_c overflow'; - mty_strcpy(ptr, buf); + mty_strcpy(ptr, buf); } function mty_get_uint8(ptr) { - return new DataView(MTY_MEMORY.buffer).getUint8(ptr); + return new DataView(MTY_MEMORY.buffer).getUint8(ptr); } function mty_set_int8(ptr, value) { - new DataView(MTY_MEMORY.buffer).setInt8(ptr, value); + new DataView(MTY_MEMORY.buffer).setInt8(ptr, value); } function mty_set_uint16(ptr, value) { - new DataView(MTY_MEMORY.buffer).setUint16(ptr, value, true); + new DataView(MTY_MEMORY.buffer).setUint16(ptr, value, true); } function mty_get_uint32(ptr) { - return new DataView(MTY_MEMORY.buffer).getUint32(ptr, true); + return new DataView(MTY_MEMORY.buffer).getUint32(ptr, true); } function mty_set_uint32(ptr, value) { - new DataView(MTY_MEMORY.buffer).setUint32(ptr, value, true); + new DataView(MTY_MEMORY.buffer).setUint32(ptr, value, true); } function mty_get_uint64(ptr, value) { - return new DataView(MTY_MEMORY.buffer).getBigUint64(ptr, true); + return new DataView(MTY_MEMORY.buffer).getBigUint64(ptr, true); } function mty_set_uint64(ptr, value) { - new DataView(MTY_MEMORY.buffer).setBigUint64(ptr, BigInt(value), true); + new DataView(MTY_MEMORY.buffer).setBigUint64(ptr, BigInt(value), true); } function mty_set_float(ptr, value) { - new DataView(MTY_MEMORY.buffer).setFloat32(ptr, value, true); + new DataView(MTY_MEMORY.buffer).setFloat32(ptr, value, true); } - // Base64 function mty_buf_to_b64(buf) { - let bstr = ''; - for (let x = 0; x < buf.byteLength; x++) - bstr += String.fromCharCode(buf[x]); + let bstr = ''; + for (let x = 0; x < buf.byteLength; x++) bstr += String.fromCharCode(buf[x]); - return btoa(bstr); + return btoa(bstr); } function mty_b64_to_buf(b64) { - const bstr = atob(b64); - const buf = new Uint8Array(bstr.length); + const bstr = atob(b64); + const buf = new Uint8Array(bstr.length); - for (let x = 0; x < bstr.length; x++) - buf[x] = bstr.charCodeAt(x); + for (let x = 0; x < bstr.length; x++) buf[x] = bstr.charCodeAt(x); - return buf; + return buf; } - // Synchronization function mty_wait(sync) { - if (Atomics.compareExchange(sync, 0, 0, 1) == 0) - Atomics.wait(sync, 0, 1); + if (Atomics.compareExchange(sync, 0, 0, 1) == 0) Atomics.wait(sync, 0, 1); - Atomics.store(sync, 0, 0); + Atomics.store(sync, 0, 0); } function mty_signal(sync, allow_miss = false) { - if (Atomics.compareExchange(sync, 0, 0, 1) != 0) - while (Atomics.notify(sync, 0, 1) == 0 && !allow_miss); + if (Atomics.compareExchange(sync, 0, 0, 1) != 0) + while (Atomics.notify(sync, 0, 1) == 0 && !allow_miss); } function MTY_SignalPtr(csync) { - mty_signal(new Int32Array(MTY_MEMORY.buffer, csync, 1)); + mty_signal(new Int32Array(MTY_MEMORY.buffer, csync, 1)); } - // Input function mty_scaled(num) { - return Math.round(num * window.devicePixelRatio); + return Math.round(num * window.devicePixelRatio); } function mty_correct_relative() { - if (!document.pointerLockElement && MTY.relative) - MTY.canvas.requestPointerLock(); + if (!document.pointerLockElement && MTY.relative) + MTY.canvas.requestPointerLock(); } function mty_get_mods(ev) { - let mods = 0; + let mods = 0; - if (ev.shiftKey) mods |= 0x01; - if (ev.ctrlKey) mods |= 0x02; - if (ev.altKey) mods |= 0x04; - if (ev.metaKey) mods |= 0x08; + if (ev.shiftKey) mods |= 0x01; + if (ev.ctrlKey) mods |= 0x02; + if (ev.altKey) mods |= 0x04; + if (ev.metaKey) mods |= 0x08; - if (ev.getModifierState("CapsLock")) mods |= 0x10; - if (ev.getModifierState("NumLock") ) mods |= 0x20; + if (ev.getModifierState('CapsLock')) mods |= 0x10; + if (ev.getModifierState('NumLock')) mods |= 0x20; - return mods; + return mods; } function mty_set_pointer_lock(enable) { - if (enable && !document.pointerLockElement) { - MTY.canvas.requestPointerLock(); - - } else if (!enable && document.pointerLockElement) { - MTY.synthesizeEsc = false; - document.exitPointerLock(); - } - - MTY.relative = enable; + if (enable && !document.pointerLockElement) { + MTY.canvas.requestPointerLock(); + } else if (!enable && document.pointerLockElement) { + MTY.synthesizeEsc = false; + document.exitPointerLock(); + } + + MTY.relative = enable; } function mty_allow_default(ev) { - // The "allowed" browser hotkey list. Copy/Paste, Refresh, fullscreen, developer console, and tab switching - - return ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyV') || - ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyC') || - ((ev.ctrlKey || ev.shiftKey) && ev.code == 'KeyI') || - (ev.ctrlKey && ev.code == 'KeyR') || - (ev.ctrlKey && ev.code == 'F5') || - (ev.ctrlKey && ev.code == 'Digit1') || - (ev.ctrlKey && ev.code == 'Digit2') || - (ev.ctrlKey && ev.code == 'Digit3') || - (ev.ctrlKey && ev.code == 'Digit4') || - (ev.ctrlKey && ev.code == 'Digit5') || - (ev.ctrlKey && ev.code == 'Digit6') || - (ev.ctrlKey && ev.code == 'Digit7') || - (ev.ctrlKey && ev.code == 'Digit8') || - (ev.ctrlKey && ev.code == 'Digit9') || - (ev.code == 'F5') || - (ev.code == 'F11') || - (ev.code == 'F12'); + // The "allowed" browser hotkey list. Copy/Paste, Refresh, fullscreen, developer console, and tab switching + + return ( + ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyV') || + ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyC') || + ((ev.ctrlKey || ev.shiftKey) && ev.code == 'KeyI') || + (ev.ctrlKey && ev.code == 'KeyR') || + (ev.ctrlKey && ev.code == 'F5') || + (ev.ctrlKey && ev.code == 'Digit1') || + (ev.ctrlKey && ev.code == 'Digit2') || + (ev.ctrlKey && ev.code == 'Digit3') || + (ev.ctrlKey && ev.code == 'Digit4') || + (ev.ctrlKey && ev.code == 'Digit5') || + (ev.ctrlKey && ev.code == 'Digit6') || + (ev.ctrlKey && ev.code == 'Digit7') || + (ev.ctrlKey && ev.code == 'Digit8') || + (ev.ctrlKey && ev.code == 'Digit9') || + ev.code == 'F5' || + ev.code == 'F11' || + ev.code == 'F12' + ); } function mty_add_input_events(thread) { - MTY.canvas.addEventListener('mousemove', (ev) => { - let x = mty_scaled(ev.clientX); - let y = mty_scaled(ev.clientY); - - if (MTY.relative) { - x = ev.movementX; - y = ev.movementY; - } - - thread.postMessage({ - type: 'motion', - relative: MTY.relative, - x: x, - y: y, - }); - }); - - document.addEventListener('pointerlockchange', (ev) => { - // Left relative via the ESC key, which swallows a natural ESC keypress - if (!document.pointerLockElement && MTY.synthesizeEsc) { - const msg = { - type: 'keyboard', - pressed: true, - code: 'Escape', - key: 'Escape', - mods: 0, - }; - - thread.postMessage(msg); - - msg.pressed = false; - thread.postMessage(msg); - } - - MTY.synthesizeEsc = true; - }); - - window.addEventListener('click', (ev) => { - // Popup blockers can interfere with window.open if not called from within the 'click' listener - mty_run_action(); - ev.preventDefault(); - }); - - window.addEventListener('mousedown', (ev) => { - mty_correct_relative(); - ev.preventDefault(); - - thread.postMessage({ - type: 'button', - pressed: true, - button: ev.button, - x: mty_scaled(ev.clientX), - y: mty_scaled(ev.clientY), - }); - }); - - window.addEventListener('mouseup', (ev) => { - ev.preventDefault(); - - thread.postMessage({ - type: 'button', - pressed: false, - button: ev.button, - x: mty_scaled(ev.clientX), - y: mty_scaled(ev.clientY), - }); - }); - - MTY.canvas.addEventListener('contextmenu', (ev) => { - ev.preventDefault(); - }); - - MTY.canvas.addEventListener('dragover', (ev) => { - ev.preventDefault(); - }); - - MTY.canvas.addEventListener('wheel', (ev) => { - let x = ev.deltaX > 0 ? 120 : ev.deltaX < 0 ? -120 : 0; - let y = ev.deltaY > 0 ? 120 : ev.deltaY < 0 ? -120 : 0; - - thread.postMessage({ - type: 'scroll', - x: x, - y: y, - }); - }, {passive: true}); - - window.addEventListener('keydown', (ev) => { - mty_correct_relative(); - - thread.postMessage({ - type: 'keyboard', - pressed: true, - code: ev.code, - key: ev.key, - mods: mty_get_mods(ev), - }); - - if (MTY.kb_grab || !mty_allow_default(ev)) - ev.preventDefault(); - }); - - window.addEventListener('keyup', (ev) => { - thread.postMessage({ - type: 'keyboard', - pressed: false, - code: ev.code, - key: '', - mods: mty_get_mods(ev), - }); - - if (MTY.kb_grab || !mty_allow_default(ev)) - ev.preventDefault(); - }); - - document.addEventListener('visibilitychange', (ev) => { - thread.postMessage({ - type: 'focus', - focus: document.visibilityState == 'visible', - }); - }); - - window.addEventListener('resize', (ev) => { - const rect = mty_update_canvas(MTY.canvas); - - thread.postMessage({ - type: 'size', - width: mty_scaled(rect.width), - height: mty_scaled(rect.height), - }); - }); - - MTY.canvas.addEventListener('drop', (ev) => { - ev.preventDefault(); - - if (!ev.dataTransfer.items) - return; - - for (let x = 0; x < ev.dataTransfer.items.length; x++) { - if (ev.dataTransfer.items[x].kind == 'file') { - let file = ev.dataTransfer.items[x].getAsFile(); - - const reader = new FileReader(); - reader.addEventListener('loadend', (fev) => { - if (reader.readyState == 2) { - thread.postMessage({ - type: 'drop', - name: file.name, - data: reader.result, - }, [reader.result]); - } - }); - - reader.readAsArrayBuffer(file); - break; - } - } - }); + MTY.canvas.addEventListener('mousemove', (ev) => { + let x = mty_scaled(ev.clientX); + let y = mty_scaled(ev.clientY); + + if (MTY.relative) { + x = ev.movementX; + y = ev.movementY; + } + + thread.postMessage({ + type: 'motion', + relative: MTY.relative, + x: x, + y: y, + }); + }); + + document.addEventListener('pointerlockchange', (ev) => { + // Left relative via the ESC key, which swallows a natural ESC keypress + if (!document.pointerLockElement && MTY.synthesizeEsc) { + const msg = { + type: 'keyboard', + pressed: true, + code: 'Escape', + key: 'Escape', + mods: 0, + }; + + thread.postMessage(msg); + + msg.pressed = false; + thread.postMessage(msg); + } + + MTY.synthesizeEsc = true; + }); + + window.addEventListener('click', (ev) => { + // Popup blockers can interfere with window.open if not called from within the 'click' listener + mty_run_action(); + ev.preventDefault(); + }); + + window.addEventListener('mousedown', (ev) => { + mty_correct_relative(); + ev.preventDefault(); + + thread.postMessage({ + type: 'button', + pressed: true, + button: ev.button, + x: mty_scaled(ev.clientX), + y: mty_scaled(ev.clientY), + }); + }); + + window.addEventListener('mouseup', (ev) => { + ev.preventDefault(); + + thread.postMessage({ + type: 'button', + pressed: false, + button: ev.button, + x: mty_scaled(ev.clientX), + y: mty_scaled(ev.clientY), + }); + }); + + MTY.canvas.addEventListener('contextmenu', (ev) => { + ev.preventDefault(); + }); + + MTY.canvas.addEventListener('dragover', (ev) => { + ev.preventDefault(); + }); + + MTY.canvas.addEventListener( + 'wheel', + (ev) => { + let x = ev.deltaX > 0 ? 120 : ev.deltaX < 0 ? -120 : 0; + let y = ev.deltaY > 0 ? 120 : ev.deltaY < 0 ? -120 : 0; + + thread.postMessage({ + type: 'scroll', + x: x, + y: y, + }); + }, + { passive: true }, + ); + + window.addEventListener('keydown', (ev) => { + mty_correct_relative(); + + thread.postMessage({ + type: 'keyboard', + pressed: true, + code: ev.code, + key: ev.key, + mods: mty_get_mods(ev), + }); + + if (MTY.kb_grab || !mty_allow_default(ev)) ev.preventDefault(); + }); + + window.addEventListener('keyup', (ev) => { + thread.postMessage({ + type: 'keyboard', + pressed: false, + code: ev.code, + key: '', + mods: mty_get_mods(ev), + }); + + if (MTY.kb_grab || !mty_allow_default(ev)) ev.preventDefault(); + }); + + document.addEventListener('visibilitychange', (ev) => { + thread.postMessage({ + type: 'focus', + focus: document.visibilityState == 'visible', + }); + }); + + window.addEventListener('resize', (ev) => { + const rect = mty_update_canvas(MTY.canvas); + + thread.postMessage({ + type: 'size', + width: mty_scaled(rect.width), + height: mty_scaled(rect.height), + }); + }); + + MTY.canvas.addEventListener('drop', (ev) => { + ev.preventDefault(); + + if (!ev.dataTransfer.items) return; + + for (let x = 0; x < ev.dataTransfer.items.length; x++) { + if (ev.dataTransfer.items[x].kind == 'file') { + let file = ev.dataTransfer.items[x].getAsFile(); + + const reader = new FileReader(); + reader.addEventListener('loadend', (fev) => { + if (reader.readyState == 2) { + thread.postMessage( + { + type: 'drop', + name: file.name, + data: reader.result, + }, + [reader.result], + ); + } + }); + + reader.readAsArrayBuffer(file); + break; + } + } + }); } - // Dialog function mty_alert(title, msg) { - window.alert(mty_str_to_js(title) + '\n\n' + mty_str_to_js(msg)); + window.alert(mty_str_to_js(title) + '\n\n' + mty_str_to_js(msg)); } - // URI opener function mty_run_action() { - setTimeout(() => { - if (MTY.action) { - MTY.action(); - delete MTY.action; - } - }, 100); + setTimeout(() => { + if (MTY.action) { + MTY.action(); + delete MTY.action; + } + }, 100); } function mty_set_action(action) { - MTY.action = action; + MTY.action = action; - // In case click handler doesn't happen - mty_run_action(); + // In case click handler doesn't happen + mty_run_action(); } - // Window function mty_is_visible() { - if (document.hidden != undefined) { - return !document.hidden; - - } else if (document.webkitHidden != undefined) { - return !document.webkitHidden; - } + if (document.hidden != undefined) { + return !document.hidden; + } else if (document.webkitHidden != undefined) { + return !document.webkitHidden; + } - return true; + return true; } function mty_window_info() { - const rect = MTY.canvas.getBoundingClientRect(); - - return { - posX: window.screenX, - posY: window.screenY, - relative: MTY.relative, - devicePixelRatio: window.devicePixelRatio, - hasFocus: document.hasFocus(), - screenWidth: screen.width, - screenHeight: screen.height, - fullscreen: document.fullscreenElement != null, - visible: mty_is_visible(), - canvasWidth: mty_scaled(rect.width), - canvasHeight: mty_scaled(rect.height), - }; + const rect = MTY.canvas.getBoundingClientRect(); + + return { + posX: window.screenX, + posY: window.screenY, + relative: MTY.relative, + devicePixelRatio: window.devicePixelRatio, + hasFocus: document.hasFocus(), + screenWidth: screen.width, + screenHeight: screen.height, + fullscreen: document.fullscreenElement != null, + visible: mty_is_visible(), + canvasWidth: mty_scaled(rect.width), + canvasHeight: mty_scaled(rect.height), + }; } function mty_update_canvas(canvas) { - const rect = canvas.getBoundingClientRect(); - canvas.width = rect.width; - canvas.height = rect.height; + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; - return rect; + return rect; } function mty_set_fullscreen(fullscreen) { - if (fullscreen && !document.fullscreenElement) { - if (navigator.keyboard) - navigator.keyboard.lock(["Escape"]); + if (fullscreen && !document.fullscreenElement) { + if (navigator.keyboard) navigator.keyboard.lock(['Escape']); - document.documentElement.requestFullscreen(); + document.documentElement.requestFullscreen(); + } else if (!fullscreen && document.fullscreenElement) { + document.exitFullscreen(); - } else if (!fullscreen && document.fullscreenElement) { - document.exitFullscreen(); - - if (navigator.keyboard) - navigator.keyboard.unlock(); - } + if (navigator.keyboard) navigator.keyboard.unlock(); + } } async function mty_wake_lock(enable) { - try { - if (enable && !MTY.wakeLock) { - MTY.wakeLock = await navigator.wakeLock.request('screen'); - - } else if (!enable && MTY.wakeLock) { - MTY.wakeLock.release(); - delete MTY.wakeLock; - } - } catch (e) { - delete MTY.wakeLock; - } + try { + if (enable && !MTY.wakeLock) { + MTY.wakeLock = await navigator.wakeLock.request('screen'); + } else if (!enable && MTY.wakeLock) { + MTY.wakeLock.release(); + delete MTY.wakeLock; + } + } catch (e) { + delete MTY.wakeLock; + } } - // Cursor function mty_show_cursor(show) { - MTY.canvas.style.cursor = show ? '': 'none'; + MTY.canvas.style.cursor = show ? '' : 'none'; } function mty_use_default_cursor(use_default) { - if (MTY.cursorClass.length > 0) { - if (use_default) { - MTY.canvas.classList.remove(MTY.cursorClass); - - } else { - MTY.canvas.classList.add(MTY.cursorClass); - } - } - - MTY.defaultCursor = use_default; + if (MTY.cursorClass.length > 0) { + if (use_default) { + MTY.canvas.classList.remove(MTY.cursorClass); + } else { + MTY.canvas.classList.add(MTY.cursorClass); + } + } + + MTY.defaultCursor = use_default; } function mty_set_cursor(url, hot_x, hot_y) { - if (url) { - if (!MTY.cursorCache[url]) { - MTY.cursorCache[url] = `cursor-x-${MTY.cursorId}`; - - const style = document.createElement('style'); - style.type = 'text/css'; - style.innerHTML = `.cursor-x-${MTY.cursorId++} ` + - `{cursor: url(${url}) ${hot_x} ${hot_y}, auto;}`; - document.querySelector('head').appendChild(style); - } - - if (MTY.cursorClass.length > 0) - MTY.canvas.classList.remove(MTY.cursorClass); - - MTY.cursorClass = MTY.cursorCache[url]; - - if (!MTY.defaultCursor) - MTY.canvas.classList.add(MTY.cursorClass); - - } else { - if (!MTY.defaultCursor && MTY.cursorClass.length > 0) - MTY.canvas.classList.remove(MTY.cursorClass); - - MTY.cursorClass = ''; - } + if (url) { + if (!MTY.cursorCache[url]) { + MTY.cursorCache[url] = `cursor-x-${MTY.cursorId}`; + + const style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = + `.cursor-x-${MTY.cursorId++} ` + + `{cursor: url(${url}) ${hot_x} ${hot_y}, auto;}`; + document.querySelector('head').appendChild(style); + } + + if (MTY.cursorClass.length > 0) + MTY.canvas.classList.remove(MTY.cursorClass); + + MTY.cursorClass = MTY.cursorCache[url]; + + if (!MTY.defaultCursor) MTY.canvas.classList.add(MTY.cursorClass); + } else { + if (!MTY.defaultCursor && MTY.cursorClass.length > 0) + MTY.canvas.classList.remove(MTY.cursorClass); + + MTY.cursorClass = ''; + } } function mty_set_png_cursor(buf, hot_x, hot_y) { - const url = buf ? 'data:image/png;base64,' + mty_buf_to_b64(buf) : null; - mty_set_cursor(url, hot_x, hot_y); + const url = buf ? 'data:image/png;base64,' + mty_buf_to_b64(buf) : null; + mty_set_cursor(url, hot_x, hot_y); } function mty_set_rgba_cursor(buf, width, height, hot_x, hot_y) { - let url = null; + let url = null; - if (buf) { - if (!MTY.ccanvas) { - MTY.ccanvas = document.createElement('canvas'); - MTY.cctx = MTY.ccanvas.getContext('2d', {"willReadFrequently": true}); - } + if (buf) { + if (!MTY.ccanvas) { + MTY.ccanvas = document.createElement('canvas'); + MTY.cctx = MTY.ccanvas.getContext('2d', { willReadFrequently: true }); + } - MTY.ccanvas.width = width; - MTY.ccanvas.height = height; + MTY.ccanvas.width = width; + MTY.ccanvas.height = height; - const image = MTY.cctx.getImageData(0, 0, width, height); - image.data.set(buf); + const image = MTY.cctx.getImageData(0, 0, width, height); + image.data.set(buf); - MTY.cctx.putImageData(image, 0, 0); + MTY.cctx.putImageData(image, 0, 0); - url = MTY.ccanvas.toDataURL(); - } + url = MTY.ccanvas.toDataURL(); + } - mty_set_cursor(url, hot_x, hot_y); + mty_set_cursor(url, hot_x, hot_y); } - // Gamepads function mty_rumble_gamepad(id, low, high) { - const gps = navigator.getGamepads(); - const gp = gps[id]; - - if (gp && gp.vibrationActuator) - gp.vibrationActuator.playEffect('dual-rumble', { - startDelay: 0, - duration: 2000, - weakMagnitude: low, - strongMagnitude: high, - }); + const gps = navigator.getGamepads(); + const gp = gps[id]; + + if (gp && gp.vibrationActuator) + gp.vibrationActuator.playEffect('dual-rumble', { + startDelay: 0, + duration: 2000, + weakMagnitude: low, + strongMagnitude: high, + }); } function mty_poll_gamepads(thread) { - const gps = navigator.getGamepads(); - - for (let x = 0; x < 4; x++) { - const gp = gps[x]; - - if (gp) { - let state = 0; - - // Connected - if (!MTY.gps[x]) { - MTY.gps[x] = true; - state = 1; - } - - let lx = 0; - let ly = 0; - let rx = 0; - let ry = 0; - let lt = 0; - let rt = 0; - let buttons = 0; - - if (gp.buttons) { - if (gp.buttons[6]) lt = gp.buttons[6].value; - if (gp.buttons[7]) rt = gp.buttons[7].value; - - for (let i = 0; i < gp.buttons.length && i < 32; i++) - if (gp.buttons[i].pressed) - buttons |= 1 << i; - } - - if (gp.axes) { - if (gp.axes[0]) lx = gp.axes[0]; - if (gp.axes[1]) ly = gp.axes[1]; - if (gp.axes[2]) rx = gp.axes[2]; - if (gp.axes[3]) ry = gp.axes[3]; - } - - thread.postMessage({ - type: 'controller', - id: x, - state: state, - buttons: buttons, - lx: lx, - ly: ly, - rx: rx, - ry: ry, - lt: lt, - rt: rt, - }); - - // Disconnected - } else if (MTY.gps[x]) { - thread.postMessage({ - type: 'controller-disconnect', - id: x, - state: 2, - }); - - MTY.gps[x] = false; - } - } + const gps = navigator.getGamepads(); + + for (let x = 0; x < 4; x++) { + const gp = gps[x]; + + if (gp) { + let state = 0; + + // Connected + if (!MTY.gps[x]) { + MTY.gps[x] = true; + state = 1; + } + + let lx = 0; + let ly = 0; + let rx = 0; + let ry = 0; + let lt = 0; + let rt = 0; + let buttons = 0; + + if (gp.buttons) { + if (gp.buttons[6]) lt = gp.buttons[6].value; + if (gp.buttons[7]) rt = gp.buttons[7].value; + + for (let i = 0; i < gp.buttons.length && i < 32; i++) + if (gp.buttons[i].pressed) buttons |= 1 << i; + } + + if (gp.axes) { + if (gp.axes[0]) lx = gp.axes[0]; + if (gp.axes[1]) ly = gp.axes[1]; + if (gp.axes[2]) rx = gp.axes[2]; + if (gp.axes[3]) ry = gp.axes[3]; + } + + thread.postMessage({ + type: 'controller', + id: x, + state: state, + buttons: buttons, + lx: lx, + ly: ly, + rx: rx, + ry: ry, + lt: lt, + rt: rt, + }); + + // Disconnected + } else if (MTY.gps[x]) { + thread.postMessage({ + type: 'controller-disconnect', + id: x, + state: 2, + }); + + MTY.gps[x] = false; + } + } } - // Audio -async function mty_audio_queue(ctx, sampleRate, minBuffer, maxBuffer, channels) { - // Initialize on first queue otherwise the browser may complain about user interaction - if (!MTY.audioCtx) { - MTY.audioCtx = new AudioContext({sampleRate: sampleRate}); - - const baseFile = MTY_CURRENT_SCRIPT.pathname; - await MTY.audioCtx.audioWorklet.addModule(baseFile.replace('.js', '-worker.js')); - - const node = new AudioWorkletNode(MTY.audioCtx, 'MTY_Audio', { - outputChannelCount: [channels], - processorOptions: { - minBuffer, - maxBuffer, - }, - }); - - node.connect(MTY.audioCtx.destination); - node.port.postMessage(MTY.audioObjs); - } +async function mty_audio_queue( + ctx, + sampleRate, + minBuffer, + maxBuffer, + channels, +) { + // Initialize on first queue otherwise the browser may complain about user interaction + if (!MTY.audioCtx) { + MTY.audioCtx = new AudioContext({ sampleRate: sampleRate }); + + const baseFile = MTY_CURRENT_SCRIPT.pathname; + await MTY.audioCtx.audioWorklet.addModule( + baseFile.replace('.js', '-worker.js'), + ); + + const node = new AudioWorkletNode(MTY.audioCtx, 'MTY_Audio', { + outputChannelCount: [channels], + processorOptions: { + minBuffer, + maxBuffer, + }, + }); + + node.connect(MTY.audioCtx.destination); + node.port.postMessage(MTY.audioObjs); + } } - // Image async function mty_decode_image(input) { - const img = new Image(); - img.src = URL.createObjectURL(new Blob([input])); + const img = new Image(); + img.src = URL.createObjectURL(new Blob([input])); - await img.decode(); + await img.decode(); - const width = img.naturalWidth; - const height = img.naturalHeight; + const width = img.naturalWidth; + const height = img.naturalHeight; - const canvas = new OffscreenCanvas(width, height); - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, width, height); + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); - return ctx.getImageData(0, 0, width, height); + return ctx.getImageData(0, 0, width, height); } - // Net function mty_ws_new(obj) { - MTY.wsObj[MTY.wsIndex] = obj; + MTY.wsObj[MTY.wsIndex] = obj; - return MTY.wsIndex++; + return MTY.wsIndex++; } function mty_ws_del(index) { - let obj = MTY.wsObj[index]; + let obj = MTY.wsObj[index]; - delete MTY.wsObj[index]; + delete MTY.wsObj[index]; - return obj; + return obj; } function mty_ws_obj(index) { - return MTY.wsObj[index]; + return MTY.wsObj[index]; } async function mty_http_request(url, method, headers, body, buf) { - let error = false - let size = 0; - let status = 0; - let data = null; - - try { - const response = await fetch(url, { - method: method, - headers: headers, - body: body, - }); - - const res_ab = await response.arrayBuffer(); - data = new Uint8Array(res_ab); - - status = response.status; - size = data.byteLength; - - } catch (err) { - console.error(err); - error = true; - } - - return { - data, - error, - size, - status, - }; + let error = false; + let size = 0; + let status = 0; + let data = null; + + try { + const response = await fetch(url, { + method: method, + headers: headers, + body: body, + }); + + const res_ab = await response.arrayBuffer(); + data = new Uint8Array(res_ab); + + status = response.status; + size = data.byteLength; + } catch (err) { + console.error(err); + error = true; + } + + return { + data, + error, + size, + status, + }; } async function mty_ws_connect(url) { - return new Promise((resolve, reject) => { - const ws = new WebSocket(url); - const sab = new SharedArrayBuffer(4); - ws.sync = new Int32Array(sab, 0, 1); - ws.closeCode = 0; - ws.msgs = []; - - let tm = 0; // JS clients can't do WebSocket ping/pong, so we do our own pseudo keepalive to prevent dropped connections - ws.onclose = (ev) => { - ws.closeCode = ev.code == 1005 ? 1000 : ev.code; - clearInterval(tm); - resolve(null); - }; - - ws.onerror = (err) => { - console.error(err); - clearInterval(tm); - resolve(null); - }; - - ws.onopen = () => { - resolve(ws); - tm = setInterval(() => {ws.send('__ping__');}, 60000); // we've seen timeouts at the 10-minute mark, - // so a 60-second period should be more than enough - }; - - ws.onmessage = (ev) => { - ws.msgs.push(ev.data); - Atomics.notify(ws.sync, 0, 1); - }; - }); + return new Promise((resolve, reject) => { + const ws = new WebSocket(url); + const sab = new SharedArrayBuffer(4); + ws.sync = new Int32Array(sab, 0, 1); + ws.closeCode = 0; + ws.msgs = []; + + let tm = 0; // JS clients can't do WebSocket ping/pong, so we do our own pseudo keepalive to prevent dropped connections + ws.onclose = (ev) => { + ws.closeCode = ev.code == 1005 ? 1000 : ev.code; + clearInterval(tm); + resolve(null); + }; + + ws.onerror = (err) => { + console.error(err); + clearInterval(tm); + resolve(null); + }; + + ws.onopen = () => { + resolve(ws); + tm = setInterval(() => { + ws.send('__ping__'); + }, 60000); // we've seen timeouts at the 10-minute mark, + // so a 60-second period should be more than enough + }; + + ws.onmessage = (ev) => { + ws.msgs.push(ev.data); + Atomics.notify(ws.sync, 0, 1); + }; + }); } async function mty_ws_read(ws, timeout) { - let msg = ws.msgs.shift() + let msg = ws.msgs.shift(); - if (!msg) { - const r0 = Atomics.waitAsync(ws.sync, 0, 0, timeout); - const r1 = await r0.value; + if (!msg) { + const r0 = Atomics.waitAsync(ws.sync, 0, 0, timeout); + const r1 = await r0.value; - if (r1 != 'timed-out') - msg = ws.msgs.shift() - } + if (r1 != 'timed-out') msg = ws.msgs.shift(); + } - return msg ? mty_encode(msg) : null; + return msg ? mty_encode(msg) : null; } - // Entry function mty_supports_web_gl() { - try { - return document.createElement('canvas').getContext('webgl2'); - } catch (e) {} + try { + return document.createElement('canvas').getContext('webgl2'); + } catch (e) {} - return false; + return false; } function mty_update_interval(thread) { - // Poll gamepads - if (document.hasFocus()) - mty_poll_gamepads(thread); - - // Poll position changes - if (MTY.posX != window.screenX || MTY.posY != window.screenY) { - MTY.posX = window.screenX; - MTY.posY = window.screenY; - - thread.postMessage({ - type: 'move', - }); - } - - // send rect event - thread.postMessage({ - type: 'window-update', - windowInfo: mty_window_info(), - }); + // Poll gamepads + if (document.hasFocus()) mty_poll_gamepads(thread); + + // Poll position changes + if (MTY.posX != window.screenX || MTY.posY != window.screenY) { + MTY.posX = window.screenX; + MTY.posY = window.screenY; + + thread.postMessage({ + type: 'move', + }); + } + + // send rect event + thread.postMessage({ + type: 'window-update', + windowInfo: mty_window_info(), + }); } -function mty_thread_start(threadId, bin, wasmBuf, memory, startArg, userEnv, kbMap, psync, audioObjs, name) { - const baseFile = MTY_CURRENT_SCRIPT.pathname; - const worker = new Worker(baseFile.replace('.js', '-worker.js'), {name: name}); - - worker.onmessage = mty_thread_message; - - worker.postMessage({ - type: 'init', - file: baseFile, - bin: bin, - wasmBuf: wasmBuf, - psync: psync, - windowInfo: mty_window_info(), - args: window.location.search, - hostname: window.location.hostname, - userEnv: userEnv ? Object.keys(userEnv) : [], - kbMap: kbMap, - startArg: startArg, - threadId: threadId, - memory: memory, - audioObjs, - }); - - return worker; +function mty_thread_start( + threadId, + bin, + wasmBuf, + memory, + startArg, + userEnv, + kbMap, + psync, + audioObjs, + name, +) { + const baseFile = MTY_CURRENT_SCRIPT.pathname; + const worker = new Worker(baseFile.replace('.js', '-worker.js'), { + name: name, + }); + + worker.onmessage = mty_thread_message; + + worker.postMessage({ + type: 'init', + file: baseFile, + bin: bin, + wasmBuf: wasmBuf, + psync: psync, + windowInfo: mty_window_info(), + args: window.location.search, + hostname: window.location.hostname, + userEnv: userEnv ? Object.keys(userEnv) : [], + kbMap: kbMap, + startArg: startArg, + threadId: threadId, + memory: memory, + audioObjs, + }); + + return worker; } async function MTY_Start(bin, container, userEnv) { - if (!mty_supports_web_gl()) - return false; - - MTY.bin = bin; - MTY.userEnv = userEnv; - MTY.psync = new Int32Array(new SharedArrayBuffer(4)); - MTY.audioObjs = { - buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)), - control: new Int32Array(new SharedArrayBuffer(32)), - }; - - // Drawing surface - MTY.canvas = document.createElement('canvas'); - MTY.renderer = MTY.canvas.getContext('bitmaprenderer'); - MTY.canvas.style.width = '100%'; - MTY.canvas.style.height = '100%'; - container.appendChild(MTY.canvas); - mty_update_canvas(MTY.canvas); - - // WASM binary - const wasmRes = await fetch(bin); - MTY.wasmBuf = await wasmRes.arrayBuffer(); - - // Shared global memory - MTY_MEMORY = new WebAssembly.Memory({ - initial: 512, // 32 MB - maximum: 16384, // 1 GB - shared: true, - }); - - // Load keyboard map - MTY.kbMap = {}; - if (navigator.keyboard) { - const layout = await navigator.keyboard.getLayoutMap(); - - layout.forEach((currentValue, index) => { - MTY.kbMap[index] = currentValue; - }); - } - - // Main thread - MTY.mainThread = mty_thread_start(MTY.threadId, bin, MTY.wasmBuf, MTY_MEMORY, - 0, userEnv, MTY.kbMap, MTY.psync, MTY.audioObjs, 'main'); - - // Init position, update loop - MTY.posX = window.screenX; - MTY.posY = window.screenY; - setInterval(() => { - mty_update_interval(MTY.mainThread); - }, 10); - - // Vsync - const vsync = () => { - mty_signal(MTY.psync, true); - requestAnimationFrame(vsync); - }; - requestAnimationFrame(vsync); - - // Add input events - mty_add_input_events(MTY.mainThread); - - return true; + if (!mty_supports_web_gl()) return false; + + MTY.bin = bin; + MTY.userEnv = userEnv; + MTY.psync = new Int32Array(new SharedArrayBuffer(4)); + MTY.audioObjs = { + buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)), + control: new Int32Array(new SharedArrayBuffer(32)), + }; + + // Drawing surface + MTY.canvas = document.createElement('canvas'); + MTY.renderer = MTY.canvas.getContext('bitmaprenderer'); + MTY.canvas.style.width = '100%'; + MTY.canvas.style.height = '100%'; + container.appendChild(MTY.canvas); + mty_update_canvas(MTY.canvas); + + // WASM binary + const wasmRes = await fetch(bin); + MTY.wasmBuf = await wasmRes.arrayBuffer(); + + // Shared global memory + MTY_MEMORY = new WebAssembly.Memory({ + initial: 512, // 32 MB + maximum: 16384, // 1 GB + shared: true, + }); + + // Load keyboard map + MTY.kbMap = {}; + if (navigator.keyboard) { + const layout = await navigator.keyboard.getLayoutMap(); + + layout.forEach((currentValue, index) => { + MTY.kbMap[index] = currentValue; + }); + } + + // Main thread + MTY.mainThread = mty_thread_start( + MTY.threadId, + bin, + MTY.wasmBuf, + MTY_MEMORY, + 0, + userEnv, + MTY.kbMap, + MTY.psync, + MTY.audioObjs, + 'main', + ); + + // Init position, update loop + MTY.posX = window.screenX; + MTY.posY = window.screenY; + setInterval(() => { + mty_update_interval(MTY.mainThread); + }, 10); + + // Vsync + const vsync = () => { + mty_signal(MTY.psync, true); + requestAnimationFrame(vsync); + }; + requestAnimationFrame(vsync); + + // Add input events + mty_add_input_events(MTY.mainThread); + + return true; } async function mty_thread_message(ev) { - const msg = ev.data; - - switch (msg.type) { - case 'user-env': - msg.sab[0] = MTY.userEnv[msg.name](...msg.args); - mty_signal(msg.sync); - break; - case 'thread': { - MTY.threadId++; - - const worker = mty_thread_start(MTY.threadId, MTY.bin, MTY.wasmBuf, MTY_MEMORY, - msg.startArg, MTY.userEnv, MTY.kbMap, MTY.psync, MTY.audioObjs, 'thread-' + MTY.threadId); - - msg.sab[0] = MTY.threadId; - mty_signal(msg.sync); - break; - } - case 'present': - MTY.renderer.transferFromImageBitmap(msg.image); - break; - case 'decode-image': { - const image = await mty_decode_image(msg.input); - - this.tmp = image.data; - msg.sab[0] = image.width; - msg.sab[1] = image.height; - - mty_signal(msg.sync); - break; - } - case 'kb-grab': - MTY.kb_grab = msg.grab; - break; - case 'title': - document.title = msg.title; - break; - case 'get-ls': { - const val = window.localStorage[msg.key]; - - if (val) { - this.tmp = mty_b64_to_buf(val); - msg.sab[0] = this.tmp.byteLength; - - } else { - msg.sab[0] = 0; - } - - mty_signal(msg.sync); - break; - } - case 'set-ls': - window.localStorage[msg.key] = mty_buf_to_b64(msg.val); - mty_signal(msg.sync); - break; - case 'remove-ls': - window.localStorage.removeItem(msg.key); - mty_signal(msg.sync); - break; - case 'alert': - mty_alert(msg.title, msg.msg); - break; - case 'fullscreen': - mty_set_fullscreen(msg.fullscreen); - break; - case 'wake-lock': - mty_wake_lock(msg.enable); - break; - case 'rumble': - mty_rumble_gamepad(msg.id, msg.low, msg.high); - break; - case 'show-cursor': - mty_show_cursor(msg.show); - break; - case 'get-clip': - // FIXME Unsupported on Firefox - if (navigator.clipboard.readText) { - const text = await navigator.clipboard.readText(); - - this.tmp = mty_encode(text); - msg.sab[0] = this.tmp.byteLength; - - } else { - msg.sab[0] = 0; - } - - mty_signal(msg.sync); - break; - case 'set-clip': - navigator.clipboard.writeText(mty_str_to_js(msg.text)); - break; - case 'pointer-lock': - mty_set_pointer_lock(msg.enable); - break; - case 'cursor-default': - mty_use_default_cursor(msg.use_default); - break; - case 'cursor-rgba': - mty_set_rgba_cursor(msg.buf, msg.width, msg.height, msg.hot_x, msg.hot_y); - break; - case 'cursor-png': - mty_set_png_cursor(msg.buf, msg.hot_x, msg.hot_y); - break; - case 'uri': - mty_set_action(() => { - window.open(msg.uri, '_blank'); - }); - break; - case 'http': { - const res = await mty_http_request(msg.url, msg.method, msg.headers, msg.body); - - this.tmp = res.data; - msg.sab[0] = res.error ? 1 : 0; - msg.sab[1] = res.size; - msg.sab[2] = res.status; - - mty_signal(msg.sync); - break; - } - case 'ws-connect': { - const ws = await mty_ws_connect(msg.url); - msg.sab[0] = ws ? mty_ws_new(ws) : 0; - mty_signal(msg.sync); - break; - } - case 'ws-read': { - msg.sab[0] = 3; // MTY_ASYNC_ERROR - - const ws = mty_ws_obj(msg.ctx); - - if (ws) { - if (ws.closeCode != 0) { - msg.sab[0] = 1; // MTY_ASYNC_DONE - - } else { - const buf = await mty_ws_read(ws, msg.timeout); - - if (buf) { - this.tmp = buf; - msg.sab[0] = 0; // MTY_ASYNC_OK - msg.sab[1] = buf.length; - - } else { - msg.sab[0] = 2; // MTY_ASYNC_CONTINUE - } - } - } - - mty_signal(msg.sync); - break; - } - case 'ws-write': { - const ws = mty_ws_obj(msg.ctx); - if (ws) - ws.send(msg.text) - break; - } - case 'ws-close': { - const ws = mty_ws_obj(msg.ctx); - if (ws) { - ws.close(); - mty_ws_del(msg.ctx); - } - break; - } - case 'ws-code': { - msg.sab[0] = 0; - - const ws = mty_ws_obj(msg.ctx); - if (ws) - msg.sab[0] = ws.closeCode; - - mty_signal(msg.sync); - break; - } - case 'audio-queue': - mty_audio_queue(MTY.audio, msg.sampleRate, msg.minBuffer, - msg.maxBuffer, msg.channels); - break; - case 'audio-destroy': - if (MTY.audioCtx) - MTY.audioCtx.close(); - delete MTY.audioCtx; - break; - case 'async-copy': - msg.sab8.set(this.tmp); - delete this.tmp; - - mty_signal(msg.sync); - break; - case 'wv-create': - MTY.webview = document.createElement('iframe'); - - MTY.webview.style.visibility = 'hidden'; - MTY.webview.style.position = 'fixed'; - MTY.webview.style.border = 'none'; - MTY.webview.style.width = '100%'; - MTY.webview.style.height = '100%'; - MTY.webview.style.inset = '0'; - - window.addEventListener('message', function (message) { - MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message.data}); - }); - - document.body.appendChild(MTY.webview); - break; - case 'wv-destroy': - document.body.removeChild(MTY.webview); - delete MTY.webview; - break; - case 'wv-navigate': - if (msg.url) { - MTY.webview.src = msg.source; - - MTY.webview.onload = () => { - try { - const script = MTY.webview.contentWindow.document.createElement('script'); - script.textContent = "window.parent.postMessage('R');window.MTY_NativeSendText = (text) => { window.parent.postMessage('T' + text); }"; - MTY.webview.contentWindow.document.head.appendChild(script); - const userAgentScript = MTY.webview.contentWindow.document.createElement('script'); - userAgentScript.textContent = "Object.defineProperty(window, 'MTY_GetPlatform', { value: () => 'web'});"; - MTY.webview.contentWindow.document.head.appendChild(userAgentScript); - setTimeout(() => MTY.webview.style.visibility = 'visible', 250); - } catch (e) { - console.error('Failed to inject script into iframe (cross-origin restriction):', e); - setTimeout(() => MTY.webview.style.visibility = 'visible', 250); - } - }; - } else { - const blob = new Blob([msg.source], { type: 'text/html' }); - MTY.webview.src = URL.createObjectURL(blob); - } - break; - case 'wv-show': - MTY.webview.style.visibility = msg.show ? 'visible' : 'hidden'; - break; - case 'wv-is-visible': - msg.sab[0] = MTY.webview.style.visibility != 'visible'; - mty_signal(msg.sync); - break; - case 'wv-send-text': - // wv-send-text sends native app messages back to the running UI - MTY.webview.contentWindow.MTY_NativeListener(msg.message); - break; - case 'wv-reload': - MTY.webview.contentWindow.location.reload(); - break; - } -} \ No newline at end of file + const msg = ev.data; + + switch (msg.type) { + case 'user-env': + msg.sab[0] = MTY.userEnv[msg.name](...msg.args); + mty_signal(msg.sync); + break; + case 'thread': { + MTY.threadId++; + + const worker = mty_thread_start( + MTY.threadId, + MTY.bin, + MTY.wasmBuf, + MTY_MEMORY, + msg.startArg, + MTY.userEnv, + MTY.kbMap, + MTY.psync, + MTY.audioObjs, + 'thread-' + MTY.threadId, + ); + + msg.sab[0] = MTY.threadId; + mty_signal(msg.sync); + break; + } + case 'present': + MTY.renderer.transferFromImageBitmap(msg.image); + break; + case 'decode-image': { + const image = await mty_decode_image(msg.input); + + this.tmp = image.data; + msg.sab[0] = image.width; + msg.sab[1] = image.height; + + mty_signal(msg.sync); + break; + } + case 'kb-grab': + MTY.kb_grab = msg.grab; + break; + case 'title': + document.title = msg.title; + break; + case 'get-ls': { + const val = window.localStorage[msg.key]; + + if (val) { + this.tmp = mty_b64_to_buf(val); + msg.sab[0] = this.tmp.byteLength; + } else { + msg.sab[0] = 0; + } + + mty_signal(msg.sync); + break; + } + case 'set-ls': + window.localStorage[msg.key] = mty_buf_to_b64(msg.val); + mty_signal(msg.sync); + break; + case 'remove-ls': + window.localStorage.removeItem(msg.key); + mty_signal(msg.sync); + break; + case 'alert': + mty_alert(msg.title, msg.msg); + break; + case 'fullscreen': + mty_set_fullscreen(msg.fullscreen); + break; + case 'wake-lock': + mty_wake_lock(msg.enable); + break; + case 'rumble': + mty_rumble_gamepad(msg.id, msg.low, msg.high); + break; + case 'show-cursor': + mty_show_cursor(msg.show); + break; + case 'get-clip': + // FIXME Unsupported on Firefox + if (navigator.clipboard.readText) { + const text = await navigator.clipboard.readText(); + + this.tmp = mty_encode(text); + msg.sab[0] = this.tmp.byteLength; + } else { + msg.sab[0] = 0; + } + + mty_signal(msg.sync); + break; + case 'set-clip': + navigator.clipboard.writeText(mty_str_to_js(msg.text)); + break; + case 'pointer-lock': + mty_set_pointer_lock(msg.enable); + break; + case 'cursor-default': + mty_use_default_cursor(msg.use_default); + break; + case 'cursor-rgba': + mty_set_rgba_cursor(msg.buf, msg.width, msg.height, msg.hot_x, msg.hot_y); + break; + case 'cursor-png': + mty_set_png_cursor(msg.buf, msg.hot_x, msg.hot_y); + break; + case 'uri': + mty_set_action(() => { + window.open(msg.uri, '_blank'); + }); + break; + case 'http': { + const res = await mty_http_request( + msg.url, + msg.method, + msg.headers, + msg.body, + ); + + this.tmp = res.data; + msg.sab[0] = res.error ? 1 : 0; + msg.sab[1] = res.size; + msg.sab[2] = res.status; + + mty_signal(msg.sync); + break; + } + case 'ws-connect': { + const ws = await mty_ws_connect(msg.url); + msg.sab[0] = ws ? mty_ws_new(ws) : 0; + mty_signal(msg.sync); + break; + } + case 'ws-read': { + msg.sab[0] = 3; // MTY_ASYNC_ERROR + + const ws = mty_ws_obj(msg.ctx); + + if (ws) { + if (ws.closeCode != 0) { + msg.sab[0] = 1; // MTY_ASYNC_DONE + } else { + const buf = await mty_ws_read(ws, msg.timeout); + + if (buf) { + this.tmp = buf; + msg.sab[0] = 0; // MTY_ASYNC_OK + msg.sab[1] = buf.length; + } else { + msg.sab[0] = 2; // MTY_ASYNC_CONTINUE + } + } + } + + mty_signal(msg.sync); + break; + } + case 'ws-write': { + const ws = mty_ws_obj(msg.ctx); + if (ws) ws.send(msg.text); + break; + } + case 'ws-close': { + const ws = mty_ws_obj(msg.ctx); + if (ws) { + ws.close(); + mty_ws_del(msg.ctx); + } + break; + } + case 'ws-code': { + msg.sab[0] = 0; + + const ws = mty_ws_obj(msg.ctx); + if (ws) msg.sab[0] = ws.closeCode; + + mty_signal(msg.sync); + break; + } + case 'audio-queue': + mty_audio_queue( + MTY.audio, + msg.sampleRate, + msg.minBuffer, + msg.maxBuffer, + msg.channels, + ); + break; + case 'audio-destroy': + if (MTY.audioCtx) MTY.audioCtx.close(); + delete MTY.audioCtx; + break; + case 'async-copy': + msg.sab8.set(this.tmp); + delete this.tmp; + + mty_signal(msg.sync); + break; + case 'wv-create': + MTY.webview = document.createElement('iframe'); + MTY.webview.title = 'Parsec application'; + + MTY.webview.style.visibility = 'hidden'; + MTY.webview.style.position = 'fixed'; + MTY.webview.style.border = 'none'; + MTY.webview.style.width = '100%'; + MTY.webview.style.height = '100%'; + MTY.webview.style.inset = '0'; + + window.addEventListener('message', function (message) { + MTY.mainThread.postMessage({ + type: 'wv-event', + ctx: msg.ctx, + message: message.data, + }); + }); + + document.body.appendChild(MTY.webview); + break; + case 'wv-destroy': + document.body.removeChild(MTY.webview); + delete MTY.webview; + break; + case 'wv-navigate': + if (msg.url) { + MTY.webview.src = msg.source; + + MTY.webview.onload = () => { + try { + const script = + MTY.webview.contentWindow.document.createElement('script'); + script.textContent = + "window.parent.postMessage('R');window.MTY_NativeSendText = (text) => { window.parent.postMessage('T' + text); }"; + MTY.webview.contentWindow.document.head.appendChild(script); + const userAgentScript = + MTY.webview.contentWindow.document.createElement('script'); + userAgentScript.textContent = + "Object.defineProperty(window, 'MTY_GetPlatform', { value: () => 'web'});"; + MTY.webview.contentWindow.document.head.appendChild( + userAgentScript, + ); + setTimeout(() => (MTY.webview.style.visibility = 'visible'), 250); + } catch (e) { + console.error( + 'Failed to inject script into iframe (cross-origin restriction):', + e, + ); + setTimeout(() => (MTY.webview.style.visibility = 'visible'), 250); + } + }; + } else { + const blob = new Blob([msg.source], { type: 'text/html' }); + MTY.webview.src = URL.createObjectURL(blob); + } + break; + case 'wv-show': + MTY.webview.style.visibility = msg.show ? 'visible' : 'hidden'; + break; + case 'wv-is-visible': + msg.sab[0] = MTY.webview.style.visibility != 'visible'; + mty_signal(msg.sync); + break; + case 'wv-send-text': + // wv-send-text sends native app messages back to the running UI + MTY.webview.contentWindow.MTY_NativeListener(msg.message); + break; + case 'wv-reload': + MTY.webview.contentWindow.location.reload(); + break; + } +} From 0e47136936e976dc29f989c59c90961b45898b41 Mon Sep 17 00:00:00 2001 From: margaux Date: Tue, 2 Jun 2026 16:02:14 +0200 Subject: [PATCH 28/30] Revert "add title on iframe that render the webview" This reverts commit f39f6c4a76e107232281646e93960ffd93eb0e9f. --- src/unix/web/matoya.js | 1845 ++++++++++++++++++++-------------------- 1 file changed, 905 insertions(+), 940 deletions(-) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 6c329281..22c38739 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -2,6 +2,7 @@ // If a copy of the MIT License was not distributed with this file, // You can obtain one at https://spdx.org/licenses/MIT.html. + // Global State let MTY_MEMORY; @@ -9,1167 +10,1131 @@ let MTY_CURRENT_SCRIPT; // Worker if (typeof importScripts == 'function') { - MTY_CURRENT_SCRIPT = location; + MTY_CURRENT_SCRIPT = location; - // Main thread +// Main thread } else { - MTY_CURRENT_SCRIPT = new URL(document.currentScript.src); - - window.MTY = { - wsIndex: 1, - wsObj: {}, - cursorId: 0, - threadId: 1, - cursorCache: {}, - cursorClass: '', - defaultCursor: false, - synthesizeEsc: true, - relative: false, - gps: [false, false, false, false], - }; + MTY_CURRENT_SCRIPT = new URL(document.currentScript.src); + + window.MTY = { + wsIndex: 1, + wsObj: {}, + cursorId: 0, + threadId: 1, + cursorCache: {}, + cursorClass: '', + defaultCursor: false, + synthesizeEsc: true, + relative: false, + gps: [false, false, false, false], + }; } + // Memory function mty_encode(str) { - return new TextEncoder().encode(str); + return new TextEncoder().encode(str); } function mty_decode(buf) { - return new TextDecoder().decode(buf); + return new TextDecoder().decode(buf); } function mty_strlen(buf) { - let len = 0; - for (; buf[len] != 0; len++); + let len = 0; + for (; buf[len] != 0; len++); - return len; + return len; } function mty_memcpy(ptr, buf) { - new Uint8Array(MTY_MEMORY.buffer, ptr, buf.byteLength).set(buf); + new Uint8Array(MTY_MEMORY.buffer, ptr, buf.byteLength).set(buf); } function mty_strcpy(ptr, buf) { - mty_memcpy(ptr, buf); - mty_set_int8(ptr + buf.byteLength, 0); + mty_memcpy(ptr, buf); + mty_set_int8(ptr + buf.byteLength, 0); } function mty_dup(ptr, size) { - return new Uint8Array(MTY_MEMORY.buffer, ptr).slice(0, size); + return new Uint8Array(MTY_MEMORY.buffer, ptr).slice(0, size); } function mty_str_to_js(ptr) { - const buf = new Uint8Array(MTY_MEMORY.buffer, ptr); + const buf = new Uint8Array(MTY_MEMORY.buffer, ptr); - return mty_decode(buf.slice(0, mty_strlen(buf))); + return mty_decode(buf.slice(0, mty_strlen(buf))); } function mty_str_to_c(str, ptr, size) { - const buf = mty_encode(str); + const buf = mty_encode(str); - if (buf.byteLength >= size) throw 'mty_str_to_c overflow'; + if (buf.byteLength >= size) + throw 'mty_str_to_c overflow' - mty_strcpy(ptr, buf); + mty_strcpy(ptr, buf); } function mty_get_uint8(ptr) { - return new DataView(MTY_MEMORY.buffer).getUint8(ptr); + return new DataView(MTY_MEMORY.buffer).getUint8(ptr); } function mty_set_int8(ptr, value) { - new DataView(MTY_MEMORY.buffer).setInt8(ptr, value); + new DataView(MTY_MEMORY.buffer).setInt8(ptr, value); } function mty_set_uint16(ptr, value) { - new DataView(MTY_MEMORY.buffer).setUint16(ptr, value, true); + new DataView(MTY_MEMORY.buffer).setUint16(ptr, value, true); } function mty_get_uint32(ptr) { - return new DataView(MTY_MEMORY.buffer).getUint32(ptr, true); + return new DataView(MTY_MEMORY.buffer).getUint32(ptr, true); } function mty_set_uint32(ptr, value) { - new DataView(MTY_MEMORY.buffer).setUint32(ptr, value, true); + new DataView(MTY_MEMORY.buffer).setUint32(ptr, value, true); } function mty_get_uint64(ptr, value) { - return new DataView(MTY_MEMORY.buffer).getBigUint64(ptr, true); + return new DataView(MTY_MEMORY.buffer).getBigUint64(ptr, true); } function mty_set_uint64(ptr, value) { - new DataView(MTY_MEMORY.buffer).setBigUint64(ptr, BigInt(value), true); + new DataView(MTY_MEMORY.buffer).setBigUint64(ptr, BigInt(value), true); } function mty_set_float(ptr, value) { - new DataView(MTY_MEMORY.buffer).setFloat32(ptr, value, true); + new DataView(MTY_MEMORY.buffer).setFloat32(ptr, value, true); } + // Base64 function mty_buf_to_b64(buf) { - let bstr = ''; - for (let x = 0; x < buf.byteLength; x++) bstr += String.fromCharCode(buf[x]); + let bstr = ''; + for (let x = 0; x < buf.byteLength; x++) + bstr += String.fromCharCode(buf[x]); - return btoa(bstr); + return btoa(bstr); } function mty_b64_to_buf(b64) { - const bstr = atob(b64); - const buf = new Uint8Array(bstr.length); + const bstr = atob(b64); + const buf = new Uint8Array(bstr.length); - for (let x = 0; x < bstr.length; x++) buf[x] = bstr.charCodeAt(x); + for (let x = 0; x < bstr.length; x++) + buf[x] = bstr.charCodeAt(x); - return buf; + return buf; } + // Synchronization function mty_wait(sync) { - if (Atomics.compareExchange(sync, 0, 0, 1) == 0) Atomics.wait(sync, 0, 1); + if (Atomics.compareExchange(sync, 0, 0, 1) == 0) + Atomics.wait(sync, 0, 1); - Atomics.store(sync, 0, 0); + Atomics.store(sync, 0, 0); } function mty_signal(sync, allow_miss = false) { - if (Atomics.compareExchange(sync, 0, 0, 1) != 0) - while (Atomics.notify(sync, 0, 1) == 0 && !allow_miss); + if (Atomics.compareExchange(sync, 0, 0, 1) != 0) + while (Atomics.notify(sync, 0, 1) == 0 && !allow_miss); } function MTY_SignalPtr(csync) { - mty_signal(new Int32Array(MTY_MEMORY.buffer, csync, 1)); + mty_signal(new Int32Array(MTY_MEMORY.buffer, csync, 1)); } + // Input function mty_scaled(num) { - return Math.round(num * window.devicePixelRatio); + return Math.round(num * window.devicePixelRatio); } function mty_correct_relative() { - if (!document.pointerLockElement && MTY.relative) - MTY.canvas.requestPointerLock(); + if (!document.pointerLockElement && MTY.relative) + MTY.canvas.requestPointerLock(); } function mty_get_mods(ev) { - let mods = 0; + let mods = 0; - if (ev.shiftKey) mods |= 0x01; - if (ev.ctrlKey) mods |= 0x02; - if (ev.altKey) mods |= 0x04; - if (ev.metaKey) mods |= 0x08; + if (ev.shiftKey) mods |= 0x01; + if (ev.ctrlKey) mods |= 0x02; + if (ev.altKey) mods |= 0x04; + if (ev.metaKey) mods |= 0x08; - if (ev.getModifierState('CapsLock')) mods |= 0x10; - if (ev.getModifierState('NumLock')) mods |= 0x20; + if (ev.getModifierState("CapsLock")) mods |= 0x10; + if (ev.getModifierState("NumLock") ) mods |= 0x20; - return mods; + return mods; } function mty_set_pointer_lock(enable) { - if (enable && !document.pointerLockElement) { - MTY.canvas.requestPointerLock(); - } else if (!enable && document.pointerLockElement) { - MTY.synthesizeEsc = false; - document.exitPointerLock(); - } - - MTY.relative = enable; + if (enable && !document.pointerLockElement) { + MTY.canvas.requestPointerLock(); + + } else if (!enable && document.pointerLockElement) { + MTY.synthesizeEsc = false; + document.exitPointerLock(); + } + + MTY.relative = enable; } function mty_allow_default(ev) { - // The "allowed" browser hotkey list. Copy/Paste, Refresh, fullscreen, developer console, and tab switching - - return ( - ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyV') || - ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyC') || - ((ev.ctrlKey || ev.shiftKey) && ev.code == 'KeyI') || - (ev.ctrlKey && ev.code == 'KeyR') || - (ev.ctrlKey && ev.code == 'F5') || - (ev.ctrlKey && ev.code == 'Digit1') || - (ev.ctrlKey && ev.code == 'Digit2') || - (ev.ctrlKey && ev.code == 'Digit3') || - (ev.ctrlKey && ev.code == 'Digit4') || - (ev.ctrlKey && ev.code == 'Digit5') || - (ev.ctrlKey && ev.code == 'Digit6') || - (ev.ctrlKey && ev.code == 'Digit7') || - (ev.ctrlKey && ev.code == 'Digit8') || - (ev.ctrlKey && ev.code == 'Digit9') || - ev.code == 'F5' || - ev.code == 'F11' || - ev.code == 'F12' - ); + // The "allowed" browser hotkey list. Copy/Paste, Refresh, fullscreen, developer console, and tab switching + + return ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyV') || + ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyC') || + ((ev.ctrlKey || ev.shiftKey) && ev.code == 'KeyI') || + (ev.ctrlKey && ev.code == 'KeyR') || + (ev.ctrlKey && ev.code == 'F5') || + (ev.ctrlKey && ev.code == 'Digit1') || + (ev.ctrlKey && ev.code == 'Digit2') || + (ev.ctrlKey && ev.code == 'Digit3') || + (ev.ctrlKey && ev.code == 'Digit4') || + (ev.ctrlKey && ev.code == 'Digit5') || + (ev.ctrlKey && ev.code == 'Digit6') || + (ev.ctrlKey && ev.code == 'Digit7') || + (ev.ctrlKey && ev.code == 'Digit8') || + (ev.ctrlKey && ev.code == 'Digit9') || + (ev.code == 'F5') || + (ev.code == 'F11') || + (ev.code == 'F12'); } function mty_add_input_events(thread) { - MTY.canvas.addEventListener('mousemove', (ev) => { - let x = mty_scaled(ev.clientX); - let y = mty_scaled(ev.clientY); - - if (MTY.relative) { - x = ev.movementX; - y = ev.movementY; - } - - thread.postMessage({ - type: 'motion', - relative: MTY.relative, - x: x, - y: y, - }); - }); - - document.addEventListener('pointerlockchange', (ev) => { - // Left relative via the ESC key, which swallows a natural ESC keypress - if (!document.pointerLockElement && MTY.synthesizeEsc) { - const msg = { - type: 'keyboard', - pressed: true, - code: 'Escape', - key: 'Escape', - mods: 0, - }; - - thread.postMessage(msg); - - msg.pressed = false; - thread.postMessage(msg); - } - - MTY.synthesizeEsc = true; - }); - - window.addEventListener('click', (ev) => { - // Popup blockers can interfere with window.open if not called from within the 'click' listener - mty_run_action(); - ev.preventDefault(); - }); - - window.addEventListener('mousedown', (ev) => { - mty_correct_relative(); - ev.preventDefault(); - - thread.postMessage({ - type: 'button', - pressed: true, - button: ev.button, - x: mty_scaled(ev.clientX), - y: mty_scaled(ev.clientY), - }); - }); - - window.addEventListener('mouseup', (ev) => { - ev.preventDefault(); - - thread.postMessage({ - type: 'button', - pressed: false, - button: ev.button, - x: mty_scaled(ev.clientX), - y: mty_scaled(ev.clientY), - }); - }); - - MTY.canvas.addEventListener('contextmenu', (ev) => { - ev.preventDefault(); - }); - - MTY.canvas.addEventListener('dragover', (ev) => { - ev.preventDefault(); - }); - - MTY.canvas.addEventListener( - 'wheel', - (ev) => { - let x = ev.deltaX > 0 ? 120 : ev.deltaX < 0 ? -120 : 0; - let y = ev.deltaY > 0 ? 120 : ev.deltaY < 0 ? -120 : 0; - - thread.postMessage({ - type: 'scroll', - x: x, - y: y, - }); - }, - { passive: true }, - ); - - window.addEventListener('keydown', (ev) => { - mty_correct_relative(); - - thread.postMessage({ - type: 'keyboard', - pressed: true, - code: ev.code, - key: ev.key, - mods: mty_get_mods(ev), - }); - - if (MTY.kb_grab || !mty_allow_default(ev)) ev.preventDefault(); - }); - - window.addEventListener('keyup', (ev) => { - thread.postMessage({ - type: 'keyboard', - pressed: false, - code: ev.code, - key: '', - mods: mty_get_mods(ev), - }); - - if (MTY.kb_grab || !mty_allow_default(ev)) ev.preventDefault(); - }); - - document.addEventListener('visibilitychange', (ev) => { - thread.postMessage({ - type: 'focus', - focus: document.visibilityState == 'visible', - }); - }); - - window.addEventListener('resize', (ev) => { - const rect = mty_update_canvas(MTY.canvas); - - thread.postMessage({ - type: 'size', - width: mty_scaled(rect.width), - height: mty_scaled(rect.height), - }); - }); - - MTY.canvas.addEventListener('drop', (ev) => { - ev.preventDefault(); - - if (!ev.dataTransfer.items) return; - - for (let x = 0; x < ev.dataTransfer.items.length; x++) { - if (ev.dataTransfer.items[x].kind == 'file') { - let file = ev.dataTransfer.items[x].getAsFile(); - - const reader = new FileReader(); - reader.addEventListener('loadend', (fev) => { - if (reader.readyState == 2) { - thread.postMessage( - { - type: 'drop', - name: file.name, - data: reader.result, - }, - [reader.result], - ); - } - }); - - reader.readAsArrayBuffer(file); - break; - } - } - }); + MTY.canvas.addEventListener('mousemove', (ev) => { + let x = mty_scaled(ev.clientX); + let y = mty_scaled(ev.clientY); + + if (MTY.relative) { + x = ev.movementX; + y = ev.movementY; + } + + thread.postMessage({ + type: 'motion', + relative: MTY.relative, + x: x, + y: y, + }); + }); + + document.addEventListener('pointerlockchange', (ev) => { + // Left relative via the ESC key, which swallows a natural ESC keypress + if (!document.pointerLockElement && MTY.synthesizeEsc) { + const msg = { + type: 'keyboard', + pressed: true, + code: 'Escape', + key: 'Escape', + mods: 0, + }; + + thread.postMessage(msg); + + msg.pressed = false; + thread.postMessage(msg); + } + + MTY.synthesizeEsc = true; + }); + + window.addEventListener('click', (ev) => { + // Popup blockers can interfere with window.open if not called from within the 'click' listener + mty_run_action(); + ev.preventDefault(); + }); + + window.addEventListener('mousedown', (ev) => { + mty_correct_relative(); + ev.preventDefault(); + + thread.postMessage({ + type: 'button', + pressed: true, + button: ev.button, + x: mty_scaled(ev.clientX), + y: mty_scaled(ev.clientY), + }); + }); + + window.addEventListener('mouseup', (ev) => { + ev.preventDefault(); + + thread.postMessage({ + type: 'button', + pressed: false, + button: ev.button, + x: mty_scaled(ev.clientX), + y: mty_scaled(ev.clientY), + }); + }); + + MTY.canvas.addEventListener('contextmenu', (ev) => { + ev.preventDefault(); + }); + + MTY.canvas.addEventListener('dragover', (ev) => { + ev.preventDefault(); + }); + + MTY.canvas.addEventListener('wheel', (ev) => { + let x = ev.deltaX > 0 ? 120 : ev.deltaX < 0 ? -120 : 0; + let y = ev.deltaY > 0 ? 120 : ev.deltaY < 0 ? -120 : 0; + + thread.postMessage({ + type: 'scroll', + x: x, + y: y, + }); + }, {passive: true}); + + window.addEventListener('keydown', (ev) => { + mty_correct_relative(); + + thread.postMessage({ + type: 'keyboard', + pressed: true, + code: ev.code, + key: ev.key, + mods: mty_get_mods(ev), + }); + + if (MTY.kb_grab || !mty_allow_default(ev)) + ev.preventDefault(); + }); + + window.addEventListener('keyup', (ev) => { + thread.postMessage({ + type: 'keyboard', + pressed: false, + code: ev.code, + key: '', + mods: mty_get_mods(ev), + }); + + if (MTY.kb_grab || !mty_allow_default(ev)) + ev.preventDefault(); + }); + + document.addEventListener('visibilitychange', (ev) => { + thread.postMessage({ + type: 'focus', + focus: document.visibilityState == 'visible', + }); + }); + + window.addEventListener('resize', (ev) => { + const rect = mty_update_canvas(MTY.canvas); + + thread.postMessage({ + type: 'size', + width: mty_scaled(rect.width), + height: mty_scaled(rect.height), + }); + }); + + MTY.canvas.addEventListener('drop', (ev) => { + ev.preventDefault(); + + if (!ev.dataTransfer.items) + return; + + for (let x = 0; x < ev.dataTransfer.items.length; x++) { + if (ev.dataTransfer.items[x].kind == 'file') { + let file = ev.dataTransfer.items[x].getAsFile(); + + const reader = new FileReader(); + reader.addEventListener('loadend', (fev) => { + if (reader.readyState == 2) { + thread.postMessage({ + type: 'drop', + name: file.name, + data: reader.result, + }, [reader.result]); + } + }); + + reader.readAsArrayBuffer(file); + break; + } + } + }); } + // Dialog function mty_alert(title, msg) { - window.alert(mty_str_to_js(title) + '\n\n' + mty_str_to_js(msg)); + window.alert(mty_str_to_js(title) + '\n\n' + mty_str_to_js(msg)); } + // URI opener function mty_run_action() { - setTimeout(() => { - if (MTY.action) { - MTY.action(); - delete MTY.action; - } - }, 100); + setTimeout(() => { + if (MTY.action) { + MTY.action(); + delete MTY.action; + } + }, 100); } function mty_set_action(action) { - MTY.action = action; + MTY.action = action; - // In case click handler doesn't happen - mty_run_action(); + // In case click handler doesn't happen + mty_run_action(); } + // Window function mty_is_visible() { - if (document.hidden != undefined) { - return !document.hidden; - } else if (document.webkitHidden != undefined) { - return !document.webkitHidden; - } + if (document.hidden != undefined) { + return !document.hidden; + + } else if (document.webkitHidden != undefined) { + return !document.webkitHidden; + } - return true; + return true; } function mty_window_info() { - const rect = MTY.canvas.getBoundingClientRect(); - - return { - posX: window.screenX, - posY: window.screenY, - relative: MTY.relative, - devicePixelRatio: window.devicePixelRatio, - hasFocus: document.hasFocus(), - screenWidth: screen.width, - screenHeight: screen.height, - fullscreen: document.fullscreenElement != null, - visible: mty_is_visible(), - canvasWidth: mty_scaled(rect.width), - canvasHeight: mty_scaled(rect.height), - }; + const rect = MTY.canvas.getBoundingClientRect(); + + return { + posX: window.screenX, + posY: window.screenY, + relative: MTY.relative, + devicePixelRatio: window.devicePixelRatio, + hasFocus: document.hasFocus(), + screenWidth: screen.width, + screenHeight: screen.height, + fullscreen: document.fullscreenElement != null, + visible: mty_is_visible(), + canvasWidth: mty_scaled(rect.width), + canvasHeight: mty_scaled(rect.height), + }; } function mty_update_canvas(canvas) { - const rect = canvas.getBoundingClientRect(); - canvas.width = rect.width; - canvas.height = rect.height; + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; - return rect; + return rect; } function mty_set_fullscreen(fullscreen) { - if (fullscreen && !document.fullscreenElement) { - if (navigator.keyboard) navigator.keyboard.lock(['Escape']); + if (fullscreen && !document.fullscreenElement) { + if (navigator.keyboard) + navigator.keyboard.lock(["Escape"]); - document.documentElement.requestFullscreen(); - } else if (!fullscreen && document.fullscreenElement) { - document.exitFullscreen(); + document.documentElement.requestFullscreen(); - if (navigator.keyboard) navigator.keyboard.unlock(); - } + } else if (!fullscreen && document.fullscreenElement) { + document.exitFullscreen(); + + if (navigator.keyboard) + navigator.keyboard.unlock(); + } } async function mty_wake_lock(enable) { - try { - if (enable && !MTY.wakeLock) { - MTY.wakeLock = await navigator.wakeLock.request('screen'); - } else if (!enable && MTY.wakeLock) { - MTY.wakeLock.release(); - delete MTY.wakeLock; - } - } catch (e) { - delete MTY.wakeLock; - } + try { + if (enable && !MTY.wakeLock) { + MTY.wakeLock = await navigator.wakeLock.request('screen'); + + } else if (!enable && MTY.wakeLock) { + MTY.wakeLock.release(); + delete MTY.wakeLock; + } + } catch (e) { + delete MTY.wakeLock; + } } + // Cursor function mty_show_cursor(show) { - MTY.canvas.style.cursor = show ? '' : 'none'; + MTY.canvas.style.cursor = show ? '': 'none'; } function mty_use_default_cursor(use_default) { - if (MTY.cursorClass.length > 0) { - if (use_default) { - MTY.canvas.classList.remove(MTY.cursorClass); - } else { - MTY.canvas.classList.add(MTY.cursorClass); - } - } - - MTY.defaultCursor = use_default; + if (MTY.cursorClass.length > 0) { + if (use_default) { + MTY.canvas.classList.remove(MTY.cursorClass); + + } else { + MTY.canvas.classList.add(MTY.cursorClass); + } + } + + MTY.defaultCursor = use_default; } function mty_set_cursor(url, hot_x, hot_y) { - if (url) { - if (!MTY.cursorCache[url]) { - MTY.cursorCache[url] = `cursor-x-${MTY.cursorId}`; - - const style = document.createElement('style'); - style.type = 'text/css'; - style.innerHTML = - `.cursor-x-${MTY.cursorId++} ` + - `{cursor: url(${url}) ${hot_x} ${hot_y}, auto;}`; - document.querySelector('head').appendChild(style); - } - - if (MTY.cursorClass.length > 0) - MTY.canvas.classList.remove(MTY.cursorClass); - - MTY.cursorClass = MTY.cursorCache[url]; - - if (!MTY.defaultCursor) MTY.canvas.classList.add(MTY.cursorClass); - } else { - if (!MTY.defaultCursor && MTY.cursorClass.length > 0) - MTY.canvas.classList.remove(MTY.cursorClass); - - MTY.cursorClass = ''; - } + if (url) { + if (!MTY.cursorCache[url]) { + MTY.cursorCache[url] = `cursor-x-${MTY.cursorId}`; + + const style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = `.cursor-x-${MTY.cursorId++} ` + + `{cursor: url(${url}) ${hot_x} ${hot_y}, auto;}`; + document.querySelector('head').appendChild(style); + } + + if (MTY.cursorClass.length > 0) + MTY.canvas.classList.remove(MTY.cursorClass); + + MTY.cursorClass = MTY.cursorCache[url]; + + if (!MTY.defaultCursor) + MTY.canvas.classList.add(MTY.cursorClass); + + } else { + if (!MTY.defaultCursor && MTY.cursorClass.length > 0) + MTY.canvas.classList.remove(MTY.cursorClass); + + MTY.cursorClass = ''; + } } function mty_set_png_cursor(buf, hot_x, hot_y) { - const url = buf ? 'data:image/png;base64,' + mty_buf_to_b64(buf) : null; - mty_set_cursor(url, hot_x, hot_y); + const url = buf ? 'data:image/png;base64,' + mty_buf_to_b64(buf) : null; + mty_set_cursor(url, hot_x, hot_y); } function mty_set_rgba_cursor(buf, width, height, hot_x, hot_y) { - let url = null; + let url = null; - if (buf) { - if (!MTY.ccanvas) { - MTY.ccanvas = document.createElement('canvas'); - MTY.cctx = MTY.ccanvas.getContext('2d', { willReadFrequently: true }); - } + if (buf) { + if (!MTY.ccanvas) { + MTY.ccanvas = document.createElement('canvas'); + MTY.cctx = MTY.ccanvas.getContext('2d', {"willReadFrequently": true}); + } - MTY.ccanvas.width = width; - MTY.ccanvas.height = height; + MTY.ccanvas.width = width; + MTY.ccanvas.height = height; - const image = MTY.cctx.getImageData(0, 0, width, height); - image.data.set(buf); + const image = MTY.cctx.getImageData(0, 0, width, height); + image.data.set(buf); - MTY.cctx.putImageData(image, 0, 0); + MTY.cctx.putImageData(image, 0, 0); - url = MTY.ccanvas.toDataURL(); - } + url = MTY.ccanvas.toDataURL(); + } - mty_set_cursor(url, hot_x, hot_y); + mty_set_cursor(url, hot_x, hot_y); } + // Gamepads function mty_rumble_gamepad(id, low, high) { - const gps = navigator.getGamepads(); - const gp = gps[id]; - - if (gp && gp.vibrationActuator) - gp.vibrationActuator.playEffect('dual-rumble', { - startDelay: 0, - duration: 2000, - weakMagnitude: low, - strongMagnitude: high, - }); + const gps = navigator.getGamepads(); + const gp = gps[id]; + + if (gp && gp.vibrationActuator) + gp.vibrationActuator.playEffect('dual-rumble', { + startDelay: 0, + duration: 2000, + weakMagnitude: low, + strongMagnitude: high, + }); } function mty_poll_gamepads(thread) { - const gps = navigator.getGamepads(); - - for (let x = 0; x < 4; x++) { - const gp = gps[x]; - - if (gp) { - let state = 0; - - // Connected - if (!MTY.gps[x]) { - MTY.gps[x] = true; - state = 1; - } - - let lx = 0; - let ly = 0; - let rx = 0; - let ry = 0; - let lt = 0; - let rt = 0; - let buttons = 0; - - if (gp.buttons) { - if (gp.buttons[6]) lt = gp.buttons[6].value; - if (gp.buttons[7]) rt = gp.buttons[7].value; - - for (let i = 0; i < gp.buttons.length && i < 32; i++) - if (gp.buttons[i].pressed) buttons |= 1 << i; - } - - if (gp.axes) { - if (gp.axes[0]) lx = gp.axes[0]; - if (gp.axes[1]) ly = gp.axes[1]; - if (gp.axes[2]) rx = gp.axes[2]; - if (gp.axes[3]) ry = gp.axes[3]; - } - - thread.postMessage({ - type: 'controller', - id: x, - state: state, - buttons: buttons, - lx: lx, - ly: ly, - rx: rx, - ry: ry, - lt: lt, - rt: rt, - }); - - // Disconnected - } else if (MTY.gps[x]) { - thread.postMessage({ - type: 'controller-disconnect', - id: x, - state: 2, - }); - - MTY.gps[x] = false; - } - } + const gps = navigator.getGamepads(); + + for (let x = 0; x < 4; x++) { + const gp = gps[x]; + + if (gp) { + let state = 0; + + // Connected + if (!MTY.gps[x]) { + MTY.gps[x] = true; + state = 1; + } + + let lx = 0; + let ly = 0; + let rx = 0; + let ry = 0; + let lt = 0; + let rt = 0; + let buttons = 0; + + if (gp.buttons) { + if (gp.buttons[6]) lt = gp.buttons[6].value; + if (gp.buttons[7]) rt = gp.buttons[7].value; + + for (let i = 0; i < gp.buttons.length && i < 32; i++) + if (gp.buttons[i].pressed) + buttons |= 1 << i; + } + + if (gp.axes) { + if (gp.axes[0]) lx = gp.axes[0]; + if (gp.axes[1]) ly = gp.axes[1]; + if (gp.axes[2]) rx = gp.axes[2]; + if (gp.axes[3]) ry = gp.axes[3]; + } + + thread.postMessage({ + type: 'controller', + id: x, + state: state, + buttons: buttons, + lx: lx, + ly: ly, + rx: rx, + ry: ry, + lt: lt, + rt: rt, + }); + + // Disconnected + } else if (MTY.gps[x]) { + thread.postMessage({ + type: 'controller-disconnect', + id: x, + state: 2, + }); + + MTY.gps[x] = false; + } + } } + // Audio -async function mty_audio_queue( - ctx, - sampleRate, - minBuffer, - maxBuffer, - channels, -) { - // Initialize on first queue otherwise the browser may complain about user interaction - if (!MTY.audioCtx) { - MTY.audioCtx = new AudioContext({ sampleRate: sampleRate }); - - const baseFile = MTY_CURRENT_SCRIPT.pathname; - await MTY.audioCtx.audioWorklet.addModule( - baseFile.replace('.js', '-worker.js'), - ); - - const node = new AudioWorkletNode(MTY.audioCtx, 'MTY_Audio', { - outputChannelCount: [channels], - processorOptions: { - minBuffer, - maxBuffer, - }, - }); - - node.connect(MTY.audioCtx.destination); - node.port.postMessage(MTY.audioObjs); - } +async function mty_audio_queue(ctx, sampleRate, minBuffer, maxBuffer, channels) { + // Initialize on first queue otherwise the browser may complain about user interaction + if (!MTY.audioCtx) { + MTY.audioCtx = new AudioContext({sampleRate: sampleRate}); + + const baseFile = MTY_CURRENT_SCRIPT.pathname; + await MTY.audioCtx.audioWorklet.addModule(baseFile.replace('.js', '-worker.js')); + + const node = new AudioWorkletNode(MTY.audioCtx, 'MTY_Audio', { + outputChannelCount: [channels], + processorOptions: { + minBuffer, + maxBuffer, + }, + }); + + node.connect(MTY.audioCtx.destination); + node.port.postMessage(MTY.audioObjs); + } } + // Image async function mty_decode_image(input) { - const img = new Image(); - img.src = URL.createObjectURL(new Blob([input])); + const img = new Image(); + img.src = URL.createObjectURL(new Blob([input])); - await img.decode(); + await img.decode(); - const width = img.naturalWidth; - const height = img.naturalHeight; + const width = img.naturalWidth; + const height = img.naturalHeight; - const canvas = new OffscreenCanvas(width, height); - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, width, height); + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); - return ctx.getImageData(0, 0, width, height); + return ctx.getImageData(0, 0, width, height); } + // Net function mty_ws_new(obj) { - MTY.wsObj[MTY.wsIndex] = obj; + MTY.wsObj[MTY.wsIndex] = obj; - return MTY.wsIndex++; + return MTY.wsIndex++; } function mty_ws_del(index) { - let obj = MTY.wsObj[index]; + let obj = MTY.wsObj[index]; - delete MTY.wsObj[index]; + delete MTY.wsObj[index]; - return obj; + return obj; } function mty_ws_obj(index) { - return MTY.wsObj[index]; + return MTY.wsObj[index]; } async function mty_http_request(url, method, headers, body, buf) { - let error = false; - let size = 0; - let status = 0; - let data = null; - - try { - const response = await fetch(url, { - method: method, - headers: headers, - body: body, - }); - - const res_ab = await response.arrayBuffer(); - data = new Uint8Array(res_ab); - - status = response.status; - size = data.byteLength; - } catch (err) { - console.error(err); - error = true; - } - - return { - data, - error, - size, - status, - }; + let error = false + let size = 0; + let status = 0; + let data = null; + + try { + const response = await fetch(url, { + method: method, + headers: headers, + body: body, + }); + + const res_ab = await response.arrayBuffer(); + data = new Uint8Array(res_ab); + + status = response.status; + size = data.byteLength; + + } catch (err) { + console.error(err); + error = true; + } + + return { + data, + error, + size, + status, + }; } async function mty_ws_connect(url) { - return new Promise((resolve, reject) => { - const ws = new WebSocket(url); - const sab = new SharedArrayBuffer(4); - ws.sync = new Int32Array(sab, 0, 1); - ws.closeCode = 0; - ws.msgs = []; - - let tm = 0; // JS clients can't do WebSocket ping/pong, so we do our own pseudo keepalive to prevent dropped connections - ws.onclose = (ev) => { - ws.closeCode = ev.code == 1005 ? 1000 : ev.code; - clearInterval(tm); - resolve(null); - }; - - ws.onerror = (err) => { - console.error(err); - clearInterval(tm); - resolve(null); - }; - - ws.onopen = () => { - resolve(ws); - tm = setInterval(() => { - ws.send('__ping__'); - }, 60000); // we've seen timeouts at the 10-minute mark, - // so a 60-second period should be more than enough - }; - - ws.onmessage = (ev) => { - ws.msgs.push(ev.data); - Atomics.notify(ws.sync, 0, 1); - }; - }); + return new Promise((resolve, reject) => { + const ws = new WebSocket(url); + const sab = new SharedArrayBuffer(4); + ws.sync = new Int32Array(sab, 0, 1); + ws.closeCode = 0; + ws.msgs = []; + + let tm = 0; // JS clients can't do WebSocket ping/pong, so we do our own pseudo keepalive to prevent dropped connections + ws.onclose = (ev) => { + ws.closeCode = ev.code == 1005 ? 1000 : ev.code; + clearInterval(tm); + resolve(null); + }; + + ws.onerror = (err) => { + console.error(err); + clearInterval(tm); + resolve(null); + }; + + ws.onopen = () => { + resolve(ws); + tm = setInterval(() => {ws.send('__ping__');}, 60000); // we've seen timeouts at the 10-minute mark, + // so a 60-second period should be more than enough + }; + + ws.onmessage = (ev) => { + ws.msgs.push(ev.data); + Atomics.notify(ws.sync, 0, 1); + }; + }); } async function mty_ws_read(ws, timeout) { - let msg = ws.msgs.shift(); + let msg = ws.msgs.shift() - if (!msg) { - const r0 = Atomics.waitAsync(ws.sync, 0, 0, timeout); - const r1 = await r0.value; + if (!msg) { + const r0 = Atomics.waitAsync(ws.sync, 0, 0, timeout); + const r1 = await r0.value; - if (r1 != 'timed-out') msg = ws.msgs.shift(); - } + if (r1 != 'timed-out') + msg = ws.msgs.shift() + } - return msg ? mty_encode(msg) : null; + return msg ? mty_encode(msg) : null; } + // Entry function mty_supports_web_gl() { - try { - return document.createElement('canvas').getContext('webgl2'); - } catch (e) {} + try { + return document.createElement('canvas').getContext('webgl2'); + } catch (e) {} - return false; + return false; } function mty_update_interval(thread) { - // Poll gamepads - if (document.hasFocus()) mty_poll_gamepads(thread); - - // Poll position changes - if (MTY.posX != window.screenX || MTY.posY != window.screenY) { - MTY.posX = window.screenX; - MTY.posY = window.screenY; - - thread.postMessage({ - type: 'move', - }); - } - - // send rect event - thread.postMessage({ - type: 'window-update', - windowInfo: mty_window_info(), - }); + // Poll gamepads + if (document.hasFocus()) + mty_poll_gamepads(thread); + + // Poll position changes + if (MTY.posX != window.screenX || MTY.posY != window.screenY) { + MTY.posX = window.screenX; + MTY.posY = window.screenY; + + thread.postMessage({ + type: 'move', + }); + } + + // send rect event + thread.postMessage({ + type: 'window-update', + windowInfo: mty_window_info(), + }); } -function mty_thread_start( - threadId, - bin, - wasmBuf, - memory, - startArg, - userEnv, - kbMap, - psync, - audioObjs, - name, -) { - const baseFile = MTY_CURRENT_SCRIPT.pathname; - const worker = new Worker(baseFile.replace('.js', '-worker.js'), { - name: name, - }); - - worker.onmessage = mty_thread_message; - - worker.postMessage({ - type: 'init', - file: baseFile, - bin: bin, - wasmBuf: wasmBuf, - psync: psync, - windowInfo: mty_window_info(), - args: window.location.search, - hostname: window.location.hostname, - userEnv: userEnv ? Object.keys(userEnv) : [], - kbMap: kbMap, - startArg: startArg, - threadId: threadId, - memory: memory, - audioObjs, - }); - - return worker; +function mty_thread_start(threadId, bin, wasmBuf, memory, startArg, userEnv, kbMap, psync, audioObjs, name) { + const baseFile = MTY_CURRENT_SCRIPT.pathname; + const worker = new Worker(baseFile.replace('.js', '-worker.js'), {name: name}); + + worker.onmessage = mty_thread_message; + + worker.postMessage({ + type: 'init', + file: baseFile, + bin: bin, + wasmBuf: wasmBuf, + psync: psync, + windowInfo: mty_window_info(), + args: window.location.search, + hostname: window.location.hostname, + userEnv: userEnv ? Object.keys(userEnv) : [], + kbMap: kbMap, + startArg: startArg, + threadId: threadId, + memory: memory, + audioObjs, + }); + + return worker; } async function MTY_Start(bin, container, userEnv) { - if (!mty_supports_web_gl()) return false; - - MTY.bin = bin; - MTY.userEnv = userEnv; - MTY.psync = new Int32Array(new SharedArrayBuffer(4)); - MTY.audioObjs = { - buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)), - control: new Int32Array(new SharedArrayBuffer(32)), - }; - - // Drawing surface - MTY.canvas = document.createElement('canvas'); - MTY.renderer = MTY.canvas.getContext('bitmaprenderer'); - MTY.canvas.style.width = '100%'; - MTY.canvas.style.height = '100%'; - container.appendChild(MTY.canvas); - mty_update_canvas(MTY.canvas); - - // WASM binary - const wasmRes = await fetch(bin); - MTY.wasmBuf = await wasmRes.arrayBuffer(); - - // Shared global memory - MTY_MEMORY = new WebAssembly.Memory({ - initial: 512, // 32 MB - maximum: 16384, // 1 GB - shared: true, - }); - - // Load keyboard map - MTY.kbMap = {}; - if (navigator.keyboard) { - const layout = await navigator.keyboard.getLayoutMap(); - - layout.forEach((currentValue, index) => { - MTY.kbMap[index] = currentValue; - }); - } - - // Main thread - MTY.mainThread = mty_thread_start( - MTY.threadId, - bin, - MTY.wasmBuf, - MTY_MEMORY, - 0, - userEnv, - MTY.kbMap, - MTY.psync, - MTY.audioObjs, - 'main', - ); - - // Init position, update loop - MTY.posX = window.screenX; - MTY.posY = window.screenY; - setInterval(() => { - mty_update_interval(MTY.mainThread); - }, 10); - - // Vsync - const vsync = () => { - mty_signal(MTY.psync, true); - requestAnimationFrame(vsync); - }; - requestAnimationFrame(vsync); - - // Add input events - mty_add_input_events(MTY.mainThread); - - return true; + if (!mty_supports_web_gl()) + return false; + + MTY.bin = bin; + MTY.userEnv = userEnv; + MTY.psync = new Int32Array(new SharedArrayBuffer(4)); + MTY.audioObjs = { + buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)), + control: new Int32Array(new SharedArrayBuffer(32)), + }; + + // Drawing surface + MTY.canvas = document.createElement('canvas'); + MTY.renderer = MTY.canvas.getContext('bitmaprenderer'); + MTY.canvas.style.width = '100%'; + MTY.canvas.style.height = '100%'; + container.appendChild(MTY.canvas); + mty_update_canvas(MTY.canvas); + + // WASM binary + const wasmRes = await fetch(bin); + MTY.wasmBuf = await wasmRes.arrayBuffer(); + + // Shared global memory + MTY_MEMORY = new WebAssembly.Memory({ + initial: 512, // 32 MB + maximum: 16384, // 1 GB + shared: true, + }); + + // Load keyboard map + MTY.kbMap = {}; + if (navigator.keyboard) { + const layout = await navigator.keyboard.getLayoutMap(); + + layout.forEach((currentValue, index) => { + MTY.kbMap[index] = currentValue; + }); + } + + // Main thread + MTY.mainThread = mty_thread_start(MTY.threadId, bin, MTY.wasmBuf, MTY_MEMORY, + 0, userEnv, MTY.kbMap, MTY.psync, MTY.audioObjs, 'main'); + + // Init position, update loop + MTY.posX = window.screenX; + MTY.posY = window.screenY; + setInterval(() => { + mty_update_interval(MTY.mainThread); + }, 10); + + // Vsync + const vsync = () => { + mty_signal(MTY.psync, true); + requestAnimationFrame(vsync); + }; + requestAnimationFrame(vsync); + + // Add input events + mty_add_input_events(MTY.mainThread); + + return true; } async function mty_thread_message(ev) { - const msg = ev.data; - - switch (msg.type) { - case 'user-env': - msg.sab[0] = MTY.userEnv[msg.name](...msg.args); - mty_signal(msg.sync); - break; - case 'thread': { - MTY.threadId++; - - const worker = mty_thread_start( - MTY.threadId, - MTY.bin, - MTY.wasmBuf, - MTY_MEMORY, - msg.startArg, - MTY.userEnv, - MTY.kbMap, - MTY.psync, - MTY.audioObjs, - 'thread-' + MTY.threadId, - ); - - msg.sab[0] = MTY.threadId; - mty_signal(msg.sync); - break; - } - case 'present': - MTY.renderer.transferFromImageBitmap(msg.image); - break; - case 'decode-image': { - const image = await mty_decode_image(msg.input); - - this.tmp = image.data; - msg.sab[0] = image.width; - msg.sab[1] = image.height; - - mty_signal(msg.sync); - break; - } - case 'kb-grab': - MTY.kb_grab = msg.grab; - break; - case 'title': - document.title = msg.title; - break; - case 'get-ls': { - const val = window.localStorage[msg.key]; - - if (val) { - this.tmp = mty_b64_to_buf(val); - msg.sab[0] = this.tmp.byteLength; - } else { - msg.sab[0] = 0; - } - - mty_signal(msg.sync); - break; - } - case 'set-ls': - window.localStorage[msg.key] = mty_buf_to_b64(msg.val); - mty_signal(msg.sync); - break; - case 'remove-ls': - window.localStorage.removeItem(msg.key); - mty_signal(msg.sync); - break; - case 'alert': - mty_alert(msg.title, msg.msg); - break; - case 'fullscreen': - mty_set_fullscreen(msg.fullscreen); - break; - case 'wake-lock': - mty_wake_lock(msg.enable); - break; - case 'rumble': - mty_rumble_gamepad(msg.id, msg.low, msg.high); - break; - case 'show-cursor': - mty_show_cursor(msg.show); - break; - case 'get-clip': - // FIXME Unsupported on Firefox - if (navigator.clipboard.readText) { - const text = await navigator.clipboard.readText(); - - this.tmp = mty_encode(text); - msg.sab[0] = this.tmp.byteLength; - } else { - msg.sab[0] = 0; - } - - mty_signal(msg.sync); - break; - case 'set-clip': - navigator.clipboard.writeText(mty_str_to_js(msg.text)); - break; - case 'pointer-lock': - mty_set_pointer_lock(msg.enable); - break; - case 'cursor-default': - mty_use_default_cursor(msg.use_default); - break; - case 'cursor-rgba': - mty_set_rgba_cursor(msg.buf, msg.width, msg.height, msg.hot_x, msg.hot_y); - break; - case 'cursor-png': - mty_set_png_cursor(msg.buf, msg.hot_x, msg.hot_y); - break; - case 'uri': - mty_set_action(() => { - window.open(msg.uri, '_blank'); - }); - break; - case 'http': { - const res = await mty_http_request( - msg.url, - msg.method, - msg.headers, - msg.body, - ); - - this.tmp = res.data; - msg.sab[0] = res.error ? 1 : 0; - msg.sab[1] = res.size; - msg.sab[2] = res.status; - - mty_signal(msg.sync); - break; - } - case 'ws-connect': { - const ws = await mty_ws_connect(msg.url); - msg.sab[0] = ws ? mty_ws_new(ws) : 0; - mty_signal(msg.sync); - break; - } - case 'ws-read': { - msg.sab[0] = 3; // MTY_ASYNC_ERROR - - const ws = mty_ws_obj(msg.ctx); - - if (ws) { - if (ws.closeCode != 0) { - msg.sab[0] = 1; // MTY_ASYNC_DONE - } else { - const buf = await mty_ws_read(ws, msg.timeout); - - if (buf) { - this.tmp = buf; - msg.sab[0] = 0; // MTY_ASYNC_OK - msg.sab[1] = buf.length; - } else { - msg.sab[0] = 2; // MTY_ASYNC_CONTINUE - } - } - } - - mty_signal(msg.sync); - break; - } - case 'ws-write': { - const ws = mty_ws_obj(msg.ctx); - if (ws) ws.send(msg.text); - break; - } - case 'ws-close': { - const ws = mty_ws_obj(msg.ctx); - if (ws) { - ws.close(); - mty_ws_del(msg.ctx); - } - break; - } - case 'ws-code': { - msg.sab[0] = 0; - - const ws = mty_ws_obj(msg.ctx); - if (ws) msg.sab[0] = ws.closeCode; - - mty_signal(msg.sync); - break; - } - case 'audio-queue': - mty_audio_queue( - MTY.audio, - msg.sampleRate, - msg.minBuffer, - msg.maxBuffer, - msg.channels, - ); - break; - case 'audio-destroy': - if (MTY.audioCtx) MTY.audioCtx.close(); - delete MTY.audioCtx; - break; - case 'async-copy': - msg.sab8.set(this.tmp); - delete this.tmp; - - mty_signal(msg.sync); - break; - case 'wv-create': - MTY.webview = document.createElement('iframe'); - MTY.webview.title = 'Parsec application'; - - MTY.webview.style.visibility = 'hidden'; - MTY.webview.style.position = 'fixed'; - MTY.webview.style.border = 'none'; - MTY.webview.style.width = '100%'; - MTY.webview.style.height = '100%'; - MTY.webview.style.inset = '0'; - - window.addEventListener('message', function (message) { - MTY.mainThread.postMessage({ - type: 'wv-event', - ctx: msg.ctx, - message: message.data, - }); - }); - - document.body.appendChild(MTY.webview); - break; - case 'wv-destroy': - document.body.removeChild(MTY.webview); - delete MTY.webview; - break; - case 'wv-navigate': - if (msg.url) { - MTY.webview.src = msg.source; - - MTY.webview.onload = () => { - try { - const script = - MTY.webview.contentWindow.document.createElement('script'); - script.textContent = - "window.parent.postMessage('R');window.MTY_NativeSendText = (text) => { window.parent.postMessage('T' + text); }"; - MTY.webview.contentWindow.document.head.appendChild(script); - const userAgentScript = - MTY.webview.contentWindow.document.createElement('script'); - userAgentScript.textContent = - "Object.defineProperty(window, 'MTY_GetPlatform', { value: () => 'web'});"; - MTY.webview.contentWindow.document.head.appendChild( - userAgentScript, - ); - setTimeout(() => (MTY.webview.style.visibility = 'visible'), 250); - } catch (e) { - console.error( - 'Failed to inject script into iframe (cross-origin restriction):', - e, - ); - setTimeout(() => (MTY.webview.style.visibility = 'visible'), 250); - } - }; - } else { - const blob = new Blob([msg.source], { type: 'text/html' }); - MTY.webview.src = URL.createObjectURL(blob); - } - break; - case 'wv-show': - MTY.webview.style.visibility = msg.show ? 'visible' : 'hidden'; - break; - case 'wv-is-visible': - msg.sab[0] = MTY.webview.style.visibility != 'visible'; - mty_signal(msg.sync); - break; - case 'wv-send-text': - // wv-send-text sends native app messages back to the running UI - MTY.webview.contentWindow.MTY_NativeListener(msg.message); - break; - case 'wv-reload': - MTY.webview.contentWindow.location.reload(); - break; - } -} + const msg = ev.data; + + switch (msg.type) { + case 'user-env': + msg.sab[0] = MTY.userEnv[msg.name](...msg.args); + mty_signal(msg.sync); + break; + case 'thread': { + MTY.threadId++; + + const worker = mty_thread_start(MTY.threadId, MTY.bin, MTY.wasmBuf, MTY_MEMORY, + msg.startArg, MTY.userEnv, MTY.kbMap, MTY.psync, MTY.audioObjs, 'thread-' + MTY.threadId); + + msg.sab[0] = MTY.threadId; + mty_signal(msg.sync); + break; + } + case 'present': + MTY.renderer.transferFromImageBitmap(msg.image); + break; + case 'decode-image': { + const image = await mty_decode_image(msg.input); + + this.tmp = image.data; + msg.sab[0] = image.width; + msg.sab[1] = image.height; + + mty_signal(msg.sync); + break; + } + case 'kb-grab': + MTY.kb_grab = msg.grab; + break; + case 'title': + document.title = msg.title; + break; + case 'get-ls': { + const val = window.localStorage[msg.key]; + + if (val) { + this.tmp = mty_b64_to_buf(val); + msg.sab[0] = this.tmp.byteLength; + + } else { + msg.sab[0] = 0; + } + + mty_signal(msg.sync); + break; + } + case 'set-ls': + window.localStorage[msg.key] = mty_buf_to_b64(msg.val); + mty_signal(msg.sync); + break; + case 'remove-ls': + window.localStorage.removeItem(msg.key); + mty_signal(msg.sync); + break; + case 'alert': + mty_alert(msg.title, msg.msg); + break; + case 'fullscreen': + mty_set_fullscreen(msg.fullscreen); + break; + case 'wake-lock': + mty_wake_lock(msg.enable); + break; + case 'rumble': + mty_rumble_gamepad(msg.id, msg.low, msg.high); + break; + case 'show-cursor': + mty_show_cursor(msg.show); + break; + case 'get-clip': + // FIXME Unsupported on Firefox + if (navigator.clipboard.readText) { + const text = await navigator.clipboard.readText(); + + this.tmp = mty_encode(text); + msg.sab[0] = this.tmp.byteLength; + + } else { + msg.sab[0] = 0; + } + + mty_signal(msg.sync); + break; + case 'set-clip': + navigator.clipboard.writeText(mty_str_to_js(msg.text)); + break; + case 'pointer-lock': + mty_set_pointer_lock(msg.enable); + break; + case 'cursor-default': + mty_use_default_cursor(msg.use_default); + break; + case 'cursor-rgba': + mty_set_rgba_cursor(msg.buf, msg.width, msg.height, msg.hot_x, msg.hot_y); + break; + case 'cursor-png': + mty_set_png_cursor(msg.buf, msg.hot_x, msg.hot_y); + break; + case 'uri': + mty_set_action(() => { + window.open(msg.uri, '_blank'); + }); + break; + case 'http': { + const res = await mty_http_request(msg.url, msg.method, msg.headers, msg.body); + + this.tmp = res.data; + msg.sab[0] = res.error ? 1 : 0; + msg.sab[1] = res.size; + msg.sab[2] = res.status; + + mty_signal(msg.sync); + break; + } + case 'ws-connect': { + const ws = await mty_ws_connect(msg.url); + msg.sab[0] = ws ? mty_ws_new(ws) : 0; + mty_signal(msg.sync); + break; + } + case 'ws-read': { + msg.sab[0] = 3; // MTY_ASYNC_ERROR + + const ws = mty_ws_obj(msg.ctx); + + if (ws) { + if (ws.closeCode != 0) { + msg.sab[0] = 1; // MTY_ASYNC_DONE + + } else { + const buf = await mty_ws_read(ws, msg.timeout); + + if (buf) { + this.tmp = buf; + msg.sab[0] = 0; // MTY_ASYNC_OK + msg.sab[1] = buf.length; + + } else { + msg.sab[0] = 2; // MTY_ASYNC_CONTINUE + } + } + } + + mty_signal(msg.sync); + break; + } + case 'ws-write': { + const ws = mty_ws_obj(msg.ctx); + if (ws) + ws.send(msg.text) + break; + } + case 'ws-close': { + const ws = mty_ws_obj(msg.ctx); + if (ws) { + ws.close(); + mty_ws_del(msg.ctx); + } + break; + } + case 'ws-code': { + msg.sab[0] = 0; + + const ws = mty_ws_obj(msg.ctx); + if (ws) + msg.sab[0] = ws.closeCode; + + mty_signal(msg.sync); + break; + } + case 'audio-queue': + mty_audio_queue(MTY.audio, msg.sampleRate, msg.minBuffer, + msg.maxBuffer, msg.channels); + break; + case 'audio-destroy': + if (MTY.audioCtx) + MTY.audioCtx.close(); + delete MTY.audioCtx; + break; + case 'async-copy': + msg.sab8.set(this.tmp); + delete this.tmp; + + mty_signal(msg.sync); + break; + case 'wv-create': + MTY.webview = document.createElement('iframe'); + + MTY.webview.style.visibility = 'hidden'; + MTY.webview.style.position = 'fixed'; + MTY.webview.style.border = 'none'; + MTY.webview.style.width = '100%'; + MTY.webview.style.height = '100%'; + MTY.webview.style.inset = '0'; + + window.addEventListener('message', function (message) { + MTY.mainThread.postMessage({type: 'wv-event', ctx: msg.ctx, message: message.data}); + }); + + document.body.appendChild(MTY.webview); + break; + case 'wv-destroy': + document.body.removeChild(MTY.webview); + delete MTY.webview; + break; + case 'wv-navigate': + if (msg.url) { + MTY.webview.src = msg.source; + + MTY.webview.onload = () => { + try { + const script = MTY.webview.contentWindow.document.createElement('script'); + script.textContent = "window.parent.postMessage('R');window.MTY_NativeSendText = (text) => { window.parent.postMessage('T' + text); }"; + MTY.webview.contentWindow.document.head.appendChild(script); + const userAgentScript = MTY.webview.contentWindow.document.createElement('script'); + userAgentScript.textContent = "Object.defineProperty(window, 'MTY_GetPlatform', { value: () => 'web'});"; + MTY.webview.contentWindow.document.head.appendChild(userAgentScript); + setTimeout(() => MTY.webview.style.visibility = 'visible', 250); + } catch (e) { + console.error('Failed to inject script into iframe (cross-origin restriction):', e); + setTimeout(() => MTY.webview.style.visibility = 'visible', 250); + } + }; + } else { + const blob = new Blob([msg.source], { type: 'text/html' }); + MTY.webview.src = URL.createObjectURL(blob); + } + break; + case 'wv-show': + MTY.webview.style.visibility = msg.show ? 'visible' : 'hidden'; + break; + case 'wv-is-visible': + msg.sab[0] = MTY.webview.style.visibility != 'visible'; + mty_signal(msg.sync); + break; + case 'wv-send-text': + // wv-send-text sends native app messages back to the running UI + MTY.webview.contentWindow.MTY_NativeListener(msg.message); + break; + case 'wv-reload': + MTY.webview.contentWindow.location.reload(); + break; + } +} \ No newline at end of file From 049a8cdba7b75a457695044291ebd8ff412c543c Mon Sep 17 00:00:00 2001 From: margaux Date: Tue, 2 Jun 2026 16:03:40 +0200 Subject: [PATCH 29/30] Add title on iframe --- src/unix/web/matoya.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index 22c38739..a80a3cb2 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -1081,6 +1081,7 @@ async function mty_thread_message(ev) { break; case 'wv-create': MTY.webview = document.createElement('iframe'); + MTY.webview.title = 'Parsec application'; MTY.webview.style.visibility = 'hidden'; MTY.webview.style.position = 'fixed'; From f05a071d9a956f1bc87d01d7754226ba19a87064 Mon Sep 17 00:00:00 2001 From: margaux Date: Thu, 4 Jun 2026 16:16:01 +0200 Subject: [PATCH 30/30] add attributes to canvas --- src/unix/web/matoya.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/unix/web/matoya.js b/src/unix/web/matoya.js index a80a3cb2..3f9efc2b 100644 --- a/src/unix/web/matoya.js +++ b/src/unix/web/matoya.js @@ -841,6 +841,9 @@ async function MTY_Start(bin, container, userEnv) { MTY.renderer = MTY.canvas.getContext('bitmaprenderer'); MTY.canvas.style.width = '100%'; MTY.canvas.style.height = '100%'; + MTY.canvas.setAttribute('role', 'img'); + MTY.canvas.setAttribute('aria-label', 'Parsec overlay and mouse cursor'); + container.appendChild(MTY.canvas); mty_update_canvas(MTY.canvas);