GPU Resource Binding
August 25, 2026 · View on GitHub
Yetty packs an arbitrary number of buffers and textures from an arbitrary number of components into a fixed, small set of GPU bindings. This sidesteps WebGPU's per-group binding limits: a text layer plus its font, plus dozens of PDF-embedded subset fonts, all collapse into one storage buffer + one atlas per format + one uniform block.
This document covers the data structures and the binder flow. See Design Overview for the rationale and Render Pipeline for the per-frame upload and recompilation logic.
Resource set tree
Every component that needs GPU resources fills in a
struct yetty_yrender_gpu_resource_set (declared in
include/yetty/yrender/gpu-resource-set.h). Resource sets form a tree: a
layer's set lists the font's set as a child, and the binder flattens the whole
tree depth-first (children before parents).
struct yetty_yrender_gpu_resource_set {
char namespace[YETTY_YRENDER_NAME_MAX]; /* prefixes generated WGSL names */
struct yetty_ycore_pixel_size pixel_size;
struct yetty_yrender_texture textures[YETTY_YRENDER_RS_MAX_TEXTURES]; /* 4 */
size_t texture_count;
struct yetty_yrender_buffer buffers[YETTY_YRENDER_RS_MAX_BUFFERS]; /* 4 */
size_t buffer_count;
struct yetty_yrender_uniform uniforms[YETTY_YRENDER_RS_MAX_UNIFORMS]; /* 32 */
size_t uniform_count;
struct yetty_yrender_shader_code shader; /* WGSL body for this provider */
struct yetty_yrender_gpu_resource_set *children[YETTY_YRENDER_RS_MAX_CHILDREN]; /* 64 */
size_t children_count;
uint32_t instance_count; /* draw shape: 6 verts × instance_count. 0 = skip. */
};
Each buffer/texture/uniform carries a description plus a CPU data
pointer and a dirty flag. The component owns the backing data; the resource set
just points at it.
The struct lives in the
yetty_ydraw_namespace for historical reasons but is declared and used throughoutyrender. The matching result type isyetty_yrender_gpu_resource_set_result.
The binder
struct yetty_yrender_gpu_resource_binder
(include/yetty/yrender/gpu-resource-binder.h) owns the merged GPU objects and
the compiled pipeline. It is driven through its ops table:
struct yetty_yrender_gpu_resource_binder_ops {
void (*destroy)(struct yetty_yrender_gpu_resource_binder *self);
/* Per frame: hand the binder a (sub)tree of resource sets to collect. */
struct yetty_ycore_void_result (*submit)(
struct yetty_yrender_gpu_resource_binder *self,
const struct yetty_yrender_gpu_resource_set *rs);
/* One-time: flatten, pack, create GPU objects, compile pipeline. */
struct yetty_ycore_void_result (*finalize)(struct yetty_yrender_gpu_resource_binder *self);
/* Per frame: upload only dirty data; re-finalize on structural change. */
struct yetty_ycore_void_result (*update)(struct yetty_yrender_gpu_resource_binder *self);
/* Per frame: bind to a render pass at the given group index. */
struct yetty_ycore_void_result (*bind)(struct yetty_yrender_gpu_resource_binder *self,
WGPURenderPassEncoder pass, uint32_t group_index);
WGPURenderPipeline (*get_pipeline)(const struct yetty_yrender_gpu_resource_binder *self);
WGPUBuffer (*get_quad_vertex_buffer)(const struct yetty_yrender_gpu_resource_binder *self);
/* Streaming: write a sub-range of one flattened storage buffer with a
* single wgpuQueueWriteBuffer — no rebind, no re-finalize. Used for live
* data (audio samples, scrolling time series). */
struct yetty_ycore_void_result (*write_buffer_chunk)(
struct yetty_yrender_gpu_resource_binder *self,
size_t buffer_index, size_t byte_offset, const void *data, size_t size);
};
Two construction modes
/* Binder owns its own pipeline (compiles its shader at finalize).
* Used by layer / render-target code where pipeline-per-binder is fine. */
yetty_yrender_gpu_resource_binder_create(device, queue, surface_format, allocator);
/* Binder uses an externally-owned, shared pipeline; it owns only the
* per-instance side (uniform buffer, storage buffer, bind group). Used by
* complex drawables (yplot, yimage, ...) so all instances of one kind share
* one compiled shader. The pipeline must outlive the binder. */
yetty_yrender_gpu_resource_binder_create_with_pipeline(device, queue, allocator, pipeline);
What finalize() produces
The binder flattens the resource set tree and packs everything into a fixed binding layout, generating the WGSL glue automatically:
- One storage buffer — every component buffer concatenated (4-byte aligned)
into a single
array<u32>, with generated offset constants (text_grid_buffer_offset,raster_font_buffer_offset, …). - One atlas texture per format (R8, RGBA8) — every component texture
shelf-packed, with generated UV-region constants (
<namespace>_texture_region). - One uniform block — every component uniform packed into a single WGSL struct following WGSL alignment rules (vec2→8, vec3/vec4/mat4→16, scalar→4).
- Merged shader — generated binding declarations, offset constants, and region constants are prepended to each provider's shader body.
The result is a constant number of GPU bindings regardless of how many
components contributed. See Render Pipeline for the exact
finalize() / update() step lists and the change-detection that triggers a
re-finalize.
Per-frame flow
/* A renderer owns one binder. Each frame: */
binder->ops->submit(binder, &layer_resource_set); /* collect the tree */
binder->ops->update(binder); /* upload dirty data, */
/* re-finalize if structure changed */
binder->ops->bind(binder, pass, /*group*/ 0);
wgpuRenderPassEncoderDraw(pass, 6, instance_count, 0, 0);
- First dirty frame:
finalize()runs (viaupdate's first-call path) — create GPU objects, compile pipeline. - Subsequent frames:
update()uploads only buffers/textures whose dirty flag is set, clearing the flag after upload. - Structural change (a buffer size or texture dimension changed, or shader code changed by hash): the binder releases its GPU objects and re-finalizes.
Key files
include/yetty/yrender/gpu-resource-set.h— resource set tree structinclude/yetty/yrender/gpu-resource-binder.h— binder ops + constructorsinclude/yetty/yrender/types.h— buffer / texture / uniform typessrc/yetty/yrender/gpu-resource-binder.c— flatten, pack, codegen, uploadsrc/yetty/yrender/pipeline.c— shared pipeline (two-tier mode)src/yetty/yrender/types.c— size / alignment / hash helpers