Skip to content

Renderer

The Renderer is the main entry point for FragmentColor and normally the first object you create.

It is used to render Shaders and Passes (single passes or any iterable of them) to a Target (canvas, window, or texture).

The Renderer internals are lazily initialized when the user creates a Target.

See the constructor Renderer::new() description below for details.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Shader, Renderer, Target};
let renderer = Renderer::new();
// Use your platform's windowing system to create a window
let window = fragmentcolor::headless_window([800, 600]);
// Create a Target from it
let target = renderer.create_target(window).await?;
let texture_target = renderer.create_texture_target([16, 16]).await?;
// RENDERING
renderer.render(&Shader::default(), &texture_target)?;
// That's it. Welcome to FragmentColor!
7 collapsed lines
let s = target.size();
assert_eq!([s.width, s.height], [800, 600]);
let s2 = texture_target.size();
assert_eq!([s2.width, s2.height], [16, 16]);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Create a new Renderer.

The renderer’s GPU adapter and device are initialized lazily on the first target you create, so the same Renderer works whether you end up rendering offscreen or attaching it to a window. By the time render() is called the GPU resources are already in place. render requires a Target, and the only way to build one is through this renderer:

  • renderer.create_target(Window) for an on-screen target, or
  • renderer.create_texture_target([w, h]) for offscreen rendering.
1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, Target};
let renderer = Renderer::new();
let texture_target = renderer.create_texture_target([16, 16]).await?;
5 collapsed lines
let s = texture_target.size();
assert_eq!([s.width, s.height], [16, 16]);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Renderer::create_target(target: Canvas | Window)

Section titled “Renderer::create_target(target: Canvas | Window)”

Creates a Target attached to a platform-specific canvas or window.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, Target};
let renderer = Renderer::new();
// Use your platform's windowing system to create a window.
// We officially support Winit. Check the examples folder for details.
let window = fragmentcolor::headless_window([800, 600]);
let target = renderer.create_target(window).await?;
5 collapsed lines
let s = target.size();
assert_eq!([s.width, s.height], [800, 600]);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Renderer::create_texture_target(size: [u32; 2])

Section titled “Renderer::create_texture_target(size: [u32; 2])”

Render to an offscreen texture without a Window or Canvas.

This is useful for tests, server-side rendering, or running examples in CI.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, Shader, Target};
let renderer = Renderer::new();
// Create an offscreen texture target with a size of 64x64 pixels.
let target = renderer.create_texture_target([64, 64]).await?;
renderer.render(&Shader::default(), &target)?;
// get the rendered image
let image = target.get_image().await;
5 collapsed lines
// RGBA8
assert_eq!(image.len(), 64 * 64 * 4);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Create a Texture from bytes, a file path, a URL, a KTX2 container, or a pre-built Mipmap.

When you pass a size, the bytes are treated as raw pixels in the chosen format. Without a size, the bytes (or the file/URL contents) are decoded as PNG, JPEG, BMP, HDR, etc.

Decode and mipmap generation run on a background worker on native platforms. On the web they run inline on the caller’s thread; move heavy decode into a Web Worker if you need parallelism.

  • Rust: create_texture(input). input accepts bare bytes, (bytes, [w, h]), (bytes, format), (bytes, options), a path, a URL, a Mipmap, or a KTX2 input.
  • JS: await renderer.createTexture(input, options?). input is Uint8Array / ArrayBuffer / URL string / CSS selector / HTMLImageElement / HTMLCanvasElement / OffscreenCanvas / ImageData / a Mipmap handle. options is { size?, format?, mipmaps? }; when size is set, input is read as raw pixel bytes.
  • Python: renderer.create_texture(input, size=None, format=None, mipmaps=None). input is bytes / list[int] / str (path) / numpy ndarray[H, W, C] / Mipmap. Numpy arrays fill in size for you.
  • Swift / Kotlin: try await renderer.createTexture(bytes) or renderer.createTexture(chain). Overloads in the binding wrap the underlying enum, so you write the natural call.
Form Treatment
bytes (no size) Encoded image; decoded internally (PNG, JPEG, BMP, HDR, etc.).
(bytes, size) / options with size set Raw pixel bytes; bpp(format) * width * height long, no decode.
(bytes, format) / options with format set Encoded image reinterpreted as format (e.g. Rgba8Unorm for normal-map data, R16Unorm for 16-bit grayscale).
path / URL File or HTTP fetch, then decoded.
Mipmap Pre-built CPU mip chain; GPU-only upload. Build via Mipmap::build on a worker thread.
Ktx2Bytes / Ktx2Path / Ktx2Url KTX2 container (BC / ETC2 / ASTC / uncompressed); the file’s declared format and pre-baked mip chain win.

A full mipmap chain is generated for source images by default (options.mipmaps = true). Set to false to skip the CPU work for textures that won’t be sampled at distance.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Renderer;
let renderer = Renderer::new();
let image = std::fs::read("logo.png")?;
let tex = renderer.create_texture(&image[..]).await?;
4 collapsed lines
_ = tex.size();
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Create a storage-class texture for compute shaders, image store/load, or as a render target. The input shapes mirror Renderer::create_texture and Mipmap::build.

Common forms:

  • (size, format): empty storage texture, no initial data.
  • (size, format, bytes): storage texture pre-seeded with bytes.
  • A full TextureInput { data, options } literal: pass options.usage (via TextureOptions::with_usage(...)) for non-default usage flags.

size is required; storage textures have no source to infer dimensions from. Missing it returns TextureError::InvalidInput.

options.usage overrides the default mask of STORAGE | TEXTURE | COPY_SRC | COPY_DST. The bindings expose it as a u32 bitmask, so the same flag values work in every language.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, TextureFormat};
let r = Renderer::new();
// Empty storage texture.
let tex = r.create_storage_texture(([64, 64], TextureFormat::Rgba)).await?;
1 collapsed line
_ = tex;
// Pre-seeded with bytes.
let pixels = vec![0; 64 * 64 * 4];
let tex2 = r
.create_storage_texture(([64, 64], TextureFormat::Rgba, pixels))
.await?;
4 collapsed lines
_ = tex2;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Create a depth texture using Depth32Float.

The created depth texture inherits the renderer’s current sample count:

  • If you called create_target(window) (surface-backed), it matches the negotiated MSAA (e.g., 2×/4×) for that surface.
  • If you are rendering offscreen via create_texture_target, it defaults to 1.

This ensures the depth attachment sample_count matches the pass sample_count. If you attach a depth texture with a different sample_count than the pass, rendering will return a descriptive validation error.

use fragmentcolor::Renderer;
let r = Renderer::new();
let depth = r.create_depth_texture([800, 600]);

Explicitly remove a texture from the renderer’s registry.

  • Call this when you replace a texture, stop a video stream, or tear down a scene, to release GPU memory.
  • If the texture is still referenced elsewhere, it will remain alive until all strong references are dropped.
1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, TextureFormat};
let renderer = Renderer::new();
let texture = renderer.create_storage_texture(([16, 16], TextureFormat::Rgba)).await?;
let id = *texture.id();
renderer.unregister_texture(id)?;
3 collapsed lines
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Read back the mip-0 contents of a registered texture as tightly-packed bytes in the texture’s native format.

  • Works on native and on the web; the call awaits the GPU readback mapping.
  • Equivalent to calling [Texture::get_image] on the texture handle. Use this entry point when you only kept the TextureId around.
  • The texture must have COPY_SRC usage. Creation helpers like create_storage_texture enable it by default.
1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, TextureFormat};
let renderer = Renderer::new();
let texture = renderer.create_storage_texture(([64, 64], TextureFormat::Rgba)).await?;
texture.write(&vec![0; 64 * 64 * 4])?;
let bytes = renderer.read_texture(*texture.id()).await?;
4 collapsed lines
assert_eq!(bytes.len(), 64 * 64 * 4);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Wrap a native platform video-frame source as an external texture so a WGSL shader can sample it directly via texture_external / textureSampleBaseClampToEdge, without an intermediate CPU upload.

The source argument is platform-specific:

  • Web: HTMLVideoElement (or anything that decodes into one).
  • iOS: a CVPixelBuffer-backed handle (passed as a raw UInt64 pointer over the uniffi boundary).
  • Android: a SurfaceTexture handle (passed as a raw ULong pointer).

Currently returns an error on every platform. The public signature is stable; native-side decode hookup is on the roadmap. Track support via the typed RendererError you get back.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Renderer;
let renderer = Renderer::new();
// platform-specific source handle passed here
3 collapsed lines
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Realize every pending GPU upload referenced by a renderable. Material’s texture setters (base_color_texture, metallic_roughness_texture, …) accept paths, bytes, URLs, and DynamicImage values in addition to already-uploaded Texture handles; the path / bytes / URL variants are stored on the Material’s Shader as pending uploads. load walks every Shader the renderable visits, drains its pending list, calls Renderer::create_texture for each, and writes the resulting Texture into the matching uniform.

Renderer::render calls load automatically the first time it sees a renderable with pending uploads, so explicit load is optional. Reach for it when you want to amortize the decode + upload cost outside the render loop, for example in a loading screen that prepares the scene before the first frame goes out.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Material, Mesh, Model, Renderer, Scene, Vertex};
let renderer = Renderer::new();
let target = renderer.create_texture_target([64, 64]).await?;
let mesh = Mesh::new();
mesh.add_vertex(
Vertex::pbr([0.0, 0.5, 0.0]).set(Vertex::UV0, [0.5, 1.0]),
);
// Raw 2×2 RGBA pixel bytes — uploaded lazily by `Renderer::load` below.
// In practice the loader hands the setter encoded PNG/JPEG bytes (from a
// BIN chunk) or a file path (from a URI); the same `Into<TextureInput>`
// vocabulary covers all of them.
let red_pixels: Vec<u8> = vec![
255, 0, 0, 255, 0, 255, 0, 255,
0, 0, 255, 255, 255, 255, 255, 255,
];
let red_tex = renderer.create_texture((red_pixels, [2, 2])).await?;
let material = Material::pbr().base_color_texture(&red_tex);
let model = Model::new(mesh, material);
let scene = Scene::new();
scene.add(&model)?;
// Eager prewarm — uploads the pending texture(s) so the next render is
// GPU-only.
renderer.load(&scene).await?;
renderer.render(&scene, &target)?;
3 collapsed lines
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Round-trip the GPU-side bytes of a Shader’s storage binding back to the CPU. Submits any pending uploads, copies the storage buffer into a pooled readback staging buffer, maps it, and returns the bytes.

This is the only way to observe what a compute pass wrote into a storage binding. The cheap CPU-mirror accessors (Shader::get and Shader::get_bytes) reflect what was last set on the CPU side; GPU writes are not visible through them.

read_storage is async on every platform because the underlying map_async requires an awaited callback. On native the implementation drives the device forward (device.poll(Wait)) before awaiting, so callers don’t have to spin their own poll loop. On Web the browser schedules the callback; the helper just awaits.

Returns RendererError::StorageBindingNotFound when the shader does not declare a storage binding called binding, or when no render pass has yet materialised the GPU-side buffer (storage buffers are allocated lazily on first bind).

2 collapsed lines
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use bytemuck;
use fragmentcolor::{Pass, Renderer, Shader};
let renderer = Renderer::new();
let target = renderer.create_texture_target([16, 16]).await?;
let compute = Shader::new(
r#"
struct Out { values: array<f32, 4> };
@group(0) @binding(0) var<storage, read_write> out: Out;
@compute @workgroup_size(1) fn main() {
out.values[0] = 1.0;
out.values[1] = 2.0;
out.values[2] = 3.0;
out.values[3] = 4.0;
}
"#,
)?;
let pass = Pass::compute("seed");
pass.set_compute_dispatch(1, 1, 1);
pass.add_shader(&compute);
renderer.render(&pass, &target)?;
let bytes = renderer.read_storage(&compute, "out").await?;
5 collapsed lines
let values: &[f32] = bytemuck::cast_slice(&bytes);
assert_eq!(values, &[1.0, 2.0, 3.0, 4.0]);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Renders the given object to the given Target.

renderable can be a Shader, a Pass, or any iterable of Pass (Vec<Pass>, &[Pass], &[&Pass]). Passes in an iterable are rendered in order.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, Shader};
let renderer = Renderer::new();
let target = renderer.create_texture_target([10, 10]).await?;
let shader = Shader::default();
renderer.render(&shader, &target)?;
3 collapsed lines
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }