> Cinder 0.1.0 documentation: cinder.render API
> Canonical page: https://cinder.ash10.dev/docs/api/render/ · Source: `docs/api/render.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt

# `cinder.render`


## `cinder.render`

Source: `cinder/render/__init__.mojo`

Renderer: cameras, meshes, materials, textures, the frame graph and the forward+ pipeline. Everything here talks to the GPU only through `cinder.gfx`.


## `cinder.render.camera`

Source: `cinder/render/camera.mojo`

Cameras and frustum culling. Right-handed, Y up, camera looks down -Z. Projection is reverse-Z with an infinite far plane, so the frustum has five planes. `cull_spheres` tests four spheres at a time with SIMD.

- `comptime F4 = SIMD[DType.float32, 4]` — Four float32 lanes: one coordinate of four spheres in `Frustum.cull_spheres_into`.
### struct `Plane`

```mojo
struct Plane(ImplicitlyCopyable, RegisterPassable)
```

`normal . p + d >= 0` is the inside half-space.

Fields:

- `normal`: Vec3
- `d`: Float32

- **`from_row`** `from_row(v: Vec4) -> Plane`  
  Plane from a clip-space row combination, scaled so `normal` has unit length.
- **`distance`** `distance(self, p: Vec3) -> Float32`  
  Signed distance from `p` to the plane (positive inside), in the units of `p`.

### struct `Frustum`

```mojo
struct Frustum(Copyable, Movable)
```

Left, right, bottom, top, near planes extracted from a view-projection matrix (Gribb/Hartmann). Reverse-Z with infinite far: the near plane is `row3 - row2` (clip z <= w); no far plane.

Fields:

- `planes`: Array[Plane, 5]

- **`__init__`** `__init__(out self, view_proj: Mat4)`
- **`contains_sphere`** `contains_sphere(self, centre: Vec3, radius: Float32) -> Bool`  
  True unless the sphere lies entirely behind one of the five planes. Conservative: a sphere near a frustum edge or corner may pass without intersecting it.
- **`contains_point`** `contains_point(self, p: Vec3) -> Bool`  
  True when `p` is on the inside of all five planes (near, left, right, bottom, top).
- **`cull_spheres_into`** `cull_spheres_into(self, spheres: List[Vec4], mut out: List[Bool])`  
  Visibility per sphere (xyz centre, w radius), four spheres per SIMD step, written into a list supplied by the caller (cleared first, capacity kept).
- **`cull_spheres`** `cull_spheres(self, spheres: List[Vec4]) -> List[Bool]`  
  Visibility per sphere (xyz centre, w radius); allocates the result list. Use `cull_spheres_into` in per-frame code to reuse a scratch list.

### struct `Camera`

```mojo
struct Camera(ImplicitlyCopyable, Movable)
```

A perspective camera. `near` is the only clip distance (infinite far).

Fields:

- `position`: Vec3
- `rotation`: Quat
- `fov_y`: Float32
- `aspect`: Float32
- `near`: Float32

- **`__init__`** `__init__( out self, position: Vec3, rotation: Quat, fov_y: Float32 = 1.0471976, aspect: Float32 = 16.0 / 9.0, near: Float32 = 0.05, )`
- **`look_at`** `look_at( eye: Vec3, target: Vec3, up: Vec3 = Vec3.unit_y(), fov_y: Float32 = 1.0471976, aspect: Float32 = 16.0 / 9.0, near: Float32 = 0.05, ) -> Camera`  
  A camera at `eye` looking at `target`, with its rotation taken from `Mat4.look_at`. `up` only orients the camera (the stored `up()` is re-orthogonalised); `fov_y` is the vertical field of view in radians, `near` the near clip distance in metres.
- **`forward`** `forward(self) -> Vec3`  
  World-space view direction: `Vec3.forward()` (-Z) rotated by `rotation`, unit length.
- **`right`** `right(self) -> Vec3`  
  World-space right axis: +X rotated by `rotation`.
- **`up`** `up(self) -> Vec3`  
  World-space up axis: +Y rotated by `rotation`.
- **`view`** `view(self) -> Mat4`  
  World-to-view matrix (right-handed, looking down -Z), column-major.
- **`projection`** `projection(self) -> Mat4`  
  Reverse-Z perspective with an infinite far plane: depth 1 at `near`, 0 at infinity.
- **`view_projection`** `view_projection(self) -> Mat4`  
  `projection() * view()`: world space to clip space.
- **`frustum`** `frustum(self) -> Frustum`  
  The five-plane culling frustum of `view_projection()` (no heap allocation).
- **`set_viewport`** `set_viewport(mut self, width: Int, height: Int)`  
  Sets `aspect` to `width / height` in pixels; ignored when `height` is not positive.


## `cinder.render.frame_graph`

Source: `cinder/render/frame_graph.mojo`

Frame graph: declared passes, transient textures with lifetimes and aliasing, automatic ordering. The graph is rebuilt every frame (cheap: a few passes). `compile` orders passes by their read/write dependencies, culls passes that contribute nothing to an exported resource, computes each transient's lifetime as a pass-index interval and assigns physical textures so that transients with compatible descriptions and disjoint lifetimes share one allocation. Physical textures persist in a pool across frames and are dropped when their description stops being requested (resize). The renderer executes the compiled order itself (`order`) and looks up each transient's view with `view_of`; the graph never calls back into user code (no closures cross the GPU layer). `to_graphviz` renders the compiled graph for the debug UI.

### struct `TransientDesc`

```mojo
struct TransientDesc(Equatable, ImplicitlyCopyable, Movable)
```

Size in pixels, format, `WGPUTextureUsage` bits, sample and mip count of a transient. Transients share one pooled physical texture only when their descriptions are equal.

Fields:

- `width`: UInt32
- `height`: UInt32
- `format`: WGPUTextureFormat
- `usage`: UInt64
- `sample_count`: UInt32
- `mip_levels`: UInt32


### struct `FrameGraph`

```mojo
struct FrameGraph(Movable)
```

One frame's passes and resources, compiled into an execution order and pooled textures. Per frame: `reset`, declare resources (`create`, `import_view`, `create_external`) and passes (`add_pass`, `read`, `write`), `export` what must survive, `compile`, then walk `order` and look up views with `view_of`. Physical textures are owned by the graph and freed by `release` (or when a compile no longer requests them); declaration and compile do not allocate in steady state.

Fields:

- `resources`: List[_Resource]
- `passes`: List[_Pass]
- `order`: List[Int] — Pass indices in execution order (set by `compile`).
- `physicals`: List[_Physical]
- `compiled`: Bool
- `culled_count`: Int

- **`__init__`** `__init__(out self)`
- **`reset`** `reset(mut self)`  
  Starts a new frame's declaration. Physical textures stay pooled; pass records keep their read/write list capacity for reuse.
- **`create`** `create(mut self, name: String, desc: TransientDesc) -> Int`  
  Declares a transient texture and returns its resource index (valid until `reset`). The physical texture is assigned by `compile`, possibly shared with other transients.
- **`import_view`** `import_view( mut self, name: String, view: TextureViewHandle, desc: TransientDesc ) -> Int`  
  A view the caller keeps alive (swapchain, persistent target). Never aliased.
- **`create_external`** `create_external(mut self, name: String) -> Int`  
  A buffer-backed resource (no texture): passes writing it are ordered before readers and kept alive by them, but no physical texture is allocated.
- **`export`** `export(mut self, resource: Int)`  
  Marks a resource as needed outside the graph; passes feeding it are kept.
- **`add_pass`** `add_pass(mut self, name: String) -> Int`  
  Declares a pass and returns its index; reuses a spare pass record when there is one.
- **`read`** `read(mut self, pass_: Int, resource: Int)`  
  Declares that `pass_` reads `resource` (ordered after the resource's producer).
- **`write`** `write(mut self, pass_: Int, resource: Int)`  
  Declares that `pass_` writes `resource`; writers of a resource keep declaration order.
- **`compile`** `compile(mut self, mut res: Resources) raises`  
  Orders, culls, computes lifetimes and assigns physical textures.
- **`release`** `release(mut self, mut res: Resources) raises`  
  Destroys every pooled physical texture and its view; raises on a `Resources` error.
- **`view_of`** `view_of(self, resource: Int) raises -> TextureViewHandle`  
  View of `resource` after `compile`: the imported view or its physical texture's view. Raises for an external (buffer-backed) resource and for one with no physical texture (unused, culled or not compiled).
- **`texture_of`** `texture_of(self, resource: Int) raises -> TextureHandle`  
  Physical texture behind a transient after `compile`. Raises for imported and external resources and for transients without a physical texture.
- **`find`** `find(self, name: String) -> Int`  
  Index of the resource with this name, or -1.
- **`physical_count`** `physical_count(self) -> Int`  
  Pooled physical textures after the last `compile` (`FrameStats.transient_textures`).
- **`is_culled`** `is_culled(self, pass_: Int) -> Bool`  
  True when the last `compile` culled the pass (it feeds no exported resource).
- **`lifetime`** `lifetime(self, resource: Int) -> Tuple[Int, Int]`  
  (first, last) execution-order indices using the resource; (-1, -1) if no kept pass does.
- **`to_graphviz`** `to_graphviz(self) -> String`  
  DOT text: passes as boxes in execution order, resources as ellipses, edges for reads/writes.


## `cinder.render.lights`

Source: `cinder/render/lights.mojo`

Lights and the clustered-culling configuration shared with `shaders/common.wgsl`.

- `comptime CLUSTER_X = 16` — Cluster grid columns across the screen; mirrors `CLUSTER_X` in common.wgsl.
- `comptime CLUSTER_Y = 9` — Cluster grid rows across the screen; mirrors `CLUSTER_Y` in common.wgsl.
- `comptime CLUSTER_Z = 24` — Logarithmic depth slices of the cluster grid; mirrors `CLUSTER_Z` in common.wgsl.
- `comptime CLUSTER_COUNT = CLUSTER_X * CLUSTER_Y * CLUSTER_Z` — Total clusters (16 x 9 x 24); sizes the `cluster_grid` count buffer (one u32 each).
- `comptime CLUSTER_MAX_LIGHTS = 32` — Light indices stored per cluster; lights beyond this are dropped by `clusters.wgsl`.
- `comptime LIGHT_BYTES = 64` — Bytes per packed light: the size of `Light` in common.wgsl.
- `comptime MAX_LIGHTS = 1024` — Most local lights the renderer uploads per frame; extra lights are dropped.
### struct `LightKind`

```mojo
struct LightKind(Equatable, ImplicitlyCopyable, RegisterPassable)
```

Light type tag; values mirror `LIGHT_*` in common.wgsl (0 directional, 1 point, 2 spot).

Fields:

- `value`: UInt32

- **`directional`** `directional() -> LightKind`  
  Sun-like light at infinity; only its direction matters.
- **`point`** `point() -> LightKind`  
  Omnidirectional light at a position, attenuated to zero at `range`.
- **`spot`** `spot() -> LightKind`  
  Cone light at a position along `direction`, attenuated to zero at `range`.

### struct `Light`

```mojo
struct Light(ImplicitlyCopyable, Movable)
```

A light. Directional: `direction` (travel direction), `color * intensity` in lux. Point and spot: position, range (metres), `intensity` in watts, spot cone angles in radians.

Fields:

- `kind`: LightKind
- `position`: Vec3
- `direction`: Vec3
- `color`: Vec3
- `intensity`: Float32
- `range`: Float32
- `inner_angle`: Float32
- `outer_angle`: Float32
- `cast_shadows`: Bool

- **`__init__`** `__init__( out self, kind: LightKind, position: Vec3 = Vec3.zero(), direction: Vec3 = Vec3(0.0, -1.0, 0.0), color: Vec3 = Vec3.one(), intensity: Float32 = 1.0, range: Float32 = 10.0, inner_angle: Float32 = 0.4, outer_angle: Float32 = 0.6, cast_shadows: Bool = False, )`
- **`sun`** `sun( direction: Vec3, color: Vec3 = Vec3.one(), lux: Float32 = 3.0, cast_shadows: Bool = True, ) -> Light`  
  Directional light travelling along `direction` (normalised), with `lux` illuminance. Position, range and cone angles are zero; shadows default to on (cascaded shadow maps).
- **`point_light`** `point_light( position: Vec3, color: Vec3, watts: Float32, range: Float32, cast_shadows: Bool = False, ) -> Light`  
  Point light at `position` (metres) emitting `watts`, reaching zero at `range` metres.
- **`spot_light`** `spot_light( position: Vec3, direction: Vec3, color: Vec3, watts: Float32, range: Float32, inner: Float32, outer: Float32, cast_shadows: Bool = False, ) -> Light`  
  Spot light along `direction` (normalised) emitting `watts` with a `range` in metres. `inner` and `outer` are cone half-angles in radians from the axis: full intensity inside `inner`, fading to zero at `outer`.
- **`bounding_sphere`** `bounding_sphere(self) -> Vec4`  
  Influence sphere: xyz position, w range in metres (meaningless for directional lights).
- **`pack`** `pack(self, mut w: ByteWriter, shadow_view: Int)`  
  64 bytes matching `Light` in common.wgsl.

### def `pack_lights_into`

```mojo
def pack_lights_into( mut w: ByteWriter, lights: List[Light], shadow_views: List[Int] )
```

Appends the GPU light array (at least one zeroed entry) to `w`.


### def `pack_lights`

```mojo
def pack_lights(lights: List[Light], shadow_views: List[Int]) -> List[UInt8]
```

`pack_lights_into` into a new byte list (allocates; at least one zeroed 64-byte entry). `shadow_views[i]` is light i's first shadow-view index (-1 or missing: no shadow).


### struct `ClusterParams`

```mojo
struct ClusterParams(ImplicitlyCopyable, Movable)
```

Logarithmic z slicing: slice = log2(z) * scale + bias between `near` and `far`.

Fields:

- `scale`: Float32
- `bias`: Float32
- `far`: Float32

- **`compute`** `compute(near: Float32, far: Float32) -> ClusterParams`  
  Slice parameters mapping `near` to slice 0 and `far` to `CLUSTER_Z` (metres).
- **`slice_of`** `slice_of(self, view_z: Float32) -> Int`  
  Z slice for a positive view-space distance in metres, clamped to [0, CLUSTER_Z - 1]. Mirrors `cluster_z_slice` in common.wgsl.


## `cinder.render.material`

Source: `cinder/render/material.mojo`

glTF 2.0 metallic-roughness materials and their GPU bind groups. Uniform layout (96 bytes, 16-byte aligned fields, mirrored by `shaders/material.wgsl`): 0  base_color: vec4 16 emissive.rgb, emissive_strength 32 metallic, roughness, normal_scale, occlusion_strength 48 alpha_cutoff, alpha_mode (0 opaque, 1 mask, 2 blend), double_sided, texture_flags 64 uv_scale.xy, uv_offset.xy            (KHR_texture_transform, applied to every map) 80 uv_rotation, texcoord_bits (3 bits per map: base, mr, normal, occlusion, emissive), pad, pad

- `comptime MATERIAL_UNIFORM_SIZE = 96` — Bytes in the material uniform (group 2, binding 0); see the module docstring for the layout.
### struct `AlphaMode`

```mojo
struct AlphaMode(Equatable, ImplicitlyCopyable, RegisterPassable)
```

How alpha is used; packed into the material uniform and mirrored in WGSL.

Fields:

- `value`: UInt32

- **`opaque`** `opaque() -> AlphaMode`  
  Alpha is ignored; the surface is fully opaque.
- **`mask`** `mask() -> AlphaMode`  
  Alpha-tested: fragments with alpha below `alpha_cutoff` are discarded.
- **`blend`** `blend() -> AlphaMode`  
  Alpha-blended; drawn after opaque batches, sorted back to front.

- `comptime TEX_BASE_COLOR = 0` — Slot of the base colour map in `Material.textures`, `texcoords` and `texture_flags`.
- `comptime TEX_METALLIC_ROUGHNESS = 1` — Slot of the metallic-roughness map in `Material.textures`, `texcoords` and `texture_flags`.
- `comptime TEX_NORMAL = 2` — Slot of the normal map in `Material.textures`, `texcoords` and `texture_flags`.
- `comptime TEX_OCCLUSION = 3` — Slot of the occlusion map in `Material.textures`, `texcoords` and `texture_flags`.
- `comptime TEX_EMISSIVE = 4` — Slot of the emissive map in `Material.textures`, `texcoords` and `texture_flags`.
### struct `Material`

```mojo
struct Material(Copyable, Movable)
```

CPU description. Texture views are null when a map is absent (defaults are bound instead).

Fields:

- `name`: String
- `base_color`: Vec4
- `metallic`: Float32
- `roughness`: Float32
- `emissive`: Vec3
- `emissive_strength`: Float32
- `normal_scale`: Float32
- `occlusion_strength`: Float32
- `alpha_mode`: AlphaMode
- `alpha_cutoff`: Float32
- `double_sided`: Bool
- `uv_scale`: Vec2
- `uv_offset`: Vec2
- `uv_rotation`: Float32
- `textures`: Array[TextureViewHandle, 5]
- `texcoords`: Array[UInt8, 5]
- `sampler`: SamplerHandle

- **`__init__`** `__init__(out self, name: String = String("material"))`
- **`texture_flags`** `texture_flags(self) -> UInt32`  
  Bit `i` set when map slot `i` (`TEX_*`) has a texture view.
- **`texcoord_bits`** `texcoord_bits(self) -> UInt32`  
  Texture coordinate set per map, 3 bits per `TEX_*` slot (the shader takes uv1 for 1).
- **`pack_uniform`** `pack_uniform(self) -> List[UInt8]`  
  The 96-byte uniform laid out as in the module docstring, in a new list (allocates).

### def `material_bind_group_layout`

```mojo
def material_bind_group_layout( mut res: Resources, ) raises -> BindGroupLayoutHandle
```

Creates the group-2 material layout matching `shaders/material.wgsl`; raises on a GPU error. Binding 0 is the uniform (vertex and fragment), 1 the sampler, 2..6 the maps in `TEX_*` order.


### struct `GpuMaterial`

```mojo
struct GpuMaterial(ImplicitlyCopyable, Movable)
```

Uniform buffer and bind group of an uploaded material. `alpha_mode` and `double_sided` are copied from the `Material` to select its pipeline.

Fields:

- `uniform`: BufferHandle
- `bind_group`: BindGroupHandle
- `alpha_mode`: AlphaMode
- `double_sided`: Bool


### def `upload_material`

```mojo
def upload_material( mut res: Resources, layout: BindGroupLayoutHandle, defaults: DefaultTextures, material: Material, ) raises -> GpuMaterial
```

Creates the material's uniform buffer and group-2 bind group; raises on a GPU error. Absent maps bind the defaults (white base colour, metallic-roughness and occlusion, flat normal, black emissive) and a null sampler binds `sampler_linear_repeat`. The texture views stay owned by the caller; the returned buffer and bind group are new allocations.


### def `update_material`

```mojo
def update_material( mut res: Resources, gpu: GpuMaterial, material: Material ) raises
```

Re-uploads the parameters (textures are fixed per bind group).



## `cinder.render.mesh`

Source: `cinder/render/mesh.mojo`

Mesh data on the CPU, GPU meshes, the engine vertex format and procedural primitives. Vertex stream (56 bytes): position f32x3 @0, normal f32x3 @12, tangent f32x4 @24 (w = handedness), uv0 f32x2 @40, uv1 f32x2 @48. Skinning attributes live in a second stream (`SkinVertex`, 24 bytes): joints u16x4 @0, weights f32x4 @8. Indices are u32.

- `comptime VERTEX_STRIDE = 56` — Bytes per vertex in the static stream (slot 0); layout in the module docstring.
- `comptime SKIN_VERTEX_STRIDE = 24` — Bytes per vertex in the skinning stream (slot 1): u16x4 joints, f32x4 weights.
- `comptime PI = Float32(3.14159265358979)` — Pi as Float32, for the procedural primitives.
### def `vertex_layout`

```mojo
def vertex_layout() -> VertexLayout
```

Slot 0: the static vertex stream. Locations 0..4.


### def `skin_vertex_layout`

```mojo
def skin_vertex_layout() -> VertexLayout
```

Slot 1: joints and weights. Locations 5 and 6.


### struct `Bounds`

```mojo
struct Bounds(ImplicitlyCopyable, Movable)
```

Axis-aligned bounding box in mesh space (metres).

Fields:

- `min`: Vec3
- `max`: Vec3

- **`centre`** `centre(self) -> Vec3`  
  Midpoint of the box.
- **`radius`** `radius(self) -> Float32`  
  Half the box diagonal: the radius of the bounding sphere around `centre()`.
- **`sphere`** `sphere(self) -> Vec4`  
  Bounding sphere: xyz centre, w radius (the input of `Frustum.cull_spheres_into`).
- **`empty`** `empty() -> Bounds`  
  Inverted box (min +3.4e38, max -3.4e38); the first `include` collapses it to a point.
- **`include`** `include(mut self, p: Vec3)`  
  Grows the box to contain `p`.

### struct `MorphTarget`

```mojo
struct MorphTarget(Copyable, Movable)
```

Per-vertex position and normal deltas of one morph target (normals may be empty).

Fields:

- `positions`: List[Vec3]
- `normals`: List[Vec3]


### struct `MeshLod`

```mojo
struct MeshLod(ImplicitlyCopyable, Movable)
```

An index range of the combined index buffer and the simplification error it carries.

Fields:

- `first_index`: UInt32
- `index_count`: UInt32
- `error`: Float32


### struct `MeshData`

```mojo
struct MeshData(Copyable, Movable)
```

CPU-side triangle mesh. Parallel per-vertex arrays; `tangents`, `uv1`, `joints` and `weights` may be empty and are filled with defaults at upload.

Fields:

- `positions`: List[Vec3]
- `normals`: List[Vec3]
- `tangents`: List[Vec4]
- `uv0`: List[Vec2]
- `uv1`: List[Vec2]
- `joints`: List[SIMD[DType.uint16, 4]]
- `weights`: List[Vec4]
- `indices`: List[UInt32]
- `morph_targets`: List[MorphTarget]

- **`__init__`** `__init__(out self)`
- **`vertex_count`** `vertex_count(self) -> Int`  
  Number of vertices (`len(positions)`).
- **`triangle_count`** `triangle_count(self) -> Int`  
  Number of triangles (`len(indices) // 3`).
- **`has_skin`** `has_skin(self) -> Bool`  
  True when every vertex has joint indices and weights (`joints` and `weights` both match `positions`, non-empty), which is what `pack_skin_vertices` reads.
- **`bounds`** `bounds(self) -> Bounds`  
  Bounds of `positions`; a zero box at the origin when the mesh has no vertices.
- **`validate`** `validate(self) raises`  
  Raises when the arrays are inconsistent. Checks that normals and uv0 match the vertex count, that tangents and uv1 are empty or match it, that `joints` and `weights` are either both empty or both match it (a skin needs one of each per vertex), that the index count is a multiple of 3 and that every index is in range. Morph targets are not checked (missing deltas pack as zero).
- **`compute_normals`** `compute_normals(mut self)`  
  Area-weighted face normals accumulated per vertex.
- **`compute_tangents`** `compute_tangents(mut self, lib: NativePtr) raises`  
  MikkTSpace tangents through the shim (requires normals and uv0).
- **`pack_vertices`** `pack_vertices(self) -> List[UInt8]`  
  The 56-byte-per-vertex static stream as a new byte list (allocates). Missing tangents become (1, 0, 0, 1) and missing uv1 repeats uv0. Expects normals and uv0 for every vertex (see `validate`).
- **`pack_skin_vertices`** `pack_skin_vertices(self) -> List[UInt8]`  
  The 24-byte-per-vertex skin stream: little-endian u16x4 joints, then f32x4 weights. Allocates; expects `joints` and `weights` for every vertex.
- **`pack_indices`** `pack_indices(self) -> List[UInt8]`  
  `indices` as little-endian u32 bytes in a new list (allocates).
- **`pack_morph_deltas`** `pack_morph_deltas(self) -> List[UInt8]`  
  Per target, per vertex: position delta (vec4) then normal delta (vec4), as common.wgsl reads.
- **`build_lods`** `build_lods( self, lib: NativePtr, levels: Int = 4 ) raises -> List[List[UInt32]]`  
  LOD index lists (level 0 is the original, vertex-cache optimised) via meshoptimizer. Each level targets half the triangles of the previous one with a relative error bound of 2%; a level that cannot be simplified further ends the chain.

### struct `GpuMesh`

```mojo
struct GpuMesh(Copyable, Movable)
```

Buffers for one mesh. `skin_vertices` is null when the mesh is not skinned, `morph_buffer` when it has no morph targets. `lods[0]` is the full-detail index range. `index_count` is the full-detail index count (`lods[0].index_count`, 0 without LODs), the same for `upload_mesh_lods` and `upload_mesh_bytes`; the index buffer holds every LOD range, so its total length is the sum over `lods`.

Fields:

- `vertices`: BufferHandle
- `skin_vertices`: BufferHandle
- `indices`: BufferHandle
- `index_count`: UInt32
- `vertex_count`: UInt32
- `bounds`: Bounds
- `lods`: List[MeshLod]
- `morph_buffer`: BufferHandle
- `morph_target_count`: UInt32

- **`__init__`** `__init__( out self, vertices: BufferHandle, skin_vertices: BufferHandle, indices: BufferHandle, index_count: UInt32, vertex_count: UInt32, bounds: Bounds, var lods: List[MeshLod], morph_buffer: BufferHandle, morph_target_count: UInt32, )`
- **`select_lod`** `select_lod( self, projected_diameter_px: Float32, screen_height_px: Float32 ) -> Int`  
  Smaller on screen -> coarser LOD. Level 1 below 1/4 of the screen height, level 2 below 1/8, level 3 below 1/16 (resolution independent).

### def `upload_mesh`

```mojo
def upload_mesh( mut res: Resources, label: String, data: MeshData ) raises -> GpuMesh
```

Uploads with a single LOD.


### def `upload_mesh_lods`

```mojo
def upload_mesh_lods( mut res: Resources, label: String, data: MeshData, lod_indices: List[List[UInt32]], ) raises -> GpuMesh
```

Uploads vertex streams plus one combined index buffer holding every LOD range.


### def `upload_mesh_bytes`

```mojo
def upload_mesh_bytes( mut res: Resources, label: String, vertex_count: UInt32, vertex_bytes: List[UInt8], skin_bytes: List[UInt8], index_bytes: List[UInt8], var lods: List[MeshLod], bounds: Bounds, morph_target_count: UInt32, morph_bytes: List[UInt8], ) raises -> GpuMesh
```

Uploads streams that are already in GPU layout (cooked meshes).


### def `destroy_mesh`

```mojo
def destroy_mesh(mut res: Resources, mesh: GpuMesh) raises
```

Destroys the mesh's vertex and index buffers, and its skin and morph buffers when present. The handles in `mesh` are left as they are; raises on a `Resources` error.


### def `cube`

```mojo
def cube(half: Float32 = 0.5) -> MeshData
```

24-vertex cube with per-face normals, CCW winding seen from outside.


### def `uv_sphere`

```mojo
def uv_sphere( radius: Float32 = 0.5, rings: Int = 16, segments: Int = 32 ) -> MeshData
```

UV sphere centred at the origin, poles on +Y and -Y; `radius` in metres. `(rings + 1) * (segments + 1)` vertices (the seam is duplicated), counter-clockwise seen from outside. u runs around the sphere from +X towards +Z, v from the top pole (0) to the bottom (1); tangents point along increasing u.


### def `plane`

```mojo
def plane(half: Float32 = 0.5, subdivisions: Int = 1) -> MeshData
```

An XZ plane facing +Y.



## `cinder.render.renderer`

Source: `cinder/render/renderer.mojo`

The forward+ renderer: shadows, depth prepass, GTAO, clustered lights, PBR, sky, bloom, tonemap. `Renderer.render(scene, target)` builds the frame graph for the current settings, compiles it (ordering, transient aliasing), fills the GPU buffers from the scene and records every pass into one command buffer. CPU work per frame: frustum culling (SIMD), LOD selection, batching by (mesh, material, skinning), cascade fitting, light packing. Nothing here waits on the GPU.

- `comptime FRAME_UNIFORM_SIZE = 720` — Bytes in the frame uniform: the size of `Frame` in common.wgsl (checked each frame).
- `comptime PASS_UNIFORM_STRIDE = 256` — Bytes per view-projection slot in `pass_uniforms`, the dynamic-offset alignment.
- `comptime MAX_PASS_VIEWS = 128` — Slots in `pass_uniforms`: four cascades, the atlas views, then the camera.
- `comptime INSTANCE_BYTES = 144` — Bytes per instance in the `instances` buffer: model, normal matrix, u32x4 params.
- `comptime MORPH_WEIGHTS_BYTES = 32` — Bytes of morph weights bound per morphed instance: eight f32 weights.
- `comptime POST_UNIFORM_STRIDE = 256` — Bytes per slot in `post_uniforms` (and per morph-weight slot), the dynamic-offset alignment.
- `comptime MAX_CACHED_POST_GROUPS = 64` — Post bind groups kept across frames before the cache is rebuilt.
- `comptime MAX_POST_PASSES = 32` — Slots in `post_uniforms` per frame.
- `comptime INITIAL_MORPH_SLOTS = 32` — Initial slots in the morph-weight buffer; it doubles when a frame has more morphed instances.
- `comptime BLOOM_MIPS = 6` — Levels in the bloom chain; level 0 is half the output resolution.
- `comptime ENV_SIZE = 256` — Face size in pixels of the baked environment cube (RGBA16F, full mip chain).
- `comptime PREFILTER_SIZE = 128` — Face size in pixels of the prefiltered specular cube.
- `comptime PREFILTER_MIPS = 6` — Mips of the prefiltered cube; mip m holds roughness `m / (PREFILTER_MIPS - 1)`.
- `comptime IRRADIANCE_SIZE = 32` — Face size in pixels of the diffuse irradiance cube.
- `comptime BRDF_LUT_SIZE = 256` — Size in pixels of the split-sum BRDF lookup texture.
- `comptime SHADER_DIR = "cinder/render/shaders"` — Directory of the renderer's WGSL files, relative to the working directory.
### struct `Tonemapper`

```mojo
struct Tonemapper(Equatable, ImplicitlyCopyable, RegisterPassable)
```

Tone curve of the tonemap pass; values mirror `post.settings.z` in post.wgsl.

Fields:

- `value`: UInt32

- **`agx`** `agx() -> Tonemapper`  
  AgX (the default).
- **`aces`** `aces() -> Tonemapper`  
  ACES filmic curve.
- **`none`** `none() -> Tonemapper`  
  No curve: the exposed colour is clamped to [0, 1]; also used for debug views.

### struct `DebugMode`

```mojo
struct DebugMode(Equatable, ImplicitlyCopyable, RegisterPassable)
```

Mirrors DEBUG_* in common.wgsl.

Fields:

- `value`: UInt32

- **`none`** `none() -> DebugMode`  
  Normal lit output.
- **`normals`** `normals() -> DebugMode`  
  World-space normals mapped to colour (`n * 0.5 + 0.5`).
- **`cascades`** `cascades() -> DebugMode`  
  Colour per sun shadow cascade, shaded by the sun angle.
- **`clusters`** `clusters() -> DebugMode`  
  Heat map of the light count in each fragment's cluster (full scale at 16 lights).
- **`overdraw`** `overdraw() -> DebugMode`  
  Overdraw count: forward pipelines blend additively with the depth test off, so every rasterised fragment (hidden or not, opaque or blended) adds 0.1 grey; brightness is the fragment count / 10 (white at 10 or more) and empty sky is black.
- **`lod`** `lod() -> DebugMode`  
  Colour per instance from its LOD index (bits 8..15 of the instance flags), skinned and morphed instances included.
- **`albedo`** `albedo() -> DebugMode`  
  Unlit base colour (material factor times the base colour map).
- **`ao`** `ao() -> DebugMode`  
  The ambient occlusion term as greyscale.

### struct `RenderSettings`

```mojo
struct RenderSettings(ImplicitlyCopyable, Movable)
```

Quality knobs, read at the start of every `render` (change them between frames). `cascade_resolution`, `atlas_size` and `atlas_tile` are read at construction only.

Fields:

- `msaa`: UInt32 — 1 or 4.
- `bloom`: Bool
- `bloom_strength`: Float32
- `bloom_threshold`: Float32
- `tonemapper`: Tonemapper
- `exposure`: Float32
- `fxaa`: Bool
- `ao`: Bool
- `shadows`: Bool
- `shadow_distance`: Float32
- `cascade_resolution`: Int
- `atlas_size`: Int
- `atlas_tile`: Int
- `ibl_intensity`: Float32
- `debug`: DebugMode
- `wireframe`: Bool
- `cull`: Bool

- **`__init__`** `__init__(out self)`

### struct `Environment`

```mojo
struct Environment(ImplicitlyCopyable, Movable)
```

Procedural sky by default; `equirect` (an RGBA16F 2D texture view) replaces it when set.

Fields:

- `zenith`: Vec3
- `ground`: Vec3
- `sun_intensity`: Float32
- `equirect`: TextureViewHandle

- **`__init__`** `__init__(out self)`

### struct `RenderInstance`

```mojo
struct RenderInstance(Copyable, Movable)
```

One drawn object: mesh and material indices into the renderer's registries.

Fields:

- `mesh`: Int
- `material`: Int
- `transform`: Mat4
- `joint_offset`: Int — Index of the first joint matrix in `RenderScene.joints` (skinned meshes), else -1.
- `morph_weights`: List[Float32]
- `cast_shadows`: Bool

- **`__init__`** `__init__( out self, mesh: Int, material: Int, transform: Mat4, joint_offset: Int = -1, cast_shadows: Bool = True, )`

### struct `RenderScene`

```mojo
struct RenderScene(Movable)
```

Everything one `render` call draws; owned by the caller and only read by the renderer.

Fields:

- `camera`: Camera
- `lights`: List[Light]
- `instances`: List[RenderInstance]
- `joints`: List[Mat4] — Skinning matrices (joint world * inverse bind), referenced by instances.
- `environment`: Environment

- **`__init__`** `__init__(out self, camera: Camera)`

### struct `FrameStats`

```mojo
struct FrameStats(ImplicitlyCopyable, Movable)
```

Counters of the last `render`, in `Renderer.stats`.

Fields:

- `instances_submitted`: Int
- `instances_drawn`: Int
- `draw_calls`: Int — Forward (lit) draws, opaque and blended.
- `prepass_draw_calls`: Int
- `shadow_draw_calls`: Int
- `lights`: Int
- `shadow_views`: Int
- `passes`: Int
- `transient_textures`: Int


### struct `Renderer`

```mojo
struct Renderer(Movable)
```

The forward+ renderer; `render` draws a `RenderScene` into a target view. Owns the pipelines, persistent GPU buffers and textures, the mesh and material registries, the frame graph, the shader library and per-frame scratch (steady-state frames do not allocate). Holds raw pointers to `Gfx` and `Resources`, which must outlive it.

Fields:

- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `res`: Pointer[Resources, MutUntrackedOrigin]
- `lib`: NativePtr
- `shaders`: ShaderManager
- `library`: ShaderLibrary
- `settings`: RenderSettings
- `defaults`: DefaultTextures
- `graph`: FrameGraph
- `stats`: FrameStats
- `frame_index`: Int
- `graphviz`: String — DOT text of the last compiled frame graph; refreshed by `graph_dot()` on demand.
- `morph_groups`: List[BindGroupHandle] — Object groups binding a morphed mesh's delta buffer, one per morph buffer in use; batches of meshes without morph targets use `object_group`.
- `morph_group_keys`: List[SIMD[DType.int64, 2]] — (index, generation) of the morph buffer each entry of `morph_groups` binds.
- `post_group_keys`: List[SIMD[DType.int64, 4]] — (input index, input generation, bloom index, bloom generation) per cached post group.
- `alloc_marks`: Bool — Print the Mojo allocation counter after each render phase (needs the counter library).
- `gtao_groups`: List[BindGroupHandle]
- `gtao_group_keys`: List[SIMD[DType.int64, 4]]
- `meshes`: List[GpuMesh]
- `materials`: List[GpuMaterial]
- `material_params`: List[Material]
- `forward_shader`: ShaderHandle
- `depth_shader`: ShaderHandle
- `clusters_shader`: ShaderHandle
- `sky_shader`: ShaderHandle
- `ibl_shader`: ShaderHandle
- `post_shader`: ShaderHandle
- `gtao_shader`: ShaderHandle
- `gtao_msaa_shader`: ShaderHandle
- `frame_layout`: BindGroupLayoutHandle
- `object_layout`: BindGroupLayoutHandle
- `material_layout`: BindGroupLayoutHandle
- `pass_layout`: BindGroupLayoutHandle
- `cluster_out_layout`: BindGroupLayoutHandle
- `gtao_layout`: BindGroupLayoutHandle
- `gtao_msaa_layout`: BindGroupLayoutHandle
- `ibl_layout`: BindGroupLayoutHandle
- `post_layout`: BindGroupLayoutHandle
- `scene_pipeline_layout`: PipelineLayoutHandle
- `cluster_pipeline_layout`: PipelineLayoutHandle
- `gtao_pipeline_layout`: PipelineLayoutHandle
- `gtao_msaa_pipeline_layout`: PipelineLayoutHandle
- `ibl_pipeline_layout`: PipelineLayoutHandle
- `post_pipeline_layout`: PipelineLayoutHandle
- `frame_uniform`: BufferHandle
- `pass_uniforms`: BufferHandle
- `post_uniforms`: BufferHandle
- `lights_buffer`: BufferHandle
- `shadow_views_buffer`: BufferHandle
- `cluster_grid`: BufferHandle
- `cluster_indices`: BufferHandle
- `instances_buffer`: BufferHandle
- `instances_capacity`: Int
- `joints_buffer`: BufferHandle
- `joints_capacity`: Int
- `morph_weights_buffer`: BufferHandle
- `morph_slot_capacity`: Int — Morph-weight slots (POST_UNIFORM_STRIDE bytes each) in `morph_weights_buffer`.
- `empty_morph_buffer`: BufferHandle
- `cascade_texture`: TextureHandle
- `cascade_array_view`: TextureViewHandle
- `cascade_layer_views`: List[TextureViewHandle]
- `atlas_texture`: TextureHandle
- `atlas_view`: TextureViewHandle
- `env_texture`: TextureHandle
- `env_cube_view`: TextureViewHandle
- `prefiltered_texture`: TextureHandle
- `prefiltered_view`: TextureViewHandle
- `irradiance_texture`: TextureHandle
- `irradiance_view`: TextureViewHandle
- `brdf_texture`: TextureHandle
- `brdf_view`: TextureViewHandle
- `shadow_sampler`: SamplerHandle
- `env_sampler`: SamplerHandle
- `post_sampler`: SamplerHandle
- `nearest_sampler`: SamplerHandle
- `env_baked`: Bool
- `env_key`: String
- `frame_group`: BindGroupHandle
- `frame_group_ao_view`: TextureViewHandle
- `shadow_frame_group`: BindGroupHandle
- `cluster_frame_group`: BindGroupHandle — Frame group variant for the cluster pass: the cluster buffers it writes are replaced by a dummy.
- `dummy_cluster_buffer`: BufferHandle — Frame group variant for shadow passes: the shadow maps being written are replaced by dummies.
- `dummy_depth_array`: TextureHandle
- `dummy_depth_2d`: TextureHandle
- `object_group`: BindGroupHandle
- `object_group_key`: String
- `pass_group`: BindGroupHandle
- `cluster_out_group`: BindGroupHandle
- `pipelines`: List[_CachedPipeline]
- `sky_pipeline`: RenderPipelineHandle
- `sky_pipeline_msaa`: UInt32
- `cluster_pipeline`: ComputePipelineHandle
- `gtao_pipeline`: ComputePipelineHandle
- `gtao_blur_pipeline`: ComputePipelineHandle
- `gtao_msaa_pipeline`: ComputePipelineHandle
- `gtao_msaa_blur_pipeline`: ComputePipelineHandle
- `ibl_env_pipeline`: ComputePipelineHandle
- `ibl_prefilter_pipeline`: ComputePipelineHandle
- `ibl_irradiance_pipeline`: ComputePipelineHandle
- `ibl_brdf_pipeline`: ComputePipelineHandle
- `bloom_down_pipeline`: RenderPipelineHandle
- `bloom_up_pipeline`: RenderPipelineHandle
- `tonemap_pipeline`: RenderPipelineHandle
- `fxaa_pipeline`: RenderPipelineHandle
- `blit_pipeline`: RenderPipelineHandle
- `post_target_format`: WGPUTextureFormat
- `atlas`: ShadowAtlas
- `batches`: List[_Batch]
- `blend_batches`: List[_Batch]
- `instance_bytes`: ByteWriter
- `post_groups`: List[BindGroupHandle]
- `cluster_params`: ClusterParams

- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], res: Pointer[Resources, MutUntrackedOrigin], lib: NativePtr, settings: RenderSettings, target_format: WGPUTextureFormat, ) raises`
- **`add_mesh`** `add_mesh( mut self, label: String, data: MeshData, lods: Bool = True ) raises -> Int`  
  Uploads `data` and returns its index in `meshes`; raises if validation or upload fails. With `lods` and more than 64 triangles, up to four LODs are built with meshoptimizer; otherwise the mesh has one. Always appends (removed slots are only reused by `add_cooked_mesh`).
- **`add_cooked_mesh`** `add_cooked_mesh( mut self, label: String, cooked: CookedMesh ) raises -> Int`  
  Registers a cooked mesh (streams already in GPU layout). Reuses a removed slot if any.
- **`replace_cooked_mesh`** `replace_cooked_mesh( mut self, index: Int, label: String, cooked: CookedMesh ) raises`  
  Hot reload: swaps the GPU buffers behind `index`; instances keep their mesh index.
- **`remove_mesh`** `remove_mesh(mut self, index: Int) raises`  
  Frees the GPU buffers; the slot is reused by the next `add_cooked_mesh`. Instances that still reference it are the caller's error (rendering them raises on the invalid handle).
- **`add_material`** `add_material(mut self, material: Material) raises -> Int`  
  Uploads `material` and returns its index in `materials`; raises on a GPU error. A copy of the parameters is kept in `material_params`; texture views stay the caller's.
- **`replace_material`** `replace_material(mut self, index: Int, material: Material) raises`  
  Hot reload: uploads `material` into slot `index` (instances keep their material index) and frees the uniform buffer and bind group of the version it replaces. The new version is uploaded before anything is freed, so a failed upload raises and leaves the slot as it was. Replacing a removed slot makes it live again. `material_params[index]` becomes a copy of `material`. Texture views and samplers are the caller's: the old ones are not destroyed (destroy them after this call if nothing else uses them). Raises when `index` is out of range.
- **`remove_material`** `remove_material(mut self, index: Int) raises`  
  Frees the uniform buffer and bind group of slot `index` and leaves the slot empty (null handles); removing an empty slot does nothing. Indices stay stable: `add_material` always appends, so a removed slot is never handed out again (only `replace_material` on the same index revives it). `render` raises if a visible instance still uses a removed material. Texture views and samplers are the caller's, and `material_params[index]` keeps the last parameters. Raises when `index` is out of range.
- **`material_is_live`** `material_is_live(self, index: Int) -> Bool`  
  True when slot `index` exists and has not been removed.
- **`mesh_lod_count`** `mesh_lod_count(self, mesh: Int) -> Int`  
  Number of LOD index ranges of mesh `mesh`; level 0 is full detail.
- **`reload_shaders`** `reload_shaders(mut self) raises -> Int`  
  Hot reload: recompiles changed programs and rebuilds every pipeline on success. Pipelines keep their handles; a failed compile keeps the last good module and pipelines.
- **`render`** `render( mut self, scene: RenderScene, target: TextureViewHandle, width: Int, height: Int, ) raises`  
  Renders `scene` into `target` (format given at construction). Raises GfxError on device loss.
- **`graph_dot`** `graph_dot(mut self) -> String`  
  DOT text of the last frame's graph (built on demand: debug UI, docs).
- **`release`** `release(mut self) raises`  
  Frees the frame graph's pooled textures (everything else dies with Resources).


## `cinder.render.shader_library`

Source: `cinder/render/shader_library.mojo`

Shader programs assembled from WGSL files with `#include "file.wgsl"` lines, with hot reload. WGSL has no preprocessor, so includes are resolved here (relative to the shader directory, once per program, in order of first appearance). Error locations reported by naga refer to the assembled text; `ShaderLibrary` maps them back to the file and line they came from before raising.

### struct `AssembledShader`

```mojo
struct AssembledShader(Copyable, Movable)
```

WGSL text with every include spliced in, mapped back to its source files. `chunks` locate each assembled line in its file; `files` lists every file read (`directory/name`), in order of first inclusion.

Fields:

- `text`: String
- `chunks`: List[_Chunk]
- `files`: List[String]

- **`__init__`** `__init__(out self)`
- **`locate`** `locate(self, assembled_line: Int) -> Tuple[String, Int]`  
  File and file line for a line of the assembled text.

### def `assemble`

```mojo
def assemble(directory: String, entry: String) raises -> AssembledShader
```

Reads `entry` and splices every `#include "x"` (each file at most once) into one text.


### struct `ShaderLibrary`

```mojo
struct ShaderLibrary(Movable)
```

Loads `directory/<entry>.wgsl` programs with includes; `check_for_changes` hot reloads.

Fields:

- `gfx`: Pointer[Gfx, MutUntrackedOrigin]
- `res`: Pointer[Resources, MutUntrackedOrigin]
- `shaders`: Pointer[ShaderManager, MutUntrackedOrigin]
- `directory`: String
- `programs`: List[_Program]
- `diagnostics`: List[LogRecord]
- `reloaded`: List[ShaderHandle] — Handles whose module changed in the last `check_for_changes` (pipelines must be rebuilt).

- **`__init__`** `__init__( out self, gfx: Pointer[Gfx, MutUntrackedOrigin], res: Pointer[Resources, MutUntrackedOrigin], shaders: Pointer[ShaderManager, MutUntrackedOrigin], directory: String, )`
- **`compile`** `compile(mut self, entry: String) raises -> WGPUShaderModule`  
  Assembles and compiles without registering (used for one-off modules and by `load`).
- **`load`** `load(mut self, entry: String) raises -> ShaderHandle`  
  Assembles, compiles and registers `entry` (a file in `directory`) for hot reload. Returns the handle `Resources` holds the module under. Raises on a missing include or a compile error, with locations mapped to the source files.
- **`check_for_changes`** `check_for_changes(mut self) -> Int`  
  Recompiles programs whose files changed. Returns how many modules were replaced; failures keep the last good module and add an error diagnostic naming file:line:column.
- **`take_diagnostics`** `take_diagnostics(mut self) -> List[LogRecord]`  
  Returns and clears the reload records from `check_for_changes` (info or error).


## `cinder.render.shadows`

Source: `cinder/render/shadows.mojo`

Cascaded shadow maps for the sun and an atlas for spot and point lights (CPU-side maths). Cascade fitting: each cascade covers a slice of the camera frustum (practical split scheme, lambda 0.75). The orthographic projection is fitted to the slice's bounding sphere so it has a constant size while the camera rotates, and its origin is snapped to whole shadow texels in light space so edges do not shimmer when the camera moves.

- `comptime CASCADE_COUNT = 4` — Sun shadow cascades; matches the four-entry cascade arrays of `Frame` in common.wgsl.
- `comptime SHADOW_VIEW_BYTES = 96` — Bytes per packed `ShadowView`: the size of `ShadowView` in common.wgsl.
### struct `Cascade`

```mojo
struct Cascade(ImplicitlyCopyable, Movable)
```

One fitted sun cascade: an orthographic reverse-Z light view-projection (world to clip).

Fields:

- `view_proj`: Mat4
- `split_end`: Float32 — View-space distance where this cascade ends.
- `texel_world`: Float32 — World-space size of one shadow texel in this cascade.


### def `cascade_splits_into`

```mojo
def cascade_splits_into( mut out: Array[Float32, CASCADE_COUNT], near: Float32, shadow_distance: Float32, lambda_: Float32 = 0.75, )
```

End distances of the four cascades (practical split: blend of log and uniform) into a stack array (no allocation).


### def `cascade_splits`

```mojo
def cascade_splits( near: Float32, shadow_distance: Float32, lambda_: Float32 = 0.75 ) -> List[Float32]
```

End distances of the four cascades (practical split: blend of log and uniform).


### def `fit_cascade`

```mojo
def fit_cascade( camera: Camera, light_dir: Vec3, z0: Float32, z1: Float32, resolution: Int ) -> Cascade
```

Orthographic light view-projection for one slice with texel snapping. `light_dir` is the direction light travels. Depth range is the sphere diameter plus a generous margin behind the slice so casters outside the camera frustum still cast.


### def `fit_cascades_into`

```mojo
def fit_cascades_into( mut out: List[Cascade], camera: Camera, light_dir: Vec3, shadow_distance: Float32, resolution: Int, )
```

`fit_cascades` into a list supplied by the caller (cleared first, capacity kept).


### def `fit_cascades`

```mojo
def fit_cascades( camera: Camera, light_dir: Vec3, shadow_distance: Float32, resolution: Int ) -> List[Cascade]
```

`fit_cascades_into` into a new list (allocates); per-frame code uses the `_into` variant.


### struct `ShadowView`

```mojo
struct ShadowView(ImplicitlyCopyable, Movable)
```

One atlas tile: a light-space view-projection and its uv rectangle (matches common.wgsl).

Fields:

- `view_proj`: Mat4
- `uv_origin_x`: Float32
- `uv_origin_y`: Float32
- `uv_size`: Float32
- `texel_uv`: Float32
- `near`: Float32
- `far`: Float32

- **`pack`** `pack(self, mut w: ByteWriter)`  
  Appends the 96-byte GPU form (`ShadowView` in common.wgsl). `view_proj`, then the uv rectangle (origin x, origin y, size, size), then the texel size in uv, near, far and a zero pad.

### struct `ShadowAtlas`

```mojo
struct ShadowAtlas(Movable)
```

Fixed grid of square tiles in one depth texture. Spot lights take one tile, point lights six.

Fields:

- `size`: Int
- `tile_size`: Int
- `tiles_per_row`: Int
- `next_tile`: Int
- `views`: List[ShadowView]

- **`__init__`** `__init__(out self, size: Int = 4096, tile_size: Int = 512)`
- **`capacity`** `capacity(self) -> Int`  
  Number of tiles in the atlas (`tiles_per_row` squared).
- **`reset`** `reset(mut self)`  
  Frees every tile and clears `views` (capacity kept); called at the start of each frame.
- **`tile_pixel_origin`** `tile_pixel_origin(self, view_index: Int) -> Tuple[Int, Int]`  
  Top-left pixel of the tile used by shadow view `view_index` (views fill tiles in order).
- **`allocate`** `allocate(mut self, light: Light) -> Int`  
  Adds the light's shadow views; returns the first view index or -1 when the atlas is full.
- **`pack_views_into`** `pack_views_into(self, mut w: ByteWriter)`  
  Appends the GPU shadow-view array (at least one zeroed entry) to `w`.
- **`pack_views`** `pack_views(self) -> List[UInt8]`  
  `pack_views_into` into a new byte list (allocates; at least one zeroed entry).


## `cinder.render.textures`

Source: `cinder/render/textures.mojo`

GPU textures from CPU images: CPU box-filter mip chains, sRGB or linear formats, defaults.

### struct `GpuTexture`

```mojo
struct GpuTexture(ImplicitlyCopyable, Movable)
```

An uploaded 2D texture and a view of all its mips; `width` and `height` are mip 0 in pixels. The texture and view are owned by whoever uploaded them and freed through `Resources`.

Fields:

- `texture`: TextureHandle
- `view`: TextureViewHandle
- `width`: UInt32
- `height`: UInt32
- `mip_levels`: UInt32


### def `mip_count`

```mojo
def mip_count(width: Int, height: Int) -> Int
```

Levels in a full mip chain down to 1x1 (`floor(log2(max(width, height))) + 1`).


### def `downsample`

```mojo
def downsample(img: Image) -> Image
```

2x2 box filter (edge pixels of odd sizes are clamped), in the image's own encoding.


### def `upload_image`

```mojo
def upload_image( mut res: Resources, label: String, img: Image, srgb: Bool, mips: Bool = True ) raises -> GpuTexture
```

Uploads an RGBA8 image as a 2D texture with a CPU box-filtered mip chain. `srgb` selects `RGBA8UnormSrgb` (colour data) over `RGBA8Unorm` (linear data); `mips = False` uploads level 0 only. The caller owns the result; raises on a GPU error.


### def `solid_image`

```mojo
def solid_image(r: UInt8, g: UInt8, b: UInt8, a: UInt8) -> Image
```

1x1 RGBA8 image of one colour (used for the default textures).


### struct `DefaultTextures`

```mojo
struct DefaultTextures(ImplicitlyCopyable, Movable)
```

1x1 fallbacks: white (base colour, occlusion, metallic-roughness), flat normal, black emissive.

Fields:

- `white`: GpuTexture
- `normal`: GpuTexture
- `black`: GpuTexture
- `sampler_linear_repeat`: SamplerHandle
- `sampler_nearest_clamp`: SamplerHandle

- **`__init__`** `__init__(out self, mut res: Resources) raises`
