diff --git a/Makefile.emscripten b/Makefile.emscripten
index ae415a04f961..a5a11ad31c28 100644
--- a/Makefile.emscripten
+++ b/Makefile.emscripten
@@ -87,7 +87,10 @@ ifeq ($(HAVE_SDL2), 1)
endif
ifeq ($(HAVE_WASMFS), 1)
- LIBS += -s WASMFS -s FORCE_FILESYSTEM=1
+ LIBS += -s WASMFS -s FORCE_FILESYSTEM=1 -lfetchfs.js -lopfs.js
+ EXPORTS = 'FS', 'FETCHFS', 'OPFS'
+else
+ EXPORTS = 'FS'
endif
ifeq ($(HAVE_WORKER), 1)
@@ -99,7 +102,7 @@ else
endif
LDFLAGS := -L. --no-heap-copy $(LIBS) -s TOTAL_MEMORY=$(MEMORY) -s NO_EXIT_RUNTIME=0 -s FULL_ES2=1 \
- -s "EXPORTED_RUNTIME_METHODS=['callMain', 'FS', 'PATH', 'ERRNO_CODES']" \
+ -s "EXPORTED_RUNTIME_METHODS=[$(EXPORTS), '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_$(subst -,_,$(LIBRETRO))" \
-s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=0 \
diff --git a/frontend/drivers/platform_emscripten.c b/frontend/drivers/platform_emscripten.c
index eaf48d4ba3a4..014cc7bad970 100644
--- a/frontend/drivers/platform_emscripten.c
+++ b/frontend/drivers/platform_emscripten.c
@@ -120,8 +120,8 @@ static void frontend_emscripten_get_env(int *argc, char *argv[],
"config", sizeof(g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG]));
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_MENU_CONTENT], user_path,
"content", sizeof(g_defaults.dirs[DEFAULT_DIR_MENU_CONTENT]));
- fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS], user_path,
- "content/downloads", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS]));
+ fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS], base_path,
+ "downloads", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS]));
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_PLAYLIST], user_path,
"playlists", sizeof(g_defaults.dirs[DEFAULT_DIR_PLAYLIST]));
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_REMAP], g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG],
diff --git a/pkg/emscripten/libretro/browserfs.min.js b/pkg/emscripten/libretro-classic/browserfs.min.js
similarity index 100%
rename from pkg/emscripten/libretro/browserfs.min.js
rename to pkg/emscripten/libretro-classic/browserfs.min.js
diff --git a/pkg/emscripten/libretro-classic/index.html b/pkg/emscripten/libretro-classic/index.html
new file mode 100644
index 000000000000..fe9fea1fd469
--- /dev/null
+++ b/pkg/emscripten/libretro-classic/index.html
@@ -0,0 +1,210 @@
+
+
+
+
+ RetroArch Web Player
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/emscripten/libretro-classic/indexer b/pkg/emscripten/libretro-classic/indexer
new file mode 100755
index 000000000000..5544f481cb82
--- /dev/null
+++ b/pkg/emscripten/libretro-classic/indexer
@@ -0,0 +1,34 @@
+#! /usr/bin/env coffee
+
+fs = require 'fs'
+path = require 'path'
+
+symLinks = {}
+
+rdSync = (dpath, tree, name) ->
+ files = fs.readdirSync(dpath)
+ for file in files
+ # ignore non-essential directories / files
+ continue if file in ['.git', 'node_modules', 'bower_components', 'build'] or file[0] is '.'
+ fpath = dpath + '/' + file
+ try
+ # Avoid infinite loops.
+ lstat = fs.lstatSync(fpath)
+ if lstat.isSymbolicLink()
+ symLinks[lstat.dev] ?= {}
+ # Ignore if we've seen it before
+ continue if symLinks[lstat.dev][lstat.ino]?
+ symLinks[lstat.dev][lstat.ino] = 0
+
+ fstat = fs.statSync(fpath)
+ if fstat.isDirectory()
+ tree[file] = child = {}
+ rdSync(fpath, child, file)
+ else
+ tree[file] = null
+ catch e
+ # Ignore and move on.
+ return tree
+
+fs_listing = rdSync(process.cwd(), {}, '/')
+console.log(JSON.stringify(fs_listing))
diff --git a/pkg/emscripten/libretro-classic/libretro.css b/pkg/emscripten/libretro-classic/libretro.css
new file mode 100644
index 000000000000..4d08dc2eb4eb
--- /dev/null
+++ b/pkg/emscripten/libretro-classic/libretro.css
@@ -0,0 +1,120 @@
+/**
+ * RetroArch Web Player
+ *
+ * This provides the basic styling for the RetroArch web player.
+ */
+
+/**
+ * Make sure the background of the player is black.
+ * Also make sure line height is 0 so there's no extra space on the bottom.
+ */
+.webplayer-container {
+ background-color: black;
+ line-height: 0;
+}
+
+/**
+ * Webplayer Preview when not loaded.
+ */
+.webplayer-preview {
+ margin: 0 auto;
+ cursor: wait;
+ opacity: 0.2;
+ transition: all 0.8s;
+ -webkit-animation: loading 0.8s ease-in-out infinite alternate;
+ -moz-animation: loading 0.8s ease-in-out infinite alternate;
+ animation: loading 0.8s ease-in-out infinite alternate;
+}
+.webplayer-preview.loaded {
+ cursor: pointer;
+ opacity: 1;
+ -webkit-animation: loaded 0.8s ease-in-out;
+ -moz-animation: loaded 0.8s ease-in-out;
+ animation: loaded 0.8s ease-in-out;
+}
+@keyframes loaded {
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@-moz-keyframes loaded {
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@-webkit-keyframes loaded {
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@keyframes loading{
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 0.35;
+ }
+}
+@-moz-keyframes loading{
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 0.35;
+ }
+}
+@-webkit-keyframes loading {
+ from {
+ opacity: 0.2;
+ }
+ to {
+ opacity: 0.35;
+ }
+}
+
+/**
+ * Disable the border around the player.
+ */
+canvas.webplayer {
+ border: none;
+ outline: none;
+}
+
+textarea {
+ font-family: monospace;
+ font-size: 0.7em;
+ height: 95%;
+ width: 95%;
+ border-style: none;
+ border-color: transparent;
+ overflow: auto;
+ resize: none;
+}
+
+/**
+ * Toggle Top Navigation
+ */
+.toggleMenu {
+ float: right;
+}
+.showMenu {
+ position: absolute;
+ right: 0;
+ cursor: pointer;
+}
+#icnShowMenu {
+ color: #565656 !important;
+}
+
+.navbar {
+ box-shadow: none;
+}
diff --git a/pkg/emscripten/libretro-classic/libretro.js b/pkg/emscripten/libretro-classic/libretro.js
new file mode 100644
index 000000000000..a4bb5620f43f
--- /dev/null
+++ b/pkg/emscripten/libretro-classic/libretro.js
@@ -0,0 +1,406 @@
+/**
+ * RetroArch Web Player
+ *
+ * This provides the basic JavaScript for the RetroArch web player.
+ */
+var BrowserFS = BrowserFS;
+var afs;
+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()
+{
+ localStorage.clear();
+ if (BrowserFS.FileSystem.IndexedDB.isAvailable())
+ {
+ var req = indexedDB.deleteDatabase("RetroArch");
+ req.onsuccess = function () {
+ console.log("Deleted database successfully");
+ };
+ req.onerror = function () {
+ console.log("Couldn't delete database");
+ };
+ req.onblocked = function () {
+ console.log("Couldn't delete database due to the operation being blocked");
+ };
+ }
+
+ document.getElementById("btnClean").disabled = true;
+}
+
+function idbfsInit()
+{
+ $('#icnLocal').removeClass('fa-globe');
+ $('#icnLocal').addClass('fa-spinner fa-spin');
+ var imfs = new BrowserFS.FileSystem.InMemory();
+ if (BrowserFS.FileSystem.IndexedDB.isAvailable())
+ {
+ afs = new BrowserFS.FileSystem.AsyncMirror(imfs,
+ new BrowserFS.FileSystem.IndexedDB(function(e, fs)
+ {
+ if (e)
+ {
+ //fallback to imfs
+ afs = new BrowserFS.FileSystem.InMemory();
+ console.log("WEBPLAYER: error: " + e + " falling back to in-memory filesystem");
+ appInitialized();
+ }
+ else
+ {
+ // initialize afs by copying files from async storage to sync storage.
+ afs.initialize(function (e)
+ {
+ if (e)
+ {
+ afs = new BrowserFS.FileSystem.InMemory();
+ console.log("WEBPLAYER: error: " + e + " falling back to in-memory filesystem");
+ appInitialized();
+ }
+ else
+ {
+ idbfsSyncComplete();
+ }
+ });
+ }
+ },
+ "RetroArch"));
+ }
+}
+
+function idbfsSyncComplete()
+{
+ $('#icnLocal').removeClass('fa-spinner').removeClass('fa-spin');
+ $('#icnLocal').addClass('fa-check');
+ console.log("WEBPLAYER: idbfs setup successful");
+
+ appInitialized();
+}
+
+function appInitialized()
+{
+ /* Need to wait for the file system, the wasm runtime, and the zip download
+ to complete before enabling the Run button. */
+ initializationCount++;
+ if (initializationCount == 3)
+ {
+ setupFileSystem("browser");
+ preLoadingComplete();
+ }
+ }
+
+function preLoadingComplete()
+{
+ /* Make the Preview image clickable to start RetroArch. */
+ $('.webplayer-preview').addClass('loaded').click(function () {
+ startRetroArch();
+ return false;
+ });
+ document.getElementById("btnRun").disabled = false;
+ $('#btnRun').removeClass('disabled');
+}
+
+var zipTOC;
+
+function zipfsInit() {
+ // 256 MB max bundle size
+ let buffer = new ArrayBuffer(256*1024*1024);
+ let bufferView = new Uint8Array(buffer);
+ let idx = 0;
+ // bundle should be in four parts (this can be changed later)
+ Promise.all([fetch("assets/frontend/bundle.zip.aa"),
+ fetch("assets/frontend/bundle.zip.ab"),
+ fetch("assets/frontend/bundle.zip.ac"),
+ fetch("assets/frontend/bundle.zip.ad")
+ ]).then(function(resps) {
+ Promise.all(resps.map((r) => r.arrayBuffer())).then(function(buffers) {
+ for (let buf of buffers) {
+ if (idx+buf.byteLength > buffer.maxByteLength) {
+ console.log("WEBPLAYER: error: bundle.zip is too large");
+ }
+ bufferView.set(new Uint8Array(buf), idx, buf.byteLength);
+ idx += buf.byteLength;
+ }
+ BrowserFS.FileSystem.ZipFS.computeIndex(BrowserFS.BFSRequire('buffer').Buffer(new Uint8Array(buffer, 0, idx)), function(toc) {
+ zipTOC = toc;
+ appInitialized();
+ });
+ })
+ });
+}
+function setupFileSystem(backend)
+{
+ /* create a mountable filesystem that will server as a root
+ mountpoint for browserfs */
+ var mfs = new BrowserFS.FileSystem.MountableFileSystem();
+
+ /* create an XmlHttpRequest filesystem for the bundled data */
+ var xfs1 = new BrowserFS.FileSystem.ZipFS(zipTOC);
+ /* create an XmlHttpRequest filesystem for core assets */
+ var xfs2 = new BrowserFS.FileSystem.XmlHttpRequest
+ (".index-xhr", "assets/cores/");
+
+ console.log("WEBPLAYER: initializing filesystem: " + backend);
+ mfs.mount('/home/web_user/retroarch/userdata', afs);
+
+ mfs.mount('/home/web_user/retroarch/', xfs1);
+ mfs.mount('/home/web_user/retroarch/userdata/content/downloads', xfs2);
+ BrowserFS.initialize(mfs);
+ var BFS = new BrowserFS.EmscriptenFS(Module.FS, Module.PATH, Module.ERRNO_CODES);
+ Module.FS.mount(BFS, {root: '/home'}, '/home');
+ console.log("WEBPLAYER: " + backend + " filesystem initialization successful");
+}
+
+/**
+ * Retrieve the value of the given GET parameter.
+ */
+function getParam(name) {
+ var results = new RegExp('[?&]' + name + '=([^]*)').exec(window.location.href);
+ if (results) {
+ return results[1] || null;
+ }
+}
+
+function startRetroArch()
+{
+ $('.webplayer').show();
+ $('.webplayer-preview').hide();
+ document.getElementById("btnRun").disabled = true;
+
+ $('#btnFullscreen').removeClass('disabled');
+ $('#btnMenu').removeClass('disabled');
+ $('#btnAdd').removeClass('disabled');
+ $('#btnRom').removeClass('disabled');
+
+ document.getElementById("btnAdd").disabled = false;
+ document.getElementById("btnRom").disabled = false;
+ document.getElementById("btnMenu").disabled = false;
+ document.getElementById("btnFullscreen").disabled = false;
+
+ Module["canvas"] = document.getElementById("canvas");
+ Module["canvas"].addEventListener("click", () => Module["canvas"].focus());
+ Module['callMain'](Module['arguments']);
+ Module['resumeMainLoop']();
+ Module['canvas'].focus();
+}
+function selectFiles(files)
+{
+ $('#btnAdd').addClass('disabled');
+ $('#icnAdd').removeClass('fa-plus');
+ $('#icnAdd').addClass('fa-spinner spinning');
+ var count = files.length;
+
+ for (var i = 0; i < count; i++)
+ {
+ filereader = new FileReader();
+ filereader.file_name = files[i].name;
+ filereader.readAsArrayBuffer(files[i]);
+ filereader.onload = function(){uploadData(this.result, this.file_name)};
+ filereader.onloadend = function(evt)
+ {
+ console.log("WEBPLAYER: file: " + this.file_name + " upload complete");
+ if (evt.target.readyState == FileReader.DONE)
+ {
+ $('#btnAdd').removeClass('disabled');
+ $('#icnAdd').removeClass('fa-spinner spinning');
+ $('#icnAdd').addClass('fa-plus');
+ }
+ }
+ }
+}
+
+function uploadData(data,name)
+{
+ var dataView = new Uint8Array(data);
+ Module.FS.createDataFile('/', name, dataView, true, false);
+
+ var data = Module.FS.readFile(name,{ encoding: 'binary' });
+ Module.FS.writeFile('/home/web_user/retroarch/userdata/content/' + name, data ,{ encoding: 'binary' });
+ Module.FS.unlink(name);
+}
+
+function switchCore(corename) {
+ localStorage.setItem("core", corename);
+}
+
+function switchStorage(backend) {
+ if (backend != localStorage.getItem("backend"))
+ {
+ localStorage.setItem("backend", backend);
+ location.reload();
+ }
+}
+
+// When the browser has loaded everything.
+$(function() {
+ // Enable all available ToolTips.
+ $('.tooltip-enable').tooltip({
+ placement: 'right'
+ });
+
+ // Allow hiding the top menu.
+ $('.showMenu').hide();
+ $('#btnHideMenu, .showMenu').click(function () {
+ $('nav').slideToggle('slow');
+ $('.showMenu').toggle('slow');
+ });
+
+ /**
+ * Attempt to disable some default browser keys.
+ */
+ var keys = {
+ 9: "tab",
+ 13: "enter",
+ 16: "shift",
+ 18: "alt",
+ 27: "esc",
+ 33: "rePag",
+ 34: "avPag",
+ 35: "end",
+ 36: "home",
+ 37: "left",
+ 38: "up",
+ 39: "right",
+ 40: "down",
+ 112: "F1",
+ 113: "F2",
+ 114: "F3",
+ 115: "F4",
+ 116: "F5",
+ 117: "F6",
+ 118: "F7",
+ 119: "F8",
+ 120: "F9",
+ 121: "F10",
+ 122: "F11",
+ 123: "F12"
+ };
+ window.addEventListener('keydown', function (e) {
+ if (keys[e.which]) {
+ e.preventDefault();
+ }
+ });
+
+ // Switch the core when selecting one.
+ $('#core-selector a').click(function () {
+ var coreChoice = $(this).data('core');
+ switchCore(coreChoice);
+ });
+ // Find which core to load.
+ var core = localStorage.getItem("core", core);
+ if (!core) {
+ core = 'gambatte';
+ }
+ loadCore(core);
+});
+
+function loadCore(core) {
+ // Make the core the selected core in the UI.
+ var coreTitle = $('#core-selector a[data-core="' + core + '"]').addClass('active').text();
+ $('#dropdownMenu1').text(coreTitle);
+ // Load the Core's related JavaScript.
+ import("./"+core+"_libretro.js").then(script => {
+ script.default(Module).then(mod => {
+ Module = mod;
+ $('#icnRun').removeClass('fa-spinner').removeClass('fa-spin');
+ $('#icnRun').addClass('fa-play');
+ $('#lblDrop').removeClass('active');
+ $('#lblLocal').addClass('active');
+ idbfsInit();
+ zipfsInit();
+ }).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 kp(k, event) {
+ var oEvent = new KeyboardEvent(event, { code: k });
+
+ document.dispatchEvent(oEvent);
+ document.getElementById('canvas').focus();
+ }
+ kp(k, "keydown");
+ setTimeout(function(){kp(k, "keyup")}, 50);
+}
diff --git a/pkg/emscripten/libretro/index.html b/pkg/emscripten/libretro/index.html
index eae5da79f17d..f6961185b07a 100644
--- a/pkg/emscripten/libretro/index.html
+++ b/pkg/emscripten/libretro/index.html
@@ -198,13 +198,14 @@ Quick Menu
-
-
-
-
+
+
+
+
-
+
+