@ 261106.14 · fetching
current
/**
 * @license
 * Copyright 2011 The Emscripten Authors
 * SPDX-License-Identifier: MIT
 */

// Utilities for browser environments
var LibraryBrowser = {
  $workerHandles__internal: true,
  $workerHandles: 'new HandleAllocator();',

  $Browser__deps: [
#if FILESYSTEM
#if MAIN_MODULE
    '$preloadedWasm',
#endif
#endif
  ],

  $Browser: {
    useWebGL: false,
    isFullscreen: false,
    pointerLock: false,
    moduleContextCreatedCallbacks: [],
    preloadedImages: {},
    preloadedAudios: {},

    getCanvas: () => Module['canvas'],

    init() {
      if (Browser.initted) return;
      Browser.initted = true;

#if FILESYSTEM
      // Support for plugins that can process preloaded files. You can add more of these to
      // your app by creating and appending to preloadPlugins.
      //
      // Each plugin is asked if it can handle a file based on the file's name. If it can,
      // it is given the file's raw data. When it is done, it calls a callback with the file's
      // (possibly modified) data. For example, a plugin might decompress a file, or it
      // might create some side data structure for use later (like an Image element, etc.).

      var imagePlugin = {};
      imagePlugin['canHandle'] = (name) => {
        return !Module['noImageDecoding'] && /\.(jpg|jpeg|png|bmp|webp)$/i.test(name);
      };
      imagePlugin['handle'] = async (byteArray, name) => {
        var b = new Blob([byteArray], { type: Browser.getMimetype(name) });
        if (b.size !== byteArray.length) { // Safari bug #118630
          // Safari's Blob can only take an ArrayBuffer
          b = new Blob([(new Uint8Array(byteArray)).buffer], { type: Browser.getMimetype(name) });
        }
        var url = URL.createObjectURL(b);
        return new Promise((resolve, reject) => {
          var img = new Image();
          img.onload = () => {
#if ASSERTIONS
            assert(img.complete, `Image ${name} could not be decoded`);
#endif
            var canvas = /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
            canvas.width = img.width;
            canvas.height = img.height;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);
            Browser.preloadedImages[name] = canvas;
            URL.revokeObjectURL(url);
            resolve(byteArray);
          };
          img.onerror = (event) => {
            err(`Image ${url} could not be decoded`);
            reject();
          };
          img.src = url;
        });
      };
      preloadPlugins.push(imagePlugin);

      var audioPlugin = {};
      audioPlugin['canHandle'] = (name) => {
        return !Module['noAudioDecoding'] && name.slice(-4) in { '.ogg': 1, '.wav': 1, '.mp3': 1 };
      };
      audioPlugin['handle'] = async (byteArray, name) => {
        return new Promise((resolve, reject) => {
          var done = false;
          function finish(audio) {
            if (done) return;
            done = true;
            Browser.preloadedAudios[name] = audio;
            resolve(byteArray);
          }
          var b = new Blob([byteArray], { type: Browser.getMimetype(name) });
          var url = URL.createObjectURL(b); // XXX we never revoke this!
          var audio = new Audio();
          audio.addEventListener('canplaythrough', () => finish(audio)); // use addEventListener due to chromium bug 124926
          audio.onerror = (event) => {
            if (done) return;
            err(`warning: browser could not fully decode audio ${name}, trying slower base64 approach`);
            function encode64(data) {
              var BASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
              const PAD = '=';
              var ret = '';
              var leftchar = 0;
              var leftbits = 0;
              for (var byte of data) {
                leftchar = (leftchar << 8) | byte;
                leftbits += 8;
                while (leftbits >= 6) {
                  var curr = (leftchar >> (leftbits-6)) & 0x3f;
                  leftbits -= 6;
                  ret += BASE[curr];
                }
              }
              if (leftbits == 2) {
                ret += BASE[(leftchar&3) << 4];
                ret += PAD + PAD;
              } else if (leftbits == 4) {
                ret += BASE[(leftchar&0xf) << 2];
                ret += PAD;
              }
              return ret;
            }
            audio.src = 'data:audio/x-' + name.slice(-3) + ';base64,' + encode64(byteArray);
            finish(audio); // we don't wait for confirmation this worked - but it's worth trying
          };
          audio.src = url;
          // workaround for chrome bug 124926 - we do not always get oncanplaythrough or onerror
          safeSetTimeout(() => {
            finish(audio); // try to use it even though it is not necessarily ready to play
          }, 10_000);
        });
      };
      preloadPlugins.push(audioPlugin);
#endif

      // Canvas event setup

      function pointerLockChange() {
        var canvas = Browser.getCanvas();
        Browser.pointerLock = document.pointerLockElement === canvas;
      }
      var canvas = Browser.getCanvas();
      if (canvas) {
        // forced aspect ratio can be enabled by defining 'forcedAspectRatio' on Module
        // Module['forcedAspectRatio'] = 4 / 3;

        document.addEventListener('pointerlockchange', pointerLockChange);

#if expectToReceiveOnModule('elementPointerLock')
        if (Module['elementPointerLock']) {
          canvas.addEventListener('click', (ev) => {
            if (!Browser.pointerLock && Browser.getCanvas().requestPointerLock) {
              Browser.getCanvas().requestPointerLock();
              ev.preventDefault();
            }
          });
        }
#endif
      }
    },

    createContext(/** @type {HTMLCanvasElement} */ canvas, useWebGL, setInModule, webGLContextAttributes) {
      if (useWebGL && Module['ctx'] && canvas == Browser.getCanvas()) return Module['ctx']; // no need to recreate GL context if it's already been created for this canvas.

      var ctx;
      var contextHandle;
      if (useWebGL) {
        // For GLES2/desktop GL compatibility, adjust a few defaults to be different to WebGL defaults, so that they align better with the desktop defaults.
        var contextAttributes = {
          antialias: false,
          alpha: false,
#if MIN_WEBGL_VERSION >= 2
          majorVersion: 2,
#elif MAX_WEBGL_VERSION >= 2 // libbrowser.js defaults: use the WebGL version chosen at compile time (unless overridden below)
          majorVersion: (typeof WebGL2RenderingContext !== 'undefined') ? 2 : 1,
#else
          majorVersion: 1,
#endif
        };

        if (webGLContextAttributes) {
          for (var attribute in webGLContextAttributes) {
            contextAttributes[attribute] = webGLContextAttributes[attribute];
          }
        }

        // This check of existence of GL is here to satisfy Closure compiler, which yells if variable GL is referenced below but GL object is not
        // actually compiled in because application is not doing any GL operations. TODO: Ideally if GL is not being used, this function
        // Browser.createContext() should not even be emitted.
        if (typeof GL !== 'undefined') {
          contextHandle = GL.createContext(canvas, contextAttributes);
          if (contextHandle) {
            ctx = GL.getContext(contextHandle).GLctx;
          }
        }
      } else {
        ctx = canvas.getContext('2d');
      }

      if (!ctx) return null;

      if (setInModule) {
#if ASSERTIONS
        if (!useWebGL) assert(typeof GLctx === 'undefined', 'cannot set in module if GLctx is used, but we are a non-GL context that would replace it');
#endif
        Module['ctx'] = ctx;
        if (useWebGL) GL.makeContextCurrent(contextHandle);
        Browser.useWebGL = useWebGL;
        Browser.moduleContextCreatedCallbacks.forEach((callback) => callback());
        Browser.init();
      }
      return ctx;
    },

    fullscreenHandlersInstalled: false,
    lockPointer: undefined,
    resizeCanvas: undefined,
    requestFullscreen(lockPointer, resizeCanvas) {
      Browser.lockPointer = lockPointer;
      Browser.resizeCanvas = resizeCanvas;
      if (typeof Browser.lockPointer === 'undefined') Browser.lockPointer = true;
      if (typeof Browser.resizeCanvas === 'undefined') Browser.resizeCanvas = false;

      var canvas = Browser.getCanvas();
      function fullscreenChange() {
        Browser.isFullscreen = false;
        var canvasContainer = canvas.parentNode;
        if (getFullscreenElement() === canvasContainer) {
          canvas.exitFullscreen = Browser.exitFullscreen;
          if (Browser.lockPointer) canvas.requestPointerLock?.();
          Browser.isFullscreen = true;
          if (Browser.resizeCanvas) {
            Browser.setFullscreenCanvasSize();
          } else {
            Browser.updateCanvasDimensions(canvas);
          }
        } else {
          // remove the full screen specific parent of the canvas again to restore the HTML structure from before going full screen
          canvasContainer.parentNode.insertBefore(canvas, canvasContainer);
          canvasContainer.parentNode.removeChild(canvasContainer);

          if (Browser.resizeCanvas) {
            Browser.setWindowedCanvasSize();
          } else {
            Browser.updateCanvasDimensions(canvas);
          }
        }
#if expectToReceiveOnModule('onFullScreen')
        Module['onFullScreen']?.(Browser.isFullscreen);
        Module['onFullscreen']?.(Browser.isFullscreen);
#endif
      }

      if (!Browser.fullscreenHandlersInstalled) {
        Browser.fullscreenHandlersInstalled = true;
        document.addEventListener('fullscreenchange', fullscreenChange);
#if MIN_SAFARI_VERSION < 160400
        document.addEventListener('webkitfullscreenchange', fullscreenChange);
#endif
      }

      // create a new parent to ensure the canvas has no siblings. this allows browsers to optimize full screen performance when its parent is the full screen root
      var canvasContainer = document.createElement('div');
      canvas.parentNode.insertBefore(canvasContainer, canvas);
      canvasContainer.appendChild(canvas);

      // use parent of canvas as full screen root to allow aspect ratio correction (Firefox stretches the root to screen size)
#if MIN_SAFARI_VERSION < 160400
      // Safari didn't support Element.requestFullscreen until 16.4
      // See: https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen
      /** @suppress {checkTypes} */
      canvasContainer.requestFullscreen ??= (canvasContainer['webkitRequestFullscreen'] ? () => canvasContainer['webkitRequestFullscreen'](Element.ALLOW_KEYBOARD_INPUT) : null) ??
                                            (canvasContainer['webkitRequestFullScreen'] ? () => canvasContainer['webkitRequestFullScreen'](Element.ALLOW_KEYBOARD_INPUT) : null);

#endif
      canvasContainer.requestFullscreen();
    },

    exitFullscreen() {
      // This is workaround for chrome. Trying to exit from fullscreen
      // not in fullscreen state will cause 'TypeError: Document not active'
      // in chrome. See https://github.com/emscripten-core/emscripten/pull/8236
      if (!Browser.isFullscreen) {
        return false;
      }

#if MIN_SAFARI_VERSION < 160400
      var CFS = document.exitFullscreen ?? document['webkitCancelFullScreen'];
      CFS.call(document);
#else
      document.exitFullscreen();
#endif
      return true;
    },

    getMimetype(name) {
      return {
        'jpg': 'image/jpeg',
        'jpeg': 'image/jpeg',
        'png': 'image/png',
        'bmp': 'image/bmp',
        'ogg': 'audio/ogg',
        'wav': 'audio/wav',
        'mp3': 'audio/mpeg'
      }[name.slice(name.lastIndexOf('.')+1)];
    },

    // Browsers specify wheel direction according to the page CSS pixel Y direction:
    // Scrolling mouse wheel down (==towards user/away from screen) on Windows/Linux (and macOS without 'natural scroll' enabled)
    // is the positive wheel direction. Scrolling mouse wheel up (towards the screen) is the negative wheel direction.
    // This function returns the wheel direction in the browser page coordinate system (+: down, -: up). Note that this is often the
    // opposite of native code: In native APIs the positive scroll direction is to scroll up (away from the user).
    // NOTE: The mouse wheel delta is a decimal number, and can be a fractional value within -1 and 1. If you need to represent
    //       this as an integer, don't simply cast to int, or you may receive scroll events for wheel delta == 0.
    // NOTE: We convert all units returned by events into steps, i.e. individual wheel notches.
    //       These conversions are only approximations. Changing browsers, operating systems, or even settings can change the values.
    getMouseWheelDelta(event) {
      var delta = 0;
      switch (event.type) {
        case 'DOMMouseScroll':
          // 3 lines make up a step
          delta = event.detail / 3;
          break;
        case 'mousewheel':
          // 120 units make up a step
          delta = event.wheelDelta / 120;
          break;
        case 'wheel':
          delta = event.deltaY
          switch (event.deltaMode) {
            case 0:
              // DOM_DELTA_PIXEL: 100 pixels make up a step
              delta /= 100;
              break;
            case 1:
              // DOM_DELTA_LINE: 3 lines make up a step
              delta /= 3;
              break;
            case 2:
              // DOM_DELTA_PAGE: A page makes up 80 steps
              delta *= 80;
              break;
            default:
              abort('unrecognized mouse wheel delta mode: ' + event.deltaMode);
          }
          break;
        default:
          abort('unrecognized mouse wheel event: ' + event.type);
      }
      return delta;
    },

    mouseX: 0,
    mouseY: 0,
    mouseMovementX: 0,
    mouseMovementY: 0,
    touches: {},
    lastTouches: {},

    // Return the mouse coordinates relative to the top, left of the canvas, corrected for scroll offset.
    calculateMouseCoords(pageX, pageY) {
      // Calculate the movement based on the changes
      // in the coordinates.
      var canvas = Browser.getCanvas();
      var rect = canvas.getBoundingClientRect();

      var adjustedX = pageX - (window.scrollX + rect.left);
      var adjustedY = pageY - (window.scrollY + rect.top);

      // the canvas might be CSS-scaled compared to its backbuffer;
      // SDL-using content will want mouse coordinates in terms
      // of backbuffer units.
      adjustedX = adjustedX * (canvas.width / rect.width);
      adjustedY = adjustedY * (canvas.height / rect.height);

      return { x: adjustedX, y: adjustedY };
    },

    // Directly set the Browser state with new mouse coordinates calculated using calculateMouseCoords.
    setMouseCoords(pageX, pageY) {
      const {x, y} = Browser.calculateMouseCoords(pageX, pageY);
      Browser.mouseMovementX = x - Browser.mouseX;
      Browser.mouseMovementY = y - Browser.mouseY;
      Browser.mouseX = x;
      Browser.mouseY = y;
    },

    // Unpack a 'mouse' event, handling SDL touch paths and pointerlock compatibility stuff.
    calculateMouseEvent(event) { // event should be mousemove, mousedown or mouseup
      if (Browser.pointerLock) {
        // When the pointer is locked, calculate the coordinates
        // based on the movement of the mouse.
        Browser.mouseMovementX = event.movementX;
        Browser.mouseMovementY = event.movementY;

        // add the mouse delta to the current absolute mouse position
        Browser.mouseX += Browser.mouseMovementX;
        Browser.mouseY += Browser.mouseMovementY;
      } else {
        if (event.type === 'touchstart' || event.type === 'touchend' || event.type === 'touchmove') {
          var touch = event.touch;
          if (touch === undefined) {
            return; // the 'touch' property is only defined in SDL

          }
          var coords = Browser.calculateMouseCoords(touch.pageX, touch.pageY);

          if (event.type === 'touchstart') {
            Browser.lastTouches[touch.identifier] = coords;
            Browser.touches[touch.identifier] = coords;
          } else if (event.type === 'touchend' || event.type === 'touchmove') {
            var last = Browser.touches[touch.identifier];
            last ||= coords;
            Browser.lastTouches[touch.identifier] = last;
            Browser.touches[touch.identifier] = coords;
          }
          return;
        }

        Browser.setMouseCoords(event.pageX, event.pageY);
      }
    },

    resizeListeners: [],

    updateResizeListeners() {
      var canvas = Browser.getCanvas();
      Browser.resizeListeners.forEach((listener) => listener(canvas.width, canvas.height));
    },

    setCanvasSize(width, height, noUpdates = undefined) {
      var canvas = Browser.getCanvas();
      Browser.updateCanvasDimensions(canvas, width, height);
      if (!noUpdates) Browser.updateResizeListeners();
    },

    setFullscreenCanvasSize() {
      // check if SDL is available
      if (typeof SDL !== 'undefined') {
        var flags = {{{ makeGetValue('SDL.screen', '0', 'u32') }}};
        flags = flags | SDL_FULLSCREEN;
        {{{ makeSetValue('SDL.screen', '0', 'flags', 'i32') }}};
      }
      Browser.updateCanvasDimensions(Browser.getCanvas());
      Browser.updateResizeListeners();
    },

    setWindowedCanvasSize() {
      // check if SDL is available
      if (typeof SDL !== 'undefined') {
        var flags = {{{ makeGetValue('SDL.screen', '0', 'u32') }}};
        flags = flags & ~SDL_FULLSCREEN;
        {{{ makeSetValue('SDL.screen', '0', 'flags', 'i32') }}};
      }
      Browser.updateCanvasDimensions(Browser.getCanvas());
      Browser.updateResizeListeners();
    },

    updateCanvasDimensions(canvas, wNative = undefined, hNative = undefined) {
      if (wNative && hNative) {
        canvas.widthNative = wNative;
        canvas.heightNative = hNative;
      } else {
        wNative = canvas.widthNative;
        hNative = canvas.heightNative;
      }
      var w = wNative;
      var h = hNative;
#if expectToReceiveOnModule('forcedAspectRatio')
      if (Module['forcedAspectRatio'] > 0) {
        if (w/h < Module['forcedAspectRatio']) {
          w = Math.round(h * Module['forcedAspectRatio']);
        } else {
          h = Math.round(w / Module['forcedAspectRatio']);
        }
      }
#endif
      if ((getFullscreenElement() === canvas.parentNode) && (typeof screen !== 'undefined')) {
         var factor = Math.min(screen.width / w, screen.height / h);
         w = Math.round(w * factor);
         h = Math.round(h * factor);
      }
      if (Browser.resizeCanvas) {
        if (canvas.width  != w) canvas.width  = w;
        if (canvas.height != h) canvas.height = h;
        if (typeof canvas.style !== 'undefined') {
          canvas.style.removeProperty( 'width');
          canvas.style.removeProperty('height');
        }
      } else {
        if (canvas.width  != wNative) canvas.width  = wNative;
        if (canvas.height != hNative) canvas.height = hNative;
        if (typeof canvas.style !== 'undefined') {
          if (w != wNative || h != hNative) {
            canvas.style.setProperty( 'width', w + 'px', 'important');
            canvas.style.setProperty('height', h + 'px', 'important');
          } else {
            canvas.style.removeProperty( 'width');
            canvas.style.removeProperty('height');
          }
        }
      }
    },
  },

  $requestFullscreen: 'Browser.requestFullscreen',
  $setCanvasSize: 'Browser.setCanvasSize',
  $createContext: 'Browser.createContext',

  // Explicit dependency on `$Browser` is needed here because the preload
  // plugins themselves are registered in `Browser.init`.
  emscripten_run_preload_plugins__deps: ['$Browser'],
  emscripten_run_preload_plugins__proxy: 'sync',
  emscripten_run_preload_plugins: (file, onload, onerror) => {
    {{{ runtimeKeepalivePush() }}}

    var _file = UTF8ToString(file);
    var contents;
    try {
      contents = FS.readFile(_file);
    } catch (e) {
      return -1;
    }
#if ASSERTIONS
    assert(contents.subarray, 'unexpected file content');
#endif
    FS.createPreloadedFile(
      PATH.dirname(_file),
      PATH.basename(_file),
      contents, /*canRead=*/true, /*canWrite=*/true,
      () => {
        {{{ runtimeKeepalivePop() }}}
        if (onload) {{{ makeDynCall('vp', 'onload') }}}(file);
      },
      () => {
        {{{ runtimeKeepalivePop() }}}
        if (onerror) {{{ makeDynCall('vp', 'onerror') }}}(file);
      },
      /*dontCreateFile=*/true // it's already there
    );
    return 0;
  },

  $Browser_asyncPrepareDataCounter: 0,

  emscripten_run_preload_plugins_data__proxy: 'sync',
  emscripten_run_preload_plugins_data: (data, size, suffix, arg, onload, onerror) => {
    {{{ runtimeKeepalivePush() }}}

    suffix = UTF8ToString(suffix);
    var name = `prepare_data_${Browser_asyncPrepareDataCounter++}.${suffix}`;
    var cname = stringToNewUTF8(name);
    FS.createPreloadedFile(
      '/',
      name,
      HEAPU8.subarray(data, data + size),
      true, true,
      () => {
        {{{ runtimeKeepalivePop() }}}
        if (onload) {{{ makeDynCall('vpp', 'onload') }}}(arg, cname);
      },
      () => {
        {{{ runtimeKeepalivePop() }}}
        if (onerror) {{{ makeDynCall('vp', 'onerror') }}}(arg);
      },
      true // don'tCreateFile - it's already there
    );
  },

  // Callable from pthread, executes in pthread context.
  emscripten_async_run_script: (script, millis) => {
    // TODO: cache these to avoid generating garbage
    safeSetTimeout(() => _emscripten_run_script(script), millis);
  },

  // TODO: currently not callable from a pthread, but immediately calls onerror() if not on main thread.
  emscripten_async_load_script: async (url, onload, onerror) => {
    url = UTF8ToString(url);
#if PTHREADS
    if (ENVIRONMENT_IS_PTHREAD) {
      err(`emscripten_async_load_script("${url}") failed, emscripten_async_load_script is currently not available in pthreads!`);
      onerror && {{{ makeDynCall('v', 'onerror') }}}();
      return;
    }
#endif
#if ASSERTIONS
    assert(!runDependencies, 'async_load_script must be run when no other dependencies are active');
#endif
    {{{ runtimeKeepalivePush() }}}

    var loadDone = () => {
      {{{ runtimeKeepalivePop() }}}
      if (onload) {
        resolveRunDependencies().then(() => callUserCallback({{{ makeDynCall('v', 'onload') }}}));
      }
    }

    var loadError = () => {
      {{{ runtimeKeepalivePop() }}}
      if (onerror) {
        callUserCallback({{{ makeDynCall('v', 'onerror') }}});
      }
    };

#if ENVIRONMENT_MAY_BE_NODE && DYNAMIC_EXECUTION
    if (ENVIRONMENT_IS_NODE) {
      try {
        var data = await readAsync(url, false);
        eval(data);
        loadDone();
      } catch (e) {
        err(e);
        loadError();
      }
      return;
    }
#endif

    var script = document.createElement('script');
    script.onload = loadDone;
    script.onerror = loadError;
    script.src = url;
    document.body.appendChild(script);
  },

  emscripten_get_window_title__deps: ['malloc'],
  emscripten_get_window_title__proxy: 'sync',
  emscripten_get_window_title: () => {
    const buflen = 256;

    if (!_emscripten_get_window_title.buffer) {
      _emscripten_get_window_title.buffer = _malloc(buflen);
    }

    stringToUTF8(document.title, _emscripten_get_window_title.buffer, buflen);

    return _emscripten_get_window_title.buffer;
  },

  emscripten_set_window_title__proxy: 'sync',
  emscripten_set_window_title: (title) => document.title = UTF8ToString(title),

  emscripten_get_screen_size__proxy: 'sync',
  emscripten_get_screen_size: (width, height) => {
    {{{ makeSetValue('width', '0', 'screen.width', 'i32') }}};
    {{{ makeSetValue('height', '0', 'screen.height', 'i32') }}};
  },

  emscripten_hide_mouse__proxy: 'sync',
  emscripten_hide_mouse: () => {
    var styleSheet = document.styleSheets[0];
    var rules = styleSheet.cssRules;
    for (var i = 0; i < rules.length; i++) {
      if (rules[i].cssText.startsWith('canvas')) {
        styleSheet.deleteRule(i);
        i--;
      }
    }
    styleSheet.insertRule('canvas.emscripten { border: 1px solid black; cursor: none; }', 0);
  },

  emscripten_set_canvas_size__proxy: 'sync',
  emscripten_set_canvas_size: (width, height) => Browser.setCanvasSize(width, height),

  emscripten_get_canvas_size__proxy: 'sync',
  emscripten_get_canvas_size: (width, height, isFullscreen) => {
    var canvas = Browser.getCanvas();
    {{{ makeSetValue('width', '0', 'canvas.width', 'i32') }}};
    {{{ makeSetValue('height', '0', 'canvas.height', 'i32') }}};
    {{{ makeSetValue('isFullscreen', '0', 'Browser.isFullscreen ? 1 : 0', 'i32') }}};
  },

  // To avoid creating worker parent->child chains, always proxies to execute on the main thread.
  emscripten_create_worker__proxy: 'sync',
  emscripten_create_worker__deps: ['realloc'],
  emscripten_create_worker: (url) => {
    url = UTF8ToString(url);
    var worker = new Worker(url);
    var info = {
      worker,
      callbacks: [],
      awaited: 0,
      buffer: 0,
    };
    var id = workerHandles.allocate(info);
    worker.onmessage = (msg) => {
      if (ABORT) return;
      if (!workerHandles.has(id)) return; // worker was destroyed meanwhile
      var info = workerHandles.get(id);
      var callbackId = msg.data['callbackId'];
      var callbackInfo = info.callbacks[callbackId];
      if (!callbackInfo) return; // no callback or callback removed meanwhile
      // Don't trash our callback state if we expect additional calls.
      if (msg.data['finalResponse']) {
        info.awaited--;
        info.callbacks[callbackId] = null; // TODO: reuse callbackIds, compress this
        {{{ runtimeKeepalivePop() }}}
      }
      var data = msg.data['data'];
      if (data) {
        if (!data.byteLength) data = new Uint8Array(data);
        info.buffer = _realloc(info.buffer, data.length);
        HEAPU8.set(data, info.buffer);
        callbackInfo.func(info.buffer, data.length, callbackInfo.arg);
      } else {
        callbackInfo.func(0, 0, callbackInfo.arg);
      }
    };
    return id;
  },

  emscripten_destroy_worker__deps: ['free'],
  emscripten_destroy_worker__proxy: 'sync',
  emscripten_destroy_worker: (id) => {
    var info = workerHandles.get(id);
    info.worker.terminate();
    _free(info.buffer);
    workerHandles.free(id);
  },

  emscripten_call_worker__proxy: 'sync',
  emscripten_call_worker: (id, funcName, data, size, callback, arg) => {
    funcName = UTF8ToString(funcName);
    var info = workerHandles.get(id);
    var callbackId = -1;
    if (callback) {
      // If we are waiting for a response from the worker we need to keep
      // the runtime alive at least long enough to receive it.
      // The corresponding runtimeKeepalivePop is in the `finalResponse`
      // handler above.
      {{{ runtimeKeepalivePush() }}}
      callbackId = info.callbacks.length;
      info.callbacks.push({
        func: {{{ makeDynCall('vpip', 'callback') }}},
        arg
      });
      info.awaited++;
    }
    var transferObject = {
      'funcName': funcName,
      'callbackId': callbackId,
      'data': data ? HEAPU8.slice(data, data + size) : 0
    };
    if (data) {
      info.worker.postMessage(transferObject, [transferObject.data.buffer]);
    } else {
      info.worker.postMessage(transferObject);
    }
  },

#if BUILD_AS_WORKER
  emscripten_worker_respond_provisionally__proxy: 'sync',
  emscripten_worker_respond_provisionally: (data, size) => {
    if (workerResponded) abort('already responded with final response!');
    var transferObject = {
      'callbackId': workerCallbackId,
      'finalResponse': false,
      'data': data ? HEAPU8.slice(data, data + size) : 0
    };
    if (data) {
      postMessage(transferObject, [transferObject.data.buffer]);
    } else {
      postMessage(transferObject);
    }
  },

  emscripten_worker_respond__proxy: 'sync',
  emscripten_worker_respond: (data, size) => {
    if (workerResponded) abort('already responded with final response!');
    workerResponded = true;
    var transferObject = {
      'callbackId': workerCallbackId,
      'finalResponse': true,
      'data': data ? HEAPU8.slice(data, data + size) : 0
    };
    if (data) {
      postMessage(transferObject, [transferObject.data.buffer]);
    } else {
      postMessage(transferObject);
    }
  },
#endif

  emscripten_get_worker_queue_size__proxy: 'sync',
  emscripten_get_worker_queue_size: (id) => {
    if (!workerHandles.has(id)) return -1;
    var info = workerHandles.get(id);
    return info.awaited;
  },

  emscripten_get_preloaded_image_data__proxy: 'sync',
  emscripten_get_preloaded_image_data: (path, w, h) => getPreloadedImageData(UTF8ToString(path), w, h),

  $getPreloadedImageData__internal: true,
  $getPreloadedImageData__deps: ['malloc'],
  $getPreloadedImageData: (path, w, h) => {
    path = PATH_FS.resolve(path);

    var canvas = /** @type {HTMLCanvasElement} */(Browser.preloadedImages[path]);
    if (!canvas) return 0;

    var ctx = canvas.getContext('2d');
    var image = ctx.getImageData(0, 0, canvas.width, canvas.height);
    var buf = _malloc(canvas.width * canvas.height * 4);

    HEAPU8.set(image.data, buf);

    {{{ makeSetValue('w', '0', 'canvas.width', 'i32') }}};
    {{{ makeSetValue('h', '0', 'canvas.height', 'i32') }}};
    return buf;
  },

#if !WASMFS // WasmFS implements this in wasm
  emscripten_get_preloaded_image_data_from_FILE__deps: ['fileno'],
  emscripten_get_preloaded_image_data_from_FILE__proxy: 'sync',
  emscripten_get_preloaded_image_data_from_FILE: (file, w, h) => {
    var fd = _fileno(file);
    var stream = FS.getStream(fd);
    if (stream) {
      return getPreloadedImageData(stream.path, w, h);
    }

    return 0;
  }
#endif
};

addToLibrary(LibraryBrowser);