mjolnir/render/*)The Render module provides rendering subsystems including geometry rendering, lighting, shadows, transparency, particles, post-processing, and camera management. It is driven by a declarative frame graph that handles resource lifetime, barrier inference, and pass scheduling automatically.
The render system is organized as a set of sub-module renderers coordinated by a frame graph:
render/geometry — Opaque PBR
geometryrender/ambient — Ambient lighting
passrender/direct_light — Per-light
(point/spot/directional) direct lightingrender/shadow_render /
render/shadow_sphere_render — Shadow map
generationrender/shadow_culling /
render/shadow_sphere_culling — GPU shadow
cull per lightrender/occlusion_culling — GPU
occlusion cull per camerarender/depth_pyramid — Hierarchical
depth pyramid for occlusion queriesrender/transparent —
Transparent/blended objectsrender/particles_compute /
render/particles_render — GPU particle
simulation and renderingrender/post_process — Screen-space
post-processing effectsrender/ui — 2D overlay renderingThe frame graph is compiled from pass declarations before the first frame and recompiled whenever the scene topology changes (camera or light count changes).
Each pass runs at one of five scopes:
rg.PassScope :: enum {
GLOBAL, // Runs once per frame
PER_CAMERA, // Runs once per active camera
PER_POINT_LIGHT, // Runs once per active point light
PER_SPOT_LIGHT, // Runs once per active spot light
PER_DIRECTIONAL_LIGHT // Runs once per active directional light
}
Each render sub-module declares its resources and execute callback:
PassDecl :: struct {
name: string,
queue: QueueType, // .GRAPHICS or .COMPUTE
scope: PassScope,
declare_resources: PassSetupProc, // proc(setup: ^PassSetup, builder: ^PassBuilder)
execute: PassExecuteProc, // proc(ctx: rawptr, resources: ^PassResources, cmd: vk.CommandBuffer, fi: u32)
}
During declare_resources, passes declare what they read,
write, and create:
declare_resources :: proc(setup: ^rg.PassSetup, builder: ^rg.PassBuilder) {
// Create a graph-owned texture (auto-scoped to this pass's scope)
gbuffer_color := rg.create_texture(setup, builder, "gbuffer_color", rg.TextureDesc{
width = setup.camera_extents[setup.instance_idx].width,
height = setup.camera_extents[setup.instance_idx].height,
format = .R8G8B8A8_SRGB,
usage = {.COLOR_ATTACHMENT, .SAMPLED},
aspect = {.COLOR},
})
// Declare write access
rg.write_texture(builder, gbuffer_color)
// Register external resource (managed outside the graph)
depth := rg.register_external_texture(setup, builder, "depth", rg.TextureDesc{...})
// Declare read access on a resource from another scope
shadow_map := rg.read_texture_by_name(setup, builder, "shadow_map_spot_0")
}
Resources are auto-scoped by the graph based on their pass scope: -
"gbuffer_color" declared in a PER_CAMERA pass
at instance 0 → "gbuffer_color_cam_0" - Cross-scope reads
use explicit scoped names: "shadow_map_spot_0",
"gbuffer_cam_1"
At runtime, execute callbacks resolve resources by name:
execute :: proc(ctx: rawptr, resources: ^rg.PassResources, cmd: vk.CommandBuffer, fi: u32) {
// Resolve texture as VkImage for attachment
color_image := rg.get_texture_image(resources, "gbuffer_color", fi)
depth_image := rg.get_texture_image(resources, "depth", fi)
// Resolve buffer
vertex_buf := rg.get_buffer(resources, "vertex_buffer")
// Scope is embedded in resources — simple names auto-resolve
}
The engine drives the frame graph using a pass iterator:
iter := rg.make_pass_iterator(
&self.render.frame_graph,
self.frame_index,
graphics_cmd,
compute_cmd,
)
for {
pass := rg.next_pass(&iter) or_break
pass.execute(&self.render, &iter.resources, iter.cmd, self.frame_index)
rg.pass_done(&iter)
}
Before each frame, external resources (swapchain, depth buffer) must be updated:
rg.update_external_texture(&render.frame_graph, "swapchain", image, view)
rg.update_external_texture(&render.frame_graph, "depth", depth_image, depth_view)
import post_process "../../mjolnir/render/post_process"
// Add crosshatch effect
post_process.add_crosshatch(&engine.render.post_process, {800, 600})
Different material types control how objects are rendered:
// PBR material (default)
world.create_material(
&engine.world,
type = .PBR,
metallic_value = 0.8,
roughness_value = 0.2,
)
// Random color (debugging)
world.create_material(
&engine.world,
type = .RANDOM_COLOR,
)
// Line strip rendering
world.create_material(
&engine.world,
type = .LINE_STRIP,
base_color_factor = {1.0, 0.8, 0.0, 1.0},
)
// Enable shadow casting for lights
directional_light := world.create_directional_light_attachment(
{1.0, 1.0, 1.0, 1.0},
intensity = 10.0,
cast_shadow = true,
)
point_light := world.create_point_light_attachment(
{1.0, 0.8, 0.6, 1.0},
intensity = 100.0,
cast_shadow = true,
)
spot_light := world.create_spot_light_attachment(
{0.8, 0.9, 1.0, 1.0},
intensity = 50.0,
outer_cone_angle = math.PI * 0.25,
cast_shadow = true,
)
// Enable shadow casting for meshes
mesh_attachment := world.MeshAttachment{
handle = mesh,
material = material,
cast_shadow = true,
}
The 19 declared passes in execution order:
| Pass | Scope | Queue |
|---|---|---|
particles_compute |
GLOBAL | Compute |
depth_pyramid |
PER_CAMERA | Compute |
occlusion_culling |
PER_CAMERA | Compute |
shadow_culling_spot |
PER_SPOT_LIGHT | Compute |
shadow_culling_directional |
PER_DIRECTIONAL_LIGHT | Compute |
shadow_culling_sphere |
PER_POINT_LIGHT | Compute |
shadow_render_spot |
PER_SPOT_LIGHT | Graphics |
shadow_render_directional |
PER_DIRECTIONAL_LIGHT | Graphics |
shadow_render_sphere |
PER_POINT_LIGHT | Graphics |
geometry |
PER_CAMERA | Graphics |
ambient |
PER_CAMERA | Graphics |
direct_light_point |
PER_CAMERA | Graphics |
direct_light_spot |
PER_CAMERA | Graphics |
direct_light_directional |
PER_CAMERA | Graphics |
particles_render |
PER_CAMERA | Graphics |
transparent |
PER_CAMERA | Graphics |
post_process |
GLOBAL | Graphics |
ui |
GLOBAL | Graphics |
debug_ui |
GLOBAL | Graphics |
The frame graph is automatically rebuilt when scene topology changes:
// Trigger on next frame
engine.render.force_graph_rebuild = true
Rebuild happens automatically when a camera or light is added/removed.