Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Textures & the Texture Cache

Nightshade manages GPU textures through a centralized TextureCache with generational indexing and reference counting. Textures can be loaded synchronously from bytes, asynchronously from disk or HTTP, or generated procedurally at runtime.

Texture cache

The TextureCache stores every loaded texture as a TextureEntry (a wgpu texture plus its view plus its sampler) in a GenerationalRegistry. Each texture is identified by a TextureId containing an index and a generation counter. Stale references are detected because the generation increments when a slot is recycled, so an old TextureId will not match the slot's current contents.

Loading textures

The most common way to load a texture is through WorldCommand::LoadTexture.

#![allow(unused)]
fn main() {
world.queue_command(WorldCommand::LoadTexture {
    name: "my_texture".to_string(),
    rgba_data: image_bytes,
    width: 512,
    height: 512,
});
}

The renderer drains the command queue, uploads the RGBA data to the GPU, and stores the texture in the cache under the given name.

Procedural textures

The engine provides three built-in procedural textures loaded at startup via load_procedural_textures().

#![allow(unused)]
fn main() {
load_procedural_textures(world);
}
NameDescription
"checkerboard"Black and white checkerboard pattern
"gradient"Horizontal gradient
"uv_test"UV coordinate visualization

Looking up textures

Find a loaded texture by name.

#![allow(unused)]
fn main() {
let texture_id = texture_cache_lookup_id(&cache, "my_texture");
}

Reference counting

Textures use reference counting for lifecycle management. The cache will not free a texture until every holder has released it.

#![allow(unused)]
fn main() {
texture_cache_add_reference(&mut cache, "my_texture");
texture_cache_remove_reference(&mut cache, "my_texture");
texture_cache_remove_unused(&mut cache);
}

When a texture's reference count reaches zero, texture_cache_remove_unused() frees it.

Dummy textures

If a texture is missing, texture_cache_ensure_dummy() creates a 64x64 purple-and-black checkerboard placeholder. The fallback keeps the renderer from erroring out and makes missing assets obvious in the viewport.

Async texture loading

For loading textures without blocking the main thread, use the TextureLoadQueue system.

Setup

#![allow(unused)]
fn main() {
use nightshade::ecs::texture_loader::*;

struct MyState {
    queue: SharedTextureQueue,
    loading_state: AssetLoadingState,
}

fn initialize(&mut self, world: &mut World) {
    self.queue = create_shared_queue();

    queue_texture_from_path(&self.queue, "assets/textures/albedo.png");
    queue_texture_from_path(&self.queue, "assets/textures/normal.png");

    self.loading_state = AssetLoadingState::new(2);
}
}

Processing each frame

#![allow(unused)]
fn main() {
fn run_systems(&mut self, world: &mut World) {
    let status = process_and_load_textures(
        &self.queue,
        world,
        &mut self.loading_state,
        4,
    );

    if status == AssetLoadingStatus::Complete {
        // All textures loaded
    }
}
}

Loading progress

Track progress for a loading screen.

#![allow(unused)]
fn main() {
let progress = self.loading_state.progress(); // 0.0 to 1.0
let is_done = self.loading_state.is_complete();
let loaded = self.loading_state.loaded_textures;
let failed = self.loading_state.failed_textures;
}

Platform behavior

PlatformLoading Method
DesktopSynchronous file read from disk
WASMAsync HTTP fetch via ehttp

Asset search paths

Configure the directories where texture files are searched.

#![allow(unused)]
fn main() {
set_asset_search_paths(vec![
    "assets/".to_string(),
    "content/textures/".to_string(),
]);

queue_texture_from_path(&queue, "player.png");
// Searches: assets/player.png, content/textures/player.png
}

Material textures

PBR materials reference textures by name through MaterialRef.

#![allow(unused)]
fn main() {
let material = Material {
    base_texture: Some("albedo".to_string()),
    normal_texture: Some("normal_map".to_string()),
    metallic_roughness_texture: Some("metallic_roughness".to_string()),
    ..Default::default()
};
}

See Materials for the full PBR material workflow.