Two findings from a sweep over the WGSL prelude and the position widgets, scoped against upstream/main atca2b4b2. `module_source` already builds each shader's preamble from iris_core's own constants, so the move-chain work's second copy of `MOVE_NONE` and `CHAIN_LIMIT` -- under "keep in step with iris_core::CHAIN_LIMIT" -- asked a reader by hand for what the mechanism beside it exists to do. Both are injected now, with `MASK_NONE` beside them replacing a bare literal, and the shader declares none of them. `Scroll`'s `content_len` is never less than its box, so `slack` and the `anchor` computed from it were always zero whatever the alignment: the framework centres short content by placing the answer in the whole box, and the comment credited arithmetic that could not have done it. The same belief guarded the fits-in-the-box contract with `align == NEG`, so at the default alignment -- the middle -- every box change redrew the scroll, measured as 1 widget against 0 at TOP_LEFT. `align` now has no reader at all. `UiSpan::translated` and `UiRegion::translated` are reachable only from each other and from nothing else. Format, clippy with and without layout-diagnostics, and the 131-test suite are clean. The cold dump over 400 depth-5 trees is byte-identical to1096c31, and all three seed scans pass: 400 at depth 5 in 69.07s, 1,000 at depth 6 in 169.29s, 2,000 at depth 4 in 300.75s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
457 lines
16 KiB
Rust
457 lines
16 KiB
Rust
use crate::{
|
|
UiData, UiRenderState,
|
|
render::{data::PrimitiveInstance, util::ArrBuf},
|
|
util::{HashMap, Vec2},
|
|
};
|
|
use data::WindowUniform;
|
|
use wgpu::{
|
|
util::{BufferInitDescriptor, DeviceExt},
|
|
*,
|
|
};
|
|
|
|
mod atlas;
|
|
mod data;
|
|
mod page;
|
|
mod primitive;
|
|
mod texture;
|
|
mod util;
|
|
|
|
pub use atlas::*;
|
|
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
|
pub use primitive::*;
|
|
|
|
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
|
|
|
fn module_source(wgsl: &str) -> String {
|
|
// Every number both sides count in, written once here rather than a
|
|
// second time in the shader: a grid the two disagree about puts every
|
|
// coordinate somewhere else, and a sentinel they disagree about makes one
|
|
// of them walk a chain from a slot the other says is not there.
|
|
format!(
|
|
"const PX_STEP: f32 = 1.0 / {}.0;\n\
|
|
const REL_STEP: f32 = 1.0 / {}.0;\n\
|
|
const MASK_NONE: u32 = {}u;\n\
|
|
const MOVE_NONE: u32 = {}u;\n\
|
|
const CHAIN_LIMIT: u32 = {}u;\n\
|
|
{PRELUDE}\n{wgsl}",
|
|
1u32 << crate::PX_SHIFT,
|
|
1u32 << crate::REL_SHIFT,
|
|
MaskIdx::NONE.idx(),
|
|
MoveIdx::NONE.idx(),
|
|
crate::CHAIN_LIMIT,
|
|
)
|
|
}
|
|
|
|
pub struct UiRenderNode {
|
|
shared_layout: BindGroupLayout,
|
|
shared_group: BindGroup,
|
|
format: TextureFormat,
|
|
|
|
/// One per registered primitive, in id order.
|
|
primitives: Vec<PrimitivePipeline>,
|
|
|
|
layers: HashMap<usize, RenderLayer>,
|
|
active: Vec<usize>,
|
|
window_buffer: Buffer,
|
|
masks: ArrBuf<Mask>,
|
|
moves: ArrBuf<MoveOffset>,
|
|
}
|
|
|
|
struct RenderLayer {
|
|
/// One per registered primitive, `None` where this layer draws none.
|
|
primitives: Vec<Option<ListBuffers>>,
|
|
}
|
|
|
|
/// What draws one registered primitive.
|
|
struct PrimitivePipeline {
|
|
data_layout: BindGroupLayout,
|
|
pipeline: RenderPipeline,
|
|
render: Box<dyn PrimitiveRender>,
|
|
}
|
|
|
|
/// One list's vertex buffer and the data its shader reads.
|
|
struct ListBuffers {
|
|
instance: ArrBuf<PrimitiveInstance>,
|
|
data: ArrBuf<u8>,
|
|
group: Option<BindGroup>,
|
|
/// What the primitive asked to keep per instance, if anything.
|
|
bindings: Vec<u32>,
|
|
}
|
|
|
|
impl UiRenderNode {
|
|
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
|
pass.set_bind_group(0, &self.shared_group, &[]);
|
|
for i in &self.active {
|
|
let layer = &self.layers[i];
|
|
for (id, list) in layer.primitives.iter().enumerate() {
|
|
let Some(list) = list else { continue };
|
|
let Some(group) = &list.group else { continue };
|
|
let primitive = &self.primitives[id];
|
|
pass.set_pipeline(&primitive.pipeline);
|
|
pass.set_bind_group(1, group, &[]);
|
|
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
|
|
primitive.render.draw(
|
|
pass,
|
|
ListDraw {
|
|
instances: list.instance.len() as u32,
|
|
bindings: &list.bindings,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update(
|
|
&mut self,
|
|
device: &Device,
|
|
queue: &Queue,
|
|
ui: &mut UiData,
|
|
ui_render: &mut UiRenderState,
|
|
) {
|
|
// Before the layers: each list is given its pipeline's data layout.
|
|
self.build_pipelines(device, queue, &ui.primitives);
|
|
self.active.clear();
|
|
for (i, draws) in ui_render.layers.iter_mut() {
|
|
self.active.push(i);
|
|
for change in draws.apply_free() {
|
|
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
|
for primitive in &mut inst.primitives {
|
|
let h = &mut primitive.handle;
|
|
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
|
|
h.inst_idx = change.new;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
|
|
if draws.updated {
|
|
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(lists)
|
|
.zip(&self.primitives)
|
|
{
|
|
let Some(list) = list else {
|
|
continue;
|
|
};
|
|
buffers
|
|
.get_or_insert_with(|| ListBuffers::new(device))
|
|
.update(device, queue, primitive, list);
|
|
}
|
|
draws.updated = false;
|
|
}
|
|
}
|
|
for primitive in &mut self.primitives {
|
|
primitive.render.update(ui);
|
|
}
|
|
let mut regroup = false;
|
|
if ui.masks.changed {
|
|
ui.masks.changed = false;
|
|
regroup |= self.masks.update(device, queue, &ui.masks[..]);
|
|
}
|
|
if ui_render.moves.changed {
|
|
ui_render.moves.changed = false;
|
|
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
|
|
}
|
|
if regroup {
|
|
self.shared_group = Self::shared_group(
|
|
device,
|
|
&self.shared_layout,
|
|
&self.window_buffer,
|
|
&self.masks,
|
|
&self.moves,
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
|
let size = size.into();
|
|
let slice = &[WindowUniform { dim: size }];
|
|
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
|
}
|
|
|
|
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
|
let window_uniform = WindowUniform {
|
|
dim: Vec2::new(config.width as f32, config.height as f32),
|
|
};
|
|
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
|
label: Some("window"),
|
|
contents: bytemuck::cast_slice(&[window_uniform]),
|
|
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
|
});
|
|
|
|
let shared_layout = Self::shared_layout(device);
|
|
let masks = ArrBuf::new(
|
|
device,
|
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
"ui masks",
|
|
);
|
|
let moves = ArrBuf::new(
|
|
device,
|
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
"ui move offsets",
|
|
);
|
|
let shared_group =
|
|
Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves);
|
|
|
|
Self {
|
|
shared_layout,
|
|
shared_group,
|
|
format: config.format,
|
|
primitives: Vec::new(),
|
|
window_buffer,
|
|
layers: HashMap::default(),
|
|
active: Vec::new(),
|
|
masks,
|
|
moves,
|
|
}
|
|
}
|
|
|
|
/// Compiles a pipeline for every primitive registered since the last call.
|
|
/// Sources only ever arrive at the end, so an id keeps its pipeline.
|
|
fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) {
|
|
for source in ®istry.sources()[self.primitives.len()..] {
|
|
let render = (source.render)(device, queue);
|
|
let data_layout = Self::data_layout(device, source.stride);
|
|
let mut groups = vec![Some(&self.shared_layout), Some(&data_layout)];
|
|
groups.extend(render.layout().map(Some));
|
|
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
|
label: Some(source.label),
|
|
bind_group_layouts: &groups,
|
|
immediate_size: 0,
|
|
});
|
|
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
|
|
self.primitives.push(PrimitivePipeline {
|
|
data_layout,
|
|
pipeline,
|
|
render,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn pipeline(
|
|
device: &Device,
|
|
layout: &PipelineLayout,
|
|
format: TextureFormat,
|
|
wgsl: &str,
|
|
label: &str,
|
|
) -> RenderPipeline {
|
|
let module = device.create_shader_module(ShaderModuleDescriptor {
|
|
label: Some(label),
|
|
source: ShaderSource::Wgsl(module_source(wgsl).into()),
|
|
});
|
|
device.create_render_pipeline(&RenderPipelineDescriptor {
|
|
label: Some(label),
|
|
layout: Some(layout),
|
|
vertex: VertexState {
|
|
module: &module,
|
|
entry_point: Some("vs_main"),
|
|
buffers: &[Some(PrimitiveInstance::desc())],
|
|
compilation_options: Default::default(),
|
|
},
|
|
fragment: Some(FragmentState {
|
|
module: &module,
|
|
entry_point: Some("fs_main"),
|
|
targets: &[Some(ColorTargetState {
|
|
format,
|
|
blend: Some(BlendState::ALPHA_BLENDING),
|
|
write_mask: ColorWrites::ALL,
|
|
})],
|
|
compilation_options: Default::default(),
|
|
}),
|
|
primitive: PrimitiveState {
|
|
topology: PrimitiveTopology::TriangleStrip,
|
|
strip_index_format: None,
|
|
front_face: FrontFace::Cw,
|
|
cull_mode: Some(Face::Back),
|
|
polygon_mode: PolygonMode::Fill,
|
|
unclipped_depth: false,
|
|
conservative: false,
|
|
},
|
|
depth_stencil: None,
|
|
multisample: MultisampleState {
|
|
count: 1,
|
|
mask: !0,
|
|
alpha_to_coverage_enabled: false,
|
|
},
|
|
multiview_mask: None,
|
|
cache: None,
|
|
})
|
|
}
|
|
|
|
/// What every draw in the ui is given: the window, the masks and the
|
|
/// move chain every position is resolved through.
|
|
fn shared_layout(device: &Device) -> BindGroupLayout {
|
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
|
entries: &[
|
|
BindGroupLayoutEntry {
|
|
binding: 0,
|
|
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
|
ty: BindingType::Buffer {
|
|
ty: BufferBindingType::Uniform,
|
|
has_dynamic_offset: false,
|
|
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64),
|
|
},
|
|
count: None,
|
|
},
|
|
BindGroupLayoutEntry {
|
|
binding: 1,
|
|
visibility: ShaderStages::FRAGMENT,
|
|
ty: BindingType::Buffer {
|
|
ty: BufferBindingType::Storage { read_only: true },
|
|
has_dynamic_offset: false,
|
|
min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
|
|
},
|
|
count: None,
|
|
},
|
|
BindGroupLayoutEntry {
|
|
binding: 2,
|
|
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
|
ty: BindingType::Buffer {
|
|
ty: BufferBindingType::Storage { read_only: true },
|
|
has_dynamic_offset: false,
|
|
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64),
|
|
},
|
|
count: None,
|
|
},
|
|
],
|
|
label: Some("ui shared"),
|
|
})
|
|
}
|
|
|
|
fn shared_group(
|
|
device: &Device,
|
|
layout: &BindGroupLayout,
|
|
window: &Buffer,
|
|
masks: &ArrBuf<Mask>,
|
|
moves: &ArrBuf<MoveOffset>,
|
|
) -> BindGroup {
|
|
device.create_bind_group(&BindGroupDescriptor {
|
|
layout,
|
|
entries: &[
|
|
BindGroupEntry {
|
|
binding: 0,
|
|
resource: window.as_entire_binding(),
|
|
},
|
|
BindGroupEntry {
|
|
binding: 1,
|
|
resource: masks.buffer.as_entire_binding(),
|
|
},
|
|
BindGroupEntry {
|
|
binding: 2,
|
|
resource: moves.buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
label: Some("ui shared"),
|
|
})
|
|
}
|
|
|
|
/// Layout for a list of one primitive's data. Every size in the ui is
|
|
/// stated, so "is the buffer big enough for one entry?" is answered when
|
|
/// the bind group is made; a `None` size is wgpu's to check on every draw.
|
|
fn data_layout(device: &Device, stride: u64) -> 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: BufferSize::new(stride),
|
|
},
|
|
count: None,
|
|
}],
|
|
label: Some("ui primitive data"),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl RenderLayer {
|
|
fn new() -> Self {
|
|
Self {
|
|
primitives: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ListBuffers {
|
|
fn new(device: &Device) -> Self {
|
|
Self {
|
|
instance: ArrBuf::new(
|
|
device,
|
|
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
|
"instance",
|
|
),
|
|
data: ArrBuf::new(
|
|
device,
|
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
"primitive data",
|
|
),
|
|
group: None,
|
|
bindings: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn update(
|
|
&mut self,
|
|
device: &Device,
|
|
queue: &Queue,
|
|
primitive: &PrimitivePipeline,
|
|
list: &InstanceList,
|
|
) {
|
|
self.bindings.clear();
|
|
primitive.render.instance_bindings(list, &mut self.bindings);
|
|
self.instance.update(device, queue, list.instances());
|
|
let resized = self.data.update(device, queue, list.data());
|
|
if list.instances().is_empty() {
|
|
self.group = None;
|
|
} else if resized || self.group.is_none() {
|
|
self.group = Some(device.create_bind_group(&BindGroupDescriptor {
|
|
layout: &primitive.data_layout,
|
|
entries: &[BindGroupEntry {
|
|
binding: 0,
|
|
resource: self.data.buffer.as_entire_binding(),
|
|
}],
|
|
label: Some("ui primitive data"),
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::module_source;
|
|
use wgpu::naga::{
|
|
front::wgsl,
|
|
valid::{Capabilities, ValidationFlags, Validator},
|
|
};
|
|
|
|
/// Every shader file, composed as the renderer composes it, parses and
|
|
/// validates with no device -- so an edit that breaks one fails here and
|
|
/// not in the first window opened.
|
|
#[test]
|
|
fn every_shader_validates() {
|
|
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/render/shader");
|
|
let mut checked = 0;
|
|
for entry in std::fs::read_dir(dir).unwrap() {
|
|
let path = entry.unwrap().path();
|
|
if path.extension().is_none_or(|e| e != "wgsl") || path.ends_with("prelude.wgsl") {
|
|
continue;
|
|
}
|
|
let source = module_source(&std::fs::read_to_string(&path).unwrap());
|
|
let module = wgsl::parse_str(&source)
|
|
.unwrap_or_else(|e| panic!("{}: {}", path.display(), e.emit_to_string(&source)));
|
|
Validator::new(ValidationFlags::all(), Capabilities::all())
|
|
.validate(&module)
|
|
.unwrap_or_else(|e| panic!("{}: {e:?}", path.display()));
|
|
checked += 1;
|
|
}
|
|
assert!(checked > 0, "no shaders found in {dir}");
|
|
}
|
|
}
|