Skip to content

TextureMipChain

A pre-computed CPU mipmap chain ready to upload as a Texture. Build one off the renderer thread (worker, thread pool, async task, Web Worker) so the GPU thread only has to do queue.write_texture calls.

Most callers never need to construct this directly: Renderer::create_texture(bytes) already runs decode and mipmap generation on a background worker on every native target. Reach for TextureMipChain when you want explicit control:

  • share one chain across many textures without rebuilding it,
  • bake mipmap generation into a decode pipeline that already runs on a worker,
  • drive prep on your own thread pool (rayon, Swift Task, Kotlin Dispatchers.Default, Python ThreadPoolExecutor, Web Worker).

Build with TextureMipChain::prepare(input); the input shapes match Renderer::create_texture and Renderer::create_storage_texture. Whether the bytes are decoded depends on size:

  • prepare((bytes, format)) — encoded image bytes (PNG, JPEG, etc.); size is inferred from the decoded image.
  • prepare((bytes, format, size)) — raw pixel bytes already laid out for format at size.

In Swift, Kotlin, JS, and Python, the binding is prepare(bytes, format, size?) with size optional.

Upload by passing the chain to Renderer::create_texture(chain); cross-language users hand the chain handle directly to createTexture.

Supported formats: Rgba8Unorm, Rgba8UnormSrgb, Bgra8Unorm, Bgra8UnormSrgb, R8Unorm, Rg8Unorm, R16Unorm, Rg16Unorm, Rgba16Unorm. Other formats return TextureError::UnsupportedMipmapFormat.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, TextureFormat, TextureMipChain};
let renderer = Renderer::new();
// Encoded image bytes the caller has on hand (could come off a worker).
let png: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
// ... full PNG body ...
1 collapsed line
// Hidden: build a real 1×1 PNG so the doctest doesn't need a fixture.
];
4 collapsed lines
let img = image::DynamicImage::ImageRgba8(image::RgbaImage::from_vec(1, 1, vec![255, 0, 0, 255]).unwrap());
let mut png_buf = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png_buf), image::ImageFormat::Png)?;
let png = png_buf.as_slice();
let chain = TextureMipChain::prepare((png, TextureFormat::Rgba8UnormSrgb))?;
// Upload the chain through the regular create_texture entry point.
let texture = renderer.create_texture(chain).await?;
4 collapsed lines
_ = texture.size();
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Build a mip chain off the renderer thread, then hand it to Renderer::create_texture for a GPU-only upload. The work is pure CPU; call it from any worker, thread pool, async task, or Web Worker.

prepare(input) accepts the same shapes as Renderer::create_texture and Renderer::create_storage_texture. Whether the bytes are decoded depends on size:

  • Encoded: size absent. The bytes are decoded as PNG / JPEG / etc. and the chain inherits the image’s dimensions. Use this when you have an encoded blob from disk, the network, or a decoder that emits encoded data.
  • Raw: size present. The bytes are treated as already laid out for format at size. Use this when your decoder already produced pixel-format-matching bytes (typical for tile-cache pipelines that bake mipmap generation into the same step).

prepare runs synchronously and accepts only sync-friendly inputs: bytes, a DynamicImage, or a file path. URL inputs (which need async fetch), KTX2 inputs (already pre-baked), an existing chain, an existing texture, and empty inputs all return TextureError::InvalidInput with a message pointing at the right entry point.

Supported formats: Rgba8Unorm, Rgba8UnormSrgb, Bgra8Unorm, Bgra8UnormSrgb, R8Unorm, Rg8Unorm, R16Unorm, Rg16Unorm, Rgba16Unorm. Other formats return TextureError::UnsupportedMipmapFormat. Decode failures surface as MalformedImageError; size and byte-count mismatches as InvalidInput.

The cross-language bindings expose prepare(bytes, format, size?) with size optional. Swift and Kotlin add overloads so you call TextureMipChain.prepare(bytes:format:) for the encoded path and TextureMipChain.prepare(bytes:format:size:) for the raw path.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, TextureFormat, TextureMipChain};
5 collapsed lines
// Build a real 1×1 PNG so the doctest doesn't need a fixture.
let img = image::DynamicImage::ImageRgba8(image::RgbaImage::from_vec(1, 1, vec![255, 0, 0, 255]).unwrap());
let mut png_buf = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png_buf), image::ImageFormat::Png)?;
let encoded_png_bytes = png_buf.as_slice();
// Encoded path: pass bytes plus the format you expect.
let chain = TextureMipChain::prepare((encoded_png_bytes, TextureFormat::Rgba8UnormSrgb))?;
1 collapsed line
let raw_rgba = vec![200u8; 8 * 8 * 4];
// Raw path: include the size so prepare skips decoding.
let chain_raw = TextureMipChain::prepare((
raw_rgba.as_slice(),
TextureFormat::Rgba8UnormSrgb,
[8, 8],
))?;
// Upload the chain through the regular create_texture entry point.
let renderer = Renderer::new();
let texture = renderer.create_texture(chain).await?;
5 collapsed lines
_ = texture.size();
_ = chain_raw.level_count();
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Return the wgpu texture format the chain was prepared for. Matches the format argument passed to prepare(...).

use fragmentcolor::{TextureFormat, TextureMipChain};
let pixels: Vec<u8> = vec![200; 4 * 4 * 4];
let chain = TextureMipChain::prepare((
pixels.as_slice(),
TextureFormat::Rgba8UnormSrgb,
[4, 4],
))?;
let _ = chain.format();
1 collapsed line
Ok::<(), Box<dyn std::error::Error>>(())

Return the base level (level 0) dimensions as (width, height). The mip chain has a level for each 1 + floor(log2(max(width, height))).

use fragmentcolor::{TextureFormat, TextureMipChain};
let pixels: Vec<u8> = vec![0; 16 * 16 * 4];
let chain = TextureMipChain::prepare((
pixels.as_slice(),
TextureFormat::Rgba8UnormSrgb,
[16, 16],
))?;
let (width, height) = chain.base_size();
2 collapsed lines
assert_eq!(width, 16);
assert_eq!(height, 16);
let _ = (width, height);
1 collapsed line
Ok::<(), Box<dyn std::error::Error>>(())

Return the tightly-packed bytes for each mip level, level 0 first. Each level has bytes_per_pixel(format) * max(1, base_w >> level) * max(1, base_h >> level) bytes.

This is mostly useful for inspection, debugging, or persisting the chain to disk. The renderer reads the bytes directly when uploading via Renderer::create_texture(chain); you do not need to copy them yourself.

use fragmentcolor::{TextureFormat, TextureMipChain};
let pixels: Vec<u8> = vec![0; 8 * 8 * 4];
let chain = TextureMipChain::prepare((
pixels.as_slice(),
TextureFormat::Rgba8UnormSrgb,
[8, 8],
))?;
let level_zero_bytes = &chain.levels()[0];
1 collapsed line
assert_eq!(level_zero_bytes.len(), 8 * 8 * 4);
let _ = level_zero_bytes;
1 collapsed line
Ok::<(), Box<dyn std::error::Error>>(())

Return the number of mip levels in the chain (always >= 1). For a base size of w x h, this is 1 + floor(log2(max(w, h))).

use fragmentcolor::{TextureFormat, TextureMipChain};
let pixels: Vec<u8> = vec![0; 8 * 8 * 4];
let chain = TextureMipChain::prepare((
pixels.as_slice(),
TextureFormat::Rgba8UnormSrgb,
[8, 8],
))?;
let count = chain.level_count();
1 collapsed line
assert_eq!(count, 4); // 8, 4, 2, 1
let _ = count;
1 collapsed line
Ok::<(), Box<dyn std::error::Error>>(())