Fix what reviewing the primitive rework turned up
Three defects, two of them invisible to every case I had run. An image alone in a layer failed validation. The texture pipeline never bound group 1, and every earlier case happened to have a rect in the same layer, which left one bound from the primitive draw -- so the bug was hidden by the tests passing. A `min_binding_size: None` binding takes its minimum from the first pipeline built against that bind group layout, so one shared group 1 layout held every primitive to the largest. Rect and glyph coexisted only because glyph is the bigger of the two; the texture slots, at four bytes, did not. Each primitive now gets its own layout with its entry size stated, which is also why `PrimitiveRegistry` records the stride. Because the pipeline layouts now differ per primitive, a pipeline change drops the bound groups, so group 2 moves after `set_pipeline`. An empty list still built a bind group over a buffer too small for one entry, which the stated minimum would now reject. It gets no bind group, and nothing draws it. Also from the read-through: `UiRenderNode` kept a `Device` beside the one `update` is handed, `PrimitiveRegistry::default` registered inside an `assert_eq!`, and `mask_idx` was an unqualified integer varying where `idx` beside it was `flat`.
This commit is contained in:
1 parent
4d9839f380
commit
a08f61a80c
3 files changed
+95
-52
No files matched your search
+79
-36
@@ -29,17 +29,15 @@ const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
|||||||
const TEXTURE_SHADER: &str = include_str!("./shader/texture.wgsl");
|
const TEXTURE_SHADER: &str = include_str!("./shader/texture.wgsl");
|
||||||
|
|
||||||
pub struct UiRenderNode {
|
pub struct UiRenderNode {
|
||||||
device: Device,
|
|
||||||
shared_layout: BindGroupLayout,
|
shared_layout: BindGroupLayout,
|
||||||
shared_group: BindGroup,
|
shared_group: BindGroup,
|
||||||
data_layout: BindGroupLayout,
|
texture_data_layout: BindGroupLayout,
|
||||||
texture_layout: BindGroupLayout,
|
texture_layout: BindGroupLayout,
|
||||||
pipeline_layout: PipelineLayout,
|
|
||||||
format: TextureFormat,
|
format: TextureFormat,
|
||||||
|
|
||||||
/// One per registered primitive, in id order. The texture pipeline is
|
/// One per registered primitive, in id order. The texture pipeline is
|
||||||
/// apart because a texture binds group 2 per instance, not per draw.
|
/// apart because a texture binds group 2 per instance, not per draw.
|
||||||
pipelines: Vec<RenderPipeline>,
|
primitives: Vec<PrimitivePipeline>,
|
||||||
texture_pipeline: RenderPipeline,
|
texture_pipeline: RenderPipeline,
|
||||||
|
|
||||||
layers: HashMap<usize, RenderLayer>,
|
layers: HashMap<usize, RenderLayer>,
|
||||||
@@ -59,6 +57,13 @@ struct RenderLayer {
|
|||||||
texture_slots: Vec<u32>,
|
texture_slots: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What draws one registered primitive. The group 1 layout is its own rather
|
||||||
|
/// than shared, so its entry size is the minimum only for it.
|
||||||
|
struct PrimitivePipeline {
|
||||||
|
data_layout: BindGroupLayout,
|
||||||
|
pipeline: RenderPipeline,
|
||||||
|
}
|
||||||
|
|
||||||
/// One list's vertex buffer and the data its shader reads at group 1.
|
/// One list's vertex buffer and the data its shader reads at group 1.
|
||||||
struct ListBuffers {
|
struct ListBuffers {
|
||||||
instance: ArrBuf<PrimitiveInstance>,
|
instance: ArrBuf<PrimitiveInstance>,
|
||||||
@@ -74,18 +79,26 @@ impl UiRenderNode {
|
|||||||
// Types run in registration order, so a rect is under a glyph is
|
// Types run in registration order, so a rect is under a glyph is
|
||||||
// under a texture. Ordering beyond that is what `Layers` is for --
|
// under a texture. Ordering beyond that is what `Layers` is for --
|
||||||
// freeing an instance swaps another into its place.
|
// freeing an instance swaps another into its place.
|
||||||
pass.set_bind_group(2, self.pages.group(), &[]);
|
|
||||||
for (id, list) in layer.primitives.iter().enumerate() {
|
for (id, list) in layer.primitives.iter().enumerate() {
|
||||||
let (Some(group), true) = (&list.group, list.instance.len() > 0) else {
|
let Some(group) = &list.group else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
pass.set_pipeline(&self.pipelines[id]);
|
pass.set_pipeline(&self.primitives[id].pipeline);
|
||||||
|
// Both after the pipeline: each primitive has its own pipeline
|
||||||
|
// layout, and a change drops the groups from where they differ.
|
||||||
pass.set_bind_group(1, group, &[]);
|
pass.set_bind_group(1, group, &[]);
|
||||||
|
pass.set_bind_group(2, self.pages.group(), &[]);
|
||||||
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
|
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
|
||||||
pass.draw(0..4, 0..list.instance.len() as u32);
|
pass.draw(0..4, 0..list.instance.len() as u32);
|
||||||
}
|
}
|
||||||
if !layer.texture_slots.is_empty() {
|
if !layer.texture_slots.is_empty() {
|
||||||
|
// Group 1 too, unread as it is: a layer holding only an image
|
||||||
|
// never ran the loop above, so nothing is bound there.
|
||||||
|
let Some(data) = &layer.textures.group else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
pass.set_pipeline(&self.texture_pipeline);
|
pass.set_pipeline(&self.texture_pipeline);
|
||||||
|
pass.set_bind_group(1, data, &[]);
|
||||||
pass.set_vertex_buffer(0, layer.textures.instance.buffer.slice(..));
|
pass.set_vertex_buffer(0, layer.textures.instance.buffer.slice(..));
|
||||||
for (i, &slot) in layer.texture_slots.iter().enumerate() {
|
for (i, &slot) in layer.texture_slots.iter().enumerate() {
|
||||||
let Some(group) = self.textures.group(slot) else {
|
let Some(group) = self.textures.group(slot) else {
|
||||||
@@ -126,12 +139,17 @@ impl UiRenderNode {
|
|||||||
rlayer
|
rlayer
|
||||||
.primitives
|
.primitives
|
||||||
.resize_with(draws.primitives().len(), || ListBuffers::new(device));
|
.resize_with(draws.primitives().len(), || ListBuffers::new(device));
|
||||||
for (list, draws) in rlayer.primitives.iter_mut().zip(draws.primitives()) {
|
for ((list, draws), pipeline) in rlayer
|
||||||
list.update(device, queue, &self.data_layout, draws);
|
.primitives
|
||||||
|
.iter_mut()
|
||||||
|
.zip(draws.primitives())
|
||||||
|
.zip(&self.primitives)
|
||||||
|
{
|
||||||
|
list.update(device, queue, &pipeline.data_layout, draws);
|
||||||
}
|
}
|
||||||
rlayer
|
rlayer
|
||||||
.textures
|
.textures
|
||||||
.update(device, queue, &self.data_layout, &draws.textures);
|
.update(device, queue, &self.texture_data_layout, &draws.textures);
|
||||||
rlayer.texture_slots.clear();
|
rlayer.texture_slots.clear();
|
||||||
// Read rather than cast: the payload is a byte vec, so it
|
// Read rather than cast: the payload is a byte vec, so it
|
||||||
// carries no alignment a `u32` slice could borrow.
|
// carries no alignment a `u32` slice could borrow.
|
||||||
@@ -142,7 +160,7 @@ impl UiRenderNode {
|
|||||||
draws.updated = false;
|
draws.updated = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.build_pipelines(&ui.primitives);
|
self.build_pipelines(device, &ui.primitives);
|
||||||
if ui.masks.changed {
|
if ui.masks.changed {
|
||||||
ui.masks.changed = false;
|
ui.masks.changed = false;
|
||||||
if self.masks.update(device, queue, &ui.masks[..]) {
|
if self.masks.update(device, queue, &ui.masks[..]) {
|
||||||
@@ -181,7 +199,6 @@ impl UiRenderNode {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let shared_layout = Self::shared_layout(device);
|
let shared_layout = Self::shared_layout(device);
|
||||||
let data_layout = Self::data_layout(device);
|
|
||||||
let texture_layout = Self::texture_layout(device);
|
let texture_layout = Self::texture_layout(device);
|
||||||
|
|
||||||
let masks = ArrBuf::new(
|
let masks = ArrBuf::new(
|
||||||
@@ -195,30 +212,29 @@ impl UiRenderNode {
|
|||||||
let pages = GpuPages::new(device, queue, &texture_layout, &sampler);
|
let pages = GpuPages::new(device, queue, &texture_layout, &sampler);
|
||||||
let textures = GpuTextures::new(device, queue);
|
let textures = GpuTextures::new(device, queue);
|
||||||
|
|
||||||
// One layout for every pipeline, so a primitive that does not sample
|
// A texture instance's data is the slot it binds, which its shader
|
||||||
// or read a buffer binds what is there instead of needing its own.
|
// never reads -- but group 1 is in the layout, so it is bound anyway.
|
||||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
let texture_data_layout = Self::data_layout(device, size_of::<u32>() as u64);
|
||||||
label: Some("ui"),
|
|
||||||
bind_group_layouts: &[&shared_layout, &data_layout, &texture_layout],
|
|
||||||
immediate_size: 0,
|
|
||||||
});
|
|
||||||
let texture_pipeline = Self::pipeline(
|
let texture_pipeline = Self::pipeline(
|
||||||
device,
|
device,
|
||||||
&pipeline_layout,
|
&Self::pipeline_layout(
|
||||||
|
device,
|
||||||
|
&shared_layout,
|
||||||
|
&texture_data_layout,
|
||||||
|
&texture_layout,
|
||||||
|
),
|
||||||
config.format,
|
config.format,
|
||||||
TEXTURE_SHADER,
|
TEXTURE_SHADER,
|
||||||
"texture",
|
"texture",
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
device: device.clone(),
|
|
||||||
shared_layout,
|
shared_layout,
|
||||||
shared_group,
|
shared_group,
|
||||||
data_layout,
|
texture_data_layout,
|
||||||
texture_layout,
|
texture_layout,
|
||||||
pipeline_layout,
|
|
||||||
format: config.format,
|
format: config.format,
|
||||||
pipelines: Vec::new(),
|
primitives: Vec::new(),
|
||||||
texture_pipeline,
|
texture_pipeline,
|
||||||
window_buffer,
|
window_buffer,
|
||||||
layers: HashMap::default(),
|
layers: HashMap::default(),
|
||||||
@@ -232,18 +248,36 @@ impl UiRenderNode {
|
|||||||
|
|
||||||
/// Compiles a pipeline for every primitive registered since the last call.
|
/// Compiles a pipeline for every primitive registered since the last call.
|
||||||
/// Sources only ever arrive at the end, so an id keeps its pipeline.
|
/// Sources only ever arrive at the end, so an id keeps its pipeline.
|
||||||
fn build_pipelines(&mut self, registry: &PrimitiveRegistry) {
|
fn build_pipelines(&mut self, device: &Device, registry: &PrimitiveRegistry) {
|
||||||
for source in ®istry.sources()[self.pipelines.len()..] {
|
for source in ®istry.sources()[self.primitives.len()..] {
|
||||||
self.pipelines.push(Self::pipeline(
|
let data_layout = Self::data_layout(device, source.stride);
|
||||||
&self.device,
|
let layout = Self::pipeline_layout(
|
||||||
&self.pipeline_layout,
|
device,
|
||||||
self.format,
|
&self.shared_layout,
|
||||||
source.wgsl,
|
&data_layout,
|
||||||
source.label,
|
&self.texture_layout,
|
||||||
));
|
);
|
||||||
|
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
|
||||||
|
self.primitives.push(PrimitivePipeline {
|
||||||
|
data_layout,
|
||||||
|
pipeline,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pipeline_layout(
|
||||||
|
device: &Device,
|
||||||
|
shared: &BindGroupLayout,
|
||||||
|
data: &BindGroupLayout,
|
||||||
|
texture: &BindGroupLayout,
|
||||||
|
) -> PipelineLayout {
|
||||||
|
device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||||
|
label: Some("ui"),
|
||||||
|
bind_group_layouts: &[shared, data, texture],
|
||||||
|
immediate_size: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn pipeline(
|
fn pipeline(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
layout: &PipelineLayout,
|
layout: &PipelineLayout,
|
||||||
@@ -346,7 +380,11 @@ impl UiRenderNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Group 1: one list's per-instance data, whatever its shader reads it as.
|
/// Group 1: one list's per-instance data, whatever its shader reads it as.
|
||||||
fn data_layout(device: &Device) -> BindGroupLayout {
|
///
|
||||||
|
/// One per primitive with `stride` stated, because a `None` minimum is
|
||||||
|
/// filled in from the first pipeline built against the layout -- sharing
|
||||||
|
/// one would hold every primitive to the largest.
|
||||||
|
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
|
||||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
entries: &[BindGroupLayoutEntry {
|
entries: &[BindGroupLayoutEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
@@ -354,7 +392,7 @@ impl UiRenderNode {
|
|||||||
ty: BindingType::Buffer {
|
ty: BindingType::Buffer {
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: false,
|
||||||
min_binding_size: None,
|
min_binding_size: std::num::NonZero::new(stride),
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
}],
|
}],
|
||||||
@@ -428,7 +466,12 @@ impl ListBuffers {
|
|||||||
list: &InstanceList,
|
list: &InstanceList,
|
||||||
) {
|
) {
|
||||||
self.instance.update(device, queue, list.instances());
|
self.instance.update(device, queue, list.instances());
|
||||||
if self.data.update(device, queue, list.data()) || self.group.is_none() {
|
let resized = self.data.update(device, queue, list.data());
|
||||||
|
// An empty list has no buffer big enough for one entry, and nothing
|
||||||
|
// draws it, so it has no bind group either.
|
||||||
|
if list.instances().is_empty() {
|
||||||
|
self.group = None;
|
||||||
|
} else if resized || self.group.is_none() {
|
||||||
self.group = Some(device.create_bind_group(&BindGroupDescriptor {
|
self.group = Some(device.create_bind_group(&BindGroupDescriptor {
|
||||||
layout,
|
layout,
|
||||||
entries: &[BindGroupEntry {
|
entries: &[BindGroupEntry {
|
||||||
|
|||||||
@@ -51,24 +51,20 @@ pub struct PrimitiveRegistry {
|
|||||||
pub struct PrimitiveSource {
|
pub struct PrimitiveSource {
|
||||||
pub wgsl: &'static str,
|
pub wgsl: &'static str,
|
||||||
pub label: &'static str,
|
pub label: &'static str,
|
||||||
|
/// Size of one instance's entry, which the renderer states as the group 1
|
||||||
|
/// binding's minimum rather than leaving it to be inferred.
|
||||||
|
pub stride: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PrimitiveRegistry {
|
impl Default for PrimitiveRegistry {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut kinds = Self { kinds: Vec::new() };
|
let mut registry = Self { kinds: Vec::new() };
|
||||||
assert_eq!(
|
let rect = registry.register::<RectPrimitive>(include_str!("shader/rect.wgsl"), "rect");
|
||||||
kinds
|
let glyph = registry.register::<GlyphPrimitive>(include_str!("shader/glyph.wgsl"), "glyph");
|
||||||
.register::<RectPrimitive>(include_str!("shader/rect.wgsl"), "rect")
|
// The built-ins have constant ids so a widget can name one without the
|
||||||
.id(),
|
// registry; registering them first is what makes those constants true.
|
||||||
RECT.id()
|
assert_eq!((rect.id(), glyph.id()), (RECT.id(), GLYPH.id()));
|
||||||
);
|
registry
|
||||||
assert_eq!(
|
|
||||||
kinds
|
|
||||||
.register::<GlyphPrimitive>(include_str!("shader/glyph.wgsl"), "glyph")
|
|
||||||
.id(),
|
|
||||||
GLYPH.id()
|
|
||||||
);
|
|
||||||
kinds
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +74,11 @@ impl PrimitiveRegistry {
|
|||||||
wgsl: &'static str,
|
wgsl: &'static str,
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
) -> PrimitiveKind<P> {
|
) -> PrimitiveKind<P> {
|
||||||
self.kinds.push(PrimitiveSource { wgsl, label });
|
self.kinds.push(PrimitiveSource {
|
||||||
|
wgsl,
|
||||||
|
label,
|
||||||
|
stride: size_of::<P>() as u64,
|
||||||
|
});
|
||||||
PrimitiveKind::new(self.kinds.len() as u32 - 1)
|
PrimitiveKind::new(self.kinds.len() as u32 - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ struct VertexOutput {
|
|||||||
@location(0) top_left: vec2<f32>,
|
@location(0) top_left: vec2<f32>,
|
||||||
@location(1) bot_right: vec2<f32>,
|
@location(1) bot_right: vec2<f32>,
|
||||||
@location(2) uv: vec2<f32>,
|
@location(2) uv: vec2<f32>,
|
||||||
@location(3) mask_idx: u32,
|
@location(3) @interpolate(flat) mask_idx: u32,
|
||||||
// The instance's own index, which is also where its data sits in group 1.
|
// The instance's own index, which is also where its data sits in group 1.
|
||||||
@location(4) @interpolate(flat) idx: u32,
|
@location(4) @interpolate(flat) idx: u32,
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
|||||||
Reference in new issue
Block a user