pebble-engine 0.14.0

A modular, ECS-style graphics/app framework for Rust.
Documentation
# wgpu API Reference

A task-indexed lookup, not a tutorial — "I need a buffer, what do I call?" For the concepts behind any of this (why materials and instances are separate, why bind group entries need explicit visibility, ...), see the relevant chapter in [Part II](./ch06-assets-and-handles.md); this page just gets you to working code fast. Everything below is reachable via `use pebble::wgpu::prelude::*;` unless noted otherwise.

## Buffers

### A plain, uniform, or storage buffer

[`BufferBuilder`](../src/wgpu/buffers.rs) — empty (write into it later) or pre-populated:

```rust
// Empty, written into later via `queue.write_buffer`/`update_buffer`.
let camera_buffer = BufferBuilder::new().label("camera").uniform().size(64).build(&device);

// Pre-populated.
let vertex_buffer = BufferBuilder::new()
    .label("mesh vertices")
    .usage(wgpu::BufferUsages::VERTEX)
    .data(bytemuck::cast_slice(&vertices))
    .build(&device);
```

`.uniform()`/`.storage()` are shorthand for the usual `UNIFORM | COPY_DST`/`STORAGE | COPY_DST` flag pairs; use `.usage(...)` directly for anything else (vertex/index buffers, a `MAP_READ` staging buffer).

### A dynamically-offset buffer (many elements, one buffer)

[`DynamicBufferBuilder`](../src/wgpu/buffers.rs) — sized and aligned for `count` elements of `element_size` bytes, selected later via `set_bind_group`'s dynamic offset. Returns `(Buffer, stride)` — you need the stride for both the offset and later per-element writes, so it comes back alongside the buffer instead of needing to be recomputed:

```rust
let (buffer, stride) = DynamicBufferBuilder::uniform(element_size, count).build(&device);
// ... later, per element:
update_buffer_at(&queue, &buffer, index as u64 * stride, &element_bytes);
// ... at draw time:
pass.set_bind_group(0, Some(&bind_group), &[index as u32 * stride as u32]);
```

Pair with [`BindingKind::dynamic_uniform_buffer`](#a-bind-group-layout) for the layout and [`BindGroupBuilder::dynamic_buffer`](#a-bind-group) for the bind group.

### Writing to an existing buffer

Not a builder — just an existing buffer and new bytes:

```rust
update_buffer(&queue, &buffer, &new_bytes);          // whole buffer, offset 0
update_buffer_at(&queue, &buffer, offset, &bytes);   // one dynamic-offset element
```

## Bind Groups

### A bind group layout

[`BindGroupLayoutBuilder`](../src/wgpu/binding.rs) — one [`BindingKind`](../src/wgpu/binding.rs) per entry, visibility always explicit:

```rust
let layout = BindGroupLayoutBuilder::new()
    .label("camera_layout")
    .entry("camera", 0, BindingKind::uniform_buffer(wgpu::ShaderStages::VERTEX))
    .build(&device);
```

`BindingKind` constructors, one per resource shape: `texture_2d`/`texture_2d_array`/`texture_cubemap`, `storage_texture`, `sampler`/`comparison_sampler`, `uniform_buffer`/`dynamic_uniform_buffer`, `storage_buffer_read_only`/`storage_buffer_read_write`/`dynamic_storage_buffer` — every one takes `wgpu::ShaderStages` explicitly, nothing defaulted. Building a layout by hand this way is mostly for resources outside the material/compute system (a camera — see [Chapter 10](./ch10-camera-and-depth.md)); `MaterialDescriptor`/`ComputeDescriptor` below build their own layout internally from `entries`.

`.build()` panics on a duplicate `@binding(N)` — a shader/layout mismatch fails loudly here instead of at draw time.

### A bind group

[`BindGroupBuilder`](../src/wgpu/buffers.rs) — one resource per binding, against an already-built layout:

```rust
let bind_group = BindGroupBuilder::new(&layout)
    .label("camera_bind_group")
    .buffer(&camera_buffer)      // @binding(0), call order
    .build(&device);
```

`.buffer()`/`.texture()`/`.sampler()`/`.dynamic_buffer()` assign `@binding(N)` in call order starting at 0 — the common case. If your bindings aren't contiguous from 0 (looked up by name, as material/compute instances are — see below), use the `_at` variants (`.buffer_at(2, &buf)`) instead.

## Pipeline Layouts (multiple bind groups)

[`GroupLayout`](../src/wgpu/layout.rs)/[`assemble_bind_group_layouts`](../src/wgpu/layout.rs) assemble several already-built layouts into the array a pipeline layout needs, keyed by explicit `@group(N)` rather than position — this is what `MaterialDescriptor`/`ComputeDescriptor` use internally to combine their own bind group (`own_group`) with `extra_layouts`. You rarely call it directly; the field to reach for is `extra_layouts` on a material/compute descriptor:

```rust
MaterialDescriptor {
    // ... own entries occupy own_group (default Some(0)) ...
    extra_layouts: vec![OwnedGroupLayout { group: 1, layout: camera.bind_group_layout.clone() }],
    ..Default::default()
}
```

Panics on a gap or a collision in `0..=max` across `own_group` + `extra_layouts` combined — see [Chapter 10](./ch10-camera-and-depth.md#wiring-the-camera-into-the-materials-pipeline-layout).

## Materials (render pipelines)

[`MaterialDescriptor`](../src/wgpu/material.rs) → `Assets<MaterialDescriptor>::insert` → `Handle<MaterialDescriptor>`, uploaded automatically to `ProcessedAssets<GPUMaterial>` (registered by `WGPUPlugin`, no manual plugin needed):

```rust
use pebble::wgpu::{material::MaterialDescriptor, mesh::Vertex};

let material = materials.insert("lit", MaterialDescriptor {
    label: Some("lit"),
    shader_source: SHADER,
    vertex_layouts: vec![Vertex::layout()],
    entries: material_entries(),   // Vec<BindingEntry>, see "A bind group layout" above
    own_group: Some(0),            // None if entries is empty — no bind group at all
    targets: vec![wgpu::ColorTargetState {
        format: backend.config.format,
        blend: None,
        write_mask: Default::default(),
    }],
    ..Default::default()
});
```

`..Default::default()` covers `vertex_entry`/`fragment_entry` (`"vs_main"`/`"fs_main"`), `cull_mode: Some(Face::Back)`, no depth testing, fill polygon mode, no `extra_layouts`. See [Chapter 8](./ch08-first-triangle.md) (no bind group) and [Chapter 9](./ch09-textures.md) (textured).

### A material instance (concrete resources bound to a material)

[`MaterialInstanceDescriptor`](../src/wgpu/instance.rs) — matches `(name, BindingInstanceEntry)` pairs against the material's own `entries` by name:

```rust
use pebble::wgpu::instance::{BindingInstanceEntry, MaterialInstanceDescriptor};

let instance = instances.insert("brick_instance", MaterialInstanceDescriptor::new(
    material.id, // RawAssetHandle, not the typed Handle
    vec![
        ("albedo", BindingInstanceEntry::Texture(brick.id)),
        ("albedo_sampler", BindingInstanceEntry::Sampler(SamplerKind::LinearRepeat)),
    ],
));
```

`BindingInstanceEntry` variants: `Texture`/`TextureArray`/`Cubemap`/`Sampler` (all by handle/kind, resolved against existing processed assets) and `Uniform(Vec<u8>)`/`Storage(Vec<u8>)` (a buffer the instance allocates and owns itself, updatable later via `instance.update(&queue, name, &bytes)`). Full walkthrough: [Chapter 9](./ch09-textures.md).

## Compute Pipelines

[`ComputeDescriptor`](../src/wgpu/compute.rs) mirrors `MaterialDescriptor` — same `entries`/`own_group`/`extra_layouts` shape, `build_compute` panics if any entry isn't visible to *exactly* `COMPUTE`:

```rust
use pebble::wgpu::compute::ComputeDescriptor;

let pass = computes.insert("double", ComputeDescriptor {
    label: Some("double"),
    shader_source: COMPUTE_SHADER,
    entries: vec![BindingEntry {
        name: "data",
        binding: 0,
        kind: BindingKind::storage_buffer_read_write(wgpu::ShaderStages::COMPUTE),
    }],
    ..Default::default()
});
```

Dispatching happens directly against `backend.device`/`backend.queue` — no `FrameOperations`-mediated pass, since a compute pass isn't tied to an acquired frame. See [Chapter 11](./ch11-compute.md).

### A compute instance

[`ComputeInstanceDescriptor`](../src/wgpu/instance.rs) — same type as `MaterialInstanceDescriptor` (`GPUBindingInstance<T>` generic over the target), just `T = GPUCompute`:

```rust
use pebble::wgpu::instance::{BindingInstanceEntry, ComputeInstanceDescriptor};

let instance = instances.insert("double_instance", ComputeInstanceDescriptor::new(
    pass.id,
    vec![("data", BindingInstanceEntry::Storage(bytes))],
));
```

## Meshes

[`MeshDescriptor`](../src/wgpu/mesh.rs) — a fixed [`Vertex`](../src/wgpu/mesh.rs) layout (position, UV, normal, tangent) plus indices:

```rust
use pebble::wgpu::mesh::{MeshDescriptor, Vertex};

let mesh = meshes.insert("triangle", MeshDescriptor {
    vertices: vec![
        Vertex::new(glam::Vec3::new(0.0, 0.6, 0.0), glam::Vec2::ZERO, glam::Vec3::Z, glam::Vec4::new(1.0, 0.0, 0.0, 1.0)),
        // ...
    ],
    indices: vec![0, 1, 2],
});
```

Uploads to `ProcessedAssets<GPUMesh>` (`vertex_buffer`/`index_buffer`/`index_count`) automatically. See [Chapter 8](./ch08-first-triangle.md).

## Textures

Three descriptors, same `from_*` constructor pattern — decode/upload happens on the asset pipeline like any other asset, no manual plugin registration needed:

```rust
use pebble::wgpu::{textures::TextureDescriptor, texture_array::TextureArrayDescriptor, cubemap::CubemapDescriptor};

textures.insert("brick", TextureDescriptor::from_file("assets/brick.png").with_mips());
arrays.insert("atlas", TextureArrayDescriptor::from_files(vec!["a.png", "b.png", "c.png"]));
cubemaps.insert("sky", CubemapDescriptor::from_files(1024, [
    "px.png", "nx.png", "py.png", "ny.png", "pz.png", "nz.png",
]));
```

`from_data`/`from_faces` variants take raw bytes instead of a file path, for procedurally-generated or embedded pixel data. `.with_mips()` (texture only) generates a full mip chain via `MipmapGenerator`. Uploads to `ProcessedAssets<GPUTexture>`/`GPUTextureArray`/`GPUCubemap`, each exposing `texture`/`view`. See [Chapter 9](./ch09-textures.md).

## Samplers

Not built per-use — [`SamplerKind`](../src/wgpu/samplers.rs) picks from a small pre-built cache ([`GlobalSamplers`](../src/wgpu/samplers.rs), set up automatically by `WGPUPlugin`):

```rust
BindingInstanceEntry::Sampler(SamplerKind::LinearRepeat)  // in an instance's params
samplers.get(SamplerKind::LinearClamp)                     // -> &wgpu::Sampler, e.g. for a hand-built BindGroupBuilder
```

Variants: `LinearRepeat`, `LinearClamp`, `LinearClampNoMip`, `Nearest`, `NearestClampBorder` (falls back to edge-clamping on web — WebGPU has no border color), `CompareLess` (shadow-map `textureSampleCompare`).

## Custom GPU Resources

Anything one-off that needs the device before it can be built and isn't a material/mesh/texture — a camera, a depth buffer — is a [`LazyResource`](crate::assets::singleton_asset::LazyResource), constructed once `WGPUBackend` (or your own `Deps`) exists, built from the pieces above:

```rust
impl LazyResource<WGPUBackend> for Camera {
    type Deps<'a> = ();
    fn construct<'a>(backend: &WGPUBackend, _deps: &()) -> Option<Self> {
        let bind_group_layout = BindGroupLayoutBuilder::new() /* ... */ .build(&backend.device);
        let buffer = BufferBuilder::new().uniform().size(64).build(&backend.device);
        let bind_group = BindGroupBuilder::new(&bind_group_layout).buffer(&buffer).build(&backend.device);
        Some(Camera { buffer, bind_group_layout, bind_group })
    }
}
```

Full walkthrough, including wiring the resulting layout into a material via `extra_layouts`: [Chapter 10](./ch10-camera-and-depth.md).