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;