use crate::batch;
use bytemuck::{Pod, Zeroable};
use frus_core::{
BoxFit, Color, ColorFilter, ImageData, ImageFilter, LayerFilter, MaskShader, Path, Point,
Primitive, Rect, Scene,
};
use std::collections::HashMap;
use wgpu::util::DeviceExt;
use crate::filter::FilterPainter;
use crate::image::ImagePainter;
use crate::painter::Painter;
use crate::path::PathPainter;
use crate::text::TextPainter;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct QuadVertex {
pos: [f32; 2],
}
impl QuadVertex {
fn layout() -> wgpu::VertexBufferLayout<'static> {
const ATTRS: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x2];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<QuadVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &ATTRS,
}
}
}
const QUAD_VERTICES: &[QuadVertex] = &[
QuadVertex { pos: [0.0, 0.0] },
QuadVertex { pos: [1.0, 0.0] },
QuadVertex { pos: [1.0, 1.0] },
QuadVertex { pos: [0.0, 0.0] },
QuadVertex { pos: [1.0, 1.0] },
QuadVertex { pos: [0.0, 1.0] },
];
const QUAD_VERTEX_COUNT: u32 = 6;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct CompInstance {
clip: [f32; 4],
inv_lin: [f32; 4],
inv_tr_opacity: [f32; 4],
shape: [f32; 4],
radii: [f32; 4],
region: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct Viewport {
size: [f32; 2],
_pad: [f32; 2],
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Default)]
struct FilterParams {
flags: [f32; 4],
rows: [[f32; 4]; 4],
constants: [f32; 4],
mode_color: [f32; 4],
mask_geom: [f32; 4],
mask_c0: [f32; 4],
mask_c1: [f32; 4],
blend: [f32; 4],
}
impl FilterParams {
const STRIDE: wgpu::BufferAddress = 256;
fn new(filter: &LayerFilter) -> FilterParams {
let mut p = FilterParams::default();
match filter.color {
None => {}
Some(ColorFilter::Matrix(m)) => {
p.flags[0] = 1.0;
for row in 0..4 {
p.rows[row] = [m[row * 5], m[row * 5 + 1], m[row * 5 + 2], m[row * 5 + 3]];
p.constants[row] = m[row * 5 + 4];
}
}
Some(ColorFilter::Mode(color, mode)) => {
p.flags[0] = 2.0;
p.flags[2] = mode.code() as f32;
p.mode_color = color.to_array();
}
}
if let Some(mask) = filter.mask {
p.flags[1] = mask.shader.code() as f32 + 1.0;
p.flags[3] = mask.blend.code() as f32;
let (geom, c0, c1) = match mask.shader {
MaskShader::Linear {
from,
to,
from_color,
to_color,
} => ([from.x, from.y, to.x, to.y], from_color, to_color),
MaskShader::Radial {
center,
radius,
from_color,
to_color,
} => ([center.x, center.y, radius, 0.0], from_color, to_color),
};
p.mask_geom = geom;
p.mask_c0 = c0.to_array();
p.mask_c1 = c1.to_array();
}
if let Some(mode) = filter.blend {
p.blend[0] = mode.code() as f32 + 1.0;
}
p
}
}
pub(crate) const MSAA_SAMPLES: u32 = 4;
pub(crate) fn preferred_sample_count(adapter: &wgpu::Adapter, format: wgpu::TextureFormat) -> u32 {
let flags = adapter.get_texture_format_features(format).flags;
if flags.sample_count_supported(MSAA_SAMPLES) {
MSAA_SAMPLES
} else {
1
}
}
struct LayerComposite {
view: wgpu::TextureView,
mask: wgpu::TextureView,
opacity: f32,
clip: [f32; 4],
shape: [f32; 4],
radii: [f32; 4],
inverse: [f32; 6],
filter: LayerFilter,
region: Option<[f32; 4]>,
}
fn layer_region(
clip: Rect,
clip_shape: &frus_core::ClipShape,
transform: &Option<frus_core::LayerTransform>,
filter: &LayerFilter,
primitives: &[Primitive],
w: u32,
h: u32,
) -> Option<Rect> {
if transform.is_some()
|| filter.image.is_some_and(|f| !f.is_identity())
|| filter.backdrop.is_some()
|| matches!(clip_shape, frus_core::ClipShape::Path(_))
|| !primitives.iter().all(boundable)
{
return None;
}
let x0 = clip.x.max(0.0).floor();
let y0 = clip.y.max(0.0).floor();
let x1 = (clip.x + clip.width).min(w as f32).ceil();
let y1 = (clip.y + clip.height).min(h as f32).ceil();
if x1 - x0 < 1.0 || y1 - y0 < 1.0 {
return Some(Rect::new(
x0.min(w as f32 - 1.0).max(0.0),
y0.min(h as f32 - 1.0).max(0.0),
1.0,
1.0,
));
}
if x0 <= 0.0 && y0 <= 0.0 && x1 >= w as f32 && y1 >= h as f32 {
return None;
}
Some(Rect::new(x0, y0, x1 - x0, y1 - y0))
}
fn boundable(primitive: &Primitive) -> bool {
match primitive {
Primitive::Layer {
primitives,
clip_shape,
transform,
filter,
..
} => {
transform.is_none()
&& filter.image.is_none_or(|f| f.is_identity())
&& filter.backdrop.is_none()
&& !matches!(clip_shape, frus_core::ClipShape::Path(_))
&& primitives.iter().all(boundable)
}
_ => true,
}
}
fn into_region(
primitives: &[Primitive],
region: Option<Rect>,
) -> std::borrow::Cow<'_, [Primitive]> {
match region {
Some(r) => std::borrow::Cow::Owned(
primitives
.iter()
.map(|p| p.translated(-r.x, -r.y))
.collect(),
),
None => std::borrow::Cow::Borrowed(primitives),
}
}
struct Cut {
at: usize,
scene: usize,
kind: CutKind,
}
enum CutKind {
Backdrop {
filter: ImageFilter,
replace: bool,
key: Option<u64>,
},
Blend,
}
fn copy_of(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
source: &wgpu::Texture,
w: u32,
h: u32,
) -> wgpu::Texture {
let size = wgpu::Extent3d {
width: w.max(1),
height: h.max(1),
depth_or_array_layers: 1,
};
let copy = device.create_texture(&wgpu::TextureDescriptor {
label: Some("frus.under"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frus.encoder.under"),
});
encoder.copy_texture_to_texture(source.as_image_copy(), copy.as_image_copy(), size);
queue.submit(std::iter::once(encoder.finish()));
copy
}
struct ContentPlan<'a> {
batches: &'a [batch::Batch],
rect_ranges: &'a [std::ops::Range<u32>],
image_ranges: &'a [std::ops::Range<u32>],
path_ranges: &'a [std::ops::Range<u32>],
decoration_ranges: &'a [std::ops::Range<u32>],
decoration_base: u32,
layer_draws: &'a HashMap<usize, std::ops::Range<usize>>,
text_slots: &'a [usize],
}
struct CachedLayer {
primitives: Vec<Primitive>,
width: u32,
height: u32,
texture: wgpu::Texture,
image: Option<ImageFilter>,
}
struct MsaaScratch {
width: u32,
height: u32,
format: wgpu::TextureFormat,
texture: wgpu::Texture,
}
struct CompositePainter {
pipeline: wgpu::RenderPipeline,
pipeline_src: wgpu::RenderPipeline,
viewport_buffer: wgpu::Buffer,
viewport_bind_group: wgpu::BindGroup,
texture_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
quad_vertex_buffer: wgpu::Buffer,
instance_buffer: wgpu::Buffer,
instance_capacity: usize,
bind_groups: Vec<wgpu::BindGroup>,
white_mask: wgpu::Texture,
filter_buffer: wgpu::Buffer,
filter_capacity: usize,
}
impl CompositePainter {
fn white_mask_view(&self) -> wgpu::TextureView {
self.white_mask
.create_view(&wgpu::TextureViewDescriptor::default())
}
}
impl CompositePainter {
fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
sample_count: u32,
cache: Option<&wgpu::PipelineCache>,
) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("frus.composite.shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/composite.wgsl").into()),
});
let viewport_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frus.composite.viewport"),
size: std::mem::size_of::<Viewport>() as wgpu::BufferAddress,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let viewport_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("frus.composite.viewport.bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let viewport_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frus.composite.viewport.bind_group"),
layout: &viewport_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: viewport_buffer.as_entire_binding(),
}],
});
let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("frus.composite.texture.bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
],
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("frus.composite.sampler"),
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("frus.composite.pipeline_layout"),
bind_group_layouts: &[Some(&viewport_layout), Some(&texture_layout)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("frus.composite.pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[Some(QuadVertex::layout()), Some(comp_instance_layout())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache,
});
let pipeline_src = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("frus.composite.pipeline.src"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[Some(QuadVertex::layout()), Some(comp_instance_layout())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache,
});
let quad_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("frus.composite.quad_vertex_buffer"),
contents: bytemuck::cast_slice(QUAD_VERTICES),
usage: wgpu::BufferUsages::VERTEX,
});
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frus.composite.instance_buffer"),
size: (8 * std::mem::size_of::<CompInstance>()) as wgpu::BufferAddress,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let filter_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frus.composite.filter_buffer"),
size: FilterParams::STRIDE * 8,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let white = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some("frus.composite.white_mask"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
&[255, 255, 255, 255],
);
Self {
pipeline,
pipeline_src,
viewport_buffer,
viewport_bind_group,
texture_layout,
sampler,
quad_vertex_buffer,
instance_buffer,
instance_capacity: 8,
bind_groups: Vec::new(),
white_mask: white,
filter_buffer,
filter_capacity: 8,
}
}
fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
layers: &[LayerComposite],
w: f32,
h: f32,
) {
let viewport = Viewport {
size: [w.max(1.0), h.max(1.0)],
_pad: [0.0, 0.0],
};
queue.write_buffer(&self.viewport_buffer, 0, bytemuck::bytes_of(&viewport));
self.bind_groups.clear();
if layers.is_empty() {
return;
}
let instances: Vec<CompInstance> = layers
.iter()
.map(|l| {
let i = l.inverse;
CompInstance {
clip: l.clip,
inv_lin: [i[0], i[1], i[2], i[3]],
inv_tr_opacity: [i[4], i[5], l.opacity, 0.0],
shape: l.shape,
radii: l.radii,
region: l.region.unwrap_or([0.0, 0.0, w.max(1.0), h.max(1.0)]),
}
})
.collect();
if instances.len() > self.instance_capacity {
let cap = instances.len().next_power_of_two();
self.instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frus.composite.instance_buffer"),
size: (cap * std::mem::size_of::<CompInstance>()) as wgpu::BufferAddress,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.instance_capacity = cap;
}
queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
if layers.len() > self.filter_capacity {
let cap = layers.len().next_power_of_two();
self.filter_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frus.composite.filter_buffer"),
size: FilterParams::STRIDE * cap as wgpu::BufferAddress,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.filter_capacity = cap;
}
for (i, layer) in layers.iter().enumerate() {
let params = FilterParams::new(&layer.filter);
queue.write_buffer(
&self.filter_buffer,
i as wgpu::BufferAddress * FilterParams::STRIDE,
bytemuck::bytes_of(¶ms),
);
}
let white = self.white_mask_view();
for (i, layer) in layers.iter().enumerate() {
let group = self.bind_group(device, i, &layer.view, &layer.mask, &white);
self.bind_groups.push(group);
}
}
fn bind_group(
&self,
device: &wgpu::Device,
index: usize,
view: &wgpu::TextureView,
mask: &wgpu::TextureView,
under: &wgpu::TextureView,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frus.composite.texture.bind_group"),
layout: &self.texture_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(mask),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &self.filter_buffer,
offset: index as wgpu::BufferAddress * FilterParams::STRIDE,
size: wgpu::BufferSize::new(std::mem::size_of::<FilterParams>() as u64),
}),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(under),
},
],
})
}
fn rebind(&mut self, device: &wgpu::Device, index: usize, view: &wgpu::TextureView) {
let white = self.white_mask_view();
self.bind_groups[index] = self.bind_group(device, index, view, &white, &white);
}
fn rebind_under(
&mut self,
device: &wgpu::Device,
index: usize,
layer: &LayerComposite,
under: &wgpu::TextureView,
) {
self.bind_groups[index] = self.bind_group(device, index, &layer.view, &layer.mask, under);
}
fn draw_range<'pass>(
&'pass self,
pass: &mut wgpu::RenderPass<'pass>,
range: std::ops::Range<usize>,
replace_at: Option<usize>,
) {
if range.is_empty() || self.bind_groups.is_empty() {
return;
}
pass.set_bind_group(0, &self.viewport_bind_group, &[]);
pass.set_vertex_buffer(0, self.quad_vertex_buffer.slice(..));
pass.set_vertex_buffer(1, self.instance_buffer.slice(..));
let mut bound: Option<bool> = None;
for i in range {
let replace = replace_at == Some(i);
if bound != Some(replace) {
pass.set_pipeline(if replace {
&self.pipeline_src
} else {
&self.pipeline
});
bound = Some(replace);
}
pass.set_bind_group(1, &self.bind_groups[i], &[]);
let inst = i as u32;
pass.draw(0..QUAD_VERTEX_COUNT, inst..inst + 1);
}
}
}
fn comp_instance_layout() -> wgpu::VertexBufferLayout<'static> {
const ATTRS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
1 => Float32x4,
2 => Float32x4,
3 => Float32x4,
4 => Float32x4,
5 => Float32x4,
6 => Float32x4,
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<CompInstance>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &ATTRS,
}
}
pub(crate) struct Painters {
rect: Painter,
image: ImagePainter,
path: PathPainter,
text: TextPainter,
composite: CompositePainter,
filter: FilterPainter,
sample_count: u32,
msaa: Vec<MsaaScratch>,
stage: Option<MsaaScratch>,
layer_cache: Vec<CachedLayer>,
#[allow(dead_code)]
layer_renders: u64,
}
impl Painters {
pub(crate) fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
sample_count: u32,
cache: Option<&wgpu::PipelineCache>,
) -> Self {
Self {
rect: Painter::new(device, format, sample_count, cache),
image: ImagePainter::new(device, format, sample_count, cache),
path: PathPainter::new(device, format, sample_count, cache),
text: TextPainter::new(device, queue, format, sample_count),
composite: CompositePainter::new(device, queue, format, sample_count, cache),
filter: FilterPainter::new(device, format, cache),
sample_count,
msaa: Vec::new(),
stage: None,
layer_cache: Vec::new(),
layer_renders: 0,
}
}
#[cfg(test)]
pub(crate) fn layer_render_count(&self) -> u64 {
self.layer_renders
}
fn ensure_msaa(
&mut self,
device: &wgpu::Device,
format: wgpu::TextureFormat,
w: u32,
h: u32,
) -> Option<wgpu::TextureView> {
if self.sample_count == 1 {
return None;
}
let at = self
.msaa
.iter()
.position(|s| s.width == w && s.height == h && s.format == format);
let at = match at {
Some(at) => at,
None => {
if self.msaa.len() >= 16 {
self.msaa.clear();
}
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("frus.msaa.scratch"),
size: wgpu::Extent3d {
width: w.max(1),
height: h.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.sample_count,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
self.msaa.push(MsaaScratch {
width: w,
height: h,
format,
texture,
});
self.msaa.len() - 1
}
};
Some(
self.msaa[at]
.texture
.create_view(&wgpu::TextureViewDescriptor::default()),
)
}
fn set_viewport(&self, queue: &wgpu::Queue, w: f32, h: f32) {
self.rect.set_viewport(queue, w, h);
self.image.set_viewport(queue, w, h);
self.path.set_viewport(queue, w, h);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn render(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
target: &wgpu::TextureView,
w: u32,
h: u32,
scene: &Scene,
clear: Option<wgpu::Color>,
) {
self.set_viewport(queue, w as f32, h as f32);
let mut draws: Vec<LayerComposite> = Vec::new();
let mut cuts: Vec<Cut> = Vec::new();
let mut layer_draws: HashMap<usize, std::ops::Range<usize>> = HashMap::new();
let mut layer_index = 0usize;
for (scene_index, primitive) in scene.primitives().iter().enumerate() {
if let Primitive::Layer {
primitives,
opacity,
clip,
clip_shape,
transform,
filter,
..
} = primitive
{
let region = layer_region(*clip, clip_shape, transform, filter, primitives, w, h);
let (rw, rh) = region.map_or((w, h), |r| (r.width as u32, r.height as u32));
let local = into_region(primitives, region);
let view = self.layer_texture(
device,
queue,
format,
layer_index,
&local,
filter.image.filter(|f| !f.is_identity()),
rw,
rh,
);
let mut entry = self.layer_entry(
device, queue, format, view, *opacity, *clip, clip_shape, transform, filter, w,
h,
);
entry.region = region.map(|r| r.to_array());
let first_draw = draws.len();
if let Some(backdrop) = filter.backdrop {
cuts.push(Cut {
at: draws.len(),
scene: scene_index,
kind: CutKind::Backdrop {
filter: backdrop.filter,
replace: backdrop.blend == frus_core::BlendMode::Src,
key: backdrop.key,
},
});
draws.push(LayerComposite {
view: self.composite.white_mask_view(),
mask: self.composite.white_mask_view(),
opacity: 1.0,
clip: entry.clip,
shape: entry.shape,
radii: entry.radii,
inverse: frus_core::Affine::IDENTITY.m,
filter: LayerFilter::NONE,
region: None,
});
} else if filter.blend.is_some() {
cuts.push(Cut {
at: draws.len(),
scene: scene_index,
kind: CutKind::Blend,
});
}
draws.push(entry);
layer_draws.insert(scene_index, first_draw..draws.len());
layer_index += 1;
}
}
self.layer_cache.truncate(layer_index);
self.set_viewport(queue, w as f32, h as f32);
self.composite
.prepare(device, queue, &draws, w as f32, h as f32);
let batches = batch::plan(scene);
let (decorations, decoration_ranges) = self
.text
.prepare_frame(device, queue, scene, w, h, &batches);
let (rect_ranges, decoration_base) =
self.rect
.prepare_frame(device, queue, scene, &decorations, &batches);
let image_ranges = self.image.prepare_frame(device, queue, scene, &batches);
let path_ranges = self.path.prepare_frame(device, queue, scene, &batches);
let mut text_slots = Vec::with_capacity(batches.len());
let mut slots = 0usize;
for batch in &batches {
text_slots.push(slots);
if batch.kind == batch::Kind::Text {
slots += 1;
}
}
let content = ContentPlan {
batches: &batches,
rect_ranges: &rect_ranges,
image_ranges: &image_ranges,
path_ranges: &path_ranges,
decoration_ranges: &decoration_ranges,
decoration_base: decoration_base.start,
layer_draws: &layer_draws,
text_slots: &text_slots,
};
let msaa_view = self.ensure_msaa(device, format, w, h);
let load = match clear {
Some(c) => wgpu::LoadOp::Clear(c),
None => wgpu::LoadOp::Load,
};
if cuts.is_empty() {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frus.encoder"),
});
self.pass(
&mut encoder,
msaa_view.as_ref(),
target,
load,
&content,
0..batches.len(),
None,
);
queue.submit(std::iter::once(encoder.finish()));
return;
}
let stage = self.stage(device, format, w, h).clone();
self.draw_segmented(
device,
queue,
format,
msaa_view.as_ref(),
&stage,
load,
&content,
&cuts,
&draws,
);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frus.encoder.blit"),
});
let stage_view = stage.create_view(&wgpu::TextureViewDescriptor::default());
self.filter
.blit(device, queue, &mut encoder, &stage_view, target);
queue.submit(std::iter::once(encoder.finish()));
}
#[allow(clippy::too_many_arguments)]
fn draw_segmented(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
msaa_view: Option<&wgpu::TextureView>,
target: &wgpu::Texture,
load: wgpu::LoadOp<wgpu::Color>,
content: &ContentPlan,
cuts: &[Cut],
draws: &[LayerComposite],
) {
let (w, h) = (target.width(), target.height());
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
let mut shared: HashMap<u64, wgpu::Texture> = HashMap::new();
let batches = content.batches;
let cut_at = |scene: usize| {
batches
.iter()
.position(|b| b.kind == batch::Kind::Layer && b.members.first() == Some(&scene))
.unwrap_or(batches.len())
};
let mut cursor = 0usize;
let mut first = true;
let mut replace_at: Option<usize> = None;
for cut in cuts {
{
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frus.encoder.segment"),
});
self.pass(
&mut encoder,
msaa_view,
&view,
if first { load } else { wgpu::LoadOp::Load },
content,
cursor..cut_at(cut.scene),
replace_at,
);
queue.submit(std::iter::once(encoder.finish()));
}
first = false;
match cut.kind {
CutKind::Backdrop {
filter,
replace,
key,
} => {
let reuse = key
.and_then(|k| shared.get(&k))
.map(|t| t.create_view(&wgpu::TextureViewDescriptor::default()));
let filtered = match reuse {
Some(view) => view,
None => {
let filtered = self
.filter
.apply(device, queue, format, target, filter, w, h);
let view =
filtered.create_view(&wgpu::TextureViewDescriptor::default());
if let Some(k) = key {
shared.insert(k, filtered);
}
view
}
};
self.composite.rebind(device, cut.at, &filtered);
replace_at = replace.then_some(cut.at);
}
CutKind::Blend => {
let under = copy_of(device, queue, format, target, w, h)
.create_view(&wgpu::TextureViewDescriptor::default());
self.composite
.rebind_under(device, cut.at, &draws[cut.at], &under);
replace_at = Some(cut.at);
}
}
cursor = cut_at(cut.scene);
}
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frus.encoder.tail"),
});
self.pass(
&mut encoder,
msaa_view,
&view,
if first { load } else { wgpu::LoadOp::Load },
content,
cursor..batches.len(),
replace_at,
);
queue.submit(std::iter::once(encoder.finish()));
}
#[allow(clippy::too_many_arguments)]
fn layer_entry(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
view: wgpu::TextureView,
opacity: f32,
clip: Rect,
clip_shape: &frus_core::ClipShape,
transform: &Option<frus_core::LayerTransform>,
filter: &LayerFilter,
w: u32,
h: u32,
) -> LayerComposite {
let inverse = match transform {
Some(t) => t.affine.inverse().m,
None => frus_core::Affine::IDENTITY.m,
};
let (shape, radii) = match clip_shape {
frus_core::ClipShape::Rect => ([0.0, 0.0, 0.0, 0.0], [0.0; 4]),
frus_core::ClipShape::RRect(br) => ([1.0, 0.0, 0.0, 0.0], br.to_array()),
frus_core::ClipShape::Oval => ([2.0, 0.0, 0.0, 0.0], [0.0; 4]),
frus_core::ClipShape::Path(_) => ([3.0, 0.0, 0.0, 0.0], [0.0; 4]),
};
let mask = match clip_shape {
frus_core::ClipShape::Path(path) => self.render_mask(device, queue, format, path, w, h),
_ => self.composite.white_mask_view(),
};
LayerComposite {
view,
mask,
opacity,
clip: clip.to_array(),
shape,
radii,
inverse,
filter: LayerFilter {
image: None,
backdrop: None,
blend: filter.blend.filter(|_| filter.backdrop.is_none()),
..*filter
},
region: None,
}
}
#[allow(clippy::too_many_arguments)]
fn pass(
&self,
encoder: &mut wgpu::CommandEncoder,
msaa_view: Option<&wgpu::TextureView>,
target: &wgpu::TextureView,
load: wgpu::LoadOp<wgpu::Color>,
content: &ContentPlan,
batches: std::ops::Range<usize>,
replace_at: Option<usize>,
) {
if batches.is_empty() && !matches!(load, wgpu::LoadOp::Clear(_)) {
return;
}
let (view, resolve_target) = match msaa_view {
Some(msaa) => (msaa, Some(target)),
None => (target, None),
};
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("frus.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target,
ops: wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
for i in batches {
let batch = &content.batches[i];
match batch.kind {
batch::Kind::Rect => self.rect.draw(&mut pass, content.rect_ranges[i].clone()),
batch::Kind::Image => self.image.draw(&mut pass, content.image_ranges[i].clone()),
batch::Kind::Path => self.path.draw(&mut pass, content.path_ranges[i].clone()),
batch::Kind::Text => {
let base = content.decoration_base;
let d = &content.decoration_ranges[i];
self.rect.draw(&mut pass, base + d.start..base + d.end);
self.text.draw(&mut pass, content.text_slots[i]);
}
batch::Kind::Layer => {
if let Some(range) = batch
.members
.first()
.and_then(|scene| content.layer_draws.get(scene))
{
self.composite
.draw_range(&mut pass, range.clone(), replace_at);
}
}
}
}
}
fn stage(
&mut self,
device: &wgpu::Device,
format: wgpu::TextureFormat,
w: u32,
h: u32,
) -> &wgpu::Texture {
let stale = match &self.stage {
Some(s) => s.width != w || s.height != h || s.format != format,
None => true,
};
if stale {
self.stage = Some(MsaaScratch {
width: w,
height: h,
format,
texture: device.create_texture(&wgpu::TextureDescriptor {
label: Some("frus.stage"),
size: wgpu::Extent3d {
width: w.max(1),
height: h.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
}),
});
}
&self.stage.as_ref().expect("just created").texture
}
#[allow(clippy::too_many_arguments)]
fn layer_texture(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
index: usize,
primitives: &[Primitive],
image: Option<ImageFilter>,
w: u32,
h: u32,
) -> wgpu::TextureView {
let hit = matches!(
self.layer_cache.get(index),
Some(c) if c.width == w && c.height == h && c.image == image
&& c.primitives.as_slice() == primitives
);
if !hit {
let mut texture = self.render_group(device, queue, format, primitives, w, h);
if let Some(f) = image {
texture = self.filter.apply(device, queue, format, &texture, f, w, h);
}
let entry = CachedLayer {
primitives: primitives.to_vec(),
width: w,
height: h,
texture,
image,
};
if index < self.layer_cache.len() {
self.layer_cache[index] = entry;
} else {
self.layer_cache.push(entry);
}
}
self.layer_cache[index]
.texture
.create_view(&wgpu::TextureViewDescriptor::default())
}
fn render_mask(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
path: &frus_core::Path,
w: u32,
h: u32,
) -> wgpu::TextureView {
let prim = Primitive::Path {
path: path.clone(),
fill: Some(frus_core::Color::WHITE),
gradient: None,
stroke: None,
clip: frus_core::Rect::UNBOUNDED,
owner: 0,
};
let tex = self.render_group(device, queue, format, &[prim], w, h);
tex.create_view(&wgpu::TextureViewDescriptor::default())
}
fn render_group(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
primitives: &[Primitive],
w: u32,
h: u32,
) -> wgpu::Texture {
self.layer_renders += 1;
let mut nested: Vec<LayerComposite> = Vec::new();
let mut nested_of: HashMap<usize, usize> = HashMap::new();
let mut cuts: Vec<Cut> = Vec::new();
for (scene_index, primitive) in primitives.iter().enumerate() {
if let Primitive::Layer {
primitives: inner,
opacity,
clip,
clip_shape,
transform,
filter,
..
} = primitive
{
let region = layer_region(*clip, clip_shape, transform, filter, inner, w, h);
let (rw, rh) = region.map_or((w, h), |r| (r.width as u32, r.height as u32));
let local = into_region(inner, region);
let mut texture = self.render_group(device, queue, format, &local, rw, rh);
if let Some(f) = filter.image.filter(|f| !f.is_identity()) {
texture = self
.filter
.apply(device, queue, format, &texture, f, rw, rh);
}
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut entry = self.layer_entry(
device, queue, format, view, *opacity, *clip, clip_shape, transform, filter, w,
h,
);
entry.region = region.map(|r| r.to_array());
nested_of.insert(scene_index, nested.len());
if filter.blend.is_some() && filter.backdrop.is_none() {
cuts.push(Cut {
at: nested.len(),
scene: scene_index,
kind: CutKind::Blend,
});
}
nested.push(entry);
}
}
let mut sub = Scene::new();
for primitive in primitives {
sub.push_primitive(primitive.clone());
}
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("frus.layer.texture"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
self.set_viewport(queue, w as f32, h as f32);
let batches = batch::plan(&sub);
let (decorations, decoration_ranges) =
self.text.prepare_frame(device, queue, &sub, w, h, &batches);
let (rect_ranges, decoration_base) =
self.rect
.prepare_frame(device, queue, &sub, &decorations, &batches);
let image_ranges = self.image.prepare_frame(device, queue, &sub, &batches);
let path_ranges = self.path.prepare_frame(device, queue, &sub, &batches);
if !nested.is_empty() {
self.composite
.prepare(device, queue, &nested, w as f32, h as f32);
}
let msaa_view = self.ensure_msaa(device, format, w, h);
let mut text_slots = Vec::with_capacity(batches.len());
let mut slots = 0usize;
for batch in &batches {
text_slots.push(slots);
if batch.kind == batch::Kind::Text {
slots += 1;
}
}
let layer_draws: HashMap<usize, std::ops::Range<usize>> =
nested_of.iter().map(|(&s, &n)| (s, n..n + 1)).collect();
let content = ContentPlan {
batches: &batches,
rect_ranges: &rect_ranges,
image_ranges: &image_ranges,
path_ranges: &path_ranges,
decoration_ranges: &decoration_ranges,
decoration_base: decoration_base.start,
layer_draws: &layer_draws,
text_slots: &text_slots,
};
self.draw_segmented(
device,
queue,
format,
msaa_view.as_ref(),
&texture,
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
&content,
&cuts,
&nested,
);
texture
}
pub(crate) fn warm_up(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
) {
let mut scene = Scene::new();
scene.fill_rect(Rect::new(0.0, 0.0, 2.0, 2.0), Color::WHITE);
scene.fill_path(&Path::rect(Rect::new(0.0, 0.0, 2.0, 2.0)), Color::WHITE);
let img = ImageData::from_rgba(1, 1, vec![255, 255, 255, 255]).into_handle();
scene.image(&img, Rect::new(0.0, 0.0, 2.0, 2.0), BoxFit::Fill);
scene.text(
Point::new(0.0, 0.0),
"x",
&frus_core::ResolvedTextStyle::exact(8.0),
Color::WHITE,
);
scene.layer(0.5, |inner| {
inner.fill_rect(Rect::new(0.0, 0.0, 2.0, 2.0), Color::WHITE)
});
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("frus.warmup.texture"),
size: wgpu::Extent3d {
width: 4,
height: 4,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.render(
device,
queue,
format,
&view,
4,
4,
&scene,
Some(wgpu::Color::BLACK),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn headless() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: None,
force_fallback_adapter: false,
..Default::default()
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("frus.compositor.test.device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(),
memory_hints: wgpu::MemoryHints::default(),
..Default::default()
}))
.ok()
}
fn target(device: &wgpu::Device, format: wgpu::TextureFormat, n: u32) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("frus.compositor.test.target"),
size: wgpu::Extent3d {
width: n,
height: n,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
#[test]
fn static_layer_texture_is_reused_across_frames() {
let Some((device, queue)) = headless() else {
eprintln!("no GPU adapter available: test skipped");
return;
};
let format = wgpu::TextureFormat::Rgba8UnormSrgb;
let mut painters = Painters::new(&device, &queue, format, 1, None);
let tex = target(&device, format, 16);
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let clear = Some(wgpu::Color::BLACK);
let red =
|s: &mut Scene| s.fill_rect(Rect::new(0.0, 0.0, 8.0, 8.0), Color::rgb(1.0, 0.0, 0.0));
let mut scene = Scene::new();
scene.layer(0.5, red);
painters.render(&device, &queue, format, &view, 16, 16, &scene, clear);
assert_eq!(
painters.layer_render_count(),
1,
"first frame: layer rendered"
);
painters.render(&device, &queue, format, &view, 16, 16, &scene, clear);
assert_eq!(
painters.layer_render_count(),
1,
"layer unchanged: texture reused"
);
let mut scene2 = Scene::new();
scene2.layer(0.5, |s| {
s.fill_rect(Rect::new(0.0, 0.0, 8.0, 8.0), Color::rgb(0.0, 1.0, 0.0))
});
painters.render(&device, &queue, format, &view, 16, 16, &scene2, clear);
assert_eq!(
painters.layer_render_count(),
2,
"content changed: re-render"
);
painters.render(&device, &queue, format, &view, 16, 16, &Scene::new(), clear);
assert_eq!(
painters.layer_render_count(),
2,
"no layer left: nothing to render"
);
assert!(painters.layer_cache.is_empty(), "cache purged");
}
fn layer(
clip: Rect,
primitives: Vec<Primitive>,
) -> (
Rect,
frus_core::ClipShape,
Option<frus_core::LayerTransform>,
LayerFilter,
Vec<Primitive>,
) {
(
clip,
frus_core::ClipShape::Rect,
None,
LayerFilter::NONE,
primitives,
)
}
fn square() -> Primitive {
let mut scene = Scene::new();
scene.fill_rect(Rect::new(10.0, 10.0, 20.0, 20.0), frus_core::Color::WHITE);
scene.primitives()[0].clone()
}
#[test]
fn a_layer_is_drawn_in_its_clip_and_no_more() {
let (clip, shape, transform, filter, prims) =
layer(Rect::new(10.5, 20.2, 56.0, 32.0), vec![square()]);
let region = layer_region(clip, &shape, &transform, &filter, &prims, 1080, 2340);
assert_eq!(region, Some(Rect::new(10.0, 20.0, 57.0, 33.0)));
let region = layer_region(
Rect::new(-30.0, 2300.0, 100.0, 100.0),
&shape,
&transform,
&filter,
&prims,
1080,
2340,
);
assert_eq!(region, Some(Rect::new(0.0, 2300.0, 70.0, 40.0)));
let region = layer_region(
Rect::new(2000.0, 10.0, 50.0, 50.0),
&shape,
&transform,
&filter,
&prims,
1080,
2340,
);
assert_eq!(region.map(|r| (r.width, r.height)), Some((1.0, 1.0)));
assert_eq!(
layer_region(
Rect::UNBOUNDED,
&shape,
&transform,
&filter,
&prims,
1080,
2340
),
None
);
}
#[test]
fn what_may_draw_anywhere_keeps_the_whole_target() {
let clip = Rect::new(10.0, 10.0, 100.0, 100.0);
let none = LayerFilter::NONE;
let moved = Some(frus_core::LayerTransform::new(
frus_core::Affine::translation(5.0, 0.0),
));
assert_eq!(
layer_region(
clip,
&frus_core::ClipShape::Rect,
&moved,
&none,
&[square()],
1080,
2340
),
None
);
let blurred = LayerFilter {
image: Some(ImageFilter::Blur {
sigma_x: 4.0,
sigma_y: 4.0,
}),
..LayerFilter::NONE
};
assert_eq!(
layer_region(
clip,
&frus_core::ClipShape::Rect,
&None,
&blurred,
&[square()],
1080,
2340
),
None
);
let path = frus_core::ClipShape::Path(frus_core::Path::oval(clip));
assert_eq!(
layer_region(clip, &path, &None, &none, &[square()], 1080, 2340),
None
);
let mut scene = Scene::new();
scene.push_primitive(Primitive::Layer {
primitives: vec![square()],
opacity: 0.5,
clip,
clip_shape: frus_core::ClipShape::Path(frus_core::Path::oval(clip)),
transform: None,
filter: LayerFilter::NONE,
owner: 0,
});
let inner = scene.primitives().to_vec();
assert_eq!(
layer_region(
clip,
&frus_core::ClipShape::Rect,
&None,
&none,
&inner,
1080,
2340
),
None
);
let wrapped = vec![Primitive::Layer {
primitives: inner.clone(),
opacity: 1.0,
clip,
clip_shape: frus_core::ClipShape::Rect,
transform: None,
filter: LayerFilter::NONE,
owner: 0,
}];
assert_eq!(
layer_region(
clip,
&frus_core::ClipShape::Rect,
&None,
&none,
&wrapped,
1080,
2340
),
None
);
assert!(boundable(&Primitive::Layer {
primitives: vec![square()],
opacity: 0.5,
clip,
clip_shape: frus_core::ClipShape::RRect(frus_core::BorderRadius::uniform(4.0)),
transform: None,
filter: LayerFilter::NONE,
owner: 0,
}));
}
}