iris: give masks/move_offsets their own bind group, fixing O(N) image append

GpuTextures folded the masks and move_offsets storage buffers into every
standalone image's own bind group (group 2), alongside that image's
texture view. Since ArrBuf::update hands back a new Buffer identity
whenever either buffer's length changes -- which a widget getting its
first move-offset slot can trigger, unrelated to any image -- every
live image's bind group had to be rebuilt whenever either buffer grew.
Appending a 1,001st image to 1,000 already-settled ones cost 1,001
bind-group creates, not 1 (IRIS_TODO.md, run-bench.sh images).

Moved both buffers into their own bind group (group 3 in shader.wgsl
and UiRenderNode), bound once per frame in draw() rather than once per
per-image bind group. GpuTextures's image bind groups now only
reference the atlas array view, the image's own view and the sampler --
none of which change when masks/move_offsets resize -- so a resize
touches exactly one bind group regardless of how many images are live.
This also closes the "two frames to reach steady state" item, which was
the same bug measured a second way.

Verified: cargo build/clippy/test clean (19 tests), cargo ndk build/clippy
clean, run-headless.sh tabs --shot byte-identical (27266 bytes). New
run-bench.sh images numbers: cold load unchanged at 1000/0/0/0, append
now 1 instead of 1001. Both Fix items in IRIS_TODO.md ticked with the
before/after numbers.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
irisandClaude Sonnet committed 2026-09-05 05:28:38 -04:00
1 parent e2873df92e
commit 19c36e37f2
4 files changed
+185 -149

No files matched your search

+59 -31
View File
@@ -26,43 +26,61 @@ order and what "done" looks like. Tick and date them in place.
still reaches the button — confirmed to fail on the pre-fix code and still reaches the button — confirmed to fail on the pre-fix code and
pass after. pass after.
- [ ] **Appending one image to an already-loaded list rebuilds every other - [x] **Appending one image to an already-loaded list rebuilds every other
image's bind group (2026-09-05).** Found by the benchmark below, not image's bind group (2026-09-05, fixed 2026-09-05).** Found by the
designed against: `GpuTextures::update` (`core/src/render/texture.rs`) benchmark below: `GpuTextures::update` (`core/src/render/texture.rs`)
triggers `rebuild_image_bind_groups` — a loop over *every live triggered `rebuild_image_bind_groups` — a loop over *every live
standalone image*, rebuilding its `BindGroup` — whenever the shared standalone image*, rebuilding its `BindGroup` — whenever the shared
`masks` or `move_offsets` GPU buffer is resized (`masks_resized || `masks` or `move_offsets` GPU buffer was resized (`masks_resized ||
moves_resized` in `UiRenderNode::update`, `core/src/render/mod.rs`), and moves_resized` in `UiRenderNode::update`, `core/src/render/mod.rs`), and
a widget getting its *first* move-offset slot (LAYOUT.md section 2 — a widget getting its *first* move-offset slot (LAYOUT.md section 2 —
every widget gets one on first draw) can be exactly what grows that every widget gets one on first draw) could be exactly what grows that
buffer. So one new message with one new image, appended to a transcript buffer. So one new message with one new image, appended to a transcript
that already has N images loaded, does not cost O(1): it costs one that already has N images loaded, did not cost O(1): it cost one
`create_image` for the new image plus one `make_image_bind_group` per `create_image` for the new image plus one `make_image_bind_group` per
*existing* image, because the new widget's own move slot pushed the *existing* image, because the new widget's own move slot pushed the
arena past its capacity. Measured directly in arena past its capacity. Measured directly in
`iris/examples/bench_images.rs`: appending a 1,001st image to 1,000 `iris/examples/bench_images.rs`: appending a 1,001st image to 1,000
already-settled ones reports **1,001** bind-group creates for that one already-settled ones reported **1,001** bind-group creates for that one
frame, not 1 (`./run-bench.sh images`, frame 5 in the transcript below). frame, not 1 (`./run-bench.sh images`, frame 5 in the transcript below).
This is the same class of cost LAYOUT.md's move chain exists to avoid
elsewhere in the codebase, just not yet closed off here — the fix is **Fix**: `masks`/`move_offsets` never belonged in a standalone image's own
presumably to size `masks`/`move_offsets` with headroom (the array bind group (group 2) in the first place — the group also holds that
texture already grows by doubling, `grow_array`, for the same reason) so image's own texture view, which is the only thing that is genuinely
an ordinary append does not cross a capacity boundary, or to stop tying per-image, so a buffer shared by *everything* forced a rebuild of
the *image* bind group's contents to a buffer that changes on every new *every* group the moment it moved. Gave masks/move_offsets their own
widget in the whole tree, image or not. Not designed further here per bind group (group 3 in `shader.wgsl` and `UiRenderNode`: `masks_layout`/
the "do not redesign, record it" instruction this benchmark was built `masks_group`), bound once per frame in `UiRenderNode::draw` rather than
under. once per draw call, instead of duplicating them into every per-image
- [ ] **Bind-group creation takes two frames to reach the steady state, not group. `GpuTextures` and its image bind groups now know nothing about
one (2026-09-05).** Same benchmark: loading 1,000 images cold reports either buffer — `rebuild_image_bind_groups` is called only from
1,000 creates on frame 1 (expected — this is `create_image`, one per `grow_array` (the atlas array texture growing, which genuinely does
new image) *and again* 1,000 on frame 2, with nothing between the two change what every image's own bind group must reference) — so a
frames marked dirty, before settling to 0 from frame 3. The second masks/move_offsets resize now touches exactly one bind group, ever,
frame's 1,000 is `rebuild_image_bind_groups` again, for the same regardless of how many images are live. Numbers after the fix, same
masks/move-offsets buffer-growth reason as the item above — the arena benchmark and command:
apparently does not finish growing to its steady size within the first
frame the tree is drawn. Not chased further; recorded so whoever fixes ./run-bench.sh images
the item above checks whether the fix also closes this one, since they frame=1 bind_group_creates=1000 (cold load, unchanged)
look like the same root cause measured two different ways. frame=2 bind_group_creates=0 (was 1000 -- see the item below)
frame=3 bind_group_creates=0
frame=4 bind_group_creates=0
(append one image here)
frame=5 bind_group_creates=1 (was 1001)
frame=6 bind_group_creates=0
`run-headless.sh tabs --shot` still 27266 bytes, byte-for-byte unchanged,
confirming the bind-group restructuring changed nothing about what is
drawn.
- [x] **Bind-group creation takes two frames to reach the steady state, not
one (2026-09-05, closed by the fix above, 2026-09-05).** Same benchmark:
loading 1,000 images cold used to report 1,000 creates on frame 1
(expected — `create_image`, one per new image) *and again* 1,000 on
frame 2, before settling to 0 from frame 3. This was `rebuild_image_bind_groups`
firing a second time for the same masks/move-offsets buffer-growth
reason as the item above, confirming the guess recorded here — the two
were exactly the same root cause measured two different ways. Frame 2
now reports 0 (see the numbers above); not a separate fix.
## Build ## Build
@@ -123,7 +141,7 @@ order and what "done" looks like. Tick and date them in place.
draws=320 rewrites=40 moves=160 (identical at every N) draws=320 rewrites=40 moves=160 (identical at every N)
per-line average: 0.0012-0.0013ms (identical at every N) per-line average: 0.0012-0.0013ms (identical at every N)
cd iris && ./run-bench.sh images cd iris && ./run-bench.sh images (2026-09-05, before the fix)
frame=1 bind_group_creates=1000 (cold load) frame=1 bind_group_creates=1000 (cold load)
frame=2 bind_group_creates=1000 (see Fix item above) frame=2 bind_group_creates=1000 (see Fix item above)
frame=3 bind_group_creates=0 frame=3 bind_group_creates=0
@@ -132,6 +150,15 @@ order and what "done" looks like. Tick and date them in place.
frame=5 bind_group_creates=1001 (see Fix item above) frame=5 bind_group_creates=1001 (see Fix item above)
frame=6 bind_group_creates=0 frame=6 bind_group_creates=0
cd iris && ./run-bench.sh images (2026-09-05, after the fix)
frame=1 bind_group_creates=1000 (cold load, unchanged -- genuine work)
frame=2 bind_group_creates=0
frame=3 bind_group_creates=0
frame=4 bind_group_creates=0
(append one image here)
frame=5 bind_group_creates=1 (one image's own create_image, O(1))
frame=6 bind_group_creates=0
**Reading it**: (a) is real, necessary work — shaping and laying out N **Reading it**: (a) is real, necessary work — shaping and laying out N
never-before-seen text rows — and scales with N as it must, ~10x cost never-before-seen text rows — and scales with N as it must, ~10x cost
per 10x N. (b) and (c) are the pass conditions that matter: both are per 10x N. (b) and (c) are the pass conditions that matter: both are
@@ -140,8 +167,9 @@ order and what "done" looks like. Tick and date them in place.
the message list — draws/moves per tick or per line do not grow with the message list — draws/moves per tick or per line do not grow with
list size, and the per-operation cost (a fraction of a microsecond) is list size, and the per-operation cost (a fraction of a microsecond) is
nowhere near a frame budget. (d)'s cold-load and steady-state halves nowhere near a frame budget. (d)'s cold-load and steady-state halves
behave as designed; its *append* half did not, which is the two Fix behave as designed; its *append* half did not, until the fix above moved
items above. masks/move_offsets out of the per-image bind group — now flat at O(1)
the same way (b) and (c) are.
- [ ] **Masks defined relative to each other.** Wanted: mask A multiplies - [ ] **Masks defined relative to each other.** Wanted: mask A multiplies
by something *and also* applies mask B — a mask can reference a parent by something *and also* applies mask B — a mask can reference a parent
mask, the way the move chain references a parent offset. Today masks mask, the way the move chain references a parent offset. Today masks
+94 -46
View File
@@ -35,6 +35,19 @@ pub struct UiRenderNode {
textures: GpuTextures, textures: GpuTextures,
masks: ArrBuf<Mask>, masks: ArrBuf<Mask>,
move_offsets: ArrBuf<MoveOffset>, move_offsets: ArrBuf<MoveOffset>,
/// Group 3: the masks and move-offsets storage buffers, on their own --
/// see IRIS_TODO.md's "Appending one image ... rebuilds every other
/// image's bind group". These used to live in group 2 alongside each
/// standalone image's own texture view, so an image's bind group named
/// the masks/move_offsets buffer directly; the moment either buffer
/// resized (which a widget getting its *first* move slot can trigger,
/// unrelated to any image), `ArrBuf::update` handed back a new `Buffer`
/// identity and every image's bind group -- one per live image -- had
/// to be rebuilt to reference it. Pulling both buffers into their own
/// group, bound once per frame rather than once per draw call, means a
/// buffer resize now rebuilds exactly this one group instead of N.
masks_layout: BindGroupLayout,
masks_group: BindGroup,
} }
struct RenderLayer { struct RenderLayer {
@@ -54,6 +67,13 @@ impl UiRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) { pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline); pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_group, &[]); pass.set_bind_group(0, &self.uniform_group, &[]);
// Set once, not per layer or per image: masks/move_offsets are read
// by every primitive and every standalone image alike, and living
// in their own group (rather than folded into group 2 alongside the
// per-image texture view) is what keeps an image's own bind group
// from naming a buffer that changes size on an unrelated widget's
// first draw -- see the comment on `masks_group` below.
pass.set_bind_group(3, &self.masks_group, &[]);
for i in &self.active { for i in &self.active {
let layer = &self.layers[i]; let layer = &self.layers[i];
if layer.instance.len() == 0 && layer.image_instance.len() == 0 { if layer.instance.len() == 0 && layer.image_instance.len() == 0 {
@@ -164,21 +184,13 @@ impl UiRenderNode {
} else { } else {
false false
}; };
let rebuild_main = self.textures.update( if masks_resized || moves_resized {
&mut ui.textures, self.masks_group =
&self.rsc_layout, Self::masks_group(device, &self.masks_layout, &self.masks, &self.move_offsets);
&self.masks, }
&self.move_offsets, let rebuild_main = self.textures.update(&mut ui.textures, &self.rsc_layout);
masks_resized || moves_resized,
);
if rebuild_main { if rebuild_main {
self.rsc_group = Self::rsc_group( self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures);
device,
&self.rsc_layout,
&self.textures,
&self.masks,
&self.move_offsets,
);
} }
} }
@@ -265,11 +277,18 @@ impl UiRenderNode {
); );
let rsc_layout = Self::rsc_layout(device); let rsc_layout = Self::rsc_layout(device);
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks, &move_offsets); let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager);
let masks_layout = Self::masks_layout(device);
let masks_group = Self::masks_group(device, &masks_layout, &masks, &move_offsets);
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"), label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout], bind_group_layouts: &[
&uniform_layout,
&primitive_layout,
&rsc_layout,
&masks_layout,
],
immediate_size: 0, immediate_size: 0,
}); });
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
@@ -322,6 +341,8 @@ impl UiRenderNode {
textures: tex_manager, textures: tex_manager,
masks, masks,
move_offsets, move_offsets,
masks_layout,
masks_group,
} }
} }
@@ -355,12 +376,13 @@ impl UiRenderNode {
}) })
} }
/// Group 2: the shared atlas array, one standalone-image slot (a null /// Group 2: the shared atlas array and one standalone-image slot (a null
/// view for the main draw, a real one for each image's own bind group -- /// view for the main draw, a real one for each image's own bind group --
/// see `GpuTextures`), one sampler and the masks buffer. No `count` on /// see `GpuTextures`), plus one sampler. No `count` on any entry: this
/// any entry: this needs nothing beyond plain Vulkan 1.0 / GLES /// needs nothing beyond plain Vulkan 1.0 / GLES sampling, unlike the
/// sampling, unlike the `binding_array` layout it replaced (see /// `binding_array` layout it replaced (see TEXTURES.md's "Recommended
/// TEXTURES.md's "Recommended shape"). /// shape"). Masks and move_offsets are deliberately *not* here -- see
/// `masks_layout` below for why they get their own group.
fn rsc_layout(device: &Device) -> BindGroupLayout { fn rsc_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
@@ -390,26 +412,6 @@ impl UiRenderNode {
ty: BindingType::Sampler(SamplerBindingType::NonFiltering), ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: None, count: None,
}, },
BindGroupLayoutEntry {
binding: 3,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
BindGroupLayoutEntry {
binding: 4,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
], ],
label: Some("ui rsc"), label: Some("ui rsc"),
}) })
@@ -421,8 +423,6 @@ impl UiRenderNode {
device: &Device, device: &Device,
layout: &BindGroupLayout, layout: &BindGroupLayout,
tex_manager: &GpuTextures, tex_manager: &GpuTextures,
masks: &ArrBuf<Mask>,
move_offsets: &ArrBuf<MoveOffset>,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
layout, layout,
@@ -439,16 +439,64 @@ impl UiRenderNode {
binding: 2, binding: 2,
resource: BindingResource::Sampler(tex_manager.sampler()), resource: BindingResource::Sampler(tex_manager.sampler()),
}, },
],
label: Some("ui rsc"),
})
}
/// Group 3: the masks and move_offsets storage buffers, shared by the
/// main draw and every standalone image alike (see the field comment on
/// `masks_group`). Bound once per frame in `draw()` rather than folded
/// into group 2, so a resize of either buffer -- which an unrelated
/// widget's first move slot can trigger -- rebuilds this one group
/// instead of every image's.
fn masks_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: Some("ui masks"),
})
}
fn masks_group(
device: &Device,
layout: &BindGroupLayout,
masks: &ArrBuf<Mask>,
move_offsets: &ArrBuf<MoveOffset>,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry { BindGroupEntry {
binding: 3, binding: 0,
resource: masks.buffer.as_entire_binding(), resource: masks.buffer.as_entire_binding(),
}, },
BindGroupEntry { BindGroupEntry {
binding: 4, binding: 1,
resource: move_offsets.buffer.as_entire_binding(), resource: move_offsets.buffer.as_entire_binding(),
}, },
], ],
label: Some("ui rsc"), label: Some("ui masks"),
}) })
} }
+5 -2
View File
@@ -72,9 +72,12 @@ var atlas: texture_2d_array<f32>;
var image_texture: texture_2d<f32>; var image_texture: texture_2d<f32>;
@group(2) @binding(2) @group(2) @binding(2)
var samp: sampler; var samp: sampler;
@group(2) @binding(3) // Their own group, bound once per frame rather than folded into group 2: see
// UiRenderNode::masks_layout for why an image's own bind group must not name
// either buffer.
@group(3) @binding(0)
var<storage> masks: array<Mask>; var<storage> masks: array<Mask>;
@group(2) @binding(4) @group(3) @binding(1)
var<storage> move_offsets: array<MoveOffset>; var<storage> move_offsets: array<MoveOffset>;
// A move chain more than this deep means something else is wrong (an // A move chain more than this deep means something else is wrong (an
+27 -70
View File
@@ -1,9 +1,7 @@
use image::{DynamicImage, EncodableLayout, GenericImageView}; use image::{DynamicImage, EncodableLayout, GenericImageView};
use wgpu::{util::DeviceExt, *}; use wgpu::{util::DeviceExt, *};
use crate::{ use crate::{PatchRect, TextureKind, TextureUpdate, Textures};
Mask, MoveOffset, PatchRect, TextureKind, TextureUpdate, Textures, render::util::ArrBuf,
};
use super::atlas::PAGE; use super::atlas::PAGE;
@@ -74,32 +72,21 @@ pub struct GpuTextures {
impl GpuTextures { impl GpuTextures {
/// Applies queued `Textures` updates, then reports whether the *main* /// Applies queued `Textures` updates, then reports whether the *main*
/// bind group (the one rects and glyphs draw with) needs rebuilding -- /// bind group (the one rects and glyphs draw with) needs rebuilding --
/// true when the atlas array was recreated (its view identity changed) /// true exactly when the atlas array was recreated (its view identity
/// or the masks buffer was, since both are bound there. Pushing or /// changed). Pushing or freeing a standalone image never touches that
/// freeing a standalone image never touches that group: it built or drops /// group: it built or drops its own. Masks/move_offsets resizing is
/// its own. /// `UiRenderNode`'s own concern now (its `masks_group`, group 3) --
pub fn update( /// see that struct's field comment for why standalone images no longer
&mut self, /// hear about either buffer at all.
textures: &mut Textures, pub fn update(&mut self, textures: &mut Textures, rsc_layout: &BindGroupLayout) -> bool {
rsc_layout: &BindGroupLayout, let mut rebuild_main = false;
masks: &ArrBuf<Mask>,
move_offsets: &ArrBuf<MoveOffset>,
masks_resized: bool,
) -> bool {
let mut rebuild_main = masks_resized;
if masks_resized {
// The masks or move-offsets buffer just moved, so every bind
// group holding a reference to either -- one per live
// standalone image -- is stale.
self.rebuild_image_bind_groups(rsc_layout, masks, move_offsets);
}
for update in textures.updates() { for update in textures.updates() {
match update { match update {
TextureUpdate::Push(kind, image) => { TextureUpdate::Push(kind, image) => {
rebuild_main |= self.push(kind, image, rsc_layout, masks, move_offsets); rebuild_main |= self.push(kind, image, rsc_layout);
} }
TextureUpdate::Set(kind, i, image) => { TextureUpdate::Set(kind, i, image) => {
rebuild_main |= self.set(kind, i, image, rsc_layout, masks, move_offsets); rebuild_main |= self.set(kind, i, image, rsc_layout);
} }
// A patch changes texture contents, not which layer or bind // A patch changes texture contents, not which layer or bind
// group exists, so it never asks for a rebuild -- rebuilding // group exists, so it never asks for a rebuild -- rebuilding
@@ -118,10 +105,8 @@ impl GpuTextures {
kind: TextureKind, kind: TextureKind,
image: &DynamicImage, image: &DynamicImage,
rsc_layout: &BindGroupLayout, rsc_layout: &BindGroupLayout,
masks: &ArrBuf<Mask>,
move_offsets: &ArrBuf<MoveOffset>,
) -> bool { ) -> bool {
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks, move_offsets); let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout);
self.slots.push(slot); self.slots.push(slot);
rebuilt rebuilt
} }
@@ -132,10 +117,8 @@ impl GpuTextures {
i: u32, i: u32,
image: &DynamicImage, image: &DynamicImage,
rsc_layout: &BindGroupLayout, rsc_layout: &BindGroupLayout,
masks: &ArrBuf<Mask>,
move_offsets: &ArrBuf<MoveOffset>,
) -> bool { ) -> bool {
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks, move_offsets); let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout);
self.slots[i as usize] = slot; self.slots[i as usize] = slot;
rebuilt rebuilt
} }
@@ -145,18 +128,16 @@ impl GpuTextures {
kind: TextureKind, kind: TextureKind,
image: &DynamicImage, image: &DynamicImage,
rsc_layout: &BindGroupLayout, rsc_layout: &BindGroupLayout,
masks: &ArrBuf<Mask>,
move_offsets: &ArrBuf<MoveOffset>,
) -> (Slot, bool) { ) -> (Slot, bool) {
match kind { match kind {
TextureKind::Image => { TextureKind::Image => {
let gpu = self.create_image(image, rsc_layout, masks, move_offsets); let gpu = self.create_image(image, rsc_layout);
(Slot::Image(gpu), false) (Slot::Image(gpu), false)
} }
TextureKind::Page { layer } => { TextureKind::Page { layer } => {
let mut rebuilt = false; let mut rebuilt = false;
if layer >= self.array_capacity { if layer >= self.array_capacity {
self.grow_array(rsc_layout, masks, move_offsets); self.grow_array(rsc_layout);
rebuilt = true; rebuilt = true;
} }
self.write_full_layer(layer, image); self.write_full_layer(layer, image);
@@ -244,12 +225,7 @@ impl GpuTextures {
/// copies the old layers across GPU-side -- no readback. Recreates the /// copies the old layers across GPU-side -- no readback. Recreates the
/// array's view, which invalidates every bind group that referenced it, /// array's view, which invalidates every bind group that referenced it,
/// so this also rebuilds all of them before returning. /// so this also rebuilds all of them before returning.
fn grow_array( fn grow_array(&mut self, rsc_layout: &BindGroupLayout) {
&mut self,
rsc_layout: &BindGroupLayout,
masks: &ArrBuf<Mask>,
move_offsets: &ArrBuf<MoveOffset>,
) {
let new_capacity = self.array_capacity * 2; let new_capacity = self.array_capacity * 2;
let new_texture = Self::create_array_texture(&self.device, new_capacity); let new_texture = Self::create_array_texture(&self.device, new_capacity);
if self.page_count > 0 { if self.page_count > 0 {
@@ -285,15 +261,14 @@ impl GpuTextures {
..Default::default() ..Default::default()
}); });
self.array_capacity = new_capacity; self.array_capacity = new_capacity;
self.rebuild_image_bind_groups(rsc_layout, masks, move_offsets); self.rebuild_image_bind_groups(rsc_layout);
} }
fn rebuild_image_bind_groups( /// Called only from `grow_array`: the atlas array's view identity is the
&mut self, /// one thing an image's bind group (group 2) still names that can
rsc_layout: &BindGroupLayout, /// change out from under it. Masks/move_offsets resizing no longer
masks: &ArrBuf<Mask>, /// reaches here at all -- see `UiRenderNode::masks_group`.
move_offsets: &ArrBuf<MoveOffset>, fn rebuild_image_bind_groups(&mut self, rsc_layout: &BindGroupLayout) {
) {
for slot in &mut self.slots { for slot in &mut self.slots {
if let Slot::Image(gpu) = slot { if let Slot::Image(gpu) = slot {
gpu.bind_group = Self::make_image_bind_group( gpu.bind_group = Self::make_image_bind_group(
@@ -302,21 +277,13 @@ impl GpuTextures {
&self.array_view, &self.array_view,
&gpu.view, &gpu.view,
&self.sampler, &self.sampler,
masks,
move_offsets,
); );
self.bind_group_creates += 1; self.bind_group_creates += 1;
} }
} }
} }
fn create_image( fn create_image(&mut self, image: &DynamicImage, rsc_layout: &BindGroupLayout) -> ImageGpu {
&mut self,
image: &DynamicImage,
rsc_layout: &BindGroupLayout,
masks: &ArrBuf<Mask>,
move_offsets: &ArrBuf<MoveOffset>,
) -> ImageGpu {
let rgba = image.to_rgba8(); let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions(); let (width, height) = rgba.dimensions();
let texture = self.device.create_texture_with_data( let texture = self.device.create_texture_with_data(
@@ -345,8 +312,6 @@ impl GpuTextures {
&self.array_view, &self.array_view,
&view, &view,
&self.sampler, &self.sampler,
masks,
move_offsets,
); );
self.bind_group_creates += 1; self.bind_group_creates += 1;
ImageGpu { ImageGpu {
@@ -357,16 +322,16 @@ impl GpuTextures {
} }
/// Builds group 2 for one standalone image: the shared atlas array, this /// Builds group 2 for one standalone image: the shared atlas array, this
/// image's own view, the shared sampler, and the shared masks buffer -- /// image's own view and the shared sampler -- the same layout the main
/// the same layout the main draw uses with a null view in the image slot. /// draw uses with a null view in the image slot. Deliberately does not
/// touch masks/move_offsets (group 3, `UiRenderNode::masks_group`): see
/// that field's comment for why folding them in here was the bug.
fn make_image_bind_group( fn make_image_bind_group(
device: &Device, device: &Device,
rsc_layout: &BindGroupLayout, rsc_layout: &BindGroupLayout,
array_view: &TextureView, array_view: &TextureView,
image_view: &TextureView, image_view: &TextureView,
sampler: &Sampler, sampler: &Sampler,
masks: &ArrBuf<Mask>,
move_offsets: &ArrBuf<MoveOffset>,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
layout: rsc_layout, layout: rsc_layout,
@@ -383,14 +348,6 @@ impl GpuTextures {
binding: 2, binding: 2,
resource: BindingResource::Sampler(sampler), resource: BindingResource::Sampler(sampler),
}, },
BindGroupEntry {
binding: 3,
resource: masks.buffer.as_entire_binding(),
},
BindGroupEntry {
binding: 4,
resource: move_offsets.buffer.as_entire_binding(),
},
], ],
label: Some("ui rsc image"), label: Some("ui rsc image"),
}) })