Skip to content

Commit 0c0e50b

Browse files
committed
initial commit
0 parents  commit 0c0e50b

14 files changed

+1269
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
Cargo.lock

Cargo.toml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "bevy_app_compute"
3+
version = "0.10.0"
4+
authors = ["Kjolnyr <[email protected]>"]
5+
edition = "2021"
6+
description = "App compute plugin for Bevy"
7+
repository = "https://github.com/Kjolnyr/bevy_app_compute"
8+
homepage = "https://github.com/Kjolnyr/bevy_app_compute"
9+
documentation = "https://docs.rs/bevy_app_compute"
10+
license = "MIT OR Apache-2.0"
11+
readme = "README.md"
12+
categories = ["game-development"]
13+
14+
15+
[dependencies]
16+
bevy = "0.10"
17+
parking_lot = "0.12.1"
18+
wgpu = "0.15.1"
19+
codespan-reporting = "0.11.1"
20+
futures-lite = "1.13.0"
21+
22+
23+
[[example]]
24+
name = "simple"
25+
26+
[[example]]
27+
name = "async_heavy_work"

README.md

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Bevy App Compute
2+
3+
![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)
4+
[![Doc](https://docs.rs/bevy_app_compute/badge.svg)](https://docs.rs/bevy_app_compute)
5+
[![Crate](https://img.shields.io/crates/v/bevy_mod_compute.svg)](https://crates.io/crates/bevy_mod_compute)
6+
7+
8+
Dispatch and run compute shaders on bevy from App World .
9+
10+
## Usage
11+
12+
### Setup
13+
14+
Make an empty struct and implement `ComputeShader` on it. the `shader()` fn should point to your shader source code:
15+
```rust
16+
struct SimpleComputeShader;
17+
18+
impl ComputeShader for SimpleComputeShader {
19+
fn shader() -> ShaderRef {
20+
"shaders/simple.wgsl".into()
21+
}
22+
}
23+
```
24+
25+
Add the plugin to your app:
26+
27+
```rust
28+
use bevy::prelude::*;
29+
use bevy_app_compute::AppComputePlugin;
30+
31+
fn main() {
32+
App::new()
33+
.add_plugin(AppComputePlugin::<SimpleComputeShader>::default());
34+
}
35+
```
36+
37+
And then use the `AppCompute<T>` resource to run compute shaders!
38+
39+
```rust
40+
use bevy::prelude::*;
41+
use bevy_app_compute::prelude::*;
42+
43+
fn my_system(
44+
app_compute: Res<AppCompute<SimpleComputeShader>>,
45+
) {
46+
47+
// Create a new worker
48+
let Some(mut worker) = app_compute.worker() else { return; };
49+
50+
// Add some uniforms and storages with default values
51+
worker.add_uniform(0, "uni", 5f32);
52+
worker.add_storage(0, "storage", vec![0f32; 8]);
53+
54+
// Create a buffer needed to get data back from the GPU
55+
// It has to be linked to a storage.
56+
worker.add_staging_buffer("staging", "storage", std::mem::size_of::<f32>() * 8);
57+
58+
// run the shader
59+
worker.run((8, 1, 1));
60+
61+
// You can read data from your staging buffer now
62+
let result = worker.get_data("staging");
63+
let value: &[f32] = cast_slice(&result);
64+
65+
println!("value: {:?}", value);
66+
}
67+
```
68+
69+
70+
### Asynchronous computation
71+
72+
You can run your compute shaders asynchronously to avoid loosing frame time.
73+
74+
```rust
75+
use bevy::prelude::*;
76+
use bevy_app_compute::prelude::*;
77+
78+
fn my_sender_system(
79+
mut app_compute: ResMut<AppCompute<MyComputeShader>>
80+
){
81+
82+
let Some(mut worker) = app_compute.worker() else { return; };
83+
84+
worker.add_storage(0, "storage", [0f32; 30]);
85+
worker.add_staging_buffer("staging", "storage", std::mem::size_of::<f32>() * 30);
86+
87+
// queue your worker for later use
88+
app_compute.queue(worker, (30, 1, 1));
89+
}
90+
91+
fn my_receiver_system(
92+
mut worker_events: EventReader<WorkerEvent<MyComputeShader>>
93+
) {
94+
// An event is fired once a worker has finished processing
95+
for ev in &mut worker_events.iter() {
96+
let worker = &ev.worker;
97+
98+
let result = worker.get_data("staging");
99+
let value: &[f32] = cast_slice(&result);
100+
101+
println!("got {} items back!", value.len());
102+
}
103+
}
104+
```
105+
106+
## Examples
107+
108+
See [examples](https://github.com/kjolnyr/bevy_app_compute/tree/main/examples)
109+

assets/shaders/heavy_work.wgsl

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
3+
@group(0) @binding(0)
4+
var<storage, read_write> my_storage: array<f32>;
5+
6+
7+
fn index_from_coords(coords: vec3<u32>) -> u32 {
8+
return coords.z * 65535u * 65535u + coords.y * 65535u + coords.x;
9+
}
10+
11+
@compute @workgroup_size(1)
12+
fn main(@builtin(global_invocation_id) invocation_id: vec3<u32>) {
13+
14+
my_storage[index_from_coords(invocation_id)] = f32(index_from_coords(invocation_id));
15+
16+
}

assets/shaders/simple.wgsl

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
@group(0) @binding(0)
3+
var<uniform> uni: f32;
4+
5+
@group(0) @binding(1)
6+
var<storage, read_write> my_storage: array<f32>;
7+
8+
@compute @workgroup_size(1)
9+
fn main(@builtin(global_invocation_id) invocation_id: vec3<u32>) {
10+
11+
my_storage[invocation_id.x] = pow(uni, f32(invocation_id.x));
12+
13+
}

examples/async_heavy_work.rs

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use bevy::{core::cast_slice, prelude::*, render::render_resource::ShaderRef};
2+
use bevy_app_compute::prelude::*;
3+
4+
pub struct HeavyComputeShader;
5+
6+
impl ComputeShader for HeavyComputeShader {
7+
fn shader() -> ShaderRef {
8+
"shaders/heavy_work.wgsl".into()
9+
}
10+
}
11+
12+
fn main() {
13+
App::new()
14+
.add_plugins(DefaultPlugins)
15+
.add_plugin(AppComputePlugin::<HeavyComputeShader>::default())
16+
.add_system(test)
17+
.add_system(receive_data)
18+
.run();
19+
}
20+
21+
fn test(buttons: Res<Input<MouseButton>>, mut app_compute: ResMut<AppCompute<HeavyComputeShader>>) {
22+
if !buttons.just_pressed(MouseButton::Left) {
23+
return;
24+
};
25+
26+
let storage_vec = vec![0f32; 30];
27+
let Some(mut worker) = app_compute.worker() else { return; };
28+
29+
worker.add_storage(0, "storage", storage_vec);
30+
worker.add_staging_buffer("staging", "storage", std::mem::size_of::<f32>() * 30);
31+
32+
app_compute.queue(worker, (30, 1, 1));
33+
}
34+
35+
fn receive_data(mut worker_events: EventReader<WorkerEvent<HeavyComputeShader>>) {
36+
for ev in &mut worker_events.iter() {
37+
let worker = &ev.worker;
38+
39+
let result = worker.get_data("staging");
40+
41+
let value: &[f32] = cast_slice(&result);
42+
43+
println!("got {} items back", value.len());
44+
}
45+
}

examples/simple.rs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use bevy::{core::cast_slice, prelude::*, render::render_resource::ShaderRef};
2+
use bevy_app_compute::prelude::*;
3+
4+
struct SimpleComputeShader;
5+
6+
impl ComputeShader for SimpleComputeShader {
7+
fn shader() -> ShaderRef {
8+
"shaders/simple.wgsl".into()
9+
}
10+
}
11+
12+
fn main() {
13+
App::new()
14+
.add_plugins(DefaultPlugins)
15+
.add_plugin(AppComputePlugin::<SimpleComputeShader>::default())
16+
.add_system(on_click_compute)
17+
.run();
18+
}
19+
20+
fn on_click_compute(
21+
buttons: Res<Input<MouseButton>>,
22+
app_compute: Res<AppCompute<SimpleComputeShader>>,
23+
) {
24+
if !buttons.just_pressed(MouseButton::Left) {
25+
return;
26+
};
27+
28+
let Some(mut worker) = app_compute.worker() else { return; };
29+
30+
worker.add_uniform(0, "uni", 5f32);
31+
worker.add_storage(0, "storage", vec![0f32; 8]);
32+
worker.add_staging_buffer("staging", "storage", std::mem::size_of::<f32>() * 8);
33+
34+
worker.run((8, 1, 1));
35+
36+
let result = worker.get_data("staging");
37+
38+
let value: &[f32] = cast_slice(&result);
39+
40+
println!("value: {:?}", value); // [1.0, 5.0, 24.999998, 124.999985, 624.9999, 3124.9993, 15624.996, 78124.98]
41+
}

rust-toolchain.toml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[toolchain]
2+
channel = "nightly"

src/app_compute.rs

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use std::{future::IntoFuture, marker::PhantomData};
2+
3+
use bevy::{
4+
prelude::*,
5+
render::{
6+
render_resource::ComputePipeline,
7+
renderer::{RenderDevice, RenderQueue},
8+
},
9+
tasks::{AsyncComputeTaskPool, Task},
10+
};
11+
use futures_lite::future;
12+
13+
use crate::{worker::AppComputeWorker, ComputeShader, WorkerEvent};
14+
15+
16+
// Struct responsible for creating new workers and processing tasks
17+
// It requires <C> so that we don't mix tasks between different <C>
18+
#[derive(Resource)]
19+
pub struct AppCompute<C: ComputeShader> {
20+
render_device: RenderDevice,
21+
render_queue: RenderQueue,
22+
pub(crate) pipeline: Option<ComputePipeline>,
23+
tasks: Vec<Task<AppComputeWorker>>,
24+
_phantom: PhantomData<C>,
25+
}
26+
27+
impl<C: ComputeShader> FromWorld for AppCompute<C> {
28+
fn from_world(world: &mut bevy::prelude::World) -> Self {
29+
let render_device = world.resource::<RenderDevice>().clone();
30+
let render_queue = world.resource::<RenderQueue>().clone();
31+
32+
Self {
33+
render_device,
34+
render_queue,
35+
pipeline: None,
36+
tasks: vec![],
37+
_phantom: PhantomData::default(),
38+
}
39+
}
40+
}
41+
42+
impl<C: ComputeShader> AppCompute<C> {
43+
pub fn worker(&self) -> Option<AppComputeWorker> {
44+
if let Some(pipeline) = &self.pipeline {
45+
// Probably could avoid cloning with some cursed lifetime rust code
46+
Some(AppComputeWorker::new(
47+
self.render_device.clone(),
48+
self.render_queue.clone(),
49+
pipeline.clone(),
50+
))
51+
} else {
52+
None
53+
}
54+
}
55+
56+
// Add a new compute tasks to the queue, this allow running compute shaders without blocking the main thread
57+
pub fn queue(&mut self, mut worker: AppComputeWorker, workgroups: (u32, u32, u32)) {
58+
let pool = AsyncComputeTaskPool::get();
59+
60+
let task = pool.spawn(async move {
61+
worker.run(workgroups);
62+
worker
63+
});
64+
65+
self.tasks.push(task);
66+
}
67+
68+
// Process the tasks and send an event once finished with the data
69+
pub fn process_tasks(
70+
mut app_compute: ResMut<Self>,
71+
mut worker_events: EventWriter<WorkerEvent<C>>,
72+
) {
73+
if app_compute.tasks.is_empty() {
74+
return;
75+
}
76+
77+
let mut indices_to_remove = vec![];
78+
79+
for (idx, task) in &mut app_compute.tasks.iter_mut().enumerate() {
80+
let Some(worker) = future::block_on(future::poll_once(task.into_future())) else { continue; };
81+
82+
worker_events.send(WorkerEvent::new(worker));
83+
84+
indices_to_remove.push(idx);
85+
}
86+
87+
for idx in indices_to_remove {
88+
let _ = app_compute.tasks.remove(idx);
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)