Skip to content

Commit ec4005d

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. Now, it is time to bridge all these components together. Leverage xterm.js as the frontend terminal in the web browser and bridge it with the backend VM shell through custom buffer management. 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. The SDL backend is also supported. After booting the guest Linux system, can run doom-riscv, quake, or smolnes to experience graphical applications. Can press Ctrl+C to exit SDL-based programs, or use their built-in exit commands. To reduce the size of the WASM file, the build is now separated into user and system targets. As a result, there are two HTML files: - user.html - system.html and two corresponding preload JavaScript: - user-pre.js - system-pre.js Tested on latest Chrome, Firefox and Safari.
1 parent 58e975e commit ec4005d

File tree

10 files changed

+487
-25
lines changed

10 files changed

+487
-25
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: 310 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@
166166
/* important to add some delay for waiting cancellation of main loop before next run */
167167
/* Otherwise, get error: only one main loop can be existed */
168168
setTimeout(() => {
169-
Module['onRuntimeInitialized'](target_elf);
169+
Module['run_user'](target_elf);
170170
}, 1000);
171171
}
172172

assets/wasm/js/pre.js

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

assets/wasm/js/system-pre.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 (target_elf) {
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+
Module._set_input_buf_avail(true);
63+
64+
term.scrollToBottom();
65+
});
66+
};

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_avail,_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/rootfs.cpio@rootfs.cpio \
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: 53 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+
#ifdef __EMSCRIPTEN__
16+
#include <emscripten.h>
17+
#endif
18+
1419
#include "uart.h"
1520
/* Emulate 8250 (plain, without loopback mode support) */
1621

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

41+
#if defined(__EMSCRIPTEN__)
42+
43+
#define INPUT_BUF_MAX_CAP 128
44+
45+
static char input_buf[INPUT_BUF_MAX_CAP];
46+
static uint8_t input_buf_size;
47+
static uint8_t input_buf_start = 0;
48+
bool is_input_buf_avail = false;
49+
50+
char *get_input_buf()
51+
{
52+
return input_buf;
53+
}
54+
55+
uint8_t get_input_buf_cap()
56+
{
57+
return INPUT_BUF_MAX_CAP;
58+
}
59+
60+
void set_input_buf_avail(bool flag)
61+
{
62+
is_input_buf_avail = flag;
63+
}
64+
65+
void set_input_buf_size(uint8_t size)
66+
{
67+
input_buf_size = size;
68+
}
69+
70+
#endif
71+
3672
void u8250_check_ready(u8250_state_t *uart)
3773
{
3874
if (uart->in_ready)
3975
return;
4076

77+
#if defined(__EMSCRIPTEN__)
78+
if (is_input_buf_avail)
79+
uart->in_ready = true;
80+
#else
4181
struct pollfd pfd = {uart->in_fd, POLLIN, 0};
4282
poll(&pfd, 1, 0);
4383
if (pfd.revents & POLLIN)
4484
uart->in_ready = true;
85+
#endif
4586
}
4687

4788
static void u8250_handle_out(u8250_state_t *uart, uint8_t value)
@@ -57,12 +98,22 @@ static uint8_t u8250_handle_in(u8250_state_t *uart)
5798
if (!uart->in_ready)
5899
return value;
59100

101+
#if defined(__EMSCRIPTEN__)
102+
value = (uint8_t)
103+
input_buf[input_buf_start];
104+
input_buf_start++;
105+
if (--input_buf_size == 0) {
106+
input_buf_start = 0;
107+
set_input_buf_avail(false);
108+
}
109+
#else
60110
if (read(uart->in_fd, &value, 1) < 0)
61111
rv_log_error("Failed to read UART input: %s", strerror(errno));
112+
#endif
62113
uart->in_ready = false;
63-
u8250_check_ready(uart);
64114

65-
if (value == 1) { /* start of heading (Ctrl-a) */
115+
if (value == 1) { /* start of heading (Ctrl-a) */
116+
u8250_check_ready(uart);
66117
if (getchar() == 120) { /* keyboard x */
67118
rv_log_info("RISC-V emulator is destroyed");
68119
exit(EXIT_SUCCESS);

src/emulate.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,12 +1003,19 @@ static bool rv_has_plic_trap(riscv_t *rv)
10031003
(rv->csr_sip & rv->csr_sie));
10041004
}
10051005

1006+
#if defined(__EMSCRIPTEN__)
1007+
extern bool is_input_buf_avail;
1008+
#endif
1009+
10061010
static void rv_check_interrupt(riscv_t *rv)
10071011
{
10081012
vm_attr_t *attr = PRIV(rv);
10091013
if (peripheral_update_ctr-- == 0) {
10101014
peripheral_update_ctr = 64;
10111015

1016+
#if defined(__EMSCRIPTEN__)
1017+
escape:
1018+
#endif
10121019
u8250_check_ready(PRIV(rv)->uart);
10131020
if (PRIV(rv)->uart->in_ready)
10141021
emu_update_uart_interrupts(rv);
@@ -1031,6 +1038,11 @@ static void rv_check_interrupt(riscv_t *rv)
10311038
break;
10321039
case (SUPERVISOR_EXTERNAL_INTR & 0xf):
10331040
SET_CAUSE_AND_TVAL_THEN_TRAP(rv, SUPERVISOR_EXTERNAL_INTR, 0);
1041+
#if defined(__EMSCRIPTEN__)
1042+
/* escape character has more than 1 byte */
1043+
if (is_input_buf_avail)
1044+
goto escape;
1045+
#endif
10341046
break;
10351047
default:
10361048
break;

src/riscv.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ static void map_file(char **ram_loc, const char *name)
234234
struct stat st;
235235
fstat(fd, &st);
236236

237-
#if HAVE_MMAP
237+
// EMSCRIPTEN: We don't currently support location hints for the address of the mapping
238+
// https://github.com/emscripten-core/emscripten/blob/52bc455316b2f44d3a94104776a335a5861ad73b/system/lib/libc/emscripten_mmap.c#L105
239+
#if HAVE_MMAP && !defined(__EMSCRIPTEN__)
238240
/* remap to a memory region */
239241
*ram_loc = mmap(*ram_loc, st.st_size, PROT_READ | PROT_WRITE,
240242
MAP_FIXED | MAP_PRIVATE, fd, 0);

0 commit comments

Comments
 (0)