From: Lockl00p <97256723+Lockl00p@users.noreply.github.com> Date: Sun, 11 Aug 2024 17:43:09 +0000 (-0500) Subject: Merge PR 'Added Webassembly support and fixed GLES2 (somewhat)' X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=558f16723b3b4708c1d2d3c3b973180296e85266;p=xonotic%2Fdarkplaces.git Merge PR 'Added Webassembly support and fixed GLES2 (somewhat)' Webassembly version doesn’t work online and screen flashes on GLES2 are opaque. See https://github.com/DarkPlacesEngine/DarkPlaces/pull/169 --------- Signed-off-by: bones_was_here Co-authored-by: James O'Neill Co-authored-by: bones_was_here --- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27fbd2d0..43d4c5a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,8 +8,7 @@ on: pull_request: jobs: - build: - + sdl-release: runs-on: ubuntu-latest container: image: debian:latest @@ -26,7 +25,7 @@ jobs: # make `git describe` show the correct commit hash fetch-depth: '0' - - name: Compile DP + - name: Compile run: | # prevent git complaining about dubious ownership of the repo chown -R root:root . @@ -43,3 +42,56 @@ jobs: path: | darkplaces-sdl + wasm-release: + runs-on: ubuntu-latest + container: + image: debian:latest + steps: + - name: Install dependencies + run: | + apt update + apt install --yes build-essential python3-certifi + + - name: Fetch repository + uses: actions/checkout@v4.1.1 + with: + # make `git describe` show the correct commit hash + fetch-depth: '0' + + - uses: actions/setup-python@v5 + with: + python-version: '3' + + - name: Install emsdk + uses: actions/checkout@v4.1.1 + with: + repository: emscripten-core/emsdk + path: emsdk + + - name: Compile + shell: bash + run: | + cd emsdk + + # Download and install the latest SDK tools. + ./emsdk install latest + + # Make the "latest" SDK "active" for the current user. (writes .emscripten file) + ./emsdk activate latest + + # Activate PATH and other environment variables in the current terminal + source ./emsdk_env.sh + + cd .. + + # fail if there's any warnings + #export CC="cc" + + make emscripten-release + + - name: Upload WASM artifacts + uses: actions/upload-artifact@v4 + with: + name: Wasm + path: | + darkplaces-wasm.js diff --git a/.gitignore b/.gitignore index 4c4c53c9..1270a552 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ ChangeLog darkplaces-agl darkplaces-glx darkplaces-sdl +darkplaces-wasm.html +darkplaces-wasm.js darkplaces-dedicated gmon.out *.ncb @@ -41,3 +43,10 @@ Makefile.win *.pdb *.lib *.exp + +# emscripten +build-obj/ +emsdk/ +docs/output/ +wasm/preload/* +!wasm/preload/runhere diff --git a/README.md b/README.md index df037d5c..00f6f1fe 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,29 @@ The Release build crashes. The Debug x64 build doesn't crash (but is rather slow To get a build suitable for playing you'll need to use MinGW GCC, or download the autobuild from Xonotic (see above). +### Web-Assembly (Emscripten) + +Note that this requires a linux device or WSL2. + +1. Install the [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended) +1. After activating and sourcing emsdk, compile DarkPlaces for wasm using; + ```shell + make emscripten-release + ``` +1. Copy `darkplaces-wasm.js`, `wasm/index.html`, and `wasm/autoexec.cfg` files to your web server +1. Copy the Quake `pak0.pak` and any other files into the same web server directory + +For the standalone version (single HTML file containing engine and data): +1. Before compiling, copy game data and .cfg files to the appropriate gamedir in `wasm/preload` (for example, pak0 from Quake would be in `wasm/preload/id1/pak0.pak`) +1. After activating and sourcing emsdk, compile DarkPlaces for wasm using; + ```shell + make emscripten-standalone + ``` +1. To start DP you must click somewhere in the window! +1. If you want to upload files into the game filesystem, use `em_upload` in the darkplaces console (upload to /save if you want it to save across restarts) +1. To save the stuff you uploaded to /save, use `em_save` (note that if you embedded the game, you won't be able to save changes to `/save/games`) + + ## Contributing [DarkPlaces Contributing Guidelines](CONTRIBUTING.md) diff --git a/fs.c b/fs.c index 57828961..2ce128e8 100644 --- a/fs.c +++ b/fs.c @@ -2128,6 +2128,10 @@ void FS_Init_Commands(void) Cmd_AddCommand(CF_SHARED, "ls", FS_Ls_f, "list files in searchpath matching an * filename pattern, multiple per line"); Cmd_AddCommand(CF_SHARED, "which", FS_Which_f, "accepts a file name as argument and reports where the file is taken from"); +#ifdef __EMSCRIPTEN__ + Sys_EM_Register_Commands(); +#endif + if (com_startupgamegroup == GAME_NORMAL) Cmd_AddCommand(CF_SHARED, "game", FS_GameDir_f, "alias of gamedir, for compatibility with some Quake mod READMEs"); } diff --git a/gl_backend.c b/gl_backend.c index b5336a22..1dd6d742 100644 --- a/gl_backend.c +++ b/gl_backend.c @@ -11,7 +11,11 @@ cvar_t gl_printcheckerror = {CF_CLIENT, "gl_printcheckerror", "0", "prints all O cvar_t r_render = {CF_CLIENT, "r_render", "1", "enables rendering 3D views (you want this on!)"}; cvar_t r_renderview = {CF_CLIENT, "r_renderview", "1", "enables rendering 3D views (you want this on!)"}; cvar_t r_waterwarp = {CF_CLIENT | CF_ARCHIVE, "r_waterwarp", "1", "warp view while underwater"}; +#ifdef USE_GLES2 +cvar_t gl_polyblend = {CF_CLIENT | CF_ARCHIVE, "gl_polyblend", "0", "tints view while underwater, hurt, etc"}; +#else cvar_t gl_polyblend = {CF_CLIENT | CF_ARCHIVE, "gl_polyblend", "1", "tints view while underwater, hurt, etc"}; +#endif cvar_t v_flipped = {CF_CLIENT, "v_flipped", "0", "mirror the screen (poor man's left handed mode)"}; qbool v_flipped_state = false; @@ -966,8 +970,12 @@ int R_Mesh_CreateFramebufferObject(rtexture_t *depthtexture, rtexture_t *colorte case RENDERPATH_GLES2: CHECKGLERROR qglGenFramebuffers(1, (GLuint*)&temp);CHECKGLERROR - R_Mesh_SetRenderTargets(temp, NULL, NULL, NULL, NULL, NULL); + +#ifndef USE_GLES2 + R_Mesh_SetRenderTargets(temp, NULL, NULL, NULL, NULL, NULL); // This breaks GLES2. // GL_ARB_framebuffer_object (GL3-class hardware) - depth stencil attachment +#endif + #ifdef USE_GLES2 // FIXME: separate stencil attachment on GLES if (depthtexture && depthtexture->texnum ) { qglFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT , depthtexture->gltexturetypeenum , depthtexture->texnum , 0);CHECKGLERROR } @@ -984,6 +992,7 @@ int R_Mesh_CreateFramebufferObject(rtexture_t *depthtexture, rtexture_t *colorte if (depthtexture->glisdepthstencil) { qglFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT , GL_RENDERBUFFER, depthtexture->renderbuffernum );CHECKGLERROR } } #endif + if (colortexture && colortexture->texnum ) { qglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 , colortexture->gltexturetypeenum , colortexture->texnum , 0);CHECKGLERROR } if (colortexture2 && colortexture2->texnum) { qglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1 , colortexture2->gltexturetypeenum, colortexture2->texnum, 0);CHECKGLERROR } if (colortexture3 && colortexture3->texnum) { qglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2 , colortexture3->gltexturetypeenum, colortexture3->texnum, 0);CHECKGLERROR } diff --git a/host.c b/host.c index 78536822..ab0b0779 100644 --- a/host.c +++ b/host.c @@ -211,7 +211,9 @@ void Host_SaveConfig(const char *file) Key_WriteBindings (f); Cvar_WriteVariables (&cvars_all, f); - +#ifdef __EMSCRIPTEN__ + js_syncFS(false); +#endif FS_Close (f); } } @@ -371,7 +373,7 @@ void Host_UnlockSession(void) Host_Init ==================== */ -static void Host_Init (void) +void Host_Init (void) { int i; char vabuf[1024]; @@ -388,6 +390,9 @@ static void Host_Init (void) host.state = host_init; + host.realtime = 0; + host.dirtytime = Sys_DirtyTime(); + if (setjmp(host.abortframe)) // Huh?! Sys_Error("Engine initialization failed. Check the console (if available) for additional information.\n"); @@ -567,10 +572,10 @@ static void Host_Init (void) =============== Host_Shutdown -Cleanly shuts down after the main loop exits. +Cleanly shuts down, Host_Frame() must not be called again after this. =============== */ -static void Host_Shutdown(void) +void Host_Shutdown(void) { if (Sys_CheckParm("-profilegameonly")) Sys_AllowProfiling(false); @@ -617,7 +622,7 @@ Host_Frame Runs all active servers ================== */ -static double Host_Frame(double time) +double Host_Frame(double time) { double cl_wait, sv_wait; @@ -655,56 +660,3 @@ static double Host_Frame(double time) else return min(cl_wait, sv_wait); // listen server or singleplayer } - -// Cloudwalk: Most overpowered function declaration... -static inline double Host_UpdateTime (double newtime, double oldtime) -{ - double time = newtime - oldtime; - - if (time < 0) - { - // warn if it's significant - if (time < -0.01) - Con_Printf(CON_WARN "Host_UpdateTime: time stepped backwards (went from %f to %f, difference %f)\n", oldtime, newtime, time); - time = 0; - } - else if (time >= 1800) - { - Con_Printf(CON_WARN "Host_UpdateTime: time stepped forward (went from %f to %f, difference %f)\n", oldtime, newtime, time); - time = 0; - } - - return time; -} - -void Host_Main(void) -{ - double time, oldtime, sleeptime; - - Host_Init(); // Start! - - host.realtime = 0; - oldtime = Sys_DirtyTime(); - - // Main event loop - while(host.state < host_shutdown) // see Sys_HandleCrash() comments - { - // Something bad happened, or the server disconnected - if (setjmp(host.abortframe)) - { - host.state = host_active; // In case we were loading - continue; - } - - host.dirtytime = Sys_DirtyTime(); - host.realtime += time = Host_UpdateTime(host.dirtytime, oldtime); - oldtime = host.dirtytime; - - sleeptime = Host_Frame(time); - ++host.framecount; - sleeptime -= Sys_DirtyTime() - host.dirtytime; // execution time - host.sleeptime = Sys_Sleep(sleeptime); - } - - Host_Shutdown(); -} diff --git a/host.h b/host.h index d826c0b4..e72d75a2 100644 --- a/host.h +++ b/host.h @@ -54,13 +54,14 @@ typedef struct host_static_s } host_static_t; extern host_static_t host; - -void Host_Main(void); void Host_Error(const char *error, ...) DP_FUNC_PRINTF(1) DP_FUNC_NORETURN; void Host_UpdateVersion(void); void Host_LockSession(void); void Host_UnlockSession(void); void Host_AbortCurrentFrame(void) DP_FUNC_NORETURN; void Host_SaveConfig(const char *file); +void Host_Init(void); +double Host_Frame(double time); +void Host_Shutdown(void); #endif diff --git a/makefile b/makefile index b62194cd..923c8d8e 100644 --- a/makefile +++ b/makefile @@ -110,6 +110,35 @@ ifeq ($(DP_MAKE_TARGET), linux) DP_LINK_XMP?=dlopen endif +ifeq ($(DP_MAKE_TARGET), wasm) + MAKE=emmake make +# CFLAGS_EXTRA+=--use-port=sdl2 \ +# --use-port=libpng \ +# --use-port=libjpeg \ +# --use-port=zlib \ +# -DNOSUPPORTIPV6 \ +# -DUSE_GLES2 + CFLAGS_EXTRA+=-s USE_SDL=2 \ + -s USE_LIBPNG=1 \ + -s USE_LIBJPEG=1 \ + -s USE_ZLIB=1 \ + -DNOSUPPORTIPV6 \ + -DUSE_GLES2 + + SDLCONFIG_CFLAGS=$(SDLCONFIG_UNIXCFLAGS) $(SDLCONFIG_UNIXCFLAGS_X11) + SDLCONFIG_LIBS=$(SDLCONFIG_UNIXLIBS) $(SDLCONFIG_UNIXLIBS_X11) + SDLCONFIG_STATICLIBS=$(SDLCONFIG_UNIXSTATICLIBS) $(SDLCONFIG_UNIXSTATICLIBS_X11) + DP_SSE=0 + + DP_LINK_SDL?=shared + DP_LINK_ZLIB?=shared + DP_LINK_JPEG?=dlopen + DP_LINK_ODE?= + DP_LINK_CRYPTO?=dlopen + DP_LINK_CRYPTO_RIJNDAEL?=dlopen + DP_LINK_XMP?=dlopen +endif + # Mac OS X configuration ifeq ($(DP_MAKE_TARGET), macosx) OBJ_ICON= diff --git a/makefile.inc b/makefile.inc index 33cbc043..c0ec06fe 100644 --- a/makefile.inc +++ b/makefile.inc @@ -128,7 +128,7 @@ OBJ_MENU= \ # built to give the executable a proper date string OBJ_SV= builddate.c sys_null.o vid_null.o thread_null.o $(OBJ_SND_NULL) $(OBJ_COMMON) OBJ_SDL= builddate.c sys_sdl.o vid_sdl.o thread_sdl.o $(OBJ_MENU) $(OBJ_SND_COMMON) $(OBJ_SND_XMP) snd_sdl.o $(OBJ_VIDEO_CAPTURE) $(OBJ_COMMON) - +OBJ_WASM= builddate.c sys_wasm.o vid_sdl.o thread_sdl.o $(OBJ_MENU) $(OBJ_SND_COMMON) $(OBJ_SND_XMP) snd_sdl.o $(OBJ_VIDEO_CAPTURE) $(OBJ_COMMON) # Compilation # -D_POSIX_C_SOURCE=200809L doesn't enable all of POSIX 2008, wtf? @@ -212,6 +212,8 @@ EXE_UNIXSV=darkplaces-dedicated EXE_UNIXSDL=darkplaces-sdl EXE_UNIXSVNEXUIZ=nexuiz-dedicated EXE_UNIXSDLNEXUIZ=nexuiz-sdl +EXE_WASMJS=darkplaces-wasm.js +EXE_WASM=darkplaces-wasm.html CMD_UNIXRM=rm -rf CMD_UNIXCP=cp -f @@ -227,6 +229,47 @@ LDFLAGS_LINUXSV=$(LDFLAGS_UNIXCOMMON) -lrt -ldl -rdynamic LDFLAGS_LINUXSDL=$(LDFLAGS_UNIXCOMMON) -lrt -ldl -rdynamic $(LDFLAGS_UNIXSDL) +##### WASM specific variables ##### + +LDFLAGS_WASMJS=$(LDFLAGS_UNIXCOMMON) -s USE_SDL=2 \ + -s USE_LIBPNG=1 \ + -s USE_LIBJPEG=1 \ + -s USE_ZLIB=1 \ + -s INITIAL_MEMORY=128mb \ + -s MAXIMUM_MEMORY=1gb \ + -s SINGLE_FILE \ + -s FULL_ES3 \ + -s MIN_WEBGL_VERSION=2 \ + -s MAX_WEBGL_VERSION=2 \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s ASSERTIONS=2 \ + -s TOTAL_STACK=32mb \ + -DUSE_GLES2 \ + -lidbfs.js \ + --pre-js ../../../wasm/pre.js \ + -s EXPORTED_RUNTIME_METHODS=callMain,addRunDependency,removeRunDependency + +LDFLAGS_WASM=$(LDFLAGS_UNIXCOMMON) -s USE_SDL=2 \ + -s USE_LIBPNG=1 \ + -s USE_LIBJPEG=1 \ + -s USE_ZLIB=1 \ + -s INITIAL_MEMORY=128mb \ + -s MAXIMUM_MEMORY=1gb \ + -s SINGLE_FILE \ + -s FULL_ES3 \ + -s MIN_WEBGL_VERSION=2 \ + -s MAX_WEBGL_VERSION=2 \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s ASSERTIONS=2 \ + -s TOTAL_STACK=32mb \ + -DUSE_GLES2 \ + -lidbfs.js \ + --pre-js ../../../wasm/standaloneprejs.js \ + -s EXPORTED_RUNTIME_METHODS=callMain,addRunDependency,removeRunDependency \ + --shell-file ../../../wasm/standalone-shell.html \ + --embed-file ../../../wasm/preload@/preload + + ##### Mac OS X specific variables ##### # Link @@ -270,7 +313,7 @@ VPATH := ../../../ .PHONY : clean clean-profile help \ debug profile release \ sv-debug sv-profile sv-release \ - sdl-debug sdl-profile sdl-release + sdl-debug sdl-profile sdl-release emscripten-release help: @echo @@ -301,6 +344,9 @@ help: @echo "* $(MAKE) sdl-release : make SDL client (release version)" @echo "* $(MAKE) sdl-nexuiz : make SDL client with nexuiz icon (release version)" @echo + @echo "* $(MAKE) emscripten-release : make WASM client (release version)" + @echo "* $(MAKE) emscripten-standalone: make standalone WASM client (release version)" + @echo debug : $(MAKE) $(TARGETS_DEBUG) @@ -357,6 +403,24 @@ sdl-release : DP_MAKE_TARGET=$(DP_MAKE_TARGET) \ EXE='$(EXE_SDL)' CFLAGS_FEATURES='$(CFLAGS_CLIENT)' CFLAGS_SDL='$(SDLCONFIG_CFLAGS)' LDFLAGS_COMMON='$(LDFLAGS_SDL)' LEVEL=1 +emscripten-release : + $(MAKE) wasm-release \ + DP_MAKE_TARGET="wasm" \ + EXE='$(EXE_WASMJS)' \ + CFLAGS_FEATURES='$(CFLAGS_CLIENT)' \ + CFLAGS_SDL='$(SDLCONFIG_CFLAGS)' \ + LDFLAGS_COMMON='$(LDFLAGS_WASMJS)' \ + LEVEL=1 + +emscripten-standalone : + $(MAKE) wasm-release \ + DP_MAKE_TARGET="wasm" \ + EXE='$(EXE_WASM)' \ + CFLAGS_FEATURES='$(CFLAGS_CLIENT) -DWASM_USER_ADJUSTABLE' \ + CFLAGS_SDL='$(SDLCONFIG_CFLAGS)' \ + LDFLAGS_COMMON='$(LDFLAGS_WASM)' \ + LEVEL=1 + sdl-release-profile : $(MAKE) bin-release-profile \ DP_MAKE_TARGET=$(DP_MAKE_TARGET) \ @@ -409,6 +473,16 @@ bin-release-profile : LDFLAGS='$(LDFLAGS_RELEASE) $(LDFLAGS_COMMON)' LEVEL=2 $(STRIP) $(EXE) +wasm-release : + $(CHECKLEVEL1) + @echo + @echo '========== $(EXE) (release) ==========' + $(MAKE) prepare BUILD_DIR=build-obj/release/$(EXE) + $(MAKE) -C build-obj/release/$(EXE) $(EXE) \ + DP_MAKE_TARGET=$(DP_MAKE_TARGET) \ + CFLAGS='$(CFLAGS_COMMON) $(CFLAGS_FEATURES) $(CFLAGS_EXTRA) $(CFLAGS_RELEASE) $(OPTIM_RELEASE)'\ + LDFLAGS='$(LDFLAGS_RELEASE) $(LDFLAGS_COMMON)' LEVEL=2 + prepare : $(CMD_MKDIR) $(BUILD_DIR) $(CMD_CP) makefile.inc $(BUILD_DIR)/ @@ -483,6 +557,15 @@ $(EXE_SDL): $(OBJ_SDL) $(OBJ_ICON) $(CHECKLEVEL2) $(DO_LD) +$(EXE_WASM): $(OBJ_WASM) $(OBJ_ICON) + $(CHECKLEVEL2) + $(DO_LD) + +$(EXE_WASMJS): $(OBJ_WASM) $(OBJ_ICON) + $(CHECKLEVEL2) + $(DO_LD) + + $(EXE_SVNEXUIZ): $(OBJ_SV) $(OBJ_ICON_NEXUIZ) $(CHECKLEVEL2) $(DO_LD) @@ -497,6 +580,8 @@ $(EXE_SDLNEXUIZ): $(OBJ_SDL) $(OBJ_ICON_NEXUIZ) clean: -$(CMD_RM) $(EXE_SV) -$(CMD_RM) $(EXE_SDL) + -$(CMD_RM) $(EXE_WASM) + -$(CMD_RM) $(EXE_WASMJS) -$(CMD_RM) $(EXE_SVNEXUIZ) -$(CMD_RM) $(EXE_SDLNEXUIZ) -$(CMD_RM) *.o diff --git a/shader_glsl.h b/shader_glsl.h index 5a4b224a..db72342b 100644 --- a/shader_glsl.h +++ b/shader_glsl.h @@ -41,6 +41,9 @@ "\n", "invariant gl_Position; // fix for lighting polygons not matching base surface\n", "# endif\n", +#ifdef USE_GLES2 +"precision highp float;\n", +#endif "#if defined(GLSL130) || defined(GLSL140)\n", "precision highp float;\n", "# ifdef VERTEX_SHADER\n", diff --git a/sv_save.c b/sv_save.c index c2c7442b..5901b6dc 100644 --- a/sv_save.c +++ b/sv_save.c @@ -20,6 +20,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "quakedef.h" #include "prvm_cmds.h" +#include "sys.h" /* =============================================================================== @@ -173,6 +174,9 @@ void SV_Savegame_to(prvm_prog_t *prog, const char *name) #endif FS_Close (f); +#ifdef __EMSCRIPTEN__ + js_syncFS(false); +#endif Con_Print("done.\n"); } diff --git a/sys.h b/sys.h index a5c6b275..c2f238e2 100644 --- a/sys.h +++ b/sys.h @@ -42,6 +42,10 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #endif # define DP_MOBILETOUCH 1 # define DP_FREETYPE_STATIC 1 +#elif defined(__EMSCRIPTEN__) //this also defines linux, so it must come first +# define DP_OS_NAME "Browser" +# define DP_OS_STR "browser" +# define DP_ARCH_STR "WASM-32" #elif defined(__linux__) # define DP_OS_NAME "Linux" # define DP_OS_STR "linux" @@ -252,6 +256,11 @@ void Sys_SDL_HandleEvents(void); char *Sys_SDL_GetClipboardData (void); +#ifdef __EMSCRIPTEN__ //WASM-specific functions +bool js_syncFS (bool x); +void Sys_EM_Register_Commands(void); +#endif + extern qbool sys_supportsdlgetticks; unsigned int Sys_SDL_GetTicks (void); // wrapper to call SDL_GetTicks void Sys_SDL_Delay (unsigned int milliseconds); // wrapper to call SDL_Delay diff --git a/sys_shared.c b/sys_shared.c index b955e827..d460b77b 100644 --- a/sys_shared.c +++ b/sys_shared.c @@ -1102,6 +1102,73 @@ static void Sys_InitSignals(void) #endif } +// Cloudwalk: Most overpowered function declaration... +static inline double Sys_UpdateTime (double newtime, double oldtime) +{ + double time = newtime - oldtime; + + if (time < 0) + { + // warn if it's significant + if (time < -0.01) + Con_Printf(CON_WARN "Host_UpdateTime: time stepped backwards (went from %f to %f, difference %f)\n", oldtime, newtime, time); + time = 0; + } + else if (time >= 1800) + { + Con_Printf(CON_WARN "Host_UpdateTime: time stepped forward (went from %f to %f, difference %f)\n", oldtime, newtime, time); + time = 0; + } + + return time; +} + +#ifdef __EMSCRIPTEN__ + #include +#endif +/// JS+WebGL doesn't support a main loop, only a function called to run a frame. +static void Sys_Frame(void) +{ + double time, newtime, sleeptime; +#ifdef __EMSCRIPTEN__ + static double sleepstarttime = 0; + host.sleeptime = Sys_DirtyTime() - sleepstarttime; +#endif + + if (setjmp(host.abortframe)) // Something bad happened, or the server disconnected + host.state = host_active; // In case we were loading + + if (host.state >= host_shutdown) // see Sys_HandleCrash() comments + { +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); +#endif +#ifdef __ANDROID__ + Sys_AllowProfiling(false); +#endif + Host_Shutdown(); + exit(0); + } + + newtime = Sys_DirtyTime(); + host.realtime += time = Sys_UpdateTime(newtime, host.dirtytime); + host.dirtytime = newtime; + + sleeptime = Host_Frame(time); + ++host.framecount; + sleeptime -= Sys_DirtyTime() - host.dirtytime; // execution time + +#ifdef __EMSCRIPTEN__ + // This platform doesn't support a main loop... it will sleep when Sys_Frame() returns. + // Not using emscripten_sleep() via Sys_Sleep() because it would cause two sleeps per frame. + if (!vid_vsync.integer) // see VID_SetVsync_c() + emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, host.restless ? 0 : sleeptime * 1000.0); + sleepstarttime = Sys_DirtyTime(); +#else + host.sleeptime = Sys_Sleep(sleeptime); +#endif +} + /** main() but renamed so we can wrap it in sys_sdl.c and sys_null.c * to avoid needing to include SDL.h in this file (would make the dedicated server require SDL). * SDL builds need SDL.h in the file where main() is defined because SDL renames and wraps main(). @@ -1141,18 +1208,13 @@ int Sys_Main(int argc, char *argv[]) Sys_SetTimerResolution(); #endif - Host_Main(); - -#ifdef __ANDROID__ - Sys_AllowProfiling(false); -#endif - -#ifndef WIN32 - fcntl(fileno(stdout), F_SETFL, fcntl(fileno(stdout), F_GETFL, 0) & ~O_NONBLOCK); - fcntl(fileno(stderr), F_SETFL, fcntl(fileno(stderr), F_GETFL, 0) & ~O_NONBLOCK); + Host_Init(); +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(Sys_Frame, 0, true); // doesn't return +#else + while(true) + Sys_Frame(); #endif - fflush(stdout); - fflush(stderr); return 0; } diff --git a/sys_wasm.c b/sys_wasm.c new file mode 100644 index 00000000..167cea5c --- /dev/null +++ b/sys_wasm.c @@ -0,0 +1,307 @@ +/* + * Include this BEFORE darkplaces.h because it breaks wrapping + * _Static_assert. Cloudwalk has no idea how or why so don't ask. + */ +#include + +#include "darkplaces.h" +#include "fs.h" +#include "vid.h" + +#include +#include +#include + + +EM_JS(float, js_GetViewportWidth, (void), { + return document.documentElement.clientWidth +}); +EM_JS(float, js_GetViewportHeight, (void), { + return document.documentElement.clientHeight +}); +static EM_BOOL em_on_resize(int etype, const EmscriptenUiEvent *event, void *UData) +{ + if(vid_resizable.integer) + { + Cvar_SetValueQuick(&vid_width, js_GetViewportWidth()); + Cvar_SetValueQuick(&vid_height, js_GetViewportHeight()); + Cvar_SetQuick(&vid_fullscreen, "0"); + } + return EM_FALSE; +} + + +// ======================================================================= +// General routines +// ======================================================================= + +#ifdef WASM_USER_ADJUSTABLE +EM_JS(char *, js_listfiles, (const char *directory), { + if(UTF8ToString(directory) == "") + { + console.log("listing cwd"); + return stringToNewUTF8(FS.readdir(FS.cwd()).toString()); + } + + try + { + return stringToNewUTF8(FS.readdir(UTF8ToString(directory)).toString()); + } + catch (error) + { + return stringToNewUTF8("directory not found"); + } +}); +static void em_listfiles_f(cmd_state_t *cmd) +{ + char *output = js_listfiles(Cmd_Argc(cmd) == 2 ? Cmd_Argv(cmd, 1) : ""); + + Con_Printf("%s\n", output); + free(output); +} + +EM_JS(char *, js_upload, (const char *todirectory), { + if (UTF8ToString(todirectory).slice(-1) != "/") + { + currentname = UTF8ToString(todirectory) + "/"; + } + else + { + currentname = UTF8ToString(todirectory); + } + + file_selector.click(); + return stringToNewUTF8("Upload started"); +}); +static void em_upload_f(cmd_state_t *cmd) +{ + char *output = js_upload(Cmd_Argc(cmd) == 2 ? Cmd_Argv(cmd, 1) : fs_basedir); + + Con_Printf("%s\n", output); + free(output); +} + +EM_JS(char *, js_rm, (const char *path), { + const mode = FS.lookupPath(UTF8ToString(path)).node.mode; + + if (FS.isFile(mode)) + { + FS.unlink(UTF8ToString(path)); + return stringToNewUTF8("File removed"); + } + + return stringToNewUTF8(UTF8ToString(path)+" is not a File."); +}); +static void em_rm_f(cmd_state_t *cmd) +{ + if (Cmd_Argc(cmd) != 2) + Con_Printf("No file to remove\n"); + else + { + char *output = js_rm(Cmd_Argv(cmd, 1)); + Con_Printf("%s\n", output); + free(output); + } +} + +EM_JS(char *, js_rmdir, (const char *path), { + const mode = FS.lookupPath(UTF8ToString(path)).node.mode; + if (FS.isDir(mode)) + { + try + { + FS.rmdir(UTF8ToString(path)); + } + catch (error) + { + return stringToNewUTF8("Unable to remove directory. Is it not empty?"); + } + return stringToNewUTF8("Directory removed"); + } + + return stringToNewUTF8(UTF8ToString(path)+" is not a directory."); +}); +static void em_rmdir_f(cmd_state_t *cmd) +{ + if (Cmd_Argc(cmd) != 2) + Con_Printf("No directory to remove\n"); + else + { + char *output = js_rmdir(Cmd_Argv(cmd, 1)); + Con_Printf("%s\n", output); + free(output); + } +} + +EM_JS(char *, js_mkd, (const char *path), { + try + { + FS.mkdir(UTF8ToString(path)); + } + catch (error) + { + return stringToNewUTF8("Unable to create directory. Does it already exist?"); + } + return stringToNewUTF8(UTF8ToString(path)+" directory was created."); +}); +static void em_mkdir_f(cmd_state_t *cmd) +{ + if (Cmd_Argc(cmd) != 2) + Con_Printf("No directory to create\n"); + else + { + char *output = js_mkd(Cmd_Argv(cmd, 1)); + Con_Printf("%s\n", output); + free(output); + } +} + +EM_JS(char *, js_move, (const char *oldpath, const char *newpath), { + try + { + FS.rename(UTF8ToString(oldpath),UTF8ToString(newpath)) + } + catch (error) + { + return stringToNewUTF8("unable to move."); + } + return stringToNewUTF8("File Moved"); +}); +static void em_mv_f(cmd_state_t *cmd) +{ + if (Cmd_Argc(cmd) != 3) + Con_Printf("Nothing to move\n"); + else + { + char *output = js_move(Cmd_Argv(cmd,1), Cmd_Argv(cmd,2)); + Con_Printf("%s\n", output); + free(output); + } +} + +static void em_wss_f(cmd_state_t *cmd) +{ + if (Cmd_Argc(cmd) != 3) + Con_Printf("Not Enough Arguments (Expected URL and subprotocol)\n"); + else + { + if(strcmp(Cmd_Argv(cmd,2),"binary") == 0 || strcmp(Cmd_Argv(cmd,2),"text") == 0) + Con_Printf("Set Websocket URL to %s and subprotocol to %s.\n", Cmd_Argv(cmd,1), Cmd_Argv(cmd,2)); + else + Con_Printf("subprotocol must be either binary or text\n"); + } +} +#endif // WASM_USER_ADJUSTABLE + +EM_JS(bool, js_syncFS, (bool populate), { + FS.syncfs(populate, function(err) { + if(err) + { + alert("FileSystem Save Error: " + err); + return false; + } + + alert("Filesystem Saved!"); + return true; + }); +}); +static void em_savefs_f(cmd_state_t *cmd) +{ + Con_Printf("Saving Files\n"); + js_syncFS(false); +} + +void Sys_SDL_Shutdown(void) +{ + js_syncFS(false); + SDL_Quit(); +} + +// Sys_Abort early in startup might screw with automated +// workflows or something if we show the dialog by default. +static qbool nocrashdialog = true; +void Sys_SDL_Dialog(const char *title, const char *string) +{ + if(!nocrashdialog) + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, string, NULL); +} + +// In a browser the clipboard can only be read asynchronously +// doing this efficiently and cleanly requires JSPI +// enable in makefile.inc with emcc option: -s JSPI +/* TODO: enable this code when JSPI is enabled by default in browsers +EM_ASYNC_JS(char *, js_getclipboard, (void), +{ + try + { + const text = await navigator.clipboard.readText(); + return stringToNewUTF8(text); + } + catch (err) + { + return stringToNewUTF8("clipboard error: ", err); + } +}); */ +EM_JS(char *, js_getclipboard, (void), { + return stringToNewUTF8("clipboard access requires JSPI which is not currently enabled."); +}); +char *Sys_SDL_GetClipboardData (void) +{ + char *data = NULL; + char *cliptext; + + // SDL_GetClipboardText() does nothing in a browser, see above + cliptext = js_getclipboard(); + if (cliptext != NULL) { + size_t allocsize; + allocsize = min(MAX_INPUTLINE, strlen(cliptext) + 1); + data = (char *)Z_Malloc (allocsize); + dp_strlcpy (data, cliptext, allocsize); + free(cliptext); + } + + return data; +} + +void Sys_SDL_Init(void) +{ + if (SDL_Init(0) < 0) + Sys_Error("SDL_Init failed: %s\n", SDL_GetError()); + + // we don't know which systems we'll want to init, yet... + // COMMANDLINEOPTION: sdl: -nocrashdialog disables "Engine Error" crash dialog boxes + if(!Sys_CheckParm("-nocrashdialog")) + nocrashdialog = false; + + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, EM_FALSE, em_on_resize); +} + +void Sys_EM_Register_Commands(void) +{ +#ifdef WASM_USER_ADJUSTABLE + Cmd_AddCommand(CF_SHARED, "em_ls", em_listfiles_f, "Lists Files in specified directory defaulting to the current working directory (Emscripten Only)"); + Cmd_AddCommand(CF_SHARED, "em_upload", em_upload_f, "Upload file to specified directory defaulting to basedir (Emscripten Only)"); + Cmd_AddCommand(CF_SHARED, "em_rm", em_rm_f, "Remove a file from game Filesystem (Emscripten Only)"); + Cmd_AddCommand(CF_SHARED, "em_rmdir", em_rmdir_f, "Remove a directory from game Filesystem (Emscripten Only)"); + Cmd_AddCommand(CF_SHARED, "em_mkdir", em_mkdir_f, "Make a directory in game Filesystem (Emscripten Only)"); + Cmd_AddCommand(CF_SHARED, "em_mv", em_mv_f, "Rename or Move an item in game Filesystem (Emscripten only)"); + Cmd_AddCommand(CF_SHARED, "em_wss", em_wss_f, "Set Websocket URL and Protocol (Emscripten Only)"); +#endif + Cmd_AddCommand(CF_SHARED, "em_save", em_savefs_f, "Save file changes to browser (Emscripten Only)"); +} + +qbool sys_supportsdlgetticks = true; +unsigned int Sys_SDL_GetTicks(void) +{ + return SDL_GetTicks(); +} + +void Sys_SDL_Delay(unsigned int milliseconds) +{ + SDL_Delay(milliseconds); +} + +int main(int argc, char *argv[]) +{ + return Sys_Main(argc, argv); +} diff --git a/vid_sdl.c b/vid_sdl.c index 681ba540..631b0da8 100644 --- a/vid_sdl.c +++ b/vid_sdl.c @@ -1537,6 +1537,7 @@ On Xorg it returns the correct value. return; */ + // __EMSCRIPTEN__ SDL_GL_SetSwapInterval() calls emscripten_set_main_loop_timing() if (SDL_GL_SetSwapInterval(vsyncwanted) >= 0) Con_DPrintf("Vsync %s\n", vsyncwanted ? "activated" : "deactivated"); else @@ -1711,10 +1712,8 @@ static qbool VID_InitModeGL(const viddef_mode_t *mode) { int windowflags = SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL; int i; -#ifndef USE_GLES2 // SDL usually knows best const char *drivername = NULL; -#endif // video display selection (multi-monitor) Cvar_SetValueQuick(&vid_info_displaycount, SDL_GetNumVideoDisplays()); diff --git a/wasm/autoexec.cfg b/wasm/autoexec.cfg new file mode 100644 index 00000000..b1164d52 --- /dev/null +++ b/wasm/autoexec.cfg @@ -0,0 +1,2 @@ +cl_particles_quake "1" + diff --git a/wasm/index.html b/wasm/index.html new file mode 100644 index 00000000..3cbb0d20 --- /dev/null +++ b/wasm/index.html @@ -0,0 +1,31 @@ + + + + + DarkPlaces Quake WASM + + + + + + + + + + + diff --git a/wasm/pre.js b/wasm/pre.js new file mode 100644 index 00000000..ec05bf2b --- /dev/null +++ b/wasm/pre.js @@ -0,0 +1,101 @@ +if (!Object.hasOwn(Module, 'arguments')) { + Module['arguments'] = ['-basedir', '/game']; +} +else { + Module['arguments'] = ['-basedir', '/game'].concat(Module['arguments']); +} + +Module['print'] = function(text) { + console.log(text); +} + +Module['printErr'] = function(text) { + console.error(text); +} + + +Module['preRun'] = [ + function() + { + function stdin(){ + return '\n'; // Return a newline/line feed character so the user is not prompted for input + }; + FS.init(stdin, null, null); // null for both stdout and stderr + + function createParentDirectory(filePath) { + // + // Split a filePath into parts, create the directory hierarchy + // + parts = filePath.split('/'); + for (let i = parts.length - 1; i > 0; i--) { + localDir = '/game/' + parts.slice(0, -i).join('/') + try { + FS.mkdir(localDir); + } + catch { + // Directory already exists + } + } + } + + function startDownload(localPath, remotePath) { + // + // Return a promise of a file download + // + Module['addRunDependency'](localPath); // Tell Emscripten about the dependency + + return fetch(remotePath) + .then(response => { + return response.arrayBuffer(); + }) + .then(arrayBuffer => { + const buffer = new Uint8Array(arrayBuffer); + stream = FS.open("/game/" + localPath, "w"); + FS.write(stream, buffer, 0, buffer.byteLength); + FS.close(stream); + console.log("Downloaded " + localPath); + Module['removeRunDependency'](localPath); // Tells Emscripten we've finished the download + }); + } + + function createBaseDir() { + // + // Creates the Quake basedir and mounts it to IDBFS + // + FS.mkdir('/game'); + //mounts IDBFS to where the game would save + FS.mount(IDBFS, {}, '/home/web_user/'); + } + + function downloadGameFiles() { + // + // Download files specified in the Module.files object + // + createBaseDir(); + + let downloads = []; + for (const [localPath, remotePath] of Object.entries(Module.files)) { + console.log("Downloading " + remotePath + " to " + localPath); + + createParentDirectory(localPath); + + downloads.push( + startDownload(localPath, remotePath) + ); + } + + // Wait for downloads to finish, sync the filesystem, start the game + Promise.all(downloads) + .then(function(results) { + FS.syncfs(true, function (err) { + assert(!err); + Module.callMain(Module.arguments); + }); + }); + } + + downloadGameFiles(); + } +]; + +Module['noInitialRun'] = true; diff --git a/wasm/preload/runhere b/wasm/preload/runhere new file mode 100644 index 00000000..e69de29b diff --git a/wasm/standalone-shell.html b/wasm/standalone-shell.html new file mode 100644 index 00000000..b2779ca2 --- /dev/null +++ b/wasm/standalone-shell.html @@ -0,0 +1,66 @@ + + + + + + + DarkPlaces + + + + + + + + + + + + + + {{{ SCRIPT }}} + + + + diff --git a/wasm/standaloneprejs.js b/wasm/standaloneprejs.js new file mode 100644 index 00000000..30f8e90d --- /dev/null +++ b/wasm/standaloneprejs.js @@ -0,0 +1,63 @@ +//current command in ascii decimal +let currentcmd = [0,0,0] +let currentfile = ""; +const sleep = ms => new Promise(r => setTimeout(r, ms)); + +Module['print'] = function(text) { console.log(text); } + +Module['preRun'] = function() +{ + function stdin() { return 10 }; + var stdout = null; + var stderr = null; + FS.init(stdin, stdout, stderr); + FS.mount(IDBFS, {}, "/home/web_user/"); + FS.symlink("/home/web_user", "/save"); +} + +Module['noInitialRun'] = true + +document.addEventListener('click', (ev) => { + console.log("event is captured only once."); + args = [] + if(window.location.href.indexOf("file://") > -1) + { + try + { + args = args.concat(prompt("Enter command line arguments").split(" ")) + } + catch (error) + { + console.log("Error: ", error); + console.log("Failed to concat extra arguments (likely passed nothing for the argument)") + } + } + else + { + parms = new URLSearchParams(window.location.search); + try + { + args = args.concat(parms.get("args").split(" ")) + } + catch (error) + { + console.log("Error: ", error); + console.log("Failed to concat extra arguments (likely passed nothing for the argument)") + } + } + + FS.syncfs(true, function() + { + if(FS.analyzePath("/preload/runhere").exists) + { + FS.symlink("/preload", "/home/web_user/games"); + args = args.concat(["-basedir", "/home/web_user/games"]) + } + else + { + args = args.concat(["-basedir", "/home/web_user/"]) + } + + Module.callMain(args); + }); +}, { once: true });