Skip to content

Commit

Permalink
Merge pull request #385 from ChinYikMing/emcc-port-video-games
Browse files Browse the repository at this point in the history
Enable run video games using WebAssembly
  • Loading branch information
jserv authored Mar 25, 2024
2 parents e39b8ac + 07b86f6 commit 971b42c
Show file tree
Hide file tree
Showing 10 changed files with 422 additions and 23 deletions.
46 changes: 37 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 $<

Expand Down Expand Up @@ -259,22 +273,36 @@ 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

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
Expand Down
277 changes: 277 additions & 0 deletions assets/html/index.html

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions assets/js/pre.js
Original file line number Diff line number Diff line change
@@ -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]);
};
24 changes: 19 additions & 5 deletions mk/external.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
28 changes: 24 additions & 4 deletions mk/toolchain.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 15 additions & 2 deletions src/emulate.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
#include <stdlib.h>
#include <string.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

#if RV32_HAS(EXT_F)
#include <math.h>
#include "softfloat.h"
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down
22 changes: 20 additions & 2 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,31 @@ 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 */
#endif
#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)) {
Expand All @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/riscv.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
#define STDERR_FILENO FILENO(stderr)
#endif

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

#include "elf.h"
#include "mpool.h"
#include "riscv.h"
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/riscv.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions src/syscall_sdl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 971b42c

Please sign in to comment.