Rether / docs
engine/gamegine/Gamegine architecture
gardenThe layers, what each owns, and which way the dependencies point

Gamegine architecture

updated 2026.08.10created 2026.08.10~3 min

The present state of the engine, in the present tense. This note is rewritten in place when the code changes — it is never appended to. What I did on a given day lives in the log notes instead.

Layers

The engine is a static library eng, consumed by an application in game/. Lower layers never depend on higher ones. That single rule decides most arguments about where a thing belongs.

#PackagePrefixHolds
1rhi/rhi_Pure abstract protocols. No Vulkan symbol.
2rhi/vlk/vlk_The Vulkan implementation of every protocol.
3core/core_Engine, Window, input, timing, Vertex.
4rnd/rnd_ / bareRenderer, Mesh, Texture, Material.
5scn/scn_ECS over EnTT. Components, systems.
6asset/asset_AssetManager, importers producing ModelData.
7ui/ui_Dear ImGui integration.

rnd uses RHI protocols and never touches Vulkan directly. rhi_factory picks the backend at construction time.

The consequence people trip on

asset sits above rnd, so a type needed by both is declared in rnd and translated on the way up. AlphaMode exists twice for that reason — eng::asset::AlphaMode comes out of the importer, eng::rnd::AlphaMode is what a Material holds, and AssetManager converts between them. Sharing one enum would mean rnd including an asset header, which inverts the order.

The duplication is the cheaper of the two costs. It is not an oversight.

Ownership

Nothing in the renderer owns what it draws. The split:

OwnerOwns
Rendererswapchain, camera UBO, both resource layouts, every pipeline
AssetManagertextures, materials, resource sets, loaded models
Materialits own factors UBO — and nothing else
Scenethe entt::registry

Material holds raw non-owning pointers to its pipeline, its resource set and its five textures. All of them are owned by AssetManager or Renderer and must outlive it. The one exception is the uniform buffer holding its factors, which is 1:1 with the material and has no reason to be shared — hence the unique_ptr and the constructor taking a context.

Resource sets

Two descriptor sets, and the whole contract with the shader is in Shader contract.

Set 0, the global set. One binding, the camera uniform buffer, visible from both stages: the vertex stage reads the view projection, the fragment stage the camera position it needs for the view vector. That is what ShaderStage::VertexFragment exists for.

Set 1, the material set. One binding per slot of rnd::TextureSlot, then the factors buffer. Every slot is bound on every material — a neutral 1×1 image stands in for a texture a material declares none for, so no descriptor of the set is ever left unwritten.

The descriptor pool sizes its sampler budget as maxSets × 8. A flat 1000 would have capped the engine at 200 materials rather than 1000, silently, until an allocation failed.

Pipelines

Materials do not each get a pipeline. Four cover every state a glTF material can ask for:

// code
PBR_Opaque              PBR_Opaque_DoubleSided
PBR_Blend               PBR_Blend_DoubleSided

Opaque and masked materials share one, because masking is a discard in the fragment shader and no pipeline state expresses it. Renderer::materialPipeline(mode, doubleSided) resolves the right one; the name is built internally so a typo cannot reach a call site.

Wireframe and UI also exist and are currently looked up by nobody.

Known warts

rnd::Texture decodes file formats. stb_image is included straight into texture.cpp, so the class both decodes PNG/JPEG and manages GPU residency. Two jobs. It holds for PNG and breaks for KTX2, which carries its own mip chain and its own format and does not decode to RGBA8 at all. The fix is a TextureData POD produced by decoders in asset/, leaving rnd::Texture knowing nothing about file formats.

createTexture generates the mip chain. Its signature takes a single pixel pointer, which no compressed format can satisfy.

core::Vertex carries no tangent. Normal mapping currently rebuilds the tangent frame from screen space derivatives. It works and costs more per fragment than reading an attribute would.

Transparent draws are not sorted. Blended materials are drawn in ECS iteration order, with no back-to-front pass.

Keyboard

search this vaultK / /
step the sidebarjk
open selected
switch browse mode123
switch vaultgv
toggle themet
close / clearesc
this help?
esc to close