> Cinder 0.1.0 documentation: cinder.assets API
> Canonical page: https://cinder.ash10.dev/docs/api/assets/ · Source: `docs/api/assets.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.assets`


## `cinder.assets`

Source: `cinder/assets/__init__.mojo`

Assets: glTF import into engine meshes, materials, textures, skins and scenes.


## `cinder.assets.cook`

Source: `cinder/assets/cook.mojo`

The asset cooker: glTF sources become cooked meshes, materials and animation clips. Cooking is incremental and keyed on content: the manifest (`manifest.txt` in the output directory) records, per logical asset, the FNV-1a hash of the source file and the output file name. A source whose hash is unchanged and whose outputs still exist is skipped. Output files are named by the asset id of their logical path, so ids are stable across cooks and machines. Textures are cooked by `cook_textures` (KTX2) in the native-backed path; this module records the image references materials need (`<prefix>#image<N>`).

### struct `ManifestEntry`

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

One manifest line: logical asset path, FNV-1a source hash (`hex64`) and output file name. `file` is relative to the output directory and named by `asset_file_name(logical, ...)`.

Fields:

- `logical`: String
- `source_hash`: String
- `file`: String


### struct `CookManifest`

```mojo
struct CookManifest(Movable)
```

The cook manifest (`<out_dir>/manifest.txt`), shared by the cooker and `AssetServer`. One entry per logical asset; lookups are linear scans. Re-cooking a source drops (`prune`) the entries of outputs it no longer produces.

Fields:

- `entries`: List[ManifestEntry]

- **`__init__`** `__init__(out self)`
- **`load`** `load(path: String) raises -> CookManifest`  
  Reads a manifest; a missing file yields an empty one. Lines without exactly three space-separated fields (`<logical> <hash> <file>`) are skipped, so logical paths must not contain spaces. Raises when the file exists but cannot be read.
- **`save`** `save(self, path: String) raises`  
  Writes a `<logical> <hash> <file>` line per entry, replacing `path`; raises on error.
- **`find`** `find(self, logical: String) -> Int`  
  Index of the entry for `logical`, or -1.
- **`record`** `record(mut self, logical: String, source_hash: String, file: String)`  
  Replaces the entry for `logical`, or appends one when there is none.
- **`prune`** `prune( mut self, root: String, produced: List[String] ) -> List[ManifestEntry]`  
  Removes the entries under `root` (logical paths starting with it, `root` itself excepted) that are not in `produced`, and returns them in manifest order.
- **`up_to_date`** `up_to_date( self, logical: String, source_hash: String, out_dir: String ) -> Bool`  
  Whether `logical` is recorded with `source_hash` and its file exists in `out_dir`.

### struct `CookReport`

```mojo
struct CookReport(Movable)
```

What one `cook_gltf` call did. `cooked` counts outputs written; `skipped` counts outputs found up to date when the whole source was skipped (the two never mix in one report).

Fields:

- `cooked`: Int
- `skipped`: Int
- `outputs`: List[String] — Logical paths produced (or found up to date), in cook order.

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

### def `cook_gltf`

```mojo
def cook_gltf( lib: NativePtr, source_path: String, prefix: String, out_dir: String, mut manifest: CookManifest, ) raises -> CookReport
```

Cooks every mesh primitive, material and animation of a glTF file. `prefix` is the logical name of the source (e.g. `models/Fox`); outputs are `<prefix>#mesh<i>/prim<p>`, `<prefix>#material<m>` and `<prefix>#anim<a>`.



## `cinder.assets.cooked`

Source: `cinder/assets/cooked.mojo`

Cooked asset formats (little-endian binary, versioned, bounds-checked on read). - `.cmesh`: engine vertex stream (56-byte vertices), optional skin stream, u32 indices with LOD ranges, bounds, morph deltas. Written by the cooker from glTF primitives (tangents generated, LODs built with meshoptimizer), loaded by `AssetServer` without any further processing. - `.cclip`: an animation clip (channels of keyframes targeting node indices). Asset ids are the FNV-1a hash of the logical asset path (`models/Fox.glb#mesh0/prim1`), so they are stable across cooks and machines.

- `comptime MESH_MAGIC = "CMSH"` — First four bytes of a `.cmesh` file.
- `comptime MESH_VERSION = 1` — `.cmesh` layout version; `read_mesh` rejects any other (re-cook, there are no migrations).
- `comptime CLIP_MAGIC = "CCLP"` — First four bytes of a `.cclip` file.
- `comptime CLIP_VERSION = 1` — `.cclip` layout version; `read_clip` rejects any other.
### def `asset_id`

```mojo
def asset_id(logical_path: String) -> UInt64
```

Stable 64-bit id of a logical asset path: its FNV-1a hash.


### def `asset_file_name`

```mojo
def asset_file_name(logical_path: String, extension: String) -> String
```

`<16 hex digits>.<extension>`: content-addressed by logical path, not by contents.


### struct `CookedMesh`

```mojo
struct CookedMesh(Movable)
```

What `.cmesh` holds. Vertex and index bytes are already in GPU layout.

Fields:

- `vertex_count`: UInt32
- `vertex_bytes`: List[UInt8]
- `skin_bytes`: List[UInt8]
- `index_bytes`: List[UInt8]
- `lods`: List[MeshLod]
- `bounds_min`: Vec3
- `bounds_max`: Vec3
- `morph_target_count`: UInt32
- `morph_bytes`: List[UInt8]
- `material`: String — Logical path of the cooked material, or empty.

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

### def `cook_mesh`

```mojo
def cook_mesh( data: MeshData, lod_indices: List[List[UInt32]], material: String ) -> CookedMesh
```

Packs a validated `MeshData` and its LOD index lists.


### def `write_mesh`

```mojo
def write_mesh(mesh: CookedMesh) -> List[UInt8]
```

Serialises a `CookedMesh` to `.cmesh` bytes (version MESH_VERSION, little-endian). Layout: magic, version, vertex count, byte lengths of the vertex, skin and index streams, LOD count, bounds min and max, morph target count and byte length, material path (u32 length + bytes), the LOD table (first index, index count, error), then the vertex, skin, index and morph streams.


### struct `Cursor`

```mojo
struct Cursor(Movable)
```

Bounds-checked little-endian reader over an owned byte buffer (the cooked-format readers). Every read raises when it would run past the end of the data.

Fields:

- `data`: List[UInt8]
- `pos`: Int

- **`__init__`** `__init__(out self, var data: List[UInt8])`
- **`remaining`** `remaining(self) -> Int`  
  Bytes left after the read position.
- **`u8`** `u8(mut self) raises -> UInt8`  
  Reads one byte; raises at the end of the data.
- **`u32`** `u32(mut self) raises -> UInt32`  
  Reads a little-endian u32; raises when fewer than 4 bytes remain.
- **`f32`** `f32(mut self) raises -> Float32`  
  Reads a float32 from the bit pattern of a little-endian u32.
- **`bytes`** `bytes(mut self, count: Int) raises -> List[UInt8]`  
  Copies the next `count` bytes; raises if `count` is negative or above `remaining()`.
- **`string`** `string(mut self) raises -> String`  
  Reads a u32 byte length and that many bytes of UTF-8 text; raises when the length exceeds the data or the bytes are not valid UTF-8.
- **`magic`** `magic(mut self, expected: String) raises`  
  Reads the bytes of `expected`; raises `bad magic` at the first mismatch.

### def `read_mesh`

```mojo
def read_mesh(var data: List[UInt8]) raises -> CookedMesh
```

Parses `.cmesh` bytes into a `CookedMesh`, taking ownership of `data`. Raises on a wrong magic or version; stream lengths that disagree with the vertex count (56 bytes per vertex, 24 per skin vertex, 32 per vertex and morph target); index bytes not a multiple of 4; a LOD count outside 1..16; a LOD range outside the index buffer or not a multiple of 3; any index >= vertex count; truncated data or trailing bytes.


### struct `ClipChannel`

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

Keyframes for one node property. `path`: 0 translation, 1 rotation, 2 scale, 3 weights. `interpolation`: 0 linear, 1 step, 2 cubic spline. `values` holds `components` floats per key (3 per cubic-spline key: in-tangent, value, out-tangent).

Fields:

- `node`: Int
- `path`: Int
- `interpolation`: Int
- `components`: Int
- `times`: List[Float32]
- `values`: List[Float32]


### struct `Clip`

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

A cooked animation clip: name, duration (the latest keyframe time) and its channels.

Fields:

- `name`: String
- `duration`: Float32
- `channels`: List[ClipChannel]

- **`__init__`** `__init__(out self, name: String)`

### def `write_clip`

```mojo
def write_clip(clip: Clip) -> List[UInt8]
```

Serialises a clip to `.cclip` bytes (version CLIP_VERSION, little-endian). Layout: magic, version, name, duration, channel count, then per channel its node, path, interpolation, components, key count and value count followed by the times and values.


### def `read_clip`

```mojo
def read_clip(var data: List[UInt8]) raises -> Clip
```

Parses `.cclip` bytes into a `Clip`, taking ownership of `data`. Raises on a wrong magic or version, more than 100000 channels, a path above 3, an interpolation above 2, components outside 1..64, a value count other than keys * components (* 3 for cubic spline), decreasing keyframe times, truncated data or trailing bytes. Node indices are not checked.


- `comptime MATERIAL_MAGIC = "CMAT"` — First four bytes of a `.cmat` file.
- `comptime MATERIAL_VERSION = 1` — `.cmat` layout version; `read_material` rejects any other.
### struct `CookedMaterial`

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

Material parameters plus the logical image path per map slot (empty when absent).

Fields:

- `material`: Material
- `images`: Array[String, 5]
- `linear_filter`: Bool
- `address_mode`: UInt32

- **`__init__`** `__init__(out self, var material: Material)`

### def `write_material`

```mojo
def write_material(cm: CookedMaterial) -> List[UInt8]
```

Serialises a `CookedMaterial` to `.cmat` bytes (version MATERIAL_VERSION, little-endian). Layout: magic, version, name, factors, alpha mode and cutoff, double-sided flag, UV transform, then per map slot (5) the texcoord set and image path, then the sampler's linear-filter flag and address mode.


### def `read_material`

```mojo
def read_material(var data: List[UInt8]) raises -> CookedMaterial
```

Parses `.cmat` bytes into a `CookedMaterial`, taking ownership of `data`. Raises on a wrong magic or version, an alpha mode above 2, truncated data or trailing bytes. Only the parameters and image paths are read; `AssetServer` resolves the textures and creates the sampler.



## `cinder.assets.gltf`

Source: `cinder/assets/gltf.mojo`

glTF 2.0 import through cgltf: meshes (with tangents, second UV set, skinning, morph targets), metallic-roughness materials (KHR_texture_transform, KHR_materials_emissive_strength), images (file, data URI or GLB buffer view), nodes, skins and scenes. `load_gltf` produces a CPU-side `GltfAsset`; `instantiate` uploads it through a `Renderer` and fills a `RenderScene` with one instance per (node, primitive), joints at the bind pose and the default morph weights. Malformed files raise; they never crash (cgltf validates the structure and every accessor read is bounds-checked by cgltf).

### struct `GltfImage`

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

An image decoded on the CPU by `load_gltf`, with its glTF name (empty when unnamed).

Fields:

- `name`: String
- `image`: Image


### struct `GltfTexture`

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

A glTF texture: image index (-1 without one) plus sampler settings. `linear_filter` is True for a linear or unspecified magnification filter; `address_mode` is the WGPU address mode from the sampler's `wrap_s` (repeat when there is no sampler).

Fields:

- `image`: Int
- `linear_filter`: Bool
- `address_mode`: UInt32


### struct `GltfMaterial`

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

Engine material parameters plus the asset texture index per map (-1 when absent).

Fields:

- `material`: Material
- `textures`: Array[Int, 5]

- **`__init__`** `__init__(out self, var material: Material)`

### struct `GltfPrimitive`

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

A triangle primitive as validated `MeshData` and its material index (-1 when none). Point, line and strip primitives are dropped on import.

Fields:

- `mesh`: MeshData
- `material`: Int


### struct `GltfMesh`

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

A glTF mesh: name, its triangle primitives and the mesh's default morph weights.

Fields:

- `name`: String
- `primitives`: List[GltfPrimitive]
- `weights`: List[Float32]


### struct `GltfNode`

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

A scene node; every reference is an index into `GltfAsset`, -1 when absent. `local` and `world` are computed by cgltf at import (the world matrix walks the parents); `weights` are the node's morph weight overrides (empty when it has none).

Fields:

- `name`: String
- `parent`: Int
- `children`: List[Int]
- `mesh`: Int
- `skin`: Int
- `local`: Mat4
- `world`: Mat4
- `weights`: List[Float32]


### struct `GltfSkin`

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

Joint node indices and an inverse bind matrix each (identity where the file has none).

Fields:

- `joints`: List[Int]
- `inverse_bind`: List[Mat4]


### struct `GltfChannel`

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

One animated node property. `path`: 0 translation, 1 rotation, 2 scale, 3 weights; `interpolation`: 0 linear, 1 step, 2 cubic spline (values hold in-tangent, value, out-tangent).

Fields:

- `node`: Int
- `path`: Int
- `interpolation`: Int
- `components`: Int
- `times`: List[Float32]
- `values`: List[Float32]


### struct `GltfAnimation`

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

An animation: name, duration (latest keyframe time over its channels) and channels. Channels without a sampler, target node or supported target path are skipped on import.

Fields:

- `name`: String
- `duration`: Float32
- `channels`: List[GltfChannel]

- **`__init__`** `__init__(out self, name: String)`

### struct `SceneBounds`

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

Axis-aligned box from `GltfAsset.world_bounds`; all zero when the asset has no meshes.

Fields:

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

- **`centre`** `centre(self) -> Vec3`  
  Midpoint of the box.
- **`radius`** `radius(self) -> Float32`  
  Half the diagonal: the radius of the sphere around `centre` that encloses the box.

### struct `GltfAsset`

```mojo
struct GltfAsset(Movable)
```

Everything `load_gltf` imported from one file, CPU side; cross references are list indices. `roots` holds the default scene's root nodes, or every parentless node when the file has no default scene.

Fields:

- `path`: String
- `images`: List[GltfImage]
- `textures`: List[GltfTexture]
- `materials`: List[GltfMaterial]
- `meshes`: List[GltfMesh]
- `nodes`: List[GltfNode]
- `skins`: List[GltfSkin]
- `animations`: List[GltfAnimation]
- `roots`: List[Int]

- **`__init__`** `__init__(out self, path: String)`
- **`world_bounds`** `world_bounds(self) -> SceneBounds`  
  Axis-aligned bounds of every mesh instance in world space. Skinned meshes are posed on the CPU with their bind-pose joint matrices (what the GPU draws at rest).

### def `load_gltf`

```mojo
def load_gltf(lib: NativePtr, path: String) raises -> GltfAsset
```

Parses a .gltf or .glb file (and its buffers and images) into a `GltfAsset`.


### struct `UploadedGltf`

```mojo
struct UploadedGltf(Movable)
```

Renderer ids for an uploaded asset: `mesh_ids[mesh][primitive]`, `material_ids[material]` and a default material for primitives without one.

Fields:

- `mesh_ids`: List[List[Int]]
- `material_ids`: List[Int]
- `default_material`: Int

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

### def `upload_gltf`

```mojo
def upload_gltf( mut renderer: Renderer, mut res: Resources, asset: GltfAsset, lods: Bool = True, ) raises -> UploadedGltf
```

Uploads every image, material and primitive of `asset` to the renderer. Base colour and emissive images are sRGB; the rest are linear. Materials without textures share the renderer's default textures.


### def `instantiate`

```mojo
def instantiate( mut renderer: Renderer, mut res: Resources, asset: GltfAsset, mut scene: RenderScene, lods: Bool = True, ) raises -> Int
```

Uploads the asset and appends its instances, joints and morph weights to `scene`. Returns the number of instances added. Base colour and emissive images are sRGB; the rest are linear. Materials without textures share the renderer's default textures.



## `cinder.assets.runtime`

Source: `cinder/assets/runtime.mojo`

Runtime asset server: asynchronous loading of cooked assets on the job system, reference counting, and hot reload. Loading a cooked mesh has two halves: a job reads and parses the file (CPU only, any thread) into a heap slot; `update()` on the main thread uploads finished slots to the GPU through the renderer and flips the handle to ready. Nothing GPU-related happens off the main thread. Each logical asset is loaded once and shared; `release` drops a reference and frees the GPU mesh when the last one goes. `check_for_changes` reloads assets whose cooked file changed on disk and swaps the GPU mesh (or material) under the same renderer index, so instances keep working.

### struct `AssetState`

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

Load state of an `AssetServer` record: loading, ready or failed.

Fields:

- `value`: Int

- **`loading`** `loading() -> AssetState`  
  Load started and not finished; a material also stays here while its textures load.
- **`ready`** `ready() -> AssetState`  
  Loaded; the accessors return data.
- **`failed`** `failed() -> AssetState`  
  The load failed (see `AssetServer.error`) or the record was released.

### struct `AssetRecord`

```mojo
struct AssetRecord(Movable)
```

One logical asset known to an `AssetServer` and its loaded result. `kind` is 0 mesh, 1 clip, 2 texture, 3 material. Records are never removed: releasing the last reference leaves the record with state failed and error `released` (and an empty `logical`, so loading the path again makes a new record).

Fields:

- `logical`: String
- `file`: String
- `kind`: Int
- `state`: AssetState
- `refcount`: Int
- `mesh_index`: Int — Renderer mesh index once ready (meshes).
- `clip`: Optional[Clip]
- `texture_view`: TextureViewHandle
- `texture`: TextureHandle
- `material_index`: Int — Renderer material index once built (materials); kept across hot reloads.
- `sampler`: SamplerHandle — Material: the sampler the server created for it (null until built).
- `cooked_material`: Optional[CookedMaterial]
- `pending_textures`: List[Int] — Material: asset indices of textures that must be ready before the material is built (one reference each, held until the material is released or reloaded).
- `retired_textures`: List[Int] — Material being hot reloaded: the previous version's texture references, released once the new version is built (the renderer still draws the old one until then).
- `error`: String
- `mtime`: Int
- `slot_index`: Int
- `reloads`: Int

- **`__init__`** `__init__(out self, logical: String, file: String, kind: Int)`

### struct `AssetServer`

```mojo
struct AssetServer(Movable)
```

Loads cooked assets listed in `<root>/manifest.txt`, shared by logical path and refcounted. Jobs only read, parse and transcode files; `update` does every GPU upload. All methods are main-thread calls (there are no locks). Indices from `load_*` stay valid for the server's lifetime. Dropping the server waits for all jobs but does not free the GPU resources of assets that were never released.

Fields:

- `lib`: NativePtr
- `jobs`: Pointer[JobSystem, MutUntrackedOrigin]
- `renderer`: Pointer[Renderer, MutUntrackedOrigin]
- `root`: String
- `manifest`: CookManifest
- `records`: List[AssetRecord]
- `counter`: Counter
- `in_flight`: Int
- `slots`: List[List[_LoadSlot]] — Load slots as single-element lists: their heap storage never moves, so jobs can hold pointers into them while this list grows.
- `slot_busy`: List[Bool]

- **`__init__`** `__init__( out self, lib: NativePtr, jobs: Pointer[JobSystem, MutUntrackedOrigin], renderer: Pointer[Renderer, MutUntrackedOrigin], root: String, ) raises`
- **`load_mesh`** `load_mesh(mut self, logical: String) raises -> Int`  
  Returns the asset index (also a reference). Loading starts immediately on a job. Raises when `logical` is not in the manifest or is known as another kind of asset. A known path returns its existing record (refcount + 1); one whose load failed is loaded again.
- **`load_clip`** `load_clip(mut self, logical: String) raises -> Int`  
  Returns the asset index of a cooked clip (also a reference); a job parses the file. Same sharing, kind check and retry of failed loads as `load_mesh`.
- **`load_texture`** `load_texture(mut self, logical: String) raises -> Int`  
  Returns the asset index of a cooked KTX2 texture (also a reference). The job reads and transcodes it (BC7 when the device supports block compression, else RGBA8); `update` uploads it. Same sharing, kind check and retry of failed loads as `load_mesh`.
- **`load_material`** `load_material(mut self, logical: String) raises -> Int`  
  Loads a cooked material and every texture it references; ready when all of them are. Same sharing, kind check and retry of failed loads as `load_mesh`.
- **`material_index`** `material_index(self, index: Int) raises -> Int`  
  Renderer material index of a ready material; raises otherwise.
- **`texture_view`** `texture_view(self, index: Int) raises -> TextureViewHandle`  
  View of a ready texture; raises otherwise. A hot reload destroys it for a new one.
- **`update`** `update(mut self) raises -> Int`  
  Main thread: finishes loads whose job completed (GPU upload for meshes). Returns how many assets became ready or failed this call.
- **`wait_all`** `wait_all(mut self) raises`  
  Blocks until every pending load is applied (tests, loading screens).
- **`state`** `state(self, index: Int) -> AssetState`  
  State of the record at `index` (failed once released).
- **`mesh_index`** `mesh_index(self, index: Int) raises -> Int`  
  Renderer mesh index of a ready asset, stable across hot reloads; raises when not ready. The kind is not checked: a ready asset that is not a mesh returns -1.
- **`clip`** `clip(self, index: Int) raises -> Clip`  
  A copy of a ready clip with all its keyframes; raises when not a ready clip.
- **`error`** `error(self, index: Int) -> String`  
  Message of a failed load (`released` after the last release), empty otherwise.
- **`refcount`** `refcount(self, index: Int) -> Int`  
  Outstanding references to the record at `index`; 0 once released.
- **`release`** `release(mut self, index: Int) raises`  
  Drops one reference; the last one frees what the asset holds on the GPU (mesh, texture, renderer material and its sampler), releases a material's textures and forgets the asset.
- **`check_for_changes`** `check_for_changes(mut self) -> Int`  
  Starts a reload for every ready asset whose cooked file changed. Returns how many.
- **`live_count`** `live_count(self) -> Int`  
  Number of records with a positive refcount.


## `cinder.assets.textures`

Source: `cinder/assets/textures.mojo`

KTX2 textures: cooking (BasisU ETC1S or UASTC with full mip chains) and runtime loading (transcode to BC7 when the device has block compression, otherwise to RGBA8), through libktx.

- `comptime VK_FORMAT_R8G8B8A8_UNORM: UInt32 = 37` — Vulkan format id for linear RGBA8: written for non-sRGB images, accepted when uncompressed.
- `comptime VK_FORMAT_R8G8B8A8_SRGB: UInt32 = 43` — Vulkan format id for sRGB RGBA8: written for sRGB images and read back as "is sRGB".
- `comptime VK_FORMAT_BC7_SRGB_BLOCK: UInt32 = 146` — Vulkan format id libktx reports after transcoding an sRGB texture to BC7.
### def `cook_texture_ktx2`

```mojo
def cook_texture_ktx2( lib: NativePtr, img: Image, path: String, srgb: Bool, normal_map: Bool, uastc: Bool, quality: Int = 128, ) raises
```

Writes `img` with a full mip chain as a BasisU-compressed KTX2 file. `uastc` picks the high-quality UASTC codec (normal maps, detail textures); otherwise ETC1S at `quality` (1..255). sRGB images carry the sRGB transfer function in the data format descriptor so loaders choose an sRGB GPU format.


### struct `LoadedTexture`

```mojo
struct LoadedTexture(Movable)
```

A KTX2 file transcoded on the CPU: one byte list per mip level, ready for `upload_loaded`.

Fields:

- `width`: Int
- `height`: Int
- `levels`: List[List[UInt8]]
- `bc7`: Bool
- `srgb`: Bool

- **`__init__`** `__init__(out self, width: Int, height: Int, bc7: Bool, srgb: Bool)`

### def `load_ktx2`

```mojo
def load_ktx2( lib: NativePtr, path: String, prefer_bc7: Bool ) raises -> LoadedTexture
```

Reads and transcodes a KTX2 file (CPU only; safe on a job thread).


### def `upload_loaded`

```mojo
def upload_loaded( mut res: Resources, label: String, loaded: LoadedTexture ) raises -> Tuple[TextureHandle, TextureViewHandle]
```

Creates the GPU texture (BC7 or RGBA8, sRGB variants when flagged) and uploads every level.
