chiark / gitweb /
Update web puzzles to current WASM-based Emscripten.
authorSimon Tatham <anakin@pobox.com>
Sat, 3 Apr 2021 07:42:04 +0000 (08:42 +0100)
committerSimon Tatham <anakin@pobox.com>
Sat, 3 Apr 2021 08:22:49 +0000 (09:22 +0100)
I presume this will improve performance. Also, if I've understood
correctly, WASM-based compiled web code is capable of automatically
growing its memory, which the previous asm.js build of the puzzles
could not do, and occasionally caused people to complain that if they
tried to play a _really big_ game in their browser, the JS would
eventually freeze because the emulated memory ran out.

I've been putting off doing this for ages because my previous
Emscripten build setup was so finicky that I didn't like to meddle
with it. But now that the new cmake system in this source tree makes
things generally easier, and particularly since I've just found out
that the up-to-date Emscripten is available as a Docker image (namely
"emscripten/emsdk"), this seemed like a good moment to give it a try.

The source and build changes required for this update weren't too
onerous. I was half expecting a huge API upheaval, and indeed there
was _some_ change, but very little:

 - in the JS initPuzzle function, move the call to Module.callMain()
   into Module.onRuntimeInitialized instead of doing it at the top
   level, because New Emscripten's .js output likes to load the
   accompanying .wasm file asynchronously, so you can't call the WASM
   main() until it actually exists.

 - in the JS-side library code, replace all uses of Emscripten's
   Pointer_stringify() function with the new name UTF8ToString(). (The
   new version also has an ASCIIToString(), so I guess the reason for
   the name change is that now you get to choose which character set
   you meant. I need to use UTF-8, so that the × and ÷ signs in Keen
   will work.)

 - set EXTRA_EXPORTED_RUNTIME_METHODS=[cwrap,callMain] on the emcc
   link command line, otherwise they aren't available for my JS setup
   code to call.

 - (removed -s ASM_JS=1 from the link options, though I'm not actually
   sure it made any difference one way or the other in the new WASM
   world)

 - be prepared for a set of .wasm files to show up as build products
   alongside the .js ones.

 - stop building with -DCMAKE_BUILD_TYPE=Release! I'm not sure why
   that was needed, but if I leave that flag on my cmake command line,
   the output .js file fails to embed my emccpre.js, so the initial
   call to initPuzzle() fails from the HTML wrapper page, meaning
   nothing at all happens.

Buildscr
cmake/platforms/emscripten.cmake
emcclib.js
emccpre.js

index 29d4e569305fa3ab4d012e850011dfa3dcaa8cb6..d9c9a96e7d063ff8c72c3185cbc9cd193dd7b3fa 100644 (file)
--- a/Buildscr
+++ b/Buildscr
@@ -131,19 +131,28 @@ endif
 # delegation.
 ifneq "$(NOJS)" yes then
   delegate emscripten
-    in puzzles do emcmake cmake -B build-emscripten -DCMAKE_BUILD_TYPE=Release $(web_unfinished_option) .
+    in puzzles do emcmake cmake -B build-emscripten $(web_unfinished_option) .
     in puzzles/build-emscripten do make -j$(nproc) VERBOSE=1
     return puzzles/build-emscripten/*.js
+    return puzzles/build-emscripten/*.wasm
     return puzzles/build-emscripten/unfinished/group.js
+    return puzzles/build-emscripten/unfinished/group.wasm
   enddelegate
 
   # Build a set of wrapping HTML pages for easy testing of the
-  # Javascript puzzles. These aren't quite the same as the versions that
-  # will go on my live website, because those ones will substitute in a
-  # different footer, and not have to link to the .js files with the
-  # ../js/ prefix. But these ones should be good enough to just open
-  # using a file:// URL in a browser after running a build, and make
-  # sure the main functionality works.
+  # Javascript puzzles.
+  #
+  # These aren't quite the same as the HTML pages that will go on my
+  # live website. The live ones will substitute in a different footer
+  # that links back to the main puzzles page, and they'll have a
+  # different filesystem layout so that ther links to the .js files
+  # won't need the ../js/ prefix used below.
+  #
+  # But these test pages should be good enough to just open after
+  # running a build, to make sure the main functionality works.
+  # Unfortunately, due to some kind of WASM loading restriction, this
+  # can't be done using a file:// URL; you have to actually point an
+  # HTTP or HTTPS server at the build output directory.
   in puzzles do mkdir jstest
   in puzzles/jstest do ../html/jspage.pl --jspath=../js/ /dev/null ../html/*.html
 endif
@@ -182,7 +191,9 @@ ifneq "$(NOJAVA)" yes then
 endif
 ifneq "$(NOJS)" yes then
   deliver puzzles/build-emscripten/*.js js/$@
+  deliver puzzles/build-emscripten/*.wasm js/$@
   deliver puzzles/build-emscripten/unfinished/*.js js/$@
+  deliver puzzles/build-emscripten/unfinished/*.wasm js/$@
   deliver puzzles/jstest/*.html jstest/$@
   deliver puzzles/html/*.html html/$@
   deliver puzzles/html/*.pl html/$@
index d9230837e1cde01e554fe508e95093db720d53c7..89223a55b97f9d7879345dc9b25cb2b5260aeb27 100644 (file)
@@ -29,7 +29,9 @@ set(emcc_export_list
 list(TRANSFORM emcc_export_list PREPEND \")
 list(TRANSFORM emcc_export_list APPEND \")
 string(JOIN "," emcc_export_string ${emcc_export_list})
-set(CMAKE_C_LINK_FLAGS "-s ASM_JS=1 -s EXPORTED_FUNCTIONS='[${emcc_export_string}]'")
+set(CMAKE_C_LINK_FLAGS "\
+-s EXPORTED_FUNCTIONS='[${emcc_export_string}]' \
+-s EXTRA_EXPORTED_RUNTIME_METHODS='[cwrap,callMain]'")
 
 set(build_cli_programs FALSE)
 
index 907dc19995ea326001b9ca307d346bfe3d421a8c..c81c576582af8499d0443626ae6acfc5e98e0965 100644 (file)
@@ -23,7 +23,7 @@ mergeInto(LibraryManager.library, {
      * Unused in production, but handy in development.
      */
     js_debug: function(ptr) {
-        console.log(Pointer_stringify(ptr));
+        console.log(UTF8ToString(ptr));
     },
 
     /*
@@ -34,7 +34,7 @@ mergeInto(LibraryManager.library, {
      * in a configuration dialog).
      */
     js_error_box: function(ptr) {
-        alert(Pointer_stringify(ptr));
+        alert(UTF8ToString(ptr));
     },
 
     /*
@@ -68,7 +68,7 @@ mergeInto(LibraryManager.library, {
      * clicked.
      */
     js_add_preset: function(menuid, ptr, value) {
-        var name = Pointer_stringify(ptr);
+        var name = UTF8ToString(ptr);
         var item = document.createElement("li");
         item.setAttribute("data-index", value);
         var tick = document.createElement("span");
@@ -96,7 +96,7 @@ mergeInto(LibraryManager.library, {
      * js_add_preset or this function.
      */
     js_add_preset_submenu: function(menuid, ptr, value) {
-        var name = Pointer_stringify(ptr);
+        var name = UTF8ToString(ptr);
         var item = document.createElement("li");
         // We still create a transparent tick element, even though it
         // won't ever be selected, to make submenu titles line up
@@ -167,13 +167,13 @@ mergeInto(LibraryManager.library, {
      * the random seed permalink.
      */
     js_update_permalinks: function(desc, seed) {
-        desc = Pointer_stringify(desc);
+        desc = UTF8ToString(desc);
         permalink_desc.href = "#" + desc;
 
         if (seed == 0) {
             permalink_seed.style.display = "none";
         } else {
-            seed = Pointer_stringify(seed);
+            seed = UTF8ToString(seed);
             permalink_seed.href = "#" + seed;
             permalink_seed.style.display = "inline";
         }
@@ -277,7 +277,7 @@ mergeInto(LibraryManager.library, {
      * Draw a rectangle.
      */
     js_canvas_draw_rect: function(x, y, w, h, colptr) {
-        ctx.fillStyle = Pointer_stringify(colptr);
+        ctx.fillStyle = UTF8ToString(colptr);
         ctx.fillRect(x, y, w, h);
     },
 
@@ -314,7 +314,7 @@ mergeInto(LibraryManager.library, {
      * Postscriptish drawing frameworks).
      */
     js_canvas_draw_line: function(x1, y1, x2, y2, width, colour) {
-        colour = Pointer_stringify(colour);
+        colour = UTF8ToString(colour);
 
         ctx.beginPath();
         ctx.moveTo(x1 + 0.5, y1 + 0.5);
@@ -345,13 +345,13 @@ mergeInto(LibraryManager.library, {
                        getValue(pointptr+8*i+4, 'i32') + 0.5);
         ctx.closePath();
         if (fill != 0) {
-            ctx.fillStyle = Pointer_stringify(fill);
+            ctx.fillStyle = UTF8ToString(fill);
             ctx.fill();
         }
         ctx.lineWidth = '1';
         ctx.lineCap = 'round';
         ctx.lineJoin = 'round';
-        ctx.strokeStyle = Pointer_stringify(outline);
+        ctx.strokeStyle = UTF8ToString(outline);
         ctx.stroke();
     },
 
@@ -366,13 +366,13 @@ mergeInto(LibraryManager.library, {
         ctx.beginPath();
         ctx.arc(x + 0.5, y + 0.5, r, 0, 2*Math.PI);
         if (fill != 0) {
-            ctx.fillStyle = Pointer_stringify(fill);
+            ctx.fillStyle = UTF8ToString(fill);
             ctx.fill();
         }
         ctx.lineWidth = '1';
         ctx.lineCap = 'round';
         ctx.lineJoin = 'round';
-        ctx.strokeStyle = Pointer_stringify(outline);
+        ctx.strokeStyle = UTF8ToString(outline);
         ctx.stroke();
     },
 
@@ -397,7 +397,7 @@ mergeInto(LibraryManager.library, {
      * per (font,height) pair.
      */
     js_canvas_find_font_midpoint: function(height, font) {
-        font = Pointer_stringify(font);
+        font = UTF8ToString(font);
 
         // Reuse cached value if possible
         if (midpoint_cache[font] !== undefined)
@@ -451,12 +451,12 @@ mergeInto(LibraryManager.library, {
      * function to do it for us with almost no extra effort.
      */
     js_canvas_draw_text: function(x, y, halign, colptr, fontptr, text) {
-        ctx.font = Pointer_stringify(fontptr);
-        ctx.fillStyle = Pointer_stringify(colptr);
+        ctx.font = UTF8ToString(fontptr);
+        ctx.fillStyle = UTF8ToString(colptr);
         ctx.textAlign = (halign == 0 ? 'left' :
                          halign == 1 ? 'center' : 'right');
         ctx.textBaseline = 'alphabetic';
-        ctx.fillText(Pointer_stringify(text), x, y);
+        ctx.fillText(UTF8ToString(text), x, y);
     },
 
     /*
@@ -542,7 +542,7 @@ mergeInto(LibraryManager.library, {
      * Set the text in the status bar.
      */
     js_canvas_set_statusbar: function(ptr) {
-        var text = Pointer_stringify(ptr);
+        var text = UTF8ToString(ptr);
         statusbar.replaceChild(document.createTextNode(text),
                                statusbar.lastChild);
     },
@@ -574,7 +574,7 @@ mergeInto(LibraryManager.library, {
      * overlay on top of the rest of the puzzle web page.
      */
     js_dialog_init: function(titletext) {
-        dialog_init(Pointer_stringify(titletext));
+        dialog_init(UTF8ToString(titletext));
     },
 
     /*
@@ -584,10 +584,10 @@ mergeInto(LibraryManager.library, {
      * construction.
      */
     js_dialog_string: function(index, title, initialtext) {
-        dlg_form.appendChild(document.createTextNode(Pointer_stringify(title)));
+        dlg_form.appendChild(document.createTextNode(UTF8ToString(title)));
         var editbox = document.createElement("input");
         editbox.type = "text";
-        editbox.value = Pointer_stringify(initialtext);
+        editbox.value = UTF8ToString(initialtext);
         dlg_form.appendChild(editbox);
         dlg_form.appendChild(document.createElement("br"));
 
@@ -607,9 +607,9 @@ mergeInto(LibraryManager.library, {
      * gives the separator.
      */
     js_dialog_choices: function(index, title, choicelist, initvalue) {
-        dlg_form.appendChild(document.createTextNode(Pointer_stringify(title)));
+        dlg_form.appendChild(document.createTextNode(UTF8ToString(title)));
         var dropdown = document.createElement("select");
-        var choicestr = Pointer_stringify(choicelist);
+        var choicestr = UTF8ToString(choicelist);
         var items = choicestr.slice(1).split(choicestr[0]);
         var options = [];
         for (var i in items) {
@@ -653,7 +653,7 @@ mergeInto(LibraryManager.library, {
         dlg_form.appendChild(checkbox);
         var checkboxlabel = document.createElement("label");
         checkboxlabel.setAttribute("for", checkbox.id);
-        checkboxlabel.textContent = Pointer_stringify(title);
+        checkboxlabel.textContent = UTF8ToString(title);
         dlg_form.appendChild(checkboxlabel);
         dlg_form.appendChild(document.createElement("br"));
 
index 56f69721f7197e6c60bbc10bc666c1c2baa6bc93..1579ead4209652b8ab6c7d75ae12619800070d2f 100644 (file)
@@ -486,15 +486,17 @@ function initPuzzle() {
         }
     });
 
-    // Run the C setup function, passing argv[1] as the fragment
-    // identifier (so that permalinks of the form puzzle.html#game-id
-    // can launch the specified id).
-    Module.callMain([location.hash]);
-
-    // And if we get here with everything having gone smoothly, i.e.
-    // we haven't crashed for one reason or another during setup, then
-    // it's probably safe to hide the 'sorry, no puzzle here' div and
-    // show the div containing the actual puzzle.
-    document.getElementById("apology").style.display = "none";
-    document.getElementById("puzzle").style.display = "inline";
+    Module.onRuntimeInitialized = function() {
+        // Run the C setup function, passing argv[1] as the fragment
+        // identifier (so that permalinks of the form puzzle.html#game-id
+        // can launch the specified id).
+        Module.callMain([location.hash]);
+
+        // And if we get here with everything having gone smoothly, i.e.
+        // we haven't crashed for one reason or another during setup, then
+        // it's probably safe to hide the 'sorry, no puzzle here' div and
+        // show the div containing the actual puzzle.
+        document.getElementById("apology").style.display = "none";
+        document.getElementById("puzzle").style.display = "inline";
+    };
 }