# zenfg
[](https://crates.io/crates/zenfg)
[](https://docs.rs/zenfg/0.2.0/)
[](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/LICENSE)
`zenfg` is a renderer-agnostic FrameGraph compiler and transient-resource
executor for wgpu. It records logical resources and accesses, validates content
flow, builds dependencies, culls dead work, derives usage, plans transient
aliasing, and optionally materializes retained work on a caller-owned device and
queue.
Build domain-specific rendering and compute with wgpu while keeping explicit
resource and execution boundaries. See the
[composition model](https://uinosoft.github.io/zenfg/docs/concepts.html#integration-levels).
ZenFG does not own scenes, pipelines, bind groups, samplers, surfaces,
presentation, or device-loss policy. Public APIs may change before 1.0; pin exact
package versions and review migration notes while integrating.
## Installation
```sh
cargo add zenfg@=0.2.0
```
## Features
| `default` | Yes | Empty feature set; the core compiler and device-backed executor are always available |
| `serde` | No | Serde support for current-version runtime compilation reports; use Snapshot for portable exchange |
| `snapshot` | No | `zenfg-snapshot`, wire-type re-exports, and portable report export |
`FrameGraph::new()` is CPU-only. `FrameGraph::with_device()` stores a cloned
`wgpu::Device`, owns a transient pool, and enables execution. The queue and all
imported resources remain caller-owned.
## Quick start
Use a Cargo application with the toolchain in [Compatibility](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/docs/compatibility.md).
Place this code inside `fn main() -> Result<(), zenfg::FrameGraphError>` and finish
with `Ok(())`. Run `cargo run`; the assertion succeeds without requiring a GPU.
Lines prefixed with `#` in this Markdown are rustdoc test scaffolding, not lines to paste.
This complete CPU-only example records one transient output, retains it, and
compiles a diagnostic plan:
```rust
use zenfg::{
BufferDesc, BufferRange, CompileOptions, FrameGraph, RootReason,
UsagePolicy, WriteContents,
};
let mut graph = FrameGraph::new();
let mut frame = graph.begin_frame();
let output = frame.create_buffer(BufferDesc {
label: "output".into(),
size: 1024,
usage: UsagePolicy::Infer,
})?;
let mut pass = frame.compute_pass("produce");
let _output = pass.storage_buffer_write(
output,
BufferRange::whole(),
WriteContents::Overwrite,
)?;
pass.finish()?;
frame.mark_buffer_root(output, BufferRange::whole(), RootReason::Output)?;
let compiled = frame.compile(CompileOptions::full_report())?;
assert_eq!(compiled.report().unwrap().summary.retained_node_count, 1);
# Ok::<(), zenfg::FrameGraphError>(())
```
## Lifecycle
```text
FrameGraph -> Frame<'frame> -> CompiledFrame<'frame> -> execute(queue)
runtime recording retained CPU plan optional, one-shot
```
- `begin_frame()` exclusively borrows the runtime and creates one recording.
- `Frame::compile()` consumes the recording. Handles and access tokens carry
runtime and recording identities enforced by the API and validation.
- `CompiledFrame::execute()` is one-shot. Native bindings and callbacks needed
only by culled work are released after compilation.
- Surface acquisition and presentation remain caller-owned; import and bind a
fresh current surface texture for each presentation frame.
- Dropping `FrameGraph` releases retained pool and profiler resources, but not
caller-owned imported resources.
## Validation Boundary
ZenFG validates graph semantics: logical resource ranges, content flow, usage
contracts, format categories, and bounded planning inputs. Non-empty
buffer-texture copies also require a known format footprint so ZenFG can track
the buffer byte range used by the copy. Device features, device limits, native
descriptor validity, render-pass compatibility, texture-view compatibility,
and native copy-command alignment and format validity remain wgpu
responsibilities. Native wgpu
validation may therefore be reported during allocation or command encoding
rather than during graph recording. The graph does not take ownership of the
caller's error handling or device-loss policy.
ZenFG validates device-independent texture shape rules and structural usage
requirements for retained transient allocations. It preserves wgpu's
format/backend-dependent multisample support rather than imposing the browser
WebGPU sample-count set. `TRANSIENT_ATTACHMENT` is currently rejected because
Snapshot 1.2 does not encode that usage; it is distinct from ZenFG's transient
resource lifetime policy.
Zero-sized copies are retained as native no-op commands when their node is
retained, but they have no graph content effect and cannot produce an output
value.
## Common tasks
| Create a CPU-only compiler | `FrameGraph::new()` |
| Create a device-backed runtime | `FrameGraph::with_device()` |
| Start a recording | `begin_frame()` |
| Create transient storage | `create_texture()`, `create_buffer()` |
| Register imported storage | `import_texture()`, `import_surface_texture()`, `import_buffer()` |
| Bind imported native objects | `bind_imported_texture()`, `bind_imported_buffer()` |
| Select texture subresources | `create_texture_view()` |
| Record render or compute work | `render_pass()` / `finish_render()`, `compute_pass()` / `finish_compute()` |
| Record copies or clears | `copy_pass()` with typed copy methods, `clear_buffer()`, `clear_buffers()` |
| Encode custom graph-owned commands | `command_pass()` / `finish_command()` |
| Call a renderer that submits itself | `external_submission()` / `finish_external()` |
| Retain observable values | `mark_present()`, `mark_buffer_root()`, `mark_texture_root()`, `mark_readback()` |
| Compile compact or full diagnostics | `compile(CompileOptions::default())`, `compile(CompileOptions::full_report())` |
| Execute retained work | `execute()`, `execute_with_options()` |
| Request CPU/GPU timing | `execute_with_timing()` |
| Inspect or clear retained allocations | `resource_pool_stats()`, `clear_resource_pool()` |
| Export Snapshot 1.2 | `snapshot::create_frame_graph_snapshot()` with feature `snapshot` |
Exact signatures, fields, defaults, and structured `FGxxxx` errors are
documented on [docs.rs](https://docs.rs/zenfg).
## Key pattern: bind and resolve typed access
Imported storage is declared logically, then bound to a caller-owned native
object. Transient and imported storage resolve through the same typed pass
tokens:
```rust,no_run
use zenfg::{
BufferDesc, BufferRange, CompileOptions, FrameGraph, ImportBufferOptions,
InitialContents,
};
# fn record(device: &wgpu::Device, queue: &wgpu::Queue, native: &wgpu::Buffer)
# -> Result<(), zenfg::FrameGraphError> {
let mut graph = FrameGraph::with_device(device);
let mut frame = graph.begin_frame();
let buffer = frame.import_buffer(
BufferDesc::new("input", native.size()),
ImportBufferOptions::new(InitialContents::Defined),
)?;
frame.bind_imported_buffer(buffer, native)?;
let mut pass = frame.compute_pass("consume");
let input = pass.storage_buffer_read(buffer, BufferRange::whole())?;
pass.finish_compute(move |ctx| {
let _native = ctx.resources.buffer(input)?;
// Set a compute pipeline and dispatch through ctx.pass.
Ok(())
})?;
frame.compile(CompileOptions::default())?.execute(queue)?;
# Ok(())
# }
```
Resolved transient objects are valid only inside their execution callback.
Imported objects remain caller-owned, but every graph-visible access still
needs a matching declaration.
Tokens from another graph or recording return `ForeignHandle` before local
pass/access IDs are checked. Tokens from another pass in the same recording
continue to return `WrongPassToken`.
## Resource and integration choices
Declaration granularity is optional: complex workloads can keep private weights,
parameters, and scratch internally bound, while teaching or diagnostic use can
expose more resources. Graph-visible dependencies and access correctness still
apply. See [Choosing resource declaration granularity](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/docs/core-concepts.md#choosing-resource-declaration-granularity).
For resources exposed to the graph:
- Use transient resources for storage needed only by one compiled execution;
import storage that the caller owns or that must survive execution.
- Imported resources explicitly choose `InitialContents::Defined` or
`InitialContents::Undefined`. The first write to a transient range must fully
overwrite it.
- Texture-to-buffer copies preserve their tracked buffer range when actual row
or layer padding remains unwritten. Initialize undefined destination contents
before such a copy; unused single-row or single-layer strides do not require
preservation. A whole-buffer clear uses the logical descriptor size.
- Prefer structured render, compute, copy, and clear nodes. Use command passes
for custom work on a graph-owned encoder.
- Use external submissions for renderers that own and submit their encoders.
The boundary orders queue submissions but is not a GPU-completion fence.
- ZenFG performs no cross-frame dependency analysis and never acquires or
presents a surface for the application.
GPU timing uses two queries per retained render/compute pass. More than 2048
timed passes returns `GpuTimingUnavailableReason::TooManyTimedNodes` before
creating timing resources. Execution still proceeds, and `TimingMode::Both`
keeps CPU results.
See [Core concepts](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/docs/core-concepts.md)
for the shared ownership, content, dependency, lifetime, and integration model.
## Common mistakes
| A pass is absent from the compiled plan | Retain its final value with the appropriate root, or declare only genuine side effects. |
| A read or preserving write reports undefined contents | Overwrite the complete range first or choose the correct imported initial contents. |
| An imported resource cannot execute | Bind the matching native object and ensure descriptor and usage metadata agree. |
| A typed token fails to resolve | Resolve it only through the pass that declared it and only inside that pass's callback. |
| A transient object is used later | Never clone or retain resolved transient wgpu handles across callbacks or frames. |
| External work is incorrectly ordered | Submit all declared work on the shared queue before the external callback returns. |
| Timing is unavailable | Treat unsupported, busy, readback failure, and overflow as non-fatal timing results. |
## Complete examples
The following Cargo examples are published with the crate and compile-checked
outside the workspace:
| Minimal presentation lifecycle | [`minimal_frame.rs`](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/crates/zenfg/examples/minimal_frame.rs) |
| Transient render target to presentation | [`transient_to_present.rs`](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/crates/zenfg/examples/transient_to_present.rs) |
| Caller-owned imported resource | [`imported_resource.rs`](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/crates/zenfg/examples/imported_resource.rs) |
| Cross-frame persistent state | [`persistent_state.rs`](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/crates/zenfg/examples/persistent_state.rs) |
| Opaque third-party submission | [`external_submission.rs`](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/crates/zenfg/examples/external_submission.rs) |
| Portable Snapshot export | [`snapshot_export.rs`](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/crates/zenfg/examples/snapshot_export.rs) |
| Asynchronous GPU timing | [`gpu_timing.rs`](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/crates/zenfg/examples/gpu_timing.rs) |
| Compute storage output | [`compute_output.rs`](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/crates/zenfg/examples/compute_output.rs) |
The repository also contains CPU-only compile and pool benchmarks. Snapshot
export requires the `snapshot` feature.
## Further reading
- [ZenFG documentation index](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/docs/README.md)
- [Core concepts](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/docs/core-concepts.md)
- [`zenfg-snapshot`](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/crates/zenfg-snapshot/README.md)
- [Compatibility](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/docs/compatibility.md)
## Documentation and versions
This README describes **zenfg 0.2.0**. Registry badges show the current published channel, not your installed version.
- Exact installed APIs: read the included `src/`, or run `cargo doc --open` in your consuming project.
- [Online guide (development branch)](https://uinosoft.github.io/zenfg/docs/packages/zenfg.html). The site may describe changes newer than this package.
- [Rust API for this version](https://docs.rs/zenfg/0.2.0/).
- [Source and documentation for this release](https://github.com/uinosoft/zenfg/tree/cargo/zenfg/v0.2.0/crates/zenfg).
- [Shared concepts for this release](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/docs/core-concepts.md) and [compatibility](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/docs/compatibility.md).
- [Migration from 0.1.0](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.2.0/docs/migration-0.2.0.md).
- [Plain Markdown documentation index (development branch)](https://uinosoft.github.io/zenfg/docs/llms.txt).
- Complete Cargo recipes are included in `examples/`.
## Execution timing
Use `execute_with_timing(&queue, options, TimingMode::Cpu)`,
`TimingMode::Gpu`, or `TimingMode::Both`. The result owns an optional synchronous
CPU report and optional GPU readback. Poll the latter with `try_take()`; CPU-only
execution never waits for a GPU result. Ordinary execution collects no timing.
CPU is synchronous elapsed time for all executed node kinds, not thread CPU
usage. Execution total includes preparation, submission and transient release.