A 2D game engine and editor built from scratch around a GPU-accelerated falling-sand world, and a study in engine architecture, abstraction layers and modern graphics-API design.
Eruption is the direct successor to my Vulkan rasterizer, applying that project's explicit-graphics model to an entire 2D engine: a GPU-driven falling-sand world wrapped in a Unity-style editor, an entity-component system, data-driven tooling, rigid-body physics and hot-reloadable C++ scripting. It is as much an exercise in architecture and abstraction layers as in simulation.
From a Vulkan renderer to a full engine
This project follows directly from the Vulkan rasterizer, which covered the parts that make modern graphics intimidating: command buffers you record, descriptor sets you bind, immutable pipelines, explicit synchronisation and manual memory. With a renderer built by hand, the next step was to stop writing one renderer and start writing the engine that sits above it.
What carried over
Several lessons transferred almost directly. The explicit-API mental model became the backbone of the engine's graphics layer. The idea (already present in the Vulkan project) of splitting a generic renderer from a concrete graphics backend grew into a real abstraction. SPIR-V and shader reflection reappeared as a general reflection system. And thinking in terms of GPU buffers, barriers and compute dispatches made it natural to push the entire simulation onto the GPU.
The RHI: one explicit API, two backends
At the foundation sits a Render Hardware Interface (RHI): an abstract RHIDevice that acts as a factory for every GPU resource: buffers, textures, shaders, immutable pipelines with a pipeline cache, framebuffers, command buffers, descriptor sets and layouts, and synchronisation primitives including fences, events and timeline semaphores. Passes are recorded into command buffers, and dependencies between them are expressed as explicit barriers.The vocabulary is deliberately Vulkan-shaped, since that is the model the RHI targets, and it maps cleanly down to real APIs. Two complete backends implement the interface: OpenGL and Vulkan, as parallel sets of classes. The active backend is chosen in the engine config file, and every line of engine and game code above the RHI is identical regardless of which one is running. The hard part is designing an abstraction that expresses explicit, modern concepts and then emulates them on OpenGL where the API has no direct equivalent.
// One interface, implemented by both the GL and VK backends
class RHIDevice {
public:
virtual std::unique_ptr<RHIBuffer> create_buffer(const BufferDesc&) = 0;
virtual std::unique_ptr<RHITexture> create_texture(const TextureDesc&) = 0;
virtual std::unique_ptr<RHIPipeline> create_pipeline(const PipelineDesc&) = 0;
virtual std::unique_ptr<RHICommandBuffer> create_command_buffer() = 0;
virtual std::unique_ptr<RHIDescriptorSet> create_descriptor_set(const RHIDescriptorSetLayout*) = 0;
virtual std::unique_ptr<RHITimelineSemaphore> create_timeline_semaphore() = 0;
virtual bool supports_compute() const = 0;
virtual Backend backend() const = 0;
};
// Backend is selected at startup from config
auto device = create_rhi_device(config.graphics_backend);
The abstract RHI device: a backend-agnostic factory for GPU resources (excerpt).
Engine architecture: composition, systems & scenes
Above the RHI, the engine is a single composition root that owns the subsystems and wires them together: the platform layer (window, input, timer), the RHI device and a GPU profiler, an asset database, an event bus, a scene manager, audio, an input-action map and a save system. Nothing reaches for global singletons; subsystems are handed the dependencies they need.
Systems & the frame lifecycle
Behaviour lives in Systems with a simple init then update / fixed_update / render then shutdown lifecycle. A SystemManager runs them in three explicit phases, which cleanly separates variable-rate gameplay from fixed-rate physics from rendering, so the timing concerns never bleed into each other.
Scenes as a stack, entities with EnTT
Scenes form a stack; each owns its own EnTT[1] registry and its own SystemManager. Only the top scene ticks, while paused scenes below it keep their state, a tidy model for menus, levels and the editor's play mode. Entities and components are data-oriented through EnTT, with Transform and Hierarchy components providing parenting and a TransformSystem resolving world transforms. Fallible operations return a Result type carrying rich error information rather than throwing.
Reflection: letting data describe itself
The reflection layer is a direct descendant of the Vulkan project's shader reflection. There, SPIR-V was introspected so a material could describe its own uniforms; here, component types describe their own fields through a small macro DSL, auto-registered at startup into a type registry of names, byte offsets, types and editor hints.
A component describes its own fields; inspectors and serialization are generated from this.
From that single declaration, the engine generates an editor inspector and JSON serialization automatically, so a brand-new component becomes editable and saveable the moment it is reflected, with no bespoke UI or I/O code. It is the same principle as the renderer project: let the data describe itself, and let generic tooling consume it.
The simulation: a GPU falling-sand world
The heart of the project is the simulation. The world is a pixel grid driven by a Margolus-neighbourhood[2] cellular automaton: space is partitioned into 2×2 blocks and each step runs four phases at shifting offsets, so every neighbour eventually interacts and mass-conserving rules like falling sand and flowing liquid stay stable. Crucially, the entire simulation runs on the GPU.
Compute shaders & ping-pong buffers
The grid lives in two shader-storage buffers that ping-pong each step; a compute shader processes blocks in parallel and writes the next state into the other buffer. Explicit memory barriers between the four phases enforce ordering, the same synchronisation discipline as the Vulkan project, now expressed through the RHI instead of raw Vulkan.
Data-driven materials
Materials and their behaviours are data, not branches in a shader. Material, category and interaction definitions (density, thermal points, movement rules, reactions) are authored as data and compiled by an interaction compiler into compact GPU tables: a 256-entry material table, a packed interaction table, a colour palette and a category table that the compute shader reads each step. Conditions (temperature, contact with a material or category) and effects (transform, heat exchange, spawn a particle, destroy) let sand, water, fire, smoke and acid emerge from rules rather than hard-coded logic, carrying the Vulkan project's data-driven materials idea into simulation.
Editor
Graph
Categories
The material editor's three tabs: material properties, the interaction graph, and category definitions, all authored as data.Rule-driven material interactions: water cools lava into rock, while the lava boils the water into steam.
Only touch what moved
As pixels move, the simulation atomically marks dirty 32×32 chunks. Everything downstream (collider regeneration in particular) then only reprocesses the regions that actually changed, keeping a large, fully dynamic world affordable.
Particles & pixel-to-rigidbody physics
GPU particles
When pixels are knocked loose they become GPU particles. A two-pass compute pipeline integrates them (gravity, then DDA ray-marching against the grid for collision) and reintegrates settled particles back into the cellular grid, with a dead-list free list recycling slots so tens of thousands can live at once.A rigid body (the player) ploughing through sand knocks the pixels loose into GPU particles.
Turning pixels into colliders
To make the granular world physical, the engine converts pixel regions into Box2D[3] rigid bodies through a small computational-geometry pipeline: marching squares extracts contours from the solid pixels, segments are chained into closed loops, Ramer-Douglas-Peucker[4] simplifies them, and ear-clipping triangulates the result into Box2D polygon and chain shapes. Dynamic pixel bodies are stamped into the grid each frame so sand piles realistically on top of them, and connected-component analysis automatically splits a body into two when terrain cuts it apart.Two pixel-derived Box2D bodies, a crate and the player, colliding and pushing against each other.Carving rigid bodies apart at runtime: their colliders are re-extracted every frame, and a body cut in two becomes two independent bodies.
A full editor
Wrapping the runtime is a Unity[5]-style editor built on Dear ImGui[6]: around twenty dockable panels covering a scene hierarchy, an inspector, a viewport with gizmos, a console, a profiler, animation and animator-state-machine editors, a material editor, a pixel-art canvas and a prefab editor.
The Eruption editor: a live scene in the viewport, with the scene hierarchy, inspector, console and file explorer docked around it.
Reflection-driven inspectors, undo/redo & play mode
The inspector draws itself from the reflection data, so components expose editable UI with no per-component code. Every edit flows through a command-pattern undo/redo history with mergeable commands (dragging a gizmo collapses into a single history entry), while scenes and prefabs serialize to JSON. A runtime context drives play, pause and single-step, snapshotting and restoring both scene and physics state so you can play inside the editor and return cleanly to editing.
Profiler
A built-in profiler captures each frame for performance work, viewable as a flame-chart timeline or as a per-system, per-function CPU breakdown.
The profiler's flame-chart overview of a single frame.
The CPU breakdown, timing each system and function.
C++ gameplay scripting with hot reload
Gameplay is written in C++ scripts deriving from a component-script base with a Unity-like lifecycle (create, update, fixed-update, collision and trigger callbacks, UI events and coroutines), talking to the engine through a single host-API facade.
Compile, swap, keep playing
A file watcher detects script changes, a compiler drives CMake[7] to build them into a DLL, and a DLL manager unloads the old module and loads the new one, hot-swapping live script instances while preserving their state through JSON, all gated by an API-version check so a stale DLL can't crash the editor. Gameplay can be iterated on without ever restarting the engine.
Takeaways
Eruption was, by design, as much an architecture project as a graphics one. Building the RHI, the reflection layer, the asset and scene systems and the editor is where the real lessons were: how the parts of an engine fit together and where the seams belong, when to abstract and at what cost. It builds directly on the Vulkan rasterizer: that project demystified the GPU, and this one puts a living world on top of it.