Replaces the bindless `binding_array<texture_2d<f32>>` the renderer bound every texture through. That array needs `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack, so the old shape did not run there at all. The two things being bound want opposite treatment, so they are now split: - **Glyph atlas pages become layers of one `texture_2d_array`.** A glyph primitive carries a `layer` instead of a view/sampler index pair. A layer index is an ordinary sampling operand, so this needs nothing beyond plain Vulkan 1.0 / GLES. Growing the atlas recreates the array with headroom and `copy_texture_to_texture`s the old layers across, no readback. - **A standalone image gets its own texture and its own bind group,** and draws in its own call. It no longer needs a per-instance entry in `PrimitiveData`: the bind group has already picked the texture. `Primitives` keeps images in a list of their own as a result, with `PrimitiveChange::is_image` naming which list a renumbering belongs to -- the two have independent index spaces, so `(layer, inst_idx)` alone would collide between them. Two notes on judgement calls, since this slice was rebuilt on top of `main` rather than transplanted: - The source version renamed `GlyphEntry::is_colored` to `is_color` and added a second `IS_COLOR` flag constant beside the existing `GlyphEntry::IS_COLORED`. Both dropped: #10's naming and its `flags()` are kept, and UVs stay `Vec2` rather than going back to `[f32; 2]`. - `ImageGpu` no longer holds the `Texture` behind its view, which removes an `#[allow(dead_code)]`. A `TextureView` keeps its own reference to the texture, checked by rendering rather than assumed -- see below. ### Verification ``` cargo fmt --all --check cargo clippy --workspace --all-targets --locked -- -D warnings cargo test --workspace --locked ``` All clean; the 4 text-edit tests pass. The only clippy output is the pre-existing future-incompatibility notice about `naga`/`wgpu`/`winit`. Because this is a rendering change, it was also run for real rather than only compiled. The `tabs` example was rendered on this machine's GPU -- Venus onto an RX 7900 XT, confirmed from the loaded ICD (`libvulkan_virtio.so` on `/dev/dri/renderD128`) rather than assumed, since a failed Vulkan init here silently falls back to llvmpipe and would make the screenshots meaningless. Screenshots before and after the change are **byte-identical** (same md5) in two scenes: the default tab, which exercises text (the atlas path) and rects, and the image tab with a standalone image pushed at startup, which exercises the per-image bind group. The image-tab scene needed a temporary local edit to the example to push the image without a click; that edit is not part of this branch. The same comparison, re-run after dropping the `Texture` field, is still byte-identical -- which is the check that the view alone keeps it alive. --------- Co-authored-by: iris <2+iris@noreply.localhost> Reviewed-on: iris/iris#11 Reviewed-by: iris <2+iris@noreply.localhost> Co-authored-by: AIris <4+iris-ai@noreply.localhost>
377 lines
13 KiB
Rust
377 lines
13 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};
|
|
pub use primitive::*;
|
|
|
|
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
|
|
|
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>,
|
|
}
|
|
|
|
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 h in &mut inst.primitives {
|
|
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);
|
|
}
|
|
if ui.masks.changed {
|
|
ui.masks.changed = false;
|
|
if self.masks.update(device, queue, &ui.masks[..]) {
|
|
self.shared_group = Self::shared_group(
|
|
device,
|
|
&self.shared_layout,
|
|
&self.window_buffer,
|
|
&self.masks,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
|
let size = size.into();
|
|
let slice = &[WindowUniform {
|
|
width: size.x,
|
|
height: size.y,
|
|
}];
|
|
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
|
}
|
|
|
|
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
|
let window_uniform = WindowUniform {
|
|
width: config.width as f32,
|
|
height: 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 shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
|
|
|
Self {
|
|
shared_layout,
|
|
shared_group,
|
|
format: config.format,
|
|
primitives: Vec::new(),
|
|
window_buffer,
|
|
layers: HashMap::default(),
|
|
active: Vec::new(),
|
|
masks,
|
|
}
|
|
}
|
|
|
|
/// 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![&self.shared_layout, &data_layout];
|
|
groups.extend(render.layout());
|
|
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(format!("{PRELUDE}\n{wgsl}").into()),
|
|
});
|
|
device.create_render_pipeline(&RenderPipelineDescriptor {
|
|
label: Some(label),
|
|
layout: Some(layout),
|
|
vertex: VertexState {
|
|
module: &module,
|
|
entry_point: Some("vs_main"),
|
|
buffers: &[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 and the masks.
|
|
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,
|
|
},
|
|
],
|
|
label: Some("ui shared"),
|
|
})
|
|
}
|
|
|
|
fn shared_group(
|
|
device: &Device,
|
|
layout: &BindGroupLayout,
|
|
window: &Buffer,
|
|
masks: &ArrBuf<Mask>,
|
|
) -> 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(),
|
|
},
|
|
],
|
|
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"),
|
|
}));
|
|
}
|
|
}
|
|
}
|