rv32emu icon indicating copy to clipboard operation
rv32emu copied to clipboard

Run rv32emu inside web browser via WebAssembly translation

Open jserv opened this issue 3 years ago • 48 comments

iamlouk/riscv64-sim is written in C and capable of being translated into WebAssembly. Check its web demo. Some web-based (open source) RISC-V simulations:

The goal of this task is to expand rv32emu to the simulator, which will be used to teach computer architecture fundamentals, based on the existing RISC-V codebase. Incomplete attempt: rv32emu-wasm

jserv avatar Oct 20 '22 10:10 jserv

We might need RISC-V instruction decoder for web-based execution. See rvcodec.js: an online encoder/decoder for RISC-V instructions

jserv avatar Dec 10 '22 18:12 jserv

Recent rv32emu relies on tail-call optimization (TCO), which causes build problems with Emscripten. Thus, we have to turn it off. Apply the following changes:

diff --git a/src/common.h b/src/common.h
index 62b66bd..374bcf3 100644
--- a/src/common.h
+++ b/src/common.h
@@ -43,7 +43,7 @@
  * around this, the compiler attribute 'musttail' is used, which forces TCO
  * even without optimizations enabled.
  */
-#if defined(__has_attribute) && __has_attribute(musttail)
+#if defined(__has_attribute) && __has_attribute(musttail) && !defined(__wasm__)
 #define MUST_TAIL __attribute__((musttail))
 #else
 #define MUST_TAIL
diff --git a/src/io.c b/src/io.c
index 5d7a37d..f3de262 100644
--- a/src/io.c
+++ b/src/io.c
@@ -17,8 +17,8 @@
 #include "io.h"
 
 static uint8_t *data_memory_base;
-/* set memory size to 2^32 bytes */
-#define MEM_SIZE 0x100000000ULL
+/* set memory size to 2^31 bytes */
+#define MEM_SIZE 0x10000000ULL
 
 memory_t *memory_new()
 {

Then, build this project via emcc:

make CC=emcc ENABLE_SDL=0 ENABLE_GDBSTUB=0

build/rv32emu.wasm should be generated.

See Tail Call Extension

jserv avatar Jul 21 '23 04:07 jserv

I am going to study and solve this issue. May I know if the original assignee still working on it?

ChinYikMing avatar Dec 13 '23 05:12 ChinYikMing

I am going to study and solve this issue. May I know if the original assignee still working on it?

Go ahead!

jserv avatar Dec 13 '23 05:12 jserv

Tail call optimization(TCO) is supported by Chrome (version 112) natively, see roadmap of webassembly. If tail call optimization is enabled during the build, only Chrome can run the wasm. So, Chrome will be the main experimental runtime environment to adapt TCO for now.

ChinYikMing avatar Dec 16 '23 04:12 ChinYikMing

Tail call optimization is supported by Chrome (version 112) natively, see roadmap of webassembly. Thus, if tail call optimization is enabled during the build, only Chrome can run the wasm.

It is reasonably acceptable to restrict the WebAssembly runtime environments, given that the WebAssembly-built rv32emu is highly experimental.

jserv avatar Dec 16 '23 05:12 jserv

After the initial WebAssembly build (#293), we can proceed with the following action items:

  1. Create an HTML page, which we could potentially host on GitHub Pages, to enable the WebAssembly-built rv32emu to run sample RISC-V ELF files. We will also need a web-based console for stdout.
  2. Ensure that RV32-built video games such as Doom and Quake can run on the Chrome web browser in the form of WebAssembly.
  3. (Optional) Implement single-stepping execution, along with web-based register and internal state displays, similar to what Riples provides. This would be a valuable addition.

jserv avatar Dec 16 '23 05:12 jserv

Suppress the warning that generated when exporting functions from the same translation unit of the main function (e.g., print_usage) but the main function is not exported, see issue(fixed)

ChinYikMing avatar Dec 22 '23 01:12 ChinYikMing

  1. Create an HTML page, which we could potentially host on GitHub Pages, to enable the WebAssembly-built rv32emu to run sample RISC-V ELF files. We will also need a web-based console for stdout.

This wasm branch has attempted to support this. Try it out by following the README.

It requires some source code modifications to work:

  1. Remove static modifier from MEMIO in src/main.c. So that, the translation unit of main.c (main.o) can export the functions.
  2. Allocate required memory size using pass by value because WebAssembly's page size is 64KiB thus default memory size MEM_SIZE(2^32 - 1) can not be applied. In order to fix this, I propose that declare a mem_size parameter in state_new function and this parameter is passed to memory_new function in further. Note that rv32emu can still using MEM_SIZE outside the web.

The following are the main changes:

  • [ ] src/main.c
--- a/src/main.c
+++ b/src/main.c
@@ -44,7 +44,7 @@ static bool opt_misaligned = false;

 #define MEMIO(op) on_mem_##op
 #define IO_HANDLER_IMPL(type, op, RW)                                     \
-    static IIF(RW)(                                                       \
+    IIF(RW)(                                                       \
         /* W */ void MEMIO(op)(riscv_word_t addr, riscv_##type##_t data), \
         /* R */ riscv_##type##_t MEMIO(op)(riscv_word_t addr))            \
     {                                                                     \
@@ -222,7 +222,7 @@ int main(int argc, char **args)
         .allow_misalign = opt_misaligned,
     };

-    state_t *state = state_new();
+    state_t *state = state_new(MEM_SIZE);

     /* find the start of the heap */
     const struct Elf32_Sym *end;
  • [ ] src/riscv.h
--- a/src/riscv.h
+++ b/src/riscv.h
@@ -222,7 +222,7 @@ typedef struct {
 } state_t;

 /* create a state */
-state_t *state_new(void);
+state_t *state_new(uint32_t mem_size);

 /* delete a state */
 void state_delete(state_t *s);
  • [ ] src/riscv.c
--- a/src/riscv.c
+++ b/src/riscv.c
@@ -294,11 +294,11 @@ void rv_reset(riscv_t *rv, riscv_word_t pc, int argc, char **args)
     rv->halt = false;
 }

-state_t *state_new(void)
+state_t *state_new(uint32_t mem_size)
 {
     state_t *s = malloc(sizeof(state_t));
     assert(s);
-    s->mem = memory_new();
+    s->mem = memory_new(mem_size);
     s->break_addr = 0;

     s->fd_map = map_init(int, FILE *, map_cmp_int);
  • [ ] src/io.h
--- a/src/io.h
+++ b/src/io.h
@@ -8,16 +8,23 @@
 #include <stdint.h>
 #include <string.h>

-/* Directly map a memory with a size of 2^32 bytes. All memory read/write
- * operations can access this memory through the memory subsystem.
+/*
+ * set memory size to 2^32 - 1 bytes
+ *
+ * The memory size is set to 2^32 - 1 bytes in order to make this emulator
+ * portable for both 32-bit and 64-bit platforms. As a result, it can access
+ * any segment of the memory on either platform. Furthermore, it is safe
+ * because most of the test cases' data memory usage will not exceed this
+ * memory size.
  */
+#define MEM_SIZE 0xFFFFFFFFULL

 typedef struct {
     uint8_t *mem_base;
     uint64_t mem_size;
 } memory_t;

-memory_t *memory_new(void);
+memory_t *memory_new(uint32_t mem_size);
 void memory_delete(memory_t *m);

 /* read a C-style string from memory */
  • [ ] src/io.c
--- a/src/io.c
+++ b/src/io.c
@@ -17,44 +17,33 @@

 static uint8_t *data_memory_base;

-/*
- * set memory size to 2^32 - 1 bytes
- *
- * The memory size is set to 2^32 - 1 bytes in order to make this emulator
- * portable for both 32-bit and 64-bit platforms. As a result, it can access
- * any segment of the memory on either platform. Furthermore, it is safe
- * because most of the test cases' data memory usage will not exceed this
- * memory size.
- */
-#define MEM_SIZE 0xFFFFFFFFULL
-
-memory_t *memory_new(void)
+memory_t *memory_new(uint32_t mem_size)
 {
     memory_t *mem = malloc(sizeof(memory_t));
     assert(mem);
 #if HAVE_MMAP
-    data_memory_base = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE,
+    data_memory_base = mmap(NULL, mem_size, PROT_READ | PROT_WRITE,
                             MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
     if (data_memory_base == MAP_FAILED) {
         free(mem);
         return NULL;
     }
 #else
-    data_memory_base = malloc(MEM_SIZE);
+    data_memory_base = malloc(mem_size);
     if (!data_memory_base) {
         free(mem);
         return NULL;

ChinYikMing avatar Dec 23 '23 22:12 ChinYikMing

Is it feasible for me to host the relevant porting files (such as HTML, JS files, etc.) and then rv32emu submodule for them? So that, rv32emu can adapt to different porting implementation.

I am not sure if an HTML submodule may be hosted on a github page.

ChinYikMing avatar Dec 23 '23 22:12 ChinYikMing

Is it feasible for me to host the relevant porting files (such as HTML, JS files, etc.) and then rv32emu submodule for them? So that, rv32emu can adapt to different porting implementation.

I think so. Take Ripes for example. There are two directories resources and appdir preserving resources files.

I am not sure if an HTML submodule may be hosted on a github page.

We can follow the approach of rv32emu-bench.

jserv avatar Dec 24 '23 02:12 jserv

This wasm branch has attempted to support this. Try it out by following the README. It requires some source code modifications to work:

  1. Remove static modifier from MEMIO in src/main.c. So that, the translation unit of main.c (main.o) can export the functions.

Reflecting on the feedback from the wasm branch, it seems appropriate to refine the APIs in the src/riscv.h header. The focus should be on achieving a higher level of abstraction by integrating detailed components like memory, state, and I/O into a more unified API structure. For inspiration on creating a clean and elegant C API for executing RISC-V programs, consider examining the approach used by libriscv.

Last year, when developing semu, I improved the APIs to make them cleaner. The riscv.h header was designed to abstract the virtual machine's internals, avoiding explicit cross-references among several implementation-focused headers. Now, I believe it is time to apply a similar approach to the rv32emu project. Without this evolution, rv32emu continually faces challenges in adapting to various runtime environments.

If you are in agreement with the statement I've made, let's proceed by gradually refining our existing public headers. The aim is to enhance encapsulation while ensuring there are no disruptions to functionality.

jserv avatar Dec 24 '23 02:12 jserv

Is it feasible for me to host the relevant porting files (such as HTML, JS files, etc.) and then rv32emu submodule for them? So that, rv32emu can adapt to different porting implementation.

I think so. Take Ripes for example. There are two directories resources and appdir preserving resources files.

I am not sure if an HTML submodule may be hosted on a github page.

We can follow the approach of rv32emu-bench.

To sum up, the Github page deployment will be a repository with two submodules: "wasm and corresponding glue js"(generated by emcc and hosted in sysprog21 organization) and "wasm porting implemention"(hosted in my personal repository). Some CI might be needed to automate the build and store actions.

ChinYikMing avatar Dec 24 '23 06:12 ChinYikMing

Reflecting on the feedback from the wasm branch, it seems appropriate to refine the APIs in the src/riscv.h header. The focus should be on achieving a higher level of abstraction by integrating detailed components like memory, state, and I/O into a more unified API structure. For inspiration on creating a clean and elegant C API for executing RISC-V programs, consider examining the approach used by libriscv.

Last year, when developing semu, I improved the APIs to make them cleaner. The riscv.h header was designed to abstract the virtual machine's internals, avoiding explicit cross-references among several implementation-focused headers. Now, I believe it is time to apply a similar approach to the rv32emu project. Without this evolution, rv32emu continually faces challenges in adapting to various runtime environments.

If you are in agreement with the statement I've made, let's proceed by gradually refining our existing public headers. The aim is to enhance encapsulation while ensuring there are no disruptions to functionality.

I agree with that the the RISC-V APIs should have higher level of abstraction. Consequently, they could be easily ported to multiple platforms (e.g., desktop) or maintained. I will try to investigate the abstraction of APIs from libriscv and semu for further discussion.

ChinYikMing avatar Dec 24 '23 06:12 ChinYikMing

To sum up, the Github page deployment will be a repository with two submodules: "wasm and corresponding glue js"(generated by emcc and hosted in sysprog21 organization) and "wasm porting implemention"(hosted in my personal repository). Some CI might be needed to automate the build and store actions.

Consider awtk as another example - it is a window widget set written in C that also supports a web-based runtime through awtk-web. In the future, with the integration of semu's developments into rv32emu, we should be capable of hosting an online emulator akin to RVVM.

jserv avatar Dec 24 '23 06:12 jserv

This wasm branch has attempted to support this. Try it out by following the README.

Test environment: macOS 14.2.1 + Apple Silicon (M1)

I have installed emcc via Homebrew.

$ emcc --version | head -1
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.50-git

After executing make CC=emcc ENABLE_SDL=0 ENABLE_GDBSTUB=0, I got the following messages:

wasm-ld: error: unknown argument: -no_warn_duplicate_libraries

The issue was caused by my error in toolchain configuration detection, and I will fix it later.

After quick fix, both Drystone and CoreMark run inside Chrome browser. Current rv32emu/wasm performance:

  • Dhrystone: 82 DMIPS
  • CoreMark: 134 iter/s

RVVM:

  • CoreMark: 366 iter/s

jserv avatar Dec 25 '23 03:12 jserv

Test environment: macOS 14.2.1 + Apple Silicon (M1) [...] both Drystone and CoreMark run inside Chrome browser. Current rv32emu/wasm performance:

  • Dhrystone: 82 DMIPS
  • CoreMark: 134 iter/s RVVM:
  • CoreMark: 366 iter/s

WebAssembly performance might be tuned after the basic simulation(e.g., sample ELF and Doom & Quake video game) are done.

ChinYikMing avatar Dec 25 '23 13:12 ChinYikMing

In order to satisfy the browser's event model, the wasm branch supported cooperative multitasking mode. Try it out at demo here(only runable in Chrome). Run "ieee754.elf" to observe a notable change. The line prints out while the program is executing rather than flushing the entire buffer once the run is complete(in other words, update DOM as soon as possible). This can increase the interaction between the user and program.

This change requires some modification of source code since it leverages the APIs of Emscripten (e.g., __EMSCRIPTEN__ predefined macro, emscripten_set_main_loop_arg, emscripten_cancel_main_loop, see commit). Is it possible for them to be added to the main branch afterwards? Actually, I am still trying to figure out how to keep the source code unchanged.

ChinYikMing avatar Dec 26 '23 19:12 ChinYikMing

  1. Ensure that RV32-built video games such as Doom and Quake can run on the Chrome web browser in the form of WebAssembly.

Shooting games like Doom and Quake may now be rendered on a web browser, however the frame rate is low(see demo). I will investigate more after sound system is set up successfully because the failure of initialization of sound system will make rv32emu exit early. see issue.

ChinYikMing avatar Jan 04 '24 17:01 ChinYikMing

Tail call optimization(TCO) is supported by Chrome (version 112) natively, see roadmap of webassembly. If tail call optimization is enabled during the build, only Chrome can run the wasm. So, Chrome will be the main experimental runtime environment to adapt TCO for now.

Firefox version 121 supports TCO, so both Chrome and Firefox can be the experimental runtime environment now.

ChinYikMing avatar Jan 06 '24 08:01 ChinYikMing

  1. Ensure that RV32-built video games such as Doom and Quake can run on the Chrome web browser in the form of WebAssembly.

Shooting games like Doom and Quake may now be rendered on a web browser, however the frame rate is low(see demo). I will investigate more after sound system is set up successfully because the failure of initialization of sound system will make rv32emu exit early. see issue.

As I tweaking the cycle_per_step to 2000000, Doom and Quake run more smoothly than before. Sound system is set up successfully by linking the ported SDL library correctly. The sound are played by Timidity and the sound effect is decent in Quake but not very nice(especially on pistol sound effect) in Doom (maybe will try FluidSynth).

A small changes have to be applied to sound system calls(in "src/syscall_sdl.c"):

static void play_sfx(riscv_t *rv)
{
    ...
    pthread_create(&sfx_thread, NULL, sfx_handler, &sfx);
+#ifdef __EMSCRIPTEN__
+   pthread_join(sfx_thread, NULL);
+#endif
}

static void play_music(riscv_t *rv)
{
    ...
    pthread_create(&music_thread, NULL, music_handler, &music);
+#ifdef __EMSCRIPTEN__
+   pthread_join(music_thread, NULL);
+#endif
}

I found that emcc compiled web workers are not reused, so adding pthread_join makes them reusable. Otherwise, will get error: Tried to spawn a new thread, but the thread pool is exhausted. After applying this patch, the browser would suggest that blocking main thread(calling pthread_join) is dangerous, but it is safe actually since the sfx_handler and music_handler do not contain any blocking call.

Demo:

Note that due to the large size of the WASM file, the loading may take some time. I might rewrite the UI for noticing that the loading process (maybe a loading bar indicator). Also, please click the canvas to trigger the Web Audio API.

Next, I will focus on design, discuss and refactor riscv.[ch] and io.[ch] API for easier porting to different runtime.

ChinYikMing avatar Jan 06 '24 12:01 ChinYikMing

we could potentially host on GitHub Pages

Multithreading in Emscripten requires SharedArrayBuffer. To allow the browser to support "SharedArrayBuffer", we have to add custom response headers: "Cross-Origin-Opener-Policy" and "Cross-Origin-Embedder-Policy", see security requirements. Moreover, it is necessary for the server to have an active SSL certificate for the browser to enable "SharedArrayBuffer".

Due to the limitation for the customization of github pages response headers (maybe fixed by this blog but seems cumbersome), github pages is not a suitable place to serve multithreading applications like Quake and Doom but suitable for those sample ELF executable which does not require multithreading. Thus, I believe a better way is that having a dedicated server with an active SSL certificate and the ability to set custom response headers to serve both multithreading and non-multithreading WebAssembly applications.

ChinYikMing avatar Jan 13 '24 17:01 ChinYikMing

rv32emu public APIs might be refactored over the time, writing all exported rv32emu public APIs in Makefile(see EXPORTED_FUNCTIONS in Makefile) is not an ideal choice since keep updating Makefile to adapt new rv32emu public API is weird. Instead, we can create a file to store all exported rv32emu public APIs and make emcc link to it during compile time. It is sufficient to edit the file in order to create a new rv32emu public API. See configs of awtk-web.

ChinYikMing avatar Jan 13 '24 18:01 ChinYikMing

Ben Smith's remarkable experiment, which involves compiling and running Clang in a web browser, is detailed in his project Clang/LLD in WebAssembly. Drawing inspiration from this, we have the potential to develop WebAssembly-based versions of Clang/LLD specifically for RISC-V, along with an in-memory filesystem. This method would enable the versatile usage of these tools in a web environment. It would facilitate not only the compilation and execution of C source files but also their operation on a WebAssembly-built rv32emu.

check the file riscv-gnu-toolchain/Makefile.in and look for the target named build-llvm-newlib.

jserv avatar Jan 20 '24 17:01 jserv

Demo:

Use the latest riscv.[ch] public APIs to port rv32emu to WebAssembly and run Falling-nes for testing (performance can still be improved but it works!). See branch.

ChinYikMing avatar Mar 05 '24 16:03 ChinYikMing

#293 enable LTO to default WebAssembly build using emcc, but it fails on building emscripten-port SDL2. To be more precise, the rv32emu WebAssembly build will fail if ENABLE_SDL=1 is configured. As a result , I think we can enable LTO for CLI programs but disable it for programs that utilize SDL-related functions. (Hopefully emscripten will fix it)

Demo:

I aware this problem, so above demo programs are build with configuration ENABLE_LTO=0.

ChinYikMing avatar Mar 09 '24 16:03 ChinYikMing

I think it is time to merge some results to upstream. The plan is as follows:

  1. Enable to build rv32emu to wasm and run CLI programs using node
  2. Enable to build rv32emu to wasm and run CLI and SDL programs and start local web server to serve them using script
  3. Deploy the wasm to dedicated server

ChinYikMing avatar Mar 10 '24 16:03 ChinYikMing

#293 enable LTO to default WebAssembly build using emcc, but it fails on building emscripten-port SDL2. To be more precise, the rv32emu WebAssembly build will fail if ENABLE_SDL=1 is configured. As a result , I think we can enable LTO for CLI programs but disable it for programs that utilize SDL-related functions. (Hopefully emscripten will fix it)

Disabling LTO for WebAssembly-based builds is acceptable since, thus far, the optimizations have not shown significant impact.

jserv avatar Mar 10 '24 16:03 jserv

I think it is time to merge some results to upstream. The plan is as follows:

  1. Enable to build rv32emu to wasm and run CLI programs using node

Are there alternative methods to execute WebAssembly programs using languages like C, PHP, or Python, rather than relying on Node.js?

jserv avatar Mar 10 '24 16:03 jserv

I think it is time to merge some results to upstream. The plan is as follows:

  1. Enable to build rv32emu to wasm and run CLI programs using node

Are there alternative methods to execute WebAssembly programs using languages like C, PHP, or Python, rather than relying on Node.js?

So far as I know, there are two main ways to execute WebAssembly programs, one is JavaScript interpreter (NodeJS or Browser JS engine), second is wasm interpreter (e.g., wasmtime).

I think you want the least dependency for deployment?

ChinYikMing avatar Mar 10 '24 17:03 ChinYikMing