Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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)
Expand Down
162 changes: 162 additions & 0 deletions README.emscripten.md
Original file line number Diff line number Diff line change
@@ -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
<script src="dropin.js"></script>
```

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 `<fts.h>` support. The recursive rm has to be done using
`<dirent.h>`. 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 `<fts.h>` 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
25 changes: 25 additions & 0 deletions emscripten/launcher/dropin.js
Original file line number Diff line number Diff line change
@@ -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);
};
})();
23 changes: 23 additions & 0 deletions emscripten/launcher/launcher.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Fasttracker II clone WASM launcher</title>
<script src="launcher.js"></script>
</head>
<body>
<h1>Fasttracker II clone WASM launcher</h1>
<form method="get" action="ft2-clone.html">
<input
type="url"
placeholder="Mod URL"
name="ft2c_load_url"
style="min-width: 50%;">
<input
type="text"
placeholder="File name override (optional)"
name="ft2c_load_filename">
<button type="submit">Go</button>
</form>
</body>
</html>
43 changes: 39 additions & 4 deletions src/ft2_diskop.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
#else
#include <sys/types.h>
#include <sys/stat.h>
#include <fts.h> // for fts_open() and stuff in recursiveDelete()
#include <sys/wait.h>
#include <errno.h>
#ifndef __EMSCRIPTEN__
#include <fts.h> // for recursiveDelete()
#endif
#include <unistd.h>
#include <dirent.h>
#endif
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 };

Expand Down Expand Up @@ -541,6 +575,7 @@ static bool deleteDirRecursive(UNICHAR *strU)
fts_close(ftsp);

return ret;
#endif
}

static bool makeDirAnsi(char *str)
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down
Loading