Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Real, unmodified software compiled to WebAssembly:
| Git | 2.47 | Core version control operations |
| Vim | 9.1 | Full editor with ncurses terminal UI |
| NetHack | 3.6.7 | Classic roguelike with curses UI |
| fbDOOM | (maximevince) | id Software's DOOM via the kernel's `/dev/fb0` Linux fbdev surface |
| fbDOOM | (maximevince) | id Software's DOOM via the kernel's `/dev/fb0` Linux fbdev surface — single-player and **2-peer deathmatch over a WebRTC `RTCDataChannel`** (chocolate-doom 3.1.0 net stack + a POSIX-sockets transport; see the `doom-mp` browser demo) |
| Perl | 5.40 | Interpreter with core modules |
| Ruby | 3.3 | Interpreter with core stdlib |
| SpiderMonkey | 140 ESR | JavaScript engine backing the Node.js-compatible runtime with Intl, SharedArrayBuffer, worker_threads, and npm package installs. |
Expand Down
19 changes: 13 additions & 6 deletions apps/browser-demos/lib/relay-network-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ export const ENVELOPE_MAX_TOTAL = 1024;
export interface RelayChannelOptions {
kernel: RelayKernel;
channel: RelayDataChannel;
/** This peer's synthetic IPv4 — unused on the wire today; reserved for routing. */
localAddr: [number, number, number, number];
/** The remote peer's synthetic IPv4 — used as `from_addr` for inbound datagrams. */
peerAddr: [number, number, number, number];
}
Expand All @@ -80,7 +78,6 @@ export class RelayChannel {
this.kernel = opts.kernel;
this.channel = opts.channel;
this.peerAddr = opts.peerAddr;
void opts.localAddr; // reserved for future per-iface routing

this.inboundListener = (e: Event) => this.handleInbound(e as MessageEvent);
this.channel.addEventListener("message", this.inboundListener);
Expand Down Expand Up @@ -123,9 +120,19 @@ export class RelayChannel {

private handleOutbound(ev: RelayOutboundDatagram): void {
if (this.closed) return;
// `dstIp` is implicit in v1: the channel terminates at peerAddr. Future
// mesh routing will dispatch on dstIp; for now we just forward.
void ev.dstIp;
// This channel terminates at exactly one peer. Drop any datagram the
// kernel asks us to deliver to a different IP — silently, because UDP
// is best-effort. Without this guard, a `sendto(8.8.8.8, …)` from any
// pid would be redirected onto the WebRTC peer, leaking traffic across
// the synthetic /24 boundary.
if (
ev.dstIp[0] !== this.peerAddr[0] ||
ev.dstIp[1] !== this.peerAddr[1] ||
ev.dstIp[2] !== this.peerAddr[2] ||
ev.dstIp[3] !== this.peerAddr[3]
) {
return;
}

const total = ENVELOPE_HEADER_LEN + ev.data.length;
if (total > ENVELOPE_MAX_TOTAL) {
Expand Down
68 changes: 68 additions & 0 deletions apps/browser-demos/pages/doom-mp/doom-mp.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* Page-specific styling for the multiplayer-DOOM-over-WebRTC demo.
* Layered on top of pages/webrtc/webrtc.css (which provides .handshake,
* .sdp-pane, .webrtc-controls, .webrtc-status). */

.doom-mp-page {
display: flex;
flex-direction: column;
gap: 12px;
min-height: 0;
flex: 1 1 auto;
}

.doom-mp-signaling {
background: #fff;
border: 1px solid #e5e5e5;
border-radius: 6px;
padding: 0.75rem 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}

.role-selector {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.9rem;
color: #333;
}

.role-selector label {
display: inline-flex;
align-items: center;
gap: 0.3rem;
cursor: pointer;
}

.role-selector .role-hint {
color: #777;
font-size: 0.8rem;
font-style: italic;
}

.doom-frame {
background: #000;
padding: 8px;
border: 1px solid #333;
border-radius: 4px;
flex: 1 1 auto;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
}

canvas#fb {
image-rendering: pixelated;
max-width: 100%;
max-height: 100%;
aspect-ratio: 8 / 5;
width: auto;
height: auto;
display: block;
outline: none;
background: #000;
}

77 changes: 77 additions & 0 deletions apps/browser-demos/pages/doom-mp/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOOM (multiplayer) — wasm-posix-kernel</title>
<link rel="stylesheet" href="../../lib/layout.css">
<link rel="stylesheet" href="../webrtc/webrtc.css">
<link rel="stylesheet" href="./doom-mp.css">
</head>
<body>
<nav class="sidebar">
<div class="sidebar-title">Demos</div>
<a href="/">Simple Programs</a>
<a href="/pages/nginx/">nginx</a>
<a href="/pages/php/">PHP CLI</a>
<a href="/pages/nginx-php/">nginx + PHP-FPM</a>
<a href="/pages/mariadb/">MariaDB</a>
<a href="/pages/wordpress/">WordPress (LEMP-but-SQLite)</a>
<a href="/pages/lamp/">WordPress (LEMP)</a>
<a href="/pages/shell/">Shell</a>
<a href="/pages/doom/">DOOM</a>
<a href="/pages/doom-mp/" aria-current="page">DOOM (multiplayer)</a>
<a href="/pages/webrtc/">WebRTC</a>
</nav>
<div class="main doom-mp-page">
<div class="main-header">
<h1>DOOM multiplayer over WebRTC</h1>
<p class="subtitle">
Two browsers, one peer-to-peer <code>RTCDataChannel</code>, real BSD-sockets
UDP between two <a href="https://github.com/maximevince/fbDOOM" target="_blank" rel="noopener">fbDOOM</a>
instances. No central server.
</p>
</div>

<section class="doom-mp-signaling">
<div class="role-selector">
<strong>Role:</strong>
<label><input type="radio" name="role" value="host" checked> Host (10.99.0.1)</label>
<label><input type="radio" name="role" value="join"> Join (10.99.0.2)</label>
<span class="role-hint">Host clicks <em>Create offer</em> first. Joiner pastes the offer and clicks <em>Accept offer</em>.</span>
</div>

<div class="handshake">
<div class="sdp-pane">
<label for="local-sdp">
Local SDP
<button id="copy-local" type="button">Copy</button>
</label>
<textarea id="local-sdp" readonly placeholder="Click &quot;Create offer&quot; or &quot;Accept offer&quot; to generate&hellip;"></textarea>
</div>
<div class="sdp-pane">
<label for="remote-sdp">Remote SDP</label>
<textarea id="remote-sdp" placeholder="Paste the other peer's SDP here&hellip;"></textarea>
</div>
</div>

<div class="webrtc-controls">
<button id="create-offer" type="button">Create offer</button>
<button id="accept-offer" type="button" disabled>Accept offer</button>
<button id="accept-answer" type="button" disabled>Accept answer</button>
<button id="reset" type="button" disabled>Reset</button>
<button id="start-doom" type="button" class="primary" disabled>Start DOOM</button>
</div>

<div class="webrtc-status">
<pre id="status">idle</pre>
</div>
</section>

<div class="doom-frame">
<canvas id="fb" tabindex="0" width="640" height="400"></canvas>
</div>
</div>
<script type="module" src="./main.ts"></script>
</body>
</html>
Loading
Loading