Skip to content

Commit 7b5e4e3

Browse files
Allow images to be resized on the GPU without losing data (#19462)
# Objective #19410 added support for resizing images "in place" meaning that their data was copied into the new texture allocation on the CPU. However, there are some scenarios where an image may be created and populated entirely on the GPU. Using this method would cause data to disappear, as it wouldn't be copied into the new texture. ## Solution When an image is resized in place, if it has no data in it's asset, we'll opt into a new flag `copy_on_resize` which will issue a `copy_texture_to_texture` command on the old allocation. To support this, we require passing the old asset to all `RenderAsset` implementations. This will be generally useful in the future for reducing things like buffer re-allocations. ## Testing Tested using the example in the issue. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
1 parent 8b6fe34 commit 7b5e4e3

File tree

14 files changed

+85
-54
lines changed

14 files changed

+85
-54
lines changed

crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ impl RenderAsset for GpuAutoExposureCompensationCurve {
196196
source: Self::SourceAsset,
197197
_: AssetId<Self::SourceAsset>,
198198
(render_device, render_queue): &mut SystemParamItem<Self::Param>,
199+
_: Option<&Self>,
199200
) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
200201
let texture = render_device.create_texture_with_data(
201202
render_queue,

crates/bevy_core_pipeline/src/tonemapping/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,5 +464,6 @@ pub fn lut_placeholder() -> Image {
464464
sampler: ImageSampler::Default,
465465
texture_view_descriptor: None,
466466
asset_usage: RenderAssetUsages::RENDER_WORLD,
467+
copy_on_resize: false,
467468
}
468469
}

crates/bevy_gizmos/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ impl RenderAsset for GpuLineGizmo {
554554
gizmo: Self::SourceAsset,
555555
_: AssetId<Self::SourceAsset>,
556556
render_device: &mut SystemParamItem<Self::Param>,
557+
_: Option<&Self>,
557558
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
558559
let list_position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
559560
usage: BufferUsages::VERTEX,

crates/bevy_image/src/image.rs

Lines changed: 35 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ pub struct Image {
356356
pub sampler: ImageSampler,
357357
pub texture_view_descriptor: Option<TextureViewDescriptor<Option<&'static str>>>,
358358
pub asset_usage: RenderAssetUsages,
359+
/// Whether this image should be copied on the GPU when resized.
360+
pub copy_on_resize: bool,
359361
}
360362

361363
/// Used in [`Image`], this determines what image sampler to use when rendering. The default setting,
@@ -747,12 +749,15 @@ impl Image {
747749
label: None,
748750
mip_level_count: 1,
749751
sample_count: 1,
750-
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
752+
usage: TextureUsages::TEXTURE_BINDING
753+
| TextureUsages::COPY_DST
754+
| TextureUsages::COPY_SRC,
751755
view_formats: &[],
752756
},
753757
sampler: ImageSampler::Default,
754758
texture_view_descriptor: None,
755759
asset_usage,
760+
copy_on_resize: false,
756761
}
757762
}
758763

@@ -887,13 +892,15 @@ impl Image {
887892
/// When growing, the new space is filled with 0. When shrinking, the image is clipped.
888893
///
889894
/// For faster resizing when keeping pixel data intact is not important, use [`Image::resize`].
890-
pub fn resize_in_place(&mut self, new_size: Extent3d) -> Result<(), ResizeError> {
895+
pub fn resize_in_place(&mut self, new_size: Extent3d) {
891896
let old_size = self.texture_descriptor.size;
892897
let pixel_size = self.texture_descriptor.format.pixel_size();
893898
let byte_len = self.texture_descriptor.format.pixel_size() * new_size.volume();
899+
self.texture_descriptor.size = new_size;
894900

895901
let Some(ref mut data) = self.data else {
896-
return Err(ResizeError::ImageWithoutData);
902+
self.copy_on_resize = true;
903+
return;
897904
};
898905

899906
let mut new: Vec<u8> = vec![0; byte_len];
@@ -923,10 +930,6 @@ impl Image {
923930
}
924931

925932
self.data = Some(new);
926-
927-
self.texture_descriptor.size = new_size;
928-
929-
Ok(())
930933
}
931934

932935
/// Takes a 2D image containing vertically stacked images of the same size, and reinterprets
@@ -1591,14 +1594,6 @@ pub enum TextureError {
15911594
IncompleteCubemap,
15921595
}
15931596

1594-
/// An error that occurs when an image cannot be resized.
1595-
#[derive(Error, Debug)]
1596-
pub enum ResizeError {
1597-
/// Failed to resize an Image because it has no data.
1598-
#[error("resize method requires cpu-side image data but none was present")]
1599-
ImageWithoutData,
1600-
}
1601-
16021597
/// The type of a raw image buffer.
16031598
#[derive(Debug)]
16041599
pub enum ImageType<'a> {
@@ -1822,13 +1817,11 @@ mod test {
18221817
}
18231818

18241819
// Grow image
1825-
image
1826-
.resize_in_place(Extent3d {
1827-
width: 4,
1828-
height: 4,
1829-
depth_or_array_layers: 1,
1830-
})
1831-
.unwrap();
1820+
image.resize_in_place(Extent3d {
1821+
width: 4,
1822+
height: 4,
1823+
depth_or_array_layers: 1,
1824+
});
18321825

18331826
// After growing, the test pattern should be the same.
18341827
assert!(matches!(
@@ -1849,13 +1842,11 @@ mod test {
18491842
));
18501843

18511844
// Shrink
1852-
image
1853-
.resize_in_place(Extent3d {
1854-
width: 1,
1855-
height: 1,
1856-
depth_or_array_layers: 1,
1857-
})
1858-
.unwrap();
1845+
image.resize_in_place(Extent3d {
1846+
width: 1,
1847+
height: 1,
1848+
depth_or_array_layers: 1,
1849+
});
18591850

18601851
// Images outside of the new dimensions should be clipped
18611852
assert!(image.get_color_at(1, 1).is_err());
@@ -1898,13 +1889,11 @@ mod test {
18981889
}
18991890

19001891
// Grow image
1901-
image
1902-
.resize_in_place(Extent3d {
1903-
width: 4,
1904-
height: 4,
1905-
depth_or_array_layers: LAYERS + 1,
1906-
})
1907-
.unwrap();
1892+
image.resize_in_place(Extent3d {
1893+
width: 4,
1894+
height: 4,
1895+
depth_or_array_layers: LAYERS + 1,
1896+
});
19081897

19091898
// After growing, the test pattern should be the same.
19101899
assert!(matches!(
@@ -1929,13 +1918,11 @@ mod test {
19291918
}
19301919

19311920
// Shrink
1932-
image
1933-
.resize_in_place(Extent3d {
1934-
width: 1,
1935-
height: 1,
1936-
depth_or_array_layers: 1,
1937-
})
1938-
.unwrap();
1921+
image.resize_in_place(Extent3d {
1922+
width: 1,
1923+
height: 1,
1924+
depth_or_array_layers: 1,
1925+
});
19391926

19401927
// Images outside of the new dimensions should be clipped
19411928
assert!(image.get_color_at_3d(1, 1, 0).is_err());
@@ -1944,13 +1931,11 @@ mod test {
19441931
assert!(image.get_color_at_3d(0, 0, 1).is_err());
19451932

19461933
// Grow layers
1947-
image
1948-
.resize_in_place(Extent3d {
1949-
width: 1,
1950-
height: 1,
1951-
depth_or_array_layers: 2,
1952-
})
1953-
.unwrap();
1934+
image.resize_in_place(Extent3d {
1935+
width: 1,
1936+
height: 1,
1937+
depth_or_array_layers: 2,
1938+
});
19541939

19551940
// Pixels in the newly added layer should be zeroes.
19561941
assert!(matches!(

crates/bevy_pbr/src/material.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,7 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
14101410
alpha_mask_deferred_draw_functions,
14111411
material_param,
14121412
): &mut SystemParamItem<Self::Param>,
1413+
_: Option<&Self>,
14131414
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
14141415
let draw_opaque_pbr = opaque_draw_functions.read().id::<DrawMaterial<M>>();
14151416
let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::<DrawMaterial<M>>();

crates/bevy_pbr/src/wireframe.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ impl RenderAsset for RenderWireframeMaterial {
474474
source_asset: Self::SourceAsset,
475475
_asset_id: AssetId<Self::SourceAsset>,
476476
_param: &mut SystemParamItem<Self::Param>,
477+
_previous_asset: Option<&Self>,
477478
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
478479
Ok(RenderWireframeMaterial {
479480
color: source_asset.color.to_linear().to_f32_array(),

crates/bevy_render/src/mesh/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ impl RenderAsset for RenderMesh {
209209
mesh: Self::SourceAsset,
210210
_: AssetId<Self::SourceAsset>,
211211
(images, mesh_vertex_buffer_layouts): &mut SystemParamItem<Self::Param>,
212+
_: Option<&Self>,
212213
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
213214
let morph_targets = match mesh.morph_targets() {
214215
Some(mt) => {

crates/bevy_render/src/render_asset.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ pub trait RenderAsset: Send + Sync + 'static + Sized {
7373
source_asset: Self::SourceAsset,
7474
asset_id: AssetId<Self::SourceAsset>,
7575
param: &mut SystemParamItem<Self::Param>,
76+
previous_asset: Option<&Self>,
7677
) -> Result<Self, PrepareAssetError<Self::SourceAsset>>;
7778

7879
/// Called whenever the [`RenderAsset::SourceAsset`] has been removed.
@@ -355,7 +356,8 @@ pub fn prepare_assets<A: RenderAsset>(
355356
0
356357
};
357358

358-
match A::prepare_asset(extracted_asset, id, &mut param) {
359+
let previous_asset = render_assets.get(id);
360+
match A::prepare_asset(extracted_asset, id, &mut param, previous_asset) {
359361
Ok(prepared_asset) => {
360362
render_assets.insert(id, prepared_asset);
361363
bpf.write_bytes(write_bytes);
@@ -382,7 +384,7 @@ pub fn prepare_assets<A: RenderAsset>(
382384
// we remove previous here to ensure that if we are updating the asset then
383385
// any users will not see the old asset after a new asset is extracted,
384386
// even if the new asset is not yet ready or we are out of bytes to write.
385-
render_assets.remove(id);
387+
let previous_asset = render_assets.remove(id);
386388

387389
let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
388390
if bpf.exhausted() {
@@ -394,7 +396,7 @@ pub fn prepare_assets<A: RenderAsset>(
394396
0
395397
};
396398

397-
match A::prepare_asset(extracted_asset, id, &mut param) {
399+
match A::prepare_asset(extracted_asset, id, &mut param, previous_asset.as_ref()) {
398400
Ok(prepared_asset) => {
399401
render_assets.insert(id, prepared_asset);
400402
bpf.write_bytes(write_bytes);

crates/bevy_render/src/storage.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ impl RenderAsset for GpuShaderStorageBuffer {
116116
source_asset: Self::SourceAsset,
117117
_: AssetId<Self::SourceAsset>,
118118
render_device: &mut SystemParamItem<Self::Param>,
119+
_: Option<&Self>,
119120
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
120121
match source_asset.data {
121122
Some(data) => {

crates/bevy_render/src/texture/gpu_image.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use bevy_asset::AssetId;
77
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
88
use bevy_image::{Image, ImageSampler};
99
use bevy_math::{AspectRatio, UVec2};
10+
use tracing::warn;
1011
use wgpu::{Extent3d, TextureFormat, TextureViewDescriptor};
1112

1213
/// The GPU-representation of an [`Image`].
@@ -44,6 +45,7 @@ impl RenderAsset for GpuImage {
4445
image: Self::SourceAsset,
4546
_: AssetId<Self::SourceAsset>,
4647
(render_device, render_queue, default_sampler): &mut SystemParamItem<Self::Param>,
48+
previous_asset: Option<&Self>,
4749
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
4850
let texture = if let Some(ref data) = image.data {
4951
render_device.create_texture_with_data(
@@ -54,7 +56,38 @@ impl RenderAsset for GpuImage {
5456
data,
5557
)
5658
} else {
57-
render_device.create_texture(&image.texture_descriptor)
59+
let new_texture = render_device.create_texture(&image.texture_descriptor);
60+
if image.copy_on_resize {
61+
if let Some(previous) = previous_asset {
62+
let mut command_encoder =
63+
render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
64+
label: Some("copy_image_on_resize"),
65+
});
66+
let copy_size = Extent3d {
67+
width: image.texture_descriptor.size.width.min(previous.size.width),
68+
height: image
69+
.texture_descriptor
70+
.size
71+
.height
72+
.min(previous.size.height),
73+
depth_or_array_layers: image
74+
.texture_descriptor
75+
.size
76+
.depth_or_array_layers
77+
.min(previous.size.depth_or_array_layers),
78+
};
79+
80+
command_encoder.copy_texture_to_texture(
81+
previous.texture.as_image_copy(),
82+
new_texture.as_image_copy(),
83+
copy_size,
84+
);
85+
render_queue.submit([command_encoder.finish()]);
86+
} else {
87+
warn!("No previous asset to copy from for image: {:?}", image);
88+
}
89+
}
90+
new_texture
5891
};
5992

6093
let texture_view = texture.create_view(

0 commit comments

Comments
 (0)