Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91b71b97dc | ||
|
|
b9c4856e3f | ||
|
|
79dcc156c9 | ||
|
|
c0fcc0345c | ||
|
|
444a2cd138 | ||
|
|
23fb71ee56 | ||
|
|
01a9b8633d | ||
|
|
29d390da52 | ||
|
|
7b318e3271 | ||
|
|
89491a5949 | ||
|
|
a08f61a80c | ||
|
|
4d9839f380 | ||
|
|
b3d3da5dab | ||
|
|
f5864da3c4 | ||
|
|
0106257be0 | ||
|
|
bafaa1db6d |
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
use std::ops::{Index, IndexMut};
|
use std::ops::{Index, IndexMut};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
|
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||||
util::to_mut,
|
util::to_mut,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ struct Child {
|
|||||||
tail: usize,
|
tail: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PrimitiveLayers = Layers<Primitives>;
|
pub type DrawLayers = Layers<LayerDraws>;
|
||||||
|
|
||||||
impl<T: Default> Layers<T> {
|
impl<T: Default> Layers<T> {
|
||||||
pub fn new() -> Layers<T> {
|
pub fn new() -> Layers<T> {
|
||||||
@@ -119,7 +119,7 @@ impl<T: Default> Layers<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrimitiveLayers {
|
impl DrawLayers {
|
||||||
pub fn write<P: Primitive>(
|
pub fn write<P: Primitive>(
|
||||||
&mut self,
|
&mut self,
|
||||||
layer: LayerId,
|
layer: LayerId,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor,
|
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
||||||
util::Vec2,
|
|
||||||
};
|
};
|
||||||
use parley::{
|
use parley::{
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||||
@@ -159,7 +158,7 @@ impl TextBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextData {
|
impl TextData {
|
||||||
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
|
pub fn place(&mut self, buffer: &TextBuffer) -> Vec<PlacedGlyph> {
|
||||||
let mut placed = Vec::new();
|
let mut placed = Vec::new();
|
||||||
for line in buffer.layout.lines() {
|
for line in buffer.layout.lines() {
|
||||||
for item in line.items() {
|
for item in line.items() {
|
||||||
@@ -185,17 +184,14 @@ impl TextData {
|
|||||||
subpixel,
|
subpixel,
|
||||||
coords: coords_hash,
|
coords: coords_hash,
|
||||||
};
|
};
|
||||||
let Some(entry) = self.glyph_entry(
|
let Some(entry) = self.glyph_entry(GlyphRaster {
|
||||||
GlyphRaster {
|
|
||||||
key,
|
key,
|
||||||
font: font_ref,
|
font: font_ref,
|
||||||
font_size,
|
font_size,
|
||||||
coords,
|
coords,
|
||||||
subpixel,
|
subpixel,
|
||||||
glyph_id: glyph.id,
|
glyph_id: glyph.id,
|
||||||
},
|
}) else {
|
||||||
textures,
|
|
||||||
) else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
placed.push(PlacedGlyph {
|
placed.push(PlacedGlyph {
|
||||||
@@ -211,11 +207,7 @@ impl TextData {
|
|||||||
placed
|
placed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn glyph_entry(
|
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
|
||||||
&mut self,
|
|
||||||
glyph: GlyphRaster<'_>,
|
|
||||||
textures: &mut Textures,
|
|
||||||
) -> Option<GlyphEntry> {
|
|
||||||
if let Some(entry) = self.atlas.get(&glyph.key) {
|
if let Some(entry) = self.atlas.get(&glyph.key) {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
@@ -237,7 +229,7 @@ impl TextData {
|
|||||||
.render(&mut scaler, glyph.glyph_id as u16);
|
.render(&mut scaler, glyph.glyph_id as u16);
|
||||||
|
|
||||||
if let Some(image) = image {
|
if let Some(image) = image {
|
||||||
self.atlas.insert(glyph.key, &image, textures)
|
self.atlas.insert(glyph.key, &image)
|
||||||
} else {
|
} else {
|
||||||
self.atlas.insert_empty(glyph.key);
|
self.atlas.insert_empty(glyph.key);
|
||||||
None
|
None
|
||||||
@@ -278,10 +270,9 @@ impl TextData {
|
|||||||
buffer: &mut TextBuffer,
|
buffer: &mut TextBuffer,
|
||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
textures: &mut Textures,
|
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
buffer.shape(self, attrs, width);
|
buffer.shape(self, attrs, width);
|
||||||
let glyphs = self.place(buffer, textures);
|
let glyphs = self.place(buffer);
|
||||||
RenderedText {
|
RenderedText {
|
||||||
glyphs,
|
glyphs,
|
||||||
size: buffer.size(),
|
size: buffer.size(),
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
use crate::{
|
use crate::util::{RefCounter, Vec2};
|
||||||
render::TexturePrimitive,
|
|
||||||
util::{RefCounter, Vec2},
|
|
||||||
};
|
|
||||||
use image::{DynamicImage, GenericImageView};
|
use image::{DynamicImage, GenericImageView};
|
||||||
use std::{
|
use std::{
|
||||||
ops::Index,
|
ops::Index,
|
||||||
@@ -10,7 +7,7 @@ use std::{
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TextureHandle {
|
pub struct TextureHandle {
|
||||||
inner: TexturePrimitive,
|
slot: u32,
|
||||||
size: Vec2,
|
size: Vec2,
|
||||||
counter: RefCounter,
|
counter: RefCounter,
|
||||||
send: Sender<u32>,
|
send: Sender<u32>,
|
||||||
@@ -31,6 +28,8 @@ pub enum TextureUpdate<'a> {
|
|||||||
Set(u32, &'a DynamicImage),
|
Set(u32, &'a DynamicImage),
|
||||||
Patch(u32, PatchRect, &'a DynamicImage),
|
Patch(u32, PatchRect, &'a DynamicImage),
|
||||||
Free(u32),
|
Free(u32),
|
||||||
|
/// Added and freed before the renderer drained either update. It still has
|
||||||
|
/// to push a slot to stay lined up with `images`; `Free` then empties it.
|
||||||
PushFree,
|
PushFree,
|
||||||
SetFree,
|
SetFree,
|
||||||
}
|
}
|
||||||
@@ -64,14 +63,8 @@ impl Textures {
|
|||||||
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
||||||
let image = image.into();
|
let image = image.into();
|
||||||
let size = image.dimensions().into();
|
let size = image.dimensions().into();
|
||||||
let view_idx = self.push(image);
|
|
||||||
// 0 == default in renderer; TODO: actually create samplers here
|
|
||||||
let sampler_idx = 0;
|
|
||||||
TextureHandle {
|
TextureHandle {
|
||||||
inner: TexturePrimitive {
|
slot: self.push(image),
|
||||||
view_idx,
|
|
||||||
sampler_idx,
|
|
||||||
},
|
|
||||||
size,
|
size,
|
||||||
counter: RefCounter::new(),
|
counter: RefCounter::new(),
|
||||||
send: self.send.clone(),
|
send: self.send.clone(),
|
||||||
@@ -92,15 +85,20 @@ impl Textures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
|
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
|
||||||
self.images[handle.inner.view_idx as usize]
|
self.images[handle.slot as usize]
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("texture was freed while still held")
|
.expect("texture was freed while still held")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue an upload of just `rect`, after writing it with `image_mut`.
|
/// Queue an upload of just `rect`, after writing it with `image_mut`.
|
||||||
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
|
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
|
||||||
self.updates
|
self.updates.push(Update::Patch(handle.slot, rect));
|
||||||
.push(Update::Patch(handle.inner.view_idx, rect));
|
}
|
||||||
|
|
||||||
|
/// How many textures are live, which is what a ui can ask; the renderer's
|
||||||
|
/// copies follow from the updates it drains.
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.images.iter().flatten().count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn free(&mut self) {
|
pub fn free(&mut self) {
|
||||||
@@ -131,8 +129,9 @@ impl Textures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextureHandle {
|
impl TextureHandle {
|
||||||
pub fn primitive(&self) -> TexturePrimitive {
|
/// Index into `Textures`, and into the renderer's parallel slots.
|
||||||
self.inner
|
pub fn slot(&self) -> u32 {
|
||||||
|
self.slot
|
||||||
}
|
}
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> Vec2 {
|
||||||
self.size
|
self.size
|
||||||
@@ -142,7 +141,7 @@ impl TextureHandle {
|
|||||||
impl Drop for TextureHandle {
|
impl Drop for TextureHandle {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if self.counter.drop() {
|
if self.counter.drop() {
|
||||||
let _ = self.send.send(self.inner.view_idx);
|
let _ = self.send.send(self.slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +150,7 @@ impl Index<&TextureHandle> for Textures {
|
|||||||
type Output = DynamicImage;
|
type Output = DynamicImage;
|
||||||
|
|
||||||
fn index(&self, index: &TextureHandle) -> &Self::Output {
|
fn index(&self, index: &TextureHandle) -> &Self::Output {
|
||||||
self.images[index.inner.view_idx as usize].as_ref().unwrap()
|
self.images[index.slot as usize].as_ref().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-36
@@ -1,11 +1,12 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
PatchRect, TextureHandle, Textures,
|
PatchRect,
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
use swash::scale::image::{Content, Image};
|
use swash::scale::image::{Content, Image};
|
||||||
|
|
||||||
const PAGE: u32 = 1024;
|
/// Side of one page, and so of every layer of `render::page`'s array texture.
|
||||||
|
pub(crate) const PAGE: u32 = 1024;
|
||||||
|
|
||||||
/// Transparent margin kept around every glyph, so that sampling one cannot
|
/// Transparent margin kept around every glyph, so that sampling one cannot
|
||||||
/// pick up its neighbour along a shared edge.
|
/// pick up its neighbour along a shared edge.
|
||||||
@@ -35,8 +36,8 @@ pub struct GlyphEntry {
|
|||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub is_colored: bool,
|
pub is_colored: bool,
|
||||||
pub view_idx: u32,
|
/// Which atlas array layer this glyph is on.
|
||||||
pub sampler_idx: u32,
|
pub layer: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlyphEntry {
|
impl GlyphEntry {
|
||||||
@@ -48,18 +49,26 @@ impl GlyphEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Page {
|
struct Page {
|
||||||
handle: TextureHandle,
|
image: RgbaImage,
|
||||||
x: u32,
|
x: u32,
|
||||||
y: u32,
|
y: u32,
|
||||||
shelf_height: u32,
|
shelf_height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A rectangle of one page the renderer has not uploaded yet.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct PageUpload {
|
||||||
|
pub layer: u32,
|
||||||
|
pub rect: PatchRect,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GlyphAtlas {
|
pub struct GlyphAtlas {
|
||||||
pages: Vec<Page>,
|
pages: Vec<Page>,
|
||||||
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
||||||
/// too, so it is not re-rasterised on every layout.
|
/// too, so it is not re-rasterised on every layout.
|
||||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||||
|
uploads: Vec<PageUpload>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlyphAtlas {
|
impl GlyphAtlas {
|
||||||
@@ -67,12 +76,7 @@ impl GlyphAtlas {
|
|||||||
self.entries.get(key).copied()
|
self.entries.get(key).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(
|
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
|
||||||
&mut self,
|
|
||||||
key: GlyphKey,
|
|
||||||
image: &Image,
|
|
||||||
textures: &mut Textures,
|
|
||||||
) -> Option<GlyphEntry> {
|
|
||||||
let w = image.placement.width;
|
let w = image.placement.width;
|
||||||
let h = image.placement.height;
|
let h = image.placement.height;
|
||||||
if w == 0 || h == 0 {
|
if w == 0 || h == 0 {
|
||||||
@@ -94,23 +98,11 @@ impl GlyphAtlas {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (page_idx, x, y) = self.allocate(w, h, textures);
|
let upload = self.allocate(w, h);
|
||||||
let page = &self.pages[page_idx];
|
let PatchRect { x, y, .. } = upload.rect;
|
||||||
|
write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
|
||||||
|
self.uploads.push(upload);
|
||||||
|
|
||||||
let img = textures.image_mut(&page.handle);
|
|
||||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
|
||||||
write_glyph(rgba, image, x, y);
|
|
||||||
|
|
||||||
let handle = page.handle.clone();
|
|
||||||
let rect = PatchRect {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width: w,
|
|
||||||
height: h,
|
|
||||||
};
|
|
||||||
textures.patch(&handle, rect);
|
|
||||||
|
|
||||||
let page = &self.pages[page_idx];
|
|
||||||
let scale = 1.0 / PAGE as f32;
|
let scale = 1.0 / PAGE as f32;
|
||||||
let entry = GlyphEntry {
|
let entry = GlyphEntry {
|
||||||
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
|
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
|
||||||
@@ -120,39 +112,60 @@ impl GlyphAtlas {
|
|||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
is_colored: matches!(image.content, Content::Color),
|
is_colored: matches!(image.content, Content::Color),
|
||||||
view_idx: page.handle.primitive().view_idx,
|
layer: upload.layer,
|
||||||
sampler_idx: page.handle.primitive().sampler_idx,
|
|
||||||
};
|
};
|
||||||
self.entries.insert(key, Some(entry));
|
self.entries.insert(key, Some(entry));
|
||||||
Some(entry)
|
Some(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
|
/// Reserves room for a `w` by `h` glyph, adding a page if none has it.
|
||||||
|
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
|
||||||
|
let rect = |x, y| PatchRect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
};
|
||||||
if let Some((i, (x, y))) = self
|
if let Some((i, (x, y))) = self
|
||||||
.pages
|
.pages
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
|
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
|
||||||
{
|
{
|
||||||
return (i, x, y);
|
return PageUpload {
|
||||||
|
layer: i as u32,
|
||||||
|
rect: rect(x, y),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let handle = textures.add(RgbaImage::new(PAGE, PAGE));
|
|
||||||
self.pages.push(Page {
|
self.pages.push(Page {
|
||||||
handle,
|
image: RgbaImage::new(PAGE, PAGE),
|
||||||
x: PAD + w + PAD,
|
x: PAD + w + PAD,
|
||||||
y: PAD,
|
y: PAD,
|
||||||
shelf_height: h + PAD,
|
shelf_height: h + PAD,
|
||||||
});
|
});
|
||||||
(self.pages.len() - 1, PAD, PAD)
|
PageUpload {
|
||||||
|
layer: self.pages.len() as u32 - 1,
|
||||||
|
rect: rect(PAD, PAD),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drains what has been written since the last call, for the renderer to
|
||||||
|
/// upload. A new page needs nothing more: wgpu leaves the rest of a fresh
|
||||||
|
/// layer transparent, which is what an atlas wants.
|
||||||
|
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
|
||||||
|
let pages = &self.pages;
|
||||||
|
self.uploads
|
||||||
|
.drain(..)
|
||||||
|
.map(|upload| (upload, &pages[upload.layer as usize].image))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_empty(&mut self, key: GlyphKey) {
|
pub fn insert_empty(&mut self, key: GlyphKey) {
|
||||||
self.entries.insert(key, None);
|
self.entries.insert(key, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_count(&self) -> usize {
|
pub fn page_count(&self) -> u32 {
|
||||||
self.pages.len()
|
self.pages.len() as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn glyph_count(&self) -> usize {
|
pub fn glyph_count(&self) -> usize {
|
||||||
|
|||||||
@@ -12,20 +12,16 @@ pub struct WindowUniform {
|
|||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct PrimitiveInstance {
|
pub struct PrimitiveInstance {
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub binding: u32,
|
|
||||||
pub idx: u32,
|
|
||||||
pub mask_idx: MaskIdx,
|
pub mask_idx: MaskIdx,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrimitiveInstance {
|
impl PrimitiveInstance {
|
||||||
const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
|
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
|
||||||
0 => Float32x2,
|
0 => Float32x2,
|
||||||
1 => Float32x2,
|
1 => Float32x2,
|
||||||
2 => Float32x2,
|
2 => Float32x2,
|
||||||
3 => Float32x2,
|
3 => Float32x2,
|
||||||
4 => Uint32,
|
4 => Uint32,
|
||||||
5 => Uint32,
|
|
||||||
6 => Uint32,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn desc() -> VertexBufferLayout<'static> {
|
pub fn desc() -> VertexBufferLayout<'static> {
|
||||||
|
|||||||
+199
-190
@@ -1,8 +1,6 @@
|
|||||||
use std::num::NonZero;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
UiData, UiRenderState,
|
UiData, UiRenderState,
|
||||||
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
|
render::{data::PrimitiveInstance, util::ArrBuf},
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use data::WindowUniform;
|
use data::WindowUniform;
|
||||||
@@ -13,6 +11,7 @@ use wgpu::{
|
|||||||
|
|
||||||
mod atlas;
|
mod atlas;
|
||||||
mod data;
|
mod data;
|
||||||
|
mod page;
|
||||||
mod primitive;
|
mod primitive;
|
||||||
mod texture;
|
mod texture;
|
||||||
mod util;
|
mod util;
|
||||||
@@ -21,42 +20,63 @@ pub use atlas::*;
|
|||||||
pub use data::{Mask, MaskIdx};
|
pub use data::{Mask, MaskIdx};
|
||||||
pub use primitive::*;
|
pub use primitive::*;
|
||||||
|
|
||||||
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||||
|
|
||||||
pub struct UiRenderNode {
|
pub struct UiRenderNode {
|
||||||
uniform_group: BindGroup,
|
shared_layout: BindGroupLayout,
|
||||||
primitive_layout: BindGroupLayout,
|
shared_group: BindGroup,
|
||||||
rsc_layout: BindGroupLayout,
|
format: TextureFormat,
|
||||||
rsc_group: BindGroup,
|
|
||||||
|
|
||||||
pipeline: RenderPipeline,
|
/// One per registered primitive, in id order.
|
||||||
|
primitives: Vec<PrimitivePipeline>,
|
||||||
|
|
||||||
layers: HashMap<usize, RenderLayer>,
|
layers: HashMap<usize, RenderLayer>,
|
||||||
active: Vec<usize>,
|
active: Vec<usize>,
|
||||||
window_buffer: Buffer,
|
window_buffer: Buffer,
|
||||||
textures: GpuTextures,
|
|
||||||
masks: ArrBuf<Mask>,
|
masks: ArrBuf<Mask>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RenderLayer {
|
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>,
|
instance: ArrBuf<PrimitiveInstance>,
|
||||||
primitives: PrimitiveBuffers,
|
data: ArrBuf<u8>,
|
||||||
primitive_group: BindGroup,
|
group: Option<BindGroup>,
|
||||||
|
/// What the primitive asked to keep per instance, if anything.
|
||||||
|
bindings: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiRenderNode {
|
impl UiRenderNode {
|
||||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||||
pass.set_pipeline(&self.pipeline);
|
pass.set_bind_group(0, &self.shared_group, &[]);
|
||||||
pass.set_bind_group(0, &self.uniform_group, &[]);
|
|
||||||
pass.set_bind_group(2, &self.rsc_group, &[]);
|
|
||||||
for i in &self.active {
|
for i in &self.active {
|
||||||
let layer = &self.layers[i];
|
let layer = &self.layers[i];
|
||||||
if layer.instance.len() == 0 {
|
for (id, list) in layer.primitives.iter().enumerate() {
|
||||||
continue;
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
pass.set_bind_group(1, &layer.primitive_group, &[]);
|
|
||||||
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
|
|
||||||
pass.draw(0..4, 0..layer.instance.len() as u32);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,55 +87,56 @@ impl UiRenderNode {
|
|||||||
ui: &mut UiData,
|
ui: &mut UiData,
|
||||||
ui_render: &mut UiRenderState,
|
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();
|
self.active.clear();
|
||||||
for (i, primitives) in ui_render.layers.iter_mut() {
|
for (i, draws) in ui_render.layers.iter_mut() {
|
||||||
self.active.push(i);
|
self.active.push(i);
|
||||||
for change in primitives.apply_free() {
|
for change in draws.apply_free() {
|
||||||
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
||||||
for h in &mut inst.primitives {
|
for h in &mut inst.primitives {
|
||||||
if h.layer == i && h.inst_idx == change.old {
|
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
|
||||||
h.inst_idx = change.new;
|
h.inst_idx = change.new;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let rlayer = self.layers.entry(i).or_insert_with(|| {
|
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
|
||||||
let primitives = PrimitiveBuffers::new(device);
|
if draws.updated {
|
||||||
let primitive_group =
|
let lists = draws.primitives();
|
||||||
Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
|
// The zip would otherwise skip a list with no pipeline.
|
||||||
RenderLayer {
|
assert!(lists.len() <= self.primitives.len());
|
||||||
instance: ArrBuf::new(
|
rlayer.primitives.resize_with(lists.len(), || None);
|
||||||
device,
|
for ((buffers, list), primitive) in rlayer
|
||||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
.primitives
|
||||||
"instance",
|
.iter_mut()
|
||||||
),
|
.zip(lists)
|
||||||
primitives,
|
.zip(&self.primitives)
|
||||||
primitive_group,
|
{
|
||||||
|
let Some(list) = list else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
buffers
|
||||||
|
.get_or_insert_with(|| ListBuffers::new(device))
|
||||||
|
.update(device, queue, primitive, list);
|
||||||
}
|
}
|
||||||
});
|
draws.updated = false;
|
||||||
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(),
|
|
||||||
);
|
|
||||||
primitives.updated = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut changed = false;
|
for primitive in &mut self.primitives {
|
||||||
changed |= self.textures.update(&mut ui.textures);
|
primitive.render.update(ui);
|
||||||
|
}
|
||||||
if ui.masks.changed {
|
if ui.masks.changed {
|
||||||
ui.masks.changed = false;
|
ui.masks.changed = false;
|
||||||
self.masks.update(device, queue, &ui.masks[..]);
|
if self.masks.update(device, queue, &ui.masks[..]) {
|
||||||
changed = true;
|
self.shared_group = Self::shared_group(
|
||||||
|
device,
|
||||||
|
&self.shared_layout,
|
||||||
|
&self.window_buffer,
|
||||||
|
&self.masks,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if changed {
|
|
||||||
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,17 +149,7 @@ impl UiRenderNode {
|
|||||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
||||||
device: &Device,
|
|
||||||
queue: &Queue,
|
|
||||||
config: &SurfaceConfiguration,
|
|
||||||
limits: UiLimits,
|
|
||||||
) -> Self {
|
|
||||||
let shader = device.create_shader_module(ShaderModuleDescriptor {
|
|
||||||
label: Some("UI Shape Shader"),
|
|
||||||
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let window_uniform = WindowUniform {
|
let window_uniform = WindowUniform {
|
||||||
width: config.width as f32,
|
width: config.width as f32,
|
||||||
height: config.height as f32,
|
height: config.height as f32,
|
||||||
@@ -149,67 +160,73 @@ impl UiRenderNode {
|
|||||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||||
});
|
});
|
||||||
|
|
||||||
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
let shared_layout = Self::shared_layout(device);
|
||||||
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: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| {
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: i as u32,
|
|
||||||
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(
|
let masks = ArrBuf::new(
|
||||||
device,
|
device,
|
||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
"ui masks",
|
"ui masks",
|
||||||
);
|
);
|
||||||
|
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
||||||
|
|
||||||
let rsc_layout = Self::rsc_layout(device, &limits);
|
Self {
|
||||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
|
shared_layout,
|
||||||
|
shared_group,
|
||||||
|
format: config.format,
|
||||||
|
primitives: Vec::new(),
|
||||||
|
window_buffer,
|
||||||
|
layers: HashMap::default(),
|
||||||
|
active: Vec::new(),
|
||||||
|
masks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
/// Compiles a pipeline for every primitive registered since the last call.
|
||||||
label: Some("UI Shape Pipeline Layout"),
|
/// Sources only ever arrive at the end, so an id keeps its pipeline.
|
||||||
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
|
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,
|
immediate_size: 0,
|
||||||
});
|
});
|
||||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
|
||||||
label: Some("UI Shape Pipeline"),
|
self.primitives.push(PrimitivePipeline {
|
||||||
layout: Some(&pipeline_layout),
|
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 {
|
vertex: VertexState {
|
||||||
module: &shader,
|
module: &module,
|
||||||
entry_point: Some("vs_main"),
|
entry_point: Some("vs_main"),
|
||||||
buffers: &[PrimitiveInstance::desc()],
|
buffers: &[PrimitiveInstance::desc()],
|
||||||
compilation_options: Default::default(),
|
compilation_options: Default::default(),
|
||||||
},
|
},
|
||||||
fragment: Some(FragmentState {
|
fragment: Some(FragmentState {
|
||||||
module: &shader,
|
module: &module,
|
||||||
entry_point: Some("fs_main"),
|
entry_point: Some("fs_main"),
|
||||||
targets: &[Some(ColorTargetState {
|
targets: &[Some(ColorTargetState {
|
||||||
format: config.format,
|
format,
|
||||||
blend: Some(BlendState::ALPHA_BLENDING),
|
blend: Some(BlendState::ALPHA_BLENDING),
|
||||||
write_mask: ColorWrites::ALL,
|
write_mask: ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
@@ -232,90 +249,42 @@ impl UiRenderNode {
|
|||||||
},
|
},
|
||||||
multiview_mask: None,
|
multiview_mask: None,
|
||||||
cache: None,
|
cache: None,
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
uniform_group,
|
|
||||||
primitive_layout,
|
|
||||||
rsc_layout,
|
|
||||||
rsc_group,
|
|
||||||
pipeline,
|
|
||||||
window_buffer,
|
|
||||||
layers: HashMap::default(),
|
|
||||||
active: Vec::new(),
|
|
||||||
textures: tex_manager,
|
|
||||||
masks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
/// What every draw in the ui is given: the window and the masks.
|
||||||
device: &Device,
|
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||||
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"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rsc_layout(device: &Device, limits: &UiLimits) -> BindGroupLayout {
|
|
||||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
entries: &[
|
entries: &[
|
||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||||
ty: BindingType::Texture {
|
ty: BindingType::Buffer {
|
||||||
sample_type: TextureSampleType::Float { filterable: false },
|
ty: BufferBindingType::Uniform,
|
||||||
view_dimension: TextureViewDimension::D2,
|
has_dynamic_offset: false,
|
||||||
multisampled: false,
|
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64),
|
||||||
},
|
},
|
||||||
count: Some(NonZero::new(limits.max_textures).unwrap()),
|
count: None,
|
||||||
},
|
},
|
||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
visibility: ShaderStages::FRAGMENT,
|
||||||
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
|
||||||
count: Some(NonZero::new(limits.max_samplers).unwrap()),
|
|
||||||
},
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 2,
|
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
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: BufferSize::new(size_of::<Mask>() as u64),
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
label: Some("ui rsc"),
|
label: Some("ui shared"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rsc_group(
|
fn shared_group(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
layout: &BindGroupLayout,
|
layout: &BindGroupLayout,
|
||||||
tex_manager: &GpuTextures,
|
window: &Buffer,
|
||||||
masks: &ArrBuf<Mask>,
|
masks: &ArrBuf<Mask>,
|
||||||
) -> BindGroup {
|
) -> BindGroup {
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
@@ -323,45 +292,85 @@ impl UiRenderNode {
|
|||||||
entries: &[
|
entries: &[
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
resource: BindingResource::TextureViewArray(&tex_manager.views()),
|
resource: window.as_entire_binding(),
|
||||||
},
|
},
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: masks.buffer.as_entire_binding(),
|
resource: masks.buffer.as_entire_binding(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
label: Some("ui rsc"),
|
label: Some("ui shared"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view_count(&self) -> usize {
|
/// Layout for a list of one primitive's data. Every size in the ui is
|
||||||
self.textures.view_count()
|
/// 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"),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiLimits {
|
impl RenderLayer {
|
||||||
max_textures: u32,
|
fn new() -> Self {
|
||||||
max_samplers: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for UiLimits {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
max_textures: 100000,
|
primitives: Vec::new(),
|
||||||
max_samplers: 1000,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiLimits {
|
impl ListBuffers {
|
||||||
pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 {
|
fn new(device: &Device) -> Self {
|
||||||
self.max_textures + self.max_samplers
|
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(),
|
||||||
}
|
}
|
||||||
pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 {
|
}
|
||||||
self.max_samplers
|
|
||||||
|
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"),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
use wgpu::*;
|
||||||
|
|
||||||
|
use crate::{GlyphAtlas, UiData};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
atlas::PAGE,
|
||||||
|
primitive::{ListDraw, PrimitiveRender},
|
||||||
|
texture::{default_sampler, sampled_group, sampled_layout, write_region},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Draws glyphs from the atlas, which it owns: one array texture bound once
|
||||||
|
/// for a whole list, since every glyph in it reads the same pages.
|
||||||
|
pub struct GlyphRender {
|
||||||
|
pages: GpuPages,
|
||||||
|
layout: BindGroupLayout,
|
||||||
|
sampler: Sampler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlyphRender {
|
||||||
|
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||||
|
let layout = sampled_layout(device, TextureViewDimension::D2Array, "ui atlas");
|
||||||
|
let sampler = default_sampler(device);
|
||||||
|
Self {
|
||||||
|
pages: GpuPages::new(device, queue, &layout, &sampler),
|
||||||
|
layout,
|
||||||
|
sampler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimitiveRender for GlyphRender {
|
||||||
|
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||||
|
Some(&self.layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ui: &mut UiData) {
|
||||||
|
self.pages
|
||||||
|
.update(&mut ui.text.atlas, &self.layout, &self.sampler);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||||
|
pass.set_bind_group(2, self.pages.group(), &[]);
|
||||||
|
pass.draw(0..4, 0..list.instances);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The glyph atlas on the GPU: one array texture whose layers are the pages
|
||||||
|
/// `GlyphAtlas` packs.
|
||||||
|
///
|
||||||
|
/// One array rather than a texture per page because a layer index is ordinary
|
||||||
|
/// Vulkan 1.0 / GLES sampling, where a `binding_array` would need
|
||||||
|
/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack.
|
||||||
|
pub struct GpuPages {
|
||||||
|
device: Device,
|
||||||
|
queue: Queue,
|
||||||
|
texture: Texture,
|
||||||
|
group: BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GpuPages {
|
||||||
|
pub fn new(
|
||||||
|
device: &Device,
|
||||||
|
queue: &Queue,
|
||||||
|
layout: &BindGroupLayout,
|
||||||
|
sampler: &Sampler,
|
||||||
|
) -> Self {
|
||||||
|
let texture = create_array(device, 1);
|
||||||
|
Self {
|
||||||
|
device: device.clone(),
|
||||||
|
queue: queue.clone(),
|
||||||
|
group: atlas_group(device, layout, &texture, sampler),
|
||||||
|
texture,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||||
|
if atlas.page_count() > self.texture.depth_or_array_layers() {
|
||||||
|
self.grow(atlas.page_count(), layout, sampler);
|
||||||
|
}
|
||||||
|
for (upload, page) in atlas.uploads() {
|
||||||
|
let dst = TexelCopyTextureInfo {
|
||||||
|
texture: &self.texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: Origin3d {
|
||||||
|
x: upload.rect.x,
|
||||||
|
y: upload.rect.y,
|
||||||
|
z: upload.layer,
|
||||||
|
},
|
||||||
|
aspect: TextureAspect::All,
|
||||||
|
};
|
||||||
|
write_region(&self.queue, dst, page, upload.rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn group(&self) -> &BindGroup {
|
||||||
|
&self.group
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Doubles until `needed` fits and copies the old layers across GPU side.
|
||||||
|
/// The new texture stales the group, so that is rebuilt here.
|
||||||
|
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||||
|
let old = self.texture.depth_or_array_layers();
|
||||||
|
let mut layers = old;
|
||||||
|
while layers < needed {
|
||||||
|
layers *= 2;
|
||||||
|
}
|
||||||
|
let texture = create_array(&self.device, layers);
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&CommandEncoderDescriptor {
|
||||||
|
label: Some("atlas grow"),
|
||||||
|
});
|
||||||
|
encoder.copy_texture_to_texture(
|
||||||
|
self.texture.as_image_copy(),
|
||||||
|
texture.as_image_copy(),
|
||||||
|
Extent3d {
|
||||||
|
width: PAGE,
|
||||||
|
height: PAGE,
|
||||||
|
depth_or_array_layers: old,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
self.group = atlas_group(&self.device, layout, &texture, sampler);
|
||||||
|
self.texture = texture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atlas_group(
|
||||||
|
device: &Device,
|
||||||
|
layout: &BindGroupLayout,
|
||||||
|
texture: &Texture,
|
||||||
|
sampler: &Sampler,
|
||||||
|
) -> BindGroup {
|
||||||
|
let view = texture.create_view(&TextureViewDescriptor {
|
||||||
|
dimension: Some(TextureViewDimension::D2Array),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
sampled_group(device, layout, &view, sampler, "ui atlas")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_array(device: &Device, layers: u32) -> Texture {
|
||||||
|
device.create_texture(&TextureDescriptor {
|
||||||
|
label: Some("glyph atlas"),
|
||||||
|
size: Extent3d {
|
||||||
|
width: PAGE,
|
||||||
|
height: PAGE,
|
||||||
|
depth_or_array_layers: layers,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: TextureDimension::D2,
|
||||||
|
format: TextureFormat::Rgba8Unorm,
|
||||||
|
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
|
||||||
|
view_formats: &[],
|
||||||
|
})
|
||||||
|
}
|
||||||
+304
-204
@@ -1,115 +1,247 @@
|
|||||||
use std::ops::{Deref, DerefMut};
|
use std::{any::TypeId, marker::PhantomData};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Color, UiRegion, WidgetId,
|
Color, TextureHandle, UiData, UiRegion, WidgetId,
|
||||||
render::{
|
render::{
|
||||||
ArrBuf,
|
|
||||||
data::{MaskIdx, PrimitiveInstance},
|
data::{MaskIdx, PrimitiveInstance},
|
||||||
|
page::GlyphRender,
|
||||||
|
texture::ImageRender,
|
||||||
},
|
},
|
||||||
util::Vec2,
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use bytemuck::Pod;
|
use bytemuck::Pod;
|
||||||
use wgpu::*;
|
use wgpu::{BindGroupLayout, Device, Queue, RenderPass};
|
||||||
|
|
||||||
pub struct Primitives {
|
/// One instance of a primitive, laid out as the struct its shader reads.
|
||||||
|
///
|
||||||
|
/// The type carries its own shader, so drawing one is all the wiring it needs:
|
||||||
|
/// its list, free list, buffers and pipeline follow from being registered.
|
||||||
|
pub trait Primitive: Pod + 'static {
|
||||||
|
/// Compiled after `prelude.wgsl`, which states what it declares and what
|
||||||
|
/// it is given.
|
||||||
|
const WGSL: &'static str;
|
||||||
|
|
||||||
|
/// Made once, the first time the renderer sees this primitive. It owns
|
||||||
|
/// whatever the shader samples and records the primitive's own draws; the
|
||||||
|
/// default owns nothing and draws every instance in one call.
|
||||||
|
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let _ = (device, queue);
|
||||||
|
Box::new(Instanced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The renderer's half of a primitive: what it samples, what it uploads, and
|
||||||
|
/// what draws it records.
|
||||||
|
///
|
||||||
|
/// Everything a draw shares -- the pipeline, the window and masks, the list's
|
||||||
|
/// own data and instance buffer -- is set before this is called. What is left
|
||||||
|
/// is what only this primitive knows: its group 2, and how many draws its
|
||||||
|
/// instances are.
|
||||||
|
pub trait PrimitiveRender {
|
||||||
|
/// The layout its shader reads at group 2. `None` for a primitive whose
|
||||||
|
/// shader samples nothing, whose pipeline then has no group 2 at all.
|
||||||
|
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uploads whatever this primitive owns, once a frame, before any draw.
|
||||||
|
fn update(&mut self, ui: &mut UiData) {
|
||||||
|
let _ = ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keeps what the primitive needs per instance at draw time, read from
|
||||||
|
/// the list's own data. A primitive that binds nothing per instance --
|
||||||
|
/// most of them -- leaves this empty and draws in one call.
|
||||||
|
fn instance_bindings(&self, list: &InstanceList, out: &mut Vec<u32>) {
|
||||||
|
let _ = (list, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a `PrimitiveRender` draws: this list's instances, and whatever
|
||||||
|
/// `instance_bindings` kept for them.
|
||||||
|
pub struct ListDraw<'a> {
|
||||||
|
pub instances: u32,
|
||||||
|
pub bindings: &'a [u32],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default: nothing sampled, every instance in one call.
|
||||||
|
pub struct Instanced;
|
||||||
|
|
||||||
|
impl PrimitiveRender for Instanced {
|
||||||
|
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||||
|
pass.draw(0..4, 0..list.instances);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which registered primitive an instance is.
|
||||||
|
pub struct PrimitiveKind<P> {
|
||||||
|
id: u32,
|
||||||
|
_p: PhantomData<fn(P)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> PrimitiveKind<P> {
|
||||||
|
fn new(id: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
_p: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Clone for PrimitiveKind<P> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Copy for PrimitiveKind<P> {}
|
||||||
|
|
||||||
|
/// Every primitive a ui can draw, in the order they were first drawn.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PrimitiveRegistry {
|
||||||
|
kinds: Vec<PrimitiveSource>,
|
||||||
|
ids: HashMap<TypeId, u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PrimitiveSource {
|
||||||
|
pub wgsl: &'static str,
|
||||||
|
pub label: &'static str,
|
||||||
|
/// Size of one instance's entry, stated as the data binding's minimum.
|
||||||
|
pub stride: u64,
|
||||||
|
pub render: fn(&Device, &Queue) -> Box<dyn PrimitiveRender>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimitiveRegistry {
|
||||||
|
/// Registers `P` if this is the first time it has been drawn.
|
||||||
|
pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> {
|
||||||
|
let Self { kinds, ids } = self;
|
||||||
|
let id = *ids.entry(TypeId::of::<P>()).or_insert_with(|| {
|
||||||
|
kinds.push(PrimitiveSource {
|
||||||
|
wgsl: P::WGSL,
|
||||||
|
label: std::any::type_name::<P>(),
|
||||||
|
stride: size_of::<P>() as u64,
|
||||||
|
render: P::render,
|
||||||
|
});
|
||||||
|
kinds.len() as u32 - 1
|
||||||
|
});
|
||||||
|
PrimitiveKind::new(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sources(&self) -> &[PrimitiveSource] {
|
||||||
|
&self.kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One registered primitive's instances in one layer. Everything per-instance
|
||||||
|
/// rides here, so it stays in step through a `swap_remove`.
|
||||||
|
pub struct InstanceList {
|
||||||
instances: Vec<PrimitiveInstance>,
|
instances: Vec<PrimitiveInstance>,
|
||||||
|
/// The widget each instance belongs to, for renumbering its handles.
|
||||||
assoc: Vec<WidgetId>,
|
assoc: Vec<WidgetId>,
|
||||||
data: PrimitiveData,
|
|
||||||
free: Vec<usize>,
|
free: Vec<usize>,
|
||||||
|
/// `stride` bytes of the primitive's own data per instance.
|
||||||
|
data: Vec<u8>,
|
||||||
|
/// From the type the list was made for, so a write is never checked.
|
||||||
|
stride: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstanceList {
|
||||||
|
fn new<P: Primitive>() -> Self {
|
||||||
|
Self {
|
||||||
|
instances: Vec::new(),
|
||||||
|
assoc: Vec::new(),
|
||||||
|
free: Vec::new(),
|
||||||
|
data: Vec::new(),
|
||||||
|
stride: size_of::<P>(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instances(&self) -> &[PrimitiveInstance] {
|
||||||
|
&self.instances
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data(&self) -> &[u8] {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stride(&self) -> usize {
|
||||||
|
self.stride
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize {
|
||||||
|
if let Some(i) = self.free.pop() {
|
||||||
|
self.instances[i] = inst;
|
||||||
|
self.assoc[i] = id;
|
||||||
|
self.data[i * self.stride..][..self.stride].copy_from_slice(data);
|
||||||
|
i
|
||||||
|
} else {
|
||||||
|
let i = self.instances.len();
|
||||||
|
self.instances.push(inst);
|
||||||
|
self.assoc.push(id);
|
||||||
|
self.data.extend_from_slice(data);
|
||||||
|
i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn free(&mut self, i: usize) -> MaskIdx {
|
||||||
|
self.free.push(i);
|
||||||
|
self.instances[i].mask_idx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_free(&mut self, kind: u32) -> impl Iterator<Item = PrimitiveChange> {
|
||||||
|
self.free.sort_by(|a, b| b.cmp(a));
|
||||||
|
let instances = &mut self.instances;
|
||||||
|
let assoc = &mut self.assoc;
|
||||||
|
let data = &mut self.data;
|
||||||
|
let stride = self.stride;
|
||||||
|
self.free.drain(..).filter_map(move |i| {
|
||||||
|
instances.swap_remove(i);
|
||||||
|
assoc.swap_remove(i);
|
||||||
|
let last = instances.len();
|
||||||
|
data.copy_within(last * stride..(last + 1) * stride, i * stride);
|
||||||
|
data.truncate(last * stride);
|
||||||
|
if i == last {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let id = assoc[i];
|
||||||
|
Some(PrimitiveChange {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
old: last,
|
||||||
|
new: i,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everything one layer draws, one list per registered primitive.
|
||||||
|
pub struct LayerDraws {
|
||||||
|
/// `None` until this layer draws that primitive, because only the write
|
||||||
|
/// knows the type the list is for.
|
||||||
|
primitives: Vec<Option<InstanceList>>,
|
||||||
pub updated: bool,
|
pub updated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Primitives {
|
impl Default for LayerDraws {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
instances: Default::default(),
|
primitives: Vec::new(),
|
||||||
assoc: Default::default(),
|
|
||||||
data: Default::default(),
|
|
||||||
free: Vec::new(),
|
|
||||||
updated: true,
|
updated: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Primitive: Pod {
|
impl LayerDraws {
|
||||||
const BINDING: u32;
|
|
||||||
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! primitives {
|
|
||||||
($($name:ident: $ty:ty => $binding:expr,)*) => {
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct PrimitiveData {
|
|
||||||
$(pub(crate) $name: PrimitiveVec<$ty>,)*
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PrimitiveBuffers {
|
|
||||||
$($name: ArrBuf<$ty>,)*
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimitiveBuffers {
|
|
||||||
pub fn update(&mut self, device: &Device, queue: &Queue, data: &PrimitiveData) {
|
|
||||||
$(self.$name.update(device, queue, &data.$name);)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimitiveBuffers {
|
|
||||||
pub const LEN: usize = primitives!(@count $($name)*);
|
|
||||||
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
|
|
||||||
[
|
|
||||||
$((<$ty>::BINDING, &self.$name.buffer),)*
|
|
||||||
]
|
|
||||||
}
|
|
||||||
pub fn new(device: &Device) -> Self {
|
|
||||||
Self {
|
|
||||||
$($name: ArrBuf::new(
|
|
||||||
device,
|
|
||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
||||||
stringify!($name),
|
|
||||||
),)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimitiveData {
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
$(self.$name.clear();)*
|
|
||||||
}
|
|
||||||
pub fn free(&mut self, binding: u32, idx: usize) {
|
|
||||||
match binding {
|
|
||||||
$(<$ty>::BINDING => self.$name.free(idx),)*
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$(
|
|
||||||
unsafe impl bytemuck::Pod for $ty {}
|
|
||||||
unsafe impl bytemuck::Zeroable for $ty {}
|
|
||||||
impl Primitive for $ty {
|
|
||||||
const BINDING: u32 = $binding;
|
|
||||||
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self> {
|
|
||||||
&mut data.$name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
};
|
|
||||||
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) };
|
|
||||||
(@count $t:tt) => { 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PrimitiveInst<P> {
|
|
||||||
pub id: WidgetId,
|
|
||||||
pub primitive: P,
|
|
||||||
pub region: UiRegion,
|
|
||||||
pub mask_idx: MaskIdx,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Primitives {
|
|
||||||
pub fn write<P: Primitive>(
|
pub fn write<P: Primitive>(
|
||||||
&mut self,
|
&mut self,
|
||||||
layer: usize,
|
layer: usize,
|
||||||
PrimitiveInst {
|
PrimitiveInst {
|
||||||
|
kind,
|
||||||
id,
|
id,
|
||||||
primitive,
|
primitive,
|
||||||
region,
|
region,
|
||||||
@@ -117,65 +249,67 @@ impl Primitives {
|
|||||||
}: PrimitiveInst<P>,
|
}: PrimitiveInst<P>,
|
||||||
) -> PrimitiveHandle {
|
) -> PrimitiveHandle {
|
||||||
self.updated = true;
|
self.updated = true;
|
||||||
let vec = P::vec(&mut self.data);
|
// Grown on first use rather than sized from the registry, which a
|
||||||
let i = vec.add(primitive);
|
// layer cannot see.
|
||||||
let inst = PrimitiveInstance {
|
if self.primitives.len() <= kind.id as usize {
|
||||||
region,
|
self.primitives.resize_with(kind.id as usize + 1, || None);
|
||||||
idx: i as u32,
|
}
|
||||||
mask_idx,
|
let inst_idx = self.primitives[kind.id as usize]
|
||||||
binding: P::BINDING,
|
.get_or_insert_with(InstanceList::new::<P>)
|
||||||
};
|
.push(
|
||||||
let inst_i = if let Some(i) = self.free.pop() {
|
id,
|
||||||
self.instances[i] = inst;
|
PrimitiveInstance { region, mask_idx },
|
||||||
self.assoc[i] = id;
|
bytemuck::bytes_of(&primitive),
|
||||||
i
|
);
|
||||||
} else {
|
PrimitiveHandle {
|
||||||
let i = self.instances.len();
|
layer,
|
||||||
self.instances.push(inst);
|
kind: kind.id,
|
||||||
self.assoc.push(id);
|
inst_idx,
|
||||||
i
|
}
|
||||||
};
|
|
||||||
PrimitiveHandle::new::<P>(layer, inst_i, i)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns (old index, new index)
|
pub fn primitives(&self) -> &[Option<InstanceList>] {
|
||||||
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
|
&self.primitives
|
||||||
self.free.sort_by(|a, b| b.cmp(a));
|
|
||||||
self.free.drain(..).filter_map(|i| {
|
|
||||||
self.instances.swap_remove(i);
|
|
||||||
self.assoc.swap_remove(i);
|
|
||||||
if i == self.instances.len() {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
let id = self.assoc[i];
|
|
||||||
let old = self.instances.len();
|
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
|
||||||
Some(PrimitiveChange { id, old, new: i })
|
self.primitives
|
||||||
})
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?)))
|
||||||
|
.flat_map(|(kind, list)| list.apply_free(kind))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||||
self.updated = true;
|
self.updated = true;
|
||||||
self.data.free(h.binding, h.data_idx);
|
self.list(h).free(h.inst_idx)
|
||||||
self.free.push(h.inst_idx);
|
|
||||||
self.instances[h.inst_idx].mask_idx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn data(&self) -> &PrimitiveData {
|
|
||||||
&self.data
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
|
|
||||||
&self.instances
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
|
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
|
||||||
self.updated = true;
|
self.updated = true;
|
||||||
&mut self.instances[h.inst_idx].region
|
&mut self.list(h).instances[h.inst_idx].region
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A handle is only ever made by `write`, which is what created the list.
|
||||||
|
fn list(&mut self, h: &PrimitiveHandle) -> &mut InstanceList {
|
||||||
|
self.primitives[h.kind as usize]
|
||||||
|
.as_mut()
|
||||||
|
.expect("handle names a primitive this layer never drew")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PrimitiveInst<P> {
|
||||||
|
pub kind: PrimitiveKind<P>,
|
||||||
|
pub id: WidgetId,
|
||||||
|
pub primitive: P,
|
||||||
|
pub region: UiRegion,
|
||||||
|
pub mask_idx: MaskIdx,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PrimitiveChange {
|
pub struct PrimitiveChange {
|
||||||
pub id: WidgetId,
|
pub id: WidgetId,
|
||||||
|
/// Which registered primitive's list moved, since they index separately.
|
||||||
|
pub kind: u32,
|
||||||
pub old: usize,
|
pub old: usize,
|
||||||
pub new: usize,
|
pub new: usize,
|
||||||
}
|
}
|
||||||
@@ -183,30 +317,12 @@ pub struct PrimitiveChange {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PrimitiveHandle {
|
pub struct PrimitiveHandle {
|
||||||
pub layer: usize,
|
pub layer: usize,
|
||||||
|
pub kind: u32,
|
||||||
pub inst_idx: usize,
|
pub inst_idx: usize,
|
||||||
pub data_idx: usize,
|
|
||||||
pub binding: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrimitiveHandle {
|
|
||||||
fn new<P: Primitive>(layer: usize, inst_idx: usize, data_idx: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
layer,
|
|
||||||
inst_idx,
|
|
||||||
data_idx,
|
|
||||||
binding: P::BINDING,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
primitives!(
|
|
||||||
rects: RectPrimitive => 0,
|
|
||||||
textures: TexturePrimitive => 1,
|
|
||||||
glyphs: GlyphPrimitive => 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct RectPrimitive {
|
pub struct RectPrimitive {
|
||||||
pub color: Color<u8>,
|
pub color: Color<u8>,
|
||||||
pub radius: f32,
|
pub radius: f32,
|
||||||
@@ -214,6 +330,10 @@ pub struct RectPrimitive {
|
|||||||
pub inner_radius: f32,
|
pub inner_radius: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Primitive for RectPrimitive {
|
||||||
|
const WGSL: &'static str = include_str!("shader/rect.wgsl");
|
||||||
|
}
|
||||||
|
|
||||||
impl RectPrimitive {
|
impl RectPrimitive {
|
||||||
pub fn color(color: Color<u8>) -> Self {
|
pub fn color(color: Color<u8>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -225,71 +345,51 @@ impl RectPrimitive {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
|
||||||
#[derive(Debug, Copy, Clone)]
|
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
|
||||||
pub struct TexturePrimitive {
|
#[repr(C, align(8))]
|
||||||
pub view_idx: u32,
|
|
||||||
pub sampler_idx: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct GlyphPrimitive {
|
pub struct GlyphPrimitive {
|
||||||
pub uv_min: Vec2,
|
pub uv_min: Vec2,
|
||||||
pub uv_max: Vec2,
|
pub uv_max: Vec2,
|
||||||
pub view_idx: u32,
|
/// Which atlas array layer this glyph is on.
|
||||||
pub sampler_idx: u32,
|
pub layer: u32,
|
||||||
pub color: Color<u8>,
|
pub color: Color<u8>,
|
||||||
pub flags: u32,
|
pub flags: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PrimitiveVec<T> {
|
// Manual rather than derived: the align(8) leaves four bytes of padding, which
|
||||||
vec: Vec<T>,
|
// is how WGSL lays the struct out.
|
||||||
free: Vec<usize>,
|
unsafe impl bytemuck::Pod for GlyphPrimitive {}
|
||||||
|
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
|
||||||
|
impl Primitive for GlyphPrimitive {
|
||||||
|
const WGSL: &'static str = include_str!("shader/glyph.wgsl");
|
||||||
|
|
||||||
|
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
|
||||||
|
Box::new(GlyphRender::new(device, queue))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> PrimitiveVec<T> {
|
/// One drawn image. Its shader reads nothing per instance; the slot names the
|
||||||
pub fn new() -> Self {
|
/// texture to bind for it.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct TexturePrimitive {
|
||||||
|
pub slot: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Primitive for TexturePrimitive {
|
||||||
|
const WGSL: &'static str = include_str!("shader/texture.wgsl");
|
||||||
|
|
||||||
|
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
|
||||||
|
Box::new(ImageRender::new(device, queue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&TextureHandle> for TexturePrimitive {
|
||||||
|
fn from(handle: &TextureHandle) -> Self {
|
||||||
Self {
|
Self {
|
||||||
vec: Vec::new(),
|
slot: handle.slot(),
|
||||||
free: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn add(&mut self, t: T) -> usize {
|
|
||||||
if let Some(i) = self.free.pop() {
|
|
||||||
self.vec[i] = t;
|
|
||||||
i
|
|
||||||
} else {
|
|
||||||
let i = self.vec.len();
|
|
||||||
self.vec.push(t);
|
|
||||||
i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn free(&mut self, i: usize) {
|
|
||||||
self.free.push(i);
|
|
||||||
}
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.free.clear();
|
|
||||||
self.vec.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for PrimitiveVec<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Deref for PrimitiveVec<T> {
|
|
||||||
type Target = Vec<T>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.vec
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> DerefMut for PrimitiveVec<T> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.vec
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
const RECT: u32 = 0u;
|
|
||||||
const TEXTURE: u32 = 1u;
|
|
||||||
const GLYPH: u32 = 2u;
|
|
||||||
|
|
||||||
@group(0) @binding(0)
|
|
||||||
var<uniform> window: WindowUniform;
|
|
||||||
@group(1) @binding(RECT)
|
|
||||||
var<storage> rects: array<Rect>;
|
|
||||||
@group(1) @binding(TEXTURE)
|
|
||||||
var<storage> textures: array<TextureInfo>;
|
|
||||||
@group(1) @binding(GLYPH)
|
|
||||||
var<storage> glyphs: array<GlyphInfo>;
|
|
||||||
|
|
||||||
struct Rect {
|
|
||||||
color: u32,
|
|
||||||
radius: f32,
|
|
||||||
thickness: f32,
|
|
||||||
inner_radius: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TextureInfo {
|
|
||||||
view_idx: u32,
|
|
||||||
sampler_idx: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GlyphInfo {
|
|
||||||
uv_min: vec2<f32>,
|
|
||||||
uv_max: vec2<f32>,
|
|
||||||
view_idx: u32,
|
|
||||||
sampler_idx: u32,
|
|
||||||
color: u32,
|
|
||||||
flags: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Mask {
|
|
||||||
x: UiSpan,
|
|
||||||
y: UiSpan,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UiSpan {
|
|
||||||
start: UiScalar,
|
|
||||||
end: UiScalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UiScalar {
|
|
||||||
rel: f32,
|
|
||||||
abs: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UiVec2 {
|
|
||||||
rel: vec2<f32>,
|
|
||||||
abs: vec2<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
@group(2) @binding(0)
|
|
||||||
var views: binding_array<texture_2d<f32>>;
|
|
||||||
@group(2) @binding(1)
|
|
||||||
var samplers: binding_array<sampler>;
|
|
||||||
@group(2) @binding(2)
|
|
||||||
var<storage> masks: array<Mask>;
|
|
||||||
|
|
||||||
struct WindowUniform {
|
|
||||||
dim: vec2<f32>,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct InstanceInput {
|
|
||||||
@location(0) x_start: vec2<f32>,
|
|
||||||
@location(1) x_end: vec2<f32>,
|
|
||||||
@location(2) y_start: vec2<f32>,
|
|
||||||
@location(3) y_end: vec2<f32>,
|
|
||||||
@location(4) binding: u32,
|
|
||||||
@location(5) idx: u32,
|
|
||||||
@location(6) mask_idx: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VertexOutput {
|
|
||||||
@location(0) top_left: vec2<f32>,
|
|
||||||
@location(1) bot_right: vec2<f32>,
|
|
||||||
@location(2) uv: vec2<f32>,
|
|
||||||
@location(3) binding: u32,
|
|
||||||
@location(4) idx: u32,
|
|
||||||
@location(5) mask_idx: u32,
|
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Region {
|
|
||||||
pos: vec2<f32>,
|
|
||||||
uv: vec2<f32>,
|
|
||||||
top_left: vec2<f32>,
|
|
||||||
bot_right: vec2<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
@vertex
|
|
||||||
fn vs_main(
|
|
||||||
@builtin(vertex_index) vi: u32,
|
|
||||||
in: InstanceInput,
|
|
||||||
) -> VertexOutput {
|
|
||||||
var out: VertexOutput;
|
|
||||||
|
|
||||||
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
|
||||||
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
|
||||||
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
|
||||||
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
|
||||||
|
|
||||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
|
||||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
|
||||||
let size = bot_right - top_left;
|
|
||||||
|
|
||||||
let uv = vec2<f32>(
|
|
||||||
f32(vi % 2u),
|
|
||||||
f32(vi / 2u)
|
|
||||||
);
|
|
||||||
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
|
|
||||||
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
|
||||||
out.uv = uv;
|
|
||||||
out.binding = in.binding;
|
|
||||||
out.idx = in.idx;
|
|
||||||
out.top_left = top_left;
|
|
||||||
out.bot_right = bot_right;
|
|
||||||
out.mask_idx = in.mask_idx;
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@fragment
|
|
||||||
fn fs_main(
|
|
||||||
in: VertexOutput
|
|
||||||
) -> @location(0) vec4<f32> {
|
|
||||||
let pos = in.clip_position.xy;
|
|
||||||
let region = Region(pos, in.uv, in.top_left, in.bot_right);
|
|
||||||
let i = in.idx;
|
|
||||||
var color: vec4<f32>;
|
|
||||||
switch in.binding {
|
|
||||||
case RECT: {
|
|
||||||
color = draw_rounded_rect(region, rects[i]);
|
|
||||||
}
|
|
||||||
case TEXTURE: {
|
|
||||||
color = draw_texture(region, textures[i]);
|
|
||||||
}
|
|
||||||
case GLYPH: {
|
|
||||||
color = draw_glyph(region, glyphs[i]);
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
color = vec4(1.0, 0.0, 1.0, 1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if in.mask_idx != 4294967295u {
|
|
||||||
let mask = masks[in.mask_idx];
|
|
||||||
let tl = UiVec2(vec2(mask.x.start.rel, mask.y.start.rel), vec2(mask.x.start.abs, mask.y.start.abs));
|
|
||||||
let br = UiVec2(vec2(mask.x.end.rel, mask.y.end.rel), vec2(mask.x.end.abs, mask.y.end.abs));
|
|
||||||
|
|
||||||
let top_left = floor(tl.rel * window.dim) + floor(tl.abs);
|
|
||||||
let bot_right = floor(br.rel * window.dim) + floor(br.abs);
|
|
||||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
|
||||||
color *= 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this seems really inefficient (per frag indexing)?
|
|
||||||
fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
|
|
||||||
return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
|
|
||||||
let uv = mix(g.uv_min, g.uv_max, region.uv);
|
|
||||||
let texel = textureSample(views[g.view_idx], samplers[g.sampler_idx], uv);
|
|
||||||
if (g.flags & 1u) != 0u {
|
|
||||||
return texel;
|
|
||||||
}
|
|
||||||
var color = unpack4x8unorm(g.color);
|
|
||||||
color.a *= texel.a;
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
|
|
||||||
var color = unpack4x8unorm(rect.color);
|
|
||||||
|
|
||||||
let edge = 0.5;
|
|
||||||
|
|
||||||
let size = region.bot_right - region.top_left;
|
|
||||||
let corner = size / 2.0;
|
|
||||||
let center = region.top_left + corner;
|
|
||||||
|
|
||||||
let dist = distance_from_rect(region.pos, center, corner, rect.radius);
|
|
||||||
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
|
|
||||||
|
|
||||||
if rect.thickness > 0.0 {
|
|
||||||
let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius);
|
|
||||||
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
|
||||||
// vec from center to pixel
|
|
||||||
let p = pixel_pos - rect_center;
|
|
||||||
// vec from inner rect corner to pixel
|
|
||||||
let q = abs(p) - (rect_corner - radius);
|
|
||||||
return length(max(q, vec2(0.0))) - radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Matches `GlyphEntry::IS_COLORED`.
|
||||||
|
const COLORED: u32 = 1u;
|
||||||
|
|
||||||
|
// The glyph atlas, whose array layers are its pages.
|
||||||
|
@group(2) @binding(0)
|
||||||
|
var atlas: texture_2d_array<f32>;
|
||||||
|
@group(2) @binding(1)
|
||||||
|
var samp: sampler;
|
||||||
|
|
||||||
|
struct GlyphInfo {
|
||||||
|
uv_min: vec2<f32>,
|
||||||
|
uv_max: vec2<f32>,
|
||||||
|
// Which layer of the atlas array this glyph's page is.
|
||||||
|
layer: u32,
|
||||||
|
color: u32,
|
||||||
|
flags: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(1) @binding(0)
|
||||||
|
var<storage> glyphs: array<GlyphInfo>;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let g = glyphs[in.idx];
|
||||||
|
let uv = mix(g.uv_min, g.uv_max, in.uv);
|
||||||
|
let texel = textureSample(atlas, samp, uv, i32(g.layer));
|
||||||
|
if (g.flags & COLORED) != 0u {
|
||||||
|
return masked(in, texel);
|
||||||
|
}
|
||||||
|
var color = unpack4x8unorm(g.color);
|
||||||
|
color.a *= texel.a;
|
||||||
|
return masked(in, color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// Prepended to every primitive's shader, which declares its own instance data
|
||||||
|
// as `var<storage> <name>: array<T>` at group 1 binding 0, and an `fs_main`
|
||||||
|
// shading one instance of it. What it samples, if anything, is bound at group
|
||||||
|
// 2: the texture at binding 0 and the sampler at binding 1.
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> window: WindowUniform;
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage> masks: array<Mask>;
|
||||||
|
|
||||||
|
struct WindowUniform {
|
||||||
|
dim: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Mask {
|
||||||
|
x: UiSpan,
|
||||||
|
y: UiSpan,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UiSpan {
|
||||||
|
start: UiScalar,
|
||||||
|
end: UiScalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UiScalar {
|
||||||
|
rel: f32,
|
||||||
|
abs: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InstanceInput {
|
||||||
|
@location(0) x_start: vec2<f32>,
|
||||||
|
@location(1) x_end: vec2<f32>,
|
||||||
|
@location(2) y_start: vec2<f32>,
|
||||||
|
@location(3) y_end: vec2<f32>,
|
||||||
|
@location(4) mask_idx: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@location(0) top_left: vec2<f32>,
|
||||||
|
@location(1) bot_right: vec2<f32>,
|
||||||
|
@location(2) uv: vec2<f32>,
|
||||||
|
@location(3) @interpolate(flat) mask_idx: u32,
|
||||||
|
@location(4) @interpolate(flat) idx: u32,
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(
|
||||||
|
@builtin(vertex_index) vi: u32,
|
||||||
|
@builtin(instance_index) ii: u32,
|
||||||
|
in: InstanceInput,
|
||||||
|
) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
|
||||||
|
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
||||||
|
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
||||||
|
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
||||||
|
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
||||||
|
|
||||||
|
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
||||||
|
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
||||||
|
let size = bot_right - top_left;
|
||||||
|
|
||||||
|
let uv = vec2<f32>(
|
||||||
|
f32(vi % 2u),
|
||||||
|
f32(vi / 2u)
|
||||||
|
);
|
||||||
|
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
|
||||||
|
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
||||||
|
out.uv = uv;
|
||||||
|
out.top_left = top_left;
|
||||||
|
out.bot_right = bot_right;
|
||||||
|
out.mask_idx = in.mask_idx;
|
||||||
|
out.idx = ii;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||||
|
if in.mask_idx == 4294967295u {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
let mask = masks[in.mask_idx];
|
||||||
|
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
|
||||||
|
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
|
||||||
|
let br = vec2(mask.x.end.rel, mask.y.end.rel);
|
||||||
|
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
|
||||||
|
|
||||||
|
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
||||||
|
let bot_right = floor(br * window.dim) + floor(br_abs);
|
||||||
|
let pos = in.clip_position.xy;
|
||||||
|
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||||
|
return color * 0.0;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
struct Rect {
|
||||||
|
color: u32,
|
||||||
|
radius: f32,
|
||||||
|
thickness: f32,
|
||||||
|
inner_radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(1) @binding(0)
|
||||||
|
var<storage> rects: array<Rect>;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let rect = rects[in.idx];
|
||||||
|
var color = unpack4x8unorm(rect.color);
|
||||||
|
|
||||||
|
let edge = 0.5;
|
||||||
|
let size = in.bot_right - in.top_left;
|
||||||
|
let corner = size / 2.0;
|
||||||
|
let center = in.top_left + corner;
|
||||||
|
let pos = in.clip_position.xy;
|
||||||
|
|
||||||
|
let dist = distance_from_rect(pos, center, corner, rect.radius);
|
||||||
|
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
|
||||||
|
|
||||||
|
if rect.thickness > 0.0 {
|
||||||
|
let dist2 = distance_from_rect(pos, center, corner - rect.thickness, rect.inner_radius);
|
||||||
|
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return masked(in, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
||||||
|
// vec from center to pixel
|
||||||
|
let p = pixel_pos - rect_center;
|
||||||
|
// vec from inner rect corner to pixel
|
||||||
|
let q = abs(p) - (rect_corner - radius);
|
||||||
|
return length(max(q, vec2(0.0))) - radius;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// The image this instance draws, bound for it alone.
|
||||||
|
@group(2) @binding(0)
|
||||||
|
var image: texture_2d<f32>;
|
||||||
|
@group(2) @binding(1)
|
||||||
|
var samp: sampler;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return masked(in, textureSample(image, samp, in.uv));
|
||||||
|
}
|
||||||
+191
-139
@@ -1,119 +1,119 @@
|
|||||||
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
|
||||||
use wgpu::{util::DeviceExt, *};
|
use wgpu::{util::DeviceExt, *};
|
||||||
|
|
||||||
use crate::{PatchRect, TextureUpdate, Textures};
|
use crate::{
|
||||||
|
PatchRect, TextureUpdate, Textures, UiData,
|
||||||
|
render::{
|
||||||
|
TexturePrimitive,
|
||||||
|
primitive::{ListDraw, PrimitiveRender},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Draws standalone images, which it owns. Each is its own texture, so each
|
||||||
|
/// instance binds its own and is a draw of its own.
|
||||||
|
pub struct ImageRender {
|
||||||
|
textures: GpuTextures,
|
||||||
|
layout: BindGroupLayout,
|
||||||
|
sampler: Sampler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageRender {
|
||||||
|
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||||
|
Self {
|
||||||
|
textures: GpuTextures::new(device, queue),
|
||||||
|
layout: sampled_layout(device, TextureViewDimension::D2, "ui image"),
|
||||||
|
sampler: default_sampler(device),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimitiveRender for ImageRender {
|
||||||
|
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||||
|
Some(&self.layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ui: &mut UiData) {
|
||||||
|
self.textures
|
||||||
|
.update(&mut ui.textures, &self.layout, &self.sampler);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_bindings(&self, list: &super::InstanceList, out: &mut Vec<u32>) {
|
||||||
|
let slots = list
|
||||||
|
.data()
|
||||||
|
.chunks_exact(list.stride())
|
||||||
|
.map(|data| bytemuck::pod_read_unaligned::<TexturePrimitive>(data).slot);
|
||||||
|
out.extend(slots);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||||
|
for (i, &slot) in list.bindings.iter().enumerate() {
|
||||||
|
let Some(image) = self.textures.group(slot) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
pass.set_bind_group(2, image, &[]);
|
||||||
|
pass.draw(0..4, i as u32..i as u32 + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The standalone images a ui draws, each its own texture and bind group --
|
||||||
|
/// unlike the glyph atlas in `super::page`, which is one array they share.
|
||||||
pub struct GpuTextures {
|
pub struct GpuTextures {
|
||||||
device: Device,
|
device: Device,
|
||||||
queue: Queue,
|
queue: Queue,
|
||||||
/// Parallel to `views`; patches require textures rather than views.
|
slots: Vec<Option<ImageGpu>>,
|
||||||
textures: Vec<Option<Texture>>,
|
}
|
||||||
views: Vec<TextureView>,
|
|
||||||
view_count: usize,
|
struct ImageGpu {
|
||||||
samplers: Vec<Sampler>,
|
/// Kept for `patch`, which needs the texture rather than the view.
|
||||||
null_view: TextureView,
|
texture: Texture,
|
||||||
no_views: Vec<TextureView>,
|
group: BindGroup,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpuTextures {
|
impl GpuTextures {
|
||||||
pub fn update(&mut self, textures: &mut Textures) -> bool {
|
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||||
let mut bindings_changed = false;
|
Self {
|
||||||
|
device: device.clone(),
|
||||||
|
queue: queue.clone(),
|
||||||
|
slots: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||||
for update in textures.updates() {
|
for update in textures.updates() {
|
||||||
bindings_changed |= match update {
|
match update {
|
||||||
TextureUpdate::Push(image) => {
|
TextureUpdate::Push(image) => {
|
||||||
self.push(image);
|
let image = self.create(image, layout, sampler);
|
||||||
true
|
self.slots.push(Some(image));
|
||||||
}
|
}
|
||||||
TextureUpdate::Set(i, image) => {
|
TextureUpdate::Set(i, image) => {
|
||||||
self.set(i, image);
|
let image = self.create(image, layout, sampler);
|
||||||
true
|
self.slots[i as usize] = Some(image);
|
||||||
}
|
}
|
||||||
TextureUpdate::Patch(i, rect, image) => {
|
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
|
||||||
self.patch(i, rect, image);
|
TextureUpdate::PushFree => self.slots.push(None),
|
||||||
false
|
TextureUpdate::SetFree => {}
|
||||||
|
TextureUpdate::Free(i) => self.slots[i as usize] = None,
|
||||||
}
|
}
|
||||||
TextureUpdate::SetFree => {
|
|
||||||
self.view_count += 1;
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
TextureUpdate::Free(i) => {
|
|
||||||
self.free(i);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
TextureUpdate::PushFree => {
|
|
||||||
self.push_free();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
bindings_changed
|
|
||||||
}
|
|
||||||
fn set(&mut self, i: u32, image: &DynamicImage) {
|
|
||||||
self.view_count += 1;
|
|
||||||
let (texture, view) = self.create(image);
|
|
||||||
self.textures[i as usize] = Some(texture);
|
|
||||||
self.views[i as usize] = view;
|
|
||||||
}
|
|
||||||
fn free(&mut self, i: u32) {
|
|
||||||
self.view_count -= 1;
|
|
||||||
self.textures[i as usize] = None;
|
|
||||||
self.views[i as usize] = self.null_view.clone();
|
|
||||||
}
|
|
||||||
fn push(&mut self, image: &DynamicImage) {
|
|
||||||
self.view_count += 1;
|
|
||||||
let (texture, view) = self.create(image);
|
|
||||||
self.textures.push(Some(texture));
|
|
||||||
self.views.push(view);
|
|
||||||
}
|
|
||||||
fn push_free(&mut self) {
|
|
||||||
self.view_count += 1;
|
|
||||||
self.textures.push(None);
|
|
||||||
self.views.push(self.null_view.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
pub fn group(&self, slot: u32) -> Option<&BindGroup> {
|
||||||
let Some(texture) = &self.textures[i as usize] else {
|
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
|
||||||
return;
|
|
||||||
};
|
|
||||||
if rect.width == 0 || rect.height == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// `write_texture` requires tightly packed rows, unlike the atlas image.
|
|
||||||
let sub = image
|
|
||||||
.view(rect.x, rect.y, rect.width, rect.height)
|
|
||||||
.to_image();
|
|
||||||
self.queue.write_texture(
|
|
||||||
TexelCopyTextureInfo {
|
|
||||||
texture,
|
|
||||||
mip_level: 0,
|
|
||||||
origin: Origin3d {
|
|
||||||
x: rect.x,
|
|
||||||
y: rect.y,
|
|
||||||
z: 0,
|
|
||||||
},
|
|
||||||
aspect: TextureAspect::All,
|
|
||||||
},
|
|
||||||
sub.as_bytes(),
|
|
||||||
TexelCopyBufferLayout {
|
|
||||||
offset: 0,
|
|
||||||
bytes_per_row: Some(rect.width * 4),
|
|
||||||
rows_per_image: Some(rect.height),
|
|
||||||
},
|
|
||||||
Extent3d {
|
|
||||||
width: rect.width,
|
|
||||||
height: rect.height,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create(&self, image: &DynamicImage) -> (Texture, TextureView) {
|
fn create(
|
||||||
let image = image.to_rgba8();
|
&self,
|
||||||
let (width, height) = image.dimensions();
|
image: &DynamicImage,
|
||||||
|
layout: &BindGroupLayout,
|
||||||
|
sampler: &Sampler,
|
||||||
|
) -> ImageGpu {
|
||||||
|
let rgba = image.to_rgba8();
|
||||||
|
let (width, height) = rgba.dimensions();
|
||||||
let texture = self.device.create_texture_with_data(
|
let texture = self.device.create_texture_with_data(
|
||||||
&self.queue,
|
&self.queue,
|
||||||
&TextureDescriptor {
|
&TextureDescriptor {
|
||||||
label: None,
|
label: Some("image"),
|
||||||
size: Extent3d {
|
size: Extent3d {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -127,63 +127,115 @@ impl GpuTextures {
|
|||||||
view_formats: &[],
|
view_formats: &[],
|
||||||
},
|
},
|
||||||
wgt::TextureDataOrder::MipMajor,
|
wgt::TextureDataOrder::MipMajor,
|
||||||
image.as_bytes(),
|
rgba.as_bytes(),
|
||||||
);
|
);
|
||||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||||
(texture, view)
|
let group = sampled_group(&self.device, layout, &view, sampler, "ui image");
|
||||||
|
ImageGpu { texture, group }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||||
let null_view = null_texture_view(device);
|
let Some(Some(slot)) = self.slots.get(i as usize) else {
|
||||||
Self {
|
return;
|
||||||
device: device.clone(),
|
};
|
||||||
queue: queue.clone(),
|
let dst = TexelCopyTextureInfo {
|
||||||
textures: Vec::new(),
|
texture: &slot.texture,
|
||||||
views: Vec::new(),
|
mip_level: 0,
|
||||||
samplers: vec![default_sampler(device)],
|
origin: Origin3d {
|
||||||
no_views: vec![null_view.clone()],
|
x: rect.x,
|
||||||
null_view,
|
y: rect.y,
|
||||||
view_count: 0,
|
z: 0,
|
||||||
|
},
|
||||||
|
aspect: TextureAspect::All,
|
||||||
|
};
|
||||||
|
match image.as_rgba8() {
|
||||||
|
Some(rgba) => write_region(&self.queue, dst, rgba, rect),
|
||||||
|
// The texture is rgba8, so any other layout has to be converted --
|
||||||
|
// and converting the rectangle is cheaper than the whole image.
|
||||||
|
None => {
|
||||||
|
let sub = image
|
||||||
|
.view(rect.x, rect.y, rect.width, rect.height)
|
||||||
|
.to_image();
|
||||||
|
write_region(&self.queue, dst, &sub, PatchRect { x: 0, y: 0, ..rect });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn views(&self) -> Vec<&TextureView> {
|
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
|
||||||
if self.views.is_empty() {
|
if rect.width == 0 || rect.height == 0 {
|
||||||
&self.no_views
|
return;
|
||||||
} else {
|
|
||||||
&self.views
|
|
||||||
}
|
}
|
||||||
.iter()
|
let stride = src.width() * 4;
|
||||||
.by_ref()
|
queue.write_texture(
|
||||||
.collect()
|
dst,
|
||||||
}
|
src.as_bytes(),
|
||||||
|
TexelCopyBufferLayout {
|
||||||
pub fn samplers(&self) -> Vec<&Sampler> {
|
offset: (rect.y * stride + rect.x * 4) as u64,
|
||||||
self.samplers.iter().by_ref().collect()
|
bytes_per_row: Some(stride),
|
||||||
}
|
rows_per_image: Some(rect.height),
|
||||||
|
},
|
||||||
pub fn view_count(&self) -> usize {
|
Extent3d {
|
||||||
self.view_count
|
width: rect.width,
|
||||||
}
|
height: rect.height,
|
||||||
}
|
|
||||||
|
|
||||||
pub fn null_texture_view(device: &Device) -> TextureView {
|
|
||||||
device
|
|
||||||
.create_texture(&TextureDescriptor {
|
|
||||||
label: Some("null"),
|
|
||||||
size: Extent3d {
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
mip_level_count: 1,
|
);
|
||||||
sample_count: 1,
|
}
|
||||||
dimension: TextureDimension::D2,
|
|
||||||
format: TextureFormat::Rgba8Unorm,
|
/// What a primitive that samples binds: a texture, and the sampler that reads
|
||||||
usage: TextureUsages::TEXTURE_BINDING,
|
/// it.
|
||||||
view_formats: &[],
|
pub fn sampled_group(
|
||||||
|
device: &Device,
|
||||||
|
layout: &BindGroupLayout,
|
||||||
|
view: &TextureView,
|
||||||
|
sampler: &Sampler,
|
||||||
|
label: &'static str,
|
||||||
|
) -> BindGroup {
|
||||||
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
|
layout,
|
||||||
|
entries: &[
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: BindingResource::TextureView(view),
|
||||||
|
},
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: BindingResource::Sampler(sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: Some(label),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The layout for one of those. The dimension differs -- the atlas is an
|
||||||
|
/// array of pages and an image is not -- and nothing else does.
|
||||||
|
pub fn sampled_layout(
|
||||||
|
device: &Device,
|
||||||
|
dimension: TextureViewDimension,
|
||||||
|
label: &'static str,
|
||||||
|
) -> 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: dimension,
|
||||||
|
multisampled: false,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: ShaderStages::FRAGMENT,
|
||||||
|
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: Some(label),
|
||||||
})
|
})
|
||||||
.create_view(&TextureViewDescriptor::default())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_sampler(device: &Device) -> Sampler {
|
pub fn default_sampler(device: &Device) -> Sampler {
|
||||||
|
|||||||
@@ -21,17 +21,25 @@ impl<T: Pod> ArrBuf<T> {
|
|||||||
_pd: PhantomData,
|
_pd: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) {
|
/// Returns whether the `Buffer` was recreated, which stales any cached
|
||||||
if self.len != data.len() {
|
/// `BindGroup` holding it.
|
||||||
|
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
|
||||||
|
let resized = self.len != data.len();
|
||||||
|
if resized {
|
||||||
self.len = data.len();
|
self.len = data.len();
|
||||||
self.buffer =
|
self.buffer =
|
||||||
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
|
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
|
||||||
}
|
}
|
||||||
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
|
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
|
||||||
|
resized
|
||||||
|
}
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
}
|
}
|
||||||
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
|
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
|
||||||
let mut size = size as u64;
|
let mut size = size as u64;
|
||||||
if usage.contains(BufferUsages::STORAGE) {
|
if usage.contains(BufferUsages::STORAGE) {
|
||||||
|
// A binding cannot be empty or under the layout's minimum.
|
||||||
size = size.max(std::mem::size_of::<T>() as u64);
|
size = size.max(std::mem::size_of::<T>() as u64);
|
||||||
}
|
}
|
||||||
device.create_buffer(&BufferDescriptor {
|
device.create_buffer(&BufferDescriptor {
|
||||||
@@ -41,8 +49,4 @@ impl<T: Pod> ArrBuf<T> {
|
|||||||
usage,
|
usage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
#[allow(clippy::len_without_is_empty)]
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.len
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+6
-2
@@ -1,4 +1,6 @@
|
|||||||
use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
|
use crate::{
|
||||||
|
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||||
|
};
|
||||||
|
|
||||||
mod active;
|
mod active;
|
||||||
mod cache;
|
mod cache;
|
||||||
@@ -7,13 +9,15 @@ mod render_state;
|
|||||||
mod size;
|
mod size;
|
||||||
|
|
||||||
pub use active::*;
|
pub use active::*;
|
||||||
pub use painter::Painter;
|
pub use painter::{Painter, PrimitiveLike};
|
||||||
pub use render_state::*;
|
pub use render_state::*;
|
||||||
pub use size::*;
|
pub use size::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct UiData {
|
pub struct UiData {
|
||||||
pub widgets: Widgets,
|
pub widgets: Widgets,
|
||||||
|
/// Every primitive this ui can draw.
|
||||||
|
pub primitives: PrimitiveRegistry,
|
||||||
pub textures: Textures,
|
pub textures: Textures,
|
||||||
pub text: TextData,
|
pub text: TextData,
|
||||||
pub masks: TrackedArena<Mask, u32>,
|
pub masks: TrackedArena<Mask, u32>,
|
||||||
|
|||||||
+50
-22
@@ -1,7 +1,10 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
||||||
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
render::{
|
||||||
|
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
|
||||||
|
TexturePrimitive,
|
||||||
|
},
|
||||||
util::Vec2,
|
util::Vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,15 +24,26 @@ pub struct Painter<'a> {
|
|||||||
|
|
||||||
impl<'a> Painter<'a> {
|
impl<'a> Painter<'a> {
|
||||||
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||||
|
let kind = self.rsc.ui_mut().primitives.kind::<P>();
|
||||||
|
self.write(kind, primitive, region);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the kind, for a caller writing many of one primitive.
|
||||||
|
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
||||||
let h = self.state.layers.write(
|
let h = self.state.layers.write(
|
||||||
self.layer,
|
self.layer,
|
||||||
PrimitiveInst {
|
PrimitiveInst {
|
||||||
|
kind,
|
||||||
id: self.id,
|
id: self.id,
|
||||||
primitive,
|
primitive,
|
||||||
region,
|
region,
|
||||||
mask_idx: self.mask,
|
mask_idx: self.mask,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
self.push_primitive(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
||||||
if self.mask != MaskIdx::NONE {
|
if self.mask != MaskIdx::NONE {
|
||||||
// TODO: I have no clue if this works at all :joy:
|
// TODO: I have no clue if this works at all :joy:
|
||||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||||
@@ -38,11 +52,13 @@ impl<'a> Painter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Writes a primitive to be rendered
|
/// Writes a primitive to be rendered
|
||||||
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
|
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
|
||||||
|
let primitive = primitive.into_primitive(self);
|
||||||
self.primitive_at(primitive, self.region)
|
self.primitive_at(primitive, self.region)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
|
||||||
|
let primitive = primitive.into_primitive(self);
|
||||||
self.primitive_at(primitive, region.within(&self.region));
|
self.primitive_at(primitive, region.within(&self.region));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,21 +91,6 @@ impl<'a> Painter<'a> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
|
|
||||||
self.textures.push(handle.clone());
|
|
||||||
self.primitive_at(handle.primitive(), region.within(&self.region));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn texture(&mut self, handle: &TextureHandle) {
|
|
||||||
self.textures.push(handle.clone());
|
|
||||||
self.primitive(handle.primitive());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
|
||||||
self.textures.push(handle.clone());
|
|
||||||
self.primitive_at(handle.primitive(), region);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_text(
|
pub fn render_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &mut TextBuffer,
|
buffer: &mut TextBuffer,
|
||||||
@@ -97,10 +98,12 @@ impl<'a> Painter<'a> {
|
|||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
let ui = self.rsc.ui_mut();
|
let ui = self.rsc.ui_mut();
|
||||||
ui.text.render(buffer, attrs, width, &mut ui.textures)
|
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>();
|
||||||
for glyph in text.glyphs.iter() {
|
for glyph in text.glyphs.iter() {
|
||||||
let mut region = origin;
|
let mut region = origin;
|
||||||
region.x.end = region.x.start;
|
region.x.end = region.x.start;
|
||||||
@@ -108,12 +111,12 @@ impl<'a> Painter<'a> {
|
|||||||
let mut region = region.offset(UiVec2::abs(glyph.offset));
|
let mut region = region.offset(UiVec2::abs(glyph.offset));
|
||||||
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
|
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
|
||||||
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
|
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
|
||||||
self.primitive_at(
|
self.write(
|
||||||
|
kind,
|
||||||
GlyphPrimitive {
|
GlyphPrimitive {
|
||||||
uv_min: glyph.entry.uv_min,
|
uv_min: glyph.entry.uv_min,
|
||||||
uv_max: glyph.entry.uv_max,
|
uv_max: glyph.entry.uv_max,
|
||||||
view_idx: glyph.entry.view_idx,
|
layer: glyph.entry.layer,
|
||||||
sampler_idx: glyph.entry.sampler_idx,
|
|
||||||
color: text.color,
|
color: text.color,
|
||||||
flags: glyph.entry.flags(),
|
flags: glyph.entry.flags(),
|
||||||
},
|
},
|
||||||
@@ -169,3 +172,28 @@ impl<'a> Painter<'a> {
|
|||||||
self.state.size_ctx(self.id, self.region.size(), self.rsc)
|
self.state.size_ctx(self.id, self.region.size(), self.rsc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What `Painter::primitive` takes: a primitive, or something that yields one
|
||||||
|
/// and does whatever else drawing it needs.
|
||||||
|
pub trait PrimitiveLike {
|
||||||
|
type Primitive: Primitive;
|
||||||
|
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Primitive> PrimitiveLike for P {
|
||||||
|
type Primitive = P;
|
||||||
|
fn into_primitive(self, _: &mut Painter) -> P {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimitiveLike for &TextureHandle {
|
||||||
|
type Primitive = TexturePrimitive;
|
||||||
|
|
||||||
|
/// Retains a share of the handle, so the slot the primitive names cannot
|
||||||
|
/// be freed and reused while it is still drawn.
|
||||||
|
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
|
||||||
|
painter.textures.push(self.clone());
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-11
@@ -1,13 +1,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx,
|
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
|
||||||
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||||
ui::cache::Cache,
|
ui::cache::Cache,
|
||||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct UiRenderState {
|
pub struct UiRenderState {
|
||||||
pub active: HashMap<WidgetId, ActiveData>,
|
pub active: HashMap<WidgetId, ActiveData>,
|
||||||
pub layers: PrimitiveLayers,
|
pub layers: DrawLayers,
|
||||||
pub(super) output_size: Vec2,
|
pub(super) output_size: Vec2,
|
||||||
pub cache: Cache,
|
pub cache: Cache,
|
||||||
|
|
||||||
@@ -243,14 +243,14 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_layers(&self) {
|
pub fn debug_layers(&self) {
|
||||||
for ((idx, depth), primitives) in self.layers.iter_depth() {
|
for ((idx, depth), draws) in self.layers.iter_depth() {
|
||||||
let indent = " ".repeat(depth * 2);
|
let indent = " ".repeat(depth * 2);
|
||||||
let len = primitives.instances().len();
|
let counts: Vec<String> = draws
|
||||||
print!("{indent}{idx}: {len} primitives");
|
.primitives()
|
||||||
if len >= 1 {
|
.iter()
|
||||||
print!(" ({})", primitives.instances()[0].binding);
|
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
|
||||||
}
|
.collect();
|
||||||
println!();
|
println!("{indent}{idx}: [{}]", counts.join(", "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +308,6 @@ impl UiRenderState {
|
|||||||
source,
|
source,
|
||||||
cache: &mut self.cache,
|
cache: &mut self.cache,
|
||||||
text: &mut ui.text,
|
text: &mut ui.text,
|
||||||
textures: &mut ui.textures,
|
|
||||||
widgets: &ui.widgets,
|
widgets: &ui.widgets,
|
||||||
outer,
|
outer,
|
||||||
output_size: self.output_size,
|
output_size: self.output_size,
|
||||||
|
|||||||
+3
-5
@@ -1,11 +1,10 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
|
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
|
||||||
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SizeCtx<'a> {
|
pub struct SizeCtx<'a> {
|
||||||
pub text: &'a mut TextData,
|
pub text: &'a mut TextData,
|
||||||
pub textures: &'a mut Textures,
|
|
||||||
pub(super) source: WidgetId,
|
pub(super) source: WidgetId,
|
||||||
pub(super) widgets: &'a Widgets,
|
pub(super) widgets: &'a Widgets,
|
||||||
pub(super) cache: &'a mut Cache,
|
pub(super) cache: &'a mut Cache,
|
||||||
@@ -33,7 +32,6 @@ impl SizeCtx<'_> {
|
|||||||
.get_dyn_dynamic(id)
|
.get_dyn_dynamic(id)
|
||||||
.desired_len::<A>(&mut SizeCtx {
|
.desired_len::<A>(&mut SizeCtx {
|
||||||
text: self.text,
|
text: self.text,
|
||||||
textures: self.textures,
|
|
||||||
source: self.source,
|
source: self.source,
|
||||||
widgets: self.widgets,
|
widgets: self.widgets,
|
||||||
cache: self.cache,
|
cache: self.cache,
|
||||||
@@ -82,7 +80,7 @@ impl SizeCtx<'_> {
|
|||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
self.text.render(buffer, attrs, width, self.textures)
|
self.text.render(buffer, attrs, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn label(&self, id: WidgetId) -> &String {
|
pub fn label(&self, id: WidgetId) -> &String {
|
||||||
|
|||||||
@@ -212,10 +212,10 @@ impl DefaultAppState for Client {
|
|||||||
render: &mut UiRenderState,
|
render: &mut UiRenderState,
|
||||||
) {
|
) {
|
||||||
let new = format!(
|
let new = format!(
|
||||||
"widgets: {}\nactive: {}\nviews: {}",
|
"widgets: {}\nactive: {}\ntextures: {}",
|
||||||
rsc.widgets().len(),
|
rsc.widgets().len(),
|
||||||
render.active_widgets(),
|
render.active_widgets(),
|
||||||
self.ui_state.renderer.ui.view_count(),
|
rsc.ui().textures.count(),
|
||||||
);
|
);
|
||||||
if new != *rsc.widgets()[self.info].content {
|
if new != *rsc.widgets()[self.info].content {
|
||||||
*rsc.widgets_mut()[self.info].content = new;
|
*rsc.widgets_mut()[self.info].content = new;
|
||||||
|
|||||||
+2
-11
@@ -1,4 +1,4 @@
|
|||||||
use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState};
|
use iris_core::{UiData, UiRenderNode, UiRenderState};
|
||||||
use pollster::FutureExt;
|
use pollster::FutureExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use wgpu::*;
|
use wgpu::*;
|
||||||
@@ -83,18 +83,9 @@ impl UiRenderer {
|
|||||||
.block_on()
|
.block_on()
|
||||||
.expect("Could not get adapter!");
|
.expect("Could not get adapter!");
|
||||||
|
|
||||||
let ui_limits = UiLimits::default();
|
|
||||||
|
|
||||||
let (device, queue) = adapter
|
let (device, queue) = adapter
|
||||||
.request_device(&DeviceDescriptor {
|
.request_device(&DeviceDescriptor {
|
||||||
required_features: Features::TEXTURE_BINDING_ARRAY
|
|
||||||
| Features::PARTIALLY_BOUND_BINDING_ARRAY
|
|
||||||
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
|
|
||||||
required_limits: Limits {
|
required_limits: Limits {
|
||||||
max_binding_array_elements_per_shader_stage: ui_limits
|
|
||||||
.max_binding_array_elements_per_shader_stage(),
|
|
||||||
max_binding_array_sampler_elements_per_shader_stage: ui_limits
|
|
||||||
.max_binding_array_sampler_elements_per_shader_stage(),
|
|
||||||
max_buffer_size: 1 << 30,
|
max_buffer_size: 1 << 30,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -126,7 +117,7 @@ impl UiRenderer {
|
|||||||
|
|
||||||
let encoder = Self::create_encoder(&device);
|
let encoder = Self::create_encoder(&device);
|
||||||
|
|
||||||
let ui = UiRenderNode::new(&device, &queue, &config, ui_limits);
|
let ui = UiRenderNode::new(&device, &config);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
surface,
|
surface,
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ pub struct Image {
|
|||||||
|
|
||||||
impl Widget for Image {
|
impl Widget for Image {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
painter.texture(&self.handle);
|
painter.primitive(&self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
//! What one frame of `UiRenderNode::draw` costs on the CPU, against the number
|
||||||
|
//! of layers it walks. Recording only: the pass is built and dropped without
|
||||||
|
//! being submitted, so this is the loop's cost and not the GPU's.
|
||||||
|
//!
|
||||||
|
//! cargo test --release --test draw_cost -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! **Read the instruction count, not the clock.** Wall time here swings by 2x
|
||||||
|
//! between runs of one binary on this machine -- more under `cargo test` than
|
||||||
|
//! run directly -- while instructions retired are stable to 0.1%:
|
||||||
|
//!
|
||||||
|
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
|
||||||
|
//!
|
||||||
|
//! Measured that way on 2026-09-13, drawing each primitive through its own
|
||||||
|
//! `PrimitiveRender` rather than a match in the renderer costs **6
|
||||||
|
//! instructions per list drawn**, which is 0.1% of a frame at both 256 and
|
||||||
|
//! 1024 layers. Recording one list into the pass costs wgpu ~5,400.
|
||||||
|
//!
|
||||||
|
//! The instance is leaked on purpose. Dropping the last one makes the Vulkan
|
||||||
|
//! loader unload Mesa's ICD, which faults when a thread that touched Vulkan
|
||||||
|
//! exits -- and libtest runs every test on a spawned thread.
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris_core::{
|
||||||
|
GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData,
|
||||||
|
UiRegion, UiRenderNode, UiRenderState,
|
||||||
|
};
|
||||||
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
|
const SIZE: u32 = 1024;
|
||||||
|
const FRAMES: u32 = 200;
|
||||||
|
/// Reported as the best of this many batches. The mean moves by 15% between
|
||||||
|
/// runs on this machine, which is more than the thing being measured.
|
||||||
|
const BATCHES: u32 = 8;
|
||||||
|
|
||||||
|
fn gpu() -> Option<(Device, Queue)> {
|
||||||
|
// Probed rather than assumed: this machine's Vulkan device comes and goes,
|
||||||
|
// and GL is what is left when it is gone.
|
||||||
|
let all = Instance::new(&InstanceDescriptor::default());
|
||||||
|
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||||
|
{
|
||||||
|
Ok(_) => all,
|
||||||
|
Err(_) => Instance::new(&InstanceDescriptor {
|
||||||
|
backends: Backends::GL,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
// Leaked rather than dropped: see the note at the top of the file.
|
||||||
|
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||||
|
let adapter =
|
||||||
|
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
||||||
|
println!("adapter: {:?}", adapter.get_info());
|
||||||
|
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
||||||
|
SurfaceConfiguration {
|
||||||
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format,
|
||||||
|
width: SIZE,
|
||||||
|
height: SIZE,
|
||||||
|
present_mode: PresentMode::Fifo,
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
alpha_mode: CompositeAlphaMode::Auto,
|
||||||
|
view_formats: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every layer draws all three primitives, so the renderer takes a different
|
||||||
|
/// path for each list it walks -- which is the case a single-primitive layer
|
||||||
|
/// would never exercise. Images are bound per instance, so there are few.
|
||||||
|
fn fill(
|
||||||
|
ui: &mut UiData,
|
||||||
|
render: &mut UiRenderState,
|
||||||
|
layers: usize,
|
||||||
|
per_layer: usize,
|
||||||
|
) -> Vec<TextureHandle> {
|
||||||
|
let rect = ui.primitives.kind::<RectPrimitive>();
|
||||||
|
let glyph = ui.primitives.kind::<GlyphPrimitive>();
|
||||||
|
let texture = ui.primitives.kind::<TexturePrimitive>();
|
||||||
|
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||||
|
let handles: Vec<_> = (0..4)
|
||||||
|
.map(|_| ui.textures.add(image::RgbaImage::new(4, 4)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut layer = 0;
|
||||||
|
for _ in 0..layers {
|
||||||
|
for _ in 0..per_layer {
|
||||||
|
render.layers.write(
|
||||||
|
layer,
|
||||||
|
PrimitiveInst {
|
||||||
|
kind: rect,
|
||||||
|
id,
|
||||||
|
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||||
|
region: UiRegion::FULL,
|
||||||
|
mask_idx: MaskIdx::NONE,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
render.layers.write(
|
||||||
|
layer,
|
||||||
|
PrimitiveInst {
|
||||||
|
kind: glyph,
|
||||||
|
id,
|
||||||
|
primitive: GlyphPrimitive {
|
||||||
|
uv_min: vec2(0.0, 0.0),
|
||||||
|
uv_max: vec2(1.0, 1.0),
|
||||||
|
layer: 0,
|
||||||
|
color: UiColor::WHITE,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
region: UiRegion::FULL,
|
||||||
|
mask_idx: MaskIdx::NONE,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for h in &handles[..2] {
|
||||||
|
render.layers.write(
|
||||||
|
layer,
|
||||||
|
PrimitiveInst {
|
||||||
|
kind: texture,
|
||||||
|
id,
|
||||||
|
primitive: TexturePrimitive::from(h),
|
||||||
|
region: UiRegion::FULL,
|
||||||
|
mask_idx: MaskIdx::NONE,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
layer = render.layers.next(layer);
|
||||||
|
}
|
||||||
|
handles
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
||||||
|
let format = TextureFormat::Bgra8Unorm;
|
||||||
|
let mut node = UiRenderNode::new(device, &config(format));
|
||||||
|
let mut ui = UiData::default();
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
||||||
|
node.update(device, queue, &mut ui, &mut render);
|
||||||
|
|
||||||
|
let target = device.create_texture(&TextureDescriptor {
|
||||||
|
label: Some("draw cost"),
|
||||||
|
size: Extent3d {
|
||||||
|
width: SIZE,
|
||||||
|
height: SIZE,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: TextureDimension::D2,
|
||||||
|
format,
|
||||||
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let view = target.create_view(&TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
let record = |frames: u32| {
|
||||||
|
let start = Instant::now();
|
||||||
|
for _ in 0..frames {
|
||||||
|
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||||
|
{
|
||||||
|
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||||
|
color_attachments: &[Some(RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: Operations {
|
||||||
|
load: LoadOp::Clear(GpuColor::BLACK),
|
||||||
|
store: StoreOp::Store,
|
||||||
|
},
|
||||||
|
depth_slice: None,
|
||||||
|
})],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
node.draw(pass);
|
||||||
|
}
|
||||||
|
drop(encoder.finish());
|
||||||
|
}
|
||||||
|
start.elapsed().as_secs_f64() / frames as f64
|
||||||
|
};
|
||||||
|
record(FRAMES / 4);
|
||||||
|
(0..BATCHES)
|
||||||
|
.map(|_| record(FRAMES))
|
||||||
|
.fold(f64::MAX, f64::min)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn draw_cost_by_layer_count() {
|
||||||
|
let Some((device, queue)) = gpu() else {
|
||||||
|
panic!("no wgpu device; see the this-machine-graphics notes");
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"layers, each 8 rects + 8 glyphs + 2 images: us/frame (us per layer), best of {BATCHES}"
|
||||||
|
);
|
||||||
|
let base = frame_cost(&device, &queue, 1, 8) * 1e6;
|
||||||
|
for layers in [8, 64, 256, 1024] {
|
||||||
|
let per_frame = frame_cost(&device, &queue, layers, 8) * 1e6;
|
||||||
|
// Net of the empty pass, which is the same in any version of this.
|
||||||
|
println!(
|
||||||
|
"{layers:>5}: {per_frame:8.1} us ({:.3} us)",
|
||||||
|
(per_frame - base).max(0.0) / layers as f64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user