Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/duo-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
mkdir -p _site/data/cores _site/docs
cp duo.html _site/index.html
cp -r duo _site/duo
cp duo/sw.js _site/sw.js
cp docs/favicon.ico _site/docs/
cp -r data/compression data/localization data/src _site/data/
cp data/loader.js data/version.json data/emulator.css data/emulator.min.js data/emulator.min.css _site/data/
Expand Down
6 changes: 5 additions & 1 deletion duo.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Duo DS">
<meta name="theme-color" content="#101014">
<link rel="icon" href="docs/favicon.ico" sizes="16x16 32x32 48x48 64x64" type="image/vnd.microsoft.icon">
<link rel="manifest" href="duo/manifest.webmanifest">
<link rel="icon" href="duo/icons/icon.svg" type="image/svg+xml">
<link rel="icon" href="duo/icons/favicon-32.png" sizes="32x32" type="image/png">
<link rel="apple-touch-icon" href="duo/icons/apple-touch-icon.png">
<link rel="stylesheet" href="duo/duo.css">
</head>

Expand Down
19 changes: 19 additions & 0 deletions duo/duo.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,12 @@
// Boot EmulatorJS
const params = new URLSearchParams(location.search);

// Threads need cross-origin isolation, which the service worker provides
const useThreads = window.crossOriginIsolated === true && typeof SharedArrayBuffer === "function" && params.get("threads") !== "0";
let starting = false;

function start(gameUrl, name) {
starting = true;
render();
const game = document.createElement("div");
game.id = "game";
Expand All @@ -473,8 +478,10 @@
window.EJS_color = getComputedStyle(document.body).getPropertyValue("--accent").trim();
window.EJS_DEBUG_XX = params.get("debug") === "1";
window.EJS_askBeforeExit = false;
window.EJS_threads = useThreads;
window.EJS_defaultOptions = Object.assign({
"melonds_touch_mode": "Touch",
"melonds_threaded_renderer": useThreads ? "enabled" : "disabled",
"virtual-gamepad": "disabled",
"menu-bar-button": "hidden"
}, opts);
Expand Down Expand Up @@ -510,5 +517,17 @@

if (params.get("rom")) start("roms/" + params.get("rom"), params.get("rom"));

if ("serviceWorker" in navigator) {
// Reload once when the worker first takes over so the page becomes isolated
navigator.serviceWorker.addEventListener("controllerchange", () => {
let reloaded = false;
try { reloaded = sessionStorage.getItem("duo-ds-isolated") === "1"; } catch (e) {}
if (window.crossOriginIsolated || starting || reloaded) return;
try { sessionStorage.setItem("duo-ds-isolated", "1"); } catch (e) {}
location.reload();
});
navigator.serviceWorker.register("sw.js").catch(() => {});
}

render();
})();
Binary file added duo/icons/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added duo/icons/favicon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added duo/icons/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added duo/icons/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added duo/icons/icon-maskable-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions duo/icons/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions duo/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"id": "/",
"name": "Duo DS",
"short_name": "Duo DS",
"description": "Dual-screen DS emulator for foldables",
"start_url": "../",
"scope": "../",
"display": "fullscreen",
"display_override": ["fullscreen", "standalone"],
"orientation": "any",
"background_color": "#0c0c10",
"theme_color": "#0c0c10",
"categories": ["games", "entertainment"],
"icons": [
{ "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" },
{ "src": "icons/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }
]
}
70 changes: 70 additions & 0 deletions duo/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Deployed at the site root so it controls the whole app
const CACHE = "duo-ds-v1";
const PRECACHE = [
"./",
"duo/duo.css",
"duo/duo.js",
"duo/manifest.webmanifest",
"duo/icons/icon-192.png",
"duo/icons/apple-touch-icon.png",
"data/loader.js",
"data/emulator.min.js",
"data/emulator.min.css",
"data/compression/extract7z.js"
];

self.addEventListener("install", (event) => {
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting()));
});

self.addEventListener("activate", (event) => {
event.waitUntil(caches.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
.then(() => self.clients.claim()));
});

// Cross-origin isolation lets the threaded core use SharedArrayBuffer
function isolate(res) {
if (!res || res.status === 0 || res.type === "opaque") return res;
const headers = new Headers(res.headers);
headers.set("Cross-Origin-Opener-Policy", "same-origin");
headers.set("Cross-Origin-Embedder-Policy", "require-corp");
headers.set("Cross-Origin-Resource-Policy", "same-origin");
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
}

async function networkFirst(req) {
const cache = await caches.open(CACHE);
try {
const res = await fetch(req);
if (res.ok) cache.put(req, res.clone());
return res;
} catch (err) {
const hit = await cache.match(req, { ignoreSearch: true });
if (hit) return hit;
throw err;
}
}

async function staleWhileRevalidate(req, event) {
const cache = await caches.open(CACHE);
const hit = await cache.match(req);
const update = fetch(req).then((res) => {
if (res.ok) cache.put(req, res.clone());
return res;
});
if (hit) {
event.waitUntil(update.catch(() => {}));
return hit;
}
return update;
}

self.addEventListener("fetch", (event) => {
const req = event.request;
const url = new URL(req.url);
if (req.method !== "GET" || url.origin !== self.location.origin) return;
// Cores are large and only change with a deploy, so serve them from cache first
const res = url.pathname.endsWith(".data") ? staleWhileRevalidate(req, event) : networkFirst(req);
event.respondWith(res.then(isolate));
});
Loading