Bundles Noto Sans/Noto Sans Mono (regular/bold/italic/bold-italic, OFL licensed) into iris-core and registers them ahead of the platform's own fonts in the SansSerif/Monospace generic-family fallback lists, so text no longer depends on the platform's font enumeration succeeding or resolving weight/style correctly. Iris's phone report showed bold spans rendering as blank gaps of the correct advance width -- the glyph simply wasn't rasterised -- while the emulator's system fonts happened to resolve every style; a bundled static-per-style family removes that platform-dependent step entirely. TextData::font_diagnostics() reports what was found/resolved, for the startup log and the Diagnostics page. Also applies a content/device-pixel scale that neither backend had before: UiRenderNode::new/resize now take the window size explicitly (logical units) rather than deriving it from the surface's physical config, so a 16.0 font size is 16 logical units rather than 16 raw device pixels. Wired on desktop via window.scale_factor() (input events, window_size, and the render node's own seed); the Android side (density via DisplayMetrics, touch coordinates, layout root size) is the next commit. Also adds WgpuErrorLog and a per-frame atlas-grow counter (GpuTextures::take_pages_grown), both plumbing for the Android diagnostics page in the next commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
690 lines
29 KiB
Rust
690 lines
29 KiB
Rust
use crate::{
|
|
UiData, UiRenderState,
|
|
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
|
|
util::{HashMap, Vec2},
|
|
};
|
|
use data::WindowUniform;
|
|
use pollster::FutureExt;
|
|
use wgpu::{
|
|
util::{BufferInitDescriptor, DeviceExt},
|
|
*,
|
|
};
|
|
|
|
mod atlas;
|
|
mod data;
|
|
mod frame_report;
|
|
mod primitive;
|
|
mod texture;
|
|
mod util;
|
|
|
|
pub use atlas::*;
|
|
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
|
pub use frame_report::{FrameReport, FrameStats, JANK_THRESHOLD};
|
|
pub use primitive::*;
|
|
|
|
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
|
|
|
/// The `wgpu::Limits` both platform backends (`android::render::
|
|
/// AndroidRenderer::new`, `default::render::UiRenderer::new`) ask
|
|
/// `Adapter::request_device` for -- shared so the two copies cannot drift,
|
|
/// per AGENTS.md's "write the logic once."
|
|
///
|
|
/// Built from `Limits::default()`, **not** a downlevel variant: the shader
|
|
/// (`shader.wgsl`) reads four `var<storage>` buffers (rects, glyphs, masks,
|
|
/// move_offsets) from the vertex stage, and `Limits::downlevel_webgl2_defaults()`
|
|
/// zeroes `max_storage_buffers_per_shader_stage` along with the compute
|
|
/// limits below -- switching to it would trade one `request_device` crash
|
|
/// for a bind-group-layout one on the same downlevel hardware this is meant
|
|
/// to support. `max_buffer_size` is raised for the growing instance/atlas
|
|
/// buffers (`ArrBuf`, `GpuTextures`); everything else is `default()`'s
|
|
/// desktop-tier value, unchanged.
|
|
///
|
|
/// The six `max_compute_*` fields are zeroed because nothing in this crate
|
|
/// creates a `ComputePipeline` or writes a `@compute` shader stage --
|
|
/// grepped for both across `iris`/`iris-core` before writing this, found
|
|
/// none. `Limits::default()` requests desktop-tier compute limits
|
|
/// unconditionally (`max_compute_workgroups_per_dimension: 65535`) even
|
|
/// though nothing asks a device to actually support compute, which is what
|
|
/// crashed `request_device` on the Android emulator's software GL path
|
|
/// (`EMU_GPU=software`, `force-gles`): SwiftShader's GL reports itself as
|
|
/// OpenGL ES 3.0, which has no compute shaders, so the adapter's real limit
|
|
/// is 0 and the unconditional request fails outright
|
|
/// (`RUST.md`'s "Software mode ... crashes for a third, different reason").
|
|
/// The same would happen on a real GLES-3.0-only Android device. If a
|
|
/// future change adds a compute pass, request the specific limits it needs
|
|
/// here rather than reverting to the desktop-tier default for everything.
|
|
pub fn device_limits() -> Limits {
|
|
Limits {
|
|
max_buffer_size: 1 << 30,
|
|
max_compute_workgroup_storage_size: 0,
|
|
max_compute_invocations_per_workgroup: 0,
|
|
max_compute_workgroup_size_x: 0,
|
|
max_compute_workgroup_size_y: 0,
|
|
max_compute_workgroup_size_z: 0,
|
|
max_compute_workgroups_per_dimension: 0,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// A capped log of wgpu's *uncaptured* errors -- everything that reaches
|
|
/// `Device::on_uncaptured_error` rather than one of `UiRenderNode::new`'s
|
|
/// own error scopes, i.e. every wgpu error raised outside device/pipeline
|
|
/// creation: a validation failure during an ordinary frame's `update`/
|
|
/// `draw`, for instance. wgpu's default handler for these is `panic!` with
|
|
/// no caller able to intervene -- exactly what aborted the P0 bench APK
|
|
/// once already (this file's `UiRenderNode::new` doc comment) -- so both
|
|
/// platform backends install a handler here instead of leaving the default
|
|
/// in place, per RUST.md's P0 box ("every wgpu uncaptured error ... it
|
|
/// must never panic in release").
|
|
///
|
|
/// Cheap to `Clone` (an `Arc` around the real storage) rather than a
|
|
/// process-wide static, so a caller builds one alongside its `Device`,
|
|
/// hands one clone to `on_uncaptured_error`'s closure and keeps the other
|
|
/// for the Diagnostics page to read -- context passed explicitly, per
|
|
/// AGENTS.md/CODE_RULES.md's "no globals" rather than reached for through a
|
|
/// `OnceLock`.
|
|
#[derive(Clone)]
|
|
pub struct WgpuErrorLog {
|
|
errors: std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>>,
|
|
}
|
|
|
|
/// How many uncaptured errors the log keeps -- old ones drop off the front
|
|
/// rather than being trimmed on read, so a build spraying errors every
|
|
/// frame doesn't grow this without bound.
|
|
const WGPU_ERROR_LOG_CAP: usize = 20;
|
|
|
|
impl Default for WgpuErrorLog {
|
|
fn default() -> Self {
|
|
Self {
|
|
errors: std::sync::Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WgpuErrorLog {
|
|
pub fn record(&self, error: impl std::fmt::Display) {
|
|
let mut errors = self.errors.lock().unwrap();
|
|
if errors.len() >= WGPU_ERROR_LOG_CAP {
|
|
errors.pop_front();
|
|
}
|
|
errors.push_back(error.to_string());
|
|
}
|
|
|
|
/// A snapshot for the Diagnostics page -- cloned rather than held,
|
|
/// since the lock must not outlive one call.
|
|
pub fn snapshot(&self) -> Vec<String> {
|
|
self.errors.lock().unwrap().iter().cloned().collect()
|
|
}
|
|
}
|
|
|
|
pub struct UiRenderNode {
|
|
uniform_group: BindGroup,
|
|
primitive_layout: BindGroupLayout,
|
|
rsc_layout: BindGroupLayout,
|
|
rsc_group: BindGroup,
|
|
|
|
pipeline: RenderPipeline,
|
|
|
|
layers: HashMap<usize, RenderLayer>,
|
|
active: Vec<usize>,
|
|
window_buffer: Buffer,
|
|
textures: GpuTextures,
|
|
masks: ArrBuf<Mask>,
|
|
move_offsets: ArrBuf<MoveOffset>,
|
|
/// Group 3: the masks and move-offsets storage buffers, on their own --
|
|
/// see IRIS_TODO.md's "Appending one image ... rebuilds every other
|
|
/// image's bind group". These used to live in group 2 alongside each
|
|
/// standalone image's own texture view, so an image's bind group named
|
|
/// the masks/move_offsets buffer directly; the moment either buffer
|
|
/// resized (which a widget getting its *first* move slot can trigger,
|
|
/// unrelated to any image), `ArrBuf::update` handed back a new `Buffer`
|
|
/// identity and every image's bind group -- one per live image -- had
|
|
/// to be rebuilt to reference it. Pulling both buffers into their own
|
|
/// group, bound once per frame rather than once per draw call, means a
|
|
/// buffer resize now rebuilds exactly this one group instead of N.
|
|
masks_layout: BindGroupLayout,
|
|
masks_group: BindGroup,
|
|
}
|
|
|
|
struct RenderLayer {
|
|
instance: ArrBuf<PrimitiveInstance>,
|
|
primitives: PrimitiveBuffers,
|
|
primitive_group: BindGroup,
|
|
/// A standalone image's instances, kept apart from `instance` because
|
|
/// each one draws with its own bind group -- see `UiRenderNode::draw`.
|
|
image_instance: ArrBuf<PrimitiveInstance>,
|
|
/// The texture slot each entry of `image_instance` draws with, in the
|
|
/// same order, refreshed alongside it. Not stored in the vertex buffer
|
|
/// itself because it names a bind group, not shader data.
|
|
image_tex_indices: Vec<u32>,
|
|
}
|
|
|
|
impl UiRenderNode {
|
|
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
|
pass.set_pipeline(&self.pipeline);
|
|
pass.set_bind_group(0, &self.uniform_group, &[]);
|
|
// Set once, not per layer or per image: masks/move_offsets are read
|
|
// by every primitive and every standalone image alike, and living
|
|
// in their own group (rather than folded into group 2 alongside the
|
|
// per-image texture view) is what keeps an image's own bind group
|
|
// from naming a buffer that changes size on an unrelated widget's
|
|
// first draw -- see the comment on `masks_group` below.
|
|
pass.set_bind_group(3, &self.masks_group, &[]);
|
|
for i in &self.active {
|
|
let layer = &self.layers[i];
|
|
if layer.instance.len() == 0 && layer.image_instance.len() == 0 {
|
|
continue;
|
|
}
|
|
pass.set_bind_group(1, &layer.primitive_group, &[]);
|
|
if layer.instance.len() > 0 {
|
|
pass.set_bind_group(2, &self.rsc_group, &[]);
|
|
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
|
|
pass.draw(0..4, 0..layer.instance.len() as u32);
|
|
}
|
|
// Images draw after this layer's rects and glyphs, one draw call
|
|
// each with its own bind group. That draws every image "on top"
|
|
// within the layer, which loses nothing that currently exists:
|
|
// `Primitives::apply_free` frees with `swap_remove`, so a layer's
|
|
// draw order was already undefined before images had their own
|
|
// list -- nothing before this relied on interleaving a rect
|
|
// between two images at a particular position.
|
|
if layer.image_instance.len() > 0 {
|
|
pass.set_vertex_buffer(0, layer.image_instance.buffer.slice(..));
|
|
for (k, &tex_idx) in layer.image_tex_indices.iter().enumerate() {
|
|
pass.set_bind_group(2, self.textures.image_bind_group(tex_idx), &[]);
|
|
pass.draw(0..4, k as u32..k as u32 + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update(
|
|
&mut self,
|
|
device: &Device,
|
|
queue: &Queue,
|
|
ui: &mut UiData,
|
|
ui_render: &mut UiRenderState,
|
|
) -> FrameUpdateStats {
|
|
self.active.clear();
|
|
for (i, primitives) in ui_render.layers.iter_mut() {
|
|
self.active.push(i);
|
|
for change in primitives.apply_free() {
|
|
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
|
for h in &mut inst.primitives {
|
|
// `is_image` disambiguates: `instances` and `images`
|
|
// are separate lists with independent indices, so
|
|
// without it a rect's renumbering could be applied to
|
|
// an image handle that happened to share the same
|
|
// (layer, inst_idx).
|
|
if h.layer == i
|
|
&& h.inst_idx == change.old
|
|
&& (h.binding == IMAGE_BINDING) == change.is_image
|
|
{
|
|
h.inst_idx = change.new;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let rlayer = self.layers.entry(i).or_insert_with(|| {
|
|
let primitives = PrimitiveBuffers::new(device);
|
|
let primitive_group =
|
|
Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
|
|
RenderLayer {
|
|
instance: ArrBuf::new(
|
|
device,
|
|
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
|
"instance",
|
|
),
|
|
primitives,
|
|
primitive_group,
|
|
image_instance: ArrBuf::new(
|
|
device,
|
|
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
|
"image instance",
|
|
),
|
|
image_tex_indices: Vec::new(),
|
|
}
|
|
});
|
|
if primitives.updated {
|
|
rlayer
|
|
.instance
|
|
.update(device, queue, primitives.instances());
|
|
rlayer.primitives.update(device, queue, primitives.data());
|
|
rlayer.primitive_group = Self::primitive_group(
|
|
device,
|
|
&self.primitive_layout,
|
|
rlayer.primitives.buffers(),
|
|
);
|
|
rlayer
|
|
.image_instance
|
|
.update(device, queue, primitives.image_instances());
|
|
rlayer.image_tex_indices = primitives
|
|
.image_instances()
|
|
.iter()
|
|
.map(|inst| inst.idx)
|
|
.collect();
|
|
primitives.updated = false;
|
|
}
|
|
}
|
|
let masks_resized = if ui.masks.changed {
|
|
ui.masks.changed = false;
|
|
self.masks.update(device, queue, &ui.masks[..])
|
|
} else {
|
|
false
|
|
};
|
|
let moves_resized = if ui.move_offsets.changed {
|
|
ui.move_offsets.changed = false;
|
|
self.move_offsets
|
|
.update(device, queue, &ui.move_offsets[..])
|
|
} else {
|
|
false
|
|
};
|
|
if masks_resized || moves_resized {
|
|
self.masks_group =
|
|
Self::masks_group(device, &self.masks_layout, &self.masks, &self.move_offsets);
|
|
}
|
|
let rebuild_main = self.textures.update(&mut ui.textures, &self.rsc_layout);
|
|
if rebuild_main {
|
|
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures);
|
|
}
|
|
FrameUpdateStats {
|
|
masks_resized,
|
|
moves_resized,
|
|
}
|
|
}
|
|
|
|
/// Takes a size rather than a window type: this is the only thing the
|
|
/// core wanted from winit, and depending on a windowing backend for two
|
|
/// numbers is what put `android-activity` in the core's graph for an
|
|
/// Android build that is meant to go through android-view instead.
|
|
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));
|
|
}
|
|
|
|
/// Builds every bind group layout, the pipeline, and the two storage
|
|
/// buffers this needs -- fallibly, since this is exactly the call that
|
|
/// aborted the process on Iris's phone in a release build with no
|
|
/// message beyond "wgpu error: Validation Error" (RUST.md's P0 box,
|
|
/// "iris bench crash on the phone, 2026-09-06"). wgpu's own default
|
|
/// behaviour for an uncaptured error is `panic!` with no caller able to
|
|
/// intervene, so every `create_bind_group_layout`/`create_render_pipeline`
|
|
/// call below runs inside three nested error scopes (one per
|
|
/// `ErrorFilter`) instead: whichever scope catches something, its
|
|
/// `wgpu::Error`'s `Display` is wgpu-core's own `format_error` output
|
|
/// (`"Validation Error\n\nCaused by:\n ..."`, the same text the panic
|
|
/// would have printed before Android's crash reporter truncated it) and
|
|
/// becomes this function's `Err`. Both callers
|
|
/// (`android::render::AndroidRenderer::new`, `default::render::
|
|
/// UiRenderer::new`) already call `Device`-creation with
|
|
/// `pollster::block_on`, so returning a plain `Result` here rather than
|
|
/// making this `async fn` keeps that same synchronous shape.
|
|
pub fn new(
|
|
device: &Device,
|
|
queue: &Queue,
|
|
config: &SurfaceConfiguration,
|
|
window_size: impl Into<Vec2>,
|
|
) -> Result<Self, String> {
|
|
// Popped in reverse of this order, once every creation call below
|
|
// has run -- `Device::push_error_scope`'s own contract.
|
|
let oom_scope = device.push_error_scope(ErrorFilter::OutOfMemory);
|
|
let validation_scope = device.push_error_scope(ErrorFilter::Validation);
|
|
let internal_scope = device.push_error_scope(ErrorFilter::Internal);
|
|
|
|
let shader = device.create_shader_module(ShaderModuleDescriptor {
|
|
label: Some("UI Shape Shader"),
|
|
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
|
|
});
|
|
|
|
// Seeded from the caller's own reported size, not
|
|
// `WindowUniform::default()` (0, 0): the vertex shader divides by
|
|
// `window.dim` to reach clip space, so a window this buffer
|
|
// disagrees with means every primitive's position is NaN/Inf and is
|
|
// dropped before rasterization -- the clear colour still reaches
|
|
// the screen (the pass runs regardless) while nothing drawn on top
|
|
// of it ever does. winit's backend gets away with the old default
|
|
// because winit fires an initial `WindowEvent::Resized` that calls
|
|
// `resize()` before the first frame; android-view has no such
|
|
// automatic event, so `AndroidRenderer::new` built a node whose
|
|
// window buffer was never corrected -- this is I2's "nothing draws"
|
|
// bug (RUST.md).
|
|
//
|
|
// **Deliberately not `config.width`/`config.height`**: those are
|
|
// the surface's *physical* pixel size, which the swapchain needs,
|
|
// but everything downstream of this uniform (layout, hit-testing,
|
|
// glyph/rect positions) works in the caller's own units -- on
|
|
// Android that's *logical* (physical / density) since RUST.md's P0
|
|
// box ("text is far too small"), on desktop it's whatever
|
|
// `default::render::UiRenderer::new` already divides by
|
|
// `window.scale_factor()`. Passing it in explicitly, rather than
|
|
// deriving it from `config` here, is what keeps this crate from
|
|
// needing to know either platform's notion of density at all.
|
|
let window_uniform = {
|
|
let size = window_size.into();
|
|
WindowUniform {
|
|
width: size.x,
|
|
height: size.y,
|
|
}
|
|
};
|
|
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
|
label: Some("window"),
|
|
contents: bytemuck::cast_slice(&[window_uniform]),
|
|
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
|
});
|
|
|
|
let uniform_layout = 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: None,
|
|
},
|
|
count: None,
|
|
}],
|
|
label: Some("window"),
|
|
});
|
|
|
|
let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer);
|
|
|
|
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
|
entries: &PrimitiveBuffers::BINDINGS.map(|binding| BindGroupLayoutEntry {
|
|
binding,
|
|
visibility: ShaderStages::FRAGMENT,
|
|
ty: BindingType::Buffer {
|
|
ty: BufferBindingType::Storage { read_only: true },
|
|
has_dynamic_offset: false,
|
|
min_binding_size: None,
|
|
},
|
|
count: None,
|
|
}),
|
|
label: Some("primitive"),
|
|
});
|
|
|
|
let tex_manager = GpuTextures::new(device, queue);
|
|
let masks = ArrBuf::new(
|
|
device,
|
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
"ui masks",
|
|
);
|
|
let move_offsets = ArrBuf::new(
|
|
device,
|
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
"ui move offsets",
|
|
);
|
|
|
|
let rsc_layout = Self::rsc_layout(device);
|
|
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager);
|
|
let masks_layout = Self::masks_layout(device);
|
|
let masks_group = Self::masks_group(device, &masks_layout, &masks, &move_offsets);
|
|
|
|
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
|
label: Some("UI Shape Pipeline Layout"),
|
|
bind_group_layouts: &[
|
|
&uniform_layout,
|
|
&primitive_layout,
|
|
&rsc_layout,
|
|
&masks_layout,
|
|
],
|
|
immediate_size: 0,
|
|
});
|
|
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
|
label: Some("UI Shape Pipeline"),
|
|
layout: Some(&pipeline_layout),
|
|
vertex: VertexState {
|
|
module: &shader,
|
|
entry_point: Some("vs_main"),
|
|
buffers: &[PrimitiveInstance::desc()],
|
|
compilation_options: Default::default(),
|
|
},
|
|
fragment: Some(FragmentState {
|
|
module: &shader,
|
|
entry_point: Some("fs_main"),
|
|
targets: &[Some(ColorTargetState {
|
|
format: config.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,
|
|
});
|
|
|
|
// Reverse of the push order above. Only one of these should ever be
|
|
// `Some` in practice -- three separate scopes exist to name *which*
|
|
// kind of error it was, not because more than one is expected at
|
|
// once.
|
|
let internal_err = internal_scope.pop().block_on();
|
|
let validation_err = validation_scope.pop().block_on();
|
|
let oom_err = oom_scope.pop().block_on();
|
|
if let Some(err) = validation_err.or(oom_err).or(internal_err) {
|
|
return Err(err.to_string());
|
|
}
|
|
|
|
Ok(Self {
|
|
uniform_group,
|
|
primitive_layout,
|
|
rsc_layout,
|
|
rsc_group,
|
|
pipeline,
|
|
window_buffer,
|
|
layers: HashMap::default(),
|
|
active: Vec::new(),
|
|
textures: tex_manager,
|
|
masks,
|
|
move_offsets,
|
|
masks_layout,
|
|
masks_group,
|
|
})
|
|
}
|
|
|
|
fn bind_group_0(
|
|
device: &Device,
|
|
layout: &BindGroupLayout,
|
|
window_buffer: &Buffer,
|
|
) -> BindGroup {
|
|
device.create_bind_group(&BindGroupDescriptor {
|
|
layout,
|
|
entries: &[BindGroupEntry {
|
|
binding: 0,
|
|
resource: window_buffer.as_entire_binding(),
|
|
}],
|
|
label: Some("ui window"),
|
|
})
|
|
}
|
|
|
|
fn primitive_group(
|
|
device: &Device,
|
|
layout: &BindGroupLayout,
|
|
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
|
|
) -> BindGroup {
|
|
device.create_bind_group(&BindGroupDescriptor {
|
|
layout,
|
|
entries: &buffers.map(|(binding, buf)| BindGroupEntry {
|
|
binding,
|
|
resource: buf.as_entire_binding(),
|
|
}),
|
|
label: Some("ui primitives"),
|
|
})
|
|
}
|
|
|
|
/// Group 2: the shared atlas array and one standalone-image slot (a null
|
|
/// view for the main draw, a real one for each image's own bind group --
|
|
/// see `GpuTextures`), plus one sampler. No `count` on any entry: this
|
|
/// needs nothing beyond plain Vulkan 1.0 / GLES sampling, unlike the
|
|
/// `binding_array` layout it replaced (see TEXTURES.md's "Recommended
|
|
/// shape"). Masks and move_offsets are deliberately *not* here -- see
|
|
/// `masks_layout` below for why they get their own group.
|
|
fn rsc_layout(device: &Device) -> BindGroupLayout {
|
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
|
entries: &[
|
|
BindGroupLayoutEntry {
|
|
binding: 0,
|
|
visibility: ShaderStages::FRAGMENT,
|
|
ty: BindingType::Texture {
|
|
sample_type: TextureSampleType::Float { filterable: false },
|
|
view_dimension: TextureViewDimension::D2Array,
|
|
multisampled: false,
|
|
},
|
|
count: None,
|
|
},
|
|
BindGroupLayoutEntry {
|
|
binding: 1,
|
|
visibility: ShaderStages::FRAGMENT,
|
|
ty: BindingType::Texture {
|
|
sample_type: TextureSampleType::Float { filterable: false },
|
|
view_dimension: TextureViewDimension::D2,
|
|
multisampled: false,
|
|
},
|
|
count: None,
|
|
},
|
|
BindGroupLayoutEntry {
|
|
binding: 2,
|
|
visibility: ShaderStages::FRAGMENT,
|
|
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
|
count: None,
|
|
},
|
|
],
|
|
label: Some("ui rsc"),
|
|
})
|
|
}
|
|
|
|
/// The main group: rects and glyphs never sample the image slot, so it
|
|
/// gets a 1x1 null view rather than any live standalone image's.
|
|
fn rsc_group(
|
|
device: &Device,
|
|
layout: &BindGroupLayout,
|
|
tex_manager: &GpuTextures,
|
|
) -> BindGroup {
|
|
device.create_bind_group(&BindGroupDescriptor {
|
|
layout,
|
|
entries: &[
|
|
BindGroupEntry {
|
|
binding: 0,
|
|
resource: BindingResource::TextureView(tex_manager.array_view()),
|
|
},
|
|
BindGroupEntry {
|
|
binding: 1,
|
|
resource: BindingResource::TextureView(tex_manager.null_view()),
|
|
},
|
|
BindGroupEntry {
|
|
binding: 2,
|
|
resource: BindingResource::Sampler(tex_manager.sampler()),
|
|
},
|
|
],
|
|
label: Some("ui rsc"),
|
|
})
|
|
}
|
|
|
|
/// Group 3: the masks and move_offsets storage buffers, shared by the
|
|
/// main draw and every standalone image alike (see the field comment on
|
|
/// `masks_group`). Bound once per frame in `draw()` rather than folded
|
|
/// into group 2, so a resize of either buffer -- which an unrelated
|
|
/// widget's first move slot can trigger -- rebuilds this one group
|
|
/// instead of every image's.
|
|
fn masks_layout(device: &Device) -> 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: None,
|
|
},
|
|
count: None,
|
|
},
|
|
BindGroupLayoutEntry {
|
|
binding: 1,
|
|
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
|
ty: BindingType::Buffer {
|
|
ty: BufferBindingType::Storage { read_only: true },
|
|
has_dynamic_offset: false,
|
|
min_binding_size: None,
|
|
},
|
|
count: None,
|
|
},
|
|
],
|
|
label: Some("ui masks"),
|
|
})
|
|
}
|
|
|
|
fn masks_group(
|
|
device: &Device,
|
|
layout: &BindGroupLayout,
|
|
masks: &ArrBuf<Mask>,
|
|
move_offsets: &ArrBuf<MoveOffset>,
|
|
) -> BindGroup {
|
|
device.create_bind_group(&BindGroupDescriptor {
|
|
layout,
|
|
entries: &[
|
|
BindGroupEntry {
|
|
binding: 0,
|
|
resource: masks.buffer.as_entire_binding(),
|
|
},
|
|
BindGroupEntry {
|
|
binding: 1,
|
|
resource: move_offsets.buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
label: Some("ui masks"),
|
|
})
|
|
}
|
|
|
|
pub fn view_count(&self) -> usize {
|
|
self.textures.view_count()
|
|
}
|
|
|
|
/// Standalone-image bind groups built since the last call -- see
|
|
/// `GpuTextures::take_bind_group_creates`. Call once per frame before
|
|
/// `update()` to measure exactly that frame.
|
|
pub fn take_image_bind_group_creates(&mut self) -> u64 {
|
|
self.textures.take_bind_group_creates()
|
|
}
|
|
|
|
/// Atlas-array `grow_array` calls since the last call -- same calling
|
|
/// convention as `take_image_bind_group_creates` (call once per frame,
|
|
/// before `update()`, to read exactly the previous frame's tally). Part
|
|
/// of the Diagnostics page's per-frame report (RUST.md's P0 box, "the
|
|
/// first input frame" investigation): if a report ever shows a grow
|
|
/// landing on the same frame the glyphs vanished, that is the
|
|
/// coincidence to chase first.
|
|
pub fn take_atlas_pages_grown(&mut self) -> u64 {
|
|
self.textures.take_pages_grown()
|
|
}
|
|
}
|
|
|
|
/// What `UiRenderNode::update` changed this frame that a caller building a
|
|
/// per-frame diagnostic report cares about -- see `take_image_bind_group_creates`/
|
|
/// `take_atlas_pages_grown` for the two counters this doesn't carry (they
|
|
/// use the existing "call before update()" convention instead, so as not
|
|
/// to disturb `bench_images`' documented counts).
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct FrameUpdateStats {
|
|
pub masks_resized: bool,
|
|
pub moves_resized: bool,
|
|
}
|