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 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-13 17:23:35 -04:00
1 parent 23fb71ee56
commit 444a2cd138
2 files changed
+11 -14

No files matched your search

+10 -14
View File
@@ -117,24 +117,22 @@ impl UiRenderNode {
} }
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new); let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
if draws.updated { if draws.updated {
rlayer let lists = draws.primitives();
.primitives // The zip would otherwise skip a list with no pipeline.
.resize_with(draws.primitives().len(), || None); assert!(lists.len() <= self.primitives.len());
for (id, (buffers, list)) in rlayer rlayer.primitives.resize_with(lists.len(), || None);
for ((buffers, list), primitive) in rlayer
.primitives .primitives
.iter_mut() .iter_mut()
.zip(draws.primitives()) .zip(lists)
.enumerate() .zip(&self.primitives)
{ {
let Some(list) = list else { let Some(list) = list else {
continue; 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 buffers
.get_or_insert_with(|| ListBuffers::new(device)) .get_or_insert_with(|| ListBuffers::new(device))
.update(device, queue, layout, list); .update(device, queue, &primitive.data_layout, list);
} }
draws.updated = false; draws.updated = false;
} }
@@ -364,9 +362,8 @@ impl UiRenderNode {
}) })
} }
/// One list's per-instance data. Each primitive needs its own, stating its /// Layout for a list of one primitive's data. Stating the size rather than
/// own `stride`: a `None` minimum takes its value from the first pipeline /// leaving it `None` is what moves the check off every draw.
/// built against the layout, and then holds every later one to that.
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout { fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry { entries: &[BindGroupLayoutEntry {
@@ -442,7 +439,6 @@ impl ListBuffers {
self.slots.extend_from_slice(list.slots()); self.slots.extend_from_slice(list.slots());
self.instance.update(device, queue, list.instances()); self.instance.update(device, queue, list.instances());
let resized = self.data.update(device, queue, list.data()); 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() { if list.instances().is_empty() {
self.group = None; self.group = None;
} else if resized || self.group.is_none() { } else if resized || self.group.is_none() {
+1
View File
@@ -101,6 +101,7 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
// TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>(); let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {