Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow running web runtime in background windows #793

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
12 changes: 11 additions & 1 deletion cli/lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,17 @@ async function start (cartFile, opts) {
}

// Serve the WASM-4 developer runtime.
app.use(express.static(path.resolve(__dirname, "../assets/runtime/developer-build")));
app.use(express.static(
path.resolve(__dirname, "../assets/runtime/developer-build"),
{
setHeaders: function (res, path, stat) {
// These COOP and COEP headers allow us to get high-precision time.
// https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#security_requirements
res.set("Cross-Origin-Opener-Policy", "same-origin");
res.set("Cross-Origin-Embedder-Policy", "require-corp");
}
}
));


const first_port = opts.port;
Expand Down
7 changes: 5 additions & 2 deletions devtools/web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion devtools/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"@types/lodash-es": "^4.17.12"
},
"devDependencies": {
"@types/node": "^20.11.16",
"prettier": "3.2.4",
"rimraf": "5.0.5",
"rollup-plugin-postcss-lit": "^2.1.0",
Expand Down
3 changes: 2 additions & 1 deletion devtools/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": false,
"isolatedModules": true
"isolatedModules": true,
"types": []
},
"include": ["src/**/*.ts"],
"exclude": []
Expand Down
2 changes: 1 addition & 1 deletion runtimes/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<link rel="shortcut icon" href="https://wasm4.org/img/favicon.ico">
<link rel="shortcut icon" href="favicon.ico">
<title>WASM-4 web runtime dev</title>
</head>
<body>
Expand Down
2 changes: 0 additions & 2 deletions runtimes/web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion runtimes/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"lit": "^3.1.2"
},
"devDependencies": {
"@types/node": "^20.11.16",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"concurrently": "8.2.2",
Expand Down
1 change: 1 addition & 0 deletions runtimes/web/public/favicon.ico
2 changes: 1 addition & 1 deletion runtimes/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<link rel="shortcut icon" href="https://wasm4.org/img/favicon.ico">
<link rel="shortcut icon" href="favicon.ico">
<title>WASM-4 Cart</title>
<link rel="stylesheet" href="wasm4.css">
</head>
Expand Down
15 changes: 14 additions & 1 deletion runtimes/web/src/apu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class APU {
audioCtx: AudioContext;
processor!: APUProcessor;
processorPort!: MessagePort;
paused: boolean = false;

constructor () {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)({
Expand Down Expand Up @@ -73,17 +74,29 @@ export class APU {
}
}

unlockAudio () {
unpauseAudio () {
this.paused = false;
const audioCtx = this.audioCtx;
if (audioCtx.state == "suspended") {
audioCtx.resume();
}
}

pauseAudio () {
this.paused = true;
const audioCtx = this.audioCtx;
if (audioCtx.state == "running") {
audioCtx.suspend();
}
}

// Most web browsers won't play audio until the user has interacted with the web page.
// Even if there are already pending Promises from an AudioContext.resume() call when the
// page gets interaction, apparently the AudioContext still needs to be poked with a
// resume() call to start playing.
pokeAudio () {
if (!this.paused) {
this.audioCtx.resume();
}
}
}
10 changes: 7 additions & 3 deletions runtimes/web/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,18 @@ export class Runtime {
return this.data.getUint8(constants.ADDR_SYSTEM_FLAGS) & mask;
}

unlockAudio () {
this.apu.unlockAudio();
unpauseAudio () {
this.apu.unpauseAudio();
}

pauseAudio() {
pauseAudio () {
this.apu.pauseAudio();
}

pokeAudio () {
this.apu.pokeAudio();
}

reset (zeroMemory?: boolean) {
// Initialize default color table and palette
const mem32 = new Uint32Array(this.memory.buffer);
Expand Down
67 changes: 26 additions & 41 deletions runtimes/web/src/ui/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as z85 from "../z85";
import { Netplay, DEV_NETPLAY } from "../netplay";
import { Runtime } from "../runtime";
import { State } from "../state";
import { callAt60Hz } from "../update-timing";

import { MenuOverlay } from "./menu-overlay";
import { Notifications } from "./notifications";
Expand Down Expand Up @@ -83,7 +84,7 @@ export class App extends LitElement {
}

// Try to begin playing audio
this.runtime.unlockAudio();
this.runtime.pokeAudio();
}

constructor () {
Expand Down Expand Up @@ -261,8 +262,8 @@ export class App extends LitElement {

const down = (event.type == "keydown");

// Poke WebAudio
runtime.unlockAudio();
// Try to begin playing audio
runtime.pokeAudio();

// We're using the keyboard now, hide the mouse cursor for extra immersion
document.body.style.cursor = "none";
Expand Down Expand Up @@ -427,13 +428,7 @@ export class App extends LitElement {
}
}

// When we should perform the next update
let timeNextUpdate = performance.now();
// Track the timestamp of the last frame
let lastTimeFrameStart = timeNextUpdate;

const onFrame = (timeFrameStart: number) => {
requestAnimationFrame(onFrame);
const doUpdate = (interFrameTime: number | null) => {

pollPhysicalGamepads();
let input = this.inputState;
Expand All @@ -450,45 +445,28 @@ export class App extends LitElement {
}
}

let calledUpdate = false;

// Prevent timeFrameStart from getting too far ahead and death spiralling
if (timeFrameStart - timeNextUpdate >= 200) {
timeNextUpdate = timeFrameStart;
}

while (timeFrameStart >= timeNextUpdate) {
timeNextUpdate += 1000/60;

if (this.netplay) {
if (this.netplay.update(input.gamepad[0])) {
calledUpdate = true;
}

} else {
// Pass inputs into runtime memory
for (let playerIdx = 0; playerIdx < 4; ++playerIdx) {
runtime.setGamepad(playerIdx, input.gamepad[playerIdx]);
}
runtime.setMouse(input.mouseX, input.mouseY, input.mouseButtons);
runtime.update();
calledUpdate = true;
if (this.netplay) {
this.netplay.update(input.gamepad[0]);
} else {
// Pass inputs into runtime memory
for (let playerIdx = 0; playerIdx < 4; ++playerIdx) {
runtime.setGamepad(playerIdx, input.gamepad[playerIdx]);
}
runtime.setMouse(input.mouseX, input.mouseY, input.mouseButtons);
runtime.update();
}

if (calledUpdate) {
this.hideGamepadOverlay = !!runtime.getSystemFlag(constants.SYSTEM_HIDE_GAMEPAD_OVERLAY);
this.hideGamepadOverlay = !!runtime.getSystemFlag(constants.SYSTEM_HIDE_GAMEPAD_OVERLAY);

runtime.composite();
runtime.composite();

if (constants.GAMEDEV_MODE) {
// FIXED(2023-12-13): Pass the correct FPS for display
devtoolsManager.updateCompleted(runtime, timeFrameStart - lastTimeFrameStart);
lastTimeFrameStart = timeFrameStart;
if (constants.GAMEDEV_MODE) {
if (interFrameTime !== null) {
devtoolsManager.updateCompleted(runtime, interFrameTime);
}
}
}
requestAnimationFrame(onFrame);
callAt60Hz(doUpdate);
}

onMenuButtonPressed () {
Expand All @@ -497,12 +475,19 @@ export class App extends LitElement {
this.inputState.gamepad[0] |= constants.BUTTON_X;
} else {
this.showMenu = true;
// During netplay, opening the menu doesn't pause the game, so shouldn't
// pause the audio either. Also, when audio is paused, the browser is
// more likely to throttle our update rate when we're in the background.
if (!this.netplay) {
this.runtime.pauseAudio();
}
}
}

closeMenu () {
if (this.showMenu) {
this.showMenu = false;
this.runtime.unpauseAudio();

// Kind of a hack to prevent the button press to close the menu from being passed
// through to the game
Expand Down
Loading