From 57950f9e22ed4d20e3237fcd1daed41a5ea20ac0 Mon Sep 17 00:00:00 2001 From: ChinYikMing Date: Sat, 5 Jul 2025 23:41:59 +0800 Subject: [PATCH 1/4] Enable System Emulation in Web Browsers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-space emulation has been supported and deployable in WebAssembly since #389, but system emulation was not yet available. With #508, system emulation was introduced, and later #551 added support for trap-and-emulate of guest Linux SDL syscalls, enabling offloading to the host’s SDL backend. This commit bridges these components together to enable full system emulation in the browser. xterm.js is leveraged as the frontend terminal, bridged with the backend VM shell through a custom buffer management mechanism. This mechanism provides a shell experience in the browser that closely resembles a real terminal. To handle terminal input without blocking the browser’s cooperative multitasking model, the original approach of mapping the read() system call to window.prompt() is avoided. Blocking behavior would freeze the browser’s event loop and degrade responsiveness. Instead, input from xterm.js is stored in a shared input buffer. The rv32emu periodically checks this buffer when handling external interrupts, and if input is available, it is read and consumed by the guest OS shell. The SDL graphic and sound backend are also supported. After booting the guest Linux system, users can run graphical applications such as doom-riscv, quake, or smolnes. These programs can be exited using Ctrl+C or their built-in exit funtionality. To reduce the size of the WebAssembly build and for the sake of the modularity, the project is now separated into user and system targets. As a result, two dedicated HTML files and corresponding preload JavaScript files are introduced: - user.html with user-pre.js - system.html with system-pre.js Navigation buttons are added to the index pages of both user and system demos, allowing users to switch easily between the two modes. Note that these navigation buttons are only functional when both user and system demos are deployed together, otherwise, the target pages may not be reachable. To improve usability, logic is implemented to disable and enable the "Run" button at appropriate times, preventing accidental re-execution when the process is already running. Additional improvements: - Ensure xterm.js uses \r\n instead of \n when logging to correctly move the cursor to the beginning of the line. - Add a new src/emsc.h,c to store Emscripten-related variables and function declarations and implementations for better management. This implementation has been tested on the latest versions of Chrome, Firefox, and Safari. To serve user space emulation index page: $ make start-web CC=emcc ENABLE_SDL=1 -j8 To serve system emulation index page: $ make start-web CC=emcc ENABLE_SYSTEM=1 ENABLE_SDL=1 INITRD_SIZE=32 -j8 --- Makefile | 36 ++- assets/wasm/html/system.html | 315 +++++++++++++++++++++ assets/wasm/html/{index.html => user.html} | 17 +- assets/wasm/js/pre.js | 9 - assets/wasm/js/system-pre.js | 40 +++ assets/wasm/js/user-pre.js | 10 + mk/wasm.mk | 42 ++- src/devices/uart.c | 42 ++- src/emsc.c | 32 +++ src/emsc.h | 28 ++ src/emulate.c | 17 +- src/log.c | 4 + src/main.c | 8 + src/riscv.c | 10 +- 14 files changed, 562 insertions(+), 48 deletions(-) create mode 100644 assets/wasm/html/system.html rename assets/wasm/html/{index.html => user.html} (98%) delete mode 100644 assets/wasm/js/pre.js create mode 100644 assets/wasm/js/system-pre.js create mode 100644 assets/wasm/js/user-pre.js create mode 100644 src/emsc.c create mode 100644 src/emsc.h diff --git a/Makefile b/Makefile index 41c8324bb..02f248301 100644 --- a/Makefile +++ b/Makefile @@ -286,25 +286,33 @@ $(OUT)/emulate.o: CFLAGS += -foptimize-sibling-calls -fomit-frame-pointer -fno-s include mk/external.mk include mk/artifact.mk -include mk/wasm.mk include mk/system.mk +include mk/wasm.mk all: config $(BUILD_DTB) $(BUILD_DTB2C) $(BIN) OBJS := \ - map.o \ - utils.o \ - decode.o \ - io.o \ - syscall.o \ - emulate.o \ - riscv.o \ - log.o \ - elf.o \ - cache.o \ - mpool.o \ - $(OBJS_EXT) \ - main.o + map.o \ + utils.o \ + decode.o \ + io.o \ + syscall.o + +# emsc.o should prior to emulate.o, otherwise wasm-ld fails to link +ifeq ($(CC_IS_EMCC), 1) +OBJS += emsc.o +endif + +OBJS += \ + emulate.o \ + emsc.o \ + riscv.o \ + log.o \ + elf.o \ + cache.o \ + mpool.o \ + $(OBJS_EXT) \ + main.o OBJS := $(addprefix $(OUT)/, $(OBJS)) deps += $(OBJS:%.o=%.o.d) # mk/system.mk includes prior this line, so declare deps at there diff --git a/assets/wasm/html/system.html b/assets/wasm/html/system.html new file mode 100644 index 000000000..3c9710a0d --- /dev/null +++ b/assets/wasm/html/system.html @@ -0,0 +1,315 @@ + + + + + + Emscripten-Generated Code + + + + + + + +
+
Downloading...
+ + + Resize canvas + Lock/hide mouse + pointer     + + + + + + + + +
+

Experience graphics and sounds(remember to turn on your speaker) by running doom-riscv, quake, smolnes.

+
+ +
+ +
+ +
+ +
+ +
+ + + + + + diff --git a/assets/wasm/html/index.html b/assets/wasm/html/user.html similarity index 98% rename from assets/wasm/html/index.html rename to assets/wasm/html/user.html index 5beae7e63..e2fdf6951 100644 --- a/assets/wasm/html/index.html +++ b/assets/wasm/html/user.html @@ -121,6 +121,10 @@ + +
@@ -137,6 +141,13 @@ var spinnerElement = document.getElementById('spinner'); var runButton = document.getElementById("runButton"); runButton.addEventListener("click", runButtonClickHandler); + var toSysEmuButton = document.getElementById("toSysEmuButton"); + toSysEmuButton.addEventListener("click", toSysEmuButtonClickHandler); + + function toSysEmuButtonClickHandler() { + console.log("Navigate to system emulation"); + window.location.href = "./system" + } var elfDropdown = document.getElementById("elfDropdown"); for (var i = 0; i < elfFiles.length; i++) { @@ -163,11 +174,7 @@ element.scrollTop = element.scrollHeight; } Module._indirect_rv_halt(); - /* important to add some delay for waiting cancellation of main loop before next run */ - /* Otherwise, get error: only one main loop can be existed */ - setTimeout(() => { - Module['onRuntimeInitialized'](target_elf); - }, 1000); + Module['run_user'](target_elf); } var Module = { diff --git a/assets/wasm/js/pre.js b/assets/wasm/js/pre.js deleted file mode 100644 index c541967ea..000000000 --- a/assets/wasm/js/pre.js +++ /dev/null @@ -1,9 +0,0 @@ -Module['noInitialRun'] = true; -Module['onRuntimeInitialized'] = function(target_elf) { - if(target_elf === undefined){ - console.warn("target elf executable is undefined"); - return; - } - - callMain([target_elf]); -}; diff --git a/assets/wasm/js/system-pre.js b/assets/wasm/js/system-pre.js new file mode 100644 index 000000000..5fca6f083 --- /dev/null +++ b/assets/wasm/js/system-pre.js @@ -0,0 +1,40 @@ +Module["noInitialRun"] = true; + +Module["run_system"] = function (cli_param) { + callMain(cli_param.split(" ")); +}; + +// index.html's preRun needs to access this, thus declaring as global +let term; + +Module["onRuntimeInitialized"] = function () { + const input_buf_ptr = Module._get_input_buf(); + const input_buf_cap = Module._get_input_buf_cap(); + + term = new Terminal({ + cols: 120, + rows: 11, + }); + term.open(document.getElementById("terminal")); + + term.onKey(({ key, domEvent }) => { + + let heap = new Uint8Array( + Module.HEAPU8.buffer, + input_buf_ptr, + key.length, + ); + + for (let i = 0; i < key.length && i < input_buf_cap; i++) { + heap[i] = key.charCodeAt(i); + } + // Fill zero + for (let i = key.length; i < input_buf_cap; i++) { + heap[i] = 0; + } + + Module._set_input_buf_size(key.length); + + term.scrollToBottom(); + }); +}; diff --git a/assets/wasm/js/user-pre.js b/assets/wasm/js/user-pre.js new file mode 100644 index 000000000..f0925222f --- /dev/null +++ b/assets/wasm/js/user-pre.js @@ -0,0 +1,10 @@ +Module["noInitialRun"] = true; + +Module["run_user"] = function (target_elf) { + if (target_elf === undefined) { + console.warn("target elf executable is undefined"); + return; + } + + callMain([target_elf]); +}; diff --git a/mk/wasm.mk b/mk/wasm.mk index cf4d2c9e9..89cd04fc4 100644 --- a/mk/wasm.mk +++ b/mk/wasm.mk @@ -3,7 +3,7 @@ deps_emcc := ASSETS := assets/wasm WEB_HTML_RESOURCES := $(ASSETS)/html WEB_JS_RESOURCES := $(ASSETS)/js -EXPORTED_FUNCS := _main,_indirect_rv_halt +EXPORTED_FUNCS := _main,_indirect_rv_halt,_get_input_buf,_get_input_buf_cap,_set_input_buf_size DEMO_DIR := demo WEB_FILES := $(BIN).js \ $(BIN).wasm \ @@ -29,7 +29,19 @@ CFLAGS_emcc += -sINITIAL_MEMORY=2GB \ -s"EXPORTED_FUNCTIONS=$(EXPORTED_FUNCS)" \ -sSTACK_SIZE=4MB \ -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency \ - --embed-file build/jit-bf.elf@/jit-bf.elf \ + --embed-file build/timidity@/etc/timidity \ + -DMEM_SIZE=0x20000000 \ + -DCYCLE_PER_STEP=2000000 \ + -O3 \ + -w + +ifeq ($(call has, SYSTEM), 1) +CFLAGS_emcc += --embed-file build/linux-image/Image@Image \ + --embed-file build/linux-image/rootfs.cpio@rootfs.cpio \ + --embed-file build/minimal.dtb@/minimal.dtb \ + --pre-js $(WEB_JS_RESOURCES)/system-pre.js +else +CFLAGS_emcc += --embed-file build/jit-bf.elf@/jit-bf.elf \ --embed-file build/coro.elf@/coro.elf \ --embed-file build/fibonacci.elf@/fibonacci.elf \ --embed-file build/hello.elf@/hello.elf \ @@ -40,12 +52,9 @@ CFLAGS_emcc += -sINITIAL_MEMORY=2GB \ --embed-file build/riscv32@/riscv32 \ --embed-file build/DOOM1.WAD@/DOOM1.WAD \ --embed-file build/id1/pak0.pak@/id1/pak0.pak \ - --embed-file build/timidity@/etc/timidity \ - -DMEM_SIZE=0x60000000 \ - -DCYCLE_PER_STEP=2000000 \ - --pre-js $(WEB_JS_RESOURCES)/pre.js \ - -O3 \ - -w + --pre-js $(WEB_JS_RESOURCES)/user-pre.js +endif + $(OUT)/elf_list.js: tools/gen-elf-list-js.py $(Q)tools/gen-elf-list-js.py > $@ @@ -132,11 +141,22 @@ define cp-web-file endef # WEB_FILES could be cleaned and recompiled, thus do not mix these two files into WEB_FILES -STATIC_WEB_FILES := $(WEB_HTML_RESOURCES)/index.html \ - $(WEB_JS_RESOURCES)/coi-serviceworker.min.js +STATIC_WEB_FILES := $(WEB_JS_RESOURCES)/coi-serviceworker.min.js +ifeq ($(call has, SYSTEM), 1) +STATIC_WEB_FILES += $(WEB_HTML_RESOURCES)/system.html +else +STATIC_WEB_FILES += $(WEB_HTML_RESOURCES)/user.html +endif + +start_web_deps := check-demo-dir-exist $(BIN) +ifeq ($(call has, SYSTEM), 1) +start_web_deps += $(BUILD_DTB) $(BUILD_DTB2C) +endif -start-web: check-demo-dir-exist $(BIN) +start-web: $(start_web_deps) + $(Q)rm -f $(DEMO_DIR)/*.html $(foreach T, $(WEB_FILES), $(call cp-web-file, $(T))) $(foreach T, $(STATIC_WEB_FILES), $(call cp-web-file, $(T))) + $(Q)mv $(DEMO_DIR)/*.html $(DEMO_DIR)/index.html $(Q)python3 -m http.server --bind $(DEMO_IP) $(DEMO_PORT) --directory $(DEMO_DIR) endif diff --git a/src/devices/uart.c b/src/devices/uart.c index f554165f9..ba285db66 100644 --- a/src/devices/uart.c +++ b/src/devices/uart.c @@ -11,6 +11,10 @@ #include #include +#if defined(__EMSCRIPTEN__) +#include "emsc.h" +#endif + #include "uart.h" /* Emulate 8250 (plain, without loopback mode support) */ @@ -33,15 +37,42 @@ void u8250_update_interrupts(u8250_state_t *uart) uart->current_intr = ilog2(uart->pending_intrs); } +#if defined(__EMSCRIPTEN__) +#define INPUT_BUF_MAX_CAP 16 +static char input_buf[INPUT_BUF_MAX_CAP]; +static uint8_t input_buf_start = 0; +uint8_t input_buf_size = 0; + +char *get_input_buf() +{ + return input_buf; +} + +uint8_t get_input_buf_cap() +{ + return INPUT_BUF_MAX_CAP; +} + +void set_input_buf_size(uint8_t size) +{ + input_buf_size = size; +} +#endif + void u8250_check_ready(u8250_state_t *uart) { if (uart->in_ready) return; +#if defined(__EMSCRIPTEN__) + if (input_buf_size) + uart->in_ready = true; +#else struct pollfd pfd = {uart->in_fd, POLLIN, 0}; poll(&pfd, 1, 0); if (pfd.revents & POLLIN) uart->in_ready = true; +#endif } static void u8250_handle_out(u8250_state_t *uart, uint8_t value) @@ -57,12 +88,19 @@ static uint8_t u8250_handle_in(u8250_state_t *uart) if (!uart->in_ready) return value; +#if defined(__EMSCRIPTEN__) + value = (uint8_t) input_buf[input_buf_start]; + input_buf_start++; + if (--input_buf_size == 0) + input_buf_start = 0; +#else if (read(uart->in_fd, &value, 1) < 0) rv_log_error("Failed to read UART input: %s", strerror(errno)); +#endif uart->in_ready = false; - u8250_check_ready(uart); - if (value == 1) { /* start of heading (Ctrl-a) */ + if (value == 1) { /* start of heading (Ctrl-a) */ + u8250_check_ready(uart); if (getchar() == 120) { /* keyboard x */ rv_log_info("RISC-V emulator is destroyed"); exit(EXIT_SUCCESS); diff --git a/src/emsc.c b/src/emsc.c new file mode 100644 index 000000000..6c7a486c1 --- /dev/null +++ b/src/emsc.c @@ -0,0 +1,32 @@ +/* + * rv32emu is freely redistributable under the MIT License. See the file + * "LICENSE" for information on usage and redistribution of this file. + */ + +#include "emsc.h" + +#if defined(__EMSCRIPTEN__) +#if RV32_HAS(SYSTEM) +EM_JS(void, enable_run_button, (), { + document.getElementById('runSysButton').disabled = false; +}); +EM_JS(void, disable_run_button, (), { + document.getElementById('runSysButton').disabled = true; +}); +#else +EM_JS(void, enable_run_button, (), { + document.getElementById('runButton').disabled = false; +}); +EM_JS(void, disable_run_button, (), { + document.getElementById('runButton').disabled = true; +}); +#endif + +#if RV32_HAS(SYSTEM) && !RV32_HAS(ELF_LOADER) +extern uint8_t input_buf_size; + +char *get_input_buf(); +uint8_t get_input_buf_cap(); +void set_input_buf_size(uint8_t size); +#endif +#endif diff --git a/src/emsc.h b/src/emsc.h new file mode 100644 index 000000000..bf58c1092 --- /dev/null +++ b/src/emsc.h @@ -0,0 +1,28 @@ +/* + * rv32emu is freely redistributable under the MIT License. See the file + * "LICENSE" for information on usage and redistribution of this file. + */ + +#pragma once + +#if defined(__EMSCRIPTEN__) +#include + +/* To terminate the main loop of CPU */ +void indirect_rv_halt(); + +#if RV32_HAS(SYSTEM) && !RV32_HAS(ELF_LOADER) +/* To bridge xterm.js terminal with UART */ +extern uint8_t input_buf_size; + +char *get_input_buf(); +uint8_t get_input_buf_cap(); +void set_input_buf_size(uint8_t size); +#endif + +/* To enable/disable run button in index.html to prevent re-execution + * when the process is already running. + */ +void enable_run_button(); +void disable_run_button(); +#endif diff --git a/src/emulate.c b/src/emulate.c index ae8835e9e..251dd563f 100644 --- a/src/emulate.c +++ b/src/emulate.c @@ -10,10 +10,6 @@ #include #include -#ifdef __EMSCRIPTEN__ -#include -#endif - #if RV32_HAS(EXT_F) #include #include "softfp.h" @@ -23,6 +19,10 @@ extern struct target_ops gdbstub_ops; #endif +#if defined(__EMSCRIPTEN__) +#include "emsc.h" +#endif + #include "decode.h" #include "mpool.h" #include "riscv.h" @@ -1009,6 +1009,9 @@ static void rv_check_interrupt(riscv_t *rv) if (peripheral_update_ctr-- == 0) { peripheral_update_ctr = 64; +#if defined(__EMSCRIPTEN__) + escape_seq: +#endif u8250_check_ready(PRIV(rv)->uart); if (PRIV(rv)->uart->in_ready) emu_update_uart_interrupts(rv); @@ -1031,6 +1034,11 @@ static void rv_check_interrupt(riscv_t *rv) break; case (SUPERVISOR_EXTERNAL_INTR & 0xf): SET_CAUSE_AND_TVAL_THEN_TRAP(rv, SUPERVISOR_EXTERNAL_INTR, 0); +#if defined(__EMSCRIPTEN__) + /* escape sequence has more than 1 byte */ + if (input_buf_size) + goto escape_seq; +#endif break; default: break; @@ -1174,6 +1182,7 @@ void rv_step(void *arg) emscripten_cancel_main_loop(); rv_delete(rv); /* clean up and reuse memory */ rv_log_info("RISC-V emulator is destroyed"); + enable_run_button(); } #endif } diff --git a/src/log.c b/src/log.c index ad2746c6e..f534245e1 100644 --- a/src/log.c +++ b/src/log.c @@ -52,7 +52,11 @@ static void stdout_callback(log_event_t *ev) ev->file, ev->line); #endif /* RV32_HAS(LOG_COLOR) */ vfprintf(ev->udata, ev->fmt, ev->ap); +#if defined(__EMSCRIPTEN__) + fprintf(ev->udata, "\r\n"); +#else fprintf(ev->udata, "\n"); +#endif fflush(ev->udata); } diff --git a/src/main.c b/src/main.c index 8e366519c..b47996c3d 100644 --- a/src/main.c +++ b/src/main.c @@ -11,6 +11,10 @@ #include #include +#if defined(__EMSCRIPTEN__) +#include "emsc.h" +#endif + #include "elf.h" #include "riscv.h" #include "utils.h" @@ -286,6 +290,10 @@ int main(int argc, char **args) } rv_log_info("RISC-V emulator is created and ready to run"); +#if defined(__EMSCRIPTEN__) + disable_run_button(); +#endif + rv_run(rv); /* dump registers as JSON */ diff --git a/src/riscv.c b/src/riscv.c index 2a1e59f72..66c302835 100644 --- a/src/riscv.c +++ b/src/riscv.c @@ -27,8 +27,8 @@ #define STDERR_FILENO FILENO(stderr) #endif -#ifdef __EMSCRIPTEN__ -#include +#if defined(__EMSCRIPTEN__) +#include "emsc.h" #endif #include "elf.h" @@ -234,7 +234,11 @@ static void map_file(char **ram_loc, const char *name) struct stat st; fstat(fd, &st); -#if HAVE_MMAP +/* EMSCRIPTEN: We don't currently support location hints for the address of the + * mapping */ +/* https://github.com/emscripten-core/emscripten/blob/52bc455316b2f44d3a94104776a335a5861ad73b/system/lib/libc/emscripten_mmap.c#L105 + */ +#if HAVE_MMAP && !defined(__EMSCRIPTEN__) /* remap to a memory region */ *ram_loc = mmap(*ram_loc, st.st_size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_PRIVATE, fd, 0); From 41d371a9a337c9279d8e01ac17745ed1718d3c0f Mon Sep 17 00:00:00 2001 From: ChinYikMing Date: Sun, 6 Jul 2025 00:01:56 +0800 Subject: [PATCH 2/4] CI: Update for deploying user-wasm and system-wasm This commit updates the CI to deploy both user and system WebAssembly demos to the rv32emu-demo repository, resulting in the following file structure: . |-- coi-serviceworker.min.js |-- elf_list.js |-- index.html |-- rv32emu.js |-- rv32emu.wasm |-- rv32emu.worker.js |-- system |-- coi-serviceworker.min.js |-- index.html |-- rv32emu.js |-- rv32emu.wasm |-- rv32emu.worker.js The top-level files serve the user-space demo, while the system/ subdirectory hosts the system emulation demo. This structure allows both pages to coexist and be navigated independently in the same deployment. Improvements: - The release artifacts in the rv32emu-prebuilt repository include either a user-space executable (e.g., RISC-V ELF binaries) or a Linux image for system emulation. To distinguish between these two types of releases and trigger only the necessary deployment workflow, two separate dispatch event types are introduced: - deploy_user_wasm for user-space emulation WebAssembly deployment. - deploy_system_wasm for system emulation WebAssembly deployment. - Add needs and always() to ensure proper sequencing and execution of dependent jobs when both targets are deployed. - Add src/emsc.c as a newly monitored file. - Change the source of the shareware Doom artifact: Downloading directly from the original site often results in 403 Forbidden errors on GitHub runners recently. The artifact is now hosted in our own repository (rv32emu-prebuilt) for more reliable access. Error: Resolving www.doomworld.com (www.doomworld.com)... 172.67.171.63, 104.21.29.17, 2606:4700:3037::ac43:ab3f, ... Connecting to www.doomworld.com (www.doomworld.com)|172.67.171.63|:443... connected. HTTP request sent, awaiting response... 403 Forbidden --- .github/workflows/deploy-wasm.yml | 134 +++++++++++++++++++++++++----- .github/workflows/main.yml | 16 +--- 2 files changed, 116 insertions(+), 34 deletions(-) diff --git a/.github/workflows/deploy-wasm.yml b/.github/workflows/deploy-wasm.yml index 682a71b92..603be4d72 100644 --- a/.github/workflows/deploy-wasm.yml +++ b/.github/workflows/deploy-wasm.yml @@ -10,13 +10,108 @@ on: branches: - master repository_dispatch: # listening to rv32emu-prebuilt events - types: [deploy_wasm] + types: [deploy_user_wasm, deploy_system_wasm] jobs: - wasm-deploy: + wasm-system-deploy: if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || - github.event_name == 'repository_dispatch' + github.event_name == 'repository_dispatch' && github.event.action == 'deploy_system_wasm' + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + - name: install-dependencies + run: | + sudo apt-get update -q=2 + sudo apt-get install -q=2 device-tree-compiler + - name: Verify if the JS or HTML files has been modified + id: changed-files + uses: tj-actions/changed-files@v46 + with: + files: | + assets/wasm/html/system.html + assets/wasm/js/system-pre.js + src/emsc.c + # Files below may have a potential performance impact (reference from benchmark.yml) + src/devices/*.c + src/system.c + src/riscv.c + src/decode.c + src/emulate.c + src/rv32_template.c + src/rv32_constopt.c + - name: install emcc + if: ${{ steps.changed-files.outputs.any_modified == 'true' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_system_wasm') }} + run: | + git clone https://github.com/emscripten-core/emsdk -b 3.1.51 + cd emsdk + ./emsdk install latest + ./emsdk activate latest + source ./emsdk_env.sh + echo "$PATH" >> $GITHUB_PATH + shell: bash + - name: fetch artifact + run: | + make artifact + # get from rv32emu-prebuilt + wget -O build/shareware_doom_iwad.zip "https://raw.githubusercontent.com/sysprog21/rv32emu-prebuilt/doom-artifact/shareware_doom_iwad.zip" + unzip -d build/ build/shareware_doom_iwad.zip + - name: build with emcc and move application files to /tmp + if: ${{ steps.changed-files.outputs.any_modified == 'true' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_system_wasm') }} + run: | + make CC=emcc ENABLE_SYSTEM=1 ENABLE_SDL=1 INITRD_SIZE=32 -j + mkdir /tmp/rv32emu-system-demo + mv assets/wasm/html/system.html /tmp/rv32emu-system-demo/index.html + mv assets/wasm/js/coi-serviceworker.min.js /tmp/rv32emu-system-demo + mv build/rv32emu.js /tmp/rv32emu-system-demo + mv build/rv32emu.wasm /tmp/rv32emu-system-demo + mv build/rv32emu.worker.js /tmp/rv32emu-system-demo + ls -al /tmp/rv32emu-system-demo + - name: Check out the rv32emu-system-demo repo + if: ${{ steps.changed-files.outputs.any_modified == 'true' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_system_wasm') }} + uses: actions/checkout@v4 + with: + persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token. + repository: sysprog21/rv32emu-demo + - name: Create local changes + if: ${{ steps.changed-files.outputs.any_modified == 'true' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_system_wasm') }} + run: | + mkdir -p system + mv /tmp/rv32emu-system-demo/index.html ./system + mv /tmp/rv32emu-system-demo/coi-serviceworker.min.js ./system + mv /tmp/rv32emu-system-demo/rv32emu.js ./system + mv /tmp/rv32emu-system-demo/rv32emu.wasm ./system + mv /tmp/rv32emu-system-demo/rv32emu.worker.js ./system + - name: Commit files + if: ${{ steps.changed-files.outputs.any_modified == 'true' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_system_wasm') }} + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add system/ + git commit -m "Add changes to system emulation" + - name: Push changes + if: ${{ steps.changed-files.outputs.any_modified == 'true' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_system_wasm') }} + uses: ad-m/github-push-action@master + with: + repository: sysprog21/rv32emu-demo + github_token: ${{ secrets.RV32EMU_DEMO_TOKEN }} + branch: main + wasm-user-deploy: + needs: wasm-system-deploy # run jobs sequentially since two jobs operate on same reposity: rv32emu-demo + if: always() # ensures wasm-user-deploy runs regardless of the outcome or condition of wasm-system-deploy runs-on: ubuntu-latest steps: - name: Check out the repo @@ -26,10 +121,11 @@ jobs: uses: tj-actions/changed-files@v46 with: files: | - assets/wasm/html/index.html - assets/wasm/js/pre.js + assets/wasm/html/user.html + assets/wasm/js/user-pre.js build/*.elf tools/gen-elf-list-js.py + src/emsc.c # Files below may have a potential performance impact (reference from benchmark.yml) src/riscv.c src/decode.c @@ -39,12 +135,10 @@ jobs: - name: install emcc if: ${{ steps.changed-files.outputs.any_modified == 'true' || github.event_name == 'workflow_dispatch' || - github.event_name == 'repository_dispatch' }} + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_user_wasm') }} run: | - git clone https://github.com/emscripten-core/emsdk.git + git clone https://github.com/emscripten-core/emsdk -b 3.1.51 cd emsdk - git pull - git checkout 3.1.51 ./emsdk install latest ./emsdk activate latest source ./emsdk_env.sh @@ -53,21 +147,17 @@ jobs: - name: fetch artifact run: | make artifact - # Hack Cloudflare 403 Forbidden on GitHub Runner for Doom artifact download - wget --header="User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0" \ - --header="Referer: https://www.doomworld.com/" \ - --header="Accept-Language: en-US,en;q=0.9" \ - -O build/shareware_doom_iwad.zip \ - "https://www.doomworld.com/3ddownloads/ports/shareware_doom_iwad.zip" + # get from rv32emu-prebuilt + wget -O build/shareware_doom_iwad.zip "https://raw.githubusercontent.com/sysprog21/rv32emu-prebuilt/doom-artifact/shareware_doom_iwad.zip" unzip -d build/ build/shareware_doom_iwad.zip - name: build with emcc and move application files to /tmp if: ${{ steps.changed-files.outputs.any_modified == 'true' || github.event_name == 'workflow_dispatch' || - github.event_name == 'repository_dispatch' }} + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_user_wasm') }} run: | make CC=emcc ENABLE_SDL=1 mkdir /tmp/rv32emu-demo - mv assets/wasm/html/index.html /tmp/rv32emu-demo + mv assets/wasm/html/user.html /tmp/rv32emu-demo/index.html mv assets/wasm/js/coi-serviceworker.min.js /tmp/rv32emu-demo mv build/elf_list.js /tmp/rv32emu-demo mv build/rv32emu.js /tmp/rv32emu-demo @@ -77,7 +167,7 @@ jobs: - name: Check out the rv32emu-demo repo if: ${{ steps.changed-files.outputs.any_modified == 'true' || github.event_name == 'workflow_dispatch' || - github.event_name == 'repository_dispatch' }} + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_user_wasm') }} uses: actions/checkout@v4 with: persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token. @@ -85,7 +175,7 @@ jobs: - name: Create local changes if: ${{ steps.changed-files.outputs.any_modified == 'true' || github.event_name == 'workflow_dispatch' || - github.event_name == 'repository_dispatch' }} + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_user_wasm') }} run: | mv /tmp/rv32emu-demo/index.html . mv /tmp/rv32emu-demo/coi-serviceworker.min.js . @@ -96,16 +186,16 @@ jobs: - name: Commit files if: ${{ steps.changed-files.outputs.any_modified == 'true' || github.event_name == 'workflow_dispatch' || - github.event_name == 'repository_dispatch' }} + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_user_wasm') }} run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add --all - git commit -m "Add changes" + git commit -m "Add changes to user emulation" - name: Push changes if: ${{ steps.changed-files.outputs.any_modified == 'true' || github.event_name == 'workflow_dispatch' || - github.event_name == 'repository_dispatch' }} + (github.event_name == 'repository_dispatch' && github.event.action == 'deploy_user_wasm') }} uses: ad-m/github-push-action@master with: repository: sysprog21/rv32emu-demo diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 899fb4705..743c1447b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -80,12 +80,8 @@ jobs: make artifact make ENABLE_SYSTEM=1 artifact make ENABLE_ARCH_TEST=1 artifact - # Hack Cloudflare 403 Forbidden on GitHub Runner for Doom artifact download - wget --header="User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0" \ - --header="Referer: https://www.doomworld.com/" \ - --header="Accept-Language: en-US,en;q=0.9" \ - -O build/shareware_doom_iwad.zip \ - "https://www.doomworld.com/3ddownloads/ports/shareware_doom_iwad.zip" + # get from rv32emu-prebuilt + wget -O build/shareware_doom_iwad.zip "https://raw.githubusercontent.com/sysprog21/rv32emu-prebuilt/doom-artifact/shareware_doom_iwad.zip" unzip -d build/ build/shareware_doom_iwad.zip if: ${{ always() }} - name: default build using emcc @@ -394,12 +390,8 @@ jobs: | head -n 1 \ | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') make LATEST_RELEASE=$LATEST_RELEASE ENABLE_ARCH_TEST=1 artifact - # Hack Cloudflare 403 Forbidden on GitHub Runner for Doom artifact download - wget --header="User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15" \ - --header="Referer: https://www.doomworld.com/" \ - --header="Accept-Language: en-US,en;q=0.9" \ - -O build/shareware_doom_iwad.zip \ - "https://www.doomworld.com/3ddownloads/ports/shareware_doom_iwad.zip" + # get from rv32emu-prebuilt + wget -O build/shareware_doom_iwad.zip "https://raw.githubusercontent.com/sysprog21/rv32emu-prebuilt/doom-artifact/shareware_doom_iwad.zip" unzip -d build/ build/shareware_doom_iwad.zip if: ${{ always() }} - name: default build using emcc From 06bae745647f12b11b39b499ba7bc07b3e3d91e7 Mon Sep 17 00:00:00 2001 From: ChinYikMing Date: Sun, 6 Jul 2025 00:59:51 +0800 Subject: [PATCH 3/4] Fetch all artifacts before generating ELF list Without this, the ELF list may be generated incorrectly because the build/riscv32 ELF executable has not been fetched yet. --- mk/wasm.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mk/wasm.mk b/mk/wasm.mk index 89cd04fc4..21c0d2d60 100644 --- a/mk/wasm.mk +++ b/mk/wasm.mk @@ -56,7 +56,7 @@ CFLAGS_emcc += --embed-file build/jit-bf.elf@/jit-bf.elf \ endif -$(OUT)/elf_list.js: tools/gen-elf-list-js.py +$(OUT)/elf_list.js: artifact tools/gen-elf-list-js.py $(Q)tools/gen-elf-list-js.py > $@ # used to download all dependencies of elf executable and bundle into single wasm From a5afe4dae2caad120cf732f1d046d59ebbfc3d69 Mon Sep 17 00:00:00 2001 From: ChinYikMing Date: Sun, 6 Jul 2025 13:41:31 +0800 Subject: [PATCH 4/4] Update README To reflect the support for the system emulation in the web browsers. --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 43028be79..845f70503 100644 --- a/README.md +++ b/README.md @@ -378,16 +378,25 @@ $ source ~/emsdk/emsdk_env.sh Change the Emscripten SDK environment path if necessary. At this point, you can build and start a web server service to serve WebAssembly by running: +- user space emulation: ```shell -$ make CC=emcc start-web +$ make CC=emcc start-web -j8 +``` +- system emulation: +```shell +$ make CC=emcc start-web ENABLE_SYSTEM=1 INITRD_SIZE=32 -j8 ``` You would see the server's IP:PORT in your terminal. Copy and paste it to the browsers and you just access the index page of `rv32emu`. -You would see a dropdown menu which you can use to select the ELF executable. Select one and -click the Run button to run it. +You would see a dropdown menu which you can use to select the ELF executable for user space emulation, select one and +click the 'Run' button to run it. For system emulation, click the 'Run Linux' button to boot Linux. + +Alternatively, you may want to view a hosted `rv32emu` since building takes some time. +- [user space emulation demo page](https://sysprog21.github.io/rv32emu-demo/) +- [system emulation demo page](https://sysprog21.github.io/rv32emu-demo/system) -Alternatively, you may want to view a hosted `rv32emu` [demo page](https://sysprog21.github.io/rv32emu-demo/) since building takes some time. +Both pages can be easily switched using the navigation button. ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.