Home / Architecture
Architecture
The codebase is organized around a central Core that orchestrates scene loading, plugin loading, rendering, the optional UI and the optional cluster mode. The design favors interfaces (testability, extensibility) and cleanly separates parsing, data ownership and rendering logic.
Layered view
Each responsibility lives in a dedicated module. The table below maps every module to its location in the sources and to its role.
| Module | Location | Role |
|---|---|---|
| Entry point | main.cpp, srcs/core/Core.* | Orchestrates the full lifecycle. |
| Scene load / save | srcs/core/scene/SceneParser.*, builder/*, factory/*, SceneRegister.* | JSON reading, typed construction, reverse serialization. |
| Scene data | srcs/core/scene/Scene.*, bvh/* | Camera, primitives, lights, materials, BVH acceleration structure. |
| Plugins | srcs/core/PluginLoader.*, srcs/common/IPlugin.hpp, IPluginLoader.hpp | Dynamic loading of .so libraries. |
| Renderers / UI | srcs/plugins/renderer/*, srcs/plugins/user_interface/* | Rendering and graphical interface plugins. |
| Geometry / lights | srcs/plugins/primitive/*, srcs/plugins/light/* | Concrete primitive and light implementations. |
| Cluster (optional) | srcs/core/cluster/*, srcs/common/cluster/* | Distributed client/server rendering. |
| Tools & utilities | srcs/core/obj/*, srcs/core/utils/*, srcs/core/exceptions/*, srcs/common/* | OBJ import, PNG export, file watching, exceptions, math. |
Dependency diagram
Overview of the main runtime dependencies:
main.cpp
│
▼
Core ───────────────► PluginLoader ───► plugins/*.so ──┬─► Renderer plugins (default, viewport)
│ └─► UI plugin (user_interface)
│
├─► SceneParser ──┬─► parseCamera
│ │ ├─► parseMaterials
│ │ └─► parseObjects ──► SceneObjectBuilder ──┬─► PrimitiveBuilder
│ │ └─► LightBuilder
│ ├─► ObjParser (.obj import)
│ └─► SceneBuilder ──► Scene ──► BVHNode / buildBvh()
│
├─► ISceneRenderer (active) ──► Scene
├─► RenderExporter ──► render.png
├─► SceneRegister ──► export .json
├─► FileObserver ──► hot reload of the scene file
└─► ClusterModule (optional) ──► server / client ──► PacketFactory
The Core
The Core (srcs/core/Core.*) implements ICoreAccess and coordinates
the entire lifecycle. In six main steps:
- Load plugins —
loadPlugins()scans./plugins, thenloadRenderers()andloadUserInterface()retrieve instances by type. - Parse & build — the
.jsonfile is parsed and the scene is built through the builders (loadScene()/loadNewScene()). - Select the active renderer — among
_defaultRenderer,_viewportRendererand the internal cluster renderer. - Build the BVH & render —
scene.buildBvh()thenrenderFrame()/startRendering(). - Export the image — via
RenderExporterto_renderOutputPath(defaultrender.png). - UI loop (optional) — the UI event loop runs and the
FileObservertriggers hot reloading of the scene file.
Undo / redo history via JSON snapshots. The Core keeps a
_history vector of snapshots (capped at HISTORY_LIMIT = 100) with a
_historyIndex. Every mutation calls historyCapture(), which serializes the
current state through snapshotScene(); historyUndo() / historyRedo()
rebuild the scene from the target snapshot with restoreSnapshot(). A signature
(historySignature()) avoids storing two identical consecutive snapshots.
During an undo/redo, the _suppressWatcherReload flag temporarily
neutralizes the FileObserver so that an internal write is not mistaken for an external
change to the file.
Plugin system
The rendering and interface modules are shared libraries
(.so) loaded at runtime from ./plugins.
At startup, PluginLoader::load() calls dlopen(path, RTLD_NOW) then resolves,
with dlsym, two mandatory symbols (a C ABI contract):
extern "C" IPlugin *create_plugin();
extern "C" void destroy_plugin(IPlugin *plugin);
If either is missing, the dlopen is closed (dlclose) and the plugin rejected.
Otherwise the created instance is stored in a PluginHandle { handle, instance, destroy } and
can be queried by PluginType — currently RENDERER or USER_INTERFACE.
On shutdown, unloadAll() destroys each instance then dlcloses its
.so (important to avoid leaks reported by AddressSanitizer).
| Plugin | Type | Role |
|---|---|---|
renderer_default.so | RENDERER | “Default” renderer — full image rendering. |
renderer_viewport.so | RENDERER | “Viewport” renderer — progressive rendering for the interface. |
user_interface.so | USER_INTERFACE | SFML graphical interface (viewport, panels, gizmos). |
Primitives and lights live in srcs/plugins/… but are currently
compiled into the main executable and instantiated via the builders/factories — they
are not (yet) loaded as dynamic .so plugins. The architecture nonetheless stays ready for
that evolution (the PluginType enum even reserves commented-out PRIMITIVE/LIGHT
entries).
Scene construction
Construction is deliberately separated from parsing, to keep parsing independent from concrete construction details:
scene.json
│
▼
SceneParser ──┬─► parseCamera
│ ├─► parseMaterials
│ └─► parseObjects ──► SceneObjectBuilder ──┬─► PrimitiveBuilder
│ └─► LightBuilder
▼
SceneBuilder ──► Scene (camera + materials + objects, dispatch by ObjectType)
SceneObjectBuilder— dispatcher:withType(string)enables primitive mode,withType(LightType)light mode; common setters (name, position…) are forwarded to the active internal builder, andbuild()returns anISceneObject*(IPrimitive*orILight*).PrimitiveBuilder— builds concrete primitives (sphere,plane,cube,cylinder,cone,triangle,torus,tanglecube,fractal) from the parsed fields (position,rotation,scale,radius,height,size,vertex0..2,power,iterations,material…).LightBuilder— buildsPointLightandDirectionalLight(name, position/direction, intensity, color).SceneBuilder— aggregates camera, materials and objects: createsScene(camera, ambient, diffuse), registers materials by name, then dispatches byObjectType(PRIMITIVE → addPrimitive,LIGHT → addLight).PrimitiveFactory/LightFactory— default-value constructors (createPrimitive(type),createLight(type)), useful for creating objects from the editor without going through parsing.
SceneRegister performs the reverse direction (save path): it
re-serializes the scene state to JSON (camera, environment coefficients, objects, materials), enabling
round-trip editing in the interface. The special case type = "object" (mesh) is imported via
ObjParser.
Rendering pipeline
An image is produced by applying the same sequence to every pixel:
- Camera ray — generate a primary ray
P(t) = O + t·Dfor pixel(i, j), with boundst ∈ [t_min, t_max]. - Accelerated intersection — test against the geometry, accelerated by the BVH built
before rendering via
scene.buildBvh()(global BVH + local per-mesh BVH). - Closest hit — keep the smallest valid
t(t, hit point, normal, material). - Shading — at the hit point
H = O + t·D: compute and orient the normalN, read the material parameters. - Lights & shadow rays — for each light, an offset shadow ray
(
H + ε·N) tests visibility; if occluded the contribution is skipped, otherwise diffuse and specular are evaluated. - Accumulation — ambient + visible direct contributions, clamped to the output range.
- PNG export — once all pixels are computed, the image buffer is written to disk (see Tools and utilities).
Numerical robustness: ε offset for secondary/shadow rays
(avoids self-intersection shadow acne), rejection of near-zero denominators in linear solves
(planes/triangles), consistent t ∈ [t_min, t_max] test, and normalization of vectors used in
shading dot products.
Tools and utilities
Around the pipeline sit several focused utilities (directories srcs/core/obj/,
srcs/core/utils/ and srcs/core/exceptions/):
ObjParser — .obj import
ObjParser (srcs/core/obj/ObjParser.hpp) reads a Wavefront .obj file
and feeds the scene object list. It parses vertices (v), normals (vn) and faces
(f) to produce ObjTriangle records (with smooth normal support),
applies a transform (withPosition, withRotation, withSize), and
exposes the raw triangles, vertices and face indices. The setEmitObjects(false) mode lets you
retrieve geometry without emitting scene objects. Any parse error throws
ObjParserException.
RenderExporter — PNG export
RenderExporter::saveToFile(render, path) (srcs/core/utils/RenderExporter.hpp)
encodes a Render (width, height, pixels) into a hand-written PNG: it emits
the PNG signature and the IHDR/IDAT/IEND chunks, uses zlib
“stored” compression (uncompressed blocks), and computes the CRC32 and
Adler32 checksums. The image is written as 8-bit truecolor RGB, with no dependency on an
external image library. A failure to open the file throws ExportRenderException.
FileObserver — hot reload
FileObserver (srcs/core/utils/FileObserver.hpp) watches the scene file by
remembering its last write time (std::filesystem::file_time_type). pollChanges()
returns true when the file changed since the last call; the Core polls it in its
loop to reload the scene automatically. setFilePath() and reset() let you
(re)target the watched file.
Exceptions
Domain errors are modeled by dedicated exceptions (srcs/core/exceptions/), all derived from
std::exception:
| Exception | Thrown for |
|---|---|
LoadingSceneException | Scene loading — with an ExceptionType (UNKNOWN_FILE, CANNOT_OPEN_FILE, INVALID_FILE_EXTENSION, WRONG_FILE_CONTENT, OTHER). |
BuildingObjectException | Failure to build a scene object in the builders. |
ExportRenderException | Failure to export the image (opening/writing the PNG). |
ObjParserException | Error parsing an .obj file (defined alongside ObjParser). |
ColorException | Invalid color value. |
VectorException | Invalid vector operation (math). |
Cluster module
The ClusterModule (srcs/core/cluster/*, interface
srcs/common/cluster/*) provides a distributed rendering infrastructure: a
server splits the image into tiles and hands them out to connected clients that return their pixels. It
exposes startServer(scene, port), joinCluster(address, port) and
leaveCluster(); rendering is coordinated by ClusterRenderCoordinator and
ClusterRenderer.
Communication goes through typed packets built by PacketFactory (identified
by PacketID), for example:
| Packet | Role |
|---|---|
ClientJoinRequest | A client requests to join the cluster. |
ServerSceneData | The server sends the scene to clients. |
ServerRenderRequest | The server requests the rendering of a tile. |
ClientTileData | A client returns a tile's pixels. |
ServerRenderState | The server broadcasts the rendering progress state. |
--server [PORT]launches the interface and starts the server (startServer).--connect IP PORTlaunches the interface and joins a server (joinCluster).