Skip to content

Commit

Permalink
[Docs] Update Emscripten page
Browse files Browse the repository at this point in the history
  • Loading branch information
chnoblouch committed Jul 3, 2024
1 parent a0ea520 commit 802af2a
Showing 1 changed file with 31 additions and 41 deletions.
72 changes: 31 additions & 41 deletions docs/pages/Emscripten.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ Not all scenes from app-demo can run in the browser because OpenGL 4.0 functions

<h2>How it works</h2>

The Emscripten toolchain is made from the [Clang compiler](https://clang.llvm.org/), some runtime libraries, an implementation of the C, C++ and POSIX APIs, and ports of some popular libraries such as SDL, GLFW, libpng or zlib. This allows us to take code written for desktop platforms and port it to the Web without much effort.
The Emscripten toolchain contains a modified [Clang compiler](https://clang.llvm.org/), some runtime libraries, an implementation of the C, C++ and POSIX APIs, and ports of some popular libraries such as SDL, GLFW, libpng or zlib. This allows us to take code written for desktop platforms and port it to the Web without much effort.

<div style="width: 100%; justify-content: center; display: flex">
<img src="images/emscripten_apis.svg" width="40%">
</div>

Emscripten uses standard browser APIs to implement its libraries. For example, a call to the C function ```printf``` probably uses ```console.log``` internally. Here is how a few commonly used libraries are implented behind the scenes:
Emscripten uses standard browser APIs to implement its libraries. For example, a call to the C function `printf` might use the JavaScript function `console.log` internally. Here is how a few commonly used libraries are implented behind the scenes:

- Threads: [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
- Sockets: [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
Expand All @@ -22,50 +22,40 @@ Emscripten uses standard browser APIs to implement its libraries. For example, a

<h2>Browser Interaction</h2>

Even though Emscripten provides APIs for most browser features, it is sometimes still necessary to call some JavaScript functions directly. For example, there is currently no camera API. Emscripten provides a ```EM_ASM``` macro to embed JavaScript code directly into C/C++ code, which is comparable to inline assembly in native compilers. This allows us to call browser APIs directly in C++ and to pass values between JavaScript and C++. The JavaScript code can only run on the main thread, so we use the ```MAIN_THREAD_EM_ASM``` macro.
Even though Emscripten provides APIs for most browser features, it is sometimes still necessary to call some JavaScript functions directly. For example, there is currently no camera API. Emscripten provides a `EM_ASM` macro to embed JavaScript code directly into C/C++ code, which is comparable to inline assembly in native compilers. This allows us to call browser APIs directly in C++ and to pass values between JavaScript and C++.

<h2>Threading</h2>

<h3>JavaScript Is Asynchronous</h3>
<h2>JavaScript Is Asynchronous</h2>

JavaScript runs its code in a [event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop). Code is executed when an event
is triggered, such as a keypress or a new animation frame. The event is processed completely and then the browser can continue its work. This
is why you can't run an infinite loop in JavaScript as this would block the browser window. I/O function such as downloading files don't
block, but rather take a callback as argument that the browser calls when the data is available. This asynchronous thinking is very different
from languages such as C++ or Java where you usually block until an operation is complete. This now poses a problem: How do we download files
like textures synchronously so they are available when we want to use them in the next function call?

<h3>Web Workers</h3>

The solution is to run the entire application in a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). A
Web Worker runs its code in a separate thread, which is allowed to block. We add the flag ```-sPROXY_TO_PTHREAD``` to the Emscripten linker,
which generates a small main function that runs the original main function in a different thread. Now we are able to synchronously download assets from the server. Before starting the download, we start a spinner animation so the user knows that the app is loading and not crashing.

<h3>OffscreenCanvas</h3>

The canvas element where our graphics are drawn is not accessible in Web Workers. Fortunately, modern browsers implement
an interface called [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas). This interface allows us to
transfer the canvas from the main thread to our Web Worker and render into it. We use the Emscripten linker flag ```-sOFFSCREENCANVAS_SUPPORT=1```
to enable this feature and the flag ```-sOFFSCREENCANVASES_TO_PTHREAD='#canvas'``` to transfer the HTML canvas element to our Web Worker.
is triggered, such as a keypress or a new animation frame. The browser can only continue its work when the event is processed by the JavaScript code. This
is why you can't run an infinite loop in JavaScript as this would block the browser window. JavaScript APIs usually don't block for this reason, but
return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that represents an
asynchronous operation. The calling code can then specify callbacks that are run when the operation completes or fails.

This asynchronous thinking leads to some limitations when developing for Emscripten:
- There is no main `while` loop in `AppEmscripten.cpp`. Instead, we submit a function to the browser
that is called when a new animation frame is available. The Emscripten function to do this is `emscripten_request_animation_frame`.
- I/O functions are not allowed on the main thread. I/O operations like downloading files from a server
cannot run on the main thread because they run synchronously and block their thread. This problem
is usually handled by submitting a task to the `SLAssetLoader` to download the files on its worker thread instead.
- Core assets like fonts or common shaders are loaded asynchronously. On all platforms other than Emscripten, assets that
are used in all scenes are loaded synchronously before starting the main loop. On Emscripten, this task is done on
the worker thread of the `SLAssetLoader` because I/O is not allowed on the main thread.
- The number of threads is limited. The Emscripten implementation of C++ threads can only start the backing Web Worker
once we yield to the browser event loop. This means that after spawning a thread, we cannot use it directly
because its Web Worker hasn't started yet. The solution is to create a pool of Web Workers before running the `main` function
that are used when a new thread is created. This is done by adding the linker flag
`-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency + 4` that sets the Web Worker pool size to the number of
logical processors available plus 4 for good measure.
For more information, see [the Pthreads page in the Emscripten docs](https://emscripten.org/docs/porting/pthreads.html#special-considerations).

<h2>File I/O</h2>

When running natively, SLProject uses the file system to load and store data. In the browser environment, the type of storage depends on the asset type.

<ul>
<li>
<b>Images</b> are downloaded from a remote server (\ref SLIOReaderFetch). To store images, we open a popup in the browser that displays the image and contains a download link (\ref SLIOWriterBrowserPopup).
</li>
<li>
<b>Models and fonts</b> are downloaded from a remote server (\ref SLIOReaderFetch).
</li>
<li>
<b>Shader source files</b> are downloaded from a remote server (\ref SLIOReaderFetch).
</li>
<li>
<b>Generated shaders</b> are stored to and loaded from memory (\ref SLIOWriterMemory and \ref SLIOReaderMemory)
</li>
<li>
<b>Config files</b> are usually stored to and loaded from [browser local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) (\ref SLIOReaderLocalStorage and SLIOWriterLocalStorage). Static config files (e.g. calibrations) are downloaded from a remote server (\ref SLIOReaderFetch).
</li>
</ul>
- **Images** are downloaded from a remote server (`SLIOReaderFetch`). To store images, we open a popup in the browser that displays the image and contains a download link (`SLIOWriterBrowserPopup`).
- **Models and fonts** are downloaded from a remote server (`SLIOReaderFetch`).
- **Shader source files** are downloaded from a remote server (`SLIOReaderFetch`).
- **Generated shaders** are stored to and loaded from memory (`SLIOWriterMemory` and `SLIOReaderMemory`)
- **Config files** are usually stored to and loaded from [browser local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) (`SLIOReaderLocalStorage` and `SLIOWriterLocalStorage`).
Static config files (e.g. calibrations) are downloaded from a remote server (`SLIOReaderFetch`).

0 comments on commit 802af2a

Please sign in to comment.