Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use an array texture for the image atlas #799

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions vello/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,12 +523,14 @@ impl Renderer {
let target_proxy = ImageProxy::new(
width,
height,
1,
ImageFormat::from_wgpu(target.format)
.expect("`TargetTexture` always has a supported texture format"),
);
let surface_proxy = ImageProxy::new(
width,
height,
1,
ImageFormat::from_wgpu(surface.texture.format())
.ok_or(Error::UnsupportedSurfaceFormat)?,
);
Expand Down Expand Up @@ -792,12 +794,14 @@ impl Renderer {
let target_proxy = ImageProxy::new(
width,
height,
1,
ImageFormat::from_wgpu(target.format)
.expect("`TargetTexture` always has a supported texture format"),
);
let surface_proxy = ImageProxy::new(
width,
height,
1,
ImageFormat::from_wgpu(surface.texture.format())
.ok_or(Error::UnsupportedSurfaceFormat)?,
);
Expand Down
18 changes: 11 additions & 7 deletions vello/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct ImageProxy {
pub height: u32,
pub format: ImageFormat,
pub id: ResourceId,
pub layers: u32,
}

#[derive(Clone, Copy)]
Expand All @@ -68,7 +69,7 @@ pub enum Command {
UploadUniform(BufferProxy, Vec<u8>),
/// Commands the data to be uploaded to the given image.
UploadImage(ImageProxy, Vec<u8>),
WriteImage(ImageProxy, [u32; 2], Image),
WriteImage(ImageProxy, [u32; 3], Image),
Download(BufferProxy),
/// Commands to clear the buffer from an offset on for a length of the given size.
/// If the size is [None], it clears until the end.
Expand Down Expand Up @@ -98,6 +99,8 @@ pub enum BindType {
Image(ImageFormat),
/// A storage image with read only access.
ImageRead(ImageFormat),
/// A storage array texture with read only access.
ImageArrayRead(ImageFormat),
// TODO: Uniform, Sampler, maybe others
}

Expand Down Expand Up @@ -145,13 +148,13 @@ impl Recording {
data: impl Into<Vec<u8>>,
) -> ImageProxy {
let data = data.into();
let image_proxy = ImageProxy::new(width, height, format);
let image_proxy = ImageProxy::new(width, height, 1, format);
self.push(Command::UploadImage(image_proxy, data));
image_proxy
}

pub fn write_image(&mut self, proxy: ImageProxy, x: u32, y: u32, image: Image) {
self.push(Command::WriteImage(proxy, [x, y], image));
pub fn write_image(&mut self, proxy: ImageProxy, x: u32, y: u32, layer: u32, image: Image) {
self.push(Command::WriteImage(proxy, [x, y, layer], image));
}

pub fn dispatch<R>(&mut self, shader: ShaderId, wg_size: (u32, u32, u32), resources: R)
Expand Down Expand Up @@ -257,12 +260,13 @@ impl ImageFormat {
}

impl ImageProxy {
pub fn new(width: u32, height: u32, format: ImageFormat) -> Self {
pub fn new(width: u32, height: u32, layers: u32, format: ImageFormat) -> Self {
let id = ResourceId::next();
Self {
width,
height,
format,
layers,
id,
}
}
Expand All @@ -273,8 +277,8 @@ impl ResourceProxy {
Self::Buffer(BufferProxy::new(size, name))
}

pub fn new_image(width: u32, height: u32, format: ImageFormat) -> Self {
Self::Image(ImageProxy::new(width, height, format))
pub fn new_image(width: u32, height: u32, layers: u32, format: ImageFormat) -> Self {
Self::Image(ImageProxy::new(width, height, layers, format))
}

pub fn as_buf(&self) -> Option<&BufferProxy> {
Expand Down
17 changes: 11 additions & 6 deletions vello/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ impl Render {

let (layout, ramps, images) = resolver.resolve(encoding, &mut packed);
let gradient_image = if ramps.height == 0 {
ResourceProxy::new_image(1, 1, ImageFormat::Rgba8)
ResourceProxy::new_image(1, 1, 1, ImageFormat::Rgba8)
} else {
let data: &[u8] = bytemuck::cast_slice(ramps.data);
ResourceProxy::Image(recording.upload_image(
Expand All @@ -147,12 +147,17 @@ impl Render {
))
};
let image_atlas = if images.images.is_empty() {
ImageProxy::new(1, 1, ImageFormat::Rgba8)
ImageProxy::new(1, 1, 2, ImageFormat::Rgba8)
} else {
ImageProxy::new(images.width, images.height, ImageFormat::Rgba8)
ImageProxy::new(
images.width,
images.height,
images.layers,
ImageFormat::Rgba8,
)
};
for image in images.images {
recording.write_image(image_atlas, image.1, image.2, image.0.clone());
for (image, x, y, layer) in images.images {
recording.write_image(image_atlas, *x, *y, *layer, image.clone());
}
let cpu_config =
RenderConfig::new(&layout, params.width, params.height, &params.base_color);
Expand Down Expand Up @@ -459,7 +464,7 @@ impl Render {
recording.free_resource(draw_monoid_buf);
recording.free_resource(bin_header_buf);
recording.free_resource(path_buf);
let out_image = ImageProxy::new(params.width, params.height, ImageFormat::Rgba8);
let out_image = ImageProxy::new(params.width, params.height, 1, ImageFormat::Rgba8);
let blend_spill_buf = BufferProxy::new(
buffer_sizes.blend_spill.size_in_bytes().into(),
"vello.blend_spill",
Expand Down
2 changes: 1 addition & 1 deletion vello/src/shaders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ pub(crate) fn full_shaders(
Buffer,
Image(ImageFormat::Rgba8),
ImageRead(ImageFormat::Rgba8),
ImageRead(ImageFormat::Rgba8),
ImageArrayRead(ImageFormat::Rgba8),
// Mask LUT buffer, used only when MSAA is enabled.
BufReadOnly,
];
Expand Down
73 changes: 47 additions & 26 deletions vello/src/wgpu_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ impl WgpuEngine {
self.bind_map
.insert_image(image_proxy.id, texture, texture_view);
}
Command::WriteImage(proxy, [x, y], image) => {
Command::WriteImage(proxy, [x, y, layer], image) => {
let (texture, _) = self.bind_map.get_or_create_image(*proxy, device);
let format = proxy.format.to_wgpu();
let block_size = format
Expand All @@ -482,7 +482,11 @@ impl WgpuEngine {
wgpu::ImageCopyTexture {
texture,
mip_level: 0,
origin: wgpu::Origin3d { x: *x, y: *y, z: 0 },
origin: wgpu::Origin3d {
x: *x,
y: *y,
z: *layer,
},
aspect: TextureAspect::All,
},
wgpu::Extent3d {
Expand All @@ -496,7 +500,11 @@ impl WgpuEngine {
wgpu::ImageCopyTexture {
texture,
mip_level: 0,
origin: wgpu::Origin3d { x: *x, y: *y, z: 0 },
origin: wgpu::Origin3d {
x: *x,
y: *y,
z: *layer,
},
aspect: TextureAspect::All,
},
image.data.data(),
Expand Down Expand Up @@ -774,26 +782,31 @@ impl WgpuEngine {
},
count: None,
},
BindType::Image(format) | BindType::ImageRead(format) => {
wgpu::BindGroupLayoutEntry {
binding: i as u32,
visibility,
ty: if bind_type == BindType::ImageRead(format) {
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
}
} else {
wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: format.to_wgpu(),
view_dimension: TextureViewDimension::D2,
}
BindType::Image(format)
| BindType::ImageRead(format)
| BindType::ImageArrayRead(format) => wgpu::BindGroupLayoutEntry {
binding: i as u32,
visibility,
ty: match bind_type {
BindType::ImageRead(_) => wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: None,
}
}
BindType::Image(_) => wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: format.to_wgpu(),
view_dimension: TextureViewDimension::D2,
},
BindType::ImageArrayRead(_) => wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2Array,
multisampled: false,
},
_ => unreachable!(),
},
count: None,
},
})
.collect::<Vec<_>>()
}
Expand Down Expand Up @@ -899,7 +912,7 @@ impl BindMap {
size: wgpu::Extent3d {
width: proxy.width,
height: proxy.height,
depth_or_array_layers: 1,
depth_or_array_layers: proxy.layers,
},
mip_level_count: 1,
sample_count: 1,
Expand All @@ -910,7 +923,11 @@ impl BindMap {
});
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
label: None,
dimension: Some(TextureViewDimension::D2),
dimension: Some(if proxy.layers > 1 {
TextureViewDimension::D2Array
} else {
TextureViewDimension::D2
}),
aspect: TextureAspect::All,
mip_level_count: None,
base_mip_level: 0,
Expand Down Expand Up @@ -1086,7 +1103,7 @@ impl<'a> TransientBindMap<'a> {
size: wgpu::Extent3d {
width: proxy.width,
height: proxy.height,
depth_or_array_layers: 1,
depth_or_array_layers: proxy.layers,
},
mip_level_count: 1,
sample_count: 1,
Expand All @@ -1097,7 +1114,11 @@ impl<'a> TransientBindMap<'a> {
});
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
label: None,
dimension: Some(TextureViewDimension::D2),
dimension: Some(if proxy.layers > 1 {
TextureViewDimension::D2Array
} else {
TextureViewDimension::D2
}),
aspect: TextureAspect::All,
mip_level_count: None,
base_mip_level: 0,
Expand Down
66 changes: 45 additions & 21 deletions vello_encoding/src/image_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,27 @@ use peniko::Image;
use std::collections::hash_map::Entry;
use std::collections::HashMap;

const DEFAULT_ATLAS_SIZE: i32 = 1024;
const MAX_ATLAS_SIZE: i32 = 8192;
const ATLAS_SIZE: i32 = 2048;
const MAX_ATLAS_LAYERS: u32 = 255;

#[derive(Default)]
pub struct Images<'a> {
pub width: u32,
pub height: u32,
pub images: &'a [(Image, u32, u32)],
pub layers: u32,
pub images: &'a [(Image, u32, u32, u32)],
}

pub(crate) struct ImageCache {
atlas: AtlasAllocator,
/// Map from image blob id to atlas location.
map: HashMap<u64, (u32, u32)>,
map: HashMap<u64, (u32, u32, u32)>,
/// List of all allocated images with associated atlas location.
images: Vec<(Image, u32, u32)>,
images: Vec<(Image, u32, u32, u32)>,
/// The current layer we're resolving for
layer: u32,
/// The number of layers we use.
layers: u32,
}

impl Default for ImageCache {
Expand All @@ -33,9 +38,11 @@ impl Default for ImageCache {
impl ImageCache {
pub(crate) fn new() -> Self {
Self {
atlas: AtlasAllocator::new(size2(DEFAULT_ATLAS_SIZE, DEFAULT_ATLAS_SIZE)),
atlas: AtlasAllocator::new(size2(ATLAS_SIZE, ATLAS_SIZE)),
layer: 0,
map: Default::default(),
images: Default::default(),
layers: 4,
}
}

Expand All @@ -44,37 +51,54 @@ impl ImageCache {
width: self.atlas.size().width as u32,
height: self.atlas.size().height as u32,
images: &self.images,
layers: self.layers,
}
}

pub(crate) fn bump_size(&mut self) -> bool {
let new_size = self.atlas.size().width * 2;
if new_size > MAX_ATLAS_SIZE {
return false;
}
self.atlas = AtlasAllocator::new(size2(new_size, new_size));
self.map.clear();
self.images.clear();
true
}

pub(crate) fn clear(&mut self) {
self.atlas.clear();
self.map.clear();
self.images.clear();
self.layer = 0;
}

pub(crate) fn get_or_insert(&mut self, image: &Image) -> Option<(u32, u32)> {
pub(crate) fn get_or_insert(&mut self, image: &Image) -> Option<(u32, u32, u32)> {
match self.map.entry(image.data.id()) {
Entry::Occupied(occupied) => Some(*occupied.get()),
Entry::Vacant(vacant) => {
if image.width > ATLAS_SIZE as u32 || image.height > ATLAS_SIZE as u32 {
// We currently cannot support images larger than 2048 in any axis.
// We should probably still support that, but I think the fallback
// might end up being a second "atlas"
// We choose not to re-size the atlas in that case, because it
// would add a large amount of unused data.
return None;
}
let alloc = self
.atlas
.allocate(size2(image.width as _, image.height as _))?;
.allocate(size2(image.width as _, image.height as _));
let alloc = match alloc {
Some(alloc) => alloc,
None => {
if self.layer >= MAX_ATLAS_LAYERS {
return None;
}
// We implement a greedy system for layers; if we ever get an image that won't fit.
self.layer += 1;
if self.layer >= self.layers {
self.layers = (self.layers * 2).min(MAX_ATLAS_LAYERS);
debug_assert!(self.layer < self.layers);
}
self.atlas.clear();
// This should never fail, as it's a fresh atlas
self.atlas
.allocate(size2(image.width as _, image.height as _))?
}
};
let x = alloc.rectangle.min.x as u32;
let y = alloc.rectangle.min.y as u32;
self.images.push((image.clone(), x, y));
Some(*vacant.insert((x, y)))
self.images.push((image.clone(), x, y, self.layer));
Some(*vacant.insert((x, y, self.layer)))
}
}
}
Expand Down
Loading
Loading