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

Thoughts on Asyncify #24

Open
AntonPieper opened this issue Feb 11, 2024 · 3 comments
Open

Thoughts on Asyncify #24

AntonPieper opened this issue Feb 11, 2024 · 3 comments

Comments

@AntonPieper
Copy link

AntonPieper commented Feb 11, 2024

Asyncify

Because native Web assembly does not yet support pausing and resuming, folks have invented Asyncify to transform wasm code to allow stack unwinding and rewinding. Most people use Asyncify with emscripten, but as far as I can tell, Asyncify is completely implemented through binaryen.

It can be used without emscripten through wasm-opt (taken from asyncify-wasm package's GitHub):

wasm-opt --asyncify [-O] [--pass-arg=[email protected],...] in.wasm -o out.wasm

Then on the JavaScript side, you can use exports.asyncify_start_rewind(...) and the like to use asyncify.

The package asyncify-wasm makes this simpler and allows "just" using async functions (promises) in the implementation of imported functions (example also taken from README.md):

let { instance } = await Asyncify.instantiateStreaming(fetch('./out.wasm'), {
  get_resource_text: async url => {
    let response = await fetch(readWasmString(instance, url));
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return passStringToWasm(instance, await response.text());
  }
});

await instance.exports._start();

Relevance for project

This could allow using literally the same code for native and web without any modifications (no GameFrame) to the examples at the cost of an extra build step (wasm-opt) and some performance (instrumentation from asyncify).

One could implement WindowShouldClose as a promise that resolved on the next frame:

{
  async WindowShouldClose() {
    return new Promise((resolve) => {
        /* ... */
        requestAnimationFrame(() => resolve(/* ... */)):
    });
  }
}
@AntonPieper
Copy link
Author

AntonPieper commented Feb 19, 2024

Possible without using Async

It is theoretically possible to solve some cases of the API without using async, however blocking for arbitrary promises/callbacks is not possible due to the nature of the single core JavaScript execution model.

Textures

One could run a synchronous XMLHttpRequest, parse the image manually and put it Base64 encoded into the src attribute of the image tag. I don't know whether this synchronously sets the width height automatically but I doubt it. You probably have to parse the width/height manually as well.

Fonts

Could be achieved the same way raylib does it in C by loading the font synchronously (XMLHttpRequest) and then restarting it into a texture (as described above).

Impossible without Async

As teased above, actively waiting for promises to finish does not work in JavaScript. You can't block like this:

// Does NOT work
function block(promise) {
  let running = true;
  let result = undefined;
  async function runner() {
    result = await promise;
    running = false;
  }
  runner();
  // Will never terminate, because this while loop blocks the runner from finishing
  while (running);
  return result;
}

// Blocks forever
const fetchedResult = block(fetch("./img.png"));

while (!WindowShouldClose())

This is the part that won't work. The rendering thread is blocked by JavaScript so actively waiting via while (new Date() < nextFrame); will never allow the browser to show the drawn canvas. A requestAnimationFrame is needed and the JavaScript has to stop for the callback to ever be called.

@ratchetfreak
Copy link

IMO the simplest use of asyncify for this project would be to make BeginDrawing async:

BeginDrawing() {
    if(!this.animframe){ // or read from __asyncify_state global
        // comes from regular call from wasm
        window.requestAnimationFrame((timestamp) => {
             this.dt = (timestamp - this.previous)/1000.0;
            this.previous = timestamp;
            //this.draw_ptr needs to be a location into wasm memory that can hold 2 pointers.
            this.wasm.instance.exports.asyncify_start_rewind(this.draw_ptr); 
            window.requestAnimationFrame(next);
            this.animframe = true;
            this.wasm.instance.exports.main();
            this.wasm.instance.exports.asyncify_stop_unwind(this.draw_ptr);
        });

        this.wasm.instance.exports.asyncify_start_unwind(this.draw_ptr);
        return;
    } else {
        //comes from rewind.


        this.wasm.instance.exports.asyncify_stop_rewind(this.draw_ptr);
        this.animframe = false;
        return; //regular return;
    }
}

this corresponds better I believe with the semantics of raylib where beginDraw actually sets up the state for drawing.

@AntonPieper
Copy link
Author

This is a nice full example👍 I will try this out as well. However in regards to semantics, I have checked rcore.c and it is handled in EndDrawing there

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

Successfully merging a pull request may close this issue.

2 participants