Skip to content

Commit

Permalink
adopted: Track changed Image assets and use them to update image cach…
Browse files Browse the repository at this point in the history
…es (#547)

* Track changed Image assets and use them to update image caches.

Previously, since the bind groups do not get updated every frame, modified image assets would not be rebound. This caused hot reloading to no longer work. Similarly, the TextureArrayCache (when not using atlas) would not invalidate modified assets, so the textures would become out-of-date.

Note there can be other reasons that an asset can be modified, so this should keep the assets more up-to-date in general.

* Use AssetIds instead of Handles.

Handles have some tie to lifetimes of assets. AssetIds don't!

* Clippy

* More clippy in examples

* Revert change to tiles.png

* Fix docs better

* Match other PR

---------

Co-authored-by: andriyDev <[email protected]>
  • Loading branch information
rparrett and andriyDev authored Aug 1, 2024
1 parent 857caa5 commit 5732afc
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 35 deletions.
6 changes: 0 additions & 6 deletions examples/hexagon_generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ pub struct TileHandleHexRow(Handle<Image>);
#[derive(Deref, Resource)]
pub struct TileHandleHexCol(Handle<Image>);

#[derive(Deref, Resource)]
pub struct TileHandleSquare(Handle<Image>);

#[derive(Deref, Resource)]
pub struct TileHandleIso(Handle<Image>);

#[derive(Deref, Resource)]
pub struct FontHandle(Handle<Font>);

Expand Down
57 changes: 32 additions & 25 deletions src/render/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use super::{
pipeline::{TilemapPipeline, TilemapPipelineKey},
prepare,
queue::{ImageBindGroups, TilemapViewBindGroup},
ModifiedImageIds,
};

#[cfg(not(feature = "atlas"))]
Expand Down Expand Up @@ -498,6 +499,7 @@ pub fn bind_material_tilemap_meshes<M: MaterialTilemap>(
(standard_tilemap_meshes, materials): (Query<(&ChunkId, &TilemapId)>, Query<&Handle<M>>),
mut views: Query<(Entity, &VisibleEntities)>,
render_materials: Res<RenderMaterialsTilemap<M>>,
modified_image_ids: Res<ModifiedImageIds>,
#[cfg(not(feature = "atlas"))] (mut texture_array_cache, render_queue): (
ResMut<TextureArrayCache>,
Res<RenderQueue>,
Expand Down Expand Up @@ -567,31 +569,36 @@ pub fn bind_material_tilemap_meshes<M: MaterialTilemap>(
continue;
}

image_bind_groups
.values
.entry(chunk.texture.clone_weak())
.or_insert_with(|| {
#[cfg(not(feature = "atlas"))]
let gpu_image = texture_array_cache.get(&chunk.texture);
#[cfg(feature = "atlas")]
let gpu_image = gpu_images.get(chunk.texture.image_handle()).unwrap();
render_device.create_bind_group(
Some("sprite_material_bind_group"),
&tilemap_pipeline.material_layout,
&[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(
&gpu_image.texture_view,
),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&gpu_image.sampler),
},
],
)
});
let create_bind_group = || {
#[cfg(not(feature = "atlas"))]
let gpu_image = texture_array_cache.get(&chunk.texture);
#[cfg(feature = "atlas")]
let gpu_image = gpu_images.get(chunk.texture.image_handle()).unwrap();
render_device.create_bind_group(
Some("sprite_material_bind_group"),
&tilemap_pipeline.material_layout,
&[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&gpu_image.texture_view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&gpu_image.sampler),
},
],
)
};
if modified_image_ids.is_texture_modified(&chunk.texture) {
image_bind_groups
.values
.insert(chunk.texture.clone_weak(), create_bind_group());
} else {
image_bind_groups
.values
.entry(chunk.texture.clone_weak())
.or_insert_with(create_bind_group);
}
}
}
}
Expand Down
49 changes: 46 additions & 3 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ use bevy::{
core_pipeline::core_2d::Transparent2d,
prelude::*,
render::{
extract_resource::{extract_resource, ExtractResource},
mesh::MeshVertexAttribute,
render_phase::AddRenderCommand,
render_resource::{FilterMode, SpecializedRenderPipelines, VertexFormat},
texture::ImageSamplerDescriptor,
view::{check_visibility, VisibilitySystems},
Render, RenderApp, RenderSet,
},
utils::HashSet,
};

#[cfg(not(feature = "atlas"))]
Expand Down Expand Up @@ -122,6 +124,9 @@ impl Plugin for TilemapRenderingPlugin {
Handle::<StandardTilemapMaterial>::default().id(),
StandardTilemapMaterial::default(),
);

app.init_resource::<ModifiedImageIds>()
.add_systems(Update, collect_modified_image_asset_events);
}

fn finish(&self, app: &mut App) {
Expand Down Expand Up @@ -232,15 +237,20 @@ impl Plugin for TilemapRenderingPlugin {
#[cfg(not(feature = "atlas"))]
render_app
.init_resource::<TextureArrayCache>()
.add_systems(Render, prepare_textures.in_set(RenderSet::PrepareAssets));
.add_systems(Render, prepare_textures.in_set(RenderSet::PrepareAssets))
.add_systems(Render, texture_array_cache::remove_modified_textures);

render_app
.insert_resource(DefaultSampler(sampler))
.insert_resource(RenderChunk2dStorage::default())
.insert_resource(SecondsSinceStartup(0.0))
.add_systems(
ExtractSchedule,
(extract::extract, extract::extract_removal),
(
extract::extract,
extract::extract_removal,
extract_resource::<ModifiedImageIds>,
),
)
.add_systems(
Render,
Expand All @@ -255,7 +265,8 @@ impl Plugin for TilemapRenderingPlugin {
.init_resource::<ImageBindGroups>()
.init_resource::<SpecializedRenderPipelines<TilemapPipeline>>()
.init_resource::<MeshUniformResource>()
.init_resource::<TilemapUniformResource>();
.init_resource::<TilemapUniformResource>()
.init_resource::<ModifiedImageIds>();

render_app.add_render_command::<Transparent2d, DrawTilemap>();
}
Expand Down Expand Up @@ -340,3 +351,35 @@ fn prepare_textures(

texture_array_cache.prepare(&render_device, &render_images);
}

/// Resource to hold the ids of modified Image assets of a single frame.
#[derive(Resource, ExtractResource, Clone, Default)]
pub struct ModifiedImageIds(HashSet<AssetId<Image>>);

impl ModifiedImageIds {
// Determines whether `texture` contains any handles of modified images.
pub fn is_texture_modified(&self, texture: &TilemapTexture) -> bool {
texture
.image_handles()
.iter()
.any(|&image| self.0.contains(&image.id()))
}
}

/// A system to collect the asset events of modified images for one frame.
/// AssetEvents cannot be read from the render sub-app, so this system packs
/// them up into a convenient resource which can be extracted for rendering.
pub fn collect_modified_image_asset_events(
mut asset_events: EventReader<AssetEvent<Image>>,
mut modified_image_ids: ResMut<ModifiedImageIds>,
) {
modified_image_ids.0.clear();

for asset_event in asset_events.read() {
let id = match asset_event {
AssetEvent::Modified { id } => id,
_ => continue,
};
modified_image_ids.0.insert(*id);
}
}
25 changes: 24 additions & 1 deletion src/render/texture_array_cache.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::render::extract::ExtractedTilemapTexture;
use crate::{TilemapSpacing, TilemapTexture, TilemapTextureSize, TilemapTileSize};
use bevy::asset::Assets;
use bevy::prelude::Resource;
use bevy::prelude::{ResMut, Resource};
use bevy::{
prelude::{Image, Res, UVec2},
render::{
Expand All @@ -17,6 +17,8 @@ use bevy::{
utils::{HashMap, HashSet},
};

use super::ModifiedImageIds;

#[derive(Resource, Default, Debug, Clone)]
pub struct TextureArrayCache {
textures: HashMap<TilemapTexture, GpuImage>,
Expand Down Expand Up @@ -342,3 +344,24 @@ impl TextureArrayCache {
}
}
}

/// A system to remove any modified textures from the TextureArrayCache. Modified images will be
/// added back to the pipeline, and so will be reloaded. This allows the TextureArrayCache to be
/// responsive to hot-reloading, for example.
pub fn remove_modified_textures(
modified_image_ids: Res<ModifiedImageIds>,
mut texture_cache: ResMut<TextureArrayCache>,
) {
let texture_is_unmodified =
|texture: &TilemapTexture| !modified_image_ids.is_texture_modified(texture);

texture_cache
.textures
.retain(|texture, _| texture_is_unmodified(texture));
texture_cache
.meta_data
.retain(|texture, _| texture_is_unmodified(texture));
texture_cache.prepare_queue.retain(texture_is_unmodified);
texture_cache.queue_queue.retain(texture_is_unmodified);
texture_cache.bad_flag_queue.retain(texture_is_unmodified);
}
1 change: 1 addition & 0 deletions src/tiles/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ impl TileStorage {
/// Gets a tile entity for the given tile position, if:
/// 1) the tile position lies within the underlying tile map's extents *and*
/// 2) there is an entity associated with that tile position;
///
/// otherwise it returns `None`.
pub fn checked_get(&self, tile_pos: &TilePos) -> Option<Entity> {
if tile_pos.within_map_bounds(&self.size) {
Expand Down

0 comments on commit 5732afc

Please sign in to comment.