Skip to content

Commit 07b86f6

Browse files
committed
Enable run video games using WebAssembly
To play sound effects and musics, we would use software synthesizer, here I choose Timidity. When doom and quake target are specified, the Timidity related data are downloaded and extracted. As previous dicussion in the issue forum, the web workers are not reaped after return, thus adding pthread_join to reap them. As I tested, the sound effects and musics are only playable using the emcc version 3.1.51 which will fetch specific SDL2_mixer library, thus warning the user if the emcc version is not 3.1.51. Embed all ELF and their dependencies into single wasm when building with emcc. Consequently, a ELF executable menu bar can be introduced to select and run desired ELF programs. Since we are running the wasm program inside web browser, we have to yield to the web browser in some interval, so emscripten_set_main_loop_arg provided by emscripten comes to help. To utilize this function, we have to update the rv_step function signature. In order to switch running ELF programs, we have to cancel the browser main loop using emscripten_cancel_main_loop and do some clean up to recollect memory. _indirect_rv_halt is introduced to halt the RISC-V instance without exposing the RISC-V instance. assets directory will be used to store some required static web files. assets/html/index.html is a default main page of the ported wasm rv32emu. In order to switch running ELF programs, we have to delay some time for finishing emscripten_cancel_main_loop. I set delay to 1 second for now. TODO: more clean up have to be done, e.g., clear canvas, terminal and sound thread. Related to: #75
1 parent 928d8e7 commit 07b86f6

File tree

10 files changed

+422
-23
lines changed

10 files changed

+422
-23
lines changed

Makefile

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -193,17 +193,31 @@ OBJS := \
193193
OBJS := $(addprefix $(OUT)/, $(OBJS))
194194
deps := $(OBJS:%.o=%.o.d)
195195

196-
EXPORTED_FUNCS := _main
196+
include mk/external.mk
197+
198+
deps_emcc :=
199+
ASSETS := assets
200+
WEB_JS_RESOURCES := $(ASSETS)/js
201+
EXPORTED_FUNCS := _main,_indirect_rv_halt
197202
ifeq ("$(CC_IS_EMCC)", "1")
198203
CFLAGS_emcc += -sINITIAL_MEMORY=2GB \
199204
-sALLOW_MEMORY_GROWTH \
200205
-s"EXPORTED_FUNCTIONS=$(EXPORTED_FUNCS)" \
201-
--embed-file build \
206+
-sSTACK_SIZE=4MB \
207+
-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency \
208+
--embed-file build@/ \
209+
--embed-file build/timidity@/etc/timidity \
202210
-DMEM_SIZE=0x40000000 \
211+
-DCYCLE_PER_STEP=2000000 \
212+
--pre-js $(WEB_JS_RESOURCES)/pre.js \
213+
-O3 \
203214
-w
215+
216+
# used to download all dependencies of elf executable and bundle into single wasm
217+
deps_emcc += $(DOOM_DATA) $(QUAKE_DATA) $(TIMIDITY_DATA)
204218
endif
205219

206-
$(OUT)/%.o: src/%.c
220+
$(OUT)/%.o: src/%.c $(deps_emcc)
207221
$(VECHO) " CC\t$@\n"
208222
$(Q)$(CC) -o $@ $(CFLAGS) $(CFLAGS_emcc) -c -MMD -MF $@.d $<
209223

@@ -259,22 +273,36 @@ misalign: $(BIN)
259273
$(PRINTF) "Failed.\n"; \
260274
fi
261275

262-
include mk/external.mk
263-
264276
# Non-trivial demonstration programs
265277
ifeq ($(call has, SDL), 1)
266-
doom: $(BIN) $(DOOM_DATA)
267-
(cd $(OUT); ../$(BIN) doom.elf)
278+
doom_action := (cd $(OUT); ../$(BIN) doom.elf)
279+
ifeq ("$(CC_IS_EMCC)", "1")
280+
# TODO: check Chrome or Firefox is available and serve python httpd and open the web page
281+
# TODO: serve and open a web page, show warning if environment not support pthread runtime
282+
doom_action :=
283+
endif
284+
doom_deps += $(DOOM_DATA) $(BIN)
285+
doom: $(doom_deps)
286+
$(doom_action)
287+
268288
ifeq ($(call has, EXT_F), 1)
269-
quake: $(BIN) $(QUAKE_DATA)
270-
(cd $(OUT); ../$(BIN) quake.elf)
289+
quake_action := (cd $(OUT); ../$(BIN) quake.elf)
290+
ifeq ("$(CC_IS_EMCC)", "1")
291+
# TODO: check Chrome or Firefox is available and serve python httpd and open the web page
292+
# TODO: serve and open a web page, show warning if environment not support pthread runtime
293+
quake_action :=
294+
endif
295+
quake_deps += $(QUAKE_DATA) $(BIN)
296+
quake: $(quake_deps)
297+
$(quake_action)
271298
endif
272299
endif
273300

274301
clean:
275302
$(RM) $(BIN) $(OBJS) $(HIST_BIN) $(HIST_OBJS) $(deps) $(WEB_FILES) $(CACHE_OUT) src/rv32_jit.c
276303
distclean: clean
277304
-$(RM) $(DOOM_DATA) $(QUAKE_DATA)
305+
$(RM) -r $(TIMIDITY_DATA)
278306
$(RM) -r $(OUT)/id1
279307
$(RM) *.zip
280308
$(RM) -r $(OUT)/mini-gdbstub

assets/html/index.html

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

assets/js/pre.js

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

mk/external.mk

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,40 @@
22
# _DATA_URL : the hyperlink which points to archive.
33
# _DATA : the file to be read by specific executable.
44
# _DATA_SHA1 : the checksum of the content in _DATA
5+
# _DATA_EXTRACT : the way to extract content from compressed file
6+
# _DATA_VERIFY : the way to verify the checksum of extracted file
57

68
# Doom
79
# https://tipsmake.com/how-to-run-doom-on-raspberry-pi-without-emulator
810
DOOM_DATA_URL = http://www.doomworld.com/3ddownloads/ports/shareware_doom_iwad.zip
911
DOOM_DATA = $(OUT)/DOOM1.WAD
1012
DOOM_DATA_SHA1 = 5b2e249b9c5133ec987b3ea77596381dc0d6bc1d
13+
DOOM_DATA_EXTRACT = unzip -d $(OUT) $(notdir $($(T)_DATA_URL))
14+
DOOM_DATA_VERIFY = echo "$(strip $$($(T)_DATA_SHA1)) $$@" | $(SHA1SUM) -c
1115

1216
# Quake
1317
QUAKE_DATA_URL = https://www.libsdl.org/projects/quake/data/quakesw-1.0.6.zip
1418
QUAKE_DATA = $(OUT)/id1/pak0.pak
1519
QUAKE_DATA_SHA1 = 36b42dc7b6313fd9cabc0be8b9e9864840929735
20+
QUAKE_DATA_EXTRACT = unzip -d $(OUT) $(notdir $($(T)_DATA_URL))
21+
QUAKE_DATA_VERIFY = echo "$(strip $$($(T)_DATA_SHA1)) $$@" | $(SHA1SUM) -c
22+
23+
# Timidity software synthesizer configuration for SDL2_mixer
24+
TIMIDITY_DATA_URL = http://www.libsdl.org/projects/mixer/timidity/timidity.tar.gz
25+
TIMIDITY_DATA = $(OUT)/timidity
26+
TIMIDITY_DATA_SHA1 = cdd30736508d26968222a6414f3beabc3b7a0725
27+
TIMIDITY_DATA_EXTRACT = tar -xf $(notdir $($(T)_DATA_URL)) -C $(OUT)
28+
TIMIDITY_TMP_FILE = /tmp/timidity_sha1.txt
29+
TIMIDITY_DATA_VERIFY = echo "$(TIMIDITY_DATA_SHA1)" > $(TIMIDITY_TMP_FILE) | find $(TIMIDITY_DATA) -type f -print0 | sort -z | xargs -0 shasum | shasum | cut -f 1 -d ' '
1630

1731
define download-n-extract
1832
$($(T)_DATA):
1933
$(VECHO) " GET\t$$@\n"
2034
$(Q)curl --progress-bar -O -L -C - "$(strip $($(T)_DATA_URL))"
21-
$(Q)unzip -d $(OUT) $(notdir $($(T)_DATA_URL))
22-
$(Q)echo "$(strip $$($(T)_DATA_SHA1)) $$@" | $(SHA1SUM) -c
23-
$(Q)$(RM) $(notdir $($(T)_DATA_URL))
35+
$(Q)$($(T)_DATA_EXTRACT)
36+
$(Q)$($(T)_DATA_VERIFY)
37+
$(Q)$(RM) $(notdir $($(T)_DATA_URL)) $($(T)_TMP_FILE)
2438
endef
2539

26-
EXTERNAL_DATA = DOOM QUAKE
27-
$(foreach T,$(EXTERNAL_DATA),$(eval $(download-n-extract)))
40+
EXTERNAL_DATA = DOOM QUAKE TIMIDITY
41+
$(foreach T,$(EXTERNAL_DATA),$(eval $(download-n-extract)))

mk/toolchain.mk

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,35 @@ CC_IS_GCC :=
44
ifneq ($(shell $(CC) --version | head -n 1 | grep emcc),)
55
CC_IS_EMCC := 1
66

7+
EMCC_VERSION := $(shell $(CC) --version | head -n 1 | cut -f10 -d ' ')
8+
EMCC_MAJOR := $(shell echo $(EMCC_VERSION) | cut -f1 -d.)
9+
EMCC_MINOR := $(shell echo $(EMCC_VERSION) | cut -f2 -d.)
10+
EMCC_PATCH := $(shell echo $(EMCC_VERSION) | cut -f3 -d.)
11+
12+
# When the emcc version is not 3.1.51, the latest SDL2_mixer library is fetched by emcc and music might not be played in the web browser
13+
SDL_MUSIC_PLAY_AT_EMCC_MAJOR := 3
14+
SDL_MUSIC_PLAY_AT_EMCC_MINOR := 1
15+
SDL_MUSIC_PLAY_AT_EMCC_PATCH := 51
16+
SDL_MUSIC_CANNOT_PLAY_WARNING := Video games music might not be played. You may switch emcc to version $(SDL_MUSIC_PLAY_AT_EMCC_MAJOR).$(SDL_MUSIC_PLAY_AT_EMCC_MINOR).$(SDL_MUSIC_PLAY_AT_EMCC_PATCH)
17+
ifeq ($(shell echo $(EMCC_MAJOR)\==$(SDL_MUSIC_PLAY_AT_EMCC_MAJOR) | bc), 1)
18+
ifeq ($(shell echo $(EMCC_MINOR)\==$(SDL_MUSIC_PLAY_AT_EMCC_MINOR) | bc), 1)
19+
ifeq ($(shell echo $(EMCC_PATCH)\==$(SDL_MUSIC_PLAY_AT_EMCC_PATCH) | bc), 1)
20+
# do nothing
21+
else
22+
$(warning $(SDL_MUSIC_CANNOT_PLAY_WARNING))
23+
endif
24+
else
25+
$(warning $(SDL_MUSIC_CANNOT_PLAY_WARNING))
26+
endif
27+
else
28+
$(warning $(SDL_MUSIC_CANNOT_PLAY_WARNING))
29+
endif
30+
731
# see commit 165c1a3 of emscripten
832
MIMALLOC_SUPPORT_SINCE_MAJOR := 3
933
MIMALLOC_SUPPORT_SINCE_MINOR := 1
1034
MIMALLOC_SUPPORT_SINCE_PATCH := 50
1135
MIMALLOC_UNSUPPORTED_WARNING := mimalloc is supported after version $(MIMALLOC_SUPPORT_SINCE_MAJOR).$(MIMALLOC_SUPPORT_SINCE_MINOR).$(MIMALLOC_SUPPORT_SINCE_PATCH)
12-
EMCC_VERSION := $(shell $(CC) --version | head -n 1 | cut -f10 -d ' ')
13-
EMCC_MAJOR := $(shell echo $(EMCC_VERSION) | cut -f1 -d.)
14-
EMCC_MINOR := $(shell echo $(EMCC_VERSION) | cut -f2 -d.)
15-
EMCC_PATCH := $(shell echo $(EMCC_VERSION) | cut -f3 -d.)
1636
ifeq ($(shell echo $(EMCC_MAJOR)\>=$(MIMALLOC_SUPPORT_SINCE_MAJOR) | bc), 1)
1737
ifeq ($(shell echo $(EMCC_MINOR)\>=$(MIMALLOC_SUPPORT_SINCE_MINOR) | bc), 1)
1838
ifeq ($(shell echo $(EMCC_PATCH)\>=$(MIMALLOC_SUPPORT_SINCE_PATCH) | bc), 1)

src/emulate.c

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

13+
#ifdef __EMSCRIPTEN__
14+
#include <emscripten.h>
15+
#endif
16+
1317
#if RV32_HAS(EXT_F)
1418
#include <math.h>
1519
#include "softfloat.h"
@@ -1074,9 +1078,11 @@ static bool runtime_profiler(riscv_t *rv, block_t *block)
10741078
typedef void (*exec_block_func_t)(riscv_t *rv, uintptr_t);
10751079
#endif
10761080

1077-
void rv_step(riscv_t *rv)
1081+
void rv_step(void *arg)
10781082
{
1079-
assert(rv);
1083+
assert(arg);
1084+
riscv_t *rv = arg;
1085+
10801086
vm_attr_t *attr = PRIV(rv);
10811087
uint32_t cycles = attr->cycle_per_step;
10821088

@@ -1174,6 +1180,13 @@ void rv_step(riscv_t *rv)
11741180
#endif
11751181
prev = block;
11761182
}
1183+
1184+
#ifdef __EMSCRIPTEN__
1185+
if (rv_has_halted(rv)) {
1186+
emscripten_cancel_main_loop();
1187+
rv_delete(rv); /* clean up and reuse memory */
1188+
}
1189+
#endif
11771190
}
11781191

11791192
void ebreak_handler(riscv_t *rv)

src/main.c

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,31 @@ static void dump_test_signature(const char *prog_name)
168168
elf_delete(elf);
169169
}
170170

171+
/* CYCLE_PER_STEP shall be defined on different runtime */
172+
#ifndef CYCLE_PER_STEP
173+
#define CYCLE_PER_STEP 100
174+
#endif
171175
/* MEM_SIZE shall be defined on different runtime */
172176
#ifndef MEM_SIZE
173177
#define MEM_SIZE 0xFFFFFFFFULL /* 2^32 - 1 */
174178
#endif
175179
#define STACK_SIZE 0x1000 /* 4096 */
176180
#define ARGS_OFFSET_SIZE 0x1000 /* 4096 */
177181

182+
/* To use rv_halt function in wasm, we have to expose RISC-V instance(rv),
183+
* but we can add a layer to not expose the instance and make rv_halt
184+
* callable. A small trade-off is that declaring instance as a global
185+
* variable. rv_halt is useful when cancelling the main loop of wasm,
186+
* see rv_step in emulate.c for more detail
187+
*/
188+
riscv_t *rv;
189+
#ifdef __EMSCRIPTEN__
190+
void indirect_rv_halt()
191+
{
192+
rv_halt(rv);
193+
}
194+
#endif
195+
178196
int main(int argc, char **args)
179197
{
180198
if (argc == 1 || !parse_args(argc, args)) {
@@ -199,14 +217,14 @@ int main(int argc, char **args)
199217
.run_flag = run_flag,
200218
.profile_output_file = prof_out_file,
201219
.data.user = malloc(sizeof(vm_user_t)),
202-
.cycle_per_step = 100,
220+
.cycle_per_step = CYCLE_PER_STEP,
203221
.allow_misalign = opt_misaligned,
204222
};
205223
assert(attr.data.user);
206224
attr.data.user->elf_program = opt_prog_name;
207225

208226
/* create the RISC-V runtime */
209-
riscv_t *rv = rv_create(&attr);
227+
rv = rv_create(&attr);
210228
if (!rv) {
211229
fprintf(stderr, "Unable to create riscv emulator\n");
212230
attr.exit_code = 1;

src/riscv.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
#define STDERR_FILENO FILENO(stderr)
1919
#endif
2020

21+
#ifdef __EMSCRIPTEN__
22+
#include <emscripten.h>
23+
#endif
24+
2125
#include "elf.h"
2226
#include "mpool.h"
2327
#include "riscv.h"
@@ -315,9 +319,13 @@ void rv_run(riscv_t *rv)
315319
rv_debug(rv);
316320
#endif
317321
else {
322+
#ifdef __EMSCRIPTEN__
323+
emscripten_set_main_loop_arg(rv_step, (void *) rv, 0, 1);
324+
#else
318325
/* default main loop */
319326
for (; !rv_has_halted(rv);) /* run until the flag is done */
320327
rv_step(rv); /* step instructions */
328+
#endif
321329
}
322330

323331
if (attr->run_flag & RV_RUN_PROFILE) {

src/riscv.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ void rv_debug(riscv_t *rv);
166166
#endif
167167

168168
/* step the RISC-V emulator */
169-
void rv_step(riscv_t *rv);
169+
void rv_step(void *arg);
170170

171171
/* set the program counter of a RISC-V emulator */
172172
bool rv_set_pc(riscv_t *rv, riscv_word_t pc);

src/syscall_sdl.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,12 @@ static void play_sfx(riscv_t *rv)
708708
.volume = volume,
709709
};
710710
pthread_create(&sfx_thread, NULL, sfx_handler, &sfx);
711+
/* FIXME: In web browser runtime, web workers in thread pool do not reap
712+
* after sfx_handler return, thus we have to join them. sfx_handler does not
713+
* contain infinite loop,so do not worry to be stalled by it */
714+
#ifdef __EMSCRIPTEN__
715+
pthread_join(sfx_thread, NULL);
716+
#endif
711717
}
712718

713719
static void play_music(riscv_t *rv)
@@ -738,6 +744,12 @@ static void play_music(riscv_t *rv)
738744
.volume = volume,
739745
};
740746
pthread_create(&music_thread, NULL, music_handler, &music);
747+
/* FIXME: In web browser runtime, web workers in thread pool do not reap
748+
* after music_handler return, thus we have to join them. music_handler does
749+
* not contain infinite loop,so do not worry to be stalled by it */
750+
#ifdef __EMSCRIPTEN__
751+
pthread_join(music_thread, NULL);
752+
#endif
741753
}
742754

743755
static void stop_music(riscv_t *rv UNUSED)

0 commit comments

Comments
 (0)