zaxis 0.1.0

Drawing protocol

DrawData, global mesh indices, ordered commands, textures, and backend cache keys.

zaxis::protocol exposes DrawData, GeometryUpdate, DrawCommand, Vertex, TextureId, and TextureImage. All six are re-exported at the crate root. Older zaxis::shapes paths preserve DrawData, DrawCommand, Vertex, and TextureId.

The protocol module has no direct winit or wgpu dependency. The crate still always includes both integration dependencies. A producer can build frames without a UI context; a backend can consume complete frames without accessing UI internals.

Produce a frame

This complete example constructs an untextured triangle:

src/main.rs
use zaxis::{vec2, DrawCommand, DrawData, Rect, TextureId, Vertex};

fn main() {
    let mut data = DrawData::new(vec2(800.0, 600.0), 1.0);
    data.vertices = [[20.0, 20.0], [120.0, 20.0], [20.0, 120.0]]
        .map(|position| Vertex {
            position,
            uv: [0.5, 0.5],
            color: [1.0, 1.0, 1.0, 1.0],
        })
        .to_vec();
    data.indices = vec![0, 1, 2];
    data.commands.push(DrawCommand {
        indices: 0..3,
        clip_rect: Rect::from_min_size(vec2(0.0, 0.0), data.logical_size),
        texture: TextureId::WHITE,
        blur: None,
    scroll_hint: false,
    });
    data.revision += 1;
    // A host can now pass `&data` to Renderer::render.
}

Reuse this DrawData between frames. Calling its constructor for each pass creates a new source identity and prevents cache reuse.

DrawData

Public fieldContract
vertices: Vec<Vertex>Shared vertices; logical positions and linear colors
indices: Vec<u32>Global indices into the shared vertex vector
commands: Vec<DrawCommand>Ordered indexed draws
logical_size: Vec2Finite, positive logical viewport dimensions
scale_factor: f32Finite, positive physical pixels per logical pixel
revision: u64Geometry revision within the producer
geometry_update: Option<GeometryUpdate>Optional dirty ranges relative to one preceding revision; defaults to None
source: u64Unique producer identity, stable across its frames
textures: Vec<TextureImage>Complete current payloads for referenced nonwhite textures
texture_options: HashMap<TextureId, TextureOptions>Optional per-texture filter and managed image lifetime; omitted entries keep the legacy contract
texture_budget_bytes: u64Managed GPU texel residency budget; text/unmanaged payloads are excluded
texture_binding_budget: usizeManaged bindings budget, including aliases sharing a texel allocation
materials: Vec<MaterialSource>The user materials commands use, once each, with their validated WGSL; empty without materials
material_uniforms: Vec<u8>Packed uniform blocks addressed by MaterialDraw::uniforms; rewritten with commands

DrawData::new(logical_size, scale_factor) allocates a unique source. default() also allocates a unique source but starts with zero viewport and scale; set them before rendering. Constructor identities come from a process-local atomic counter. Independent producers must never share a geometry cache identity.

Treat the frame as immutable during rendering. Increment revision after editing vertices, indices, commands, clip rectangles, or viewport geometry. Texture changes use image revisions separately.

GeometryUpdate

GeometryUpdate contains from_revision, to_revision, and vertices/indices vectors of Range<usize>. Ranges address buffer elements, not byte offsets. Every changed vertex and index must be covered, including appended elements that are drawn. to_revision must equal the frame revision. Commands remain a complete ordered list. An empty range list permits a command or viewport change without buffer writes.

The built-in renderer uses the hint only when it has validated and uploaded the same source at exactly from_revision, and neither buffer has shrunk. Otherwise it validates and uploads the complete frame. Buffer allocation uploads that entire buffer; dense updates can also select a complete transfer. Custom backends can always ignore the hint: DrawData still contains complete geometry. Retained buffers can contain unused ranges; commands select the live draws.

Manual producers should leave geometry_update as None unless they track all modifications. Clear or replace a previous hint after changing the frame. Supplying incomplete dirty ranges violates the same cache contract as an unchanged revision.

Vertex

Vertex has #[repr(C)] and derives bytemuck::Pod and Zeroable:

FieldLayout / meaning
position: [f32; 2]Logical x/y, top-left origin
uv: [f32; 2]Normalized texture coordinates
color: [f32; 4]Linear RGB plus straight alpha

Stride is 32 bytes; offsets are 0, 8, and 16. The built-in pipeline uses a triangle list, no face culling, and u32 indices. Form complete triangles in each draw range.

Do not supply sRGB byte values divided by 255 as linear vertex RGB. For an sRGB channel s normalized to 0–1, use s / 12.92 when s <= 0.04045, otherwise ((s + 0.055) / 1.055)^2.4. Alpha remains linear and is not premultiplied.

DrawCommand

DrawCommand {
    indices: 0..6,
    clip_rect,
    texture: TextureId::WHITE,
    blur: None,
    scroll_hint: false,
    material: None,
}

Hand-built commands should end with ..Default::default() (an empty range and clip, the white texture, no effect), so a field added later keeps its neutral value. material was added with user materials: a literal that lists every field must now name it.

indices: Range<u32> selects entries in DrawData::indices, not vertex numbers. Values inside that range are global vertex indices. There is no per-command base vertex offset. Commands render in vector order; reordering translucent draws changes the output. Empty index ranges and empty physical clips are skipped.

Logical clips convert to physical scissors by flooring min coordinates, ceiling max coordinates, and clamping to the physical surface. A custom backend must preserve this DPI conversion and command ordering.

blur: Some(sigma) replaces the mesh-covered backdrop with a Gaussian blur of all preceding draws. Sigma uses logical pixels and must be finite and in (0, 64]. Use TextureId::WHITE; UV and vertex color do not affect this backdrop command. Clipping, mesh coverage, rounded geometry, and draw order still apply. Custom backends must implement the effect; it cannot be rendered as an ordinary textured draw.

scroll_hint: true uses the procedural scroll-edge fragment shader instead of sampling the texture. Use TextureId::WHITE; UV.y runs from 0 at the transparent edge to 1 at the viewport boundary (swap UV axes for horizontal hints). Multiply premultiplied vertex color by t^3 * (t * (6*t - 15) + 10), with t = clamp(UV.y, 0, 1). Ordinary commands set scroll_hint: false. A command cannot combine this with backdrop blur. Effects participate in batching and revision comparisons, so a backend must preserve them even when geometry ranges are unchanged.

effect: Some(BackdropEffect) accompanies blur with blur == Some(effect.sigma) and adds the layers of glass in this order: saturation, brightness, exclusion veil, tint luminosity, tint, noise. With lens > 0 the backdrop is first sampled bevel logical pixels deep into the shape along its inward normal (the bend is strongest at the edge), and a specular rim is added. The shape is the mesh bounds of the command as a rounded box with radii (logical pixels: top left, top right, bottom right, bottom left). A backend that only knows blur draws the plain blur.

Materials

material: Some(MaterialDraw { id, uniforms }) replaces the fragment color stage of the command with a user shader; mesh, UVs, clip, vertex color and premultiplied-alpha blending are those of any command. uniforms is a byte range of DrawData::material_uniforms: a 32-byte block (size: vec2, time, delta, texture_rect: vec4, all f32) followed by the packed parameters, a multiple of 16 bytes long, 32 to MAX_UNIFORM_BYTES (1024), starting at a multiple of 16. A backend copies each distinct block to an offset it can bind (the built-in renderer aligns to min_uniform_buffer_offset_alignment) and binds it with a dynamic offset.

DrawData::materials holds a MaterialSource { id, label, wgsl } for every id a command uses. The module has the entry points vs_main and fs_material and the binding groups described in Materials. A backend caches pipelines by id, replaces one whose wgsl changed, and builds them again from the next frame after a device loss. A command with a material must not set scroll_hint. With blur: Some(sigma) the material also reads the sharp and the blurred backdrop, and the command never merges with another.

TextureImage

use std::sync::Arc;
use zaxis::{TextureId, TextureImage};

let image = TextureImage {
    id: TextureId(1),
    size: [1, 1],
    pixels: Arc::new(vec![255, 255, 255, 255]),
    revision: 1,
};
FieldContract
id: TextureIdIdentity referenced by commands; zero is reserved
size: [u32; 2]Nonzero physical width and height
pixels: Arc<Vec<u8>>Tightly packed row-major sRGB RGBA8, straight alpha
revision: u64Increment whenever pixels change

Payload length must equal width × height × 4, without row padding. Use one current payload per texture ID. TextureId::WHITE is (0) and is supplied by the built-in renderer; do not include a replacement payload for it.

The built-in sampler filters linearly. Same-size changed images update the existing GPU texture; size changes recreate it. Uploads replace the full image rather than a dirty subregion. Glyph atlas IDs are scoped to their producer; do not merge unrelated contexts' texture payloads without remapping IDs and commands.

Backend cache keys

  • Geometry: (source, revision).
  • Textures: producer identity, texture ID, size, and image revision.
  • Viewport uniform: logical viewport size, scale factor and the origin of the area it maps to (zero for a window, the region's corner when embedded in a target the host owns).

Changing producer clears the built-in renderer's nonwhite texture cache. Omitted texture IDs within the same producer are not immediately freed by that renderer. Never rely on cache retention instead of supplying complete current texture payloads.

To consume UI-produced frames, call context.draw_data() after context.run. For upload counters and allocation behavior, use Performance.

Edit on GitHub

On this page