Grund3D

A C++20 Vulkan/SDL3 engine built around gameplay hot reload, and the cozy Finnish-forest survival game it runs. Playable on itch.io.

Highlights

  • Runtime host with a session-stable engine module and a disposable, hot-reloaded game module over a versioned C ABI
  • Vulkan 1.3 renderer: physically based sky, ray-traced shadows/reflections/GI when supported, FFT ocean, GTAO, bloom
  • Seeded terrain streamed in LOD'd chunks across a job pool, with rivers, lakes, and scattered instanced props
  • Reproducible Linux + Windows release builds from one Docker image, published to itch.io with butler

Overview #

Grund is a runtime-oriented C++/Vulkan/SDL3 game engine, and grund3d is the game it is being built for: a cozy survival game set in a late-autumn Finnish forest. You fell trees, buck them into blocks, split those into klapi (firewood), and build a nuotio (campfire). The long-term goal is a sauna you heated yourself.

The game is currently a playable pre-alpha (v0.12.0) for Linux and Windows on itch.io.

Aerial view over grund3d at noon: a dense autumn spruce and birch forest running down to a large turquoise lake with a sandy beach, more lakes and a river winding into the hazy distance, and a small red cabin among the trees.
The seeded island at noon: streamed forest, lakes, rivers and atmospheric haze all the way to the horizon.

Grund is the successor to my earlier 2D C++ engine. It keeps the idea that worked there, a runtime host that hot-reloads the gameplay code, and rebuilds everything else from scratch around a 3D renderer and a streamed world.

Architecture: runtime, engine, game #

The engine is deliberately not an editor project. The workflow is hot reload: change gameplay code, run make game, and keep playing. Everything is split into three modules plus shared code:

flowchart TD
  Runtime["<b>runtime</b> (executable)<br/>owns the process and main loop<br/>loads both modules<br/>crash handling"]
  Engine["<b>engine.so</b><br/>stable for the whole session<br/>window, Vulkan, input, audio<br/>assets, physics, jobs, memory"]
  Game["<b>game.so</b><br/>disposable, reloaded on rebuild<br/>fauna, fishing, firewood<br/>weather, saves, HUD"]
  Shared["<b>shared</b><br/>math, collision, mesh<br/>worldgen, water, pak, save"]

  Runtime -->|"load once"| Engine
  Runtime -->|"load from a copy,<br/>reload on rebuild"| Game
  Game -->|"versioned C ABI"| Engine
  Engine --- Shared
  Game --- Shared

The important difference from my previous engine is who owns the state. There, the game module had to serialize itself before a reload and restore itself afterwards. In Grund, the engine owns the persistent game state in arena-allocated memory domains, so a reload just swaps the code. The camera, the captured mouse, and the whole world carry across without a save/restore step.

A few details make that robust in practice:

  • The runtime loads game.so from a copy, so a rebuild in progress can never corrupt the loaded module.
  • The modules talk through a versioned C ABI. A mismatched module is refused instead of crashing.
  • Crashes are caught on both POSIX and Win32 and written out with symbols, so pre-alpha bug reports are actually useful.

Renderer #

The renderer is Vulkan 1.3 (through volk and VMA) with reverse-Z depth. Most of the frame is compute:

  • Sky and atmosphere: a physically based sky with transmittance, multiple-scattering and sky-view passes, plus aerial perspective over distant terrain.
  • Ray tracing: shadows, reflections and bounce lighting are ray traced and turned on automatically when the GPU supports it.
  • Water: an FFT ocean, caustics, and Snell’s window when you are underwater.
  • Post and lighting: GTAO ambient occlusion, bloom, and histogram-based auto-exposure.
  • Vegetation and weather: GPU-culled grass, foliage with impostors in the distance, particles, rain and snow.

Terrain is drawn with indirect draws, and quality presets scale the render resolution. On Wayland the game runs in native 4K fullscreen.

A bay at sunset in grund3d: dark autumn forest along a sandy shoreline, still turquoise water reflecting the trees, and a pink and violet sky over the sea.
Sunset over the bay, from the physically based sky down to the reflections in the water.
Low evening sun in a grund3d forest: tall spruces with long shadows across golden grass, a birch trunk in the foreground and warm haze between the trees.
Golden hour in the forest.
A grund3d forest in heavy rain: spruce and birch fading into thick grey fog, with rain streaks in front of the trees.
Rain and fog.
Aerial view at dusk over a red-lit spruce forest with a small village, a road with street lamps, and a lake on the right.
Dusk over the village and the lake.
A tall lattice radio tower on a hill at dawn, with birds, scattered pines and a low sun glowing through haze.
A radio tower at dawn.

A streamed, seeded world #

The world is generated from a seed. Terrain is streamed in LOD’d chunks around the camera and built across a fixed pool of eight job workers, and the chunk meshes are sub-allocated through VMA. The same jobs scatter trees, bushes and stones from the world seed and draw them instanced. Rivers run from lakes and springs down to the sea through carved channels, and nothing grows below the waterline.

Every scattered prop has an identity. When you fell a tree, the stump stays through streaming, in the ray-traced shadows, and in collision. The player is a Jolt Physics character behind the engine’s own PhysicsAPI, so you can walk, run, jump, crouch and swim.

A sunny forest slope in grund3d covered in dense grass, with a leaning birch, a spruce, and a freshly cut tree stump in the foreground.
A felled tree leaves a stump that persists through streaming, shadows and collision.

All of the art, audio, models and terrain inputs are produced by scripts under tools/. The asset registry is path-keyed and reference-counted, and textures, meshes and shaders all reload live, the same way the gameplay code does.

A red Finnish log cabin with white window frames in a sunlit grassy clearing in grund3d, with a mossy boulder, tall grass, and a small grey shed among the trees to the right.
A mökki in the clearing. The long-term goal is a sauna next to it.

Nights, villages and the map #

A lamp-lit road at night between spruces, with a few houses beside it and a full moon in the sky.
A lamp-lit village road under the moon.
A fenced electrical substation at night under a starry sky, with power lines, spruces and glowing orange lamps.
A substation and power lines under the stars.
Drone camera view looking down at three houses beside a lit road in a dark forest, with a recording overlay showing speed, altitude and battery.
The drone camera, with its recording overlay.
Night view over the forest with a red beacon on a distant hill and a row of warm lights beside a lake.
Red mast beacons and village lights from above.
Starry night over the forest with a bright red tower light and three yellow lights near a river and lake.
Towers and lights across the island at midnight.
The in-game map, Kartta: a contour map of the island with lakes and rivers, the player marker and sight lines.
Kartta, the in-game map of the island.

Shipping #

The releases are reproducible. One Docker image builds both platforms: Linux (glibc 2.36+, X11 and Wayland) natively and Windows through llvm-mingw, and the Windows tests run under Wine in the same container. make release builds, tests and packages the game together with its crash symbols, and make publish-itch uploads it with butler. The changelog is generated from conventional commits, and pre-commit (gitleaks, cppcheck, shellcheck, hadolint) also runs in Docker.

Each feature starts as a written plan and ends with a result doc describing what actually happened, including divergences from the plan and the bugs worth remembering. There are about a hundred of these in the repo by now, and they have turned out to be the most useful documentation the project has.