logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·30.10.26
← Back to news

We ported the original Doom to SQL

Original article
cedardb.com|265 points|41 comments|by Vaslo|Oct 3, 2026

We Ported the Original Doom to SQL

By Lukas Vogel | September 22, 2026 | ≈25\approx 25≈25 min read CedarDB Cloud is coming soon! →\rightarrow→ Join the Waitlist


⚡ TL;DR

We have successfully ported the game logic and rendering engine of the original 1993 Doom entirely into SQL, executing the entire experience inside a database.

ComponentPerformance / Spec
Game LoopFixed 35 FPS
RendererUp to 60 Hz (on AMD Ryzen 7 7840U)
Resolution320x200 frame buffer
Host LanguagePython (I/O only)

Try it yourself: You can play the shareware version of Episode 1 right now! 🇪🇺 [EU Servers] | 🇺🇸 [US Servers] Note: Deathmatch mode is active with four slots available on a first-come, first-served basis.

Pro Tip: If the game queue is full, you can still use SQL queries to inspect the live game state in real-time while you wait.


From Raycasting to BSP Trees

Previously, I created something that rendered ASCII art resembling Doom at 30 FPS. While popular, critics correctly noted that it was Doom actually closer to Wolfenstein 3D because it relied on a raycasting approach.

Real Doom is fundamentally different. It utilizes BSP (Binary Space Partitioning) trees, which allow for:

  • Efficient depth ordering.
  • The use of textures.
  • Arbitrary wall angles.
  • Variable floor and ceiling heights.

Determined to do it right (and utilizing some time during parental leave), I developed SQLDoom.


🛠️ The Ground Rules

To ensure this wasn't just a "cheat," I established a strict set of constraints:

  • Visual Fidelity: It must look like the authentic Doom.
  • Game Feel: It must play and behave like the original.
  • Pure Output: The only acceptable SQL output is a table or a bitmap containing exact RGB values per pixel.
  • Limited Client Scope: A client in another language is allowed, but only for:
    • Parsing user input.
    • Driving the game tics.
    • Displaying the resulting bitmap.
  • DB Flexibility: User-defined functions (UDFs) within the database are permitted.

System Architecture

I used a pygame script to handle the interface, triggering a game tic 35 times per second. The logic and rendering, however, reside entirely in the DB.

The logic runs on a strict 35 Hz loop, but the renderer is a pure function of the state tables, meaning the client can request frames as fast as the hardware allows.


📦 Loading Game Data

The .wad file format used by Doom is surprisingly relational. The hierarchy looks like this: VERTEXES →\rightarrow→ LINEDEF →\rightarrow→ SIDEDEF →\rightarrow→ SECTOR →\rightarrow→ THINGS.

Converting a WAD into a database was a straightforward process involving roughly 1,000 lines of Python. To demonstrate, here is a query that renders the famous E1M1 map from a top-down perspective:

WITH wall AS ( 
    SELECT round (( v1.x ) * t / 32.0 ) / 48 ) AS col, -- 48 units per column
    round (( v1.left_sd_id 0 AS solid -- one-sided lines are pass-through
    FROM linedefs l, generate_series ( 0 , 32 ) AS t -- walk each line in 32 steps
    JOIN vertexes v1 ON ( v1.map_id = 1 )
    SELECT string_agg ( 
        CASE 
            WHEN ( col , row ) IN ( SELECT col , row FROM wall WHERE solid ) THEN '#' 
            WHEN ( col , row ) IN ( SELECT col , row FROM wall ) THEN '.' 
            ELSE ' ' 
        END , '' 
        ORDER BY col 
    ) 
    FROM generate_series ( - 16 , 79 ) AS col, generate_series ( - 51 , - 21 ) AS row 
    GROUP BY row 
    ORDER BY row DESC;

Output:

#####################
# ..................#
# .##.### #
################
####### ####.........##.##........#######
.####### ### # ###########.#################
# #### # # ## #.# ####......
# #####

🕹️ The Game Loop

Porting the "feel" of Doom is the hardest part. Consider the complexity of a single rocket launcher blast:

  1. Input: Polling movement, turning, and firing.
  2. AI: Enemies navigating and attacking.
  3. Physics: Projectiles moving through space.
  4. Combat: Calculating blast radii for explosions.
  5. Visuals: Sprite rendering, animations, view bobbing, and the HUD.

The Timing Budget

The original game operated on a fixed clock. The math for the budget per tic is: Budget=1000 ms35 Hz≈28.6 ms\text{Budget} = \frac{1000\text{ ms}}{35\text{ Hz}} \approx 28.6\text{ ms}Budget=35 Hz1000 ms​≈28.6 ms

In SQLDoom, we maintain this 35 Hz35\text{ Hz}35 Hz logic to keep the game constants accurate, but we decouple the rendering. We use camera interpolation between tics so the movement feels smooth even if the frame rate exceeds 35 FPS.

The Tic Sequence

Because game tics are procedural, we use cedarscript (a language similar to PL/pgSQL) to plan the execution order.

Example Tic Logic:

doom_cs_clock ( map , p );
let mut plan = doom_cs_plan ( map , p ); -- returns a bitmask of functions to trigger
let use_queued = doom_tic_use ( map , p , plan );

if ( plan & 2 ) != 0 OR use_queued { 
    active = doom_cs_activate_specials ( map ); 
}

if ( plan & 4 ) != 0 OR active != 0 { 
    doom_cs_doors ( map , p ); 
}

doom_tic_move ( map , p );

![Image: GIF of rocket launcher gibbing soldiers] Caption: Rule 2 in action: The visceral feel of Doom, powered by SQL.