Gamegine architecture
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.
| # | Package | Prefix | Holds |
|---|---|---|---|
| 1 | rhi/ | rhi_ | Pure abstract protocols. No Vulkan symbol. |
| 2 | rhi/vlk/ | vlk_ | The Vulkan implementation of every protocol. |
| 3 | core/ | core_ | Engine, Window, input, timing, Vertex. |
| 4 | rnd/ | rnd_ / bare | Renderer, Mesh, Texture, Material. |
| 5 | scn/ | scn_ | ECS over EnTT. Components, systems. |
| 6 | asset/ | asset_ | AssetManager, importers producing ModelData. |
| 7 | ui/ | 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:
| Owner | Owns |
|---|---|
Renderer | swapchain, camera UBO, both resource layouts, every pipeline |
AssetManager | textures, materials, resource sets, loaded models |
Material | its own factors UBO — and nothing else |
Scene | the 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:
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.