Architecture Documentation - Refactoring Core¶
This document describes the architectural changes made during the app.c refactoring.
Overview¶
The monolithic app.c has been split into several specialized modules to improve maintainability, reduce compilation times, and clarify responsibilities.
Modules¶
| Module | Responsibility |
|---|---|
app.c / app.h |
Orchestrator: Initialization, main loop, high-level render pass management. |
app_ui.c / app_ui.h |
UI Rendering: Overlays, help screens, debug text, histograms, and loading spinners. |
app_input.c / app_input.h |
Input Handling: Keyboard callbacks, mouse/scroll events, and post-process feature toggles. Uses AppInputContext (focused pointer bundle) to decouple from the App God Object, following the same pattern as PostProcessInputContext. |
app_env.c / app_env.h |
Environment & IBL: HDR file scanning, asynchronous loading, and the IBL state machine. |
app_scene.c / app_scene.h |
Scene Rendering: Billboard groups, instanced groups, and procedural geometry updates. |
Architecture Diagram¶
Data Ownership¶
The App struct (defined in app.h) remains the central state container. Most modules take a pointer to App as their first argument.
To avoid cyclic dependencies:
- Core struct definitions (App, Camera, AsyncRequest, Shader, NBodySim, SphereInstance) use named structs instead of anonymous ones to support forward declarations.
- Module headers are included at the end of app.h to ensure they can see the full App definition if necessary (though they primarily use pointers).
- Specialized source files (.c) include app.h and the required renderer headers directly.
Scene Decomposition¶
The Scene struct is fully decomposed into six domain-aligned sub-structs, each in its own header:
SceneGPUResources(include/scene_gpu_resources.h): All GPU resource handles — 28 GLuint handles for textures, buffers, VAOs, compute programs, plus billboard UBO and IBL/SH binding caches. Access viascene->gpu.hdr_texture,scene->gpu.icosphere_vbo, etc.SceneShaders(include/scene_shaders.h): All shader pointers —pbr_instanced,pbr_billboard,debug,debug_line,skybox(+ conditionalpbr_ssbo). Access viascene->shaders.pbr_instanced, etc.SceneConfig(include/scene_config.h): Runtime configuration —wireframe,billboard_mode,sorting_mode,pbr_debug_mode,show_envmap,env_lod,subdivisions,gi_mode,show_probe_grid,specular_aa_enabled,aa_mode. Also definesSortingMode,GIMode,AAModeenums. Access viascene->config.wireframe, etc.SceneVisuals(include/scene_visuals.h): Visual effects —Skybox,TrailRenderer,ShockwaveRenderer. Access viascene->visuals.skybox, etc.SceneSimulation(include/scene_simulation.h): N-body state —NBodySim,nbody_mode. Access viascene->simulation.nbody_sim, etc.SceneLighting(include/scene_lighting.h): IBL, probes, and materials —IBLCoordinator,LightProbeGrid,MaterialLib*. Access viascene->lighting.ibl_coord, etc.
This reduces Scene's direct field count from ~50 to ~19 and moves domain-specific type definitions out of the monolithic scene.h.
App Decomposition (AppProfiling, AppInput, AppWindow)¶
The App struct is decomposed into domain-aligned sub-structs:
AppProfiling(include/app_profiling.h): Groups profiling and metrics —GPUProfiler,GPUProfilerUI,FpsCounter,TracyManager,GPUUsageMonitor,PerfModeContext,perf_mode_active,log_gpu_metrics. Access viaapp->profiling->gpu_profiler, etc. Init/cleanup delegated toapp_profiling_init()/app_profiling_cleanup()insrc/app_profiling.c.AppInput(include/app_input_state.h): Groups camera, gamepad, key-bindings, and input smoothing —Camera,GamepadState,AppBindingRegistry,AdaptiveSampler,camera_enabled. Access viaapp->input->camera, etc. Init/cleanup delegated toapp_input_state_init()/app_input_state_cleanup()insrc/app_input_state.c.AppWindow(include/app_window.h): Groups GLFW window handle and all window/resize state —GLFWwindow* handle,is_fullscreen,saved_x/y,saved_width/height,resize_pending,pending_width/height. Access viaapp->win.handle,app->win.is_fullscreen, etc.
Naming note:
app_input_state.hhosts theAppInputsub-struct definition, while the existingapp_input.hhosts theAppInputContextseam (focused pointer bundle for input handlers, issue #204).
This reduces App's direct field count from 24 to ~17. Each sub-struct header owns its type dependencies and its delegation functions, keeping app.c focused on orchestration.
PostProcess Header Decomposition¶
The PostProcess struct's type definitions are decomposed into five sub-headers, following the same pattern as the Scene decomposition. postprocess.h (648 → 330 lines) now only contains the aggregate PostProcess struct, PostProcessEffect enum, PostProcessPreset, and function signatures.
pp_params.h: All 10 uber-shader effect parameter structs (VignetteParams,GrainParams,ExposureParams,ChromAbberationParams,WhiteBalanceParams,ColorGradingParams,TonemapParams,FXAAParams,BandingParams,FogParams) plus theBandingModeenum andDEFAULT_*values. Effects with their own multi-pass pipeline (BloomParams,DoFParams,AutoExposureParams,MotionBlurParams,LUT3DParams) live in their respectivefx_*.hheaders.pp_ubo.h: GPU-sidePostProcessUBOlayout (std140) — matches the GLSL uber-shader UBO.pp_gpu_resources.h:PPGPUResourcesstruct — FBOs, textures, UBO handle, screen quad.pp_shader_state.h:PPShaderState+ShaderCacheEntry— shader compilation and caching state.pp_exposure_readback.h:PPExposureReadbackstruct — async PBO readback for auto-exposure and histogram.
PostProcess Translation-Unit Split¶
The monolithic postprocess.c (1634 lines) is split into six domain-focused translation units, each under 360 lines:
| TU | Lines | Responsibility |
|---|---|---|
postprocess_init.c |
358 | Initialization, framebuffer creation, resize, dummy textures |
postprocess_apply.c |
360 | Begin/end render pass, UBO sync, bloom/DoF/AE/MB/composite passes |
postprocess_setters.c |
268 | Enable/disable/toggle, parameter setters, preset application |
postprocess_shader.c |
223 | Shader cache lookup, optimized compile, dynamic switching |
postprocess_readback.c |
199 | PBO readback, histogram computation, luminance, matrix updates |
postprocess_cleanup.c |
97 | Resource teardown (FBOs, quad, readback buffers, shader cache) |
graph TD
PP_H[postprocess.h<br/><i>public API</i>]
PP_INT[postprocess_internal.h<br/><i>shared enums + pp_ decls</i>]
PP_INT --> PP_H
INIT[postprocess_init.c] --> PP_INT
APPLY[postprocess_apply.c] --> PP_INT
SHADER[postprocess_shader.c] --> PP_INT
CLEANUP[postprocess_cleanup.c] --> PP_INT
SETTERS[postprocess_setters.c] --> PP_H
READBACK[postprocess_readback.c] --> PP_H
Internal header postprocess_internal.h provides:
- Texture unit enums (
POSTPROCESS_TEX_UNIT_SCENE,_BLOOM,_DOF, etc.) pp_-prefixed internal function declarations shared across TUs (e.g.pp_create_framebuffer,pp_destroy_framebuffer,pp_setup_sampler_uniforms,pp_update_current_shader,pp_is_shader_in_cache)- Only included by TUs that need shared internal state;
postprocess_setters.candpostprocess_readback.conly need the publicpostprocess.h
Scene Uniform Cache Extraction¶
Shader uniform cache structs (InstancedUniforms, DebugUniforms, BillboardUBO, BillboardUniforms, MAT4_FLOAT_COUNT) are extracted from scene.h (194 → 120 lines) into scene_uniforms.h. These are GPU implementation details only accessed by scene_render.c and scene_init.c. The Scene struct retains its by-value uniform fields; the type definitions simply move to a focused sub-header.
UI Layout Data Privatization¶
Static layout data (66-entry KEY_LAYOUT_QWERTY[], 16-entry GAMEPAD_LAYOUT[], ~60 visual constants, KeyPos/GamepadControlPos struct definitions) is moved from app_ui.h (387 → 128 lines) into app_ui.c. The public header now exposes only HelpMode, AppUIOverlay, and 8 function signatures. This eliminates duplicated static const arrays across translation units and makes layout changes recompile-local.
GamepadContext Seam¶
The GamepadContext (include/gamepad_context.h) decouples gamepad_input.c from camera.h:
- Contains only the minimal camera state slice needed for gamepad input:
move_input[3],yaw_target,pitch_target,fixed_timestep, pitch limits. gamepad_write_input()takesGamepadContext*instead ofCamera*.- Bridge pattern:
Camera ↔ GamepadContextat the call site inapp.c.
Effect Decoupling (EffectContext)¶
Post-processing effects (bloom, DoF, auto-exposure, motion blur, LUT, LUT viz) are fully decoupled from the PostProcess God Object via an EffectContext seam:
EffectContext(include/effects/effect_context.h): Read-only snapshot of shared pipeline state (source texture, viewport dimensions, depth/velocity textures, exposure).- Effects receive
(FX*, Params*, const EffectContext*)instead ofPostProcess*. - This eliminates the bidirectional dependency:
postprocess.h→fx_*.h(for struct embedding) remains, butfx_*.c→postprocess.his removed. - All effects are now fully migrated: bloom, DoF, auto-exposure, motion blur, LUT 3D, and LUT viz. No effect source file includes
postprocess.hanymore.
Header Dependency Cleanup (env_manager.h, renderer.h)¶
Two module headers carried unnecessary transitive includes, increasing coupling and recompilation cost:
env_manager.h: Previously includedpostprocess.heven though it only usesPostProcess*in function signatures. Replaced with a forward declarationtypedef struct PostProcess PostProcess;. The concrete include stays inenv_manager.cwhich callspostprocess_set_exposure_target().renderer.h: Previously included 9 project headers (action_notifier.h,camera.h,effect_benchmark.h,env_manager.h,gpu_profiler.h,gpu_profiler_ui.h,postprocess.h,scene.h,ui.h) even thoughRenderContextholds only pointers. All replaced with forward declarations. Only<stdbool.h>and<stdint.h>remain as concrete includes. The.cfile includes the full headers it needs.
Extended Forward Declarations (Phase 10)¶
Systematic pass replacing #include "shader.h" (and other pointer-only includes) with typedef struct X X; across 13 additional headers. The Shader, NBodySim, and SphereInstance structs received explicit struct tags (previously anonymous) to enable forward-declaration.
| Header | Forward-declared types | Notes |
|---|---|---|
effect_benchmark.h |
PostProcess*, GPUProfiler* |
Removes 2 heavy includes |
trail_renderer.h |
Shader* |
Keeps nbody.h for NBODY_MAX_BODIES constant |
light_probes.h |
Shader*, SphereInstance* |
Removes 2 includes |
skybox.h |
Shader* |
|
shockwave.h |
Shader* |
|
ui.h |
Shader* |
Adds gl_common.h for GLuint |
pp_shader_state.h |
Shader* |
|
fx_bloom.h |
Shader* |
|
fx_dof.h |
Shader* |
|
fx_lut3d.h |
Shader* |
|
fx_lut_viz.h |
Shader* |
Adds <stdbool.h> for bool |
fx_auto_exposure.h |
Shader* |
|
fx_motion_blur.h |
Shader* |
Result: 24 headers now use forward declarations (was 6 before this pass). Each corresponding .c file adds the full #include it needs for the concrete type definition.
Include Fan-Out Reduction (app_input.c, app_ui.c)¶
The two highest fan-out source files were trimmed by moving the app_input_ctx_from_app() bridge function to app.c and removing unused/transitively-redundant includes:
app_input.c: 25 → 17 includes. The bridge function was the sole reason for includingapp.h,app_profiling.h, andapp_input_state.h. Moving it toapp.c(which already includes all three) eliminates the coupling. Additional removals:window.h(never used),action_notifier.h,app_settings.h,nbody.h,glad/glad.h,GLFW/glfw3.h(all transitively available through remaining includes).app_ui.c: 23 → 16 includes. Removedstb_image.h(zerostbi_*calls),<stdio.h>and<stdlib.h>(no direct usage), plus 4 transitively-redundant headers (action_notifier.h,adaptive_sampler.h,app_binding.h,app_settings.h).
Principle: A header that only uses a type through a pointer or reference should forward-declare that type, not include its full definition. This reduces include fan-out and speeds up incremental builds.
Build System¶
The CMakeLists.txt has been updated to include the new source files. The app executable and test_app integration test both link against the new modular structure.
Performance Impact¶
- Compilation: Parallel compilation is now more effective as changes to UI don't require re-compiling the IBL logic.
- Runtime: Zero overhead, as functions are simply moved into separate translation units. Inlining is still possible for performance-critical functions if they were moved to headers (though not currently required).
Metrics & Health¶
Last updated: April 2026 (Phase 10 — Architecture Deepening V)
Codebase Size¶
| Category | Files | Total LOC |
|---|---|---|
Sources (src/*.c) |
66 | 20 556 |
Headers (include/*.h) |
87 | 7 839 |
Tests (tests/test_*.c) |
69 | 12 666 |
Shaders (shaders/) |
60 | — |
LOC per Module (top 15)¶
| Module | LOC | Notes |
|---|---|---|
postprocess.c |
1 634 | Legacy monolith — split into 6 TUs (see PostProcess TU Split) |
app_ui.c |
1 317 | UI rendering + layout data (privatized) |
app_input.c |
925 | Keyboard/mouse input dispatch |
ui.c |
879 | Dear ImGui integration |
shader.c |
870 | Shader compilation & linking |
light_probes.c |
752 | Light probe grid |
postprocess_presets.c |
525 | Preset definitions |
scene_render.c |
494 | Scene draw calls |
postprocess_input.c |
486 | Post-process keyboard controls |
nbody.c |
486 | N-body simulation (CPU + compute) |
scene_init.c |
476 | Scene resource creation |
app.c |
474 | Orchestrator (main loop) |
billboard_sorting.c |
472 | Billboard depth sort |
ibl_coordinator.c |
443 | IBL state machine |
async_loader.c |
422 | Async HDR loading |
Guideline: modules > 500 LOC are candidates for further decomposition.
Header Include Fan-Out¶
| Header | #includes | Status |
|---|---|---|
scene.h |
14 | Aggregate — expected |
app.h |
13 | Reduced from 22 (Phase 3) |
postprocess.h |
11 | Aggregate — expected |
app_profiling.h |
7 | Sub-struct — acceptable |
gpu_profiler.h |
7 | Domain header |
utils.h |
7 | Utility grab-bag |
renderer.h |
0 | Forward-decl only ✅ |
effect_benchmark.h |
1 | Was 3 — forward-decl ✅ |
light_probes.h |
2 | Was 4 — forward-decl ✅ |
fx_*.h (effects) |
0–1 | All use forward-decl for Shader* ✅ |
Principle: aggregate headers (app.h, scene.h, postprocess.h) naturally have high fan-out. Leaf module headers should stay ≤ 5. 24 headers now use forward declarations.
Test Coverage (LLVM-Cov, April 2026)¶
| Metric | Value | Target |
|---|---|---|
| Lines | 66.6% | ≥ 78% |
| Functions | 83.1% | ≥ 85% |
| Branches | 53.6% | ≥ 50% ✅ |
68 tests pass (unit + integration, CTest).
Include-Dependency Graph¶
Top-level module dependencies (project headers only, excludes system and vendor headers):
graph TD
classDef aggregate fill:#1a1b26,stroke:#e0af68,color:#e0af68,stroke-width:2
classDef substruct fill:#1a1b26,stroke:#7aa2f7,color:#7aa2f7
classDef leaf fill:#1a1b26,stroke:#9ece6a,color:#9ece6a
classDef effect fill:#1a1b26,stroke:#bb9af7,color:#bb9af7
APP[app.h]:::aggregate
SCENE[scene.h]:::aggregate
PP[postprocess.h]:::aggregate
REND[renderer.h]:::leaf
%% App sub-structs
APP_PROF[app_profiling.h]:::substruct
APP_INP[app_input_state.h]:::substruct
APP_WIN[app_window.h]:::substruct
APP_UI[app_ui.h]:::substruct
%% Scene sub-structs
SC_GPU[scene_gpu_resources.h]:::substruct
SC_SH[scene_shaders.h]:::substruct
SC_CFG[scene_config.h]:::substruct
SC_VIS[scene_visuals.h]:::substruct
SC_SIM[scene_simulation.h]:::substruct
SC_LIT[scene_lighting.h]:::substruct
%% PostProcess sub-headers
PP_PAR[pp_params.h]:::substruct
PP_GPU[pp_gpu_resources.h]:::substruct
PP_SHD[pp_shader_state.h]:::substruct
PP_RDB[pp_exposure_readback.h]:::substruct
%% Effects
FX_BL[fx_bloom.h]:::effect
FX_DOF[fx_dof.h]:::effect
FX_AE[fx_auto_exposure.h]:::effect
FX_MB[fx_motion_blur.h]:::effect
FX_LUT[fx_lut3d.h]:::effect
EC[effect_context.h]:::effect
%% App → direct deps
APP --> SCENE
APP --> PP
APP --> APP_UI
APP --> APP_WIN
%% App sub-structs (owned, not included by app.h)
APP -.->|"owned"| APP_PROF
APP -.->|"owned"| APP_INP
%% Scene → sub-structs
SCENE --> SC_GPU
SCENE --> SC_SH
SCENE --> SC_CFG
SCENE --> SC_VIS
SCENE --> SC_SIM
SCENE --> SC_LIT
%% PostProcess → sub-headers + effects
PP --> PP_PAR
PP --> PP_GPU
PP --> PP_SHD
PP --> PP_RDB
PP --> FX_BL
PP --> FX_DOF
PP --> FX_AE
PP --> FX_MB
PP --> FX_LUT
%% Effect decoupling
FX_BL -.->|"runtime"| EC
FX_DOF -.->|"runtime"| EC
FX_AE -.->|"runtime"| EC
FX_MB -.->|"runtime"| EC
FX_LUT -.->|"runtime"| EC
%% Renderer uses forward-decls only
REND -.->|"fwd-decl"| APP
REND -.->|"fwd-decl"| SCENE
REND -.->|"fwd-decl"| PP
Legend: Solid arrows = #include dependency. Dashed arrows = forward declaration or runtime-only dependency. Colors: 🟡 aggregate, 🔵 sub-struct, 🟢 leaf, 🟣 effect.