Add doom cog: browser-playable DOOM on the Seed (first C-FFI cog, GPLv2) - #28
Add doom cog: browser-playable DOOM on the Seed (first C-FFI cog, GPLv2)#28shaal wants to merge 5 commits into
Conversation
Adds src/cogs/doom — an API-provider cog that runs the portable doomgeneric engine on-device (software rendered, no GPU/X11/SDL) and serves a touch- and keyboard-friendly game client over a loopback HTTP API. The seed agent proxies /api/v1/cogs/doom/* to 127.0.0.1:8066 with a per-cog bearer token. This is the first C-FFI cog: build.rs compiles the vendored doomgeneric C engine via the cc crate and src/main.rs drives it through extern "C" callbacks on a dedicated engine thread, publishing frames to HTTP workers. Key points: - Auth: loopback bind + bearer-token (constant-time, subtle) required on /frame,/input,/stream by default; / and /health open. Optional opt-in direct-LAN mode (DOOM_OPEN=1 + DOOM_BIND=0.0.0.0), OFF by default. - Transport: polling JPEG-over-HTTP at native 320x200 (Content-Length forced, client re-wraps as image/jpeg) — the agent proxy buffers streams and has no WebSocket support, so polling is what survives it. Adaptive frame pacing. - Game data: ships no DOOM WAD; uses the redistributable FreeDoom IWAD (v0.13.0 freedoom1.wad) as a cog.toml [[assets]] entry with real sha256. Resolved at runtime from DOOM_WAD / <exe_dir>/freedoom1.wad / ./freedoom1.wad. - Licensing: this cog is GPLv2 (it links GPLv2 doomgeneric/DOOM source) while the rest of the repo is MIT. LICENSE (full GPLv2), NOTICE, and the vendored engine's own GPLv2 LICENSE document the scope (this directory only). Also adds docs/adrs/ADR-019-doom.md and lists the platform/API cogs (017-019) in the ADR index. Verified: cargo check --release --all-targets and cargo build --release pass (C engine compiles + links); a native smoke run loaded the FreeDoom IWAD and served real JPEG frames, with /frame returning 401 without a token and 200 with.
|
Heads-up from running this cog on a Seed (Pi Zero 2 W) for a couple of days: the engine can wedge after long uptime — the render thread stops producing frames while the HTTP server stays up, so the client just shows a frozen last frame. After ~38h (~6.4M frames) the The platform-level half — the agent should detect a wedged-but-alive cog rather than trusting "process up / HTTP 200" — is filed as cognitum-one/support#54 (sibling of support#52). But there are a few cog-side improvements worth folding into this PR so
Happy to send these as a follow-up once the PR lands, or fold them in here — whichever you prefer. |
GetAdjustedTime() computed (time_ms * TICRATE) in signed 32-bit int. Once uptime passed ~17h (time_ms > INT_MAX/35) the multiply overflowed and wrapped negative, so NetUpdate()'s newtics went negative and the BuildNewTic() loop stopped running. BuildNewTic() is the only thing that advances maketic and the only caller of I_StartTic() (the input pump), so the game simulation and all input froze while D_Display() kept drawing — the HTTP server stayed up and the frame counter kept climbing, but the picture was stuck and the game ignored every key (not even ESC opened the menu). I_GetTime() had the same latent bug in uint32 arithmetic at ~34h. Widen both multiplies to 64-bit so the tic clock can't overflow. Verified by cross-compiling the armv7 binary and running it on the device: input is responsive again (ESC toggles the menu) and frames advance normally.
The cog ships no game data and resolves freedoom1.wad next to the binary at runtime, but the asset block only had source_url (the upstream ZIP) and no gcs_path — so the agent's install flow had no single re-hosted file to fetch and sha-verify, the way the cognitive-pipeline cog does. Result: a fresh install would start with no IWAD and fail to find freedoom1.wad. Add gcs_path = "wads/freedoom1.wad" so the agent pulls the WAD from the cognitum registry bucket, verifies the (already-correct) sha256, and drops it beside the binary. source_url stays as provenance. Maintainer still has to extract freedoom1.wad from the upstream ZIP and upload that one file to gs://cognitum-apps/wads/freedoom1.wad.
The agent resolves an asset's gcs_path under cogs/<arch>/ (the cognitive-pipeline cog's "models/..." path lands at cogs/arm/models/...). Spell out that for this arm-only cog the WAD must be uploaded to gs://cognitum-apps/cogs/arm/wads/freedoom1.wad, so whoever publishes the cog knows the precise destination.
|
| field | value |
|---|---|
| file | freedoom1.wad |
| size | 28,795,076 bytes |
| sha256 | 7323bcc168c5a45ff10749b339960e98314740a734c30d4b9f3337001f9e703d |
| source | extract freedoom-0.13.0/freedoom1.wad from https://github.com/freedoom/freedoom/releases/download/v0.13.0/freedoom-0.13.0.zip |
| license | BSD-3-Clause (FreeDoom) |
Without this upload, fresh installs start with no IWAD and the cog can't render. The sha256 above already matches cog.toml and is what the install handler verifies.
- Fall back to reading <exe_dir>/.cog-token when the agent spawns the cog without COGNITUM_COG_TOKEN (its cold-boot path skips it), so the paired game endpoints stop 401ing for correctly-proxied clients. - Advertise an un-proxied full-speed LAN instance in the web UI via DOOM_FAST_PORT / a `fast-port` file (the proxy caps frames at ~1 fps); the cog substitutes the port into index.html at serve time.
|
Update — pushed
Verified end-to-end through the agent proxy on the device: |
Summary
Adds
doom— an API-provider cog that runs the portabledoomgenericengine on-device (softwarerendered, no GPU / X11 / SDL) and serves a touch- and keyboard-friendly game
client to any browser. The seed agent reverse-proxies
/api/v1/cogs/doom/*→127.0.0.1:8066, injecting a per-cog bearer token.Open the Seed dashboard, tap DOOM, play in your phone's browser. Rendered
entirely on the device.
ADR: docs/adrs/ADR-019-doom.md.
What it adds
src/cogs/doom/—Cargo.toml,build.rs,cog.toml,Dockerfile,README.md,LICENSE(GPLv2),NOTICE,src/main.rs,assets/index.html,and
vendor/doomgeneric/(the vendored C engine + its GPLv2LICENSEand anorigin
README).docs/adrs/ADR-019-doom.md+ an ADR-index entry (017–019 platform/API cogs).Architecture
vendored doomgeneric C engine via
build.rs+ thecccrate and drives itthrough
extern "C"DG_*callbacks. One dedicated engine thread owns the(non-thread-safe) engine —
doomgeneric_Create()once, then a_Tick()loop— publishing frames to HTTP worker threads that only read frames / push input.
GET /frame?since=Nand posts toPOST /input. We do not use WebSocket ora persistent MJPEG stream because the agent proxy buffers response bodies and
has no WS upgrade — a polling JSON/JPEG API is what survives it unchanged.
/frameforcesContent-Length(the proxy otherwise leaks chunk framing andrelabels
image/jpeg); the client re-wraps bytes as animage/jpegBlob.320×200 keeps per-frame JPEG encode cheap on a Pi Zero 2 W. Client uses
adaptive frame pacing (backs off on 429).
bind_loopback_only = true;/frame,/input,/streamrequire the per-cog bearer token (constant-time comparevia
subtle);/and/healthare open. If the agent spawns the cog withoutinjecting
COGNITUM_COG_TOKEN, the cog falls back to the agent's own<exe_dir>/.cog-tokenfile (the same secret the proxy injects) so pairedclients keep working; unpaired clients are still rejected.
First C-FFI cog
This establishes the
build.rs+cc+extern "C"pattern for the repo. Therunner already has a C toolchain, so
cargo check/cargo buildwork in CI withno new infra. The vendored source set is the upstream
SRC_DOOMlist minus allplatform backends and the SDL/Allegro mixers (the cog provides a headless
backend; no audio in v0.1.0).
This cog directory is GPLv2; the rest of the repo stays MIT. Linking the
vendored doomgeneric engine (derived from id Software's GPLv2 DOOM source) makes
the cog binary a GPLv2 derivative, so
Cargo.tomlsetslicense = "GPL-2.0-or-later". We ship:src/cogs/doom/LICENSE— full GPLv2 text governing this cogsrc/cogs/doom/NOTICE— explains the GPLv2-in-MIT scope (this directory only)src/cogs/doom/vendor/doomgeneric/LICENSE+README— engine license + originGPL applies only to this self-contained derivative; it does not relicense the
repo. Maintainers: please confirm you're OK accepting a GPLv2 subdirectory in
this otherwise-MIT repository. If not, the alternative is hosting the doom cog
in a separate repo referenced from the registry — but vendoring keeps the PR
self-contained and CI green. This is the main thing to decide on review.
FreeDoom for game data (no DOOM WAD shipped)
The cog ships no DOOM game data. It uses the freely-redistributable
FreeDoom IWAD as a runtime asset (
cog.toml [[assets]]):freedoom1.wad7323bcc168c5a45ff10749b339960e98314740a734c30d4b9f3337001f9e703d,28,795,076 bytes
freedoom-0.13.0.zipfrom the FreeDoom GitHub releaseThe agent fetches + sha256-verifies it at install and places it next to the
binary; the cog resolves
$DOOM_WAD→<exe_dir>/freedoom1.wad→./freedoom1.wad. The WAD is not committed. Maintainers may re-host it tothe cognitum registry on publish (add
gcs_pathto the asset entry, like theother asset-bearing cogs).
Optional direct-LAN mode (off by default) — proposal
Two env flags let you play directly over the LAN, bypassing the agent proxy and
its ~1 fps rate limit:
DOOM_OPEN=1disables the token check on the gameendpoints and
DOOM_BIND=0.0.0.0binds all interfaces. Both default off.Tradeoff vs ADR-095's loopback-only posture: in this mode the game
frame/input endpoints are reachable by anyone on the LAN, unauthenticated and
unthrottled — never the device/agent API, only this cog's game endpoints. It's
opt-in, loudly logged at startup, and documented. Happy to drop the flags
entirely and ship proxy-only if maintainers prefer; nothing else depends on them.
When such a direct-LAN instance runs alongside the proxied one, set
DOOM_FAST_PORT(or drop afast-portfile next to the binary) on the proxiedinstance and its web UI shows a banner linking to the full-speed instance — the
cog substitutes the port into
index.htmlat serve time. Unset → no banner.Build / test
What I verified
cargo check --release --all-targetspasses (the gate CI runs) — the Cengine compiles via
build.rs.cargo build --releaselinks cleanly (~988 KB native binary).GET /health→ 200,GET /→ client HTML,GET /frame→ 401 without atoken / 200 + valid
image/jpeg(JFIF) with the token,POST /input→accepted.
cog.toml id = doom,[[bin]] name = cog-doom, ADR present,asset sha256 is real (no
TODO-).The armhf Docker cross-build (per-cog
Dockerfile, manual) was not run in thisenvironment.
Notes / decisions
builds without
git submodule update; the exact compiled sources are pinnedand reviewable in-tree.
cognitumpackage (not a Cargo workspace) and cogsare standalone crates, so nothing was added to the root
Cargo.toml— matcheshow
tailscale/cognitive-pipelineare set up.