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, asynchronously, or generated procedurally.

Texture Cache

The TextureCache stores all loaded textures as TextureEntry values (wgpu texture + view + sampler) in a GenerationalRegistry. Each texture is identified by a TextureId containing an index and generation counter, ensuring stale references are detected.

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 processes this command and uploads the RGBA data to the GPU. The texture is stored in the cache under the given name.

Procedural Textures

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

#![allow(unused)]
fn main() {
load_procedural_textures(world);
}

This creates three textures:

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:

#![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() will free it.

Dummy Textures

If a texture is missing, texture_cache_ensure_dummy() creates a 64x64 purple-and-black checkerboard placeholder. This prevents rendering errors from missing assets.

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 loading progress for loading screens:

#![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 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
}

Sprite Texture Atlas

Sprites use a separate texture atlas rather than the main texture cache. The atlas is a single large GPU texture divided into a grid of slots.

ConstantValue
SPRITE_ATLAS_TOTAL_SLOTS128
SPRITE_ATLAS_SLOT_SIZE512 x 512 pixels

Upload textures to specific atlas slots via WorldCommand::UploadSpriteTexture:

#![allow(unused)]
fn main() {
world.queue_command(WorldCommand::UploadSpriteTexture {
    slot: 0,
    rgba_data: image_bytes,
    width: 256,
    height: 256,
});
}

The Sprite component references textures by their slot index. See Sprites for details.

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.