Threaded emscripten fixes (#17614)

* Actually read CLI args in emscripten

* Fix fetchfs manifest parsing, increase download chunk size

The chunk size should probably be made a parameter in the future.  The
larger chunk size trades longer hitches for fewer hitches.

* Add exec command driver and API functions for emscripten.

Under WASMFS, stdin/stdout can't be customized the way they can with
the JS FS implementation.  Also, this approach frees up stdin/stdout
and simplifies interaction with the command interface for web embedders.

* fixup upload paths, show use of new emscripten cmd interface

* Add JS library function names to EXPORTS as well as EXPORTED_FUNCTIONS for older emsdk versions
This commit is contained in:
Joe Osborn 2025-02-24 09:25:05 -08:00 committed by GitHub
parent 55b59262b9
commit c413bcc626
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 146 additions and 73 deletions

View File

@ -102,9 +102,9 @@ OBJDIR := obj-emscripten
EXPORTED_FUNCTIONS = _main,_malloc,_free,_cmd_savefiles,_cmd_save_state,_cmd_load_state,_cmd_undo_save_state,_cmd_undo_load_state,_cmd_take_screenshot,\ EXPORTED_FUNCTIONS = _main,_malloc,_free,_cmd_savefiles,_cmd_save_state,_cmd_load_state,_cmd_undo_save_state,_cmd_undo_load_state,_cmd_take_screenshot,\
_cmd_toggle_menu,_cmd_reload_config,_cmd_toggle_grab_mouse,_cmd_toggle_game_focus,_cmd_reset,_cmd_toggle_pause,_cmd_pause,_cmd_unpause,\ _cmd_toggle_menu,_cmd_reload_config,_cmd_toggle_grab_mouse,_cmd_toggle_game_focus,_cmd_reset,_cmd_toggle_pause,_cmd_pause,_cmd_unpause,\
_cmd_set_volume,_cmd_set_shader,_cmd_cheat_set_code,_cmd_cheat_get_code,_cmd_cheat_toggle_index,_cmd_cheat_get_code_state,_cmd_cheat_realloc,\ _cmd_set_volume,_cmd_set_shader,_cmd_cheat_set_code,_cmd_cheat_get_code,_cmd_cheat_toggle_index,_cmd_cheat_get_code_state,_cmd_cheat_realloc,\
_cmd_cheat_get_size,_cmd_cheat_apply_cheats _cmd_cheat_get_size,_cmd_cheat_apply_cheats,EmscriptenSendCommand,EmscriptenReceiveCommandReply
EXPORTS := callMain,FS,PATH,ERRNO_CODES,ENV,stringToNewUTF8,UTF8ToString,Browser,GL EXPORTS := callMain,FS,PATH,ERRNO_CODES,ENV,stringToNewUTF8,UTF8ToString,Browser,GL,EmscriptenSendCommand,EmscriptenReceiveCommandReply
LIBS := -s USE_ZLIB=1 -lbrowser.js LIBS := -s USE_ZLIB=1 -lbrowser.js

View File

@ -337,7 +337,7 @@ command_t* command_stdin_new(void)
command_t *cmd; command_t *cmd;
command_stdin_t *stdincmd; command_stdin_t *stdincmd;
#ifndef _WIN32 #if !(defined(_WIN32) || defined(EMSCRIPTEN))
#ifdef HAVE_NETWORKING #ifdef HAVE_NETWORKING
if (!socket_nonblock(STDIN_FILENO)) if (!socket_nonblock(STDIN_FILENO))
return NULL; return NULL;
@ -363,6 +363,61 @@ command_t* command_stdin_new(void)
} }
#endif #endif
#if defined(EMSCRIPTEN)
void PlatformEmscriptenCommandReply(const char *, size_t);
int PlatformEmscriptenCommandRead(char **, size_t);
typedef struct
{
char command_buf[CMD_BUF_SIZE];
} command_emscripten_t;
static void emscripten_command_reply(command_t *_cmd,
const char *s, size_t len)
{
PlatformEmscriptenCommandReply(s, len);
}
static void emscripten_command_free(command_t *handle)
{
free(handle->userptr);
free(handle);
}
static void command_emscripten_poll(command_t *handle)
{
command_emscripten_t *emscriptencmd = (command_emscripten_t*)handle->userptr;
ptrdiff_t msg_len = PlatformEmscriptenCommandRead((char **)(&emscriptencmd->command_buf), CMD_BUF_SIZE);
if (msg_len == 0)
return;
command_parse_msg(handle, emscriptencmd->command_buf);
}
command_t* command_emscripten_new(void)
{
command_t *cmd;
command_emscripten_t *emscriptencmd;
cmd = (command_t*)calloc(1, sizeof(command_t));
emscriptencmd = (command_emscripten_t*)calloc(1, sizeof(command_emscripten_t));
if (!cmd)
return NULL;
if (!emscriptencmd)
{
free(cmd);
return NULL;
}
cmd->userptr = emscriptencmd;
cmd->poll = command_emscripten_poll;
cmd->replier = emscripten_command_reply;
cmd->destroy = emscripten_command_free;
return cmd;
}
#endif
bool command_get_config_param(command_t *cmd, const char* arg) bool command_get_config_param(command_t *cmd, const char* arg)
{ {
size_t _len; size_t _len;

View File

@ -329,11 +329,20 @@ struct rarch_state;
bool command_event(enum event_command action, void *data); bool command_event(enum event_command action, void *data);
/* Constructors for the supported drivers */ /* Constructors for the supported drivers */
#ifdef HAVE_NETWORK_CMD
command_t* command_network_new(uint16_t port); command_t* command_network_new(uint16_t port);
command_t* command_stdin_new(void);
command_t* command_uds_new(void);
bool command_network_send(const char *cmd_); bool command_network_send(const char *cmd_);
#endif
#ifdef HAVE_STDIN_CMD
command_t* command_stdin_new(void);
#endif
#ifdef LAKKA
command_t* command_uds_new(void);
#endif
#ifdef EMSCRIPTEN
command_t* command_emscripten_new(void);
#endif
void command_event_set_mixer_volume( void command_event_set_mixer_volume(
settings_t *settings, settings_t *settings,

View File

@ -12,7 +12,9 @@ var LibraryPlatformEmscripten = {
RPE.powerState.dischargeTime = Number.isFinite(e.target.dischargingTime) ? e.target.dischargingTime : 0x7FFFFFFF; RPE.powerState.dischargeTime = Number.isFinite(e.target.dischargingTime) ? e.target.dischargingTime : 0x7FFFFFFF;
RPE.powerState.level = e.target.level; RPE.powerState.level = e.target.level;
RPE.powerState.charging = e.target.charging; RPE.powerState.charging = e.target.charging;
} },
command_queue:[],
command_reply_queue:[],
}, },
PlatformEmscriptenPowerStateInit: function() { PlatformEmscriptenPowerStateInit: function() {
@ -49,6 +51,15 @@ var LibraryPlatformEmscripten = {
PlatformEmscriptenGetFreeMem: function() { PlatformEmscriptenGetFreeMem: function() {
if (!performance.memory) return 0; if (!performance.memory) return 0;
return (performance.memory.jsHeapSizeLimit || 0) - (performance.memory.usedJSHeapSize || 0); return (performance.memory.jsHeapSizeLimit || 0) - (performance.memory.usedJSHeapSize || 0);
},
$EmscriptenSendCommand__deps:["PlatformEmscriptenCommandRaiseFlag"],
$EmscriptenSendCommand: function(str) {
RPE.command_queue.push(str);
_PlatformEmscriptenCommandRaiseFlag();
},
$EmscriptenReceiveCommandReply: function() {
return RPE.command_reply_queue.shift();
} }
}; };

View File

@ -91,6 +91,33 @@ bool PlatformEmscriptenPowerStateGetCharging(void);
uint64_t PlatformEmscriptenGetTotalMem(void); uint64_t PlatformEmscriptenGetTotalMem(void);
uint64_t PlatformEmscriptenGetFreeMem(void); uint64_t PlatformEmscriptenGetFreeMem(void);
void PlatformEmscriptenCommandReply(const char *msg, size_t len) {
MAIN_THREAD_EM_ASM({
var message = UTF8ToString($0,$1);
RPE.command_reply_queue.push(message);
}, msg, len);
}
static bool command_flag = false;
size_t PlatformEmscriptenCommandRead(char **into, size_t max_len) {
if(!command_flag) { return 0; }
return MAIN_THREAD_EM_ASM_INT({
var next_command = RPE.command_queue.shift();
var length = lengthBytesUTF8(next_command);
if(length > $2) {
console.error("[CMD] Command too long, skipping",next_command);
return 0;
}
stringToUTF8(next_command, $1, $2);
if(RPE.command_queue.length == 0) {
setValue($0, 0, 'i8');
}
return length;
}, &command_flag, into, max_len);
}
void PlatformEmscriptenCommandRaiseFlag() {
command_flag = true;
}
/* begin exported functions */ /* begin exported functions */
/* saves and states */ /* saves and states */
@ -350,32 +377,46 @@ void PlatformEmscriptenMountFilesystems(void *info) {
*/ */
int max_line_len = 1024; int max_line_len = 1024;
printf("[FetchFS] read fetch manifest from %s\n",fetch_manifest); printf("[FetchFS] read fetch manifest from %s\n",fetch_manifest);
FILE *file = fopen(fetch_manifest, O_RDONLY); FILE *file = fopen(fetch_manifest, "r");
if(!file) {
printf("[FetchFS] missing manifest file\n");
abort();
}
char *line = calloc(sizeof(char), max_line_len); char *line = calloc(sizeof(char), max_line_len);
size_t len = 0; size_t len = max_line_len;
while (getline(&line, &len, file) != -1) { while (getline(&line, &len, file) != -1) {
char *path = strstr(line, " "); char *path = strstr(line, " ");
backend_t fetch; backend_t fetch;
int fd; int fd;
if (!path) { if(len <= 2 || !path) {
printf("Manifest file has invalid line %s\n",line); printf("[FetchFS] Manifest file has invalid line %s\n",line);
return; continue;
} }
*path = '\0'; *path = '\0';
path += 1; path += 1;
printf("Fetch %s from %s\n", path, line); path[strcspn(path, "\r\n")] = '\0';
printf("[FetchFS] Fetch %s from %s\n", path, line);
{ {
char *parent = strdup(path); char *parent = strdup(path);
path_parent_dir(parent, strlen(parent)); path_parent_dir(parent, strlen(parent));
if(!path_mkdir(parent)) { if(!path_mkdir(parent)) {
printf("mkdir error %d\n",errno); printf("[FetchFS] mkdir error %d\n",errno);
abort(); abort();
} }
free(parent); free(parent);
} }
fetch = wasmfs_create_fetch_backend(line, 8*1024*1024); fetch = wasmfs_create_fetch_backend(line, 16*1024*1024);
if(!fetch) {
printf("[FetchFS] couldn't create fetch backend\n");
abort();
}
fd = wasmfs_create_file(path, 0777, fetch); fd = wasmfs_create_file(path, 0777, fetch);
if(!fd) {
printf("[FetchFS] couldn't create fetch file\n");
abort();
}
close(fd); close(fd);
len = max_line_len;
} }
fclose(file); fclose(file);
free(line); free(line);
@ -433,7 +474,9 @@ void emscripten_bootup_mainloop(void *argptr) {
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
args_t *args = calloc(sizeof(args_t), 1); args_t *args = calloc(sizeof(args_t), 1);
args->argc = argc;
args->argv = argv;
PlatformEmscriptenWatchCanvasSize(); PlatformEmscriptenWatchCanvasSize();
PlatformEmscriptenPowerStateInit(); PlatformEmscriptenPowerStateInit();

View File

@ -5104,10 +5104,14 @@ void input_driver_init_command(input_driver_state_t *input_st,
} }
#endif #endif
#ifdef HAVE_LAKKA #if defined(HAVE_LAKKA)
if (!(input_st->command[2] = command_uds_new())) if (!(input_st->command[2] = command_uds_new()))
RARCH_ERR("Failed to initialize the UDS command interface.\n"); RARCH_ERR("Failed to initialize the UDS command interface.\n");
#elif defined(EMSCRIPTEN)
if (!(input_st->command[2] = command_emscripten_new()))
RARCH_ERR("Failed to initialize the emscripten command interface.\n");
#endif #endif
} }
void input_driver_deinit_command(input_driver_state_t *input_st) void input_driver_deinit_command(input_driver_state_t *input_st)

View File

@ -14,64 +14,15 @@ var Module = {
noImageDecoding: true, noImageDecoding: true,
noAudioDecoding: true, noAudioDecoding: true,
encoder: new TextEncoder(), retroArchSend: function(msg) {
message_queue: [], this.EmscriptenSendCommand(msg);
message_out: [], },
message_accum: "", retroArchRecv: function() {
return this.EmscriptenReceiveCommandReply();
retroArchSend: function(msg) { },
let bytes = this.encoder.encode(msg + "\n");
this.message_queue.push([bytes, 0]);
},
retroArchRecv: function() {
let out = this.message_out.shift();
if (out == null && this.message_accum != "") {
out = this.message_accum;
this.message_accum = "";
}
return out;
},
preRun: [ preRun: [
function(module) { function(module) {
Module.ENV['OPFS'] = "/home/web_user/retroarch"; Module.ENV['OPFS'] = "/home/web_user/retroarch";
},
function(module) {
function stdin() {
// Return ASCII code of character, or null if no input
while (module.message_queue.length > 0) {
var msg = module.message_queue[0][0];
var index = module.message_queue[0][1];
if (index >= msg.length) {
module.message_queue.shift();
} else {
module.message_queue[0][1] = index + 1;
// assumption: msg is a uint8array
return msg[index];
}
}
return null;
}
function stdout(c) {
if (c == null) {
// flush
if (module.message_accum != "") {
module.message_out.push(module.message_accum);
module.message_accum = "";
}
} else {
let s = String.fromCharCode(c);
if (s == "\n") {
if (module.message_accum != "") {
module.message_out.push(module.message_accum);
module.message_accum = "";
}
} else {
module.message_accum = module.message_accum + s;
}
}
}
module.FS.init(stdin, stdout);
} }
], ],
postRun: [], postRun: [],

View File

@ -60,7 +60,7 @@ onmessage = async (msg) => {
} }
postMessage({command:"loaded_bundle", time:resp.headers.get("last-modified")}); postMessage({command:"loaded_bundle", time:resp.headers.get("last-modified")});
} else if(msg.data.command == "upload_file") { } else if(msg.data.command == "upload_file") {
await writeFile("/home/web_user/retroarch/userdata/content/"+msg.data.name, new Uint8Array(msg.data.data)); await writeFile("userdata/content/"+msg.data.name, new Uint8Array(msg.data.data));
postMessage({command:"uploaded_file",name:msg.data.name}); postMessage({command:"uploaded_file",name:msg.data.name});
} }
} }