Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try to support GUI v2 on MFD devices #467

Open
mman opened this issue Jan 21, 2025 · 8 comments
Open

Try to support GUI v2 on MFD devices #467

mman opened this issue Jan 21, 2025 · 8 comments
Assignees
Labels
enhancement New feature or request

Comments

@mman
Copy link
Collaborator

mman commented Jan 21, 2025

With the implementation of #389 - whenever GUI v2 is set as default, MFD is no longer able to access Venus Remote Console directly due to limited support of WebAssembly in older web browsers shipping with MFD displays from various manufacturers.

Identify devices that can support GUI v2 properly and selectively allow access to GUI v2 there, especially newer devices with latest software update would work just fine.

@mman mman added the enhancement New feature or request label Jan 21, 2025
@mman mman self-assigned this Jan 21, 2025
@mman
Copy link
Collaborator Author

mman commented Jan 21, 2025

Here is the original investigation done by @ReinvdZee: victronenergy/gui-v2#909 (comment). I will be adding my own later.

@mman
Copy link
Collaborator Author

mman commented Jan 27, 2025

Investigating Qt WASM build process and build artifacts

I have examined QT WASM build process in order to better understand what are the possible failure modes of Qt based WASM app being used on a limited device like MFD.

Qt itself declares requirement of WebAssembly and WebGL (https://doc.qt.io/qt-6/wasm.html#supported-browsers). These can be tested for relatively easily.

When emscripten (a compiler used to transform C++ into WASM) compiles our app, it actually produces couple important artifacts.

  1. index.html which contains of combination of safe html and some modern js code to refer to other files like qtloader.js, gui-v2.js, and gui-v2.wasm.
  2. qtloader.js is relatively simple, yet modern JS code (uses async/await) to load .wasm file.
  3. gui-v2.js is emscripten runtime, essentially couple of important JS methods that must be supported by every browser. This code is again modern and uses at least async/await.
  4. gui-v2.wasm the web assembly binary.

The index.html and qtloader.js can be easily rewritten to support older browsers as long as WebGL and WebAssembly itself is supported. An effort to make qtloader.js more portable seems to be tracked here: https://bugreports.qt.io/browse/QTBUG-74295.

The gui-v2.js (emscripten runtime) can not easily be modified, emcc itself does not appear to be supporting many options to produce compatible JS. Can possibly be transpiled to backwards compatible JS using babel once we know that WebAssembly and WebGL are supported.

Nothing special (yet) to say about gui-v2.wasm and its portability across browsers.

Debugging

Since debugging on MFDs is nearly impossible, the only useful technique so far that proves to be working is to use index.html to pain an overlay and carefully redirect all console.log and window.onerror statements there. This while slowly adding method by method and seeing where everything breaks. Unfortunately any JS error typically breaks the MFD browser engine and stops even the logging so I have to back off one step and try again.

One issue on MFDs seems to be .js script file size limit. Hello World WASM app can produce a .js runtime file that is around ~50k, GUI-v2 currently ships with runtime file of ~300k. The GUI-v2 WASM file is 13M gzipped, again this can be above (unknown) MFD browser limit.

Here is the strategy:

  • Detect presence of common problematic features in the browser, check for async/await, WebGL, WebAssembly, object spread syntax, etc... all of which are used in modern JS.
  • See if qtloader.js can be loaded safely via <script> tag, and on what devices.
  • See if gui-v2.js can be loaded safely via <script> tag, and on what devices.
  • See if gui-v2.wasm can be loaded safely via qtLoad method exported via qtloader.js

Early Results

Raymarine Axiom 12 running Lighthouse 4 advertises support for all modern JS features as well as support for WebAssembly and WebGL. GUI v2 works, but renders very badly. Text is not very readable, especially compared to GUI v1.

Garmin is missing WebAssembly support.

Furuno is missing WebAssembly support, and async/await support.

Simard is missing nullish coalescing support. This may possibly be fixed by patching qtloader.js.

@mman
Copy link
Collaborator Author

mman commented Jan 27, 2025

Using the techniques described above I am starting to get some real progress: This is for example a Garmin trying to load qtloader.js and venus-gui-v2.js supporting files, before even reaching the .wasm part...

Image

@mman
Copy link
Collaborator Author

mman commented Jan 27, 2025

The qtloader.js line 96 is the following magic:

    config.qt.qtdir ??= 'qt';

It's the super new JS operator to assign variable to a new value when its existing value is null or undefined. It is not supported on SIMARD.

The venus-gui-v2.js SyntaxError is hard to decipher for now since venus-gui-v2.js is minimized in release builds and essentially contains just one long JS line.

The Plan

  • Rewrite qtloader.js to avoid using ??=, ??, and ?.
  • Compile gui-v2 without optimizations to get non-minified venus-gui-v2.js and get better error reporting there.

@mman
Copy link
Collaborator Author

mman commented Jan 28, 2025

With the patch attached below, the qtloader.js loads successfully on the SIMRAD MFD. The patch essentially gets rid of null coalescing operator and optional chaining operator.

However this still does not mean that GUI-v2 actually works on SIMRAD, more work needed.

--- qtloader.js.orig	2025-01-28 10:17:40
+++ qtloader.js	2025-01-28 10:22:39
@@ -93,8 +93,12 @@
     if (typeof config.qt.entryFunction !== 'function')
         config.qt.entryFunction = window.createQtAppInstance;
 
-    config.qt.qtdir ??= 'qt';
-    config.qt.preload ??= [];
+    if (config.qt.qtdir === null || config.qt.qtdir === undefined) {
+        config.qt.qtdir = 'qt';
+    }
+    if (config.qt.preload === null || config.qt.preload === undefined) {
+        config.qt.preload = [];
+    }
 
     config.qtContainerElements = config.qt.containerElements;
     delete config.qt.containerElements;
@@ -124,7 +128,10 @@
     const qtPreRun = (instance) => {
         // Copy qt.environment to instance.ENV
         throwIfEnvUsedButNotExported(instance, config);
-        for (const [name, value] of Object.entries(config.qt.environment ?? {}))
+        const environment = config.qt.environment !== null && config.qt.environment !== undefined 
+            ? config.qt.environment 
+            : {}
+        for (const [name, value] of Object.entries(environment))
             instance.ENV[name] = value;
 
         // Copy self.preloadData to MEMFS
@@ -156,7 +163,11 @@
         config.preRun = [];
     config.preRun.push(qtPreRun);
 
-    config.onRuntimeInitialized = () => config.qt.onLoaded?.();
+    config.onRuntimeInitialized = () => {
+        if (config.qt.onLoaded !== undefined && config.qt.onLoaded !== null) {
+            config.qt.onLoaded();
+        }
+    }
 
     const originalLocateFile = config.locateFile;
     config.locateFile = filename =>
@@ -169,23 +180,31 @@
 
     const originalOnExit = config.onExit;
     config.onExit = code => {
-        originalOnExit?.();
-        config.qt.onExit?.({
-            code,
-            crashed: false
-        });
+        if (originalOnExit !== undefined && originalOnExit !== null) {
+            originalOnExit();
+        }
+        if (config.qt.onExit !== undefined && config.qt.onExit !== null) {
+            config.qt.onExit({
+                code,
+                crashed: false
+            });
+        }
     }
 
     const originalOnAbort = config.onAbort;
     config.onAbort = text =>
     {
-        originalOnAbort?.();
+        if (originalOnAbort !== undefined && originalOnAbort !== null) {
+            originalOnAbort();
+        }
 
         aborted = true;
-        config.qt.onExit?.({
-            text,
-            crashed: true
-        });
+        if (config.qt.onExit !== undefined && config.qt.onExit !== null) {
+            config.qt.onExit({
+                text,
+                crashed: true
+            });
+        }
     };
 
     const fetchPreloadFiles = async () => {
@@ -211,66 +230,14 @@
         instance = await Promise.race(
             [circuitBreaker, config.qt.entryFunction(config)]);
     } catch (e) {
-        config.qt.onExit?.({
-            text: e.message,
-            crashed: true
-        });
-        throw e;
+        if (config.qt.onExit !== undefined && config.qt.onExit !== null) {
+            config.qt.onExit({
+                text: e.message,
+                crashed: true
+            });
+            throw e;
+        }
     }
 
     return instance;
 }
-
-// Compatibility API. This API is deprecated,
-// and will be removed in a future version of Qt.
-function QtLoader(qtConfig) {
-
-    const warning = 'Warning: The QtLoader API is deprecated and will be removed in ' +
-                    'a future version of Qt. Please port to the new qtLoad() API.';
-    console.warn(warning);
-
-    let emscriptenConfig = qtConfig.moduleConfig || {}
-    qtConfig.moduleConfig = undefined;
-    const showLoader = qtConfig.showLoader;
-    qtConfig.showLoader = undefined;
-    const showError = qtConfig.showError;
-    qtConfig.showError = undefined;
-    const showExit = qtConfig.showExit;
-    qtConfig.showExit = undefined;
-    const showCanvas = qtConfig.showCanvas;
-    qtConfig.showCanvas = undefined;
-    if (qtConfig.canvasElements) {
-        qtConfig.containerElements = qtConfig.canvasElements
-        qtConfig.canvasElements = undefined;
-    } else {
-        qtConfig.containerElements = qtConfig.containerElements;
-        qtConfig.containerElements = undefined;
-    }
-    emscriptenConfig.qt = qtConfig;
-
-    let qtloader = {
-        exitCode: undefined,
-        exitText: "",
-        loadEmscriptenModule: _name => {
-            try {
-                qtLoad(emscriptenConfig);
-            } catch (e) {
-                showError?.(e.message);
-            }
-        }
-    }
-
-    qtConfig.onLoaded = () => {
-        showCanvas?.();
-    }
-
-    qtConfig.onExit = exit => {
-        qtloader.exitCode = exit.code
-        qtloader.exitText = exit.text;
-        showExit?.();
-    }
-
-    showLoader?.("Loading");
-
-    return qtloader;
-};

@mman
Copy link
Collaborator Author

mman commented Jan 28, 2025

After patching qtloader.js, and disabling gui-v2 watchdog that causes instant page reloads on slow device, SIMRAD device fails to load .wasm with:

error: wasm funcion signature contains ilegal type

More investigation needed...

@mman
Copy link
Collaborator Author

mman commented Jan 28, 2025

Examining the venus-gui-v2.wasm with wasm2wat reveals that it is compiled with 64bit wasm in mind. More investigation needed around emscripten options to produce 32bit binary to support armv7 devices.

https://emscripten.org/docs/tools_reference/settings_reference.html#memory64

https://emscripten.org/docs/tools_reference/settings_reference.html#wasm-bigint

@mman
Copy link
Collaborator Author

mman commented Jan 28, 2025

The commit 5062fe9 introduces a robust and detailed check of the MFD to see what features we are currently missing to display GUI v2.

Future development may be to investigate whether we can actually make the GUI v2 run on wider spectrum of devices by getting rid of new JS features in qtloader.js and by emitting 32bit of wasm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant