Draw textures separately, and keep them out of Primitives

Interleaving textures with primitives was solving a problem that does not
exist: within a layer, order is already undefined because freeing an
instance swap-removes it, and layering is what layers are for. So the run
batching is gone.

A layer is now `LayerDraws`: a `Primitives` and a texture `InstanceList`
side by side, with `updated` covering both. `Primitives` holds only
primitives again -- its instance list plus the group-1 data those
instances read -- and `InstanceList` is the shared push/free/apply_free
the two lists have in common rather than a second copy of it.
`PrimitiveHandle` names which list with `InstanceKind`, and
`PrimitiveChange` carries the same, since the two index independently.
The handle no longer carries a group-1 index at all: a primitive
instance already records where its entry is.

The renderer gives each layer a second instance buffer and draws its
textures one at a time after the instanced draw, each binding its own
group 2.

Review fixes alongside: `GlyphAtlas::allocate` returns the `PageUpload`
it reserved instead of a bare tuple; a page or image region uploads
through a new `write_region`, which passes the row stride to
`write_texture` rather than copying the rectangle out first.

Verified by replaying taps into the `tabs` example: three images added
and one deleted leaves two drawn with two live texture slots, and the
masked text-edit tab still clips, with the images freed on tab switch.
This commit is contained in:
iris committed 2026-09-13 13:32:05 -04:00
1 parent f5864da3c4
commit b3d3da5dab
10 files changed
+254 -213

No files matched your search

+3 -3
View File
@@ -2,7 +2,7 @@ use std::ops::{Index, IndexMut};
use crate::{ use crate::{
UiRegion, WidgetId, UiRegion, WidgetId,
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives}, render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::to_mut, util::to_mut,
}; };
@@ -40,7 +40,7 @@ struct Child {
tail: usize, tail: usize,
} }
pub type PrimitiveLayers = Layers<Primitives>; pub type DrawLayers = Layers<LayerDraws>;
impl<T: Default> Layers<T> { impl<T: Default> Layers<T> {
pub fn new() -> Layers<T> { pub fn new() -> Layers<T> {
@@ -120,7 +120,7 @@ impl<T: Default> Layers<T> {
} }
} }
impl PrimitiveLayers { impl DrawLayers {
pub fn write<P: Primitive>( pub fn write<P: Primitive>(
&mut self, &mut self,
layer: LayerId, layer: LayerId,
+23 -17
View File
@@ -98,17 +98,10 @@ impl GlyphAtlas {
return None; return None;
} }
let (layer, x, y) = self.allocate(w, h); let upload = self.allocate(w, h);
write_glyph(&mut self.pages[layer as usize].image, image, x, y); let PatchRect { x, y, .. } = upload.rect;
self.uploads.push(PageUpload { write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
layer, self.uploads.push(upload);
rect: PatchRect {
x,
y,
width: w,
height: h,
},
});
let scale = 1.0 / PAGE as f32; let scale = 1.0 / PAGE as f32;
let entry = GlyphEntry { let entry = GlyphEntry {
@@ -119,20 +112,30 @@ impl GlyphAtlas {
width: w, width: w,
height: h, height: h,
is_colored: matches!(image.content, Content::Color), is_colored: matches!(image.content, Content::Color),
layer, layer: upload.layer,
}; };
self.entries.insert(key, Some(entry)); self.entries.insert(key, Some(entry));
Some(entry) Some(entry)
} }
fn allocate(&mut self, w: u32, h: u32) -> (u32, u32, u32) { /// Reserves room for a `w` by `h` glyph, adding a page if none has it.
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
let rect = |x, y| PatchRect {
x,
y,
width: w,
height: h,
};
if let Some((i, (x, y))) = self if let Some((i, (x, y))) = self
.pages .pages
.iter_mut() .iter_mut()
.enumerate() .enumerate()
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position))) .find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
{ {
return (i as u32, x, y); return PageUpload {
layer: i as u32,
rect: rect(x, y),
};
} }
self.pages.push(Page { self.pages.push(Page {
@@ -141,12 +144,15 @@ impl GlyphAtlas {
y: PAD, y: PAD,
shelf_height: h + PAD, shelf_height: h + PAD,
}); });
(self.pages.len() as u32 - 1, PAD, PAD) PageUpload {
layer: self.pages.len() as u32 - 1,
rect: rect(PAD, PAD),
}
} }
/// Drains what has been written since the last call, for the renderer to /// Drains what has been written since the last call, for the renderer to
/// upload. Nothing else is needed for a new page: wgpu leaves the rest of /// upload. A new page needs nothing more: wgpu leaves the rest of a fresh
/// a fresh layer transparent, which is what an atlas wants. /// layer transparent, which is what an atlas wants.
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> { pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
let pages = &self.pages; let pages = &self.pages;
self.uploads self.uploads
+39 -54
View File
@@ -1,5 +1,3 @@
use std::ops::Range;
use crate::{ use crate::{
UiData, UiRenderState, UiData, UiRenderState,
render::{ render::{
@@ -51,14 +49,10 @@ struct RenderLayer {
instance: ArrBuf<PrimitiveInstance>, instance: ArrBuf<PrimitiveInstance>,
primitives: PrimitiveBuffers, primitives: PrimitiveBuffers,
primitive_group: BindGroup, primitive_group: BindGroup,
draws: Vec<Draw>, texture_instance: ArrBuf<PrimitiveInstance>,
} /// Which texture each entry of `texture_instance` samples, in the same
/// order. Not in the vertex buffer because it names a bind group.
/// A run of consecutive instances sharing one group 2: `texture` names a texture_slots: Vec<u32>,
/// standalone image, or `None` is the glyph atlas rects and glyphs sample.
struct Draw {
texture: Option<u32>,
instances: Range<u32>,
} }
impl UiRenderNode { impl UiRenderNode {
@@ -68,21 +62,21 @@ impl UiRenderNode {
pass.set_bind_group(3, &self.mask_group, &[]); pass.set_bind_group(3, &self.mask_group, &[]);
for i in &self.active { for i in &self.active {
let layer = &self.layers[i]; let layer = &self.layers[i];
if layer.draws.is_empty() {
continue;
}
pass.set_bind_group(1, &layer.primitive_group, &[]); pass.set_bind_group(1, &layer.primitive_group, &[]);
if layer.instance.len() > 0 {
pass.set_bind_group(2, self.pages.group(), &[]);
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..)); pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
for draw in &layer.draws { pass.draw(0..4, 0..layer.instance.len() as u32);
let group = match draw.texture { }
Some(slot) => match self.textures.group(slot) { if !layer.texture_slots.is_empty() {
Some(group) => group, pass.set_vertex_buffer(0, layer.texture_instance.buffer.slice(..));
None => continue, for (i, &slot) in layer.texture_slots.iter().enumerate() {
}, let Some(group) = self.textures.group(slot) else {
None => self.pages.group(), continue;
}; };
pass.set_bind_group(2, group, &[]); pass.set_bind_group(2, group, &[]);
pass.draw(0..4, draw.instances.clone()); pass.draw(0..4, i as u32..i as u32 + 1);
}
} }
} }
} }
@@ -95,12 +89,12 @@ impl UiRenderNode {
ui_render: &mut UiRenderState, ui_render: &mut UiRenderState,
) { ) {
self.active.clear(); self.active.clear();
for (i, primitives) in ui_render.layers.iter_mut() { for (i, draws) in ui_render.layers.iter_mut() {
self.active.push(i); self.active.push(i);
for change in primitives.apply_free() { for change in draws.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) { if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives { for h in &mut inst.primitives {
if h.layer == i && h.inst_idx == change.old { if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new; h.inst_idx = change.new;
break; break;
} }
@@ -119,21 +113,34 @@ impl UiRenderNode {
), ),
primitives, primitives,
primitive_group, primitive_group,
draws: Vec::new(), texture_instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"texture instance",
),
texture_slots: Vec::new(),
} }
}); });
if primitives.updated { if draws.updated {
rlayer rlayer
.instance .instance
.update(device, queue, primitives.instances()); .update(device, queue, draws.primitives.instances());
rlayer.primitives.update(device, queue, primitives.data()); rlayer
.primitives
.update(device, queue, draws.primitives.data());
rlayer.primitive_group = Self::primitive_group( rlayer.primitive_group = Self::primitive_group(
device, device,
&self.primitive_layout, &self.primitive_layout,
rlayer.primitives.buffers(), rlayer.primitives.buffers(),
); );
rlayer.plan_draws(primitives.instances()); rlayer
primitives.updated = false; .texture_instance
.update(device, queue, draws.textures.instances());
rlayer.texture_slots.clear();
rlayer
.texture_slots
.extend(draws.textures.instances().iter().map(|inst| inst.idx));
draws.updated = false;
} }
} }
if ui.masks.changed { if ui.masks.changed {
@@ -312,9 +319,8 @@ impl UiRenderNode {
}) })
} }
/// Group 2: the texture a run of instances samples, and the sampler. No /// Group 2: the texture this draw samples, and the sampler. No `count` on
/// `count` on either entry -- plain Vulkan 1.0 / GLES sampling, unlike the /// either entry -- plain Vulkan 1.0 / GLES sampling is all this needs.
/// `binding_array` layout this replaced.
fn texture_layout(device: &Device) -> BindGroupLayout { fn texture_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
@@ -372,24 +378,3 @@ impl UiRenderNode {
self.textures.count() self.textures.count()
} }
} }
impl RenderLayer {
/// Only a texture instance breaks a run, so a ui without images plans one
/// draw however many rects and glyphs it has.
fn plan_draws(&mut self, instances: &[PrimitiveInstance]) {
self.draws.clear();
for (i, inst) in instances.iter().enumerate() {
let i = i as u32;
let texture = (inst.binding == TEXTURE_BINDING).then_some(inst.idx);
match self.draws.last_mut() {
Some(draw) if draw.texture.is_none() && texture.is_none() => {
draw.instances.end = i + 1;
}
_ => self.draws.push(Draw {
texture,
instances: i..i + 1,
}),
}
}
}
}
+8 -25
View File
@@ -1,18 +1,17 @@
use image::EncodableLayout;
use wgpu::*; use wgpu::*;
use crate::GlyphAtlas; use crate::GlyphAtlas;
use super::{ use super::{
atlas::PAGE, atlas::PAGE,
texture::{array_view, texture_group}, texture::{array_view, texture_group, write_region},
}; };
/// The glyph atlas on the GPU: one array texture whose layers are the pages /// The glyph atlas on the GPU: one array texture whose layers are the pages
/// `GlyphAtlas` packs. /// `GlyphAtlas` packs.
/// ///
/// An array rather than a `binding_array<texture_2d<f32>>` because a layer /// One array rather than a texture per page because a layer index is ordinary
/// index is ordinary Vulkan 1.0 / GLES sampling, while a binding array needs /// Vulkan 1.0 / GLES sampling, where a `binding_array` would need
/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack. /// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack.
pub struct GpuPages { pub struct GpuPages {
device: Device, device: Device,
@@ -43,33 +42,17 @@ impl GpuPages {
self.grow(atlas.page_count(), layout, sampler); self.grow(atlas.page_count(), layout, sampler);
} }
for (upload, page) in atlas.uploads() { for (upload, page) in atlas.uploads() {
let rect = upload.rect; let dst = TexelCopyTextureInfo {
// `write_texture` wants tightly packed rows; the page is wider.
let sub =
image::imageops::crop_imm(page, rect.x, rect.y, rect.width, rect.height).to_image();
self.queue.write_texture(
TexelCopyTextureInfo {
texture: &self.texture, texture: &self.texture,
mip_level: 0, mip_level: 0,
origin: Origin3d { origin: Origin3d {
x: rect.x, x: upload.rect.x,
y: rect.y, y: upload.rect.y,
z: upload.layer, z: upload.layer,
}, },
aspect: TextureAspect::All, aspect: TextureAspect::All,
}, };
sub.as_bytes(), write_region(&self.queue, dst, page, upload.rect);
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(rect.width * 4),
rows_per_image: Some(rect.height),
},
Extent3d {
width: rect.width,
height: rect.height,
depth_or_array_layers: 1,
},
);
} }
} }
+138 -82
View File
@@ -11,30 +11,89 @@ use crate::{
use bytemuck::Pod; use bytemuck::Pod;
use wgpu::*; use wgpu::*;
pub struct Primitives { /// Everything one layer draws. A texture binds its own group 2, so it draws on
instances: Vec<PrimitiveInstance>, /// its own and cannot join the instanced draw the primitives share.
assoc: Vec<WidgetId>, pub struct LayerDraws {
data: PrimitiveData, pub primitives: Primitives,
free: Vec<usize>, pub textures: InstanceList,
pub updated: bool, pub updated: bool,
} }
impl Default for Primitives { impl Default for LayerDraws {
fn default() -> Self { fn default() -> Self {
Self { Self {
instances: Default::default(), primitives: Default::default(),
assoc: Default::default(), textures: Default::default(),
data: Default::default(),
free: Vec::new(),
updated: true, updated: true,
} }
} }
} }
/// Not in `primitives!` and with no group-1 buffer: a texture instance's `idx` /// Which of a layer's two lists an instance is in. They index independently,
/// names the texture to bind, so there is nothing to look up per-instance. /// so a handle or a renumbering naming only a position would be ambiguous.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstanceKind {
Primitive,
Texture,
}
/// A texture instance's `idx` names the texture to bind rather than a group-1
/// entry, so it is not in `primitives!` and has no buffer of its own.
pub const TEXTURE_BINDING: u32 = 2; pub const TEXTURE_BINDING: u32 = 2;
/// Instances, the widget each belongs to, and the slots waiting to be reused.
#[derive(Default)]
pub struct InstanceList {
instances: Vec<PrimitiveInstance>,
assoc: Vec<WidgetId>,
free: Vec<usize>,
}
impl InstanceList {
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
&self.instances
}
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance) -> usize {
if let Some(i) = self.free.pop() {
self.instances[i] = inst;
self.assoc[i] = id;
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
i
}
}
fn free(&mut self, i: usize) -> MaskIdx {
self.free.push(i);
self.instances[i].mask_idx
}
fn apply_free(&mut self, kind: InstanceKind) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a));
let instances = &mut self.instances;
let assoc = &mut self.assoc;
self.free.drain(..).filter_map(move |i| {
instances.swap_remove(i);
assoc.swap_remove(i);
if i == instances.len() {
return None;
}
let id = assoc[i];
let old = instances.len();
Some(PrimitiveChange {
id,
kind,
old,
new: i,
})
})
}
}
pub trait Primitive: Pod { pub trait Primitive: Pod {
const BINDING: u32; const BINDING: u32;
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>; fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
@@ -89,8 +148,6 @@ macro_rules! primitives {
} }
$( $(
// Each is uploaded as its WGSL counterpart, so its fields have to
// sit at WGSL's offsets -- including an align Rust would not pick.
unsafe impl bytemuck::Pod for $ty {} unsafe impl bytemuck::Pod for $ty {}
unsafe impl bytemuck::Zeroable for $ty {} unsafe impl bytemuck::Zeroable for $ty {}
impl Primitive for $ty { impl Primitive for $ty {
@@ -112,32 +169,63 @@ pub struct PrimitiveInst<P> {
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
} }
/// The instanced half of a layer: a list, plus the group-1 data it reads.
#[derive(Default)]
pub struct Primitives {
list: InstanceList,
data: PrimitiveData,
}
impl Primitives { impl Primitives {
pub fn write<P: Primitive>( fn write<P: Primitive>(
&mut self, &mut self,
layer: usize,
PrimitiveInst { PrimitiveInst {
id, id,
primitive, primitive,
region, region,
mask_idx, mask_idx,
}: PrimitiveInst<P>, }: PrimitiveInst<P>,
) -> PrimitiveHandle { ) -> usize {
let data_idx = P::vec(&mut self.data).add(primitive); let idx = P::vec(&mut self.data).add(primitive) as u32;
self.push( self.list.push(
layer,
id, id,
PrimitiveInstance { PrimitiveInstance {
region, region,
idx: data_idx as u32, idx,
mask_idx, mask_idx,
binding: P::BINDING, binding: P::BINDING,
}, },
Some(data_idx),
) )
} }
/// Writes an instance that samples `texture` instead of a group-1 buffer. fn free(&mut self, i: usize) -> MaskIdx {
// The instance says where its group-1 entry is, so the handle need not.
let inst = self.list.instances[i];
self.data.free(inst.binding, inst.idx as usize);
self.list.free(i)
}
pub fn data(&self) -> &PrimitiveData {
&self.data
}
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
&self.list.instances
}
}
impl LayerDraws {
pub fn write<P: Primitive>(&mut self, layer: usize, inst: PrimitiveInst<P>) -> PrimitiveHandle {
self.updated = true;
PrimitiveHandle {
layer,
kind: InstanceKind::Primitive,
inst_idx: self.primitives.write(inst),
}
}
/// Writes an instance that samples the texture in slot `texture` instead of
/// reading a group-1 buffer.
pub fn write_texture( pub fn write_texture(
&mut self, &mut self,
layer: usize, layer: usize,
@@ -146,8 +234,11 @@ impl Primitives {
region: UiRegion, region: UiRegion,
mask_idx: MaskIdx, mask_idx: MaskIdx,
) -> PrimitiveHandle { ) -> PrimitiveHandle {
self.push( self.updated = true;
PrimitiveHandle {
layer, layer,
kind: InstanceKind::Texture,
inst_idx: self.textures.push(
id, id,
PrimitiveInstance { PrimitiveInstance {
region, region,
@@ -155,76 +246,46 @@ impl Primitives {
mask_idx, mask_idx,
binding: TEXTURE_BINDING, binding: TEXTURE_BINDING,
}, },
None, ),
)
}
fn push(
&mut self,
layer: usize,
id: WidgetId,
inst: PrimitiveInstance,
data_idx: Option<usize>,
) -> PrimitiveHandle {
self.updated = true;
let inst_idx = if let Some(i) = self.free.pop() {
self.instances[i] = inst;
self.assoc[i] = id;
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
i
};
PrimitiveHandle {
layer,
inst_idx,
data_idx,
binding: inst.binding,
} }
} }
/// returns (old index, new index)
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> { pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a)); let Self {
self.free.drain(..).filter_map(|i| { primitives,
self.instances.swap_remove(i); textures,
self.assoc.swap_remove(i); ..
if i == self.instances.len() { } = self;
return None; primitives
} .list
let id = self.assoc[i]; .apply_free(InstanceKind::Primitive)
let old = self.instances.len(); .chain(textures.apply_free(InstanceKind::Texture))
Some(PrimitiveChange { id, old, new: i })
})
} }
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.updated = true; self.updated = true;
if let Some(i) = h.data_idx { match h.kind {
self.data.free(h.binding, i); InstanceKind::Primitive => self.primitives.free(h.inst_idx),
InstanceKind::Texture => self.textures.free(h.inst_idx),
} }
self.free.push(h.inst_idx);
self.instances[h.inst_idx].mask_idx
}
pub fn data(&self) -> &PrimitiveData {
&self.data
}
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
&self.instances
} }
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion { pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.updated = true; self.updated = true;
&mut self.instances[h.inst_idx].region &mut self.list_mut(h.kind).instances[h.inst_idx].region
}
fn list_mut(&mut self, kind: InstanceKind) -> &mut InstanceList {
match kind {
InstanceKind::Primitive => &mut self.primitives.list,
InstanceKind::Texture => &mut self.textures,
}
} }
} }
pub struct PrimitiveChange { pub struct PrimitiveChange {
pub id: WidgetId, pub id: WidgetId,
pub kind: InstanceKind,
pub old: usize, pub old: usize,
pub new: usize, pub new: usize,
} }
@@ -232,10 +293,8 @@ pub struct PrimitiveChange {
#[derive(Debug)] #[derive(Debug)]
pub struct PrimitiveHandle { pub struct PrimitiveHandle {
pub layer: usize, pub layer: usize,
pub kind: InstanceKind,
pub inst_idx: usize, pub inst_idx: usize,
/// `None` for a texture instance, which has no group-1 entry to free.
pub data_idx: Option<usize>,
pub binding: u32,
} }
primitives!( primitives!(
@@ -265,9 +324,6 @@ impl RectPrimitive {
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph /// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects. /// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
///
/// `align(8)` because `vec2<f32>` aligns `GlyphInfo` to 8, which pads it to
/// 32 bytes.
#[repr(C, align(8))] #[repr(C, align(8))]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive { pub struct GlyphPrimitive {
+2 -2
View File
@@ -45,8 +45,8 @@ struct UiVec2 {
abs: vec2<f32>, abs: vec2<f32>,
} }
// Whatever this run of instances samples: the glyph atlas, whose layers are // What this draw samples: the glyph atlas, whose layers are its pages, or one
// its pages, or one standalone image as an array of one. // standalone image as an array of one.
@group(2) @binding(0) @group(2) @binding(0)
var tex: texture_2d_array<f32>; var tex: texture_2d_array<f32>;
@group(2) @binding(1) @group(2) @binding(1)
+31 -17
View File
@@ -1,4 +1,4 @@
use image::{DynamicImage, EncodableLayout, GenericImageView}; use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
use wgpu::{util::DeviceExt, *}; use wgpu::{util::DeviceExt, *};
use crate::{PatchRect, TextureUpdate, Textures}; use crate::{PatchRect, TextureUpdate, Textures};
@@ -90,15 +90,7 @@ impl GpuTextures {
let Some(Some(slot)) = self.slots.get(i as usize) else { let Some(Some(slot)) = self.slots.get(i as usize) else {
return; return;
}; };
if rect.width == 0 || rect.height == 0 { let dst = TexelCopyTextureInfo {
return;
}
// `write_texture` requires tightly packed rows, unlike the source image.
let sub = image
.view(rect.x, rect.y, rect.width, rect.height)
.to_image();
self.queue.write_texture(
TexelCopyTextureInfo {
texture: &slot.texture, texture: &slot.texture,
mip_level: 0, mip_level: 0,
origin: Origin3d { origin: Origin3d {
@@ -107,11 +99,34 @@ impl GpuTextures {
z: 0, z: 0,
}, },
aspect: TextureAspect::All, aspect: TextureAspect::All,
}, };
sub.as_bytes(), match image.as_rgba8() {
Some(rgba) => write_region(&self.queue, dst, rgba, rect),
// The texture is rgba8, so any other layout has to be converted --
// and converting the rectangle is cheaper than the whole image.
None => {
let sub = image
.view(rect.x, rect.y, rect.width, rect.height)
.to_image();
write_region(&self.queue, dst, &sub, PatchRect { x: 0, y: 0, ..rect });
}
}
}
}
/// Uploads `rect` of `src` without copying it out first: `write_texture` takes
/// a row stride, so a region can be addressed where it already is.
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
if rect.width == 0 || rect.height == 0 {
return;
}
let stride = src.width() * 4;
queue.write_texture(
dst,
src.as_bytes(),
TexelCopyBufferLayout { TexelCopyBufferLayout {
offset: 0, offset: (rect.y * stride + rect.x * 4) as u64,
bytes_per_row: Some(rect.width * 4), bytes_per_row: Some(stride),
rows_per_image: Some(rect.height), rows_per_image: Some(rect.height),
}, },
Extent3d { Extent3d {
@@ -120,11 +135,10 @@ impl GpuTextures {
depth_or_array_layers: 1, depth_or_array_layers: 1,
}, },
); );
}
} }
/// One array texture and one sampler, so the atlas and a standalone image bind /// One array texture and a sampler, so the atlas and a standalone image share a
/// the same layout and the shader samples whichever is bound. An image is a /// layout and the shader samples whichever is bound -- an image being a
/// single-layer texture viewed as an array of one. /// single-layer texture viewed as an array of one.
pub fn texture_group( pub fn texture_group(
device: &Device, device: &Device,
+3
View File
@@ -33,6 +33,9 @@ impl<T: Pod> ArrBuf<T> {
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data)); queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
resized resized
} }
pub fn len(&self) -> usize {
self.len
}
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer { fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64; let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) { if usage.contains(BufferUsages::STORAGE) {
+7 -10
View File
@@ -1,13 +1,13 @@
use crate::{ use crate::{
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx, ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
ui::cache::Cache, ui::cache::Cache,
util::{HashMap, HashSet, Vec2, forget_ref}, util::{HashMap, HashSet, Vec2, forget_ref},
}; };
pub struct UiRenderState { pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub layers: PrimitiveLayers, pub layers: DrawLayers,
pub(super) output_size: Vec2, pub(super) output_size: Vec2,
pub cache: Cache, pub cache: Cache,
@@ -243,14 +243,11 @@ impl UiRenderState {
} }
pub fn debug_layers(&self) { pub fn debug_layers(&self) {
for ((idx, depth), primitives) in self.layers.iter_depth() { for ((idx, depth), draws) in self.layers.iter_depth() {
let indent = " ".repeat(depth * 2); let indent = " ".repeat(depth * 2);
let len = primitives.instances().len(); let primitives = draws.primitives.instances().len();
print!("{indent}{idx}: {len} primitives"); let textures = draws.textures.instances().len();
if len >= 1 { println!("{indent}{idx}: {primitives} primitives, {textures} textures");
print!(" ({})", primitives.instances()[0].binding);
}
println!();
} }
} }
-3
View File
@@ -83,9 +83,6 @@ impl UiRenderer {
.block_on() .block_on()
.expect("Could not get adapter!"); .expect("Could not get adapter!");
// No binding-array features or limits: the atlas is one
// texture_2d_array and an image its own bind group, so nothing here
// needs VK_EXT_descriptor_indexing the way the old layout did.
let (device, queue) = adapter let (device, queue) = adapter
.request_device(&DeviceDescriptor { .request_device(&DeviceDescriptor {
required_limits: Limits { required_limits: Limits {