Skip to content

Commit

Permalink
Workerized (non-async) web player, using OPFS
Browse files Browse the repository at this point in the history
This patch eliminates the need for asyncify and uses modern filesystem
APIs instead of the deprecated, unmaintained BrowserFS.

This is a WIP patch because it won't fully work until these two
Emscripten PRs land and are released:

emscripten-core/emscripten#23518
emscripten-core/emscripten#23021

The former fixes an offscreen canvas context recreation bug, and the
latter adds an equivalent to BrowserFS's XHR filesystem (but without
the hazardous running-XHR-on-the-main-thread problem).

The biggest issue is that local storage of users who were using the
old version of the webplayer will be gone when they switch to the new
webplayer.  I don't have a good story for converting the old BrowserFS
IDBFS contents into the new OPFS filesystem (the move is worth doing
because OPFS supports seeking and reading only bits of a file, and
because BrowserFS is dead).

I've kept around the old libretro webplayer under
pkg/emscripten/libretro-classic, and with these make flags you can
build a non-workerized RA that uses asyncify to sleep as before:

make -f Makefile.emscripten libretro=$CORE HAVE_WORKER=0 HAVE_WASMFS=0 PTHREAD=0 HAVE_AL=1

I also moved the default directory for core content on emscripten to
not be a subdirectory of the local filesystem mount, because it's
confusing to have a subdirectory that's lazily fetched and not
mirrored to the local storage.  I think it won't impact existing users
of the classic web player because they already have a retroarch.cfg in
place.
  • Loading branch information
JoeOsborn committed Jan 28, 2025
1 parent a761596 commit 4091182
Show file tree
Hide file tree
Showing 10 changed files with 868 additions and 144 deletions.
7 changes: 5 additions & 2 deletions Makefile.emscripten
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 \
Expand Down
4 changes: 2 additions & 2 deletions frontend/drivers/platform_emscripten.c
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
File renamed without changes.
210 changes: 210 additions & 0 deletions pkg/emscripten/libretro-classic/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RetroArch Web Player</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap core CSS -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.3/css/bootstrap.min.css" rel="stylesheet" type="text/css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.0/css/font-awesome.min.css">
<!-- Material Design Bootstrap -->
<link href="//cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.1.1/css/mdb.min.css" rel="stylesheet">

<link href="libretro.css" rel="stylesheet" type="text/css">
<link rel="shortcut icon" href="media/retroarch.ico" />

</head>
<body>
<!--Navbar-->
<nav class="navbar navbar-dark bg-primary">
<div class="container">
<!--navbar content-->
<div class="navbar-toggleable-xs">
<!--Links-->
<ul class="nav navbar-nav">
<div class="dropdown">
<li class="nav-item dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Core Selection</button>
<div class="dropdown-menu dropdown-primary" aria-labelledby="dropdownMenu1" data-dropdown-in="fadeIn" data-dropdown-out="fadeOut" id="core-selector">
<a class="dropdown-item" href="." data-core="2048">2048</a>
<a class="dropdown-item" href="." data-core="arduous">Arduous</a>
<a class="dropdown-item" href="." data-core="bk">BK</a>
<a class="dropdown-item" href="." data-core="bluemsx">BlueMSX</a>
<a class="dropdown-item" href="." data-core="chailove">ChaiLove</a>
<a class="dropdown-item" href="." data-core="craft">Craft</a>
<a class="dropdown-item" href="." data-core="desmume">DeSmuME</a>
<a class="dropdown-item" href="." data-core="dosbox">DOSBox</a>
<a class="dropdown-item" href="." data-core="easyrpg">EasyRPG</a>
<a class="dropdown-item" href="." data-core="ecwolf">ECWolf</a>
<a class="dropdown-item" href="." data-core="fbalpha2012">FB Alpha 2012</a>
<a class="dropdown-item" href="." data-core="fbalpha2012_cps1">FB Alpha 2012 CPS1</a>
<a class="dropdown-item" href="." data-core="fbalpha2012_cps2">FB Alpha 2012 CPS2</a>
<a class="dropdown-item" href="." data-core="fbalpha2012_neo">FB Alpha 2012 NeoGeo</a>
<a class="dropdown-item" href="." data-core="fceumm">FCEUmm</a>
<a class="dropdown-item" href="." data-core="ffmpeg">FFmpeg</a>
<a class="dropdown-item" href="." data-core="freechaf">FreeChaF</a>
<a class="dropdown-item" href="." data-core="gambatte">Gambatte</a>
<a class="dropdown-item" href="." data-core="gme">Game Music Emu</a>
<a class="dropdown-item" href="." data-core="gearboy">GearBoy</a>
<a class="dropdown-item" href="." data-core="gearcoleco">GearColeco</a>
<a class="dropdown-item" href="." data-core="gearsystem">GearSystem</a>
<a class="dropdown-item" href="." data-core="genesis_plus_gx">Genesis Plus GX</a>
<a class="dropdown-item" href="." data-core="genesis_plus_gx_wide">Genesis Plus GX Wide</a>
<a class="dropdown-item" href="." data-core="glupen64">GLupeN64</a>
<!--<a class="dropdown-item" href="." data-core="gpsp">gPSP</a>-->
<a class="dropdown-item" href="." data-core="handy">Handy</a>
<a class="dropdown-item" href="." data-core="jaxe">JAXE</a>
<a class="dropdown-item" href="." data-core="jumpnbump">Jump 'n Bump</a>
<a class="dropdown-item" href="." data-core="lowresnx">LowResNX</a>
<a class="dropdown-item" href="." data-core="lutro">Lutro</a>
<a class="dropdown-item" href="." data-core="m2000">M2000</a>
<a class="dropdown-item" href="." data-core="mame2000">MAME 2000</a>
<a class="dropdown-item" href="." data-core="mame2003">MAME 2003</a>
<a class="dropdown-item" href="." data-core="mame2003_plus">MAME 2003-Plus</a>
<a class="dropdown-item" href="." data-core="mednafen_lynx">Mednafen Lynx</a>
<a class="dropdown-item" href="." data-core="mednafen_ngp">Mednafen Neo Geo Pocket</a>
<a class="dropdown-item" href="." data-core="mednafen_pce_fast">Mednafen PC Engine Fast</a>
<!--<a class="dropdown-item" href="." data-core="mednafen_pcfx">Mednafen/Beetle PCFX</a>-->
<a class="dropdown-item" href="." data-core="mednafen_psx">Mednafen/Beetle PSX</a>
<!--<a class="dropdown-item" href="." data-core="mednafen_saturn">Mednafen/Beetle Saturn</a>-->
<a class="dropdown-item" href="." data-core="mednafen_snes">Mednafen/Beetle SNES</a>
<a class="dropdown-item" href="." data-core="mednafen_vb">Mednafen/Beetle Virtual Boy</a>
<a class="dropdown-item" href="." data-core="mednafen_wswan">Mednafen/Beetle WonderSwan</a>
<a class="dropdown-item" href="." data-core="mgba">Mgba</a>
<a class="dropdown-item" href="." data-core="minivmac">MiniVmac</a>
<a class="dropdown-item" href="." data-core="mu">Mu</a>
<a class="dropdown-item" href="." data-core="mupen64plus">Mupen64 Plus</a>
<a class="dropdown-item" href="." data-core="mrboom">MrBoom</a>
<a class="dropdown-item" href="." data-core="nestopia">Nestopia</a>
<a class="dropdown-item" href="." data-core="nxengine">NX Engine</a>
<a class="dropdown-item" href="." data-core="o2em">O2em</a>
<a class="dropdown-item" href="." data-core="opera">Opera</a>
<a class="dropdown-item" href="." data-core="picodrive">PicoDrive</a>
<a class="dropdown-item" href="." data-core="prboom">PrBoom</a>
<a class="dropdown-item" href="." data-core="quasi88">Quasi88</a>
<a class="dropdown-item" href="." data-core="quicknes">QuickNES</a>
<a class="dropdown-item" href="." data-core="retro8">Retro8</a>
<a class="dropdown-item" href="." data-core="flycast">Flycast</a>
<a class="dropdown-item" href="." data-core="snes9x2002">Snes9x 2002</a>
<a class="dropdown-item" href="." data-core="snes9x2005">Snes9x 2005</a>
<a class="dropdown-item" href="." data-core="snes9x2010">Snes9x 2010</a>
<a class="dropdown-item" href="." data-core="snes9x">Snes9x</a>
<a class="dropdown-item" href="." data-core="squirreljme">SquirrelJME</a>
<a class="dropdown-item" href="." data-core="stella">Stella</a>
<a class="dropdown-item" href="." data-core="tgbdual">TGB Dual</a>
<a class="dropdown-item" href="." data-core="theodore">Theodore (Thomson TO8/TO9)</a>
<a class="dropdown-item" href="." data-core="tic80">TIC-80</a>
<a class="dropdown-item" href="." data-core="tyrquake">TyrQuake</a>
<a class="dropdown-item" href="." data-core="uzem">UZEM</a>
<a class="dropdown-item" href="." data-core="vaporspec">Vaporspec</a>
<a class="dropdown-item" href="." data-core="vba_next">VBA Next</a>
<a class="dropdown-item" href="." data-core="vecx">Vecx</a>
<a class="dropdown-item" href="." data-core="vice_x64">VICE x64</a>
<a class="dropdown-item" href="." data-core="vice_x64sc">VICE x64sc</a>
<a class="dropdown-item" href="." data-core="vice_x128">VICE x128</a>
<a class="dropdown-item" href="." data-core="vice_xcbm2">VICE xcbm2</a>
<a class="dropdown-item" href="." data-core="vice_xcbm5x0">VICE xcbm5x0</a>
<a class="dropdown-item" href="." data-core="vice_xpet">VICE xPET</a>
<a class="dropdown-item" href="." data-core="vice_xplus4">VICE xPlus4</a>
<a class="dropdown-item" href="." data-core="vice_xscpu64">VICE xscpu4</a>
<a class="dropdown-item" href="." data-core="vice_xvic">VICE xVIC</a>
<a class="dropdown-item" href="." data-core="vitaquake2">Vita Quake2</a>
<a class="dropdown-item" href="." data-core="vitaquake2-rogue">Vita Quake2 (rogue)</a>
<a class="dropdown-item" href="." data-core="vitaquake2-xatrix">Vita Quake2 (xatrix)</a>
<a class="dropdown-item" href="." data-core="vitaquake2-zaero">Vita Quake2 (zaero)</a>
<a class="dropdown-item" href="." data-core="virtualjaguar">Virtual Jaguar</a>
<a class="dropdown-item" href="." data-core="wasm4">WASM4</a>
<a class="dropdown-item" href="." data-core="x1">XMillenium</a>
<a class="dropdown-item" href="." data-core="xrick">XRick</a>
<a class="dropdown-item" href="." data-core="yabause">Yabause</a>
</div>
<button class="btn btn-primary disabled" id="btnRun" onclick="startRetroArch()" disabled>
<span class="fa fa-spinner fa-spin" id="icnRun"></span> Run
</button>
<button class="btn btn-primary disabled" id="btnAdd" onclick="document.getElementById('btnRom').click()" disabled>
<span class="fa fa-plus" id="icnAdd"></span> Add Content
</button>
<button class="btn btn-primary tooltip-enable" id="btnClean" onclick="cleanupStorage();" title="Cleanup storage">
<span class="fa fa-trash-o" id="icnClean"></span> <span class="sr-only">Cleanup</span>
</button>
<input class="btn btn-primary disabled" style="display: none" type="file" id="btnRom" name="upload" onclick="document.getElementById('btnAdd').click();" onchange="selectFiles(event.target.files)" multiple />
<button class="btn btn-primary disabled tooltip-enable" id="btnMenu" onclick="keyPress('F1');" title="Menu toggle" disabled>
<span class="fa fa-bars" id="btnMenu"></span> <span class="sr-only">Menu</span>
</button>
<button class="btn btn-primary disabled tooltip-enable" id="btnFullscreen" onclick="Module.requestFullscreen(false)" title="Fullscreen" disabled>
<span class="fa fa-desktop" id="icnAdd"></span> <span class="sr-only">Fullscreen</span>
</button>
</button>
<button type="button" class="btn btn-primary tooltip-enable" data-toggle="modal" data-target="#helpModal">Help</button>
</li>
</div>
</ul>
<div class="toggleMenu">
<button class="btn btn-primary" id="btnHideMenu" title="Toggle Menu">
<span class="fa fa-chevron-up" id="icnHideMenu"></span> <span class="sr-only">Hide Top Navigation</span>
</button>
</div>
</div>
<!-- Basics steps modal for Web Libretro -->
<div class="modal fade" id="helpModal" role="dialog" style="color:black;">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h1 class="modal-title">Basics</h1>
</div>
<div class="modal-body">
<h3><b>Load Core</b></h3>
<p>Load your core by clicking on the first tab. Scroll down until you reach the desired Core. We will use Nestopia for now. Don't forget - Content must be compatible with the matched Core.</p>
<li>Nes: <i>NESTOPIA</i></li>
<li>Game Boy / Color: <i>Gambatte</i></li>
</ul>
<p>etc.</p>
<p></p>
<h3><b>Load Content</b></h3>
<p>After selecting Core, click Run. After RetroArch opens, click Add Content and select your compatible ROM.</p>
<li>Nestopia > <i>YourGame.nes</i></li>
<li>Gambatte > <i>YourGame.gbc</i></li>
</ul>
<p>etc.</p>
<p></p>
<h3><b><span class="fa fa-trash-o"></span> Cleanup Storage</b></h3>
<p>The trashcan erases your existing configuration and presets. If the Web Player doesn't start, you should click the trashcan and refresh the cache in your browser (usually F5 or Shift+F5).</p>
<p></p>
<h3><b><span class="fa fa-bars"></span> Quick Menu</b></h3>
<p>If you click on the three line icons, the Quick Menu will open here as in RetroArch.</p>

</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<!--/.navbar content-->
</div>
</nav>
<div class="bg-inverse webplayer-container">
<div class="webplayer_border text-xs-center" id="canvas_div">
<div class="showMenu">
<button type="button" class="btn btn-link">
<span class="fa fa-chevron-down" id="icnShowMenu"></span> <span class="sr-only">Show Top Navigation</span>
</button>
</div>
<canvas class="webplayer" id="canvas" tabindex="1" oncontextmenu="event.preventDefault()" style="display: none"></canvas>
<img class="webplayer-preview img-fluid" src="media/canvas.png" width="960px" height="720px" alt="RetroArch Logo">
</div>
</div>

<script crossorigin="anonymous" src="//code.jquery.com/jquery-3.1.0.min.js"></script>
<script crossorigin="anonymous" src="//rawgit.com/jeresig/jquery.hotkeys/master/jquery.hotkeys.js"></script>
<script crossorigin="anonymous" src="//cdnjs.cloudflare.com/ajax/libs/tether/1.3.4/js/tether.min.js"></script>
<script crossorigin="anonymous" src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.3/js/bootstrap.min.js"></script>
<script src="analytics.js"></script>
<!--script src="//wzrd.in/standalone/[email protected]"></script-->
<script src="browserfs.min.js"></script>
<script src="libretro.js"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions pkg/emscripten/libretro-classic/indexer
Original file line number Diff line number Diff line change
@@ -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))
Loading

0 comments on commit 4091182

Please sign in to comment.