Add an experimental static library web demo using Emscripten. (#8171)
This is a proof of concept for deploying to the web using IREE. See the README for more details.
Rough structure:
* `build_static_emscripten_demo.sh` compiles an MNIST sample model targeting WebAssembly (`--iree-hal-target-backends=llvm --iree-llvm-target-triple=wasm32-unknown-unknown -iree-llvm-link-static`), compiles the runtime using Emscripten, then starts a local webserver for the provided index.html
* Parts of this could be folded into the CMakeLists.txt, but I think this split is easier to follow
* `index.html` has the web demo code (`<canvas>` for drawing, javascript for interfacing between the UI and the Wasm module)
* `main.c` has the native demo code (using IREE's high level `runtime/` API)
* `device_sync.c` and `device_multithreaded.c` implement CPU devices using the static library output
Future work _not_ finished here:
* Finish multithreading setup. I'm still debugging something in our use of threading/synchronization primitives and Emscripten's pthreads implementation (an infinite timeout instead always returns immediately, breaking our implementation)
* Support for non-static deployment by implementing an emscripten loader (similar to dlopen)
* Building/testing on our CI. We have an Emscripten build in https://github.com/google/iree/blob/main/build_tools/buildkite/cmake/build_configurations.yml that could be extended to cover this though
The resulting web page looks like this: 
(again - proof of concept, the demo program is not very accurate and the preprocessing logic may be wrong :D)
diff --git a/experimental/sample_web_static/CMakeLists.txt b/experimental/sample_web_static/CMakeLists.txt
new file mode 100644
index 0000000..2cf9582
--- /dev/null
+++ b/experimental/sample_web_static/CMakeLists.txt
@@ -0,0 +1,103 @@
+# Copyright 2022 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+if(NOT EMSCRIPTEN)
+ return()
+endif()
+
+set(_MNIST_OBJECT_NAME "iree_experimental_sample_web_static_mnist")
+add_library(${_MNIST_OBJECT_NAME} STATIC ${CMAKE_CURRENT_BINARY_DIR}/mnist_static.o)
+SET_TARGET_PROPERTIES(${_MNIST_OBJECT_NAME} PROPERTIES LINKER_LANGUAGE C)
+
+#-------------------------------------------------------------------------------
+# Sync
+#-------------------------------------------------------------------------------
+
+set(_NAME "iree_experimental_sample_web_static_sync")
+add_executable(${_NAME} "")
+target_include_directories(${_NAME} PUBLIC
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
+)
+target_sources(${_NAME}
+ PRIVATE
+ main.c
+ device_sync.c
+ ${CMAKE_CURRENT_BINARY_DIR}/mnist_static.h
+ ${CMAKE_CURRENT_BINARY_DIR}/mnist_bytecode.h
+ ${CMAKE_CURRENT_BINARY_DIR}/mnist_bytecode.c
+)
+set_target_properties(${_NAME} PROPERTIES OUTPUT_NAME "sample-web-static-sync")
+
+# Note: we have to be very careful about dependencies here.
+#
+# The general purpose libraries link in multiple executable loaders and HAL
+# drivers/devices, which include code not compatible with Emscripten.
+target_link_libraries(${_NAME}
+ ${_MNIST_OBJECT_NAME}
+ iree_runtime_runtime
+ iree_hal_local_loaders_static_library_loader
+ iree_hal_local_sync_driver
+)
+
+target_link_options(${_NAME} PRIVATE
+ # https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-ccall-cwrap
+ "-sEXPORTED_FUNCTIONS=['_setup_sample', '_cleanup_sample', '_run_sample']"
+ "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']"
+ #
+ "-sASSERTIONS=1"
+ #
+ "-gsource-map"
+ "-source-map-base="
+)
+
+#-------------------------------------------------------------------------------
+# Multithreaded
+#-------------------------------------------------------------------------------
+
+set(_NAME "iree_experimental_sample_web_static_multithreaded")
+add_executable(${_NAME} "")
+target_include_directories(${_NAME} PUBLIC
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
+)
+target_sources(${_NAME}
+ PRIVATE
+ main.c
+ device_multithreaded.c
+ ${CMAKE_CURRENT_BINARY_DIR}/mnist_static.h
+ ${CMAKE_CURRENT_BINARY_DIR}/mnist_bytecode.h
+ ${CMAKE_CURRENT_BINARY_DIR}/mnist_bytecode.c
+)
+set_target_properties(${_NAME} PROPERTIES OUTPUT_NAME "sample-web-static-multithreaded")
+
+# Note: we have to be very careful about dependencies here.
+#
+# The general purpose libraries link in multiple executable loaders and HAL
+# drivers/devices, which include code not compatible with Emscripten.
+target_link_libraries(${_NAME}
+ ${_MNIST_OBJECT_NAME}
+ iree_runtime_runtime
+ iree_hal_local_loaders_static_library_loader
+ iree_hal_local_task_driver
+ iree_task_api
+)
+
+target_link_options(${_NAME} PRIVATE
+ # https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-ccall-cwrap
+ "-sEXPORTED_FUNCTIONS=['_setup_sample', '_cleanup_sample', '_run_sample']"
+ "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']"
+ #
+ "-sASSERTIONS=1"
+ #
+ "-gsource-map"
+ "-source-map-base="
+ #
+ # https://emscripten.org/docs/porting/pthreads.html#compiling-with-pthreads-enabled
+ "-pthread"
+ # "-sINITIAL_MEMORY=67108864" # 64MB
+ "-sPTHREAD_POOL_SIZE=2"
+ # https://emscripten.org/docs/porting/pthreads.html#additional-flags
+ # "-sPROXY_TO_PTHREAD"
+)
diff --git a/experimental/sample_web_static/README.md b/experimental/sample_web_static/README.md
new file mode 100644
index 0000000..c2d55b5
--- /dev/null
+++ b/experimental/sample_web_static/README.md
@@ -0,0 +1,37 @@
+# Static Web Sample
+
+This experimental sample demonstrates one way to target the web platform with
+IREE. The output artifact is a web page containing an interactive MNIST digits
+classifier.
+
+## Quickstart
+
+1. Install IREE's host tools (e.g. by building the `install` target with CMake)
+2. Install the Emscripten SDK by
+ [following these directions](https://emscripten.org/docs/getting_started/downloads.html)
+3. Initialize your Emscripten environment (e.g. run `emsdk_env.bat`)
+4. From this directory, run `bash ./build_static_emscripten_demo.sh`
+ * You may need to set the path to your host tools install
+5. Open the localhost address linked in the script output
+
+To rebuild most parts of the demo (C runtime, sample HTML, CMake config, etc.),
+just `control + C` to stop the local webserver and rerun the script.
+
+## How it works
+
+This [MNIST model](../../iree/samples/models/mnist.mlir), also used in the
+[Vision sample](../../iree/samples/vision/), is compiled using the "static
+library" output setting of IREE's compiler (see the
+[Static library sample](../../iree/samples/static_library)). The resulting
+`.h` and `.o` files are compiled together with `main.c`, while the `.vmfb` is
+embedded into a C file that is similarly linked in.
+
+[Emscripten](https://emscripten.org/) is used (via the `emcmake` CMake wrapper)
+to compile the output binary into WebAssembly and JavaScript files.
+
+The provided `index.html` file can be served together with the output `.js`
+and `.wasm` files.
+
+## Multithreading
+
+TODO(scotttodd): this is incomplete - more changes are needed to the C runtime
diff --git a/experimental/sample_web_static/build_static_emscripten_demo.sh b/experimental/sample_web_static/build_static_emscripten_demo.sh
new file mode 100644
index 0000000..e91b96c
--- /dev/null
+++ b/experimental/sample_web_static/build_static_emscripten_demo.sh
@@ -0,0 +1,102 @@
+#!/bin/bash
+# Copyright 2022 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+set -e
+
+###############################################################################
+# Setup and checking for dependencies #
+###############################################################################
+
+if ! command -v emcmake &> /dev/null
+then
+ echo "'emcmake' not found, setup environment according to https://emscripten.org/docs/getting_started/downloads.html"
+ exit
+fi
+
+CMAKE_BIN=${CMAKE_BIN:-$(which cmake)}
+ROOT_DIR=$(git rev-parse --show-toplevel)
+
+BUILD_DIR=${ROOT_DIR?}/build-emscripten
+mkdir -p ${BUILD_DIR}
+
+BINARY_DIR=${BUILD_DIR}/experimental/sample_web_static/
+mkdir -p ${BINARY_DIR}
+
+###############################################################################
+# Compile from .mlir input to static C source files using host tools #
+###############################################################################
+
+# TODO(scotttodd): portable path ... discover from python install if on $PATH?
+INSTALL_ROOT="D:\dev\projects\iree-build\install\bin"
+TRANSLATE_TOOL="${INSTALL_ROOT?}/iree-translate.exe"
+EMBED_DATA_TOOL="${INSTALL_ROOT?}/generate_embed_data.exe"
+INPUT_NAME="mnist"
+INPUT_PATH="${ROOT_DIR?}/iree/samples/models/mnist.mlir"
+
+echo "=== Translating MLIR to static library output (.vmfb, .h, .o) ==="
+${TRANSLATE_TOOL?} ${INPUT_PATH} \
+ --iree-mlir-to-vm-bytecode-module \
+ --iree-input-type=mhlo \
+ --iree-hal-target-backends=llvm \
+ --iree-llvm-target-triple=wasm32-unknown-unknown \
+ --iree-llvm-link-embedded=false \
+ --iree-llvm-link-static \
+ --iree-llvm-static-library-output-path=${BINARY_DIR}/${INPUT_NAME}_static.o \
+ --o ${BINARY_DIR}/${INPUT_NAME}.vmfb
+
+echo "=== Embedding bytecode module (.vmfb) into C source files (.h, .c) ==="
+${EMBED_DATA_TOOL?} ${BINARY_DIR}/${INPUT_NAME}.vmfb \
+ --output_header=${BINARY_DIR}/${INPUT_NAME}_bytecode.h \
+ --output_impl=${BINARY_DIR}/${INPUT_NAME}_bytecode.c \
+ --identifier=iree_static_${INPUT_NAME} \
+ --flatten
+
+###############################################################################
+# Build the web artifacts using Emscripten #
+###############################################################################
+
+echo "=== Building web artifacts using Emscripten ==="
+
+pushd ${ROOT_DIR?}/build-emscripten
+
+# Configure using Emscripten's CMake wrapper, then build.
+# Note: The sample creates a task device directly, so no drivers are required,
+# but some targets are gated on specific CMake options.
+emcmake "${CMAKE_BIN?}" -G Ninja .. \
+ -DIREE_HOST_BINARY_ROOT=$PWD/../build-host/install \
+ -DIREE_BUILD_EXPERIMENTAL_WEB_SAMPLES=ON \
+ -DIREE_HAL_DRIVER_DEFAULTS=OFF \
+ -DIREE_HAL_DRIVER_DYLIB=ON \
+ -DIREE_BUILD_COMPILER=OFF \
+ -DIREE_BUILD_TESTS=OFF
+
+"${CMAKE_BIN?}" --build . --target \
+ iree_experimental_sample_web_static_sync
+ # iree_experimental_sample_web_static_multithreaded
+popd
+
+###############################################################################
+# Serve the demo using a local webserver #
+###############################################################################
+
+echo "=== Copying static files (index.html) to the build directory ==="
+
+cp ${ROOT_DIR?}/experimental/sample_web_static/index.html ${BINARY_DIR}
+
+echo "=== Running local webserver ==="
+echo " open at http://localhost:8000/build-emscripten/experimental/sample_web_static/"
+
+# **Note**: this serves from the root so source maps can reference code in the
+# source tree. A real deployment would bundle the output artifacts and serve
+# them from a build/release directory.
+
+# local_server.py is needed when using SharedArrayBuffer, with multithreading
+# python3 local_server.py --directory ${ROOT_DIR?}
+
+# http.server on its own is fine for single threaded use, and this doesn't
+# break CORS for external resources like easeljs from a CDN
+python3 -m http.server --directory ${ROOT_DIR?}
diff --git a/experimental/sample_web_static/device_multithreaded.c b/experimental/sample_web_static/device_multithreaded.c
new file mode 100644
index 0000000..4121f16
--- /dev/null
+++ b/experimental/sample_web_static/device_multithreaded.c
@@ -0,0 +1,62 @@
+// Copyright 2022 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "iree/hal/local/loaders/static_library_loader.h"
+#include "iree/hal/local/task_device.h"
+#include "iree/task/api.h"
+#include "mnist_static.h"
+
+iree_status_t create_device_with_static_loader(iree_allocator_t host_allocator,
+ iree_hal_device_t** out_device) {
+ iree_hal_task_device_params_t params;
+ iree_hal_task_device_params_initialize(¶ms);
+
+ // Load the statically embedded library.
+ const iree_hal_executable_library_header_t** static_library =
+ mnist_linked_llvm_library_query(
+ IREE_HAL_EXECUTABLE_LIBRARY_LATEST_VERSION,
+ /*reserved=*/NULL);
+ const iree_hal_executable_library_header_t** libraries[1] = {static_library};
+
+ iree_hal_executable_loader_t* library_loader = NULL;
+ iree_status_t status = iree_hal_static_library_loader_create(
+ IREE_ARRAYSIZE(libraries), libraries,
+ iree_hal_executable_import_provider_null(), host_allocator,
+ &library_loader);
+
+ // Create a task executor.
+ iree_task_executor_t* executor = NULL;
+ iree_task_scheduling_mode_t scheduling_mode = 0;
+ iree_host_size_t worker_local_memory = 0;
+ iree_task_topology_t topology;
+ iree_task_topology_initialize(&topology);
+ // TODO(scotttodd): Try with more threads
+ iree_task_topology_initialize_from_group_count(/*group_count=*/1, &topology);
+ if (iree_status_is_ok(status)) {
+ status = iree_task_executor_create(scheduling_mode, &topology,
+ worker_local_memory, host_allocator,
+ &executor);
+ }
+ iree_task_topology_deinitialize(&topology);
+
+ iree_string_view_t identifier = iree_make_cstring_view("task");
+ iree_hal_allocator_t* device_allocator = NULL;
+ if (iree_status_is_ok(status)) {
+ status = iree_hal_allocator_create_heap(identifier, host_allocator,
+ host_allocator, &device_allocator);
+ }
+
+ if (iree_status_is_ok(status)) {
+ status = iree_hal_task_device_create(
+ identifier, ¶ms, executor, /*loader_count=*/1, &library_loader,
+ device_allocator, host_allocator, out_device);
+ }
+
+ iree_hal_allocator_release(device_allocator);
+ iree_task_executor_release(executor);
+ iree_hal_executable_loader_release(library_loader);
+ return status;
+}
diff --git a/experimental/sample_web_static/device_sync.c b/experimental/sample_web_static/device_sync.c
new file mode 100644
index 0000000..82e291d
--- /dev/null
+++ b/experimental/sample_web_static/device_sync.c
@@ -0,0 +1,45 @@
+// Copyright 2022 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "iree/hal/local/loaders/static_library_loader.h"
+#include "iree/hal/local/sync_device.h"
+#include "mnist_static.h"
+
+iree_status_t create_device_with_static_loader(iree_allocator_t host_allocator,
+ iree_hal_device_t** out_device) {
+ iree_hal_sync_device_params_t params;
+ iree_hal_sync_device_params_initialize(¶ms);
+
+ // Load the statically embedded library.
+ const iree_hal_executable_library_header_t** static_library =
+ mnist_linked_llvm_library_query(
+ IREE_HAL_EXECUTABLE_LIBRARY_LATEST_VERSION,
+ /*reserved=*/NULL);
+ const iree_hal_executable_library_header_t** libraries[1] = {static_library};
+
+ iree_hal_executable_loader_t* library_loader = NULL;
+ iree_status_t status = iree_hal_static_library_loader_create(
+ IREE_ARRAYSIZE(libraries), libraries,
+ iree_hal_executable_import_provider_null(), host_allocator,
+ &library_loader);
+
+ iree_string_view_t identifier = iree_make_cstring_view("sync");
+ iree_hal_allocator_t* device_allocator = NULL;
+ if (iree_status_is_ok(status)) {
+ status = iree_hal_allocator_create_heap(identifier, host_allocator,
+ host_allocator, &device_allocator);
+ }
+
+ if (iree_status_is_ok(status)) {
+ status = iree_hal_sync_device_create(
+ identifier, ¶ms, /*loader_count=*/1, &library_loader,
+ device_allocator, host_allocator, out_device);
+ }
+
+ iree_hal_allocator_release(device_allocator);
+ iree_hal_executable_loader_release(library_loader);
+ return status;
+}
diff --git a/experimental/sample_web_static/index.html b/experimental/sample_web_static/index.html
new file mode 100644
index 0000000..d3da38f
--- /dev/null
+++ b/experimental/sample_web_static/index.html
@@ -0,0 +1,235 @@
+<!DOCTYPE html>
+<html>
+
+<!--
+Copyright 2022 The IREE Authors
+
+Licensed under the Apache License v2.0 with LLVM Exceptions.
+See https://llvm.org/LICENSE.txt for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+<head>
+ <meta charset="utf-8" />
+ <title>IREE Static Web Sample</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <!-- TODO(scotttodd): use local copy for CORS webserver / SharedArrayBuffer workarounds? -->
+ <script src="https://code.createjs.com/1.0.0/easeljs.min.js"></script>
+</head>
+
+<body style="background-color: #2b2c30; color: #ABB2BF">
+ <h1>IREE Static Web Sample</h1>
+
+ <canvas id="drawingCanvas" width="256" height="256"
+ style="border:2px solid #000000; background-color: #FFFFFF;"
+ oncontextmenu="return false;">
+ </canvas>
+ <canvas id="rescaledCanvas" width="28" height="28"
+ style="border:2px solid #000000; background-color: #FFFFFF;">
+ </canvas>
+
+ <br>
+ <div style="border:2px solid #000000; background-color: #CCCCCC; padding: 8px; color: #111111" width="400px" height="300px">
+ <button id="predictButton" disabled onclick="predictDigit()">Predict handwritten digit</button>
+ <br>
+ Prediction result: <div id="predictionResult"></div>
+ </div>
+
+ <script>
+ let setupNativeSample;
+ let cleanupNativeSample;
+ let runNativeSample;
+ let nativeState;
+ const predictionResultElement = document.getElementById("predictionResult");
+ const predictButtonElement = document.getElementById("predictButton");
+ let initialized = false;
+
+ const imagePixelCount = 28 * 28;
+ let imageBuffer;
+
+ var Module = {
+ print: function(text) {
+ console.log(text);
+ },
+ printErr: function(text) {
+ console.error(text);
+ },
+ onRuntimeInitialized: function() {
+ console.log("WebAssembly module onRuntimeInitialized()");
+
+ setupNativeSample = Module.cwrap("setup_sample", "number", []);
+ cleanupNativeSample = Module.cwrap("cleanup_sample", null, ["number"]);
+ runNativeSample = Module.cwrap("run_sample", "number", ["number", "number"]);
+
+ setupSample();
+ },
+ // https://emscripten.org/docs/api_reference/module.html#Module.noInitialRun
+ noInitialRun: true,
+ };
+
+ function setupSample() {
+ nativeState = setupNativeSample();
+ predictButtonElement.disabled = false;
+ imageBuffer = Module._malloc(imagePixelCount * Float32Array.BYTES_PER_ELEMENT);
+ initialized = true;
+ }
+
+ // TODO(scotttodd): call this on page suspend?
+ function cleanupSample() {
+ initialized = false;
+ Module._free(imageDataBuffer);
+ predictButtonElement.disabled = true;
+ cleanupNativeSample();
+ nativeState = null;
+ }
+
+ function predictDigit() {
+ const rawImageData = getRescaledCanvasData();
+ preprocessImageData(rawImageData);
+
+ result = runNativeSample(nativeState, imageBuffer);
+ if (result != -1) {
+ predictionResultElement.innerHTML = result;
+ } else {
+ predictionResultElement.innerHTML = "Error";
+ }
+ }
+
+ // https://becominghuman.ai/passing-and-returning-webassembly-array-parameters-a0f572c65d97
+ // https://developers.google.com/web/updates/2018/03/emscripting-a-c-library#get_an_image_from_javascript_into_wasm
+ function preprocessImageData(rawImageData) {
+ // * getImageData() returns a Uint8ClampedArray with RGBA image data
+ // * this MNIST model takes tensor<1x28x28x1xf32> with grayscale pixels
+ // in [0.0, 1.0]
+
+ // This conversion is terrible, but this is a toy demo with a small image
+ // Hopefully there aren't any logic / iteration order issues...
+ const typedArray = new Float32Array(imagePixelCount);
+ for (let y = 0; y < 28; ++y) {
+ for (let x = 0; x < 28; ++x) {
+ const typedIndex = y * 28 + x;
+ const rawIndex = 4 * (y * 28 + x) + 3; // Assume colorSpace srgb
+ typedArray[typedIndex] = rawImageData.data[rawIndex] / 255.0;
+ }
+ }
+
+ // Copy into Wasm heap.
+ // Note: we could have done the conversion in-place, but this is demo code
+ Module.HEAPF32.set(typedArray, imageBuffer >> 2);
+ }
+
+ </script>
+ <script src="sample-web-static-sync.js"></script>
+ <!-- <script src="sample-web-static-multithreaded.js"></script> -->
+
+
+ <script>
+ // Forked from:
+ // https://createjs.com/demos/easeljs/curveto
+ // https://github.com/CreateJS/EaselJS/blob/master/examples/CurveTo.html
+
+ let drawingCanvasElement;
+ let rescaledCanvasElement, rescaledCanvasContext;
+ let stage;
+ let drawingCanvasShape;
+ let oldPt, oldMidPt;
+ let titleText;
+ const primaryColor = "#000000";
+ const eraseColor = "#FFFFFF";
+ const stroke = 32;
+
+ function initDrawing() {
+ drawingCanvasElement = document.getElementById("drawingCanvas");
+
+ rescaledCanvasElement = document.getElementById("rescaledCanvas");
+ rescaledCanvasContext = rescaledCanvasElement.getContext("2d");
+ rescaledCanvasContext.imageSmoothingEnabled = false;
+ rescaledCanvasContext.mozImageSmoothingEnabled = false;
+ rescaledCanvasContext.webkitImageSmoothingEnabled = false;
+ rescaledCanvasContext.msImageSmoothingEnabled = false;
+
+ stage = new createjs.Stage(drawingCanvasElement);
+ stage.autoClear = false;
+ stage.enableDOMEvents(true);
+
+ createjs.Touch.enable(stage);
+ createjs.Ticker.framerate = 24;
+
+ stage.addEventListener("stagemousedown", handleMouseDown);
+ stage.addEventListener("stagemouseup", handleMouseUp);
+
+ drawingCanvasShape = new createjs.Shape();
+ stage.addChild(drawingCanvasShape);
+
+ // Add instruction text.
+ titleText = new createjs.Text("Click and Drag to draw", "18px Arial", "#000000");
+ titleText.x = 30;
+ titleText.y = 100;
+ stage.addChild(titleText);
+
+ stage.update();
+ }
+
+ function handleMouseDown(event) {
+ if (!event.primary && !event.secondary) { return; }
+
+ if (stage.contains(titleText)) {
+ stage.clear();
+ stage.removeChild(titleText);
+ }
+
+ oldPt = new createjs.Point(stage.mouseX, stage.mouseY);
+ oldMidPt = oldPt.clone();
+ stage.addEventListener("stagemousemove", handleMouseMove);
+ }
+
+ function handleMouseMove(event) {
+ if (!event.primary && !event.secondary) { return; }
+
+ const midPt = new createjs.Point(
+ oldPt.x + stage.mouseX >> 1, oldPt.y + stage.mouseY >> 1);
+
+ const color = event.nativeEvent.which == 1 ? primaryColor : eraseColor;
+ drawingCanvasShape.graphics.clear()
+ .setStrokeStyle(stroke, 'round', 'round')
+ .beginStroke(color).moveTo(midPt.x, midPt.y)
+ .curveTo(oldPt.x, oldPt.y, oldMidPt.x, oldMidPt.y);
+
+ oldPt.x = stage.mouseX;
+ oldPt.y = stage.mouseY;
+ oldMidPt.x = midPt.x;
+ oldMidPt.y = midPt.y;
+
+ stage.update();
+ updateRescaledCanvas();
+
+ if (initialized) {
+ // TODO(scotttodd): debounce / rate limit this
+ predictDigit();
+ }
+ }
+
+ function handleMouseUp(event) {
+ if (!event.primary && !event.default) { return; }
+ stage.removeEventListener("stagemousemove", handleMouseMove);
+ }
+
+ function updateRescaledCanvas() {
+ rescaledCanvasContext.drawImage(
+ drawingCanvasElement,
+ /*sx=*/0, /*sy=*/0,
+ /*sWidth=*/256, /*sHeight=*/256,
+ /*dx=*/0, /*dy=*/0,
+ /*dWidth=*/28, /*dHeight=*/28);
+ }
+
+ function getRescaledCanvasData() {
+ return rescaledCanvasContext.getImageData(0, 0, 28, 28);
+ }
+
+ initDrawing();
+ </script>
+</body>
+
+</html>
diff --git a/experimental/sample_web_static/local_server.py b/experimental/sample_web_static/local_server.py
new file mode 100644
index 0000000..835a760
--- /dev/null
+++ b/experimental/sample_web_static/local_server.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+# Copyright 2022 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""Local server for development, with support for CORS headers and MIME types.
+
+NOTE: This is NOT suitable for production serving, it is just a slightly
+extended version of https://docs.python.org/3/library/http.server.html.
+
+Usage:
+ python3 local_server.py --directory {build_dir}
+ (then open http://localhost:8000/ in your browser)
+"""
+
+import os
+from functools import partial
+from http import server
+
+
+class CORSHTTPRequestHandler(server.SimpleHTTPRequestHandler):
+
+ def __init__(self, *args, **kwargs):
+ # Include MIME types for files we expect to be serving.
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
+ self.extensions_map.update({
+ ".js": "application/javascript",
+ ".wasm": "application/wasm",
+ })
+ super().__init__(*args, **kwargs)
+
+ # Inspiration for this hack: https://stackoverflow.com/a/13354482
+ def end_headers(self):
+ self.send_cors_headers()
+
+ server.SimpleHTTPRequestHandler.end_headers(self)
+
+ def send_cors_headers(self):
+ # Emscripten uses SharedArrayBuffer for its multithreading, which requires
+ # Cross Origin Opener Policy and Cross Origin Embedder Policy headers:
+ # * https://emscripten.org/docs/porting/pthreads.html
+ # * https://developer.chrome.com/blog/enabling-shared-array-buffer/
+ self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
+ self.send_header("Cross-Origin-Opener-Policy", "same-origin")
+
+
+if __name__ == '__main__':
+ import argparse
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--directory',
+ '-d',
+ default=os.getcwd(),
+ help='Specify alternative directory '
+ '[default:current directory]')
+ parser.add_argument('port',
+ action='store',
+ default=8000,
+ type=int,
+ nargs='?',
+ help='Specify alternate port [default: 8000]')
+ args = parser.parse_args()
+
+ server.test(HandlerClass=partial(CORSHTTPRequestHandler,
+ directory=args.directory),
+ port=args.port)
diff --git a/experimental/sample_web_static/main.c b/experimental/sample_web_static/main.c
new file mode 100644
index 0000000..23441bf
--- /dev/null
+++ b/experimental/sample_web_static/main.c
@@ -0,0 +1,183 @@
+// Copyright 2022 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include <float.h>
+#include <stdio.h>
+
+#include "iree/runtime/api.h"
+#include "iree/vm/bytecode_module.h"
+#include "mnist_bytecode.h"
+
+//===----------------------------------------------------------------------===//
+// Public API
+//===----------------------------------------------------------------------===//
+
+typedef struct iree_sample_state_t iree_sample_state_t;
+static void iree_sample_state_initialize(iree_sample_state_t* out_state);
+
+// TODO(scotttodd): figure out error handling and state management
+// * out_state and return status would make sense, but emscripten...
+iree_sample_state_t* setup_sample();
+void cleanup_sample(iree_sample_state_t* state);
+
+int run_sample(iree_sample_state_t* state, float* image_data);
+
+//===----------------------------------------------------------------------===//
+// Implementation
+//===----------------------------------------------------------------------===//
+
+extern iree_status_t create_device_with_static_loader(
+ iree_allocator_t host_allocator, iree_hal_device_t** out_device);
+
+typedef struct iree_sample_state_t {
+ iree_runtime_instance_t* instance;
+ iree_hal_device_t* device;
+ iree_runtime_session_t* session;
+ iree_vm_module_t* module;
+ iree_runtime_call_t call;
+} iree_sample_state_t;
+
+iree_status_t create_bytecode_module(iree_vm_module_t** out_module) {
+ const struct iree_file_toc_t* module_file_toc = iree_static_mnist_create();
+ iree_const_byte_span_t module_data =
+ iree_make_const_byte_span(module_file_toc->data, module_file_toc->size);
+ return iree_vm_bytecode_module_create(module_data, iree_allocator_null(),
+ iree_allocator_system(), out_module);
+}
+
+iree_sample_state_t* setup_sample() {
+ iree_sample_state_t* state = NULL;
+ iree_status_t status = iree_allocator_malloc(
+ iree_allocator_system(), sizeof(iree_sample_state_t), (void**)&state);
+
+ iree_runtime_instance_options_t instance_options;
+ iree_runtime_instance_options_initialize(IREE_API_VERSION_LATEST,
+ &instance_options);
+ // Note: no call to iree_runtime_instance_options_use_all_available_drivers().
+
+ if (iree_status_is_ok(status)) {
+ status = iree_runtime_instance_create(
+ &instance_options, iree_allocator_system(), &state->instance);
+ }
+
+ if (iree_status_is_ok(status)) {
+ status = create_device_with_static_loader(iree_allocator_system(),
+ &state->device);
+ }
+
+ iree_runtime_session_options_t session_options;
+ iree_runtime_session_options_initialize(&session_options);
+ iree_runtime_session_t* session = NULL;
+ if (iree_status_is_ok(status)) {
+ status = iree_runtime_session_create_with_device(
+ state->instance, &session_options, state->device,
+ iree_runtime_instance_host_allocator(state->instance), &state->session);
+ }
+
+ if (iree_status_is_ok(status)) {
+ status = create_bytecode_module(&state->module);
+ }
+ if (iree_status_is_ok(status)) {
+ status = iree_runtime_session_append_module(state->session, state->module);
+ }
+
+ const char kMainFunctionName[] = "module.predict";
+ if (iree_status_is_ok(status)) {
+ status = iree_runtime_call_initialize_by_name(
+ state->session, iree_make_cstring_view(kMainFunctionName),
+ &state->call);
+ }
+
+ if (!iree_status_is_ok(status)) {
+ iree_status_fprint(stderr, status);
+ iree_status_free(status);
+ cleanup_sample(state);
+ return NULL;
+ }
+
+ return state;
+}
+
+void cleanup_sample(iree_sample_state_t* state) {
+ iree_runtime_call_deinitialize(&state->call);
+
+ // Cleanup session and instance.
+ iree_hal_device_release(state->device);
+ iree_runtime_session_release(state->session);
+ iree_runtime_instance_release(state->instance);
+ iree_vm_module_release(state->module);
+
+ free(state);
+}
+
+int run_sample(iree_sample_state_t* state, float* image_data) {
+ iree_status_t status = iree_ok_status();
+
+ iree_runtime_call_reset(&state->call);
+
+ iree_hal_buffer_view_t* arg_buffer_view = NULL;
+ iree_hal_dim_t buffer_shape[] = {1, 28, 28, 1};
+ iree_hal_memory_type_t input_memory_type =
+ IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE;
+ if (iree_status_is_ok(status)) {
+ status = iree_hal_buffer_view_allocate_buffer(
+ iree_hal_device_allocator(state->device), buffer_shape,
+ IREE_ARRAYSIZE(buffer_shape), IREE_HAL_ELEMENT_TYPE_FLOAT_32,
+ IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR, input_memory_type,
+ IREE_HAL_BUFFER_USAGE_DISPATCH | IREE_HAL_BUFFER_USAGE_TRANSFER,
+ iree_make_const_byte_span((void*)image_data, sizeof(float) * 28 * 28),
+ &arg_buffer_view);
+ }
+ if (iree_status_is_ok(status)) {
+ status = iree_runtime_call_inputs_push_back_buffer_view(&state->call,
+ arg_buffer_view);
+ }
+ iree_hal_buffer_view_release(arg_buffer_view);
+
+ if (iree_status_is_ok(status)) {
+ status = iree_runtime_call_invoke(&state->call, /*flags=*/0);
+ }
+
+ // Get the result buffers from the invocation.
+ iree_hal_buffer_view_t* ret_buffer_view = NULL;
+ if (iree_status_is_ok(status)) {
+ status = iree_runtime_call_outputs_pop_front_buffer_view(&state->call,
+ &ret_buffer_view);
+ }
+
+ // Read back the results. The output of the mnist model is a 1x10 prediction
+ // confidence values for each digit in [0, 9].
+ float predictions[1 * 10] = {0.0f};
+ if (iree_status_is_ok(status)) {
+ status =
+ iree_hal_buffer_read_data(iree_hal_buffer_view_buffer(ret_buffer_view),
+ 0, predictions, sizeof(predictions));
+ }
+ iree_hal_buffer_view_release(ret_buffer_view);
+
+ if (!iree_status_is_ok(status)) {
+ iree_status_fprint(stderr, status);
+ iree_status_free(status);
+ return -1;
+ }
+
+ // Get the highest index from the output.
+ float result_val = FLT_MIN;
+ int result_idx = 0;
+ for (iree_host_size_t i = 0; i < IREE_ARRAYSIZE(predictions); ++i) {
+ if (predictions[i] > result_val) {
+ result_val = predictions[i];
+ result_idx = i;
+ }
+ }
+ fprintf(stdout,
+ "Prediction: %d, confidences: [%.2f, %.2f, %.2f, %.2f, %.2f, %.2f, "
+ "%.2f, %.2f, %.2f, %.2f]\n",
+ result_idx, predictions[0], predictions[1], predictions[2],
+ predictions[3], predictions[4], predictions[5], predictions[6],
+ predictions[7], predictions[8], predictions[9]);
+ return result_idx;
+}