Typhon is an embedded, persistent, ACID-compliant database engine written in .NET (C#), designed for game servers and real-time simulations. Its positioning: an engine that stores data the way game engines do, while providing database-level guarantees. The author, Loïc Baumann (Nockawa), has 30 years of experience in real-time 3D engines and systems software. This post synthesizes his blog series "A Database That Thinks Like a Game Engine":
- Why I'm Building a Database Engine in C#
- What Game Engines Know About Data That Databases Forgot
- Microsecond Latency in a Managed Language
- Constrained generics = monomorphization (same optimization as Rust generics)
- sealed = devirtualization (JIT converts virtual calls to direct calls and inlines)
- static readonly = JIT dead-code elimination (disabled
ifblocks vanish from native code—zero-cost observability) - SoA layout = JIT auto-vectorization (AVX2 processes 8 floats per instruction)
- Choose storage strategy by access pattern: KV caches, model weights, and intermediate activations in AI inference also have distinct access and persistence needs
- Zone-map-style early rejection: in RAG retrieval, quickly filter clearly irrelevant documents before fine-grained ranking
- Zero-copy + blittable structs: tensor data transfer in inference engines can borrow this serialization-avoidance approach
GitHub: https://github.com/nockawa/typhon
Part 1: Why C#?
"Modern C# is two languages"
Most people only know the managed half—classes, GC, LINQ, async/await. The other half—unsafe, fixed, ref struct, Span<T>, StructLayout(Explicit), System.Runtime.Intrinsics—is essentially C-level systems programming. The JIT generates machine code identical to what C compilers produce.
The core argument: the bottleneck is memory layout, not language.
> A cache miss to DRAM costs 61-73ns (~250 CPU cycles). A CAS hitting L1 costs 1.4ns. The ratio is 50:1.
If your data structures are cache-friendly, the language barely matters; if cache-unfriendly, even Rust's zero-cost abstractions can't save you.
Roslyn Analyzers instead of a borrow checker
Instead of seeking Rust-style comprehensive safety guarantees, the author wrote custom compiler analyzers (TYPHON001–007) that enforce domain-specific safety checks only on performance-critical types. For example, ChunkAccessor must be passed by ref, and Transaction must be disposed. Error messages carry domain semantics: "causes page cache deadlock" is more actionable than Rust's "value moved here."
GC isn't the problem; allocation is
GC pause frequency depends on allocation frequency, not the language. Typhon's strategy: zero allocations on the hot path, via ref structs, stackalloc, Pinned Object Heap, and ArrayPool—four layers ensuring GC rarely triggers in steady state.
Part 2: What Game Engines Know That Databases Forgot
ECS and relational databases are the same thing
The series' most striking insight, in a table:
| ECS Concept | Database Concept | Shared Principle | |-------------|------------------|------------------| | Archetype | Table | Homogeneous, fixed-schema storage | | Component | Column | Typed, batch-iterable data | | Entity | Row | Identity with dynamic composition | | System | Query | Process all records matching a signature | | Frame Budget (16ms) | Latency SLA | Hard real-time deadline |
Two fields, separated by decades and industry boundaries, converged on structurally identical solutions—because they solve the same fundamental problem: managing structured data under performance constraints.
Three things learned from game engines
1. Cache locality by default. Row stores load entire rows (names, inventory, health) to read positions. ECS stores by type: all positions contiguous, all health contiguous. Reading 10,000 positions becomes a linear memory scan where every byte is useful.
2. Zero-copy is the default, not an optimization. Traditional databases deserialize records into language-level objects. In ECS, components already exist in final in-memory layout—you just return a pointer. Typhon keeps this: components are blittable unmanaged structs read directly from pinned memory pages.
3. Entity is pure identity. An entity is just a 64-bit ID; all data lives externally in component tables. This separation enables per-component versioning, storage modes, and indexing.
Four things learned from databases
1. ACID transactions + per-component MVCC. Traditional databases version whole rows. Typhon versions each component independently: a 12-byte circular buffer per component with a 48-bit transaction sequence number. Updating PositionComponent doesn't create a new version of InventoryComponent.
2. Indexed selective access. ECS iterates all matching entities every frame, but game servers typically process only 1–4% of entities per frame (e.g., battle royale client relevance: 500–2,000 out of 50,000). Scanning everything wastes 25–100x work. Typhon makes B+Tree indexes first-class citizens in component storage.
3. Spatial partitioning. Two-layer spatial indexing integrated into component storage: Layer 1 is a sparse hash table for O(1) rejection of empty regions; Layer 2 is a page-backed R-Tree supporting AABB, radius, ray, frustum, and kNN queries. Both run within the same transaction model.
4. Persistence. WAL crash recovery, checkpointing, configurable fsync—things game servers need but ECS frameworks never provided.
Three storage modes: not all data is equal
| Mode | MVCC History | Persistence | Change Tracking | Use Case | |------|--------------|-------------|-----------------|----------| | Versioned | Full revision chain | WAL + checkpoints | MVCC | Inventory, economy, progression | | SingleVersion | Current state only | WAL + checkpoints | DirtyBitmap | Positions, health, high-frequency updates | | Transient | Current state only | None | DirtyBitmap | AI blackboards, threat scores, pathfinding drafts |
Same engine, same transaction API—but the storage layer does what each component type needs.
Views: bridging ECS Systems and materialized views
view.Refresh() receives change pushes through a lock-free ring buffer—only entities whose indexed fields actually changed are re-evaluated. 100,000 entities match a view but only 12 changed? Do 12 evaluations, not 100,000.
Part 3: Five Performance Principles
1. Control memory layout
Most dramatic example: switching from per-entity hash table lookups to cluster-based SoA storage—a 55x improvement purely from memory layout:
| Path | ns/entity | vs baseline | |------|-----------|-------------| | Standard EntityAccessor | 139 ns | 1.0x | | ArchetypeAccessor (cached) | 94 ns | 1.5x | | Cluster iteration | 2.5 ns | 55x |
Cluster size is auto-tuned (N=8 to N=64) to maximize entities per 8KB page; non-power-of-two values often pack better (N=14 fits 28 entities per page vs 16 for N=16).
B+Tree nodes are 256 bytes because the CPU's Adjacent Line Prefetcher fetches paired 64-byte lines within 128-byte aligned regions; two ALP triggers cover the whole node—same access cost as 128-byte nodes but nearly double capacity.
2. Eliminate hot-path allocations
Four layers: ref struct (scoped access), stackalloc (small temp arrays <64 elements), Pinned Object Heap (large long-lived buffers), ArrayPool<T> (medium reusable buffers). Result: zero hot-path allocations in steady state.
3. Reduce memory indirection
Zone maps maintain per-cluster min/max bounds on indexed fields. A range query WHERE Level >= 50 checks just two integers per cluster:
| Selectivity | Without zone maps | With zone maps | Speedup | |-------------|-------------------|----------------|---------| | 100% | 13.4 ms | 1.3 ms | 10x | | 50% | 13.4 ms | 0.65 ms | 21x | | 10% | 13.4 ms | 0.16 ms | 84x | | 1% | 13.4 ms | 0.05 ms | 268x |
Division elimination: integer division (idiv, 20–80 cycles) replaced by magic multipliers (3–4 cycles). Six lines of math, 20x speedup.
4. Let the JIT help
5. Design for the hardware
Concurrency cost hierarchy:
| Level | Cost | Example | |-------|------|---------| | 0: Thread-local | ~2 ns | TLS counters | | 1: Uncontended atomics | 5–10 ns | Read latches | | 2: Contended atomics | 20–140 ns | Multiple writers, same lock | | 3: Syscalls | 500–1000 ns | Timestamps | | 4: Context switch | ~10,000 ns | Blocking locks | | 5: Oversubscription | 100,000+ ns | Threads > cores |
Every design decision maps to: stay as low as possible in this hierarchy.
Measured performance
| Operation | Latency | |-----------|---------| | Cluster iteration (per entity) | 2.5 ns | | CRUD lifecycle (spawn/read/update/destroy/commit) | 2.95 µs | | Transaction create-read-commit (100 entities) | 3.6 µs | | B+Tree point lookup (10K entries) | 191 ns | | Component read (1 MVCC version) | 703 ns | | Component read (50 MVCC versions) | 720 ns | | Uncontended RW lock acquisition | 7.5 ns | | Cascading delete of 10K entities | 7.6 µs |
MVCC version count does not affect read performance: 50 versions read at nearly the same latency as 1 (720 ns vs 703 ns).
Parallel scaling: 8 workers achieve 7.1x speedup (89% efficiency). At 16 workers it drops to 6.7x—hitting the 7950X's L3 cache / CCD boundary, a hardware wall, not a software one.
Commentary
1. Domain-specific safety beats general-purpose safety
Rust's borrow checker offers comprehensive guarantees at the cost of a steep learning curve, long compiles, and inexpressible patterns. Typhon's approach: compile-time checks only for the unsafe operations you care about, with domain-semantic error messages. A pragmatic philosophy—build a safety net exactly sufficient for your domain, not theoretical perfection.
2. Two fields converging on the same solution is no coincidence
ECS and relational databases evolved independently for decades yet converged on nearly identical storage structures (columnar storage, batch iteration, typed fields). This shows the optimal organization of data is determined by hardware physics, not industry convention. Cache line sizes, memory bandwidth, and SIMD width drove two different domains to the same destination.
3. "Not all data is equal" is an overlooked design principle
Most databases treat all data identically. Typhon's three storage modes acknowledge that game server data has fundamentally different persistence and access patterns: position updates happen 60x/second and can be re-simulated after a crash; inventory changes are rare but must never be lost; AI runtime state is recomputed every frame and worthless after restart. One mechanism for all three is either over-engineered or under-protected.
4. Implications for AI infrastructure
Though Typhon targets game servers, several ideas apply to AI infrastructure:
5. What one person can do
Most striking: this is a solo developer project. 30 years of systems programming experience + deep hardware understanding + cross-domain insight (game engines + databases) = a database engine hitting microsecond latencies at alpha stage. Deep cross-domain experience is a scarce superpower—few people have 10+ years of hands-on experience in both fields, and it was exactly this perspective that revealed the convergence insight.
Series Preview
Part four (unreleased): "Deadlock-Free by Construction"—a mathematical argument, via three pillars, that deadlocks are structurally impossible. Not detection, not timeout-retry, but eliminating deadlock by design.