(Emscripten) Modularize the JavaScript and clean up the web build (#15688)

* Increase emscripten stack size and decrease path size to fix emscripten builds broken since de45fc2

* use modularize flags for better-behaved javascript output

* makefile and loader changes

* use specialHTMLTargets to support modular access to canvas

* bind key events to canvas, not document

This way focus means focus and we can have multiple RA instances in
one page.

* Work around an emscripten bug in strict mode

* (Emscripten) Use console.error() for error messages

* increase asyncify stack size

* Fix `-lm` flag-related compile warnings in emscripten

---------

Co-authored-by: Rob Loach <robloach@gmail.com>
This commit is contained in:
Joe Osborn 2023-11-02 13:25:50 -07:00 committed by GitHub
parent 8523eaf5c0
commit 862bebf687
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 136 deletions

View File

@ -2,6 +2,7 @@ HAVE_STATIC_DUMMY ?= 0
ifeq ($(TARGET),) ifeq ($(TARGET),)
ifeq ($(LIBRETRO),) ifeq ($(LIBRETRO),)
TARGET := retroarch.js TARGET := retroarch.js
LIBRETRO = dummy
else else
TARGET := $(LIBRETRO)_libretro.js TARGET := $(LIBRETRO)_libretro.js
endif endif
@ -48,7 +49,6 @@ HAVE_7ZIP = 1
HAVE_BSV_MOVIE = 1 HAVE_BSV_MOVIE = 1
HAVE_AL = 1 HAVE_AL = 1
# WARNING -- READ BEFORE ENABLING # WARNING -- READ BEFORE ENABLING
# The rwebaudio driver is known to have several audio bugs, such as # The rwebaudio driver is known to have several audio bugs, such as
# minor crackling, or the entire page freezing/crashing. # minor crackling, or the entire page freezing/crashing.
@ -78,8 +78,11 @@ OBJDIR := obj-emscripten
#if you compile with SDL2 flag add this Emscripten flag "-s USE_SDL=2" to LDFLAGS: #if you compile with SDL2 flag add this Emscripten flag "-s USE_SDL=2" to LDFLAGS:
LIBS := -s USE_ZLIB=1 LIBS := -s USE_ZLIB=1
LDFLAGS := -L. --no-heap-copy -s $(LIBS) -s TOTAL_MEMORY=$(MEMORY) -s NO_EXIT_RUNTIME=0 -s FULL_ES2=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain']" \ LDFLAGS := -L. --no-heap-copy -s $(LIBS) -s TOTAL_MEMORY=$(MEMORY) -s NO_EXIT_RUNTIME=0 -s FULL_ES2=1 \
-s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS="['_main', '_malloc', '_cmd_savefiles', '_cmd_save_state', '_cmd_load_state', '_cmd_take_screenshot']" \ -s "EXPORTED_RUNTIME_METHODS=['callMain', 'FS', 'PATH', 'ERRNO_CODES']" \
-s ALLOW_MEMORY_GROWTH=1 -s "EXPORTED_FUNCTIONS=['_main', '_malloc', '_cmd_savefiles', '_cmd_save_state', '_cmd_load_state', '_cmd_take_screenshot']" \
-s MODULARIZE=1 -s EXPORT_ES6=1 -s EXPORT_NAME="$(LIBRETRO)" \
-s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 \
--js-library emscripten/library_errno_codes.js \ --js-library emscripten/library_errno_codes.js \
--js-library emscripten/library_rwebcam.js --js-library emscripten/library_rwebcam.js
@ -102,7 +105,10 @@ else
endif endif
ifeq ($(ASYNC), 1) ifeq ($(ASYNC), 1)
LDFLAGS += -s ASYNCIFY=$(ASYNC) LDFLAGS += -s ASYNCIFY=$(ASYNC) -s ASYNCIFY_STACK_SIZE=8192
ifeq ($(DEBUG), 1)
LDFLAGS += -s ASYNCIFY_DEBUG=1 # -s ASYNCIFY_ADVISE
endif
endif endif
ifeq ($(HAVE_SDL2), 1) ifeq ($(HAVE_SDL2), 1)
@ -127,8 +133,8 @@ ifneq ($(V), 1)
endif endif
ifeq ($(DEBUG), 1) ifeq ($(DEBUG), 1)
LDFLAGS += -O0 -g LDFLAGS += -O0 -g -gsource-map -s SAFE_HEAP=1 -s STACK_OVERFLOW_CHECK=2 -s ASSERTIONS=1
CFLAGS += -O0 -g CFLAGS += -O0 -g -gsource-map -s SAFE_HEAP=1 -s SAFE_HEAP_LOG=1 -s STACK_OVERFLOW_CHECK=2 -s ASSERTIONS=1
else else
LDFLAGS += -O3 -s WASM=1 LDFLAGS += -O3 -s WASM=1
# WARNING: some optimizations can break some cores (ex: LTO breaks tyrquake) # WARNING: some optimizations can break some cores (ex: LTO breaks tyrquake)
@ -139,7 +145,12 @@ else
CFLAGS += -O3 CFLAGS += -O3
endif endif
CFLAGS += -Wall -I. -Ilibretro-common/include -std=gnu99 $(LIBS) #\ # 128 * 1024, double the usual emscripten stack size
LDFLAGS += -s STACK_SIZE=131072
LDFLAGS += --extern-pre-js emscripten/pre.js
CFLAGS += -Wall -I. -Ilibretro-common/include -std=gnu99 #\
# -s EXPORTED_FUNCTIONS="['_main', '_malloc', '_cmd_savefiles', '_cmd_save_state', '_cmd_take_screenshot']" # -s EXPORTED_FUNCTIONS="['_main', '_malloc', '_cmd_savefiles', '_cmd_save_state', '_cmd_take_screenshot']"
RARCH_OBJ := $(addprefix $(OBJDIR)/,$(OBJ)) RARCH_OBJ := $(addprefix $(OBJDIR)/,$(OBJ))

View File

@ -247,8 +247,8 @@ for f in `ls -v *_${platform}.${EXT}`; do
if [ $MAKEFILE_GRIFFIN = "yes" ]; then if [ $MAKEFILE_GRIFFIN = "yes" ]; then
make -C ../ -f Makefile.griffin $OPTS platform=${platform} $whole_archive $big_stack -j3 || exit 1 make -C ../ -f Makefile.griffin $OPTS platform=${platform} $whole_archive $big_stack -j3 || exit 1
elif [ $PLATFORM = "emscripten" ]; then elif [ $PLATFORM = "emscripten" ]; then
echo "BUILD COMMAND: make -C ../ -f Makefile.emscripten PTHREAD=$pthread ASYNC=$async LTO=$lto -j7 TARGET=${name}_libretro.js" echo "BUILD COMMAND: make -C ../ -f Makefile.emscripten PTHREAD=$pthread ASYNC=$async LTO=$lto -j7 LIBRETRO=${name} TARGET=${name}_libretro.js"
make -C ../ -f Makefile.emscripten $OPTS PTHREAD=$pthread ASYNC=$async LTO=$lto -j7 TARGET=${name}_libretro.js || exit 1 make -C ../ -f Makefile.emscripten $OPTS PTHREAD=$pthread ASYNC=$async LTO=$lto -j7 LIBRETRO=${name} TARGET=${name}_libretro.js || exit 1
elif [ $PLATFORM = "unix" ]; then elif [ $PLATFORM = "unix" ]; then
make -C ../ -f Makefile LINK=g++ $whole_archive $big_stack -j3 || exit 1 make -C ../ -f Makefile LINK=g++ $whole_archive $big_stack -j3 || exit 1
elif [ $PLATFORM = "ctr" ]; then elif [ $PLATFORM = "ctr" ]; then

2
emscripten/pre.js Normal file
View File

@ -0,0 +1,2 @@
// To work around a bug in emscripten's polyfills for setImmediate in strict mode
var setImmediate;

View File

@ -160,8 +160,12 @@ int main(int argc, char *argv[])
{ {
dummyErrnoCodes(); dummyErrnoCodes();
emscripten_set_canvas_element_size("#canvas", 800, 600); EM_ASM({
emscripten_set_element_css_size("#canvas", 800.0, 600.0); specialHTMLTargets["!canvas"] = Module.canvas;
});
emscripten_set_canvas_element_size("!canvas", 800, 600);
emscripten_set_element_css_size("!canvas", 800.0, 600.0);
emscripten_set_main_loop(emscripten_mainloop, 0, 0); emscripten_set_main_loop(emscripten_mainloop, 0, 0);
rarch_main(argc, argv, NULL); rarch_main(argc, argv, NULL);

View File

@ -70,7 +70,7 @@ static void gfx_ctx_emscripten_get_canvas_size(int *width, int *height)
if (!is_fullscreen) if (!is_fullscreen)
{ {
r = emscripten_get_canvas_element_size("#canvas", width, height); r = emscripten_get_canvas_element_size("!canvas", width, height);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
{ {
@ -105,14 +105,14 @@ static void gfx_ctx_emscripten_check_window(void *data, bool *quit,
if ( (input_width != emscripten->fb_width) if ( (input_width != emscripten->fb_width)
|| (input_height != emscripten->fb_height)) || (input_height != emscripten->fb_height))
{ {
r = emscripten_set_canvas_element_size("#canvas", r = emscripten_set_canvas_element_size("!canvas",
input_width, input_height); input_width, input_height);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
RARCH_ERR("[EMSCRIPTEN/EGL]: error resizing canvas: %d\n", r); RARCH_ERR("[EMSCRIPTEN/EGL]: error resizing canvas: %d\n", r);
/* fix Module.requestFullscreen messing with the canvas size */ /* fix Module.requestFullscreen messing with the canvas size */
r = emscripten_set_element_css_size("#canvas", r = emscripten_set_element_css_size("!canvas",
(double)input_width, (double)input_height); (double)input_width, (double)input_height);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
@ -194,7 +194,7 @@ static void *gfx_ctx_emscripten_init(void *video_driver)
* be grabbed? */ * be grabbed? */
if ( (emscripten->initial_width == 0) if ( (emscripten->initial_width == 0)
|| (emscripten->initial_height == 0)) || (emscripten->initial_height == 0))
emscripten_get_canvas_element_size("#canvas", emscripten_get_canvas_element_size("!canvas",
&emscripten->initial_width, &emscripten->initial_width,
&emscripten->initial_height); &emscripten->initial_height);

View File

@ -291,7 +291,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
rwebinput_generate_lut(); rwebinput_generate_lut();
r = emscripten_set_keydown_callback( r = emscripten_set_keydown_callback(
EMSCRIPTEN_EVENT_TARGET_DOCUMENT, rwebinput, false, "!canvas", rwebinput, false,
rwebinput_keyboard_cb); rwebinput_keyboard_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
{ {
@ -300,7 +300,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
} }
r = emscripten_set_keyup_callback( r = emscripten_set_keyup_callback(
EMSCRIPTEN_EVENT_TARGET_DOCUMENT, rwebinput, false, "!canvas", rwebinput, false,
rwebinput_keyboard_cb); rwebinput_keyboard_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
{ {
@ -309,7 +309,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
} }
r = emscripten_set_keypress_callback( r = emscripten_set_keypress_callback(
EMSCRIPTEN_EVENT_TARGET_DOCUMENT, rwebinput, false, "!canvas", rwebinput, false,
rwebinput_keyboard_cb); rwebinput_keyboard_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
{ {
@ -317,7 +317,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create keypress callback: %d\n", r); "[EMSCRIPTEN/INPUT] failed to create keypress callback: %d\n", r);
} }
r = emscripten_set_mousedown_callback("#canvas", rwebinput, false, r = emscripten_set_mousedown_callback("!canvas", rwebinput, false,
rwebinput_mouse_cb); rwebinput_mouse_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
{ {
@ -325,7 +325,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create mousedown callback: %d\n", r); "[EMSCRIPTEN/INPUT] failed to create mousedown callback: %d\n", r);
} }
r = emscripten_set_mouseup_callback("#canvas", rwebinput, false, r = emscripten_set_mouseup_callback("!canvas", rwebinput, false,
rwebinput_mouse_cb); rwebinput_mouse_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
{ {
@ -333,7 +333,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
"[EMSCRIPTEN/INPUT] failed to create mouseup callback: %d\n", r); "[EMSCRIPTEN/INPUT] failed to create mouseup callback: %d\n", r);
} }
r = emscripten_set_mousemove_callback("#canvas", rwebinput, false, r = emscripten_set_mousemove_callback("!canvas", rwebinput, false,
rwebinput_mouse_cb); rwebinput_mouse_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
{ {
@ -342,7 +342,7 @@ static void *rwebinput_input_init(const char *joypad_driver)
} }
r = emscripten_set_wheel_callback( r = emscripten_set_wheel_callback(
EMSCRIPTEN_EVENT_TARGET_DOCUMENT, rwebinput, false, "!canvas", rwebinput, false,
rwebinput_wheel_cb); rwebinput_wheel_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS) if (r != EMSCRIPTEN_RESULT_SUCCESS)
{ {
@ -651,7 +651,7 @@ static void rwebinput_input_poll(void *data)
static void rwebinput_grab_mouse(void *data, bool state) static void rwebinput_grab_mouse(void *data, bool state)
{ {
if (state) if (state)
emscripten_request_pointerlock("#canvas", EM_TRUE); emscripten_request_pointerlock("!canvas", EM_TRUE);
else else
emscripten_exit_pointerlock(); emscripten_exit_pointerlock();
} }

View File

@ -90,7 +90,7 @@ static INLINE bool bits_any_different(uint32_t *a, uint32_t *b, uint32_t count)
} }
#ifndef PATH_MAX_LENGTH #ifndef PATH_MAX_LENGTH
#if defined(_XBOX1) || defined(_3DS) || defined(PSP) || defined(PS2) || defined(GEKKO)|| defined(WIIU) || defined(__PSL1GHT__) || defined(__PS3__) #if defined(_XBOX1) || defined(_3DS) || defined(PSP) || defined(PS2) || defined(GEKKO)|| defined(WIIU) || defined(__PSL1GHT__) || defined(__PS3__) || defined(HAVE_EMSCRIPTEN)
#define PATH_MAX_LENGTH 512 #define PATH_MAX_LENGTH 512
#else #else
#define PATH_MAX_LENGTH 4096 #define PATH_MAX_LENGTH 4096

View File

@ -6,6 +6,89 @@
var BrowserFS = BrowserFS; var BrowserFS = BrowserFS;
var afs; var afs;
var initializationCount = 0; var initializationCount = 0;
var setImmediate;
var Module = {
noInitialRun: true,
arguments: ["-v", "--menu"],
encoder: new TextEncoder(),
message_queue:[],
message_out:[],
message_accum:"",
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: [
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: [],
onRuntimeInitialized: function()
{
appInitialized();
},
print: function(text)
{
console.log(text);
},
printErr: function(text)
{
console.error(text);
},
canvas: document.getElementById("canvas"),
totalDependencies: 0,
monitorRunDependencies: function(left)
{
this.totalDependencies = Math.max(this.totalDependencies, left);
}
};
function cleanupStorage() function cleanupStorage()
{ {
@ -119,8 +202,8 @@ function setupFileSystem(backend)
mfs.mount('/home/web_user/retroarch/bundle', xfs1); mfs.mount('/home/web_user/retroarch/bundle', xfs1);
mfs.mount('/home/web_user/retroarch/userdata/content/downloads', xfs2); mfs.mount('/home/web_user/retroarch/userdata/content/downloads', xfs2);
BrowserFS.initialize(mfs); BrowserFS.initialize(mfs);
var BFS = new BrowserFS.EmscriptenFS(); var BFS = new BrowserFS.EmscriptenFS(Module.FS, Module.PATH, Module.ERRNO_CODES);
FS.mount(BFS, {root: '/home'}, '/home'); Module.FS.mount(BFS, {root: '/home'}, '/home');
console.log("WEBPLAYER: " + backend + " filesystem initialization successful"); console.log("WEBPLAYER: " + backend + " filesystem initialization successful");
} }
@ -150,11 +233,12 @@ function startRetroArch()
document.getElementById("btnMenu").disabled = false; document.getElementById("btnMenu").disabled = false;
document.getElementById("btnFullscreen").disabled = false; document.getElementById("btnFullscreen").disabled = false;
Module["canvas"] = document.getElementById("canvas");
Module["canvas"].addEventListener("click", () => Module["canvas"].focus());
Module['callMain'](Module['arguments']); Module['callMain'](Module['arguments']);
Module['resumeMainLoop'](); Module['resumeMainLoop']();
document.getElementById('canvas').focus(); Module['canvas'].focus();
} }
function selectFiles(files) function selectFiles(files)
{ {
$('#btnAdd').addClass('disabled'); $('#btnAdd').addClass('disabled');
@ -184,66 +268,13 @@ function selectFiles(files)
function uploadData(data,name) function uploadData(data,name)
{ {
var dataView = new Uint8Array(data); var dataView = new Uint8Array(data);
FS.createDataFile('/', name, dataView, true, false); Module.FS.createDataFile('/', name, dataView, true, false);
var data = FS.readFile(name,{ encoding: 'binary' }); var data = Module.FS.readFile(name,{ encoding: 'binary' });
FS.writeFile('/home/web_user/retroarch/userdata/content/' + name, data ,{ encoding: 'binary' }); Module.FS.writeFile('/home/web_user/retroarch/userdata/content/' + name, data ,{ encoding: 'binary' });
FS.unlink(name); Module.FS.unlink(name);
} }
var encoder = new TextEncoder();
var message_queue = [];
function retroArchSend(msg) {
var bytes = encoder.encode(msg+"\n");
message_queue.push([bytes,0]);
}
var Module =
{
noInitialRun: true,
arguments: ["-v", "--menu"],
preRun: [
function() {
function stdin() {
// Return ASCII code of character, or null if no input
while(message_queue.length > 0){
var msg = message_queue[0][0];
var index = message_queue[0][1];
if(index >= msg.length) {
message_queue.shift();
} else {
message_queue[0][1] = index+1;
// assumption: msg is a uint8array
return msg[index];
}
}
return null;
}
FS.init(stdin);
}
],
postRun: [],
onRuntimeInitialized: function()
{
appInitialized();
},
print: function(text)
{
console.log(text);
},
printErr: function(text)
{
console.log(text);
},
canvas: document.getElementById('canvas'),
totalDependencies: 0,
monitorRunDependencies: function(left)
{
this.totalDependencies = Math.max(this.totalDependencies, left);
}
};
function switchCore(corename) { function switchCore(corename) {
localStorage.setItem("core", corename); localStorage.setItem("core", corename);
} }
@ -258,7 +289,7 @@ function switchStorage(backend) {
// When the browser has loaded everything. // When the browser has loaded everything.
$(function() { $(function() {
// Enable all available ToolTips. // Enable all available ToolTips.
$('.tooltip-enable').tooltip({ $('.tooltip-enable').tooltip({
placement: 'right' placement: 'right'
}); });
@ -273,74 +304,77 @@ $(function() {
/** /**
* Attempt to disable some default browser keys. * Attempt to disable some default browser keys.
*/ */
var keys = { var keys = {
9: "tab", 9: "tab",
13: "enter", 13: "enter",
16: "shift", 16: "shift",
18: "alt", 18: "alt",
27: "esc", 27: "esc",
33: "rePag", 33: "rePag",
34: "avPag", 34: "avPag",
35: "end", 35: "end",
36: "home", 36: "home",
37: "left", 37: "left",
38: "up", 38: "up",
39: "right", 39: "right",
40: "down", 40: "down",
112: "F1", 112: "F1",
113: "F2", 113: "F2",
114: "F3", 114: "F3",
115: "F4", 115: "F4",
116: "F5", 116: "F5",
117: "F6", 117: "F6",
118: "F7", 118: "F7",
119: "F8", 119: "F8",
120: "F9", 120: "F9",
121: "F10", 121: "F10",
122: "F11", 122: "F11",
123: "F12" 123: "F12"
}; };
window.addEventListener('keydown', function (e) { window.addEventListener('keydown', function (e) {
if (keys[e.which]) { if (keys[e.which]) {
e.preventDefault(); e.preventDefault();
} }
}); });
// Switch the core when selecting one. // Switch the core when selecting one.
$('#core-selector a').click(function () { $('#core-selector a').click(function () {
var coreChoice = $(this).data('core'); var coreChoice = $(this).data('core');
switchCore(coreChoice); switchCore(coreChoice);
}); });
// Find which core to load. // Find which core to load.
var core = localStorage.getItem("core", core); var core = localStorage.getItem("core", core);
if (!core) { if (!core) {
core = 'gambatte'; core = 'gambatte';
} }
loadCore(core);
});
function loadCore(core) {
// Make the core the selected core in the UI. // Make the core the selected core in the UI.
var coreTitle = $('#core-selector a[data-core="' + core + '"]').addClass('active').text(); var coreTitle = $('#core-selector a[data-core="' + core + '"]').addClass('active').text();
$('#dropdownMenu1').text(coreTitle); $('#dropdownMenu1').text(coreTitle);
// Load the Core's related JavaScript. // Load the Core's related JavaScript.
$.getScript(core + '_libretro.js', function () import("./"+core+"_libretro.js").then(script => {
{ script.default(Module).then(mod => {
$('#icnRun').removeClass('fa-spinner').removeClass('fa-spin'); Module = mod;
$('#icnRun').addClass('fa-play'); $('#icnRun').removeClass('fa-spinner').removeClass('fa-spin');
$('#lblDrop').removeClass('active'); $('#icnRun').addClass('fa-play');
$('#lblLocal').addClass('active'); $('#lblDrop').removeClass('active');
idbfsInit(); $('#lblLocal').addClass('active');
}); idbfsInit();
}); }).catch(err => { console.error("Couldn't instantiate module",err); throw err; });
}).catch(err => { console.error("Couldn't load script",err); throw err; });
}
function keyPress(k) function keyPress(k)
{ {
function kp(k, event) {
var oEvent = new KeyboardEvent(event, { code: k });
document.dispatchEvent(oEvent);
document.getElementById('canvas').focus();
}
kp(k, "keydown"); kp(k, "keydown");
setTimeout(function(){kp(k, "keyup")}, 50); setTimeout(function(){kp(k, "keyup")}, 50);
} }
kp = function(k, event) {
var oEvent = new KeyboardEvent(event, { code: k });
document.dispatchEvent(oEvent);
document.getElementById('canvas').focus();
}