From 444a2cd1386557906a03e2fabd2c6a655fe0ebae Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sun, 13 Sep 2026 17:23:35 -0400 Subject: [PATCH] Zip the layer's lists against their pipelines, and stop explaining wgpu wrongly The update loop zips all three and asserts up front that no list is left without a pipeline, instead of indexing the pipelines to get that guarantee. The comment on the data layout claimed a `None` minimum takes its value from the first pipeline built against the layout. That is not documented and does not reproduce -- one shared layout with `None` renders the tabs example correctly today. What is documented is that a stated size is checked when the bind group and pipeline are created, and `None` is checked on every draw, so that is what the comment says now. Co-Authored-By: Claude Opus 5 --- core/src/render/mod.rs | 24 ++++++++++-------------- core/src/ui/painter.rs | 1 + 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index 8a31049..a5e985c 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -117,24 +117,22 @@ impl UiRenderNode { } let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new); if draws.updated { - rlayer - .primitives - .resize_with(draws.primitives().len(), || None); - for (id, (buffers, list)) in rlayer + let lists = draws.primitives(); + // The zip would otherwise skip a list with no pipeline. + assert!(lists.len() <= self.primitives.len()); + rlayer.primitives.resize_with(lists.len(), || None); + for ((buffers, list), primitive) in rlayer .primitives .iter_mut() - .zip(draws.primitives()) - .enumerate() + .zip(lists) + .zip(&self.primitives) { let Some(list) = list else { continue; }; - // Indexed, not zipped: a missing pipeline should say so - // rather than quietly leave the list unbuilt. - let layout = &self.primitives[id].data_layout; buffers .get_or_insert_with(|| ListBuffers::new(device)) - .update(device, queue, layout, list); + .update(device, queue, &primitive.data_layout, list); } draws.updated = false; } @@ -364,9 +362,8 @@ impl UiRenderNode { }) } - /// One list's per-instance data. Each primitive needs its own, stating its - /// own `stride`: a `None` minimum takes its value from the first pipeline - /// built against the layout, and then holds every later one to that. + /// Layout for a list of one primitive's data. Stating the size rather than + /// leaving it `None` is what moves the check off every draw. fn data_layout(device: &Device, stride: u64) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[BindGroupLayoutEntry { @@ -442,7 +439,6 @@ impl ListBuffers { self.slots.extend_from_slice(list.slots()); self.instance.update(device, queue, list.instances()); let resized = self.data.update(device, queue, list.data()); - // An empty list has no buffer big enough to bind, and nothing to draw. if list.instances().is_empty() { self.group = None; } else if resized || self.group.is_none() { diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 4b94c4e..5ec130b 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -101,6 +101,7 @@ impl<'a> Painter<'a> { ui.text.render(buffer, attrs, width) } + // TODO: merge the text methods into the primitive ones. pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { let kind = self.rsc.ui_mut().primitives.kind::(); for glyph in text.glyphs.iter() {