Skip to content

Commit 732ac1a

Browse files
committed
Enable System Emulation in Web Browsers
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 handles both standard ASCII input and escape sequences (such as arrow keys), providing 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 header to store Emscripten-related variables and function declarations 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
1 parent 56d20a2 commit 732ac1a

File tree

13 files changed

+543
-35
lines changed

13 files changed

+543
-35
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,8 @@ $(OUT)/emulate.o: CFLAGS += -foptimize-sibling-calls -fomit-frame-pointer -fno-s
286286

287287
include mk/external.mk
288288
include mk/artifact.mk
289-
include mk/wasm.mk
290289
include mk/system.mk
290+
include mk/wasm.mk
291291

292292
all: config $(BUILD_DTB) $(BUILD_DTB2C) $(BIN)
293293

assets/wasm/html/system.html

Lines changed: 315 additions & 0 deletions
Large diffs are not rendered by default.

assets/wasm/html/index.html renamed to assets/wasm/html/user.html

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
</select>
122122
<button id="runButton">Run</button>
123123

124+
<button id="toSysEmuButton">
125+
🚀 Navigate to System Emulation
126+
</button>
127+
124128
<div class="emscripten">
125129
<progress value="0" max="100" id="progress" hidden=1></progress>
126130
</div>
@@ -137,6 +141,13 @@
137141
var spinnerElement = document.getElementById('spinner');
138142
var runButton = document.getElementById("runButton");
139143
runButton.addEventListener("click", runButtonClickHandler);
144+
var toSysEmuButton = document.getElementById("toSysEmuButton");
145+
toSysEmuButton.addEventListener("click", toSysEmuButtonClickHandler);
146+
147+
function toSysEmuButtonClickHandler() {
148+
console.log("Navigate to system emulation");
149+
window.location.href = "/system"
150+
}
140151

141152
var elfDropdown = document.getElementById("elfDropdown");
142153
for (var i = 0; i < elfFiles.length; i++) {
@@ -163,11 +174,7 @@
163174
element.scrollTop = element.scrollHeight;
164175
}
165176
Module._indirect_rv_halt();
166-
/* important to add some delay for waiting cancellation of main loop before next run */
167-
/* Otherwise, get error: only one main loop can be existed */
168-
setTimeout(() => {
169-
Module['onRuntimeInitialized'](target_elf);
170-
}, 1000);
177+
Module['run_user'](target_elf);
171178
}
172179

173180
var Module = {

assets/wasm/js/pre.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

assets/wasm/js/system-pre.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
Module["noInitialRun"] = true;
2+
3+
Module["run_system"] = function (cli_param) {
4+
callMain(cli_param.split(" "));
5+
};
6+
7+
// index.html's preRun needs to access this, thus declaring as global
8+
let term;
9+
10+
Module["onRuntimeInitialized"] = function () {
11+
const input_buf_ptr = Module._get_input_buf();
12+
const input_buf_cap = Module._get_input_buf_cap();
13+
14+
term = new Terminal({
15+
cols: 120,
16+
rows: 11,
17+
});
18+
term.open(document.getElementById("terminal"));
19+
20+
term.onKey(({ key, domEvent }) => {
21+
const code = key.charCodeAt(0);
22+
let sequence;
23+
24+
switch (domEvent.key) {
25+
case "ArrowUp":
26+
// ESC [ A → "\x1B[A"
27+
sequence = "\x1B[A";
28+
break;
29+
case "ArrowDown":
30+
// ESC [ B → "\x1B[B"
31+
sequence = "\x1B[B";
32+
break;
33+
case "ArrowRight":
34+
// ESC [ C → "\x1B[C"
35+
sequence = "\x1B[C";
36+
break;
37+
case "ArrowLeft":
38+
// ESC [ D → "\x1B[D"
39+
sequence = "\x1B[D";
40+
break;
41+
// TODO: support more escape keys?
42+
default:
43+
sequence = key;
44+
break;
45+
}
46+
47+
let heap = new Uint8Array(
48+
Module.HEAPU8.buffer,
49+
input_buf_ptr,
50+
sequence.length,
51+
);
52+
53+
for (let i = 0; i < sequence.length && i < input_buf_cap; i++) {
54+
heap[i] = sequence.charCodeAt(i);
55+
}
56+
// Fill zero
57+
for (let i = sequence.length; i < input_buf_cap; i++) {
58+
heap[i] = 0;
59+
}
60+
61+
Module._set_input_buf_size(sequence.length);
62+
63+
term.scrollToBottom();
64+
});
65+
};

assets/wasm/js/user-pre.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Module["noInitialRun"] = true;
2+
3+
Module["run_user"] = function (target_elf) {
4+
if (target_elf === undefined) {
5+
console.warn("target elf executable is undefined");
6+
return;
7+
}
8+
9+
callMain([target_elf]);
10+
};

mk/wasm.mk

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ deps_emcc :=
33
ASSETS := assets/wasm
44
WEB_HTML_RESOURCES := $(ASSETS)/html
55
WEB_JS_RESOURCES := $(ASSETS)/js
6-
EXPORTED_FUNCS := _main,_indirect_rv_halt
6+
EXPORTED_FUNCS := _main,_indirect_rv_halt,_get_input_buf,_get_input_buf_cap,_set_input_buf_size
77
DEMO_DIR := demo
88
WEB_FILES := $(BIN).js \
99
$(BIN).wasm \
@@ -29,7 +29,19 @@ CFLAGS_emcc += -sINITIAL_MEMORY=2GB \
2929
-s"EXPORTED_FUNCTIONS=$(EXPORTED_FUNCS)" \
3030
-sSTACK_SIZE=4MB \
3131
-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency \
32-
--embed-file build/jit-bf.elf@/jit-bf.elf \
32+
--embed-file build/timidity@/etc/timidity \
33+
-DMEM_SIZE=0x20000000 \
34+
-DCYCLE_PER_STEP=2000000 \
35+
-O3 \
36+
-w
37+
38+
ifeq ($(call has, SYSTEM), 1)
39+
CFLAGS_emcc += --embed-file build/linux-image/Image@Image \
40+
--embed-file build/linux-image/[email protected] \
41+
--embed-file build/minimal.dtb@/minimal.dtb \
42+
--pre-js $(WEB_JS_RESOURCES)/system-pre.js
43+
else
44+
CFLAGS_emcc += --embed-file build/jit-bf.elf@/jit-bf.elf \
3345
--embed-file build/coro.elf@/coro.elf \
3446
--embed-file build/fibonacci.elf@/fibonacci.elf \
3547
--embed-file build/hello.elf@/hello.elf \
@@ -40,12 +52,9 @@ CFLAGS_emcc += -sINITIAL_MEMORY=2GB \
4052
--embed-file build/riscv32@/riscv32 \
4153
--embed-file build/DOOM1.WAD@/DOOM1.WAD \
4254
--embed-file build/id1/pak0.pak@/id1/pak0.pak \
43-
--embed-file build/timidity@/etc/timidity \
44-
-DMEM_SIZE=0x60000000 \
45-
-DCYCLE_PER_STEP=2000000 \
46-
--pre-js $(WEB_JS_RESOURCES)/pre.js \
47-
-O3 \
48-
-w
55+
--pre-js $(WEB_JS_RESOURCES)/user-pre.js
56+
endif
57+
4958

5059
$(OUT)/elf_list.js: tools/gen-elf-list-js.py
5160
$(Q)tools/gen-elf-list-js.py > $@
@@ -132,11 +141,22 @@ define cp-web-file
132141
endef
133142

134143
# WEB_FILES could be cleaned and recompiled, thus do not mix these two files into WEB_FILES
135-
STATIC_WEB_FILES := $(WEB_HTML_RESOURCES)/index.html \
136-
$(WEB_JS_RESOURCES)/coi-serviceworker.min.js
144+
STATIC_WEB_FILES := $(WEB_JS_RESOURCES)/coi-serviceworker.min.js
145+
ifeq ($(call has, SYSTEM), 1)
146+
STATIC_WEB_FILES += $(WEB_HTML_RESOURCES)/system.html
147+
else
148+
STATIC_WEB_FILES += $(WEB_HTML_RESOURCES)/user.html
149+
endif
150+
151+
start_web_deps := check-demo-dir-exist $(BIN)
152+
ifeq ($(call has, SYSTEM), 1)
153+
start_web_deps += $(BUILD_DTB) $(BUILD_DTB2C)
154+
endif
137155

138-
start-web: check-demo-dir-exist $(BIN)
156+
start-web: $(start_web_deps)
157+
$(Q)rm -f $(DEMO_DIR)/*.html
139158
$(foreach T, $(WEB_FILES), $(call cp-web-file, $(T)))
140159
$(foreach T, $(STATIC_WEB_FILES), $(call cp-web-file, $(T)))
160+
$(Q)mv $(DEMO_DIR)/*.html $(DEMO_DIR)/index.html
141161
$(Q)python3 -m http.server --bind $(DEMO_IP) $(DEMO_PORT) --directory $(DEMO_DIR)
142162
endif

src/devices/uart.c

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55

66
#include <assert.h>
77
#include <errno.h>
8+
#include <fcntl.h>
89
#include <poll.h>
910
#include <stdio.h>
1011
#include <stdlib.h>
1112
#include <string.h>
1213
#include <unistd.h>
1314

15+
#if defined(__EMSCRIPTEN__)
16+
#include "emsc.h"
17+
#endif
18+
1419
#include "uart.h"
1520
/* Emulate 8250 (plain, without loopback mode support) */
1621

@@ -33,15 +38,42 @@ void u8250_update_interrupts(u8250_state_t *uart)
3338
uart->current_intr = ilog2(uart->pending_intrs);
3439
}
3540

41+
#if defined(__EMSCRIPTEN__)
42+
#define INPUT_BUF_MAX_CAP 16
43+
static char input_buf[INPUT_BUF_MAX_CAP];
44+
static uint8_t input_buf_start = 0;
45+
uint8_t input_buf_size = 0;
46+
47+
char *get_input_buf()
48+
{
49+
return input_buf;
50+
}
51+
52+
uint8_t get_input_buf_cap()
53+
{
54+
return INPUT_BUF_MAX_CAP;
55+
}
56+
57+
void set_input_buf_size(uint8_t size)
58+
{
59+
input_buf_size = size;
60+
}
61+
#endif
62+
3663
void u8250_check_ready(u8250_state_t *uart)
3764
{
3865
if (uart->in_ready)
3966
return;
4067

68+
#if defined(__EMSCRIPTEN__)
69+
if (input_buf_size)
70+
uart->in_ready = true;
71+
#else
4172
struct pollfd pfd = {uart->in_fd, POLLIN, 0};
4273
poll(&pfd, 1, 0);
4374
if (pfd.revents & POLLIN)
4475
uart->in_ready = true;
76+
#endif
4577
}
4678

4779
static void u8250_handle_out(u8250_state_t *uart, uint8_t value)
@@ -57,12 +89,19 @@ static uint8_t u8250_handle_in(u8250_state_t *uart)
5789
if (!uart->in_ready)
5890
return value;
5991

92+
#if defined(__EMSCRIPTEN__)
93+
value = (uint8_t) input_buf[input_buf_start];
94+
input_buf_start++;
95+
if (--input_buf_size == 0)
96+
input_buf_start = 0;
97+
#else
6098
if (read(uart->in_fd, &value, 1) < 0)
6199
rv_log_error("Failed to read UART input: %s", strerror(errno));
100+
#endif
62101
uart->in_ready = false;
63-
u8250_check_ready(uart);
64102

65-
if (value == 1) { /* start of heading (Ctrl-a) */
103+
if (value == 1) { /* start of heading (Ctrl-a) */
104+
u8250_check_ready(uart);
66105
if (getchar() == 120) { /* keyboard x */
67106
rv_log_info("RISC-V emulator is destroyed");
68107
exit(EXIT_SUCCESS);

src/emsc.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* rv32emu is freely redistributable under the MIT License. See the file
3+
* "LICENSE" for information on usage and redistribution of this file.
4+
*/
5+
6+
#pragma once
7+
8+
#include <emscripten.h>
9+
10+
void indirect_rv_halt();
11+
12+
#if RV32_HAS(SYSTEM) && !RV32_HAS(ELF_LOADER)
13+
extern uint8_t input_buf_size;
14+
15+
char *get_input_buf();
16+
uint8_t get_input_buf_cap();
17+
void set_input_buf_size(uint8_t size);
18+
#endif

src/emulate.c

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@
1010
#include <stdlib.h>
1111
#include <string.h>
1212

13-
#ifdef __EMSCRIPTEN__
14-
#include <emscripten.h>
15-
#endif
16-
1713
#if RV32_HAS(EXT_F)
1814
#include <math.h>
1915
#include "softfp.h"
@@ -23,6 +19,19 @@
2319
extern struct target_ops gdbstub_ops;
2420
#endif
2521

22+
#if defined(__EMSCRIPTEN__)
23+
#include "emsc.h"
24+
#if RV32_HAS(SYSTEM)
25+
EM_JS(void, enable_run_button, (), {
26+
document.getElementById('runSysButton').disabled = false;
27+
});
28+
#else
29+
EM_JS(void, enable_run_button, (), {
30+
document.getElementById('runButton').disabled = false;
31+
});
32+
#endif
33+
#endif
34+
2635
#include "decode.h"
2736
#include "mpool.h"
2837
#include "riscv.h"
@@ -1009,6 +1018,9 @@ static void rv_check_interrupt(riscv_t *rv)
10091018
if (peripheral_update_ctr-- == 0) {
10101019
peripheral_update_ctr = 64;
10111020

1021+
#if defined(__EMSCRIPTEN__)
1022+
escape_seq:
1023+
#endif
10121024
u8250_check_ready(PRIV(rv)->uart);
10131025
if (PRIV(rv)->uart->in_ready)
10141026
emu_update_uart_interrupts(rv);
@@ -1031,6 +1043,11 @@ static void rv_check_interrupt(riscv_t *rv)
10311043
break;
10321044
case (SUPERVISOR_EXTERNAL_INTR & 0xf):
10331045
SET_CAUSE_AND_TVAL_THEN_TRAP(rv, SUPERVISOR_EXTERNAL_INTR, 0);
1046+
#if defined(__EMSCRIPTEN__)
1047+
/* escape sequence has more than 1 byte */
1048+
if (input_buf_size)
1049+
goto escape_seq;
1050+
#endif
10341051
break;
10351052
default:
10361053
break;
@@ -1174,6 +1191,7 @@ void rv_step(void *arg)
11741191
emscripten_cancel_main_loop();
11751192
rv_delete(rv); /* clean up and reuse memory */
11761193
rv_log_info("RISC-V emulator is destroyed");
1194+
enable_run_button();
11771195
}
11781196
#endif
11791197
}

0 commit comments

Comments
 (0)