> Development branch documentation · commit 327c05c78d2f528e4f4a679fcc7a77d9aa161f48
> Package versions: @zenfg/webgpu 0.1.0, @zenfg/snapshot 0.1.0, @zenfg/inspector 0.1.0, zenfg 0.1.0, zenfg-snapshot 0.1.0
> Source: https://github.com/uinosoft/zenfg/blob/327c05c78d2f528e4f4a679fcc7a77d9aa161f48/crates/zenfg/README.md

# zenfg

`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](/zenfg/docs/concepts.md#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

<!-- generated:installation:start -->
```sh
cargo add zenfg@=0.1.0
```
<!-- generated:installation:end -->

## Features

| Feature | Default | Adds |
| --- | --- | --- |
| `default` | Yes | Empty feature set; the core compiler and device-backed executor are always available |
| `serde` | No | Serde support for internal compilation report types |
| `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](/zenfg/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);
```

## 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.

## Common tasks

| Task | Public API |
| --- | --- |
| 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
use zenfg::{
    BufferDesc, BufferRange, CompileOptions, FrameGraph, ImportBufferOptions,
    InitialContents,
};

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)?;
```

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.

## 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](/zenfg/docs/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.
- 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.

See [Core concepts](/zenfg/docs/concepts.md)
for the shared ownership, content, dependency, lifetime, and integration model.

## Common mistakes

| Symptom | Fix |
| --- | --- |
| 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:

| Workflow | Example |
| --- | --- |
| Minimal presentation lifecycle | [`minimal_frame.rs`](/zenfg/docs/examples/zenfg/minimal-frame.md) |
| Transient render target to presentation | [`transient_to_present.rs`](/zenfg/docs/examples/zenfg/transient-to-present.md) |
| Caller-owned imported resource | [`imported_resource.rs`](/zenfg/docs/examples/zenfg/imported-resource.md) |
| Cross-frame persistent state | [`persistent_state.rs`](/zenfg/docs/examples/zenfg/persistent-state.md) |
| Opaque third-party submission | [`external_submission.rs`](/zenfg/docs/examples/zenfg/external-submission.md) |
| Portable Snapshot export | [`snapshot_export.rs`](/zenfg/docs/examples/zenfg/snapshot-export.md) |
| Asynchronous GPU timing | [`gpu_timing.rs`](/zenfg/docs/examples/zenfg/gpu-timing.md) |
| Compute storage output | [`compute_output.rs`](/zenfg/docs/examples/zenfg/compute-output.md) |

The repository also contains CPU-only compile and pool benchmarks. Snapshot
export requires the `snapshot` feature.

## Further reading

- [ZenFG documentation index](/zenfg/docs/getting-started.md)
- [Core concepts](/zenfg/docs/concepts.md)
- [`zenfg-snapshot`](/zenfg/docs/packages/zenfg-snapshot.md)
- [Compatibility](/zenfg/docs/compatibility.md)

## Documentation and versions

<!-- generated:documentation:start -->
This README describes **zenfg 0.1.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)](/zenfg/docs/packages/zenfg.md). The site may describe changes newer than this package.
- [Rust API for this version](https://docs.rs/zenfg/0.1.0/).
- [Source and documentation for this release](https://github.com/uinosoft/zenfg/tree/cargo/zenfg/v0.1.0/crates/zenfg).
- [Shared concepts for this release](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.1.0/docs/core-concepts.md) and [compatibility](https://github.com/uinosoft/zenfg/blob/cargo/zenfg/v0.1.0/docs/compatibility.md).
- [Plain Markdown documentation index (development branch)](/zenfg/docs/llms.txt).
- Complete Cargo recipes are included in `examples/`.
<!-- generated:documentation:end -->

## 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.
