From 07b86f60aa9f811691cdf54e9fcb3ddef7bfc04d Mon Sep 17 00:00:00 2001 From: ChinYikMing Date: Tue, 26 Mar 2024 01:05:03 +0800 Subject: [PATCH] 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 --- Makefile | 46 +++++-- assets/html/index.html | 277 +++++++++++++++++++++++++++++++++++++++++ assets/js/pre.js | 9 ++ mk/external.mk | 24 +++- mk/toolchain.mk | 28 ++++- src/emulate.c | 17 ++- src/main.c | 22 +++- src/riscv.c | 8 ++ src/riscv.h | 2 +- src/syscall_sdl.c | 12 ++ 10 files changed, 422 insertions(+), 23 deletions(-) create mode 100644 assets/html/index.html create mode 100644 assets/js/pre.js diff --git a/Makefile b/Makefile index 23a272781..9b31c794c 100644 --- a/Makefile +++ b/Makefile @@ -193,17 +193,31 @@ OBJS := \ OBJS := $(addprefix $(OUT)/, $(OBJS)) deps := $(OBJS:%.o=%.o.d) -EXPORTED_FUNCS := _main +include mk/external.mk + +deps_emcc := +ASSETS := assets +WEB_JS_RESOURCES := $(ASSETS)/js +EXPORTED_FUNCS := _main,_indirect_rv_halt ifeq ("$(CC_IS_EMCC)", "1") CFLAGS_emcc += -sINITIAL_MEMORY=2GB \ -sALLOW_MEMORY_GROWTH \ -s"EXPORTED_FUNCTIONS=$(EXPORTED_FUNCS)" \ - --embed-file build \ + -sSTACK_SIZE=4MB \ + -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency \ + --embed-file build@/ \ + --embed-file build/timidity@/etc/timidity \ -DMEM_SIZE=0x40000000 \ + -DCYCLE_PER_STEP=2000000 \ + --pre-js $(WEB_JS_RESOURCES)/pre.js \ + -O3 \ -w + +# used to download all dependencies of elf executable and bundle into single wasm +deps_emcc += $(DOOM_DATA) $(QUAKE_DATA) $(TIMIDITY_DATA) endif -$(OUT)/%.o: src/%.c +$(OUT)/%.o: src/%.c $(deps_emcc) $(VECHO) " CC\t$@\n" $(Q)$(CC) -o $@ $(CFLAGS) $(CFLAGS_emcc) -c -MMD -MF $@.d $< @@ -259,15 +273,28 @@ misalign: $(BIN) $(PRINTF) "Failed.\n"; \ fi -include mk/external.mk - # Non-trivial demonstration programs ifeq ($(call has, SDL), 1) -doom: $(BIN) $(DOOM_DATA) - (cd $(OUT); ../$(BIN) doom.elf) +doom_action := (cd $(OUT); ../$(BIN) doom.elf) +ifeq ("$(CC_IS_EMCC)", "1") +# TODO: check Chrome or Firefox is available and serve python httpd and open the web page +# TODO: serve and open a web page, show warning if environment not support pthread runtime +doom_action := +endif +doom_deps += $(DOOM_DATA) $(BIN) +doom: $(doom_deps) + $(doom_action) + ifeq ($(call has, EXT_F), 1) -quake: $(BIN) $(QUAKE_DATA) - (cd $(OUT); ../$(BIN) quake.elf) +quake_action := (cd $(OUT); ../$(BIN) quake.elf) +ifeq ("$(CC_IS_EMCC)", "1") +# TODO: check Chrome or Firefox is available and serve python httpd and open the web page +# TODO: serve and open a web page, show warning if environment not support pthread runtime +quake_action := +endif +quake_deps += $(QUAKE_DATA) $(BIN) +quake: $(quake_deps) + $(quake_action) endif endif @@ -275,6 +302,7 @@ clean: $(RM) $(BIN) $(OBJS) $(HIST_BIN) $(HIST_OBJS) $(deps) $(WEB_FILES) $(CACHE_OUT) src/rv32_jit.c distclean: clean -$(RM) $(DOOM_DATA) $(QUAKE_DATA) + $(RM) -r $(TIMIDITY_DATA) $(RM) -r $(OUT)/id1 $(RM) *.zip $(RM) -r $(OUT)/mini-gdbstub diff --git a/assets/html/index.html b/assets/html/index.html new file mode 100644 index 000000000..90b428632 --- /dev/null +++ b/assets/html/index.html @@ -0,0 +1,277 @@ + + + + + + Emscripten-Generated Code + + + + + +
+
Downloading...
+ + + Resize canvas + Lock/hide mouse pointer     + + + + + + + +
+ +
+ +
+ +
+ + + + + + diff --git a/assets/js/pre.js b/assets/js/pre.js new file mode 100644 index 000000000..c541967ea --- /dev/null +++ b/assets/js/pre.js @@ -0,0 +1,9 @@ +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/mk/external.mk b/mk/external.mk index 3e941303d..6a051c0ad 100644 --- a/mk/external.mk +++ b/mk/external.mk @@ -2,26 +2,40 @@ # _DATA_URL : the hyperlink which points to archive. # _DATA : the file to be read by specific executable. # _DATA_SHA1 : the checksum of the content in _DATA +# _DATA_EXTRACT : the way to extract content from compressed file +# _DATA_VERIFY : the way to verify the checksum of extracted file # Doom # https://tipsmake.com/how-to-run-doom-on-raspberry-pi-without-emulator DOOM_DATA_URL = http://www.doomworld.com/3ddownloads/ports/shareware_doom_iwad.zip DOOM_DATA = $(OUT)/DOOM1.WAD DOOM_DATA_SHA1 = 5b2e249b9c5133ec987b3ea77596381dc0d6bc1d +DOOM_DATA_EXTRACT = unzip -d $(OUT) $(notdir $($(T)_DATA_URL)) +DOOM_DATA_VERIFY = echo "$(strip $$($(T)_DATA_SHA1)) $$@" | $(SHA1SUM) -c # Quake QUAKE_DATA_URL = https://www.libsdl.org/projects/quake/data/quakesw-1.0.6.zip QUAKE_DATA = $(OUT)/id1/pak0.pak QUAKE_DATA_SHA1 = 36b42dc7b6313fd9cabc0be8b9e9864840929735 +QUAKE_DATA_EXTRACT = unzip -d $(OUT) $(notdir $($(T)_DATA_URL)) +QUAKE_DATA_VERIFY = echo "$(strip $$($(T)_DATA_SHA1)) $$@" | $(SHA1SUM) -c + +# Timidity software synthesizer configuration for SDL2_mixer +TIMIDITY_DATA_URL = http://www.libsdl.org/projects/mixer/timidity/timidity.tar.gz +TIMIDITY_DATA = $(OUT)/timidity +TIMIDITY_DATA_SHA1 = cdd30736508d26968222a6414f3beabc3b7a0725 +TIMIDITY_DATA_EXTRACT = tar -xf $(notdir $($(T)_DATA_URL)) -C $(OUT) +TIMIDITY_TMP_FILE = /tmp/timidity_sha1.txt +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 ' ' define download-n-extract $($(T)_DATA): $(VECHO) " GET\t$$@\n" $(Q)curl --progress-bar -O -L -C - "$(strip $($(T)_DATA_URL))" - $(Q)unzip -d $(OUT) $(notdir $($(T)_DATA_URL)) - $(Q)echo "$(strip $$($(T)_DATA_SHA1)) $$@" | $(SHA1SUM) -c - $(Q)$(RM) $(notdir $($(T)_DATA_URL)) + $(Q)$($(T)_DATA_EXTRACT) + $(Q)$($(T)_DATA_VERIFY) + $(Q)$(RM) $(notdir $($(T)_DATA_URL)) $($(T)_TMP_FILE) endef -EXTERNAL_DATA = DOOM QUAKE -$(foreach T,$(EXTERNAL_DATA),$(eval $(download-n-extract))) +EXTERNAL_DATA = DOOM QUAKE TIMIDITY +$(foreach T,$(EXTERNAL_DATA),$(eval $(download-n-extract))) diff --git a/mk/toolchain.mk b/mk/toolchain.mk index 814497f71..cfc7f6023 100644 --- a/mk/toolchain.mk +++ b/mk/toolchain.mk @@ -4,15 +4,35 @@ CC_IS_GCC := ifneq ($(shell $(CC) --version | head -n 1 | grep emcc),) CC_IS_EMCC := 1 + EMCC_VERSION := $(shell $(CC) --version | head -n 1 | cut -f10 -d ' ') + EMCC_MAJOR := $(shell echo $(EMCC_VERSION) | cut -f1 -d.) + EMCC_MINOR := $(shell echo $(EMCC_VERSION) | cut -f2 -d.) + EMCC_PATCH := $(shell echo $(EMCC_VERSION) | cut -f3 -d.) + + # 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 + SDL_MUSIC_PLAY_AT_EMCC_MAJOR := 3 + SDL_MUSIC_PLAY_AT_EMCC_MINOR := 1 + SDL_MUSIC_PLAY_AT_EMCC_PATCH := 51 + 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) + ifeq ($(shell echo $(EMCC_MAJOR)\==$(SDL_MUSIC_PLAY_AT_EMCC_MAJOR) | bc), 1) + ifeq ($(shell echo $(EMCC_MINOR)\==$(SDL_MUSIC_PLAY_AT_EMCC_MINOR) | bc), 1) + ifeq ($(shell echo $(EMCC_PATCH)\==$(SDL_MUSIC_PLAY_AT_EMCC_PATCH) | bc), 1) + # do nothing + else + $(warning $(SDL_MUSIC_CANNOT_PLAY_WARNING)) + endif + else + $(warning $(SDL_MUSIC_CANNOT_PLAY_WARNING)) + endif + else + $(warning $(SDL_MUSIC_CANNOT_PLAY_WARNING)) + endif + # see commit 165c1a3 of emscripten MIMALLOC_SUPPORT_SINCE_MAJOR := 3 MIMALLOC_SUPPORT_SINCE_MINOR := 1 MIMALLOC_SUPPORT_SINCE_PATCH := 50 MIMALLOC_UNSUPPORTED_WARNING := mimalloc is supported after version $(MIMALLOC_SUPPORT_SINCE_MAJOR).$(MIMALLOC_SUPPORT_SINCE_MINOR).$(MIMALLOC_SUPPORT_SINCE_PATCH) - EMCC_VERSION := $(shell $(CC) --version | head -n 1 | cut -f10 -d ' ') - EMCC_MAJOR := $(shell echo $(EMCC_VERSION) | cut -f1 -d.) - EMCC_MINOR := $(shell echo $(EMCC_VERSION) | cut -f2 -d.) - EMCC_PATCH := $(shell echo $(EMCC_VERSION) | cut -f3 -d.) ifeq ($(shell echo $(EMCC_MAJOR)\>=$(MIMALLOC_SUPPORT_SINCE_MAJOR) | bc), 1) ifeq ($(shell echo $(EMCC_MINOR)\>=$(MIMALLOC_SUPPORT_SINCE_MINOR) | bc), 1) ifeq ($(shell echo $(EMCC_PATCH)\>=$(MIMALLOC_SUPPORT_SINCE_PATCH) | bc), 1) diff --git a/src/emulate.c b/src/emulate.c index 32fe18ac1..5539aa611 100644 --- a/src/emulate.c +++ b/src/emulate.c @@ -10,6 +10,10 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include +#endif + #if RV32_HAS(EXT_F) #include #include "softfloat.h" @@ -1074,9 +1078,11 @@ static bool runtime_profiler(riscv_t *rv, block_t *block) typedef void (*exec_block_func_t)(riscv_t *rv, uintptr_t); #endif -void rv_step(riscv_t *rv) +void rv_step(void *arg) { - assert(rv); + assert(arg); + riscv_t *rv = arg; + vm_attr_t *attr = PRIV(rv); uint32_t cycles = attr->cycle_per_step; @@ -1174,6 +1180,13 @@ void rv_step(riscv_t *rv) #endif prev = block; } + +#ifdef __EMSCRIPTEN__ + if (rv_has_halted(rv)) { + emscripten_cancel_main_loop(); + rv_delete(rv); /* clean up and reuse memory */ + } +#endif } void ebreak_handler(riscv_t *rv) diff --git a/src/main.c b/src/main.c index f10fdcd41..0f85dca21 100644 --- a/src/main.c +++ b/src/main.c @@ -168,6 +168,10 @@ static void dump_test_signature(const char *prog_name) elf_delete(elf); } +/* CYCLE_PER_STEP shall be defined on different runtime */ +#ifndef CYCLE_PER_STEP +#define CYCLE_PER_STEP 100 +#endif /* MEM_SIZE shall be defined on different runtime */ #ifndef MEM_SIZE #define MEM_SIZE 0xFFFFFFFFULL /* 2^32 - 1 */ @@ -175,6 +179,20 @@ static void dump_test_signature(const char *prog_name) #define STACK_SIZE 0x1000 /* 4096 */ #define ARGS_OFFSET_SIZE 0x1000 /* 4096 */ +/* To use rv_halt function in wasm, we have to expose RISC-V instance(rv), + * but we can add a layer to not expose the instance and make rv_halt + * callable. A small trade-off is that declaring instance as a global + * variable. rv_halt is useful when cancelling the main loop of wasm, + * see rv_step in emulate.c for more detail + */ +riscv_t *rv; +#ifdef __EMSCRIPTEN__ +void indirect_rv_halt() +{ + rv_halt(rv); +} +#endif + int main(int argc, char **args) { if (argc == 1 || !parse_args(argc, args)) { @@ -199,14 +217,14 @@ int main(int argc, char **args) .run_flag = run_flag, .profile_output_file = prof_out_file, .data.user = malloc(sizeof(vm_user_t)), - .cycle_per_step = 100, + .cycle_per_step = CYCLE_PER_STEP, .allow_misalign = opt_misaligned, }; assert(attr.data.user); attr.data.user->elf_program = opt_prog_name; /* create the RISC-V runtime */ - riscv_t *rv = rv_create(&attr); + rv = rv_create(&attr); if (!rv) { fprintf(stderr, "Unable to create riscv emulator\n"); attr.exit_code = 1; diff --git a/src/riscv.c b/src/riscv.c index b8f2989ac..8507a23b1 100644 --- a/src/riscv.c +++ b/src/riscv.c @@ -18,6 +18,10 @@ #define STDERR_FILENO FILENO(stderr) #endif +#ifdef __EMSCRIPTEN__ +#include +#endif + #include "elf.h" #include "mpool.h" #include "riscv.h" @@ -315,9 +319,13 @@ void rv_run(riscv_t *rv) rv_debug(rv); #endif else { +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop_arg(rv_step, (void *) rv, 0, 1); +#else /* default main loop */ for (; !rv_has_halted(rv);) /* run until the flag is done */ rv_step(rv); /* step instructions */ +#endif } if (attr->run_flag & RV_RUN_PROFILE) { diff --git a/src/riscv.h b/src/riscv.h index 88339a3b1..57977bdee 100644 --- a/src/riscv.h +++ b/src/riscv.h @@ -166,7 +166,7 @@ void rv_debug(riscv_t *rv); #endif /* step the RISC-V emulator */ -void rv_step(riscv_t *rv); +void rv_step(void *arg); /* set the program counter of a RISC-V emulator */ bool rv_set_pc(riscv_t *rv, riscv_word_t pc); diff --git a/src/syscall_sdl.c b/src/syscall_sdl.c index e1936ec2f..05af8c90e 100644 --- a/src/syscall_sdl.c +++ b/src/syscall_sdl.c @@ -708,6 +708,12 @@ static void play_sfx(riscv_t *rv) .volume = volume, }; pthread_create(&sfx_thread, NULL, sfx_handler, &sfx); + /* FIXME: In web browser runtime, web workers in thread pool do not reap + * after sfx_handler return, thus we have to join them. sfx_handler does not + * contain infinite loop,so do not worry to be stalled by it */ +#ifdef __EMSCRIPTEN__ + pthread_join(sfx_thread, NULL); +#endif } static void play_music(riscv_t *rv) @@ -738,6 +744,12 @@ static void play_music(riscv_t *rv) .volume = volume, }; pthread_create(&music_thread, NULL, music_handler, &music); + /* FIXME: In web browser runtime, web workers in thread pool do not reap + * after music_handler return, thus we have to join them. music_handler does + * not contain infinite loop,so do not worry to be stalled by it */ +#ifdef __EMSCRIPTEN__ + pthread_join(music_thread, NULL); +#endif } static void stop_music(riscv_t *rv UNUSED)