A Vulkan rendering framework for Bevy that preserves direct, low-level GPU control while using Bevy's ECS as the backbone for scheduling, concurrency, and resource management.
Bevy's built-in renderer uses wgpu, which prioritizes safety over giving you direct control of the GPU. Pumicite takes a different approach: it provides you with the essential tools to write an efficient and flexible Vulkan application, but leaves enough room for you to fine tune your application and make the best out of Vulkan.
Instead of building a render graph abstraction on top of Bevy, it treats Bevy systems as render graph nodes and system ordering as node dependencies. A schedule build pass automatically handles command buffer allocation, barrier insertion, and queue submission.
You write Bevy systems that record Vulkan commands. Pumicite takes care of the rest.
use bevy::prelude::*;
use bevy_pumicite::prelude::*;
fn main() {
let mut app = App::new();
app.add_plugins(bevy_pumicite::DefaultPlugins);
app.add_systems(PostUpdate, clear.in_set(DefaultRenderSet));
app.run();
}
fn clear(
mut swapchain_image: Query<&mut SwapchainImage, With<bevy::window::PrimaryWindow>>,
mut state: SubmissionState,
time: Res<Time>,
) {
let Ok(mut swapchain_image) = swapchain_image.single_mut() else { return };
state.record(|encoder| {
let Some(current) = swapchain_image.current_image() else { return };
let current = encoder.lock(current, vk::PipelineStageFlags2::CLEAR);
encoder.use_image_resource(
current, &mut swapchain_image.state,
Access::CLEAR, vk::ImageLayout::TRANSFER_DST_OPTIMAL,
0..1, 0..1, false,
);
encoder.emit_barriers();
let hue = (time.elapsed_secs() * 72.0) % 360.0;
let color: bevy::color::Srgba = bevy::color::Hsla::new(hue, 0.8, 0.5, 1.0).into();
encoder.clear_color_image_with_layout(
current,
&vk::ClearColorValue { float32: color.to_f32_array() },
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
);
});
}-
System-as-Render-Graph -- Bevy systems are render graph nodes. The ECS scheduler handles dependency tracking, mutual exclusion, and parallel execution. A
ScheduleBuildPasstransforms system sets intovkQueueSubmitcalls. -
Coroutine-as-Render-Graph -- Record commands with Rust async/await, or ideally coroutines when it stabilizes. Yield points emit barriers and enable cross-future barrier merging.
-
GPUMutex -- Timeline semaphore-based cross-queue synchronization. Lock a resource on a command encoder and semaphore waits are inserted automatically. Safe deferred cleanup via a recycler thread.
-
Resource State Tracking -- Declare how you're about to use a resource and the framework computes the minimal pipeline barrier. You control the tracking granularity and where state is stored.
-
Bindless Rendering -- Global descriptor heaps using
VK_EXT_mutable_descriptor_type. All resources in one array, accessed by index through push constants. No descriptor set switching between draws. Forward compatible withVK_EXT_descriptor_heap. -
Dynamic Rendering -- No legacy
VkRenderPassorVkFramebuffer. Attachments are specified inline when you begin rendering.
- Rust: Nightly
- Vulkan: 1.2+ with
VK_KHR_synchronization2andVK_KHR_timeline_semaphore - Platform: Windows, Linux, macOS (via MoltenVK or KosmicKrisp)
[dependencies]
bevy = { version = "0.17.0-dev", default-features = false, features = [
"bevy_winit", "bevy_asset", "multi_threaded"
] }
bevy_pumicite = "0.1"
[patch.crates-io]
# Unfortunately, we need to fork bevy for now. The goal is to eventually upstream all the changes.
bevy = { git = "https://github.com/dust-engine/bevy", branch = "release-0.17.3" }fn main() {
App::new()
.add_plugins(bevy_pumicite::DefaultPlugins)
.run();
}[dependencies]
pumicite = "0.1"use pumicite::prelude::*;
let (device, mut queue) = Device::create_system_default().unwrap();
let allocator = Allocator::new(device.clone()).unwrap();Run examples with:
cargo run --example <name>| Example | Description |
|---|---|
basics |
Headless image clear -- no window, no Bevy |
clear |
Window that clears to a cycling color |
triangle |
Graphics pipeline with dynamic rendering |
mandelbrot |
Interactive compute shader with push descriptors |
bindless |
Compute shader using bindless descriptor indexing |
sky_atmosphere |
Precomputed atmospheric scattering LUTs |
mesh_shading |
Mesh shader pipeline with dynamic rendering |
mesh_shader_culling |
Mesh shader culling with egui debug UI |
egui |
egui integration for immediate-mode debug UIs |
gltf |
glTF model loading with PBR shading |
| Crate | Description |
|---|---|
pumicite |
Core Vulkan wrapper -- device, commands, sync, memory, pipelines |
bevy_pumicite |
Bevy integration -- plugins, submission sets, asset loaders, swapchain |
pumicite_egui |
egui integration for debug UIs |
pumicite_scene |
Scene and glTF loading |
The tutorial walks through Pumicite from the ground up:
- Overview -- Motivation, philosophy, and key concepts
- Getting Started -- Device creation and first command buffer
- Resource Management -- Buffers, images, and memory allocation
- Synchronization -- Barriers, resource state tracking, and GPUMutex
- Bevy Integration -- Plugins, submission sets, and the ECS render graph
- Compute -- Compute pipelines, dispatch, and multi-pass workflows
- Rendering -- Dynamic rendering, graphics pipelines, and draw commands
- Bindless -- Descriptor heaps and bindless resource indexing
Dual-licensed under MIT or Apache 2.0.
Note: Pumicite is under active development. We offer no API stability guarantee until 1.0 release. Use at your own risk.