mirror of
https://github.com/bevyengine/bevy
synced 2025-02-17 06:28:34 +00:00
# Objective - No point in keeping Meshes/Images in RAM once they're going to be sent to the GPU, and kept in VRAM. This saves a _significant_ amount of memory (several GBs) on scenes like bistro. - References - https://github.com/bevyengine/bevy/pull/1782 - https://github.com/bevyengine/bevy/pull/8624 ## Solution - Augment RenderAsset with the capability to unload the underlying asset after extracting to the render world. - Mesh/Image now have a cpu_persistent_access field. If this field is RenderAssetPersistencePolicy::Unload, the asset will be unloaded from Assets<T>. - A new AssetEvent is sent upon dropping the last strong handle for the asset, which signals to the RenderAsset to remove the GPU version of the asset. --- ## Changelog - Added `AssetEvent::NoLongerUsed` and `AssetEvent::is_no_longer_used()`. This event is sent when the last strong handle of an asset is dropped. - Rewrote the API for `RenderAsset` to allow for unloading the asset data from the CPU. - Added `RenderAssetPersistencePolicy`. - Added `Mesh::cpu_persistent_access` for memory savings when the asset is not needed except for on the GPU. - Added `Image::cpu_persistent_access` for memory savings when the asset is not needed except for on the GPU. - Added `ImageLoaderSettings::cpu_persistent_access`. - Added `ExrTextureLoaderSettings`. - Added `HdrTextureLoaderSettings`. ## Migration Guide - Asset loaders (GLTF, etc) now load meshes and textures without `cpu_persistent_access`. These assets will be removed from `Assets<Mesh>` and `Assets<Image>` once `RenderAssets<Mesh>` and `RenderAssets<Image>` contain the GPU versions of these assets, in order to reduce memory usage. If you require access to the asset data from the CPU in future frames after the GLTF asset has been loaded, modify all dependent `Mesh` and `Image` assets and set `cpu_persistent_access` to `RenderAssetPersistencePolicy::Keep`. - `Mesh` now requires a new `cpu_persistent_access` field. Set it to `RenderAssetPersistencePolicy::Keep` to mimic the previous behavior. - `Image` now requires a new `cpu_persistent_access` field. Set it to `RenderAssetPersistencePolicy::Keep` to mimic the previous behavior. - `MorphTargetImage::new()` now requires a new `cpu_persistent_access` parameter. Set it to `RenderAssetPersistencePolicy::Keep` to mimic the previous behavior. - `DynamicTextureAtlasBuilder::add_texture()` now requires that the `TextureAtlas` you pass has an `Image` with `cpu_persistent_access: RenderAssetPersistencePolicy::Keep`. Ensure you construct the image properly for the texture atlas. - The `RenderAsset` trait has significantly changed, and requires adapting your existing implementations. - The trait now requires `Clone`. - The `ExtractedAsset` associated type has been removed (the type itself is now extracted). - The signature of `prepare_asset()` is slightly different - A new `persistence_policy()` method is now required (return RenderAssetPersistencePolicy::Unload to match the previous behavior). - Match on the new `NoLongerUsed` variant for exhaustive matches of `AssetEvent`.
245 lines
7.9 KiB
Rust
245 lines
7.9 KiB
Rust
//! A compute shader that simulates Conway's Game of Life.
|
|
//!
|
|
//! Compute shaders use the GPU for computing arbitrary information, that may be independent of what
|
|
//! is rendered to the screen.
|
|
|
|
use bevy::{
|
|
prelude::*,
|
|
render::{
|
|
extract_resource::{ExtractResource, ExtractResourcePlugin},
|
|
render_asset::RenderAssetPersistencePolicy,
|
|
render_asset::RenderAssets,
|
|
render_graph::{self, RenderGraph},
|
|
render_resource::{binding_types::texture_storage_2d, *},
|
|
renderer::{RenderContext, RenderDevice},
|
|
Render, RenderApp, RenderSet,
|
|
},
|
|
window::WindowPlugin,
|
|
};
|
|
use std::borrow::Cow;
|
|
|
|
const SIZE: (u32, u32) = (1280, 720);
|
|
const WORKGROUP_SIZE: u32 = 8;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.insert_resource(ClearColor(Color::BLACK))
|
|
.add_plugins((
|
|
DefaultPlugins.set(WindowPlugin {
|
|
primary_window: Some(Window {
|
|
// uncomment for unthrottled FPS
|
|
// present_mode: bevy::window::PresentMode::AutoNoVsync,
|
|
..default()
|
|
}),
|
|
..default()
|
|
}),
|
|
GameOfLifeComputePlugin,
|
|
))
|
|
.add_systems(Startup, setup)
|
|
.run();
|
|
}
|
|
|
|
fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
|
|
let mut image = Image::new_fill(
|
|
Extent3d {
|
|
width: SIZE.0,
|
|
height: SIZE.1,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
TextureDimension::D2,
|
|
&[0, 0, 0, 255],
|
|
TextureFormat::Rgba8Unorm,
|
|
RenderAssetPersistencePolicy::Unload,
|
|
);
|
|
image.texture_descriptor.usage =
|
|
TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING;
|
|
let image = images.add(image);
|
|
|
|
commands.spawn(SpriteBundle {
|
|
sprite: Sprite {
|
|
custom_size: Some(Vec2::new(SIZE.0 as f32, SIZE.1 as f32)),
|
|
..default()
|
|
},
|
|
texture: image.clone(),
|
|
..default()
|
|
});
|
|
commands.spawn(Camera2dBundle::default());
|
|
|
|
commands.insert_resource(GameOfLifeImage(image));
|
|
}
|
|
|
|
pub struct GameOfLifeComputePlugin;
|
|
|
|
impl Plugin for GameOfLifeComputePlugin {
|
|
fn build(&self, app: &mut App) {
|
|
// Extract the game of life image resource from the main world into the render world
|
|
// for operation on by the compute shader and display on the sprite.
|
|
app.add_plugins(ExtractResourcePlugin::<GameOfLifeImage>::default());
|
|
let render_app = app.sub_app_mut(RenderApp);
|
|
render_app.add_systems(
|
|
Render,
|
|
prepare_bind_group.in_set(RenderSet::PrepareBindGroups),
|
|
);
|
|
|
|
let mut render_graph = render_app.world.resource_mut::<RenderGraph>();
|
|
render_graph.add_node("game_of_life", GameOfLifeNode::default());
|
|
render_graph.add_node_edge(
|
|
"game_of_life",
|
|
bevy::render::main_graph::node::CAMERA_DRIVER,
|
|
);
|
|
}
|
|
|
|
fn finish(&self, app: &mut App) {
|
|
let render_app = app.sub_app_mut(RenderApp);
|
|
render_app.init_resource::<GameOfLifePipeline>();
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Clone, Deref, ExtractResource)]
|
|
struct GameOfLifeImage(Handle<Image>);
|
|
|
|
#[derive(Resource)]
|
|
struct GameOfLifeImageBindGroup(BindGroup);
|
|
|
|
fn prepare_bind_group(
|
|
mut commands: Commands,
|
|
pipeline: Res<GameOfLifePipeline>,
|
|
gpu_images: Res<RenderAssets<Image>>,
|
|
game_of_life_image: Res<GameOfLifeImage>,
|
|
render_device: Res<RenderDevice>,
|
|
) {
|
|
let view = gpu_images.get(&game_of_life_image.0).unwrap();
|
|
let bind_group = render_device.create_bind_group(
|
|
None,
|
|
&pipeline.texture_bind_group_layout,
|
|
&BindGroupEntries::single(&view.texture_view),
|
|
);
|
|
commands.insert_resource(GameOfLifeImageBindGroup(bind_group));
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
pub struct GameOfLifePipeline {
|
|
texture_bind_group_layout: BindGroupLayout,
|
|
init_pipeline: CachedComputePipelineId,
|
|
update_pipeline: CachedComputePipelineId,
|
|
}
|
|
|
|
impl FromWorld for GameOfLifePipeline {
|
|
fn from_world(world: &mut World) -> Self {
|
|
let texture_bind_group_layout = world.resource::<RenderDevice>().create_bind_group_layout(
|
|
None,
|
|
&BindGroupLayoutEntries::single(
|
|
ShaderStages::COMPUTE,
|
|
texture_storage_2d(TextureFormat::Rgba8Unorm, StorageTextureAccess::ReadWrite),
|
|
),
|
|
);
|
|
let shader = world
|
|
.resource::<AssetServer>()
|
|
.load("shaders/game_of_life.wgsl");
|
|
let pipeline_cache = world.resource::<PipelineCache>();
|
|
let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
|
|
label: None,
|
|
layout: vec![texture_bind_group_layout.clone()],
|
|
push_constant_ranges: Vec::new(),
|
|
shader: shader.clone(),
|
|
shader_defs: vec![],
|
|
entry_point: Cow::from("init"),
|
|
});
|
|
let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
|
|
label: None,
|
|
layout: vec![texture_bind_group_layout.clone()],
|
|
push_constant_ranges: Vec::new(),
|
|
shader,
|
|
shader_defs: vec![],
|
|
entry_point: Cow::from("update"),
|
|
});
|
|
|
|
GameOfLifePipeline {
|
|
texture_bind_group_layout,
|
|
init_pipeline,
|
|
update_pipeline,
|
|
}
|
|
}
|
|
}
|
|
|
|
enum GameOfLifeState {
|
|
Loading,
|
|
Init,
|
|
Update,
|
|
}
|
|
|
|
struct GameOfLifeNode {
|
|
state: GameOfLifeState,
|
|
}
|
|
|
|
impl Default for GameOfLifeNode {
|
|
fn default() -> Self {
|
|
Self {
|
|
state: GameOfLifeState::Loading,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl render_graph::Node for GameOfLifeNode {
|
|
fn update(&mut self, world: &mut World) {
|
|
let pipeline = world.resource::<GameOfLifePipeline>();
|
|
let pipeline_cache = world.resource::<PipelineCache>();
|
|
|
|
// if the corresponding pipeline has loaded, transition to the next stage
|
|
match self.state {
|
|
GameOfLifeState::Loading => {
|
|
if let CachedPipelineState::Ok(_) =
|
|
pipeline_cache.get_compute_pipeline_state(pipeline.init_pipeline)
|
|
{
|
|
self.state = GameOfLifeState::Init;
|
|
}
|
|
}
|
|
GameOfLifeState::Init => {
|
|
if let CachedPipelineState::Ok(_) =
|
|
pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline)
|
|
{
|
|
self.state = GameOfLifeState::Update;
|
|
}
|
|
}
|
|
GameOfLifeState::Update => {}
|
|
}
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
_graph: &mut render_graph::RenderGraphContext,
|
|
render_context: &mut RenderContext,
|
|
world: &World,
|
|
) -> Result<(), render_graph::NodeRunError> {
|
|
let texture_bind_group = &world.resource::<GameOfLifeImageBindGroup>().0;
|
|
let pipeline_cache = world.resource::<PipelineCache>();
|
|
let pipeline = world.resource::<GameOfLifePipeline>();
|
|
|
|
let mut pass = render_context
|
|
.command_encoder()
|
|
.begin_compute_pass(&ComputePassDescriptor::default());
|
|
|
|
pass.set_bind_group(0, texture_bind_group, &[]);
|
|
|
|
// select the pipeline based on the current state
|
|
match self.state {
|
|
GameOfLifeState::Loading => {}
|
|
GameOfLifeState::Init => {
|
|
let init_pipeline = pipeline_cache
|
|
.get_compute_pipeline(pipeline.init_pipeline)
|
|
.unwrap();
|
|
pass.set_pipeline(init_pipeline);
|
|
pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
|
|
}
|
|
GameOfLifeState::Update => {
|
|
let update_pipeline = pipeline_cache
|
|
.get_compute_pipeline(pipeline.update_pipeline)
|
|
.unwrap();
|
|
pass.set_pipeline(update_pipeline);
|
|
pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|