Skip to content
HN On Hacker News ↗

GitHub - mariuz/firebird-doom: DOOM simulated and rendered inside Firebird SQL, running in the browser via Firebird WASM

▲ 6 points • 4 comments • by mariuz • 4d ago • HN discussion ↗

Pangram verdict · v3.3

We believe this text is mainly AI, with some human-written content.

79 %

AI likelihood · overall

AI
2% human-written 98% AI-generated
SEGMENTS · HUMAN 0 of 3
SEGMENTS · AI 1 of 3
WORD COUNT 1,070
PEAK AI % 87% · §3
Analyzed
Oct 6
backend: pangram/v3.3
Segments scanned
3 windows
avg 357 words each
Distribution
2 / 98%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 1,070 words · 3 segments analyzed

Human AI-generated
§1 Mixed · 63%

DOOM, simulated and rendered inside the Firebird SQL database, running entirely in your browser on Firebird 6 compiled to WebAssembly. ▶ Play: https://mariuz.github.io/firebird-doom/ Every game tic is a PSQL procedure call. Every frame is a SELECT. JavaScript only reads the keyboard and paints the rows Firebird returns. These screenshots come from npm run screenshots, which runs the same SQL and rasteriser as the page, headless in Node. This project follows CedarDB's SQL DOOM (the original WAD, rendered by a database) and DOOMQL (a DOOM-like game in pure SQL), and DuckDB-DOOM (a SQL raycaster in the browser through DuckDB-WASM). This one is graphical, plays real DOOM maps, and runs Firebird in the tab. How it works keyboard ─▶ SELECT * FROM doom_tic(tics, fwd, side, turn, fire, use, weapon, run) ← game logic SELECT * FROM frame_walls ← which wall slice is visible in which column SELECT * FROM frame_sprites ← which sprite frame, where, how bright SELECT * FROM frame_sectors ← live floor/ceiling heights and light ─▶ JS looks up texels + colormaps ─▶ <canvas> DOOM source Firebird WAD lumps VERTEXES LINEDEFS SIDEDEFS SECTORS SEGS SSECTORS NODES THINGS tables of the same names (sql/schema.sql) info.c mobjinfo THING_TYPES rows (src/thinginfo.js) R_PointInSubsector SECTOR_AT() walks the BSP; INIT_MAP locates every thing in one recursive CTE BLOCKMAP LINE_BLOCKS, a 128×128 grid built by INIT_MAP P_CheckPosition, P_TryMove CHECK_POSITION, plus wall sliding in PLAYER_THINK P_CheckSight CHECK_SIGHT() walks the blockmap cells along the sight line P_UseLines, P_CrossSpecialLine, EV_DoDoor/Plat/Floor, stairs, exits ACTIVATE_LINE, MOVERS, MOVERS_THINK A_Look / A_Chase / attacks, pain, death, barrels MONSTERS_THINK P_LineAttack (pistol, shotgun, chaingun, fist) HITSCAN P_TouchSpecialThing pickups in PLAYER_THINK light flashes, strobes, glows LIGHTS_THINK R_RenderBSPNode, R_CheckBBox, R_ClipSolidWallSegment (solidsegs) RENDER_SLICES_BSP (sql/render.sql) r_segs.c / r_plane.c clip arrays RENDER_WALLS / FRAME_WALLS (or FRAME_WALLS_WINDOWED) r_things.c RENDER_SPRITES / FRAME_SPRITES The renderer There are two wall renderers.

§2 Mixed · 42%

You can switch between them under Renderer in the page's settings; the choice is stored in VIEWCFG.USE_BSP. BSP front to back with solidsegs (the default).

§3 AI · 87%

RENDER_SLICES_BSP walks NODES from the root like R_RenderBSPNode, nearer child first. Before entering the farther child it projects that child's bounding box onto the screen (R_CheckBBox) and skips the whole subtree if every column it covers is already hidden. In each subsector it projects the segs that face the viewer (R_AddLine). When a seg is solid (one-sided, or a closed door) its columns are marked as covered (R_ClipSolidWallSegment). PSQL has no arrays, so DOOM's solidsegs list is a VARCHAR with one character per screen column, and the traversal stack is a string too. The walk stops when no '0' is left in the coverage string. On Freedoom's 36 maps this is about 3× faster than brute force, and it finds exactly the same visible walls. CI checks that. Brute force. RENDER_SLICES projects every linedef in front of the camera, whether or not anything hides it. Both generators transform into view space, clip to the near plane, and intersect each screen column's ray with the seg. That gives an exact depth, a texture column, and the vertical opening the seg leaves (open_top/open_bot) for whatever is behind it. RENDER_WALLS then plays the part of DOOM's ceilingclip[]/floorclip[] arrays: ORDER BY col, depth sorts each column front to back, and the clip window is carried down the column. The same idea, stated declaratively, is the FRAME_WALLS_WINDOWED view: SELECT * FROM (SELECT s.*, COALESCE(MAX(open_top) OVER (PARTITION BY col ORDER BY depth ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0) clip_top, COALESCE(MIN(open_bot) OVER (PARTITION BY col ORDER BY depth ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 1e9) clip_bot FROM render_slices s) WHERE clip_top < clip_bot The smoke test checks that it matches FRAME_WALLS slice for slice. The game uses the procedural clip because Firebird's window sort costs about twice as much. Two Firebird-specific performance lessons: Derived tables are inlined. Each reference to a computed CTE column re-evaluates its whole expression tree, so a five-deep chain of projections took seconds. Generator procedures compute each value once into a variable. A PSQL loop runs at about 0.2 µs per statement in WASM. Pin the join order. Joining a computed range to SCREEN_COLS took 65 s as an inner join, because the optimizer drove from the wrong side. With CROSS JOIN LATERAL or LEFT JOIN it took 0.2 s. Running locally npm install npm run fetch-wad npm test npm run serve npm run fetch-wad downloads Freedoom and writes public/wads/freedoom1.wad, minus sound and music. npm test runs the game SQL in the real WASM engine under Node. npm run serve builds dist/ and serves it on http://localhost:8080 without COOP/COEP headers, just like GitHub Pages. That way the service worker is what makes the page cross-origin isolated (Firebird's pthreads need SharedArrayBuffer). You can also load your own DOOM1.WAD / DOOM.WAD / DOOM2.WAD with the file picker. Nothing is uploaded anywhere. npm run test:all-maps loads, plays and renders every map in the WAD, and fires each map's first teleporter. npm run test:renderers compares the BSP and brute-force renderers from several spots and headings on every map, and reports the speed-up. npm run screenshots regenerates docs/*.png. node scripts/bench.mjs queries.sql times SQL statements against a loaded map, with statements separated by -- @@ lines. Controls Click the view to capture the mouse. WASD or the arrow keys move, Ctrl or a click fires, Space/E uses, Shift runs, 1–4 pick weapons, Tab shows the automap, and P pauses. Under the view you can set Detail (320 or 160 columns) and Renderer (BSP + solidsegs, or brute force). Both settings are remembered in your browser. The SQL console under the game queries the live game database. Try the IDKFA button. Deploying .github/workflows/pages.yml runs on every push to main. It installs, fetches and caches Freedoom, runs the SQL smoke test and the BSP-vs-brute-force renderer check, builds, and publishes dist/ to GitHub Pages. Pull requests run everything except the deploy. Simplifications Monster movement, attack timing and accuracy follow DOOM's rules, not its exact frame tables. Projectiles fly flat. There's no sound, no rocket launcher, plasma or BFG, and no crushers. Floors and ceilings are drawn per wall slice rather than as DOOM's visplanes. Large maps with many monsters awake at once can still drop below 10 fps. The Low detail setting (160 columns, like DOOM's own) halves the render cost. Credits Engine: Electric Firebird (firebird-wasm, Apache-2.0) Game data: Freedoom (BSD-3-Clause) DOOM © id Software. This is a clean-room SQL re-implementation that reads the WAD format. Inspired by SQL DOOM / DOOMQL by CedarDB and duckdb-doom MIT licensed.