返回资讯中心
外部精选
软件工程
#社区热议

DOOM, simulated and rendered inside the Firebird SQL database using WASM

DOOM, simulated and rendered inside the Firebird SQL database, running entirely in your

Hacker Newsmariuz2026/10/6 · 26 天前20 分钟阅读

以下正文同步自 Hacker News,版权归原站所有,已转换为易读排版。

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.

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 clip arrays, markceiling / markfloor RENDER_WALLS / FRAME_WALLS (or FRAME_WALLS_WINDOWED)

r_plane.c visplanes: R_FindPlane, R_CheckPlane, R_MakeSpans, R_MapPlane c_top/c_bot/f_top/f_bot per slice, FRAME_VISPLANES; spans in src/renderer.js

r_things.c RENDER_SPRITES / FRAME_SPRITES

The renderer

There are two wall renderers. 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). 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 FRAME_WALLS_WINDOWED 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:

DOOM doesn't texture floors column by column. While drawing walls it records, for each column, which rows of the front sector's ceiling and floor the wall leaves visible (markceiling and markfloor in R_StoreWallRange). It collects those rows into visplanes: one per distinct height, flat and light level, with at most one span per column. Then it draws each visplane as horizontal spans, because every pixel in a row of a flat surface is the same distance away.

Here RENDER_WALLS returns those rows with every slice: c_top/c_bot for the ceiling and f_top/f_bot for the floor, computed from the same clip window it uses for the walls. The browser does R_FindPlane/R_CheckPlane. It groups the spans by (height, flat, light), starts a new plane when a column is already taken, and merges all sky into one plane. R_MakeSpans then sweeps each plane left to right, turning column spans into row spans. R_MapPlane draws each row span with one distance and light lookup, stepping the texture coordinates linearly. SELECT * FROM frame_visplanes shows the current frame's planes in the SQL console, and the stats line under the view counts them. Across Freedoom's maps the busiest frame needs 42, comfortably under vanilla DOOM's MAXVISPLANES of 128.

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.

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.

.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.

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. 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.

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.

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →