Skip to content

Repository files navigation

Aestra

License: MIT OR Apache-2.0 12 workspace packages Build status

Warning

Work in progress. Aestra is under active development; features, APIs, and effect formats may change without notice.

Aestra is a Bevy-native VFX choreography toolkit. It aims to give Rust/Bevy teams the authoring depth of Niagara while keeping effect assets portable, deterministic, and comfortable to integrate into any game runtime.

Run

Install Rust through rustup; rust-toolchain.toml selects the workspace toolchain.

cargo run -p aestra-editor

The sample asset lives at assets/effects/prism_bloom.aestra.ron. The editor starts with an embedded, untitled copy. Use Save As to create a document, or open the project source from the library. Saving an open document refuses to overwrite external changes; save a separate copy or reopen the disk version to resolve the conflict.

Use File → Open Project… (also available in the Library) to select an asset folder, or a project containing an assets/ folder. The Library shows the active asset root. Effects, referenced effects, material programs, functions, presets, and textures resolve within that root; new effects default to its effects/ folder when present. Switching projects uses the unsaved-document prompt and starts an untitled document.

Opening an effect outside the active project stages a new project from its nearest assets/ ancestor, the parent of an effects/ folder, or its own directory. The editor switches only after dependencies compile successfully. For other folder layouts, choose the project root explicitly before opening the effect. Project selection lasts for the current editor session.

Effect edits, shared material-program edits, and extracted material functions remain unsaved until Save All Changes (Ctrl+S). The status bar separates effect state from pending shared-material changes. Material Graph identifies shared programs explicitly: saving one updates every effect that uses it. Save Effect As… also saves pending shared materials; it does not duplicate them. Discard abandons unsaved effect and material changes, while already saved changes remain on disk. Undo after Save creates new unsaved changes.

Recovery snapshots include material drafts and their original source bytes. Save checks all material sources for external changes before writing. Files are saved atomically one at a time; an I/O failure reports remaining unsaved changes, which can be retried without reverting files already saved.

Workspace

aestra/
├── apps/
│   ├── aestra-bench/        Runtime and compiler benchmark harness
│   ├── aestra-editor/       Bevy UI choreography editor
│   └── aestra-viewer/       Viewer, frame capture, and contact-sheet binary
├── bevy/
│   └── aestra-bevy/         Isolated Bevy game-runtime integration
├── assets/effects/          Authored `.aestra.ron` choreography assets
├── assets/textures/         Renderer textures referenced through stable asset IDs
└── crates/
    ├── aestra-core/         Engine-independent semantic effect model
    ├── aestra-authoring/    Commands, transactions, history, locks, and diffs
    ├── aestra-compiler/     Module registry, validation, and typed lowering
    ├── aestra-artifact/     Versioned engine-neutral compiled effect prototype
    ├── aestra-project/      Project indexing and dependency resolution
    ├── aestra-runtime/      Runtime plans and deterministic CPU execution
    ├── aestra-gpu/          GPU ABI, artifact lowering, WESL, and validation
    └── aestra-bevy-render/  Shared Bevy/WGPU presentation adapter

The workspace groups executable products under apps/ and the isolated Bevy game-runtime adapter under bevy/. Shared internal libraries live under crates/; aestra-core owns authored format v3 and its 3D particle model, aestra-authoring owns UI-independent editing, aestra-compiler owns module discovery and lowering, aestra-artifact owns the versioned compiled-effect prototype, aestra-runtime owns immutable execution plans and instance state, and aestra-gpu lowers those plans into a packed engine-neutral GPU ABI and produces Naga-validated WGSL from Aestra-owned WESL. aestra-bevy-render registers and adapts those portable artifacts to Bevy/WGPU presentation, while aestra-bevy owns game playback integration. Both binaries use the same compile/runtime path.

Viewer and visual analysis

Open the bundled example:

cargo run -p aestra-viewer

Open another effect:

cargo run -p aestra-viewer -- --effect path/to/effect.aestra.ron

The bundled textured example can be opened with:

cargo run -p aestra-viewer -- --effect assets/effects/ember_sigil.aestra.ron

The imported flipbook example exercises explicit atlas frames across CPU, GPU-readback, and native WESL presentation:

cargo run -p aestra-viewer -- --effect assets/effects/plasma_burst.aestra.ron

Capture evenly spaced, exact 60 Hz simulation frames plus a single AI-friendly contact sheet:

cargo run -p aestra-viewer -- --capture captures/prism-bloom --frames 9

The capture directory receives numbered PNG frames, contact-sheet.png, capture-manifest.md, and a versioned preview-report.json for automation. Select specific simulation frames or times when a visual check needs important boundaries rather than evenly spaced samples:

cargo run -p aestra-viewer -- --capture captures/prism-bloom --sample-frames 0,6,30,60
cargo run -p aestra-viewer -- --capture captures/prism-bloom --sample-times 0,0.1,0.5,1

Explicit values must be strictly increasing, remain inside the effect lifetime, and resolve to distinct 60 Hz frames. The JSON report records artifact paths, exact frame/time pairs, compiler diagnostics and optimization counts, material-program fingerprints, runtime/backend selection, adapter limits, and measured or estimated effect metrics. Compilation and capture failures return a non-zero exit code and write a failed report whenever an output directory is available. In interactive mode, use Left/Right to step exact frames, [/] to change the seed, and S for a single screenshot. Pass --seed <decimal-or-hex> to reproduce a particular run. The manifest records the requested and selected backend, fallback reason, adapter, driver, physical capacity, and configured particle budget. Use --backend auto|gpu|gpu-readback|cpu to exercise a specific policy, or --max-gpu-particles <count> to test budget fallback.

Semantic material lowering performs deterministic common-subexpression elimination for pure constants, inputs, parameters, and operations. Commutative Add and Multiply inputs are canonicalized. Implicit-derivative texture samples carry an explicit IR sampling contract and are merged only when their texture and UV operands are identical; custom WESL calls remain separate until they carry their own purity contract. The merged-expression count is preserved in compiled artifacts, the Compiler Inspector, and preview-report.json, together with authored, eliminated, and live texture-sample counts. Explicit-LOD sampling is available through the typed Sample Texture Level graph node. Its Float level participates in CSE identity and lowers portably to textureSampleLevel. Explicit-gradient sampling is available through Derivative X, Derivative Y, and Sample Texture Gradient. The typed Vec2 gradients participate in CSE identity and lower portably to dpdx, dpdy, and textureSampleGrad. Reusable material functions share an expansion when their function reference and resolved, input-ID-ordered argument bindings match. Multiple outputs reuse the same internal expressions; custom WESL functions and functions containing them stay separate. Original call IDs remain in the source map or its eliminated set. Function-call counts are available in artifacts, the Compiler Inspector, and preview reports: authored counts resolved output sites (including nested sites in shared expansions), live counts surviving invocation/output pairs, and eliminated is their difference. Shader-static parameter reads are also replaced by their typed defaults during IR lowering, so dependent expressions can fold before backend resource reflection; the authored parameter metadata remains available for inspection and specialization changes still alter the shader fingerprint. Select nodes accept either dynamic Boolean conditions or shader-static ones. A shader-static condition lowers only its chosen branch, so unused inputs, parameter bindings, texture samples, and custom calls never reach shader reflection. The branch- and feature-pruning counts are preserved alongside the other optimization metrics. Semantic sprite shaders derive a compact varying layout from optimized live inputs. Both stages use the same deterministic locations, keep required coverage/visibility fields, and share color alpha when particle color and opacity are both read. Layout identity participates in shader and pipeline caching; legacy and wireframe rendering use their separate compact interface. Native GPU presentation also unions live particle-attribute requirements across each emitter's renderers. Unused appearance calculations and vertex reads are skipped; geometry, lifetime-driven flipbook age, and wireframe color remain available where needed. The 48-byte particle storage ABI is unchanged, omitted fields receive deterministic defaults, and GPU-readback mode retains full CPU-reference data. The Compiler Inspector shows a rendered-mode attribute-count estimate; runtime material overrides and render modes may change the actual requirements.

Run the native-GPU visual regression against the approved, effect-only reference:

cargo run -p aestra-viewer -- --visual-test apps/aestra-viewer/tests/references/prism_bloom target/visual-regression/prism-bloom --frames 8

Run the editor viewport GPU smoke test after changing cameras, render layers, gizmos, or the native GPU queue:

cargo run -p aestra-viewer -- --editor-viewport-smoke target/visual-regression/editor-viewport-smoke --frames 3

This recreates the editor's constrained 3D preview camera and layer-15 overlay camera. It exits with an error if GPU particles disappear from the preview or leak into an overlay-only probe viewport, and writes the captured frames and contact sheet for review.

Use the same workflow for the textured renderer reference:

cargo run -p aestra-viewer -- --effect assets/effects/ember_sigil.aestra.ron --visual-test apps/aestra-viewer/tests/references/ember_sigil target/visual-regression/ember-sigil --frames 8

The command exits with an error when a frame exceeds the tolerant foreground RMSE, coverage, changed-pixel, or centroid limits. It writes amplified diff-*.png images and regression-report.md to the output directory. The versioned JSON report retains the thresholds, every frame metric, the worst-frame summary, and artifact paths on both passing and failing comparisons, so an automated caller can analyze a rejected candidate before refining it. After intentionally approving a visual change, regenerate the reference with:

cargo run -p aestra-viewer -- --approve-visual-reference apps/aestra-viewer/tests/references/prism_bloom --frames 8

Quality gates

Before opening a pull request, run the same deterministic checks as hosted CI:

cargo fmt --all -- --check
cargo check --workspace --all-targets --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked

The normal workflow runs on GitHub-hosted Windows runners. Native-GPU validation is separate because a software or headless adapter is not an equivalent rendering gate. It runs weekly or on demand on a self-hosted Windows x64 runner with the custom gpu label. That runner must have a current GitHub Actions runner, Rustup, and a Vulkan- or DirectX-capable GPU driver. The job validates the constrained editor viewport and all three approved effect references, then uploads captures, manifests, diffs, and reports as a retained workflow artifact.

Bevy plugin

use aestra_bevy::{AestraPlugin, EffectAsset, EffectPlayer};
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, AestraPlugin))
        .add_systems(Startup, |mut commands: Commands| {
            let effect = EffectAsset::load_ron("assets/effects/prism_bloom.aestra.ron")
                .expect("valid effect");
            commands.spawn(EffectPlayer::new(&effect));
            commands.spawn(Camera2d);
        })
        .run();
}

Each player receives an EffectProfiler component. It exposes measured CPU and particle statistics alongside compiler-estimated draw, dispatch, and buffer costs; unsupported measurements such as GPU time remain explicitly unavailable. For reusable child clips, resolve the complete project before spawning:

let index = aestra_project::ProjectAssetIndex::scan("assets");
let project = aestra_bevy::EffectCompiler::default().compile_project(&effect, &index)?;
commands.spawn(EffectPlayer::from_project(std::sync::Arc::new(project)));

Control the root player normally; the plugin manages active child presentations, clip timing, seeds, parameters and inherited motion. The editor and viewer use the same scheduler. See nested motion and playback. ProjectProfiler exposes active root-and-child totals and per-clip breakdowns after AestraSet::Profile; EffectProfiler remains root-only for existing integrations. The editor Profiler and viewer capture reports use project totals. Native-GPU live counts use lightweight asynchronous per-emitter telemetry, with stale results rejected after playback/context changes. GPU simulation timestamps provide per-instance and project costs on supported devices, separately from rendering. Actual draw-command telemetry reports submitted instances, vertex/index references, primitives and draw calls, including per-view costs. Missing measurements remain unavailable, and shared texture memory is not double-counted. Timed semantic notifications from the root and nested clips are emitted as AestraChoreographyEvent observer events. player identifies the root entity, clip_path identifies the source instance (empty for the root), and effect identifies the compiled source. Seeks, frame stepping, and external clock synchronization are silent; normal forward playback emits crossed events, including short clips and loop boundaries. Their typed payloads are intentionally distinct from emitter-to-emitter particle lifecycle links, so gameplay, audio, and camera systems can subscribe without polling playback time. Texture paths in an effect's asset registry are relative to the consuming Bevy application's AssetPlugin root. Missing files use a visible checkerboard fallback and are reported through the effect profile instead of silently removing the draw. Renderers reference stable material IDs; sprite materials own blend state, softness, particle-color or typed value bindings, texture assets, and normalized UV regions. Shared materials compile once and can be reused by multiple renderers. Flipbook renderers reference a stable atlas definition separately from their material. Definitions store an imported texture, explicit normalized frame UVs, frame rate, and loop policy; renderers select particle-age or effect-time playback with deterministic random starts and forward, reverse, or ping-pong ordering.

Development

cargo fmt --all -- --check
cargo check --workspace
cargo test --workspace

See docs/ARCHITECTURE.md for the product architecture and phased roadmap.

License

Aestra is dual-licensed under the MIT License or the Apache License 2.0, at your option.

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages