diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a8bdafc..70dff116 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,13 +2,35 @@ cmake_minimum_required(VERSION 3.7) project(ft2-clone) +if(CMAKE_SYSTEM_NAME MATCHES "Emscripten") + set(SYS_EM 1) + add_compile_options( + -pthread + --use-port=sdl2 + ) + add_link_options( + -pthread + # -sUSE_WEBGL2 + -sASYNCIFY + -sUSE_SDL=2 + -sUSE_PTHREADS=1 + -sPTHREAD_POOL_SIZE=4 + -sALLOW_MEMORY_GROWTH + -sINCLUDE_FULL_LIBRARY + # -sMALLOC=mimalloc + ) + SET(CMAKE_EXECUTABLE_SUFFIX .html) +endif() + +include(CMakeDependentOption) option(EXTERNAL_LIBFLAC "use external(system) flac library" OFF) +cmake_dependent_option(ENABLE_RTMIDI "enable MIDI support" ON SYS_EM OFF) find_package(SDL2 REQUIRED) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${ft2-clone_SOURCE_DIR}/release/other/") +string(TOLOWER ${CMAKE_BUILD_TYPE} ft2-clone_buildtype) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${ft2-clone_SOURCE_DIR}/${ft2-clone_buildtype}/other/") file(GLOB ft2-clone_SRC - "${ft2-clone_SOURCE_DIR}/src/rtmidi/*.cpp" "${ft2-clone_SOURCE_DIR}/src/*.c" "${ft2-clone_SOURCE_DIR}/src/gfxdata/*.c" "${ft2-clone_SOURCE_DIR}/src/mixer/*.c" @@ -33,7 +55,6 @@ target_link_libraries(ft2-clone PRIVATE m Threads::Threads ${SDL2_LIBRARIES}) target_compile_definitions(ft2-clone - PRIVATE HAS_MIDI PRIVATE HAS_LIBFLAC) if(UNIX) @@ -45,13 +66,22 @@ if(UNIX) target_compile_definitions(ft2-clone PRIVATE __MACOSX_CORE__) else() - target_link_libraries(ft2-clone - PRIVATE asound) - target_compile_definitions(ft2-clone - PRIVATE __LINUX_ALSA__) + if(ENABLE_RTMIDI) + target_link_libraries(ft2-clone + PRIVATE asound) + target_compile_definitions(ft2-clone + PRIVATE __LINUX_ALSA__) + endif() endif() endif() +if(ENABLE_RTMIDI) + file(GLOB ft2-rtmidi_SRCS + "${ft2-clone_SOURCE_DIR}/src/rtmidi/*.cpp") + target_sources(ft2-clone PRIVATE ${ft2-rtmidi_SRCS}) + target_compile_definitions(ft2-clone PRIVATE HAS_MIDI) +endif() + if(EXTERNAL_LIBFLAC) find_package(PkgConfig REQUIRED) pkg_check_modules(FLAC REQUIRED IMPORTED_TARGET flac) diff --git a/README.emscripten.md b/README.emscripten.md new file mode 100644 index 00000000..4357e49e --- /dev/null +++ b/README.emscripten.md @@ -0,0 +1,162 @@ +# ft2-clone emscripten note +Notes on the experimental Emscripten target support. Try out the working +prototype: + +https://snart.me/demos/emscripten/ft2-clone/ + +(the track from: https://modarchive.org/index.php?request=view_by_moduleid&query=142756) + +## Building +Use ccmake to set `CMAKE_BUILD_TYPE` to either debug or release. Default is +debug. After editing, press **c** to configure. **g** to generate the Makefile +recipe and exit. Then go to the build directory and run make. + +```sh +source "YOUR_EMSDK_PATH/emsdk_env.sh" + +emcmake ccmake -B build . +cd build +emmake make -j$(nproc) +``` + +The output will be either in `debug/other` or `release/other`. Host following +files: + +- ft2-clone.worker.js +- ft2-clone.js +- ft2-clone.wasm +- ft2-clone.html + +You probably want to link `index.html` to `ft2-clone.html`: + +```sh +ln -s ft2-clone.html index.html +``` + +### Embedding mods +In the ccmake step, press **t** to toggle the advanced mode. Edit +`CMAKE_EXE_LINKER_FLAGS` to embed mods. Example: + +``` +--embed-file pink_noise.xm +``` + +To make ft2-clone to load the mod on init, the file can be passed in the command +line arguments. Emscripten does not provide an easy way to do this... I normally +edit the output html file. Before loading the main js file, the `Module` object +can be defined to pass parameters to the WASM module. The argument vector is one +of them. In the output html file, add something like this: + +```js +//... + var Module = { + + arguments: [ "pink_noise.xm" ], + + print: (function() { +//... +``` + +where `"pink_noise.xm"` is the file you embeded in the module and want ft2-clone +to load and play on init. + +### Using the launcher +Copy [/emscripten/launcher/dropin.js](/emscripten/launcher/dropin.js) to the +same directory as the ft2-clone.html. Then add the following anywhere in the +body tag. + +```html + +``` + +The script will parse `ft2c_load_url` and `ft2c_load_filename` in the query +string and pass the file as a command line argument. See +[/emscripten/launcher/launcher.html](/emscripten/launcher/launcher.html) for +detail. + +## Hosting files (header issue) +In order to support SDL threads, pthread option is used. The caveat is that +Emscripten uses SharedArrayBuffer to have a shared memory space for the +threads[^1] and SharedArrayBuffer has been disabled by default since the +discovery of Spectre vulnerability[^2]. Therefore, to host the output files, +the special headers need to be sent by the web server: + +``` +cross-origin-embedder-policy: require-corp +cross-origin-opener-policy: same-origin +``` + +The major implementations support addition of arbitrary response headers. Here's +an example config for Nginx: + +``` + location /demos/emscripten/ { + add_header "Cross-Origin-Opener-Policy" "same-origin"; + add_header "Cross-Origin-Embedder-Policy" "require-corp"; + } +``` + +If you use http-server npm package to debug emscripten programs, you may need to +use the forks below as http-server has no curl `-H` option equivalent. + +1. https://github.com/dxdxdt/http-server/commit/4a525a75c0aca9bf567dd0ffc2a0cfe74f29197b +1. https://github.com/http-party/http-server/pull/885 + +My version is in line with semantics of curl. Here's how you run it to host the +output files locally: + +```sh +npm start -- /PATH/TO/ft2-clone \ + -H 'Cross-Origin-Opener-Policy: same-origin' \ + -H 'Cross-Origin-Embedder-Policy: require-corp' +``` + +(Hopefully, my version will make it to the main branch) + +## Debugging +Setting `CMAKE_BUILD_TYPE` to "debug" will produce debugging symbols. Chromium +can be used to utilise them: breakpoints, threads, variable inspection ... + +## Other dev notes +### `emscripten_sleep()` yielding rather than `emscripten_set_main_loop()` +For some reason, having a main loop function and letting the JS engine call it +makes the program unstable, especially when it shows a dialog. It probably has +something to do with the logic in `hpc_wait()`. So `-sASYNCIFY` had to be used +and, quite frankly, performs better than doing the main loop using +`requestAnimationFrame()`. + +### No MIDI support +The modern browsers do support MIDI[^4], but Emscripten does not(as of writing +of this note). Even when Web MIDI gets comes to Emscripten, it will probably be +not in the form of ALSA backend, so a separate driver will need to be written. + +### BUG: mouse and scaling problems +**Work in progress...**: the mouse support isn't quite right at the moment. +There seems to be some issues with scaling factors, too: Chromium honors scaling +factor of the desktop whilst Firefox does not. + +### BUG: out of memory in most of menus +**Work in progress...**: the diskop is broken. The out of memory dialog will be +shown, although the error message is obviously bogus: the `ABORTING_MALLOC` and +`ALLOW_MEMORY_GROWTH` options won't allow the application to continue to show +the dialog on memory allocation error[^3]. + +### QUIRK: placeholder `deleteDirRecursive()` implementation +Emcripten has no `` support. The recursive rm has to be done using +``. I didn't see the need to put a good working implementation because + +1. Emscripten "emulates" the filesystem strictly in memory. Having a full-blown + directory structures is a rare case +1. had no time to write a good implementation myself that does not involve + recursion + +Talking about over-engineering. The placeholder implementation is probably +faster than the original `` one, anyways. Trying to use it on Emscripten +will most likely to fail because there's no executable(`/bin/rm`) on the file +system. This is intentional. I just didn't want to leave the function blank. + + +[^1]: https://emscripten.org/docs/porting/pthreads.html +[^2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements +[^3]: https://emscripten.org/docs/tools_reference/settings_reference.html#aborting-malloc +[^4]: https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API diff --git a/emscripten/launcher/dropin.js b/emscripten/launcher/dropin.js new file mode 100644 index 00000000..22470f68 --- /dev/null +++ b/emscripten/launcher/dropin.js @@ -0,0 +1,25 @@ +(() => { + function extract_filename (str) { + const sep = str.lastIndexOf('/'); + return sep < 0 ? str : str.substr(sep + 1); + } + + const params = Object.fromEntries(new URLSearchParams(location.search)); + let filename; + + if (!('ft2c_load_url' in params)) { + return; + } + + if ('ft2c_load_filename' in params) { + filename = params.ft2c_load_filename; + } + else { + filename = extract_filename(params.ft2c_load_url); + } + + Module.arguments = [ filename ]; + Module.preRun = () => { + FS.createPreloadedFile('.', filename, params.ft2c_load_url, true, false); + }; +})(); \ No newline at end of file diff --git a/emscripten/launcher/launcher.html b/emscripten/launcher/launcher.html new file mode 100644 index 00000000..a5847687 --- /dev/null +++ b/emscripten/launcher/launcher.html @@ -0,0 +1,23 @@ + + + + + Fasttracker II clone WASM launcher + + + +

Fasttracker II clone WASM launcher

+
+ + + +
+ + \ No newline at end of file diff --git a/src/ft2_diskop.c b/src/ft2_diskop.c index e0a899aa..0aa714b0 100644 --- a/src/ft2_diskop.c +++ b/src/ft2_diskop.c @@ -17,7 +17,11 @@ #else #include #include -#include // for fts_open() and stuff in recursiveDelete() +#include +#include +#ifndef __EMSCRIPTEN__ +#include // for recursiveDelete() +#endif #include #include #endif @@ -123,7 +127,7 @@ int32_t getFileSize(UNICHAR *fileNameU) // returning -1 = filesize over 2GB if (fSize > INT32_MAX) return -1; // -1 = ">2GB" flag - + return (int32_t)fSize; } @@ -499,6 +503,36 @@ bool fileExistsAnsi(char *str) static bool deleteDirRecursive(UNICHAR *strU) { +#ifdef __EMSCRIPTEN__ + bool ret = false; + pid_t child; + + child = fork(); + if (child == 0) { + // child + static const char *EXEC_RM = "/bin/rm"; + const char *argv[] = { "-rf", "--", (const char*)strU, NULL }; + + close(STDIN_FILENO); + close(STDOUT_FILENO); + execv(EXEC_RM, (char *const*)argv); + + perror(EXEC_RM); + exit(errno == ENOENT ? 126 : 127); + } + else if (child > 0) { + // parent + int wstatus; + + waitpid(child, &wstatus, 0); + ret = WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0; + } + else { + // error + } + + return ret; +#else FTSENT *curr; char *files[] = { (char *)(strU), NULL }; @@ -541,6 +575,7 @@ static bool deleteDirRecursive(UNICHAR *strU) fts_close(ftsp); return ret; +#endif } static bool makeDirAnsi(char *str) @@ -1270,7 +1305,7 @@ static uint8_t handleEntrySkip(UNICHAR *nameU, bool isDir) char *name = unicharToCp437(nameU, false); if (name == NULL) return true; - + if (name[0] == '\0') goto skipEntry; @@ -1694,7 +1729,7 @@ static uint8_t numDigits32(uint32_t x) if (x >= 1000) return 4; if (x >= 100) return 3; if (x >= 10) return 2; - + return 1; } diff --git a/src/ft2_hpc.c b/src/ft2_hpc.c index 53829749..15f02c0a 100644 --- a/src/ft2_hpc.c +++ b/src/ft2_hpc.c @@ -16,6 +16,9 @@ #else #include #endif +#ifdef __EMSCRIPTEN__ +#include +#endif #include #include #include @@ -163,7 +166,20 @@ void hpc_Wait(hpc_t *hpc) int32_t microSecsLeft = (int32_t)((timeLeft32 * hpcFreq.dFreqMulMicro) + 0.5); // rounded if (microSecsLeft > 0) +#ifdef __EMSCRIPTEN__ + if (true) + { + // The right way + emscripten_sleep(microSecsLeft / 1000); + } + else + { + // Just using emscripten_sleep() to yield to the main thread + emscripten_sleep(1); + } +#else usleep(microSecsLeft); +#endif } // set next end time diff --git a/src/ft2_mouse.c b/src/ft2_mouse.c index cec60699..069e04c4 100644 --- a/src/ft2_mouse.c +++ b/src/ft2_mouse.c @@ -867,11 +867,15 @@ void readMouseXY(void) mouse.absX = mx; mouse.absY = my; +#ifndef __EMSCRIPTEN__ + // On Emscripten, SDL_GetWindowPosition() returns something different + // convert desktop coords to window coords SDL_GetWindowPosition(video.window, &windowX, &windowY); mx -= windowX; my -= windowY; +#endif } mouse.rawX = mx; diff --git a/src/ft2_video.c b/src/ft2_video.c index 036fd314..333ab247 100644 --- a/src/ft2_video.c +++ b/src/ft2_video.c @@ -917,10 +917,10 @@ bool setupWindow(void) SDL_GetDesktopDisplayMode(di, &dm); video.dMonitorRefreshRate = (double)dm.refresh_rate; - +#ifdef __EMSCRIPTEN__ if (dm.refresh_rate >= 59 && dm.refresh_rate <= 61) video.vsync60HzPresent = true; - +#endif if (config.windowFlags & FORCE_VSYNC_OFF) video.vsync60HzPresent = false;