zenfg
zenfg is a renderer-agnostic, wgpu-specific FrameGraph compiler.
It records per-frame logical resources and accesses, validates content flow,
builds dependencies, removes dead work, derives wgpu usage, tracks lifetimes,
and produces a transient allocation plan and optional diagnostics. Device-backed
graphs materialize that plan through a cross-frame resource pool and can encode
and submit retained render, compute, copy, clear, command, and
external-submission nodes over transient and caller-owned imported resources.
This is a public beta crate. Public APIs may change before 1.0; integration projects should pin the exact beta version.
For package and feature selection, cross-language lifecycle and ownership mapping, troubleshooting, and links to complete recipes, see the ZenFG quick reference.
Installation
Features
| Feature | Enabled by default | Description |
|---|---|---|
default |
Yes | Empty feature set; the core FrameGraph compiler and executor are always available. |
serde |
No | Enables Serde serialization for internal report types. |
snapshot |
No | Adds zenfg-snapshot, re-exports the Snapshot 1.0 wire API, and enables runtime report export. |
FrameGraph::new() remains completely CPU-only. FrameGraph::with_device()
stores a cloned wgpu::Device handle, owns its transient resource pool, and
enables one-shot execution. The queue, surface, scene data, pipelines, bind
groups, samplers, and imported resources remain caller-owned.
Lifecycle
FrameGraph -> Frame<'frame> -> CompiledFrame<'frame> -> execute(queue)
record CPU plan + report optional, one-shot
Frame::compile consumes the recording. Logical handles are bound to the
exclusive frame borrow and also carry runtime owner and recording identities.
Recording-time texture_desc, buffer_desc, and texture_view_desc queries
return snapshotted metadata; the view query resolves all descriptor defaults.
After compilation, callbacks and native bindings referenced only by culled work
are released immediately, while a Full report still describes the complete
recording.
GPU execution
Imported resources are declared logically and bound to caller-owned native wgpu objects. Transient resources need no binding: retained resources are allocated from the FrameGraph-owned pool immediately before execution. Both kinds resolve through the same typed access tokens in synchronous structured callbacks:
use ;
# #
finish_render and finish_compute expose only an active wgpu pass plus typed
resource resolution. Copy nodes own validated buffer-buffer, buffer-texture,
texture-buffer, or texture-texture operations. clear_buffer records one
aligned, non-empty range, while clear_buffers records several ordered clears
in one node. command_pass remains an escape hatch, while external submission
nodes split FrameGraph-owned command encoders into ordered segments.
Retained texture accesses are compiled into a per-execution materialization
plan. Repeated accesses to one logical TextureView share a single native
wgpu::TextureView; compatible implicit storage views are shared as well. The
cache is scoped to one execution and never enters the cross-frame resource pool.
Render nodes support multisample color resolve targets and read-only depth
attachments. Attachment declarations, sampled/storage roles, copy roles, sample
counts, and resolve compatibility are checked against texture-format
capabilities before dead-node elimination. Root declarations are also strict:
Present accepts only surface textures, Readback accepts only imported buffers
exposed exactly as MAP_READ | COPY_DST, and PersistentState accepts only
imported resources.
Execution performs a full preflight before acquiring transient resources or creating the first callback, encoder, or submission. A missing imported native binding, callback or copy operation, descriptor/usage mismatch, or token resolved from the wrong pass produces a structured error. Surface acquisition and presentation remain caller-owned.
GPU timing
GPU timing is opt-in and leaves the normal execution path allocation-free with
respect to profiler resources. execute_with_gpu_timing uses timestamp writes
only for retained render and compute passes and returns a one-shot,
#[must_use] readback handle:
# use ;
# #
GpuTimingReadback::try_take polls the correct cloned Device on native wgpu
backends and relies on the browser event loop on WebGPU. Reports use
std::time::Duration, carry the caller's frame index, and include only the
debug groups needed to interpret timed retained nodes. Unsupported devices,
an overlapping pending readback, readback failure, and query-count overflow are
reported as non-fatal Unavailable results; graph execution still proceeds.
The profiler lazily creates and grows one query set plus resolve/readback
buffers. Only one timing readback may be pending per FrameGraph.
Transient resource pool
The compiler assigns non-overlapping, compatible logical resources to one
physical allocation. Execution acquires one native resource per physical
allocation, so aliases resolve to the same wgpu object. After execution the
resources return to a per-FrameGraph pool and can be reused by later frames on
the same queue.
FrameGraph::resource_pool_stats() reports cumulative acquire, reuse, and
creation counts plus the currently retained resource count and estimated bytes.
FrameGraph::clear_resource_pool() destroys retained resources without
resetting the cumulative counters. Dropping the graph also destroys retained
resources.
References resolved from transient access tokens are valid only for the current
execution callback. They must not be cloned or retained after that callback;
wgpu handle cloning cannot be completely prohibited by Rust lifetimes.
Imported resources remain caller-owned and may be referenced by long-lived bind groups or native handles, but every node that uses them must declare the matching FrameGraph access. A transient resource must instead be resolved from the current callback's typed token and must never be cached across callbacks or frames. When several renderer domains share one native resource, the frame composer should import it once and pass the same logical handle to each domain.
Minimal example
use ;
let mut graph = new;
let mut frame = graph.begin_frame;
let output = frame.create_buffer?;
let mut pass = frame.compute_pass;
let _output = pass.storage_buffer_write?;
pass.finish?;
frame.mark_buffer_root?;
let compiled = frame.compile?;
assert_eq!;
# Ok::
Content model
- Transient and surface resources begin undefined.
- Imported resources explicitly select
InitialContents::DefinedorInitialContents::Undefined. - Reads and preserving writes require defined contents.
- Overwrites create a new logical value without consuming the previous value.
- Discarding an attachment invalidates its selected subresources.
- Value dependencies retain producers; ordering-only WAR/WAW hazards do not.
Retained passes always remain a stable subsequence of recording order. The compiler does not reorder or merge passes.
Diagnostics and Snapshot
ReportLevel::Summary records counts and compile timings. ReportLevel::Full
also records resources, views, accesses, values, dependencies, roots, lifetimes,
allocation plans, execution segments, original recording order, explicit
culling reasons, and recording debug-group hierarchy.
Frame::push_debug_group, pop_debug_group, and with_debug_group assign nodes
and resources to stable per-recording groups. ExecutionOptions can optionally
emit those retained group paths as GPU debug markers; marker emission is off by
default and is reset across external-submission segment boundaries.
The optional serde feature remains the internal report serialization switch.
The separate snapshot feature depends on the wgpu-free zenfg-snapshot
crate, re-exports its Snapshot 1.0 wire types and codec, and adds
create_frame_graph_snapshot for runtime reports.
The adapter requires a Full report and preserves groups, original recording
order, retained/culled state, normalized texture views, split texture regions,
dependencies, roots, execution segments, allocation facts, optional pool facts,
GPU timings, and diagnostics. CompiledFrame::take_report() moves that report
into an asynchronous capture path without cloning it.
Snapshot export creates an in-memory object or JSON text only. Filesystem and
capture naming policy remain caller-owned. Arbitrary JSON validation and both
historical migrations are provided by zenfg-snapshot; the normative Schema,
specification, fixtures, and conformance corpus live in @zenfg/snapshot.
Errors are structured FrameGraphError values with stable FGxxxx diagnostic
codes and graph context.
Current limits
The GPU runtime does not accept a host-owned encoder, time copy, command, or external nodes, apply a pool memory budget or eviction policy, merge or reorder passes, schedule async compute or multiple queues, analyze cross-frame dependencies, or expose a public SSA resource API. Structured render passes currently omit stencil, occlusion queries, and caller-defined timestamp writes. The crate is distributed as MIT-licensed beta software.