use crate::effect_renderer::{
projective_dest_bounds_rect, CompositeSampleMode, EffectRenderer, EffectScratchTargetProvider,
ProjectiveSurfaceComposite, RoundedCompositeMask,
};
use crate::frame_graph::{
FrameCommandRecorder, FrameTextureDescriptor, WgpuFrameGraph, WgpuFrameGraphExecutor,
};
use crate::layer_surface_cache::LayerSurfaceCache;
#[cfg(test)]
use crate::normalized_scene::{
build_scene_window, collect_layer_contents, collect_layer_contents_with_translation_context,
filtered_effect_layer_index, scene_bounds, visible_draw_rect, SceneWindowSource,
};
use crate::normalized_scene::{
collect_layer_contents_with_translation_context_and_text_layout, effect_layer_in_range,
estimate_layer_surface_rect_cached, scene_has_layer_events, translate_quad, CollectedLayer,
};
#[cfg(test)]
use crate::normalized_scene::{estimate_layer_surface_rect, motion_stable_capture_bounds};
use crate::offscreen::OffscreenTarget;
use crate::rect_to_quad;
use crate::scene::{
BackdropLayer, CompositorScene, DrawOp, DrawOpKind, DrawShape, EffectLayer, ImageDraw,
ShadowDraw, SnapAnchor, TextDraw,
};
use crate::shaders;
use crate::surface_executor::{
apply_backdrop_layer_to_target as execute_apply_backdrop_layer_to_target,
axis_aligned_quad_rect, composite_surface_to_view as execute_composite_surface_to_view,
device_pixel_bounds_for_rect, offscreen_byte_size,
render_effect_layer_to_target as execute_render_effect_layer_to_target,
render_layer_surface as execute_render_layer_surface,
render_root_direct as execute_render_root_direct, scaled_quad, snap_delta_for_anchor,
snap_motion_stable_dest_quad, surface_target_size, DevicePixelBounds, LayerSurfaceTexture,
SurfaceExecutionBackend,
};
#[cfg(test)]
use crate::surface_executor::{clamp_effect_surface_scale, visible_layer_rect};
#[cfg(test)]
use crate::surface_plan::{
composite_sample_mode_for_effect_layer, composite_sample_mode_for_requirements,
direct_translation, effect_layer_target_scale, layer_contains_descendant_backdrop,
layer_surface_requirements, layer_surface_target_scale, TranslatedContentAxes,
};
use crate::surface_plan::{
layer_surface_requirements_cached, layer_uses_external_backdrop_input,
root_can_render_directly_cached, LayerSurfaceRequest, LayerSurfaceRequirements,
TranslationRenderContext,
};
#[cfg(test)]
use crate::surface_requirements::SurfaceRequirement;
use crate::surface_requirements::SurfaceRequirementSet;
use crate::DebugCpuAllocationStats;
use crate::TextSystemState;
use bytemuck::{Pod, Zeroable};
use cranpose_core::{hash::default as default_hash, NodeId};
use cranpose_render_common::bounded_lru_cache::BoundedLruCache;
use cranpose_render_common::geometry::blur_extent_margin;
use cranpose_render_common::graph::{quad_bounds, CachePolicy, LayerNode, RenderGraph};
#[cfg(test)]
use cranpose_render_common::graph::{
PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform, RenderNode,
};
use cranpose_render_common::raster_cache::{LayerRasterCacheKey, ScaleBucket};
use cranpose_render_common::software_text_raster::{
measure_text_with_font, rasterize_text_to_image, SoftwareTextFontSet,
};
#[cfg(test)]
use cranpose_ui_graphics::GraphicsLayer;
use cranpose_ui_graphics::{
BlendMode, Brush, Color, ColorFilter, ImageBitmap, ImageSampling, Point, Rect, RenderEffect,
RenderHash, RuntimeShader, TileMode,
};
use std::cell::Cell;
use std::collections::{hash_map::DefaultHasher, HashMap};
use std::hash::{Hash, Hasher};
use std::ops::Range;
use std::rc::Rc;
use std::sync::{mpsc, Arc};
use std::time::Duration;
use crate::gpu_stats;
use crate::gpu_stats::gpu_stats_enabled;
use crate::pipeline::push_layer_shadow;
const MAX_SHAPES_PER_BATCH: usize = 200;
const MAX_GRADIENT_STOPS: usize = 256;
const HARD_MAX_BUFFER_MB: usize = 64; const MAX_SHADOW_SURFACE_CACHE_ITEMS: usize = 512;
const MAX_SHADOW_SURFACE_CACHE_BYTES: u64 = 64 * 1024 * 1024;
const MAX_TEXT_IMAGE_CACHE_ITEMS: usize = 1024;
pub(crate) const CLEAR_COLOR: wgpu::Color = wgpu::Color {
r: 18.0 / 255.0,
g: 18.0 / 255.0,
b: 24.0 / 255.0,
a: 1.0,
};
#[cfg(not(target_arch = "wasm32"))]
const INITIAL_UPLOAD_BUFFER_BYTES: u64 = 4 * 1024;
const MAX_TEXTURE_CACHE_ITEMS: usize = 256;
const RETAINED_STAGED_UPLOAD_BYTES: usize = 256 * 1024;
const RETAINED_STAGED_UPLOAD_COPIES: usize = 128;
const RETAINED_LAYER_REQUIREMENTS_CAPACITY: usize = 512;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct ShadowSurfaceCacheKey {
content_hash: u64,
pixel_size: [u32; 2],
root_scale_bits: u32,
blur_radius_bits: u32,
}
struct CachedShadowSurface {
target: Rc<OffscreenTarget>,
byte_size: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct TextImageCacheKey(u64);
struct CachedTextImage {
image: ImageBitmap,
}
#[derive(Default)]
struct RendererWarningState {
unsupported_effect_reported: Cell<bool>,
}
impl RendererWarningState {
fn warn_unsupported_effect_once(&self) {
if !self.unsupported_effect_reported.replace(true) {
log::warn!(
"WGPU renderer received an unsupported RenderEffect variant; falling back to passthrough compositing"
);
}
}
}
fn is_blend_mode_supported(mode: BlendMode) -> bool {
matches!(mode, BlendMode::SrcOver | BlendMode::DstOut)
}
fn blend_state_for_mode(mode: BlendMode) -> wgpu::BlendState {
match mode {
BlendMode::DstOut => wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
},
_ => wgpu::BlendState::ALPHA_BLENDING,
}
}
fn supported_blend_mode(mode: BlendMode) -> BlendMode {
if is_blend_mode_supported(mode) {
return mode;
}
BlendMode::SrcOver
}
fn hash_f32_for_cache<H: Hasher>(value: f32, state: &mut H) {
value.to_bits().hash(state);
}
fn hash_rect_for_cache<H: Hasher>(rect: Rect, state: &mut H) {
hash_f32_for_cache(rect.x, state);
hash_f32_for_cache(rect.y, state);
hash_f32_for_cache(rect.width, state);
hash_f32_for_cache(rect.height, state);
}
fn translated_rect_for_cache(rect: Rect, origin_x: f32, origin_y: f32) -> Rect {
Rect {
x: rect.x - origin_x,
y: rect.y - origin_y,
width: rect.width,
height: rect.height,
}
}
fn hash_shape_shadow_item<H: Hasher>(
shape: &DrawShape,
blend_mode: BlendMode,
origin_x: f32,
origin_y: f32,
state: &mut H,
) {
hash_rect_for_cache(
translated_rect_for_cache(shape.rect, origin_x, origin_y),
state,
);
hash_rect_for_cache(
translated_rect_for_cache(shape.local_rect, origin_x, origin_y),
state,
);
for point in shape.quad {
hash_f32_for_cache(point[0] - origin_x, state);
hash_f32_for_cache(point[1] - origin_y, state);
}
match shape.snap_anchor {
Some(anchor) => {
1u8.hash(state);
hash_f32_for_cache(anchor.origin.x - origin_x, state);
hash_f32_for_cache(anchor.origin.y - origin_y, state);
hash_f32_for_cache(anchor.device_pixel_step, state);
}
None => 0u8.hash(state),
}
shape.brush.render_hash().hash(state);
match shape.shape {
Some(corner_shape) => {
1u8.hash(state);
corner_shape.radii().render_hash().hash(state);
}
None => 0u8.hash(state),
}
match shape.clip {
Some(clip) => {
1u8.hash(state);
hash_rect_for_cache(translated_rect_for_cache(clip, origin_x, origin_y), state);
}
None => 0u8.hash(state),
}
blend_mode.hash(state);
shape.blend_mode.hash(state);
}
fn shape_shadow_content_hash(
shapes: &[(DrawShape, BlendMode)],
viewport_offset: [f32; 2],
root_scale: f32,
) -> u64 {
let mut hasher = DefaultHasher::new();
let origin_x = viewport_offset[0] / root_scale;
let origin_y = viewport_offset[1] / root_scale;
shapes.len().hash(&mut hasher);
for (shape, blend_mode) in shapes {
hash_shape_shadow_item(shape, *blend_mode, origin_x, origin_y, &mut hasher);
}
hasher.finish()
}
fn is_render_effect_supported(effect: &RenderEffect) -> bool {
match effect {
RenderEffect::Blur { .. } => true,
RenderEffect::Offset { .. } => true,
RenderEffect::Shader { .. } => true,
RenderEffect::Chain { first, second } => {
is_render_effect_supported(first) && is_render_effect_supported(second)
}
}
}
fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
if value.is_finite() {
origin + value
} else if value.is_sign_positive() {
origin + extent
} else {
origin
}
}
fn gradient_tile_mode_value(tile_mode: TileMode) -> u32 {
match tile_mode {
TileMode::Clamp => 0,
TileMode::Repeated => 1,
TileMode::Mirror => 2,
TileMode::Decal => 3,
}
}
fn create_shape_pipeline(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
uniform_layout: &wgpu::BindGroupLayout,
shape_layout: &wgpu::BindGroupLayout,
blend_mode: BlendMode,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shape Shader"),
source: wgpu::ShaderSource::Wgsl(shaders::SHADER.into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[Vertex::desc()],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(blend_state_for_mode(blend_mode)),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}
fn create_image_pipeline(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
uniform_layout: &wgpu::BindGroupLayout,
image_layout: &wgpu::BindGroupLayout,
blend_mode: BlendMode,
) -> wgpu::RenderPipeline {
let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Image Shader"),
source: wgpu::ShaderSource::Wgsl(shaders::IMAGE_SHADER.into()),
});
let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Image Pipeline Layout"),
bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Image Pipeline"),
layout: Some(&image_pipeline_layout),
vertex: wgpu::VertexState {
module: &image_shader,
entry_point: Some("image_vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[Vertex::desc()],
},
fragment: Some(wgpu::FragmentState {
module: &image_shader,
entry_point: Some("image_fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(blend_state_for_mode(blend_mode)),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct Vertex {
position: [f32; 2],
color: [f32; 4],
uv: [f32; 2],
uv_bounds: [f32; 4],
}
impl Vertex {
const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
0 => Float32x2,
1 => Float32x4,
2 => Float32x2,
3 => Float32x4
];
fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRIBS,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct Uniforms {
viewport: [f32; 2],
viewport_offset: [f32; 2],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct ShapeData {
rect: [f32; 4], radii: [f32; 4], gradient_params: [f32; 4], clip_rect: [f32; 4], brush_type: u32, gradient_start: u32, gradient_count: u32, gradient_tile_mode: u32, }
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct GradientStop {
color: [f32; 4],
position: [f32; 4],
}
struct CachedImageTexture {
_texture: wgpu::Texture,
_view: wgpu::TextureView,
nearest_bind_group: wgpu::BindGroup,
linear_bind_group: wgpu::BindGroup,
}
impl CachedImageTexture {
fn bind_group(&self, sampling: ImageSampling) -> &wgpu::BindGroup {
match sampling {
ImageSampling::Nearest => &self.nearest_bind_group,
ImageSampling::Linear => &self.linear_bind_group,
}
}
}
struct ImageDrawCmd {
index_start: u32,
scissor: (u32, u32, u32, u32),
image_id: u64,
sampling: ImageSampling,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct ImageUvRect {
min: [f32; 2],
max: [f32; 2],
sample_bounds: [f32; 4],
}
#[derive(Clone, Copy)]
enum LayerEventKind {
Backdrop(usize),
Effect(usize),
}
#[derive(Clone, Copy)]
struct LayerEvent {
z_index: usize,
kind: LayerEventKind,
}
impl LayerEvent {
fn kind_order(self) -> u8 {
match self.kind {
LayerEventKind::Backdrop(_) => 0,
LayerEventKind::Effect(_) => 1,
}
}
}
struct ShapeBatchBuffers {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
shape_buffer: wgpu::Buffer,
gradient_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
vertex_capacity: usize,
index_capacity: usize,
shape_capacity: usize,
gradient_capacity: usize,
}
#[cfg(target_arch = "wasm32")]
struct UniformBatchBuffer {
buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
#[cfg(target_arch = "wasm32")]
struct ImageBatchBuffers {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
vertex_capacity: usize,
index_capacity: usize,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct ViewportUniformParams {
width: u32,
height: u32,
offset: [f32; 2],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
enum UploadTarget {
Uniform,
ShapeVertex,
ShapeIndex,
ShapeData,
ShapeGradient,
ImageVertex,
ImageIndex,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
struct PendingBufferCopy {
source_offset: u64,
size: u64,
target: UploadTarget,
}
#[derive(Default)]
struct StagedBufferUploads {
bytes: Vec<u8>,
copies: Vec<PendingBufferCopy>,
}
impl StagedBufferUploads {
fn clear(&mut self) {
self.bytes.clear();
self.copies.clear();
}
fn shrink_retained_capacity(&mut self, max_bytes: usize, max_copies: usize) -> bool {
let mut shrunk = false;
if self.bytes.len() <= max_bytes && self.bytes.capacity() > max_bytes {
self.bytes.shrink_to(max_bytes);
shrunk = true;
}
if self.copies.len() <= max_copies && self.copies.capacity() > max_copies {
self.copies.shrink_to(max_copies);
shrunk = true;
}
shrunk
}
fn is_empty(&self) -> bool {
self.copies.is_empty()
}
#[cfg(test)]
fn payload_for_copy(&self, copy: PendingBufferCopy) -> &[u8] {
let start = copy.source_offset as usize;
let end = start + copy.size as usize;
&self.bytes[start..end]
}
#[cfg(not(target_arch = "wasm32"))]
fn stage(&mut self, target: UploadTarget, bytes: &[u8]) {
if bytes.is_empty() {
return;
}
debug_assert_eq!(
bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize,
0,
"buffer uploads must be aligned to copy requirements"
);
let aligned_offset = align_usize_to(self.bytes.len(), wgpu::COPY_BUFFER_ALIGNMENT as usize);
if aligned_offset > self.bytes.len() {
self.bytes.resize(aligned_offset, 0);
}
let source_offset = self.bytes.len() as u64;
self.bytes.extend_from_slice(bytes);
self.copies.push(PendingBufferCopy {
source_offset,
size: bytes.len() as u64,
target,
});
}
}
impl ShapeBatchBuffers {
fn new(device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout) -> Self {
let initial_vertex_cap = MAX_SHAPES_PER_BATCH * 4; let initial_index_cap = MAX_SHAPES_PER_BATCH * 6; let initial_shape_cap = MAX_SHAPES_PER_BATCH;
let initial_gradient_cap = MAX_GRADIENT_STOPS;
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shape Vertex Buffer"),
size: (std::mem::size_of::<Vertex>() * initial_vertex_cap) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shape Index Buffer"),
size: (std::mem::size_of::<u32>() * initial_index_cap) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shape Data Buffer"),
size: (std::mem::size_of::<ShapeData>() * initial_shape_cap) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Gradient Buffer"),
size: (std::mem::size_of::<GradientStop>() * initial_gradient_cap) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Shape Bind Group"),
layout: bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: shape_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: gradient_buffer.as_entire_binding(),
},
],
});
Self {
vertex_buffer,
index_buffer,
shape_buffer,
gradient_buffer,
bind_group,
vertex_capacity: initial_vertex_cap,
index_capacity: initial_index_cap,
shape_capacity: initial_shape_cap,
gradient_capacity: initial_gradient_cap,
}
}
fn ensure_capacity(
&mut self,
device: &wgpu::Device,
bind_group_layout: &wgpu::BindGroupLayout,
vertices_needed: usize,
indices_needed: usize,
shapes_needed: usize,
gradients_needed: usize,
) {
let mut need_bind_group_update = false;
let hard_max_bytes = HARD_MAX_BUFFER_MB * 1024 * 1024;
if vertices_needed > self.vertex_capacity {
let desired = vertices_needed.next_power_of_two();
let max_count = hard_max_bytes / std::mem::size_of::<Vertex>();
let new_cap = desired.min(max_count);
self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shape Vertex Buffer"),
size: (std::mem::size_of::<Vertex>() * new_cap) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.vertex_capacity = new_cap;
}
if indices_needed > self.index_capacity {
let desired = indices_needed.next_power_of_two();
let max_count = hard_max_bytes / std::mem::size_of::<u32>();
let new_cap = desired.min(max_count);
self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shape Index Buffer"),
size: (std::mem::size_of::<u32>() * new_cap) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.index_capacity = new_cap;
}
if shapes_needed > self.shape_capacity && self.shape_capacity < MAX_SHAPES_PER_BATCH {
let new_cap = shapes_needed.next_power_of_two().min(MAX_SHAPES_PER_BATCH);
self.shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Shape Data Buffer"),
size: (std::mem::size_of::<ShapeData>() * new_cap) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.shape_capacity = new_cap;
need_bind_group_update = true;
}
if gradients_needed > self.gradient_capacity && self.gradient_capacity < MAX_GRADIENT_STOPS
{
let new_cap = gradients_needed
.max(1)
.next_power_of_two()
.min(MAX_GRADIENT_STOPS);
self.gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Gradient Buffer"),
size: (std::mem::size_of::<GradientStop>() * new_cap) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.gradient_capacity = new_cap;
need_bind_group_update = true;
}
if need_bind_group_update {
self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Shape Bind Group"),
layout: bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.shape_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: self.gradient_buffer.as_entire_binding(),
},
],
});
}
}
}
#[cfg(target_arch = "wasm32")]
impl UniformBatchBuffer {
fn new(device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout) -> Self {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Viewport Uniform Batch Buffer"),
size: std::mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Viewport Uniform Batch Bind Group"),
layout: bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
Self { buffer, bind_group }
}
}
#[cfg(target_arch = "wasm32")]
impl ImageBatchBuffers {
fn new(device: &wgpu::Device) -> Self {
let vertex_capacity = 4;
let index_capacity = 6;
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Vertex Batch Buffer"),
size: (std::mem::size_of::<Vertex>() * vertex_capacity) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Index Batch Buffer"),
size: (std::mem::size_of::<u32>() * index_capacity) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
vertex_buffer,
index_buffer,
vertex_capacity,
index_capacity,
}
}
fn ensure_capacity(
&mut self,
device: &wgpu::Device,
vertices_needed: usize,
indices_needed: usize,
) {
let hard_max_bytes = HARD_MAX_BUFFER_MB * 1024 * 1024;
if vertices_needed > self.vertex_capacity {
let desired = vertices_needed.next_power_of_two();
let max_count = hard_max_bytes / std::mem::size_of::<Vertex>();
let new_cap = desired.min(max_count);
self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Vertex Batch Buffer"),
size: (std::mem::size_of::<Vertex>() * new_cap) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.vertex_capacity = new_cap;
}
if indices_needed > self.index_capacity {
let desired = indices_needed.next_power_of_two();
let max_count = hard_max_bytes / std::mem::size_of::<u32>();
let new_cap = desired.min(max_count);
self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Index Batch Buffer"),
size: (std::mem::size_of::<u32>() * new_cap) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.index_capacity = new_cap;
}
}
}
pub struct GpuRenderer {
pub(crate) device: Arc<wgpu::Device>,
pub(crate) queue: Arc<wgpu::Queue>,
surface_format: wgpu::TextureFormat,
pipeline: wgpu::RenderPipeline,
pipeline_dst_out: wgpu::RenderPipeline,
#[cfg(target_arch = "wasm32")]
uniform_bind_group_layout: wgpu::BindGroupLayout,
shape_bind_group_layout: wgpu::BindGroupLayout,
image_pipeline: wgpu::RenderPipeline,
image_pipeline_dst_out: wgpu::RenderPipeline,
image_bind_group_layout: wgpu::BindGroupLayout,
image_nearest_sampler: wgpu::Sampler,
image_linear_sampler: wgpu::Sampler,
text_fonts: SoftwareTextFontSet,
#[cfg(not(target_arch = "wasm32"))]
upload_buffer: wgpu::Buffer,
#[cfg(not(target_arch = "wasm32"))]
uniform_buffer: wgpu::Buffer,
#[cfg(not(target_arch = "wasm32"))]
uniform_bind_group: wgpu::BindGroup,
#[cfg(not(target_arch = "wasm32"))]
shape_buffers: ShapeBatchBuffers,
#[cfg(not(target_arch = "wasm32"))]
image_vertex_buffer: wgpu::Buffer,
#[cfg(not(target_arch = "wasm32"))]
image_index_buffer: wgpu::Buffer,
#[cfg(target_arch = "wasm32")]
wasm_uniform_batches: Vec<UniformBatchBuffer>,
#[cfg(target_arch = "wasm32")]
wasm_uniform_batch_cursor: usize,
#[cfg(target_arch = "wasm32")]
wasm_shape_batches: Vec<ShapeBatchBuffers>,
#[cfg(target_arch = "wasm32")]
wasm_shape_batch_cursor: usize,
#[cfg(target_arch = "wasm32")]
wasm_image_batches: Vec<ImageBatchBuffers>,
#[cfg(target_arch = "wasm32")]
wasm_image_batch_cursor: usize,
image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
scratch_shape_data: Vec<ShapeData>,
scratch_gradients: Vec<GradientStop>,
scratch_vertices: Vec<Vertex>,
scratch_indices: Vec<u32>,
scratch_image_vertices: Vec<Vertex>,
scratch_image_indices: Vec<u32>,
scratch_image_cmds: Vec<ImageDrawCmd>,
scratch_text_images: Vec<ImageDraw>,
scratch_segment_items: Vec<(usize, SegmentDrawItem)>,
scratch_effect_ranges: Vec<Range<usize>>,
scratch_layer_events: Vec<LayerEvent>,
staged_uploads: StagedBufferUploads,
frame_graph_executor: WgpuFrameGraphExecutor,
deferred_offscreen_releases: Vec<OffscreenTarget>,
effect_renderer: EffectRenderer,
layer_surface_cache: LayerSurfaceCache,
shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
shadow_surface_cache_bytes: u64,
layer_surface_rect_cache: HashMap<usize, Rect>,
layer_surface_requirements_cache: HashMap<usize, LayerSurfaceRequirements>,
frame_stats: gpu_stats::FrameStats,
last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
frame_count: u64,
gpu_stats_enabled: bool,
warning_state: RendererWarningState,
}
fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
let filter = match sampling {
ImageSampling::Nearest => wgpu::FilterMode::Nearest,
ImageSampling::Linear => wgpu::FilterMode::Linear,
};
wgpu::SamplerDescriptor {
label: Some(match sampling {
ImageSampling::Nearest => "Nearest Image Sampler",
ImageSampling::Linear => "Linear Image Sampler",
}),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: filter,
min_filter: filter,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
}
}
#[cfg(test)]
fn layer_raster_cache_candidate(
layer: &LayerNode,
root_scale: f32,
has_backdrop_underlay: bool,
allow_runtime_cache: bool,
) -> Option<(LayerRasterCacheKey, Rect)> {
let mut layer_surface_requirements_cache = HashMap::new();
let surface_requirements =
layer_surface_requirements_cached(layer, &mut layer_surface_requirements_cache);
let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
|| (allow_runtime_cache && surface_requirements.has_renderer_forced_surface());
if !cache_is_allowed {
return None;
}
if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
return None;
}
let logical_rect = estimate_layer_surface_rect(layer);
let pixel_size = surface_target_size(logical_rect, root_scale, u32::MAX);
Some((
LayerRasterCacheKey::new(
layer.node_id,
layer.target_content_hash(),
layer.effect_hash(),
logical_rect,
pixel_size,
ScaleBucket::from_scale(root_scale),
),
logical_rect,
))
}
impl GpuRenderer {
pub fn new(
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
surface_format: wgpu::TextureFormat,
adapter_backend: wgpu::Backend,
text_fonts: SoftwareTextFontSet,
) -> Self {
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Uniform Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let shape_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Shape Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
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: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline = create_shape_pipeline(
&device,
surface_format,
&uniform_bind_group_layout,
&shape_bind_group_layout,
BlendMode::SrcOver,
);
let pipeline_dst_out = create_shape_pipeline(
&device,
surface_format,
&uniform_bind_group_layout,
&shape_bind_group_layout,
BlendMode::DstOut,
);
let image_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Image Texture Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let image_pipeline = create_image_pipeline(
&device,
surface_format,
&uniform_bind_group_layout,
&image_bind_group_layout,
BlendMode::SrcOver,
);
let image_pipeline_dst_out = create_image_pipeline(
&device,
surface_format,
&uniform_bind_group_layout,
&image_bind_group_layout,
BlendMode::DstOut,
);
#[cfg(not(target_arch = "wasm32"))]
let upload_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Frame Upload Buffer"),
size: INITIAL_UPLOAD_BUFFER_BYTES,
usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
#[cfg(not(target_arch = "wasm32"))]
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Uniform Buffer"),
size: std::mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
#[cfg(not(target_arch = "wasm32"))]
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Uniform Bind Group"),
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
#[cfg(not(target_arch = "wasm32"))]
let shape_buffers = ShapeBatchBuffers::new(&device, &shape_bind_group_layout);
let image_nearest_sampler =
device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
let image_linear_sampler =
device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
#[cfg(not(target_arch = "wasm32"))]
let image_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Vertex Buffer"),
size: (std::mem::size_of::<Vertex>() * 4) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
#[cfg(not(target_arch = "wasm32"))]
let image_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Index Buffer"),
size: (std::mem::size_of::<u32>() * 6) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let effect_renderer = EffectRenderer::new(&device, surface_format, adapter_backend);
Self {
device,
queue,
surface_format,
pipeline,
pipeline_dst_out,
#[cfg(target_arch = "wasm32")]
uniform_bind_group_layout,
shape_bind_group_layout,
image_pipeline,
image_pipeline_dst_out,
image_bind_group_layout,
image_nearest_sampler,
image_linear_sampler,
text_fonts,
#[cfg(not(target_arch = "wasm32"))]
upload_buffer,
#[cfg(not(target_arch = "wasm32"))]
uniform_buffer,
#[cfg(not(target_arch = "wasm32"))]
uniform_bind_group,
#[cfg(not(target_arch = "wasm32"))]
shape_buffers,
#[cfg(not(target_arch = "wasm32"))]
image_vertex_buffer,
#[cfg(not(target_arch = "wasm32"))]
image_index_buffer,
#[cfg(target_arch = "wasm32")]
wasm_uniform_batches: Vec::new(),
#[cfg(target_arch = "wasm32")]
wasm_uniform_batch_cursor: 0,
#[cfg(target_arch = "wasm32")]
wasm_shape_batches: Vec::new(),
#[cfg(target_arch = "wasm32")]
wasm_shape_batch_cursor: 0,
#[cfg(target_arch = "wasm32")]
wasm_image_batches: Vec::new(),
#[cfg(target_arch = "wasm32")]
wasm_image_batch_cursor: 0,
image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
MAX_TEXTURE_CACHE_ITEMS,
),
text_image_cache: BoundedLruCache::with_capacity_at_least_one(
MAX_TEXT_IMAGE_CACHE_ITEMS,
),
scratch_shape_data: Vec::new(),
scratch_gradients: Vec::new(),
scratch_vertices: Vec::new(),
scratch_indices: Vec::new(),
scratch_image_vertices: Vec::new(),
scratch_image_indices: Vec::new(),
scratch_image_cmds: Vec::new(),
scratch_text_images: Vec::new(),
scratch_segment_items: Vec::new(),
scratch_effect_ranges: Vec::new(),
scratch_layer_events: Vec::new(),
staged_uploads: StagedBufferUploads::default(),
frame_graph_executor: WgpuFrameGraphExecutor::new(),
deferred_offscreen_releases: Vec::new(),
effect_renderer,
layer_surface_cache: LayerSurfaceCache::new(),
shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
MAX_SHADOW_SURFACE_CACHE_ITEMS,
),
shadow_surface_cache_bytes: 0,
layer_surface_rect_cache: HashMap::new(),
layer_surface_requirements_cache: HashMap::new(),
frame_stats: gpu_stats::FrameStats::default(),
last_frame_stats: None,
frame_count: 0,
gpu_stats_enabled: gpu_stats_enabled(),
warning_state: RendererWarningState::default(),
}
}
fn ensure_image_cached(&mut self, image: &ImageBitmap) -> Result<(), String> {
if self.image_texture_cache.get(&image.id()).is_some() {
return Ok(());
}
let size = wgpu::Extent3d {
width: image.width(),
height: image.height(),
depth_or_array_layers: 1,
};
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Image Texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let upload_stats = self.frame_graph_executor.upload_texture(
&self.queue,
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
image.pixels(),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * image.width()),
rows_per_image: Some(image.height()),
},
size,
);
self.frame_stats.record_command_stats(upload_stats);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
self.image_texture_cache.put(
image.id(),
CachedImageTexture {
_texture: texture,
_view: view,
nearest_bind_group,
linear_bind_group,
},
);
Ok(())
}
fn image_bind_group(
&self,
view: &wgpu::TextureView,
sampler: &wgpu::Sampler,
) -> wgpu::BindGroup {
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Image Texture Bind Group"),
layout: &self.image_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
})
}
fn max_texture_dim(&self) -> u32 {
self.effect_renderer.max_texture_dim()
}
fn acquire_offscreen(&mut self, width: u32, height: u32) -> OffscreenTarget {
self.effect_renderer
.acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
}
fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
self.acquire_offscreen(width, height)
}
fn transient_offscreen_descriptor(
&self,
label: &'static str,
width: u32,
height: u32,
) -> FrameTextureDescriptor {
let max_texture_dim = self.max_texture_dim();
FrameTextureDescriptor::render_attachment(
label,
width.min(max_texture_dim),
height.min(max_texture_dim),
self.surface_format,
)
}
fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
self.deferred_offscreen_releases.push(target);
}
fn flush_deferred_offscreen_releases(&mut self) {
for target in self.deferred_offscreen_releases.drain(..) {
self.effect_renderer.release_offscreen(target);
}
}
fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
if let LayerSurfaceTexture::Owned(target) = target {
self.defer_offscreen_release(target);
}
}
fn cached_layer_surface(
&mut self,
key: &LayerRasterCacheKey,
) -> Option<(Rc<OffscreenTarget>, Rect)> {
self.layer_surface_cache.get(key, &self.frame_stats)
}
fn insert_cached_layer_surface(
&mut self,
key: LayerRasterCacheKey,
target: OffscreenTarget,
logical_rect: Rect,
) -> Rc<OffscreenTarget> {
self.layer_surface_cache
.insert(key, target, logical_rect, &self.frame_stats)
}
fn cached_shadow_surface(
&mut self,
key: &ShadowSurfaceCacheKey,
) -> Option<Rc<OffscreenTarget>> {
self.shadow_surface_cache
.get(key)
.map(|cached| cached.target.clone())
}
fn insert_cached_shadow_surface(
&mut self,
key: ShadowSurfaceCacheKey,
target: OffscreenTarget,
) {
let byte_size = offscreen_byte_size(target.width, target.height);
while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
let Some((_evicted_key, evicted_entry)) = self.shadow_surface_cache.pop_lru() else {
break;
};
self.shadow_surface_cache_bytes = self
.shadow_surface_cache_bytes
.saturating_sub(evicted_entry.byte_size);
}
let cached = CachedShadowSurface {
target: Rc::new(target),
byte_size,
};
if let Some((_replaced_key, replaced_entry)) = self.shadow_surface_cache.push(key, cached) {
self.shadow_surface_cache_bytes = self
.shadow_surface_cache_bytes
.saturating_sub(replaced_entry.byte_size);
}
self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
}
fn layer_raster_cache_candidate(
&mut self,
layer: &LayerNode,
root_scale: f32,
has_backdrop_underlay: bool,
allow_runtime_cache: bool,
logical_rect_override: Option<Rect>,
) -> Option<(LayerRasterCacheKey, Rect)> {
let surface_requirements =
layer_surface_requirements_cached(layer, &mut self.layer_surface_requirements_cache);
let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
|| (allow_runtime_cache && surface_requirements.has_renderer_forced_surface());
if !cache_is_allowed {
return None;
}
if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
return None;
}
if layer.effect().is_some_and(|e| e.contains_runtime_shader()) {
return None;
}
let logical_rect = logical_rect_override.unwrap_or_else(|| {
estimate_layer_surface_rect_cached(
layer,
&mut self.layer_surface_rect_cache,
&mut self.layer_surface_requirements_cache,
)
});
let pixel_size = surface_target_size(logical_rect, root_scale, self.max_texture_dim());
Some((
LayerRasterCacheKey::new(
layer.node_id,
layer.target_content_hash(),
layer.effect_hash(),
logical_rect,
pixel_size,
ScaleBucket::from_scale(root_scale),
),
logical_rect,
))
}
fn supports_render_effect(&self, effect: &RenderEffect) -> bool {
is_render_effect_supported(effect)
}
}
struct RecordingSurfaceBackend<'renderer, 'recorder, C: FrameCommandRecorder> {
renderer: &'renderer mut GpuRenderer,
recorder: &'recorder mut C,
}
impl<C: FrameCommandRecorder> RecordingSurfaceBackend<'_, '_, C> {
#[allow(clippy::too_many_arguments)]
fn render_range_with_layer_events_to_target_recorded(
&mut self,
text_state: &mut TextSystemState,
target: &OffscreenTarget,
shapes: &[DrawShape],
images: &[ImageDraw],
texts: &[TextDraw],
shadow_draws: &[ShadowDraw],
draw_ops: &[DrawOp],
effect_layers: &[EffectLayer],
backdrop_layers: &[BackdropLayer],
z_start: usize,
z_end: usize,
excluded_effect_layer: Option<usize>,
width: u32,
height: u32,
root_scale: f32,
backdrop_underlay: Option<&OffscreenTarget>,
initial_load_op: wgpu::LoadOp<wgpu::Color>,
) -> Result<(), String> {
if z_start >= z_end {
if matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
self.clear_target_view_with_load_op(&target.view, initial_load_op);
}
return Ok(());
}
let mut effect_z_ranges = std::mem::take(&mut self.renderer.scratch_effect_ranges);
collect_effect_ranges(
effect_layers,
z_start,
z_end,
excluded_effect_layer,
&mut effect_z_ranges,
);
let mut events = std::mem::take(&mut self.renderer.scratch_layer_events);
collect_layer_events(
effect_layers,
backdrop_layers,
z_start,
z_end,
excluded_effect_layer,
&mut events,
);
let result = (|| -> Result<(), String> {
let mut next_load_op = initial_load_op;
let mut cursor_z = z_start;
for event in &events {
if event.z_index > cursor_z {
self.render_non_effect_segment(
text_state,
&target.view,
shapes,
images,
texts,
shadow_draws,
draw_ops,
cursor_z,
event.z_index,
&effect_z_ranges,
width,
height,
root_scale,
next_load_op,
)?;
next_load_op = wgpu::LoadOp::Load;
cursor_z = event.z_index;
} else if event.z_index < cursor_z {
continue;
}
if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
self.clear_target_view_with_load_op(&target.view, next_load_op);
next_load_op = wgpu::LoadOp::Load;
}
match event.kind {
LayerEventKind::Backdrop(index) => {
execute_apply_backdrop_layer_to_target(
self,
target,
&backdrop_layers[index],
backdrop_underlay,
width,
height,
root_scale,
)?;
}
LayerEventKind::Effect(index) => {
let layer = &effect_layers[index];
if layer.z_start < cursor_z {
continue;
}
execute_render_effect_layer_to_target(
self,
text_state,
target,
shapes,
images,
texts,
shadow_draws,
draw_ops,
effect_layers,
backdrop_layers,
index,
backdrop_underlay,
width,
height,
root_scale,
)?;
cursor_z = cursor_z.max(layer.z_end);
}
}
}
if cursor_z < z_end {
self.render_non_effect_segment(
text_state,
&target.view,
shapes,
images,
texts,
shadow_draws,
draw_ops,
cursor_z,
z_end,
&effect_z_ranges,
width,
height,
root_scale,
next_load_op,
)?;
} else if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
self.clear_target_view_with_load_op(&target.view, next_load_op);
}
Ok(())
})();
self.renderer.scratch_effect_ranges = effect_z_ranges;
self.renderer.scratch_layer_events = events;
result
}
#[allow(clippy::too_many_arguments)]
fn record_shader_composite(
&mut self,
source: &OffscreenTarget,
shader: &RuntimeShader,
effect_rect: [f32; 4],
dest_view: &wgpu::TextureView,
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
dest_viewport: Option<(f32, f32, f32, f32)>,
sample_mode: CompositeSampleMode,
) {
let device = self.renderer.device.clone();
let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
"Shader Effect Composite Scratch",
source.width,
source.height,
);
let scratch = self
.recorder
.acquire_transient_offscreen(&device, scratch_descriptor);
let shader_applied = {
self.renderer.effect_renderer.encode_shader(
self.recorder,
&device,
source,
&scratch.view,
shader,
effect_rect,
)
};
let composite_source = if shader_applied {
self.renderer
.effect_renderer
.debug_effects
.set(self.renderer.effect_renderer.debug_effects.get() + 1);
self.recorder.record_pass();
&scratch
} else {
source
};
{
self.renderer
.effect_renderer
.encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
self.recorder,
&device,
composite_source,
dest_view,
alpha,
load_op,
scissor,
None,
supported_blend_mode(blend_mode),
dest_viewport,
sample_mode,
);
}
self.recorder.record_pass();
self.renderer.effect_renderer.record_composite_pass();
self.recorder
.release_transient_offscreen(scratch_descriptor, scratch);
}
#[allow(clippy::too_many_arguments)]
fn record_shader_projective_composite(
&mut self,
source: &OffscreenTarget,
shader: &RuntimeShader,
effect_rect: [f32; 4],
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
source_size: (f32, f32),
inverse_matrix: [[f32; 3]; 3],
dest_bounds: [[f32; 2]; 4],
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
sample_mode: CompositeSampleMode,
) {
if projective_dest_bounds_rect(dest_bounds).is_none() {
return;
}
let device = self.renderer.device.clone();
let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
"Shader Projective Composite Scratch",
source.width,
source.height,
);
let scratch = self
.recorder
.acquire_transient_offscreen(&device, scratch_descriptor);
let shader_applied = {
self.renderer.effect_renderer.encode_shader(
self.recorder,
&device,
source,
&scratch.view,
shader,
effect_rect,
)
};
let composite_source = if shader_applied {
self.renderer
.effect_renderer
.debug_effects
.set(self.renderer.effect_renderer.debug_effects.get() + 1);
self.recorder.record_pass();
&scratch
} else {
source
};
let composited = {
self.renderer
.effect_renderer
.encode_composite_to_view_projective(
self.recorder,
&device,
composite_source,
dest_view,
viewport,
source_size,
inverse_matrix,
dest_bounds,
alpha,
load_op,
scissor,
supported_blend_mode(blend_mode),
sample_mode,
)
};
if composited {
self.recorder.record_pass();
self.renderer.effect_renderer.record_composite_pass();
}
self.recorder
.release_transient_offscreen(scratch_descriptor, scratch);
}
#[allow(clippy::too_many_arguments)]
fn record_effect_composite(
&mut self,
source: &OffscreenTarget,
effect: &RenderEffect,
effect_rect: [f32; 4],
dest_view: &wgpu::TextureView,
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
dest_viewport: Option<(f32, f32, f32, f32)>,
sample_mode: CompositeSampleMode,
) -> Result<(), String> {
let device = self.renderer.device.clone();
let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
"Render Effect Composite Scratch",
source.width,
source.height,
);
let scratch = self
.recorder
.acquire_transient_offscreen(&device, scratch_descriptor);
let effect_scratch_targets = self
.renderer
.effect_renderer
.acquire_recorded_effect_scratch_targets(
self.recorder,
&device,
effect,
source.width,
source.height,
self.renderer.surface_format,
);
let effect_passes = {
let mut effect_scratch_refs = effect_scratch_targets.refs();
let pass_count = self.renderer.effect_renderer.encode_effect(
self.recorder,
&device,
source,
&scratch.view,
effect,
effect_rect,
&mut effect_scratch_refs,
)?;
effect_scratch_refs.assert_consumed()?;
Ok(pass_count)
};
let effect_passes = match effect_passes {
Ok(pass_count) => pass_count,
Err(error) => {
effect_scratch_targets.release_into(self.recorder);
self.recorder
.release_transient_offscreen(scratch_descriptor, scratch);
return Err(error);
}
};
{
self.renderer
.effect_renderer
.encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
self.recorder,
&device,
&scratch,
dest_view,
alpha,
load_op,
scissor,
None,
supported_blend_mode(blend_mode),
dest_viewport,
sample_mode,
);
}
self.recorder.record_passes(effect_passes.saturating_add(1));
self.renderer.effect_renderer.record_composite_pass();
effect_scratch_targets.release_into(self.recorder);
self.recorder
.release_transient_offscreen(scratch_descriptor, scratch);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn record_effect_projective_composite(
&mut self,
source: &OffscreenTarget,
effect: &RenderEffect,
effect_rect: [f32; 4],
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
source_size: (f32, f32),
inverse_matrix: [[f32; 3]; 3],
dest_bounds: [[f32; 2]; 4],
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
sample_mode: CompositeSampleMode,
) -> Result<(), String> {
if projective_dest_bounds_rect(dest_bounds).is_none() {
return Ok(());
}
let device = self.renderer.device.clone();
let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
"Render Effect Projective Composite Scratch",
source.width,
source.height,
);
let scratch = self
.recorder
.acquire_transient_offscreen(&device, scratch_descriptor);
let effect_scratch_targets = self
.renderer
.effect_renderer
.acquire_recorded_effect_scratch_targets(
self.recorder,
&device,
effect,
source.width,
source.height,
self.renderer.surface_format,
);
let effect_passes = {
let mut effect_scratch_refs = effect_scratch_targets.refs();
let pass_count = self.renderer.effect_renderer.encode_effect(
self.recorder,
&device,
source,
&scratch.view,
effect,
effect_rect,
&mut effect_scratch_refs,
)?;
effect_scratch_refs.assert_consumed()?;
Ok(pass_count)
};
let effect_passes = match effect_passes {
Ok(pass_count) => pass_count,
Err(error) => {
effect_scratch_targets.release_into(self.recorder);
self.recorder
.release_transient_offscreen(scratch_descriptor, scratch);
return Err(error);
}
};
let composited = {
self.renderer
.effect_renderer
.encode_composite_to_view_projective(
self.recorder,
&device,
&scratch,
dest_view,
viewport,
source_size,
inverse_matrix,
dest_bounds,
alpha,
load_op,
scissor,
supported_blend_mode(blend_mode),
sample_mode,
)
};
if composited {
self.recorder.record_passes(effect_passes.saturating_add(1));
self.renderer.effect_renderer.record_composite_pass();
} else {
self.recorder.record_passes(effect_passes);
}
effect_scratch_targets.release_into(self.recorder);
self.recorder
.release_transient_offscreen(scratch_descriptor, scratch);
Ok(())
}
}
impl<C: FrameCommandRecorder> SurfaceExecutionBackend for RecordingSurfaceBackend<'_, '_, C> {
fn max_texture_dim(&self) -> u32 {
self.renderer.max_texture_dim()
}
fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
self.renderer.acquire_retained_surface(width, height)
}
fn acquire_frame_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
let descriptor =
self.renderer
.transient_offscreen_descriptor("Frame Surface", width, height);
self.recorder
.acquire_transient_offscreen(&self.renderer.device, descriptor)
}
fn release_frame_surface(&mut self, target: OffscreenTarget) {
let descriptor = self.renderer.transient_offscreen_descriptor(
"Frame Surface",
target.width,
target.height,
);
self.recorder
.release_transient_offscreen(descriptor, target);
}
fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
self.renderer.release_layer_surface_target(target);
}
fn cached_layer_surface(
&mut self,
key: &LayerRasterCacheKey,
) -> Option<(Rc<OffscreenTarget>, Rect)> {
self.renderer.cached_layer_surface(key)
}
fn insert_cached_layer_surface(
&mut self,
key: LayerRasterCacheKey,
target: OffscreenTarget,
logical_rect: Rect,
) -> Rc<OffscreenTarget> {
self.renderer
.insert_cached_layer_surface(key, target, logical_rect)
}
fn layer_raster_cache_candidate(
&mut self,
layer: &LayerNode,
root_scale: f32,
has_backdrop_underlay: bool,
allow_runtime_cache: bool,
logical_rect_override: Option<Rect>,
) -> Option<(LayerRasterCacheKey, Rect)> {
self.renderer.layer_raster_cache_candidate(
layer,
root_scale,
has_backdrop_underlay,
allow_runtime_cache,
logical_rect_override,
)
}
fn layer_surface_requirements(&mut self, layer: &LayerNode) -> LayerSurfaceRequirements {
layer_surface_requirements_cached(
layer,
&mut self.renderer.layer_surface_requirements_cache,
)
}
fn collect_layer_contents_with_translation_context<'a>(
&mut self,
text_state: &mut TextSystemState,
layer: &'a LayerNode,
inherited_clip: Option<Rect>,
inherited_translated_snap_anchor: Option<SnapAnchor>,
translation_context: TranslationRenderContext,
) -> CollectedLayer<'a> {
collect_layer_contents_with_translation_context_and_text_layout(
layer,
text_state,
inherited_clip,
inherited_translated_snap_anchor,
translation_context,
&mut self.renderer.layer_surface_rect_cache,
&mut self.renderer.layer_surface_requirements_cache,
)
}
fn clear_target_view_with_load_op(
&mut self,
target_view: &wgpu::TextureView,
load_op: wgpu::LoadOp<wgpu::Color>,
) {
{
let _clear = self
.recorder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Layer Event Clear Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
self.recorder.record_pass();
}
fn render_non_effect_segment(
&mut self,
text_state: &mut TextSystemState,
target_view: &wgpu::TextureView,
shapes: &[DrawShape],
images: &[ImageDraw],
texts: &[TextDraw],
shadow_draws: &[ShadowDraw],
draw_ops: &[DrawOp],
z_start: usize,
z_end: usize,
effect_z_ranges: &[Range<usize>],
width: u32,
height: u32,
root_scale: f32,
initial_load_op: wgpu::LoadOp<wgpu::Color>,
) -> Result<(), String> {
let mut ordered_items = std::mem::take(&mut self.renderer.scratch_segment_items);
collect_non_effect_segment_items(
shapes,
images,
texts,
shadow_draws,
draw_ops,
z_start,
z_end,
effect_z_ranges,
&mut ordered_items,
);
let result = if ordered_items.is_empty() {
Ok(SegmentCommandEncodeOutcome { first_batch: true })
} else {
self.renderer.encode_non_effect_segment_commands(
text_state,
self.recorder,
target_view,
&ordered_items,
shapes,
images,
texts,
shadow_draws,
initial_load_op,
width,
height,
root_scale,
)
};
self.renderer.scratch_segment_items = ordered_items;
let outcome = result?;
if outcome.first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
self.clear_target_view_with_load_op(target_view, initial_load_op);
}
Ok(())
}
fn render_range_with_layer_events_to_target(
&mut self,
text_state: &mut TextSystemState,
target: &OffscreenTarget,
shapes: &[DrawShape],
images: &[ImageDraw],
texts: &[TextDraw],
shadow_draws: &[ShadowDraw],
draw_ops: &[DrawOp],
effect_layers: &[EffectLayer],
backdrop_layers: &[BackdropLayer],
z_start: usize,
z_end: usize,
excluded_effect_layer: Option<usize>,
width: u32,
height: u32,
root_scale: f32,
backdrop_underlay: Option<&OffscreenTarget>,
initial_load_op: wgpu::LoadOp<wgpu::Color>,
) -> Result<(), String> {
self.render_range_with_layer_events_to_target_recorded(
text_state,
target,
shapes,
images,
texts,
shadow_draws,
draw_ops,
effect_layers,
backdrop_layers,
z_start,
z_end,
excluded_effect_layer,
width,
height,
root_scale,
backdrop_underlay,
initial_load_op,
)
}
fn render_shadow_draw(
&mut self,
text_state: &mut TextSystemState,
target_view: &wgpu::TextureView,
shadow: &ShadowDraw,
width: u32,
height: u32,
root_scale: f32,
) {
self.renderer.encode_shadow_draw(
text_state,
self.recorder,
target_view,
shadow,
width,
height,
root_scale,
);
}
fn composite_to_view_projective(
&mut self,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
source_size: (f32, f32),
inverse_matrix: [[f32; 3]; 3],
dest_bounds: [[f32; 2]; 4],
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
sample_mode: CompositeSampleMode,
) {
let device = self.renderer.device.clone();
let composited = {
self.renderer
.effect_renderer
.encode_composite_to_view_projective(
self.recorder,
&device,
source,
dest_view,
viewport,
source_size,
inverse_matrix,
dest_bounds,
alpha,
load_op,
scissor,
supported_blend_mode(blend_mode),
sample_mode,
)
};
if composited {
self.recorder.record_pass();
self.renderer.effect_renderer.record_composite_pass();
}
}
fn composite_projective_surfaces_to_view(
&mut self,
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
composites: &[ProjectiveSurfaceComposite<'_>],
) {
let device = self.renderer.device.clone();
let mut composite_count = 0_u32;
for composite in composites
.iter()
.copied()
.filter(|composite| projective_dest_bounds_rect(composite.dest_bounds).is_some())
{
let composited = {
self.renderer
.effect_renderer
.encode_composite_to_view_projective(
self.recorder,
&device,
composite.source,
dest_view,
viewport,
composite.source_size,
composite.inverse_matrix,
composite.dest_bounds,
composite.alpha,
composite.load_op,
composite.scissor,
supported_blend_mode(composite.blend_mode),
composite.sample_mode,
)
};
if composited {
composite_count = composite_count.saturating_add(1);
}
}
if composite_count > 0 {
self.recorder.record_passes(composite_count);
self.renderer
.effect_renderer
.debug_composites
.set(self.renderer.effect_renderer.debug_composites.get() + composite_count);
}
}
fn composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
&mut self,
source: &OffscreenTarget,
dest_view: &wgpu::TextureView,
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
rounded_mask: Option<RoundedCompositeMask>,
blend_mode: BlendMode,
dest_viewport: Option<(f32, f32, f32, f32)>,
sample_mode: CompositeSampleMode,
) {
let device = self.renderer.device.clone();
{
self.renderer
.effect_renderer
.encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
self.recorder,
&device,
source,
dest_view,
alpha,
load_op,
scissor,
rounded_mask,
supported_blend_mode(blend_mode),
dest_viewport,
sample_mode,
);
}
self.recorder.record_pass();
self.renderer.effect_renderer.record_composite_pass();
}
fn apply_effect_and_composite_to_view(
&mut self,
source: &OffscreenTarget,
effect: &RenderEffect,
effect_rect: [f32; 4],
dest_view: &wgpu::TextureView,
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
dest_viewport: Option<(f32, f32, f32, f32)>,
sample_mode: CompositeSampleMode,
) -> Result<(), String> {
self.record_effect_composite(
source,
effect,
effect_rect,
dest_view,
alpha,
load_op,
scissor,
blend_mode,
dest_viewport,
sample_mode,
)
}
fn apply_shader_and_composite_to_view(
&mut self,
source: &OffscreenTarget,
shader: &RuntimeShader,
effect_rect: [f32; 4],
dest_view: &wgpu::TextureView,
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
dest_viewport: Option<(f32, f32, f32, f32)>,
sample_mode: CompositeSampleMode,
) {
self.record_shader_composite(
source,
shader,
effect_rect,
dest_view,
alpha,
load_op,
scissor,
blend_mode,
dest_viewport,
sample_mode,
);
}
fn apply_shader_and_composite_to_view_projective(
&mut self,
source: &OffscreenTarget,
shader: &RuntimeShader,
effect_rect: [f32; 4],
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
source_size: (f32, f32),
inverse_matrix: [[f32; 3]; 3],
dest_bounds: [[f32; 2]; 4],
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
sample_mode: CompositeSampleMode,
) {
self.record_shader_projective_composite(
source,
shader,
effect_rect,
dest_view,
viewport,
source_size,
inverse_matrix,
dest_bounds,
alpha,
load_op,
scissor,
blend_mode,
sample_mode,
);
}
fn apply_effect_and_composite_to_view_projective(
&mut self,
source: &OffscreenTarget,
effect: &RenderEffect,
effect_rect: [f32; 4],
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
source_size: (f32, f32),
inverse_matrix: [[f32; 3]; 3],
dest_bounds: [[f32; 2]; 4],
alpha: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
blend_mode: BlendMode,
sample_mode: CompositeSampleMode,
) -> Result<(), String> {
self.record_effect_projective_composite(
source,
effect,
effect_rect,
dest_view,
viewport,
source_size,
inverse_matrix,
dest_bounds,
alpha,
load_op,
scissor,
blend_mode,
sample_mode,
)
}
fn is_render_effect_supported(&self, effect: &RenderEffect) -> bool {
self.renderer.supports_render_effect(effect)
}
fn warn_unsupported_effect_once(&self) {
self.renderer.warning_state.warn_unsupported_effect_once();
}
fn record_layer_cache_miss(&self, width: u32, height: u32) {
self.renderer
.frame_stats
.record_layer_cache_miss(width, height);
}
fn record_isolated_layer_render(
&self,
width: u32,
height: u32,
node_id: Option<NodeId>,
logical_rect: Rect,
requirements: SurfaceRequirementSet,
) {
self.renderer.frame_stats.record_isolated_layer_render(
width,
height,
node_id,
logical_rect,
requirements.into(),
);
}
}
impl GpuRenderer {
#[allow(clippy::too_many_arguments)] pub fn render(
&mut self,
text_state: &mut TextSystemState,
view: &wgpu::TextureView,
graph: &RenderGraph,
width: u32,
height: u32,
root_scale: f32,
) -> Result<(), String> {
log::trace!("🎨 Rendering graph to {}x{}", width, height);
#[cfg(target_arch = "wasm32")]
{
self.wasm_uniform_batch_cursor = 0;
self.wasm_shape_batch_cursor = 0;
self.wasm_image_batch_cursor = 0;
}
let result = self.render_graph(text_state, view, graph, width, height, root_scale);
self.flush_deferred_offscreen_releases();
#[cfg(target_arch = "wasm32")]
{
const WASM_BATCH_POOL_MARGIN: usize = 4;
self.wasm_uniform_batches.truncate(
self.wasm_uniform_batch_cursor
.saturating_add(WASM_BATCH_POOL_MARGIN),
);
self.wasm_shape_batches.truncate(
self.wasm_shape_batch_cursor
.saturating_add(WASM_BATCH_POOL_MARGIN),
);
self.wasm_image_batches.truncate(
self.wasm_image_batch_cursor
.saturating_add(WASM_BATCH_POOL_MARGIN),
);
}
self.layer_surface_rect_cache.clear();
self.layer_surface_requirements_cache.clear();
if self.layer_surface_requirements_cache.capacity() > RETAINED_LAYER_REQUIREMENTS_CAPACITY {
self.layer_surface_requirements_cache
.shrink_to(RETAINED_LAYER_REQUIREMENTS_CAPACITY);
}
self.staged_uploads
.shrink_retained_capacity(RETAINED_STAGED_UPLOAD_BYTES, RETAINED_STAGED_UPLOAD_COPIES);
self.layer_surface_cache.finish_frame(&self.frame_stats);
self.frame_stats.offscreen_pool_size.set(
self.effect_renderer
.retained_offscreen_count()
.saturating_add(self.frame_graph_executor.retained_texture_count())
as u32,
);
self.frame_stats.offscreen_pool_bytes.set(
(self.effect_renderer.retained_offscreen_bytes() as u64)
.saturating_add(self.frame_graph_executor.retained_texture_bytes()),
);
self.frame_stats
.text_pool_size
.set(self.text_image_cache.len() as u32);
self.frame_stats
.image_cache_size
.set(self.image_texture_cache.len() as u32);
self.frame_stats
.text_cache_size
.set(text_state.text_cache_len() as u32);
self.effect_renderer
.merge_and_reset_debug_counters(&self.frame_stats);
self.frame_graph_executor.reset_upload_allocators();
let snapshot = self.frame_stats.snapshot();
self.last_frame_stats = Some(snapshot);
self.frame_stats.maybe_print_snapshot(
snapshot,
&mut self.frame_count,
self.gpu_stats_enabled,
);
self.frame_stats.reset();
result
}
pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
self.last_frame_stats
}
pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
let layer_surface_cache_stats = self.layer_surface_cache.debug_stats();
DebugCpuAllocationStats {
scene_graph_node_count: 0,
scene_graph_heap_bytes: 0,
scene_hits_len: 0,
scene_hits_cap: 0,
scene_node_index_len: 0,
scene_node_index_cap: 0,
text_renderer_pool_len: self.text_image_cache.len(),
text_renderer_pool_cap: self.text_image_cache.cap().get(),
swash_image_cache_len: 0,
swash_image_cache_cap: 0,
swash_outline_cache_len: 0,
swash_outline_cache_cap: 0,
image_texture_cache_len: self.image_texture_cache.len(),
image_texture_cache_cap: self.image_texture_cache.cap().get(),
scratch_shape_data_cap: self.scratch_shape_data.capacity(),
scratch_gradients_cap: self.scratch_gradients.capacity(),
scratch_vertices_cap: self.scratch_vertices.capacity(),
scratch_indices_cap: self.scratch_indices.capacity(),
scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
scratch_image_indices_cap: self.scratch_image_indices.capacity(),
scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
scratch_segment_items_cap: self.scratch_segment_items.capacity(),
scratch_effect_ranges_cap: self.scratch_effect_ranges.capacity(),
scratch_layer_events_cap: self.scratch_layer_events.capacity(),
staged_upload_bytes_cap: self.staged_uploads.bytes.capacity(),
staged_upload_copies_cap: self.staged_uploads.copies.capacity(),
layer_surface_cache_len: layer_surface_cache_stats.entries_len,
layer_surface_cache_cap: layer_surface_cache_stats.entries_cap,
layer_surface_cache_identity_len: layer_surface_cache_stats.identity_len,
layer_surface_cache_identity_cap: layer_surface_cache_stats.identity_cap,
layer_surface_rect_cache_len: self.layer_surface_rect_cache.len(),
layer_surface_rect_cache_cap: self.layer_surface_rect_cache.capacity(),
layer_surface_requirements_cache_len: self.layer_surface_requirements_cache.len(),
layer_surface_requirements_cache_cap: self.layer_surface_requirements_cache.capacity(),
layer_cache_seen_this_frame_len: layer_surface_cache_stats.seen_this_frame_len,
layer_cache_seen_this_frame_cap: layer_surface_cache_stats.seen_this_frame_cap,
}
}
#[allow(clippy::too_many_arguments)] pub fn render_to_rgba_pixels(
&mut self,
text_state: &mut TextSystemState,
graph: &RenderGraph,
width: u32,
height: u32,
root_scale: f32,
) -> Result<Vec<u8>, String> {
if width == 0 || height == 0 {
return Err("Screenshot size must be non-zero".to_string());
}
let output_texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Screenshot Output Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.surface_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
self.render(text_state, &output_view, graph, width, height, root_scale)?;
let bytes_per_pixel = 4u32;
let unpadded_bytes_per_row = width
.checked_mul(bytes_per_pixel)
.ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
let padded_bytes_per_row =
align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Screenshot Readback Buffer"),
size: output_buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let device = self.device.clone();
let queue = self.queue.clone();
let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
let source = graph.import_surface("screenshot-copy-source");
graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
context.encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &output_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &output_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
Ok(())
});
let mut executor = std::mem::take(&mut self.frame_graph_executor);
let execution = executor.execute_recorded_graph(&device, &queue, graph);
self.frame_graph_executor = executor;
let execution = execution.map_err(|error| error.to_string())?;
let submission_index = execution.submission;
let copy_stats = execution.stats;
self.last_frame_stats = self
.last_frame_stats
.map(|snapshot| snapshot.with_command_stats_added(copy_stats));
let buffer_slice = output_buffer.slice(..);
let (tx, rx) = mpsc::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
let _ = self.device.poll(wgpu::PollType::Wait {
submission_index: Some(submission_index),
timeout: None,
});
match rx.recv_timeout(Duration::from_secs(3)) {
Ok(Ok(())) => {}
Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
}
let mapped = buffer_slice.get_mapped_range();
let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
let src_row_len = padded_bytes_per_row as usize;
let dst_row_len = unpadded_bytes_per_row as usize;
for row in 0..height as usize {
let src_offset = row * src_row_len;
let dst_offset = row * dst_row_len;
pixels[dst_offset..dst_offset + dst_row_len]
.copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
}
drop(mapped);
output_buffer.unmap();
self.convert_surface_pixels_to_rgba(&mut pixels)?;
Ok(pixels)
}
fn render_graph(
&mut self,
text_state: &mut TextSystemState,
surface_view: &wgpu::TextureView,
graph: &RenderGraph,
width: u32,
height: u32,
root_scale: f32,
) -> Result<(), String> {
let device = self.device.clone();
let queue = self.queue.clone();
#[cfg(not(target_arch = "wasm32"))]
{
let mut executor = std::mem::take(&mut self.frame_graph_executor);
let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
let surface = frame_graph.import_surface("renderer-surface");
frame_graph.add_fallible_recorded_command_pass(
Some("Renderer Frame Pass"),
&[],
&[surface],
|frame_encoder| {
self.render_graph_recorded(
text_state,
surface_view,
graph,
width,
height,
root_scale,
frame_encoder,
)
},
);
let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
self.frame_graph_executor = executor;
match execution {
Ok(execution) => {
if execution.stats.pass_count > 0 {
self.frame_stats.record_command_stats(execution.stats);
}
Ok(())
}
Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => Ok(()),
Err(error) => Err(error.to_string()),
}
}
#[cfg(target_arch = "wasm32")]
{
let mut executor = std::mem::take(&mut self.frame_graph_executor);
let (result, execution) = {
let mut frame_encoder =
executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
let initial_pass_count = frame_encoder.recorded_pass_count();
let result = self.render_graph_recorded(
text_state,
surface_view,
graph,
width,
height,
root_scale,
&mut frame_encoder,
);
let execution =
if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
Some(frame_encoder.finish())
} else {
None
};
(result, execution)
};
self.frame_graph_executor = executor;
if let Some(execution) = execution {
self.frame_stats.record_command_stats(execution.stats);
}
result
}
}
#[allow(clippy::too_many_arguments)]
fn render_graph_recorded<C: FrameCommandRecorder>(
&mut self,
text_state: &mut TextSystemState,
surface_view: &wgpu::TextureView,
graph: &RenderGraph,
width: u32,
height: u32,
root_scale: f32,
frame_encoder: &mut C,
) -> Result<(), String> {
self.layer_surface_rect_cache.clear();
self.layer_surface_requirements_cache.clear();
let direct_root = if root_can_render_directly_cached(
&graph.root,
&mut self.layer_surface_requirements_cache,
) {
let collected = collect_layer_contents_with_translation_context_and_text_layout(
&graph.root,
text_state,
None,
None,
TranslationRenderContext::default(),
&mut self.layer_surface_rect_cache,
&mut self.layer_surface_requirements_cache,
);
if !scene_has_layer_events(&collected.scene) {
Some(collected)
} else {
None
}
} else {
None
};
let mut backend = RecordingSurfaceBackend {
renderer: self,
recorder: frame_encoder,
};
if let Some(collected) = direct_root {
return execute_render_root_direct(
&mut backend,
text_state,
surface_view,
collected,
width,
height,
root_scale,
);
}
let viewport_rect = Rect {
x: 0.0,
y: 0.0,
width: width as f32 / root_scale,
height: height as f32 / root_scale,
};
let root_surface = execute_render_layer_surface(
&mut backend,
text_state,
&graph.root,
LayerSurfaceRequest {
root_scale,
backdrop_underlay: None,
allow_runtime_cache: false,
logical_rect_override: Some(viewport_rect),
capture_clip_override: None,
activates_nested_capture: false,
translation_context: TranslationRenderContext::default(),
},
)?;
let root_quad = graph
.root
.transform_to_parent
.map_rect(root_surface.logical_rect);
let root_dest_quad = scaled_quad(root_quad, root_scale);
let needs_root_composite_target =
graph.root.backdrop().is_some() || graph.root.graphics_layer.shadow_elevation > 0.0;
if needs_root_composite_target {
let composite_target = backend.acquire_frame_surface(width, height);
backend.clear_target_view_with_load_op(
&composite_target.view,
wgpu::LoadOp::Clear(CLEAR_COLOR),
);
if let Some(backdrop) = graph.root.backdrop() {
execute_apply_backdrop_layer_to_target(
&mut backend,
&composite_target,
&BackdropLayer {
rect: quad_bounds(
graph
.root
.transform_to_parent
.map_rect(graph.root.local_bounds),
),
clip: graph
.root
.clip_rect()
.map(|clip| quad_bounds(graph.root.transform_to_parent.map_rect(clip))),
effect: backdrop.clone(),
z_index: 0,
},
None,
width,
height,
root_scale,
)?;
}
let mut root_shadow_scene = CompositorScene::new();
let root_shadow_clip = graph
.root
.shadow_clip
.map(|clip| quad_bounds(graph.root.transform_to_parent.map_rect(clip)));
push_layer_shadow(
&mut root_shadow_scene,
&graph.root.graphics_layer,
graph.root.local_bounds,
quad_bounds(
graph
.root
.transform_to_parent
.map_rect(graph.root.local_bounds),
),
root_shadow_clip,
);
for shadow in &root_shadow_scene.shadow_draws {
backend.render_shadow_draw(
text_state,
&composite_target.view,
shadow,
width,
height,
root_scale,
);
}
let composite_dest_quad =
snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
execute_composite_surface_to_view(
&mut backend,
root_surface.target.target(),
&composite_target.view,
(width, height),
composite_dest_quad,
root_surface.composite_alpha,
wgpu::LoadOp::Load,
None,
root_surface.blend_mode,
root_surface.sample_mode,
)?;
backend.composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
&composite_target,
surface_view,
1.0,
wgpu::LoadOp::Clear(CLEAR_COLOR),
None,
None,
BlendMode::SrcOver,
None,
CompositeSampleMode::Linear,
);
backend.release_frame_surface(composite_target);
} else {
let composite_dest_quad =
snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
execute_composite_surface_to_view(
&mut backend,
root_surface.target.target(),
surface_view,
(width, height),
composite_dest_quad,
root_surface.composite_alpha,
wgpu::LoadOp::Clear(CLEAR_COLOR),
None,
root_surface.blend_mode,
root_surface.sample_mode,
)?;
}
backend.release_layer_surface_target(root_surface.target);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn encode_non_effect_segment_commands<C: FrameCommandRecorder>(
&mut self,
text_state: &mut TextSystemState,
frame_encoder: &mut C,
target_view: &wgpu::TextureView,
ordered_items: &[(usize, SegmentDrawItem)],
shapes: &[DrawShape],
images: &[ImageDraw],
texts: &[TextDraw],
shadow_draws: &[ShadowDraw],
initial_load_op: wgpu::LoadOp<wgpu::Color>,
width: u32,
height: u32,
root_scale: f32,
) -> Result<SegmentCommandEncodeOutcome, String> {
let mut first_batch = true;
for command in SegmentCommandIter::new(ordered_items, shapes, images) {
match command {
SegmentRenderCommand::DrawChunk(chunk) => {
let load_op = if first_batch {
initial_load_op
} else {
wgpu::LoadOp::Load
};
let outcome = self.render_segment_draw_chunk(
text_state,
frame_encoder,
target_view,
ordered_items,
shapes,
images,
texts,
chunk,
width,
height,
root_scale,
load_op,
)?;
if outcome.rendered_any {
frame_encoder.record_passes(outcome.pass_count);
first_batch = false;
}
}
SegmentRenderCommand::Shadow(index) => {
if first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
{
let _clear = frame_encoder.encoder().begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("Shadow Pre-Clear"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: initial_load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
);
}
frame_encoder.record_pass();
first_batch = false;
}
let pass_count_before = frame_encoder.recorded_pass_count();
self.encode_shadow_draw(
text_state,
frame_encoder,
target_view,
&shadow_draws[index],
width,
height,
root_scale,
);
if frame_encoder.recorded_pass_count() > pass_count_before {
first_batch = false;
}
}
}
}
Ok(SegmentCommandEncodeOutcome { first_batch })
}
#[allow(clippy::too_many_arguments)]
fn render_segment_draw_chunk<C: FrameCommandRecorder>(
&mut self,
_text_state: &mut TextSystemState,
frame_encoder: &mut C,
target_view: &wgpu::TextureView,
ordered_items: &[(usize, SegmentDrawItem)],
shapes: &[DrawShape],
images: &[ImageDraw],
texts: &[TextDraw],
chunk: SegmentDrawChunkPlan,
width: u32,
height: u32,
root_scale: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
) -> Result<SegmentRenderOutcome, String> {
let mut staged_uploads = self.take_staged_uploads();
let result = (|| {
let mut rendered_any = false;
let mut pass_count = 0_u32;
let mut next_load_op = load_op;
for batch in chunk.iter() {
staged_uploads.clear();
match batch {
SegmentBatchPlan::Shape {
start,
end,
blend_mode,
} => {
let slice = &ordered_items[start..end];
if slice.len() > MAX_SHAPES_PER_BATCH {
return Err(format!(
"shape batch contains {} shapes, exceeding the renderer limit of {}",
slice.len(),
MAX_SHAPES_PER_BATCH
));
}
let viewport = ViewportUniformParams {
width,
height,
offset: [0.0, 0.0],
};
for (_, item) in slice {
if !matches!(item, SegmentDrawItem::Shape(_)) {
return Err(format!(
"shape batch contains non-shape draw item: {item:?}"
));
}
}
let Some(prepared) = self.prepare_shapes_batch(
slice.iter().filter_map(|(_, item)| match item {
SegmentDrawItem::Shape(shape_index) => Some(&shapes[*shape_index]),
_ => None,
}),
root_scale,
viewport,
&mut staged_uploads,
) else {
continue;
};
let upload_offset = frame_encoder
.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
self.flush_staged_uploads_at(
frame_encoder.encoder(),
&staged_uploads,
upload_offset,
);
{
let mut render_pass = frame_encoder.encoder().begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("Segment Shape Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: next_load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
);
self.draw_prepared_shapes(&mut render_pass, blend_mode, prepared);
}
pass_count = pass_count.saturating_add(1);
rendered_any = true;
next_load_op = wgpu::LoadOp::Load;
}
SegmentBatchPlan::Image {
start,
end,
blend_mode,
} => {
let viewport = ViewportUniformParams {
width,
height,
offset: [0.0, 0.0],
};
for (_, item) in &ordered_items[start..end] {
if !matches!(item, SegmentDrawItem::Image(_)) {
return Err(format!(
"image batch contains non-image draw item: {item:?}"
));
}
}
let prepared_images = self.prepare_image_draw_cmds(
ordered_items[start..end]
.iter()
.filter_map(|(_, item)| match item {
SegmentDrawItem::Image(image_index) => {
Some(&images[*image_index])
}
_ => None,
}),
viewport,
root_scale,
&mut staged_uploads,
)?;
if prepared_images.is_empty() {
self.scratch_image_cmds = prepared_images.into_cmds();
continue;
}
let upload_offset = frame_encoder
.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
self.flush_staged_uploads_at(
frame_encoder.encoder(),
&staged_uploads,
upload_offset,
);
let draw_result = {
let mut render_pass = frame_encoder.encoder().begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("Segment Image Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: next_load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
);
self.draw_prepared_images(
&mut render_pass,
&prepared_images,
blend_mode,
)
};
pass_count = pass_count.saturating_add(1);
self.scratch_image_cmds = prepared_images.into_cmds();
draw_result?;
rendered_any = true;
next_load_op = wgpu::LoadOp::Load;
}
SegmentBatchPlan::Text { start, end } => {
let viewport = ViewportUniformParams {
width,
height,
offset: [0.0, 0.0],
};
for (_, item) in &ordered_items[start..end] {
if !matches!(item, SegmentDrawItem::Text(_)) {
return Err(format!(
"text batch contains non-text draw item: {item:?}"
));
}
}
let prepared_images = self.prepare_text_image_draw_cmds(
ordered_items[start..end]
.iter()
.filter_map(|(_, item)| match item {
SegmentDrawItem::Text(text_index) => Some(&texts[*text_index]),
_ => None,
}),
viewport,
root_scale,
&mut staged_uploads,
)?;
if prepared_images.is_empty() {
self.scratch_image_cmds = prepared_images.into_cmds();
continue;
}
let upload_offset = frame_encoder
.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
self.flush_staged_uploads_at(
frame_encoder.encoder(),
&staged_uploads,
upload_offset,
);
{
let mut render_pass = frame_encoder.encoder().begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("Segment Text Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: next_load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
);
self.draw_prepared_images(
&mut render_pass,
&prepared_images,
BlendMode::SrcOver,
)?;
}
self.frame_stats.bump_text();
pass_count = pass_count.saturating_add(1);
self.scratch_image_cmds = prepared_images.into_cmds();
rendered_any = true;
next_load_op = wgpu::LoadOp::Load;
}
}
}
Ok(SegmentRenderOutcome {
rendered_any,
pass_count,
})
})();
self.restore_staged_uploads(staged_uploads);
result
}
fn viewport_uniforms(params: ViewportUniformParams) -> Uniforms {
Uniforms {
viewport: [params.width as f32, params.height as f32],
viewport_offset: params.offset,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn stage_viewport_uniforms(
&self,
staged_uploads: &mut StagedBufferUploads,
params: ViewportUniformParams,
) {
let uniforms = Self::viewport_uniforms(params);
staged_uploads.stage(UploadTarget::Uniform, bytemuck::bytes_of(&uniforms));
}
#[cfg(target_arch = "wasm32")]
fn prepare_wasm_viewport_uniforms(&mut self, params: ViewportUniformParams) -> usize {
let slot = self.claim_wasm_uniform_batch();
let uniforms = Self::viewport_uniforms(params);
let bytes = bytemuck::bytes_of(&uniforms);
let upload_stats = self.frame_graph_executor.upload_buffer(
&self.queue,
&self.wasm_uniform_batches[slot].buffer,
0,
bytes,
);
self.frame_stats.record_command_stats(upload_stats);
slot
}
#[cfg(target_arch = "wasm32")]
fn claim_wasm_uniform_batch(&mut self) -> usize {
let slot = self.wasm_uniform_batch_cursor;
self.wasm_uniform_batch_cursor += 1;
while self.wasm_uniform_batches.len() <= slot {
self.wasm_uniform_batches.push(UniformBatchBuffer::new(
&self.device,
&self.uniform_bind_group_layout,
));
}
slot
}
#[cfg(target_arch = "wasm32")]
fn claim_wasm_shape_batch(&mut self) -> usize {
let slot = self.wasm_shape_batch_cursor;
self.wasm_shape_batch_cursor += 1;
while self.wasm_shape_batches.len() <= slot {
self.wasm_shape_batches.push(ShapeBatchBuffers::new(
&self.device,
&self.shape_bind_group_layout,
));
}
slot
}
#[cfg(target_arch = "wasm32")]
fn claim_wasm_image_batch(&mut self) -> usize {
let slot = self.wasm_image_batch_cursor;
self.wasm_image_batch_cursor += 1;
while self.wasm_image_batches.len() <= slot {
self.wasm_image_batches
.push(ImageBatchBuffers::new(&self.device));
}
slot
}
#[cfg(target_arch = "wasm32")]
fn write_wasm_buffer(&self, buffer: &wgpu::Buffer, bytes: &[u8]) {
let upload_stats = self
.frame_graph_executor
.upload_buffer(&self.queue, buffer, 0, bytes);
self.frame_stats.record_command_stats(upload_stats);
}
fn take_staged_uploads(&mut self) -> StagedBufferUploads {
let mut staged_uploads = std::mem::take(&mut self.staged_uploads);
debug_assert!(
staged_uploads.is_empty(),
"renderer-owned staged uploads should be restored as empty scratch storage"
);
staged_uploads.clear();
staged_uploads
}
fn restore_staged_uploads(&mut self, mut staged_uploads: StagedBufferUploads) {
staged_uploads.clear();
self.staged_uploads = staged_uploads;
}
#[cfg(not(target_arch = "wasm32"))]
fn ensure_upload_buffer_capacity(&mut self, required_bytes: u64) {
if required_bytes <= self.upload_buffer.size() {
return;
}
let new_size = required_bytes
.next_power_of_two()
.max(INITIAL_UPLOAD_BUFFER_BYTES);
self.upload_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Frame Upload Buffer"),
size: new_size,
usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
fn flush_staged_uploads_at(
&mut self,
encoder: &mut wgpu::CommandEncoder,
staged_uploads: &StagedBufferUploads,
upload_buffer_offset: u64,
) {
if staged_uploads.is_empty() {
return;
}
debug_assert_eq!(
upload_buffer_offset % wgpu::COPY_BUFFER_ALIGNMENT,
0,
"upload-buffer base offset must satisfy copy alignment"
);
#[cfg(target_arch = "wasm32")]
{
let _ = upload_buffer_offset;
let _ = encoder;
debug_assert!(
staged_uploads.is_empty(),
"wasm draw uploads use retained per-batch resource slots"
);
return;
}
#[cfg(not(target_arch = "wasm32"))]
{
self.ensure_upload_buffer_capacity(
upload_buffer_offset + staged_uploads.bytes.len() as u64,
);
let upload_stats = self.frame_graph_executor.upload_buffer(
&self.queue,
&self.upload_buffer,
upload_buffer_offset,
&staged_uploads.bytes,
);
self.frame_stats.record_command_stats(upload_stats);
for copy in &staged_uploads.copies {
let target_buffer = match copy.target {
UploadTarget::Uniform => &self.uniform_buffer,
UploadTarget::ShapeVertex => &self.shape_buffers.vertex_buffer,
UploadTarget::ShapeIndex => &self.shape_buffers.index_buffer,
UploadTarget::ShapeData => &self.shape_buffers.shape_buffer,
UploadTarget::ShapeGradient => &self.shape_buffers.gradient_buffer,
UploadTarget::ImageVertex => &self.image_vertex_buffer,
UploadTarget::ImageIndex => &self.image_index_buffer,
};
encoder.copy_buffer_to_buffer(
&self.upload_buffer,
upload_buffer_offset + copy.source_offset,
target_buffer,
0,
copy.size,
);
}
}
}
#[allow(clippy::too_many_arguments)]
fn encode_shadow_draw<C: FrameCommandRecorder>(
&mut self,
_text_state: &mut TextSystemState,
frame_encoder: &mut C,
target_view: &wgpu::TextureView,
shadow: &ShadowDraw,
width: u32,
height: u32,
root_scale: f32,
) {
if shadow.shapes.is_empty() && shadow.texts.is_empty() {
return;
}
let shape_bounds_opt = shadow
.shapes
.iter()
.map(|(shape, _)| shape.rect)
.reduce(|a, b| Rect {
x: a.x.min(b.x),
y: a.y.min(b.y),
width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
});
let text_bounds_opt = shadow
.texts
.iter()
.map(|text| text.rect)
.reduce(|a, b| Rect {
x: a.x.min(b.x),
y: a.y.min(b.y),
width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
});
let combined_bounds = match (shape_bounds_opt, text_bounds_opt) {
(Some(s), Some(t)) => Some(Rect {
x: s.x.min(t.x),
y: s.y.min(t.y),
width: (s.x + s.width).max(t.x + t.width) - s.x.min(t.x),
height: (s.y + s.height).max(t.y + t.height) - s.y.min(t.y),
}),
(Some(s), None) => Some(s),
(None, Some(t)) => Some(t),
(None, None) => None,
};
let Some(shape_bounds) = combined_bounds else {
return;
};
let blur_margin = blur_extent_margin(shadow.blur_radius);
let mut blur_bounds = Rect {
x: shape_bounds.x - blur_margin,
y: shape_bounds.y - blur_margin,
width: shape_bounds.width + blur_margin * 2.0,
height: shape_bounds.height + blur_margin * 2.0,
};
if let Some(clip) = shadow.clip {
let clip_expanded = Rect {
x: clip.x - blur_margin,
y: clip.y - blur_margin,
width: clip.width + blur_margin * 2.0,
height: clip.height + blur_margin * 2.0,
};
let Some(intersection) = blur_bounds.intersect(clip_expanded) else {
return;
};
blur_bounds = intersection;
}
let processing_scissor = scissor_rect_for_rect(blur_bounds, root_scale, width, height);
if processing_scissor.is_none() {
return;
}
if shadow.blur_radius <= 0.0 {
for (shape, blend_mode) in &shadow.shapes {
self.encode_shapes_pass(
frame_encoder,
target_view,
std::iter::once(shape),
*blend_mode,
width,
height,
root_scale,
wgpu::LoadOp::Load,
[0.0, 0.0],
);
frame_encoder.record_pass();
}
if !shadow.texts.is_empty() {
let mut staged_uploads = self.take_staged_uploads();
let viewport = ViewportUniformParams {
width,
height,
offset: [0.0, 0.0],
};
match self.prepare_text_image_draw_cmds(
shadow.texts.iter(),
viewport,
root_scale,
&mut staged_uploads,
) {
Ok(prepared_images) if !prepared_images.is_empty() => {
let upload_offset = frame_encoder
.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
self.flush_staged_uploads_at(
frame_encoder.encoder(),
&staged_uploads,
upload_offset,
);
let draw_result = {
let mut render_pass = frame_encoder.encoder().begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("Zero Blur Shadow Text Image Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
);
self.draw_prepared_images(
&mut render_pass,
&prepared_images,
BlendMode::SrcOver,
)
};
self.scratch_image_cmds = prepared_images.into_cmds();
if let Err(e) = draw_result {
eprintln!("Failed to draw text for zero-blur shadow: {}", e);
} else {
self.frame_stats.bump_text();
frame_encoder.record_pass();
}
}
Ok(prepared_images) => {
self.scratch_image_cmds = prepared_images.into_cmds();
}
Err(e) => {
eprintln!("Failed to prepare text image for zero-blur shadow: {}", e);
}
}
self.restore_staged_uploads(staged_uploads);
}
return;
}
let Some(device_bounds) =
device_pixel_bounds_for_rect(blur_bounds, width, height, root_scale)
else {
return;
};
let bounds_x = device_bounds.x;
let bounds_y = device_bounds.y;
let bounds_w = device_bounds.width;
let bounds_h = device_bounds.height;
let pixel_radius = shadow.blur_radius * root_scale;
if shadow.texts.is_empty()
&& !shadow.shapes.is_empty()
&& self.encode_shape_only_blurred_shadow_draw(
frame_encoder,
target_view,
shadow,
device_bounds,
pixel_radius,
processing_scissor,
width,
height,
root_scale,
)
{
return;
}
let device = self.device.clone();
let source_descriptor =
self.transient_offscreen_descriptor("Shadow Source", bounds_w, bounds_h);
let source = frame_encoder.acquire_transient_offscreen(&device, source_descriptor);
let viewport_offset = [bounds_x, bounds_y];
let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
let source_outcome = self.encode_shadow_shape_source_passes(
frame_encoder,
&source.view,
&shadow.shapes,
bounds_w,
bounds_h,
viewport_offset,
root_scale,
&mut next_load_op,
);
frame_encoder.record_passes(source_outcome.pass_count);
let mut rendered_any = source_outcome.rendered_any;
if !shadow.texts.is_empty() {
let mut shifted_texts = shadow.texts.clone();
for text in &mut shifted_texts {
text.rect.x -= viewport_offset[0] / root_scale;
text.rect.y -= viewport_offset[1] / root_scale;
if let Some(clip) = text.clip.as_mut() {
clip.x -= viewport_offset[0] / root_scale;
clip.y -= viewport_offset[1] / root_scale;
}
}
let mut staged_uploads = self.take_staged_uploads();
let viewport = ViewportUniformParams {
width: bounds_w,
height: bounds_h,
offset: [0.0, 0.0],
};
match self.prepare_text_image_draw_cmds(
shifted_texts.iter(),
viewport,
root_scale,
&mut staged_uploads,
) {
Ok(prepared_images) if !prepared_images.is_empty() => {
let upload_offset = frame_encoder
.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
self.flush_staged_uploads_at(
frame_encoder.encoder(),
&staged_uploads,
upload_offset,
);
let draw_result = {
let mut render_pass = frame_encoder.encoder().begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("Shadow Source Text Image Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &source.view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: next_load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
);
self.draw_prepared_images(
&mut render_pass,
&prepared_images,
BlendMode::SrcOver,
)
};
self.scratch_image_cmds = prepared_images.into_cmds();
if let Err(e) = draw_result {
eprintln!("Failed to draw text for shadow: {}", e);
} else {
self.frame_stats.bump_text();
frame_encoder.record_pass();
rendered_any = true;
}
}
Ok(prepared_images) => {
self.scratch_image_cmds = prepared_images.into_cmds();
}
Err(e) => {
eprintln!("Failed to prepare text image for shadow: {}", e);
}
}
self.restore_staged_uploads(staged_uploads);
}
if !rendered_any {
frame_encoder.release_transient_offscreen(source_descriptor, source);
return;
}
let scratch_descriptor =
self.transient_offscreen_descriptor("Shadow Blur Scratch", bounds_w, bounds_h);
let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
{
self.effect_renderer.encode_blur_scissored_ping_pong_passes(
frame_encoder,
&device,
&source,
&scratch,
&source.view,
pixel_radius,
pixel_radius,
TileMode::Decal,
None, );
}
frame_encoder.record_passes(2);
let clip_scissor = shadow
.clip
.and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
let scissor = clip_scissor.or(processing_scissor);
let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
mask.rect[0] -= viewport_offset[0];
mask.rect[1] -= viewport_offset[1];
mask
});
let dest_viewport = Some((
viewport_offset[0],
viewport_offset[1],
bounds_w as f32,
bounds_h as f32,
));
{
self.effect_renderer
.encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
frame_encoder,
&device,
&source,
target_view,
1.0,
wgpu::LoadOp::Load,
scissor,
rounded_mask,
BlendMode::SrcOver,
dest_viewport,
CompositeSampleMode::Linear,
);
}
frame_encoder.record_pass();
self.effect_renderer.record_blur_pass();
self.effect_renderer.record_composite_pass();
frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
frame_encoder.release_transient_offscreen(source_descriptor, source);
}
#[allow(clippy::too_many_arguments)]
fn encode_shadow_shape_source_passes<C: FrameCommandRecorder>(
&mut self,
frame_encoder: &mut C,
source_view: &wgpu::TextureView,
shapes: &[(DrawShape, BlendMode)],
width: u32,
height: u32,
viewport_offset: [f32; 2],
root_scale: f32,
next_load_op: &mut wgpu::LoadOp<wgpu::Color>,
) -> ShadowSourceRenderOutcome {
if shapes.is_empty() {
return ShadowSourceRenderOutcome {
rendered_any: false,
pass_count: 0,
};
}
let mut staged_uploads = self.take_staged_uploads();
let mut rendered_any = false;
let mut pass_count = 0_u32;
let mut start = 0usize;
while start < shapes.len() {
let blend_mode = supported_blend_mode(shapes[start].1);
let mut end = start + 1;
while end < shapes.len()
&& end - start < MAX_SHAPES_PER_BATCH
&& supported_blend_mode(shapes[end].1) == blend_mode
{
end += 1;
}
staged_uploads.clear();
let viewport = ViewportUniformParams {
width,
height,
offset: viewport_offset,
};
let Some(prepared_shape) = self.prepare_shapes_batch(
shapes[start..end].iter().map(|(shape, _blend_mode)| shape),
root_scale,
viewport,
&mut staged_uploads,
) else {
start = end;
continue;
};
let upload_offset =
frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
{
let mut render_pass =
frame_encoder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Shadow Source Shape Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: source_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: *next_load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.draw_prepared_shapes(&mut render_pass, blend_mode, prepared_shape);
}
pass_count = pass_count.saturating_add(1);
rendered_any = true;
*next_load_op = wgpu::LoadOp::Load;
start = end;
}
self.restore_staged_uploads(staged_uploads);
ShadowSourceRenderOutcome {
rendered_any,
pass_count,
}
}
#[allow(clippy::too_many_arguments)]
fn encode_shape_only_blurred_shadow_draw<C: FrameCommandRecorder>(
&mut self,
frame_encoder: &mut C,
target_view: &wgpu::TextureView,
shadow: &ShadowDraw,
device_bounds: DevicePixelBounds,
pixel_radius: f32,
processing_scissor: Option<(u32, u32, u32, u32)>,
width: u32,
height: u32,
root_scale: f32,
) -> bool {
let bounds_w = device_bounds.width;
let bounds_h = device_bounds.height;
let viewport_offset = [device_bounds.x, device_bounds.y];
let cache_key =
(root_scale.is_finite() && root_scale > 0.0).then(|| ShadowSurfaceCacheKey {
content_hash: shape_shadow_content_hash(
&shadow.shapes,
viewport_offset,
root_scale,
),
pixel_size: [bounds_w, bounds_h],
root_scale_bits: root_scale.to_bits(),
blur_radius_bits: pixel_radius.to_bits(),
});
if let Some(key) = cache_key {
if let Some(cached) = self.cached_shadow_surface(&key) {
let clip_scissor = shadow
.clip
.and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
let scissor = clip_scissor.or(processing_scissor);
let rounded_mask =
inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
mask.rect[0] -= viewport_offset[0];
mask.rect[1] -= viewport_offset[1];
mask
});
let dest_viewport = Some((
viewport_offset[0],
viewport_offset[1],
bounds_w as f32,
bounds_h as f32,
));
{
self.effect_renderer
.encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
frame_encoder,
&self.device,
&cached,
target_view,
1.0,
wgpu::LoadOp::Load,
scissor,
rounded_mask,
BlendMode::SrcOver,
dest_viewport,
CompositeSampleMode::Linear,
);
}
frame_encoder.record_pass();
self.effect_renderer.record_composite_pass();
return true;
}
}
let device = self.device.clone();
let source_descriptor =
self.transient_offscreen_descriptor("Shape Shadow Source", bounds_w, bounds_h);
let source_is_cacheable = cache_key.is_some();
let source = if source_is_cacheable {
self.acquire_retained_surface(bounds_w, bounds_h)
} else {
frame_encoder.acquire_transient_offscreen(&device, source_descriptor)
};
let scratch_descriptor =
self.transient_offscreen_descriptor("Shape Shadow Blur Scratch", bounds_w, bounds_h);
let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
let source_outcome = self.encode_shadow_shape_source_passes(
frame_encoder,
&source.view,
&shadow.shapes,
bounds_w,
bounds_h,
viewport_offset,
root_scale,
&mut next_load_op,
);
frame_encoder.record_passes(source_outcome.pass_count);
if !source_outcome.rendered_any {
frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
if source_is_cacheable {
self.defer_offscreen_release(source);
} else {
frame_encoder.release_transient_offscreen(source_descriptor, source);
}
return true;
}
{
self.effect_renderer.encode_blur_scissored_ping_pong_passes(
frame_encoder,
&device,
&source,
&scratch,
&source.view,
pixel_radius,
pixel_radius,
TileMode::Decal,
None,
);
}
frame_encoder.record_passes(2);
let clip_scissor = shadow
.clip
.and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
let scissor = clip_scissor.or(processing_scissor);
let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
mask.rect[0] -= viewport_offset[0];
mask.rect[1] -= viewport_offset[1];
mask
});
let dest_viewport = Some((
viewport_offset[0],
viewport_offset[1],
bounds_w as f32,
bounds_h as f32,
));
{
self.effect_renderer
.encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
frame_encoder,
&device,
&source,
target_view,
1.0,
wgpu::LoadOp::Load,
scissor,
rounded_mask,
BlendMode::SrcOver,
dest_viewport,
CompositeSampleMode::Linear,
);
}
frame_encoder.record_pass();
self.effect_renderer.record_blur_pass();
self.effect_renderer.record_composite_pass();
frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
if let Some(key) = cache_key {
self.insert_cached_shadow_surface(key, source);
} else {
frame_encoder.release_transient_offscreen(source_descriptor, source);
}
true
}
fn prepare_shapes_batch<'a, I>(
&mut self,
layer_shapes: I,
root_scale: f32,
viewport: ViewportUniformParams,
staged_uploads: &mut StagedBufferUploads,
) -> Option<PreparedShapeBatch>
where
I: Iterator<Item = &'a DrawShape>,
{
#[cfg(target_arch = "wasm32")]
let _ = staged_uploads;
self.scratch_shape_data.clear();
self.scratch_gradients.clear();
self.scratch_vertices.clear();
self.scratch_indices.clear();
for (idx, shape) in layer_shapes.take(MAX_SHAPES_PER_BATCH).enumerate() {
let snap_delta = shape
.snap_anchor
.map(|anchor| snap_delta_for_anchor(anchor, root_scale))
.unwrap_or_default();
let local_rect = shape.local_rect.translate(snap_delta.x, snap_delta.y);
let quad = translate_quad(shape.quad, snap_delta);
let clip = shape
.clip
.map(|clip| clip.translate(snap_delta.x, snap_delta.y));
let clip_rect = if let Some(clip) = clip {
[
clip.x * root_scale,
clip.y * root_scale,
clip.width * root_scale,
clip.height * root_scale,
]
} else {
[0.0, 0.0, 0.0, 0.0]
};
let mut gradient_params = [0.0f32; 4];
let mut push_gradient_entries = |colors: &[Color], stops: Option<&[f32]>| {
let start = self.scratch_gradients.len() as u32;
let count = colors.len();
let explicit_stops = stops.filter(|values| values.len() == count);
for (index, color) in colors.iter().enumerate() {
let position =
explicit_stops
.map(|values| values[index])
.unwrap_or_else(|| {
if count <= 1 {
0.0
} else {
index as f32 / (count - 1) as f32
}
});
self.scratch_gradients.push(GradientStop {
color: [color.r(), color.g(), color.b(), color.a()],
position: [position, 0.0, 0.0, 0.0],
});
}
(start, count as u32)
};
let (brush_type, gradient_start, gradient_count, gradient_tile_mode) = match &shape
.brush
{
Brush::Solid(_) => (0u32, 0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
Brush::LinearGradient {
colors,
stops,
start,
end,
tile_mode,
} => {
let (start_idx, count) = push_gradient_entries(colors, stops.as_deref());
gradient_params = [
resolve_gradient_point(
local_rect.x * root_scale,
local_rect.width * root_scale,
start.x * root_scale,
),
resolve_gradient_point(
local_rect.y * root_scale,
local_rect.height * root_scale,
start.y * root_scale,
),
resolve_gradient_point(
local_rect.x * root_scale,
local_rect.width * root_scale,
end.x * root_scale,
),
resolve_gradient_point(
local_rect.y * root_scale,
local_rect.height * root_scale,
end.y * root_scale,
),
];
(1u32, start_idx, count, gradient_tile_mode_value(*tile_mode))
}
Brush::RadialGradient {
colors,
stops,
center,
radius,
tile_mode,
} => {
let (start_idx, count) = push_gradient_entries(colors, stops.as_deref());
gradient_params = [
local_rect.x * root_scale + center.x * root_scale,
local_rect.y * root_scale + center.y * root_scale,
(radius * root_scale).max(f32::EPSILON),
0.0,
];
(2u32, start_idx, count, gradient_tile_mode_value(*tile_mode))
}
Brush::SweepGradient {
colors,
stops,
center,
} => {
let (start_idx, count) = push_gradient_entries(colors, stops.as_deref());
gradient_params = [
local_rect.x * root_scale + center.x * root_scale,
local_rect.y * root_scale + center.y * root_scale,
0.0,
0.0,
];
(
3u32,
start_idx,
count,
gradient_tile_mode_value(TileMode::Clamp),
)
}
};
let radii = if let Some(rounded) = shape.shape {
let resolved = rounded.resolve(local_rect.width, local_rect.height);
[
resolved.top_left * root_scale,
resolved.top_right * root_scale,
resolved.bottom_left * root_scale,
resolved.bottom_right * root_scale,
]
} else {
[0.0, 0.0, 0.0, 0.0]
};
let device_rect = [
local_rect.x * root_scale,
local_rect.y * root_scale,
local_rect.width * root_scale,
local_rect.height * root_scale,
];
self.scratch_shape_data.push(ShapeData {
rect: device_rect,
radii,
gradient_params,
clip_rect,
brush_type,
gradient_start,
gradient_count,
gradient_tile_mode,
});
let base_vertex = (idx * 4) as u32;
let color = match &shape.brush {
Brush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
Brush::LinearGradient { colors, .. } => {
let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
[first.r(), first.g(), first.b(), first.a()]
}
Brush::RadialGradient { colors, .. } | Brush::SweepGradient { colors, .. } => {
let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
[first.r(), first.g(), first.b(), first.a()]
}
};
let vertices = [
[quad[0][0] * root_scale, quad[0][1] * root_scale],
[quad[1][0] * root_scale, quad[1][1] * root_scale],
[quad[2][0] * root_scale, quad[2][1] * root_scale],
[quad[3][0] * root_scale, quad[3][1] * root_scale],
];
self.scratch_vertices.extend_from_slice(&[
Vertex {
position: vertices[0],
color,
uv: [0.0, 0.0],
uv_bounds: [0.0, 0.0, 1.0, 1.0],
},
Vertex {
position: vertices[1],
color,
uv: [1.0, 0.0],
uv_bounds: [0.0, 0.0, 1.0, 1.0],
},
Vertex {
position: vertices[2],
color,
uv: [0.0, 1.0],
uv_bounds: [0.0, 0.0, 1.0, 1.0],
},
Vertex {
position: vertices[3],
color,
uv: [1.0, 1.0],
uv_bounds: [0.0, 0.0, 1.0, 1.0],
},
]);
self.scratch_indices.extend_from_slice(&[
base_vertex,
base_vertex + 1,
base_vertex + 2,
base_vertex + 2,
base_vertex + 1,
base_vertex + 3,
]);
}
if self.scratch_vertices.is_empty() {
return None;
}
let shape_count = self.scratch_shape_data.len();
if shape_count == 0 {
return None;
}
#[cfg(not(target_arch = "wasm32"))]
{
self.shape_buffers.ensure_capacity(
&self.device,
&self.shape_bind_group_layout,
shape_count * 4,
shape_count * 6,
shape_count,
self.scratch_gradients.len().max(1),
);
self.stage_viewport_uniforms(staged_uploads, viewport);
staged_uploads.stage(
UploadTarget::ShapeVertex,
bytemuck::cast_slice(&self.scratch_vertices),
);
staged_uploads.stage(
UploadTarget::ShapeIndex,
bytemuck::cast_slice(&self.scratch_indices),
);
staged_uploads.stage(
UploadTarget::ShapeData,
bytemuck::cast_slice(&self.scratch_shape_data),
);
if !self.scratch_gradients.is_empty() {
staged_uploads.stage(
UploadTarget::ShapeGradient,
bytemuck::cast_slice(&self.scratch_gradients),
);
}
}
#[cfg(target_arch = "wasm32")]
let shape_slot = {
let slot = self.claim_wasm_shape_batch();
{
let buffers = &mut self.wasm_shape_batches[slot];
buffers.ensure_capacity(
&self.device,
&self.shape_bind_group_layout,
shape_count * 4,
shape_count * 6,
shape_count,
self.scratch_gradients.len().max(1),
);
}
let buffers = &self.wasm_shape_batches[slot];
self.write_wasm_buffer(
&buffers.vertex_buffer,
bytemuck::cast_slice(&self.scratch_vertices),
);
self.write_wasm_buffer(
&buffers.index_buffer,
bytemuck::cast_slice(&self.scratch_indices),
);
self.write_wasm_buffer(
&buffers.shape_buffer,
bytemuck::cast_slice(&self.scratch_shape_data),
);
if !self.scratch_gradients.is_empty() {
self.write_wasm_buffer(
&buffers.gradient_buffer,
bytemuck::cast_slice(&self.scratch_gradients),
);
}
slot
};
#[cfg(target_arch = "wasm32")]
let uniform_slot = self.prepare_wasm_viewport_uniforms(viewport);
Some(PreparedShapeBatch {
index_count: shape_count as u32 * 6,
#[cfg(target_arch = "wasm32")]
shape_slot,
#[cfg(target_arch = "wasm32")]
uniform_slot,
})
}
fn draw_prepared_shapes(
&self,
render_pass: &mut wgpu::RenderPass<'_>,
blend_mode: BlendMode,
batch: PreparedShapeBatch,
) {
self.frame_stats.bump_shapes();
render_pass.set_pipeline(match blend_mode {
BlendMode::DstOut => &self.pipeline_dst_out,
_ => &self.pipeline,
});
#[cfg(not(target_arch = "wasm32"))]
let (uniform_bind_group, shape_buffers) = (&self.uniform_bind_group, &self.shape_buffers);
#[cfg(target_arch = "wasm32")]
let (uniform_bind_group, shape_buffers) = (
&self.wasm_uniform_batches[batch.uniform_slot].bind_group,
&self.wasm_shape_batches[batch.shape_slot],
);
render_pass.set_bind_group(0, uniform_bind_group, &[]);
render_pass.set_bind_group(1, &shape_buffers.bind_group, &[]);
render_pass.set_vertex_buffer(0, shape_buffers.vertex_buffer.slice(..));
render_pass.set_index_buffer(
shape_buffers.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
#[allow(clippy::too_many_arguments)]
fn encode_shapes_pass<'a, I, C: FrameCommandRecorder>(
&mut self,
frame_encoder: &mut C,
target_view: &wgpu::TextureView,
layer_shapes: I,
blend_mode: BlendMode,
width: u32,
height: u32,
root_scale: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
viewport_offset: [f32; 2],
) where
I: Iterator<Item = &'a DrawShape>,
{
let mut staged_uploads = self.take_staged_uploads();
let viewport = ViewportUniformParams {
width,
height,
offset: viewport_offset,
};
let Some(batch) =
self.prepare_shapes_batch(layer_shapes, root_scale, viewport, &mut staged_uploads)
else {
self.restore_staged_uploads(staged_uploads);
return;
};
let upload_offset =
frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
self.restore_staged_uploads(staged_uploads);
let mut render_pass =
frame_encoder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Shape Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.draw_prepared_shapes(&mut render_pass, blend_mode, batch);
}
fn draw_prepared_images(
&mut self,
render_pass: &mut wgpu::RenderPass<'_>,
batch: &PreparedImageBatch,
blend_mode: BlendMode,
) -> Result<(), String> {
if batch.cmds.is_empty() {
return Ok(());
}
self.frame_stats.bump_images();
render_pass.set_pipeline(match blend_mode {
BlendMode::DstOut => &self.image_pipeline_dst_out,
_ => &self.image_pipeline,
});
#[cfg(not(target_arch = "wasm32"))]
let (uniform_bind_group, vertex_buffer, index_buffer) = (
&self.uniform_bind_group,
&self.image_vertex_buffer,
&self.image_index_buffer,
);
#[cfg(target_arch = "wasm32")]
let (uniform_bind_group, vertex_buffer, index_buffer) = (
&self.wasm_uniform_batches[batch.uniform_slot].bind_group,
&self.wasm_image_batches[batch.image_slot].vertex_buffer,
&self.wasm_image_batches[batch.image_slot].index_buffer,
);
render_pass.set_bind_group(0, uniform_bind_group, &[]);
render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
for cmd in &batch.cmds {
let (sx, sy, sw, sh) = cmd.scissor;
render_pass.set_scissor_rect(sx, sy, sw, sh);
let cached = self
.image_texture_cache
.get(&cmd.image_id)
.ok_or_else(|| "image texture missing from cache".to_string())?;
render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
}
Ok(())
}
fn prepare_image_draw_cmds<'a, I>(
&mut self,
layer_images: I,
viewport: ViewportUniformParams,
root_scale: f32,
staged_uploads: &mut StagedBufferUploads,
) -> Result<PreparedImageBatch, String>
where
I: Iterator<Item = &'a ImageDraw>,
{
#[cfg(target_arch = "wasm32")]
let _ = staged_uploads;
let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
image_vertices.clear();
image_indices.clear();
image_cmds.clear();
for image_draw in layer_images {
let snap_delta = image_draw
.snap_anchor
.map(|anchor| snap_delta_for_anchor(anchor, root_scale))
.unwrap_or_default();
let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
continue;
}
let (tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
if tint[3] <= 0.0 {
continue;
}
let prepared_image = if let Some(filter) = cpu_filter {
apply_filter_to_bitmap(&image_draw.image, filter)?
} else {
image_draw.image.clone()
};
self.ensure_image_cached(&prepared_image)?;
let mut adjusted_image = ImageDraw {
rect,
local_rect: image_draw.local_rect.translate(snap_delta.x, snap_delta.y),
quad: translate_quad(image_draw.quad, snap_delta),
snap_anchor: image_draw.snap_anchor,
image: image_draw.image.clone(),
alpha: image_draw.alpha,
color_filter: image_draw.color_filter,
sampling: image_draw.sampling,
z_index: image_draw.z_index,
clip: image_draw
.clip
.map(|clip| clip.translate(snap_delta.x, snap_delta.y)),
blend_mode: image_draw.blend_mode,
src_rect: image_draw.src_rect,
motion_context_animated: image_draw.motion_context_animated,
};
snap_nearest_image_to_device_pixels(&mut adjusted_image, root_scale);
let scissor = scissor_rect_for_image(
&adjusted_image,
root_scale,
viewport.width,
viewport.height,
);
let Some(scissor) = scissor else {
continue;
};
let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
continue;
};
let device_quad = nearest_image_device_quad(&adjusted_image, root_scale).unwrap_or([
[
adjusted_image.quad[0][0] * root_scale,
adjusted_image.quad[0][1] * root_scale,
],
[
adjusted_image.quad[1][0] * root_scale,
adjusted_image.quad[1][1] * root_scale,
],
[
adjusted_image.quad[2][0] * root_scale,
adjusted_image.quad[2][1] * root_scale,
],
[
adjusted_image.quad[3][0] * root_scale,
adjusted_image.quad[3][1] * root_scale,
],
]);
let base_vertex = image_vertices.len() as u32;
let index_start = image_indices.len() as u32;
image_indices.extend_from_slice(&[
base_vertex,
base_vertex + 1,
base_vertex + 2,
base_vertex + 2,
base_vertex + 1,
base_vertex + 3,
]);
image_vertices.extend_from_slice(&[
Vertex {
position: device_quad[0],
color: tint,
uv: [uv_rect.min[0], uv_rect.min[1]],
uv_bounds: uv_rect.sample_bounds,
},
Vertex {
position: device_quad[1],
color: tint,
uv: [uv_rect.max[0], uv_rect.min[1]],
uv_bounds: uv_rect.sample_bounds,
},
Vertex {
position: device_quad[2],
color: tint,
uv: [uv_rect.min[0], uv_rect.max[1]],
uv_bounds: uv_rect.sample_bounds,
},
Vertex {
position: device_quad[3],
color: tint,
uv: [uv_rect.max[0], uv_rect.max[1]],
uv_bounds: uv_rect.sample_bounds,
},
]);
image_cmds.push(ImageDrawCmd {
index_start,
scissor,
image_id: prepared_image.id(),
sampling: image_draw.sampling,
});
}
#[cfg(not(target_arch = "wasm32"))]
if !image_cmds.is_empty() {
self.stage_viewport_uniforms(staged_uploads, viewport);
let needed_bytes = (image_vertices.len() * std::mem::size_of::<Vertex>()) as u64;
if needed_bytes > self.image_vertex_buffer.size() {
self.image_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Vertex Buffer"),
size: needed_bytes,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
let needed_index_bytes = (image_indices.len() * std::mem::size_of::<u32>()) as u64;
if needed_index_bytes > self.image_index_buffer.size() {
self.image_index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Image Index Buffer"),
size: needed_index_bytes,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
staged_uploads.stage(
UploadTarget::ImageVertex,
bytemuck::cast_slice(&image_vertices),
);
staged_uploads.stage(
UploadTarget::ImageIndex,
bytemuck::cast_slice(&image_indices),
);
}
#[cfg(target_arch = "wasm32")]
let image_slot = if image_cmds.is_empty() {
0
} else {
let slot = self.claim_wasm_image_batch();
{
let buffers = &mut self.wasm_image_batches[slot];
buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
}
let buffers = &self.wasm_image_batches[slot];
self.write_wasm_buffer(
&buffers.vertex_buffer,
bytemuck::cast_slice(&image_vertices),
);
self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
slot
};
#[cfg(target_arch = "wasm32")]
let uniform_slot = if image_cmds.is_empty() {
0
} else {
self.prepare_wasm_viewport_uniforms(viewport)
};
self.scratch_image_vertices = image_vertices;
self.scratch_image_indices = image_indices;
Ok(PreparedImageBatch {
cmds: image_cmds,
#[cfg(target_arch = "wasm32")]
image_slot,
#[cfg(target_arch = "wasm32")]
uniform_slot,
})
}
fn prepare_text_image_draw_cmds<'a, I>(
&mut self,
layer_texts: I,
viewport: ViewportUniformParams,
root_scale: f32,
staged_uploads: &mut StagedBufferUploads,
) -> Result<PreparedImageBatch, String>
where
I: Iterator<Item = &'a TextDraw>,
{
let mut text_images = std::mem::take(&mut self.scratch_text_images);
text_images.clear();
for text_draw in layer_texts {
let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
self.text_raster_geometry(text_draw, root_scale)
else {
continue;
};
let cache_key = self.text_image_cache_key(text_draw, raster_rect, text_scale);
let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
cached.image.clone()
} else {
let Some(image) =
self.rasterize_text_draw_to_image(text_draw, raster_rect, text_scale)
else {
continue;
};
self.text_image_cache.put(
cache_key,
CachedTextImage {
image: image.clone(),
},
);
image
};
let draw_origin = if static_text_motion {
Point::new(raster_rect.x / root_scale, raster_rect.y / root_scale)
} else {
Point::new(logical_rect.x, logical_rect.y)
};
let draw_rect = Rect {
x: draw_origin.x,
y: draw_origin.y,
width: image.width() as f32 / root_scale,
height: image.height() as f32 / root_scale,
};
text_images.push(ImageDraw {
rect: draw_rect,
local_rect: draw_rect,
quad: rect_to_quad(draw_rect),
snap_anchor: None,
image,
alpha: 1.0,
color_filter: None,
sampling: ImageSampling::Nearest,
z_index: text_draw.z_index,
clip,
blend_mode: BlendMode::SrcOver,
src_rect: None,
motion_context_animated: text_draw.translated_content_context,
});
}
let result =
self.prepare_image_draw_cmds(text_images.iter(), viewport, root_scale, staged_uploads);
self.scratch_text_images = text_images;
result
}
fn text_raster_geometry(
&self,
text_draw: &TextDraw,
root_scale: f32,
) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
if text_draw.text.is_empty()
|| text_draw.rect.width <= 0.0
|| text_draw.rect.height <= 0.0
|| !root_scale.is_finite()
|| root_scale <= 0.0
{
return None;
}
let text_scale = text_draw.scale * root_scale;
if !text_scale.is_finite() || text_scale <= 0.0 {
return None;
}
let static_text_motion = text_draw
.text_style
.paragraph_style
.text_motion
.unwrap_or(cranpose_ui::text::TextMotion::Static)
== cranpose_ui::text::TextMotion::Static;
let snap_delta = if static_text_motion {
text_draw
.snap_anchor
.map(|anchor| snap_delta_for_anchor(anchor, root_scale))
.unwrap_or_default()
} else {
Point::default()
};
let logical_rect = text_draw.rect.translate(snap_delta.x, snap_delta.y);
let clip = text_draw
.clip
.map(|clip| clip.translate(snap_delta.x, snap_delta.y));
let mut raster_rect = Rect {
x: logical_rect.x * root_scale,
y: logical_rect.y * root_scale,
width: logical_rect.width * root_scale,
height: logical_rect.height * root_scale,
};
if static_text_motion {
raster_rect.x = raster_rect.x.round();
raster_rect.y = raster_rect.y.round();
}
raster_rect.width = raster_rect.width.ceil().max(1.0);
raster_rect.height = raster_rect.height.ceil().max(1.0);
Some((
logical_rect,
raster_rect,
clip,
text_scale,
static_text_motion,
))
}
fn text_image_cache_key(
&self,
text_draw: &TextDraw,
raster_rect: Rect,
text_scale: f32,
) -> TextImageCacheKey {
let mut state = default_hash::new();
text_draw.node_id.hash(&mut state);
text_draw.text.render_hash().hash(&mut state);
text_draw.text_style.render_hash().hash(&mut state);
text_draw.color.render_hash().hash(&mut state);
hash_rect_for_cache(raster_rect, &mut state);
text_draw.font_size.to_bits().hash(&mut state);
text_scale.to_bits().hash(&mut state);
text_draw.layout_options.hash(&mut state);
TextImageCacheKey(state.finish())
}
fn rasterize_text_draw_to_image(
&self,
text_draw: &TextDraw,
raster_rect: Rect,
text_scale: f32,
) -> Option<ImageBitmap> {
if text_draw.text.span_styles.is_empty() {
let font = self.text_fonts.resolve(&text_draw.text_style)?;
return rasterize_text_to_image(
text_draw.text.text.as_str(),
raster_rect,
&text_draw.text_style,
text_draw.color,
text_draw.font_size,
text_scale,
font,
);
}
rasterize_spanned_text_to_image(text_draw, raster_rect, text_scale, &self.text_fonts)
}
}
fn rasterize_spanned_text_to_image(
text_draw: &TextDraw,
raster_rect: Rect,
text_scale: f32,
fonts: &SoftwareTextFontSet,
) -> Option<ImageBitmap> {
let width = raster_rect.width.ceil().max(1.0) as u32;
let height = raster_rect.height.ceil().max(1.0) as u32;
let mut canvas = vec![0_u8; (width as usize) * (height as usize) * 4];
let boundaries = text_draw.text.span_boundaries();
let base_line_height = text_draw
.text_style
.resolve_line_height(14.0, text_draw.font_size)
.max(1.0);
let mut current_line_height = base_line_height;
let mut cursor_x = raster_rect.x;
let mut cursor_y = raster_rect.y;
for window in boundaries.windows(2) {
let start = window[0];
let end = window[1];
if start == end {
continue;
}
let chunk = &text_draw.text.text[start..end];
let mut merged_span = text_draw.text_style.span_style.clone();
for span in &text_draw.text.span_styles {
if span.range.start <= start && span.range.end >= end {
merged_span = merged_span.merge(&span.item);
}
}
let mut chunk_style = text_draw.text_style.clone();
chunk_style.span_style = merged_span;
for part in chunk.split_inclusive('\n') {
let has_newline = part.ends_with('\n');
let content = if has_newline {
&part[..part.len().saturating_sub(1)]
} else {
part
};
if !content.is_empty() {
let chunk_font_size = chunk_style.resolve_font_size(text_draw.font_size);
let Some(font) = fonts.resolve(&chunk_style) else {
continue;
};
let metrics = measure_text_with_font(content, &chunk_style, chunk_font_size, font);
let segment_rect = Rect {
x: cursor_x,
y: cursor_y,
width: (metrics.width * text_scale).ceil().max(1.0),
height: (metrics.height * text_scale).ceil().max(1.0),
};
if let Some(segment_image) = rasterize_text_to_image(
content,
segment_rect,
&chunk_style,
chunk_style.resolve_text_color(text_draw.color),
chunk_font_size,
text_scale,
font,
) {
composite_text_segment(
&mut canvas,
width,
height,
raster_rect,
segment_rect,
&segment_image,
);
}
cursor_x += metrics.width * text_scale;
current_line_height = current_line_height.max(metrics.line_height.max(1.0));
}
if has_newline {
cursor_x = raster_rect.x;
cursor_y += current_line_height * text_scale;
current_line_height = base_line_height;
}
}
}
ImageBitmap::from_rgba8(width, height, canvas).ok()
}
fn composite_text_segment(
canvas: &mut [u8],
canvas_width: u32,
canvas_height: u32,
canvas_rect: Rect,
segment_rect: Rect,
segment_image: &ImageBitmap,
) {
let offset_x = (segment_rect.x - canvas_rect.x).round() as i32;
let offset_y = (segment_rect.y - canvas_rect.y).round() as i32;
let src = segment_image.pixels();
for sy in 0..segment_image.height() as i32 {
let dy = offset_y + sy;
if dy < 0 || dy >= canvas_height as i32 {
continue;
}
for sx in 0..segment_image.width() as i32 {
let dx = offset_x + sx;
if dx < 0 || dx >= canvas_width as i32 {
continue;
}
let src_index = ((sy as u32 * segment_image.width() + sx as u32) * 4) as usize;
let dst_index = ((dy as u32 * canvas_width + dx as u32) * 4) as usize;
blend_rgba_pixel(
&mut canvas[dst_index..dst_index + 4],
&src[src_index..src_index + 4],
);
}
}
}
fn blend_rgba_pixel(dst: &mut [u8], src: &[u8]) {
let src_alpha = src[3] as f32 / 255.0;
if src_alpha <= 0.0 {
return;
}
let dst_alpha = dst[3] as f32 / 255.0;
let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
if out_alpha <= f32::EPSILON {
dst.copy_from_slice(&[0, 0, 0, 0]);
return;
}
for channel in 0..3 {
let src_channel = src[channel] as f32 / 255.0;
let dst_channel = dst[channel] as f32 / 255.0;
let src_premult = src_channel * src_alpha;
let dst_premult = dst_channel * dst_alpha;
dst[channel] =
(((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0) * 255.0)
.round() as u8;
}
dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
}
fn align_to(value: u32, alignment: u32) -> u32 {
debug_assert!(alignment > 0);
value.div_ceil(alignment) * alignment
}
#[cfg(not(target_arch = "wasm32"))]
fn align_usize_to(value: usize, alignment: usize) -> usize {
debug_assert!(alignment > 0);
value.div_ceil(alignment) * alignment
}
impl GpuRenderer {
fn convert_surface_pixels_to_rgba(&self, pixels: &mut [u8]) -> Result<(), String> {
match self.surface_format {
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => Ok(()),
wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
for pixel in pixels.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
Ok(())
}
format => Err(format!(
"Screenshot readback unsupported for texture format: {format:?}"
)),
}
}
}
fn is_in_effect_range(z_index: usize, effect_z_ranges: &[Range<usize>]) -> bool {
effect_z_ranges.iter().any(|range| range.contains(&z_index))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SegmentDrawItem {
Shape(usize),
Image(usize),
Text(usize),
Shadow(usize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SegmentBatchPlan {
Shape {
start: usize,
end: usize,
blend_mode: BlendMode,
},
Image {
start: usize,
end: usize,
blend_mode: BlendMode,
},
Text {
start: usize,
end: usize,
},
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct SegmentDrawChunkPlan {
batches: Vec<SegmentBatchPlan>,
}
struct SegmentRenderOutcome {
rendered_any: bool,
pass_count: u32,
}
struct SegmentCommandEncodeOutcome {
first_batch: bool,
}
struct ShadowSourceRenderOutcome {
rendered_any: bool,
pass_count: u32,
}
impl SegmentDrawChunkPlan {
fn is_empty(&self) -> bool {
self.batches.is_empty()
}
fn push(&mut self, batch: SegmentBatchPlan) {
self.batches.push(batch);
}
fn iter(&self) -> impl Iterator<Item = SegmentBatchPlan> + '_ {
self.batches.iter().copied()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum SegmentRenderCommand {
DrawChunk(SegmentDrawChunkPlan),
Shadow(usize),
}
struct SegmentCommandIter<'a> {
ordered_items: &'a [(usize, SegmentDrawItem)],
shapes: &'a [DrawShape],
images: &'a [ImageDraw],
cursor: usize,
}
impl<'a> SegmentCommandIter<'a> {
fn new(
ordered_items: &'a [(usize, SegmentDrawItem)],
shapes: &'a [DrawShape],
images: &'a [ImageDraw],
) -> Self {
Self {
ordered_items,
shapes,
images,
cursor: 0,
}
}
}
impl Iterator for SegmentCommandIter<'_> {
type Item = SegmentRenderCommand;
fn next(&mut self) -> Option<Self::Item> {
if self.cursor >= self.ordered_items.len() {
return None;
}
if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
self.cursor += 1;
return Some(SegmentRenderCommand::Shadow(index));
}
let mut chunk = SegmentDrawChunkPlan::default();
while self.cursor < self.ordered_items.len() {
if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
if chunk.is_empty() {
self.cursor += 1;
return Some(SegmentRenderCommand::Shadow(index));
}
break;
}
let Some((batch, next_cursor)) = segment_batch_plan_at_cursor(
self.ordered_items,
self.shapes,
self.images,
self.cursor,
) else {
break;
};
chunk.push(batch);
self.cursor = next_cursor;
}
Some(SegmentRenderCommand::DrawChunk(chunk))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PreparedShapeBatch {
index_count: u32,
#[cfg(target_arch = "wasm32")]
shape_slot: usize,
#[cfg(target_arch = "wasm32")]
uniform_slot: usize,
}
struct PreparedImageBatch {
cmds: Vec<ImageDrawCmd>,
#[cfg(target_arch = "wasm32")]
image_slot: usize,
#[cfg(target_arch = "wasm32")]
uniform_slot: usize,
}
impl PreparedImageBatch {
fn is_empty(&self) -> bool {
self.cmds.is_empty()
}
fn into_cmds(self) -> Vec<ImageDrawCmd> {
self.cmds
}
}
fn segment_batch_plan_at_cursor(
ordered_items: &[(usize, SegmentDrawItem)],
shapes: &[DrawShape],
images: &[ImageDraw],
start: usize,
) -> Option<(SegmentBatchPlan, usize)> {
match ordered_items[start].1 {
SegmentDrawItem::Shape(index) => {
let blend_mode = supported_blend_mode(shapes[index].blend_mode);
let mut end = start + 1;
let shape_limit = (start + MAX_SHAPES_PER_BATCH).min(ordered_items.len());
while end < shape_limit {
match ordered_items[end].1 {
SegmentDrawItem::Shape(next_index)
if supported_blend_mode(shapes[next_index].blend_mode) == blend_mode =>
{
end += 1;
}
_ => break,
}
}
Some((
SegmentBatchPlan::Shape {
start,
end,
blend_mode,
},
end,
))
}
SegmentDrawItem::Image(index) => {
let blend_mode = supported_blend_mode(images[index].blend_mode);
let mut end = start + 1;
while end < ordered_items.len() {
match ordered_items[end].1 {
SegmentDrawItem::Image(next_index)
if supported_blend_mode(images[next_index].blend_mode) == blend_mode =>
{
end += 1;
}
_ => break,
}
}
Some((
SegmentBatchPlan::Image {
start,
end,
blend_mode,
},
end,
))
}
SegmentDrawItem::Text(_) => {
let mut end = start + 1;
while end < ordered_items.len() {
if matches!(ordered_items[end].1, SegmentDrawItem::Text(_)) {
end += 1;
} else {
break;
}
}
Some((SegmentBatchPlan::Text { start, end }, end))
}
SegmentDrawItem::Shadow(_) => None,
}
}
#[allow(clippy::too_many_arguments)]
fn collect_non_effect_segment_items(
_shapes: &[DrawShape],
_images: &[ImageDraw],
_texts: &[TextDraw],
_shadow_draws: &[ShadowDraw],
draw_ops: &[DrawOp],
z_start: usize,
z_end: usize,
effect_z_ranges: &[Range<usize>],
scratch: &mut Vec<(usize, SegmentDrawItem)>,
) {
scratch.clear();
scratch.extend(draw_ops.iter().filter_map(|op| {
if op.z_index < z_start
|| op.z_index >= z_end
|| is_in_effect_range(op.z_index, effect_z_ranges)
{
return None;
}
let item = match op.kind {
DrawOpKind::Shape(index) => SegmentDrawItem::Shape(index),
DrawOpKind::Image(index) => SegmentDrawItem::Image(index),
DrawOpKind::Text(index) => SegmentDrawItem::Text(index),
DrawOpKind::Shadow(index) => SegmentDrawItem::Shadow(index),
};
Some((op.z_index, item))
}));
}
fn collect_effect_ranges(
effect_layers: &[EffectLayer],
z_start: usize,
z_end: usize,
excluded_effect_layer: Option<usize>,
out: &mut Vec<Range<usize>>,
) {
out.clear();
for (index, layer) in effect_layers.iter().enumerate() {
if Some(index) == excluded_effect_layer {
continue;
}
if effect_layer_in_range(layer, z_start, z_end) {
out.push(layer.z_start..layer.z_end);
}
}
out.sort_by_key(|range| range.start);
}
fn collect_layer_events(
effect_layers: &[EffectLayer],
backdrop_layers: &[BackdropLayer],
z_start: usize,
z_end: usize,
excluded_effect_layer: Option<usize>,
out: &mut Vec<LayerEvent>,
) {
out.clear();
for (index, layer) in backdrop_layers.iter().enumerate() {
if layer.z_index >= z_start && layer.z_index < z_end {
out.push(LayerEvent {
z_index: layer.z_index,
kind: LayerEventKind::Backdrop(index),
});
}
}
for (index, layer) in effect_layers.iter().enumerate() {
if Some(index) == excluded_effect_layer {
continue;
}
if effect_layer_in_range(layer, z_start, z_end) {
out.push(LayerEvent {
z_index: layer.z_start,
kind: LayerEventKind::Effect(index),
});
}
}
out.sort_by(|a, b| {
let z_cmp = a.z_index.cmp(&b.z_index);
if z_cmp != std::cmp::Ordering::Equal {
return z_cmp;
}
let kind_cmp = a.kind_order().cmp(&b.kind_order());
if kind_cmp != std::cmp::Ordering::Equal {
return kind_cmp;
}
match (a.kind, b.kind) {
(LayerEventKind::Effect(ai), LayerEventKind::Effect(bi)) => effect_layers[bi]
.z_end
.cmp(&effect_layers[ai].z_end)
.then_with(|| bi.cmp(&ai)),
_ => std::cmp::Ordering::Equal,
}
});
}
pub(crate) fn has_backdrop_layer_in_range(
backdrop_layers: &[BackdropLayer],
z_start: usize,
z_end: usize,
) -> bool {
backdrop_layers
.iter()
.any(|layer| layer.z_index >= z_start && layer.z_index < z_end)
}
pub(crate) fn scissor_rect_for_rect(
rect: Rect,
root_scale: f32,
width: u32,
height: u32,
) -> Option<(u32, u32, u32, u32)> {
let mut left = rect.x * root_scale;
let mut top = rect.y * root_scale;
let mut right = (rect.x + rect.width) * root_scale;
let mut bottom = (rect.y + rect.height) * root_scale;
left = left.max(0.0).min(width as f32).floor();
top = top.max(0.0).min(height as f32).floor();
right = right.max(0.0).min(width as f32).ceil();
bottom = bottom.max(0.0).min(height as f32).ceil();
if right <= left || bottom <= top {
return None;
}
Some((
left as u32,
top as u32,
(right - left) as u32,
(bottom - top) as u32,
))
}
fn scissor_rect_for_layer(
rect: Rect,
clip: Option<Rect>,
root_scale: f32,
width: u32,
height: u32,
) -> Option<(u32, u32, u32, u32)> {
let clipped_rect = match clip {
Some(clip_rect) => rect.intersect(clip_rect)?,
None => rect,
};
scissor_rect_for_rect(clipped_rect, root_scale, width, height)
}
fn tint_for_image(
color_filter: Option<ColorFilter>,
alpha: f32,
) -> ([f32; 4], Option<ColorFilter>) {
let alpha = alpha.clamp(0.0, 1.0);
match color_filter {
Some(filter) if filter.supports_gpu_vertex_modulation() => {
let Some(tint) = filter.gpu_vertex_tint() else {
return ([1.0, 1.0, 1.0, alpha], Some(filter));
};
(
[
tint[0].clamp(0.0, 1.0),
tint[1].clamp(0.0, 1.0),
tint[2].clamp(0.0, 1.0),
(tint[3] * alpha).clamp(0.0, 1.0),
],
None,
)
}
Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
None => ([1.0, 1.0, 1.0, alpha], None),
}
}
fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
let Some(src) = src_rect else {
return Some(ImageUvRect {
min: [0.0, 0.0],
max: [1.0, 1.0],
sample_bounds: [0.0, 0.0, 1.0, 1.0],
});
};
let (u_min, u_max, u_bound_min, u_bound_max) =
source_axis_uv(src.x, src.width, image.width() as f32)?;
let (v_min, v_max, v_bound_min, v_bound_max) =
source_axis_uv(src.y, src.height, image.height() as f32)?;
Some(ImageUvRect {
min: [u_min, v_min],
max: [u_max, v_max],
sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
})
}
fn snap_nearest_image_to_device_pixels(image: &mut ImageDraw, root_scale: f32) {
if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
return;
}
let Some(rect) = axis_aligned_quad_rect(image.quad) else {
return;
};
let left_px = (rect.x * root_scale).round();
let top_px = (rect.y * root_scale).round();
let width_px = (rect.width * root_scale).round().max(1.0);
let height_px = (rect.height * root_scale).round().max(1.0);
let snapped = Rect {
x: left_px / root_scale,
y: top_px / root_scale,
width: width_px / root_scale,
height: height_px / root_scale,
};
image.rect = snapped;
image.local_rect = Rect {
x: image.local_rect.x + snapped.x - rect.x,
y: image.local_rect.y + snapped.y - rect.y,
width: snapped.width,
height: snapped.height,
};
image.quad = crate::rect_to_quad(snapped);
}
fn nearest_image_device_quad(image: &ImageDraw, root_scale: f32) -> Option<[[f32; 2]; 4]> {
if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
return None;
}
let rect = axis_aligned_quad_rect(image.quad)?;
let left_px = (rect.x * root_scale).round();
let top_px = (rect.y * root_scale).round();
let width_px = (rect.width * root_scale).round().max(1.0);
let height_px = (rect.height * root_scale).round().max(1.0);
let right_px = left_px + width_px;
let bottom_px = top_px + height_px;
Some([
[left_px, top_px],
[right_px, top_px],
[left_px, bottom_px],
[right_px, bottom_px],
])
}
fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
if !start.is_finite()
|| !extent.is_finite()
|| !image_extent.is_finite()
|| extent == 0.0
|| image_extent <= 0.0
{
return None;
}
let end = start + extent;
let edge_min = start.min(end).clamp(0.0, image_extent);
let edge_max = start.max(end).clamp(0.0, image_extent);
if edge_max <= edge_min {
return None;
}
let center_min = edge_min + 0.5;
let center_max = edge_max - 0.5;
let (bound_min, bound_max) = if center_min <= center_max {
(center_min, center_max)
} else {
let center = (edge_min + edge_max) * 0.5;
(center, center)
};
Some((
edge_min / image_extent,
edge_max / image_extent,
bound_min / image_extent,
bound_max / image_extent,
))
}
fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
let mut filtered = Vec::with_capacity(image.pixels().len());
for pixel in image.pixels().chunks_exact(4) {
let rgba = [
pixel[0] as f32 / 255.0,
pixel[1] as f32 / 255.0,
pixel[2] as f32 / 255.0,
pixel[3] as f32 / 255.0,
];
let out = filter.apply_rgba(rgba);
filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
}
ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
.map_err(|error| format!("failed to build filtered bitmap: {error}"))
}
fn scissor_rect_for_image(
image: &ImageDraw,
root_scale: f32,
width: u32,
height: u32,
) -> Option<(u32, u32, u32, u32)> {
scissor_rect_for_layer(image.rect, image.clip, root_scale, width, height)
}
fn inner_shadow_composite_mask(
shadow: &ShadowDraw,
root_scale: f32,
) -> Option<RoundedCompositeMask> {
if !shadow
.shapes
.iter()
.any(|(_, mode)| *mode == BlendMode::DstOut)
{
return None;
}
let (fill, _) = shadow.shapes.first()?;
let rect = fill.local_rect;
if rect.width <= 0.0 || rect.height <= 0.0 {
return None;
}
let radii = fill.shape.map_or([0.0; 4], |rounded| {
let resolved = rounded.resolve(rect.width, rect.height);
[
resolved.top_left * root_scale,
resolved.top_right * root_scale,
resolved.bottom_left * root_scale,
resolved.bottom_right * root_scale,
]
});
Some(RoundedCompositeMask {
rect: [
rect.x * root_scale,
rect.y * root_scale,
rect.width * root_scale,
rect.height * root_scale,
],
radii,
})
}
#[cfg(test)]
mod tests {
use super::*;
use cranpose_render_common::graph::{DrawPrimitiveNode, IsolationReasons, TextPrimitiveNode};
use cranpose_render_common::raster_cache::LayerRasterCacheHashes;
use cranpose_ui::text::{
AnnotatedString, BaselineShift, RangeStyle, Shadow, SpanStyle, TextDecoration,
TextDrawStyle, TextGeometricTransform, TextUnit,
};
use cranpose_ui::{TextLayoutOptions, TextStyle};
use cranpose_ui_graphics::{
Brush, Color, CornerRadii, DrawPrimitive, Rect, RenderEffect, RoundedCornerShape,
};
fn chunk(batches: &[SegmentBatchPlan]) -> SegmentDrawChunkPlan {
let mut chunk = SegmentDrawChunkPlan::default();
for batch in batches {
chunk.push(*batch);
}
chunk
}
fn with_test_app_context<R>(block: impl FnOnce() -> R) -> R {
let app_context = cranpose_ui::AppContext::new();
app_context.enter(block)
}
fn effect_layer(z_start: usize, z_end: usize) -> EffectLayer {
EffectLayer {
rect: Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
},
clip: None,
snap_anchor: None,
effect: Some(RenderEffect::blur(4.0)),
blend_mode: BlendMode::SrcOver,
composite_alpha: 1.0,
z_start,
z_end,
requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
}
}
#[test]
fn device_pixel_bounds_for_rect_snaps_origin_and_extents() {
let bounds = device_pixel_bounds_for_rect(
Rect {
x: 10.25,
y: 14.6,
width: 20.1,
height: 9.2,
},
200,
120,
2.0,
)
.expect("rect should intersect the viewport");
assert_eq!(
bounds,
DevicePixelBounds {
x: 20.0,
y: 29.0,
width: 41,
height: 19,
}
);
}
#[test]
fn visible_layer_rect_intersects_clip_and_viewport() {
let visible = visible_layer_rect(
Rect {
x: -10.0,
y: 5.0,
width: 80.0,
height: 40.0,
},
Some(Rect {
x: 4.0,
y: 8.0,
width: 20.0,
height: 50.0,
}),
2.0,
60,
40,
)
.expect("visible rect");
assert_eq!(
visible,
Rect {
x: 4.0,
y: 8.0,
width: 20.0,
height: 12.0,
}
);
}
#[test]
fn clamp_effect_surface_scale_caps_large_surfaces_but_keeps_base_scale() {
let clamped = clamp_effect_surface_scale(
Rect {
x: 0.0,
y: 0.0,
width: 1200.0,
height: 900.0,
},
1.0,
8.0,
16_384,
);
assert!(
clamped < 8.0,
"large translated effect layers must be capped to avoid OOM, got {clamped}"
);
assert!(
clamped >= 1.0,
"effect surfaces must not fall below destination resolution, got {clamped}"
);
}
#[test]
fn clamp_effect_surface_scale_keeps_decorated_text_capture_scale() {
let clamped = clamp_effect_surface_scale(
Rect {
x: 0.0,
y: 0.0,
width: 446.0,
height: 44.0,
},
1.0,
9.0,
16_384,
);
assert_eq!(
clamped, 9.0,
"decorated text motion-stable captures must keep full scale"
);
}
fn backdrop_layer(z_index: usize) -> BackdropLayer {
BackdropLayer {
rect: Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
},
clip: None,
effect: RenderEffect::blur(2.0),
z_index,
}
}
fn test_shape(z_index: usize, blend_mode: BlendMode) -> DrawShape {
DrawShape {
rect: Rect {
x: 0.0,
y: 0.0,
width: 8.0,
height: 8.0,
},
local_rect: Rect {
x: 0.0,
y: 0.0,
width: 8.0,
height: 8.0,
},
quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
snap_anchor: None,
brush: Brush::solid(Color::BLACK),
shape: None,
z_index,
clip: None,
blend_mode,
}
}
#[test]
fn shape_shadow_content_hash_ignores_viewport_translation() {
fn translate_shape(shape: &DrawShape, dx: f32, dy: f32) -> DrawShape {
let mut translated = shape.clone();
translated.rect.x += dx;
translated.rect.y += dy;
translated.local_rect.x += dx;
translated.local_rect.y += dy;
for point in &mut translated.quad {
point[0] += dx;
point[1] += dy;
}
translated.snap_anchor = translated.snap_anchor.map(|anchor| {
SnapAnchor::rigid(Point::new(anchor.origin.x + dx, anchor.origin.y + dy))
});
translated.clip = translated.clip.map(|mut clip| {
clip.x += dx;
clip.y += dy;
clip
});
translated
}
let mut first = test_shape(1, BlendMode::SrcOver);
first.rect = Rect {
x: 10.0,
y: 20.0,
width: 80.0,
height: 40.0,
};
first.local_rect = first.rect;
first.quad = [[10.0, 20.0], [90.0, 20.0], [10.0, 60.0], [90.0, 60.0]];
first.snap_anchor = Some(SnapAnchor::rigid(Point::new(7.0, 11.0)));
first.shape = Some(RoundedCornerShape::uniform(8.0));
first.clip = Some(Rect {
x: 8.0,
y: 18.0,
width: 86.0,
height: 44.0,
});
let mut cutout = test_shape(2, BlendMode::DstOut);
cutout.rect = Rect {
x: 18.0,
y: 26.0,
width: 62.0,
height: 22.0,
};
cutout.local_rect = cutout.rect;
cutout.quad = [[18.0, 26.0], [80.0, 26.0], [18.0, 48.0], [80.0, 48.0]];
cutout.shape = Some(RoundedCornerShape::uniform(4.0));
let dx = 37.0;
let dy = -11.5;
let translated = translate_shape(&first, dx, dy);
let translated_cutout = translate_shape(&cutout, dx, dy);
let root_scale = 1.25;
let first_origin = [6.0, 12.0];
let translated_origin = [first_origin[0] + dx, first_origin[1] + dy];
let first_viewport_offset = [first_origin[0] * root_scale, first_origin[1] * root_scale];
let translated_viewport_offset = [
translated_origin[0] * root_scale,
translated_origin[1] * root_scale,
];
let first_shapes = vec![
(first.clone(), BlendMode::SrcOver),
(cutout, BlendMode::DstOut),
];
let translated_shapes = vec![
(translated.clone(), BlendMode::SrcOver),
(translated_cutout, BlendMode::DstOut),
];
let first_hash =
shape_shadow_content_hash(&first_shapes, first_viewport_offset, root_scale);
let translated_hash =
shape_shadow_content_hash(&translated_shapes, translated_viewport_offset, root_scale);
assert_eq!(first_hash, translated_hash);
let mut changed_shapes = translated_shapes;
changed_shapes[0].0.rect.width += 1.0;
let changed_hash =
shape_shadow_content_hash(&changed_shapes, translated_viewport_offset, root_scale);
assert_ne!(first_hash, changed_hash);
}
fn test_shadow_draw(shapes: Vec<(DrawShape, BlendMode)>) -> ShadowDraw {
ShadowDraw {
shapes,
texts: vec![],
blur_radius: 8.0,
clip: None,
z_index: 0,
}
}
fn test_image(z_index: usize, blend_mode: BlendMode) -> ImageDraw {
ImageDraw {
rect: Rect {
x: 0.0,
y: 0.0,
width: 8.0,
height: 8.0,
},
local_rect: Rect {
x: 0.0,
y: 0.0,
width: 8.0,
height: 8.0,
},
quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
snap_anchor: None,
image: ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image"),
alpha: 1.0,
color_filter: None,
sampling: ImageSampling::Nearest,
z_index,
clip: None,
blend_mode,
src_rect: None,
motion_context_animated: false,
}
}
#[test]
fn image_sampler_descriptors_match_requested_sampling() {
let nearest = image_sampler_descriptor(ImageSampling::Nearest);
assert_eq!(nearest.mag_filter, wgpu::FilterMode::Nearest);
assert_eq!(nearest.min_filter, wgpu::FilterMode::Nearest);
let linear = image_sampler_descriptor(ImageSampling::Linear);
assert_eq!(linear.mag_filter, wgpu::FilterMode::Linear);
assert_eq!(linear.min_filter, wgpu::FilterMode::Linear);
}
#[test]
fn image_uv_rect_clamps_source_rect_to_texel_centers() {
let image = ImageBitmap::from_rgba8(24, 16, vec![0; 24 * 16 * 4]).expect("image");
let uv = image_uv_rect(
&image,
Some(Rect {
x: 0.0,
y: 0.0,
width: 16.0,
height: 16.0,
}),
)
.expect("uv rect");
assert_eq!(uv.min, [0.0, 0.0]);
assert_eq!(uv.max, [16.0 / 24.0, 1.0]);
assert_eq!(
uv.sample_bounds,
[0.5 / 24.0, 0.5 / 16.0, 15.5 / 24.0, 15.5 / 16.0]
);
}
#[test]
fn image_uv_rect_keeps_full_image_unclamped() {
let image = ImageBitmap::from_rgba8(2, 2, vec![0; 16]).expect("image");
let uv = image_uv_rect(&image, None).expect("uv rect");
assert_eq!(uv.min, [0.0, 0.0]);
assert_eq!(uv.max, [1.0, 1.0]);
assert_eq!(uv.sample_bounds, [0.0, 0.0, 1.0, 1.0]);
}
fn test_text(z_index: usize) -> TextDraw {
TextDraw {
node_id: 0,
rect: Rect {
x: 0.0,
y: 0.0,
width: 8.0,
height: 8.0,
},
snap_anchor: None,
translated_content_context: false,
text: std::rc::Rc::new(cranpose_ui::text::AnnotatedString::from("t")),
color: Color::WHITE,
text_style: cranpose_ui::TextStyle::default(),
font_size: 12.0,
scale: 1.0,
layout_options: cranpose_ui::TextLayoutOptions::default(),
z_index,
clip: None,
}
}
fn test_draw_ops(
shapes: &[DrawShape],
images: &[ImageDraw],
texts: &[TextDraw],
shadows: &[ShadowDraw],
) -> Vec<DrawOp> {
let mut ops = Vec::new();
ops.extend(shapes.iter().enumerate().map(|(index, shape)| DrawOp {
z_index: shape.z_index,
kind: DrawOpKind::Shape(index),
}));
ops.extend(images.iter().enumerate().map(|(index, image)| DrawOp {
z_index: image.z_index,
kind: DrawOpKind::Image(index),
}));
ops.extend(texts.iter().enumerate().map(|(index, text)| DrawOp {
z_index: text.z_index,
kind: DrawOpKind::Text(index),
}));
ops.extend(shadows.iter().enumerate().map(|(index, shadow)| DrawOp {
z_index: shadow.z_index,
kind: DrawOpKind::Shadow(index),
}));
ops.sort_by_key(|op| op.z_index);
ops
}
fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
crate::test_support::layer_node(
local_bounds,
ProjectiveTransform::identity(),
GraphicsLayer::default(),
children,
)
}
fn cacheable_layer(
node_id: cranpose_core::NodeId,
local_bounds: Rect,
children: Vec<RenderNode>,
) -> LayerNode {
let mut layer = test_layer(local_bounds, children);
layer.node_id = Some(node_id);
layer.cache_policy = cranpose_render_common::graph::CachePolicy::Auto;
layer.recompute_raster_cache_hashes();
layer
}
fn text_layer_with_style(text: AnnotatedString, text_style: TextStyle) -> LayerNode {
test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 64.0,
height: 32.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
node_id: 1,
rect: Rect {
x: 2.0,
y: 3.0,
width: 48.0,
height: 18.0,
},
text,
text_style,
font_size: 14.0,
layout_options: TextLayoutOptions::default(),
clip: None,
})),
})],
)
}
fn snapped_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
LayerNode {
node_id: Some(77),
local_bounds: Rect {
x: 0.0,
y: 0.0,
width: 48.0,
height: 24.0,
},
transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
motion_context_animated: animated,
translated_content_context,
translated_content_offset: Point::default(),
graphics_layer: GraphicsLayer::default(),
clip_to_bounds: false,
shadow_clip: None,
hit_test: None,
has_hit_targets: false,
isolation: IsolationReasons::default(),
cache_policy: CachePolicy::None,
cache_hashes: LayerRasterCacheHashes::default(),
cache_hashes_valid: false,
children: vec![
RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::RoundRect {
rect: Rect {
x: 0.0,
y: 0.0,
width: 48.0,
height: 24.0,
},
brush: Brush::solid(Color(0.28, 0.30, 0.46, 0.88)),
radii: CornerRadii::uniform(6.0),
},
clip: None,
}),
}),
RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Image {
rect: Rect {
x: 2.0,
y: 2.0,
width: 12.0,
height: 12.0,
},
image: ImageBitmap::from_rgba8(
2,
2,
vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255,
255,
],
)
.expect("image"),
alpha: 1.0,
color_filter: None,
sampling: ImageSampling::Linear,
src_rect: None,
},
clip: None,
}),
}),
RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
node_id: 77,
rect: Rect {
x: 6.0,
y: 4.0,
width: 36.0,
height: 16.0,
},
text: AnnotatedString::from("48 px"),
text_style: TextStyle::default(),
font_size: 14.0,
layout_options: TextLayoutOptions::default(),
clip: None,
})),
}),
],
}
}
fn snapped_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
let text_leaf = snapped_text_leaf(animated, translated_content_context);
test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 96.0,
height: 64.0,
},
vec![RenderNode::Layer(Box::new(text_leaf))],
)
}
fn translated_content_local_surface_root() -> LayerNode {
let mut effectful_text = text_layer_with_style(
AnnotatedString::from("shadow"),
TextStyle::from_span_style(SpanStyle {
shadow: Some(Shadow {
color: Color::BLACK,
offset: Point::new(1.0, 2.0),
blur_radius: 3.0,
}),
..SpanStyle::default()
}),
);
effectful_text.translated_content_context = true;
let translated_content = LayerNode {
node_id: Some(78),
local_bounds: Rect {
x: 0.0,
y: 0.0,
width: 96.0,
height: 64.0,
},
transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
motion_context_animated: false,
translated_content_context: true,
translated_content_offset: Point::default(),
graphics_layer: GraphicsLayer::default(),
clip_to_bounds: false,
shadow_clip: None,
hit_test: None,
has_hit_targets: false,
isolation: IsolationReasons::default(),
cache_policy: CachePolicy::None,
cache_hashes: LayerRasterCacheHashes::default(),
cache_hashes_valid: false,
children: vec![RenderNode::Layer(Box::new(effectful_text))],
};
test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 160.0,
height: 120.0,
},
vec![RenderNode::Layer(Box::new(translated_content))],
)
}
#[test]
fn scissor_rect_for_layer_intersects_with_clip() {
let rect = Rect {
x: 10.0,
y: 10.0,
width: 30.0,
height: 20.0,
};
let clip = Rect {
x: 20.0,
y: 15.0,
width: 100.0,
height: 100.0,
};
let scissor = scissor_rect_for_layer(rect, Some(clip), 1.0, 200, 200);
assert_eq!(scissor, Some((20, 15, 20, 15)));
}
#[test]
fn visible_draw_rect_no_clip_returns_original() {
let rect = Rect {
x: 100.0,
y: 200.0,
width: 300.0,
height: 400.0,
};
assert_eq!(visible_draw_rect(rect, None), Some(rect));
}
#[test]
fn visible_draw_rect_with_clip_intersects() {
let rect = Rect {
x: 0.0,
y: 0.0,
width: 2000.0,
height: 5000.0,
};
let clip = Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 600.0,
};
let visible = visible_draw_rect(rect, Some(clip)).expect("should have visible area");
assert_eq!(visible.width, 800.0);
assert_eq!(visible.height, 600.0);
}
#[test]
fn visible_draw_rect_fully_clipped_returns_none() {
let rect = Rect {
x: 1000.0,
y: 1000.0,
width: 200.0,
height: 200.0,
};
let clip = Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 600.0,
};
assert!(visible_draw_rect(rect, Some(clip)).is_none());
}
#[test]
fn scene_bounds_respects_clip_on_shapes() {
let mut scene = CompositorScene::new();
scene.shapes.push(DrawShape {
rect: Rect {
x: 10.0,
y: 10.0,
width: 100.0,
height: 50.0,
},
clip: Some(Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 600.0,
}),
..test_shape(0, BlendMode::SrcOver)
});
scene.shapes.push(DrawShape {
rect: Rect {
x: 0.0,
y: 3000.0,
width: 100.0,
height: 50.0,
},
clip: Some(Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 600.0,
}),
..test_shape(1, BlendMode::SrcOver)
});
let bounds = scene_bounds(&scene).expect("should have bounds");
assert!(bounds.y + bounds.height <= 600.0);
}
#[test]
fn scene_bounds_scroll_content_clipped_to_viewport() {
let mut scene = CompositorScene::new();
let viewport_clip = Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 600.0,
};
for i in 0..20 {
scene.shapes.push(DrawShape {
rect: Rect {
x: 0.0,
y: i as f32 * 300.0,
width: 800.0,
height: 200.0,
},
clip: Some(viewport_clip),
..test_shape(i, BlendMode::SrcOver)
});
}
let bounds = scene_bounds(&scene).expect("should have bounds");
assert_eq!(bounds.x, 0.0);
assert_eq!(bounds.y, 0.0);
assert!(bounds.width <= 800.0);
assert!(bounds.height <= 600.0);
}
#[test]
fn scene_bounds_stable_across_scroll_offsets() {
let viewport_clip = Rect {
x: 0.0,
y: 0.0,
width: 400.0,
height: 50.0,
};
let compute_bounds_at_offset = |scroll_x: f32| {
let mut scene = CompositorScene::new();
for i in 0..10 {
scene.shapes.push(DrawShape {
rect: Rect {
x: i as f32 * 100.0 - scroll_x,
y: 0.0,
width: 80.0,
height: 40.0,
},
clip: Some(viewport_clip),
..test_shape(i, BlendMode::SrcOver)
});
}
scene_bounds(&scene).expect("bounds")
};
let bounds_at_0 = compute_bounds_at_offset(0.0);
let bounds_at_300 = compute_bounds_at_offset(300.0);
let bounds_at_600 = compute_bounds_at_offset(600.0);
assert!(
(bounds_at_0.width - bounds_at_300.width).abs() < 1.0,
"bounds width changed with scroll: {} vs {}",
bounds_at_0.width,
bounds_at_300.width
);
assert!(
(bounds_at_0.width - bounds_at_600.width).abs() < 1.0,
"bounds width changed with scroll: {} vs {}",
bounds_at_0.width,
bounds_at_600.width
);
}
#[test]
fn collect_effect_ranges_respects_excluded_effect() {
let layers = vec![effect_layer(10, 40), effect_layer(20, 30)];
let mut ranges = Vec::new();
collect_effect_ranges(&layers, 10, 40, Some(0), &mut ranges);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0], 20..30);
}
#[test]
fn collect_layer_events_includes_nested_when_parent_excluded() {
let effects = vec![effect_layer(10, 40), effect_layer(20, 30)];
let backdrops = vec![backdrop_layer(25)];
let mut events = Vec::new();
collect_layer_events(&effects, &backdrops, 10, 40, Some(0), &mut events);
assert_eq!(events.len(), 2);
match events[0].kind {
LayerEventKind::Effect(index) => assert_eq!(index, 1),
LayerEventKind::Backdrop(_) => panic!("expected nested effect as first event"),
}
match events[1].kind {
LayerEventKind::Backdrop(index) => assert_eq!(index, 0),
LayerEventKind::Effect(_) => panic!("expected backdrop as second event"),
}
}
fn pure_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
LayerNode {
node_id: Some(177),
local_bounds: Rect {
x: 0.0,
y: 0.0,
width: 96.0,
height: 32.0,
},
transform_to_parent: ProjectiveTransform::translation(11.4, 23.6),
motion_context_animated: animated,
translated_content_context,
translated_content_offset: Point::default(),
graphics_layer: GraphicsLayer::default(),
clip_to_bounds: false,
shadow_clip: None,
hit_test: None,
has_hit_targets: false,
isolation: IsolationReasons::default(),
cache_policy: CachePolicy::None,
cache_hashes: LayerRasterCacheHashes::default(),
cache_hashes_valid: false,
children: vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
node_id: 177,
rect: Rect {
x: 0.0,
y: 0.0,
width: 96.0,
height: 24.0,
},
clip: None,
text: AnnotatedString::from("Pure text"),
text_style: TextStyle::default(),
font_size: 14.0,
layout_options: TextLayoutOptions::default(),
})),
})],
}
}
fn pure_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
let text_leaf = pure_text_leaf(animated, translated_content_context);
test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 160.0,
height: 96.0,
},
vec![RenderNode::Layer(Box::new(text_leaf))],
)
}
#[test]
fn collect_layer_events_sorts_backdrop_before_effect_at_same_z() {
let effects = vec![effect_layer(10, 20)];
let backdrops = vec![backdrop_layer(10)];
let mut events = Vec::new();
collect_layer_events(&effects, &backdrops, 0, 30, None, &mut events);
assert_eq!(events.len(), 2);
match events[0].kind {
LayerEventKind::Backdrop(_) => {}
LayerEventKind::Effect(_) => panic!("expected backdrop to run before effect"),
}
match events[1].kind {
LayerEventKind::Effect(_) => {}
LayerEventKind::Backdrop(_) => panic!("expected effect as second event"),
}
}
#[test]
fn collect_layer_events_prefers_outer_effect_when_same_start_z() {
let effects = vec![effect_layer(10, 20), effect_layer(10, 40)];
let mut events = Vec::new();
collect_layer_events(&effects, &[], 0, 50, None, &mut events);
assert_eq!(events.len(), 2);
match events[0].kind {
LayerEventKind::Effect(index) => assert_eq!(index, 1),
LayerEventKind::Backdrop(_) => panic!("expected outer effect first"),
}
match events[1].kind {
LayerEventKind::Effect(index) => assert_eq!(index, 0),
LayerEventKind::Backdrop(_) => panic!("expected child effect second"),
}
}
#[test]
fn collect_layer_events_prefers_later_effect_when_ranges_match() {
let effects = vec![effect_layer(10, 20), effect_layer(10, 20)];
let mut events = Vec::new();
collect_layer_events(&effects, &[], 0, 30, None, &mut events);
assert_eq!(events.len(), 2);
match events[0].kind {
LayerEventKind::Effect(index) => assert_eq!(index, 1),
LayerEventKind::Backdrop(_) => panic!("expected later effect first"),
}
match events[1].kind {
LayerEventKind::Effect(index) => assert_eq!(index, 0),
LayerEventKind::Backdrop(_) => panic!("expected earlier effect second"),
}
}
#[test]
fn has_backdrop_layer_in_range_detects_nested_layers() {
let backdrops = vec![backdrop_layer(5), backdrop_layer(15), backdrop_layer(25)];
assert!(has_backdrop_layer_in_range(&backdrops, 10, 20));
assert!(has_backdrop_layer_in_range(&backdrops, 0, 6));
assert!(!has_backdrop_layer_in_range(&backdrops, 20, 25));
}
#[test]
fn layer_contains_descendant_backdrop_ignores_self_backdrop() {
let mut self_backdrop = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
},
vec![],
);
self_backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
assert!(!layer_contains_descendant_backdrop(&self_backdrop));
let mut child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 8.0,
height: 8.0,
},
vec![],
);
child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
let parent = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
},
vec![RenderNode::Layer(Box::new(child))],
);
assert!(layer_contains_descendant_backdrop(&parent));
}
#[test]
fn estimate_layer_surface_rect_includes_transformed_child_bounds() {
let mut child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 6.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 6.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
child.transform_to_parent = ProjectiveTransform::translation(18.0, 7.0);
let parent = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 4.0,
height: 4.0,
},
vec![RenderNode::Layer(Box::new(child))],
);
assert_eq!(
estimate_layer_surface_rect(&parent),
Rect {
x: 18.0,
y: 7.0,
width: 10.0,
height: 6.0,
}
);
}
#[test]
fn estimate_layer_surface_rect_clips_translated_clip_layers_without_hidden_leading_content() {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: 24.0,
y: 0.0,
width: 200.0,
height: 480.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
layer.translated_content_context = true;
layer.motion_context_animated = true;
layer.clip_to_bounds = true;
assert_eq!(
estimate_layer_surface_rect(&layer),
Rect {
x: 24.0,
y: 0.0,
width: 96.0,
height: 72.0,
}
);
}
#[test]
fn estimate_layer_surface_rect_clips_active_horizontal_scroll_content() {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: -24.0,
y: 0.0,
width: 200.0,
height: 480.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
layer.translated_content_context = true;
layer.motion_context_animated = true;
layer.clip_to_bounds = true;
assert_eq!(
estimate_layer_surface_rect(&layer),
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
}
);
}
#[test]
fn estimate_layer_surface_rect_clips_active_vertical_scroll_content() {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: -24.0,
width: 120.0,
height: 200.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
layer.translated_content_context = true;
layer.motion_context_animated = true;
layer.clip_to_bounds = true;
assert_eq!(
estimate_layer_surface_rect(&layer),
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
}
);
}
#[test]
fn estimate_layer_surface_rect_keeps_shallow_scroll_capture_origin_stable() {
fn shallow_scroll_surface_rect(content_y: f32) -> Rect {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: content_y,
width: 120.0,
height: 200.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
layer.translated_content_context = true;
layer.motion_context_animated = true;
layer.clip_to_bounds = true;
estimate_layer_surface_rect(&layer)
}
assert_eq!(
shallow_scroll_surface_rect(-24.0),
shallow_scroll_surface_rect(-25.0),
"shallow scroll capture bounds must not move the offscreen surface origin on adjacent scroll positions"
);
}
#[test]
fn estimate_layer_surface_rect_clips_active_xy_scroll_content() {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: -16.0,
y: -24.0,
width: 180.0,
height: 240.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
layer.translated_content_context = true;
layer.motion_context_animated = true;
layer.clip_to_bounds = true;
assert_eq!(
estimate_layer_surface_rect(&layer),
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
}
);
}
#[test]
fn estimate_layer_surface_rect_clips_deep_hidden_active_scroll_content() {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: -1200.0,
width: 120.0,
height: 1400.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
layer.translated_content_context = true;
layer.motion_context_animated = true;
layer.clip_to_bounds = true;
assert_eq!(
estimate_layer_surface_rect(&layer),
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
}
);
}
#[test]
fn estimate_layer_surface_rect_keeps_deep_scroll_capture_origin_stable() {
fn deep_scroll_surface_rect(content_y: f32) -> Rect {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: content_y,
width: 120.0,
height: 1400.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
layer.translated_content_context = true;
layer.motion_context_animated = true;
layer.clip_to_bounds = true;
estimate_layer_surface_rect(&layer)
}
assert_eq!(
deep_scroll_surface_rect(-1200.0),
deep_scroll_surface_rect(-1201.0),
"deep scroll capture bounds must not re-phase the offscreen surface origin on adjacent scroll positions"
);
}
#[test]
fn motion_stable_capture_bounds_bounds_shadows_for_clipped_effect_layer() {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 72.0,
},
vec![],
);
layer.clip_to_bounds = true;
layer.graphics_layer.clip = true;
layer.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
let mut shadow_shape = test_shape(0, BlendMode::SrcOver);
shadow_shape.rect = Rect {
x: -24.0,
y: -1200.0,
width: 180.0,
height: 1400.0,
};
let mut scene = CompositorScene::new();
scene
.shadow_draws
.push(test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]));
let requirements = SurfaceRequirementSet::default()
.with(SurfaceRequirement::RenderEffect)
.with(SurfaceRequirement::MotionStableCapture);
assert_eq!(
motion_stable_capture_bounds(
&layer,
&scene,
&[],
requirements,
TranslatedContentAxes::default(),
None,
),
Some(Rect {
x: -360.0,
y: -216.0,
width: 480.0,
height: 288.0,
})
);
}
#[test]
fn vertical_motion_stable_capture_uses_viewport_cross_axis_bounds() {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 200.0,
height: 100.0,
},
vec![],
);
layer.clip_to_bounds = true;
layer.graphics_layer.clip = true;
let mut shape = test_shape(0, BlendMode::SrcOver);
shape.rect = Rect {
x: 60.0,
y: -80.0,
width: 80.0,
height: 220.0,
};
let mut scene = CompositorScene::new();
scene.shapes.push(shape);
let requirements =
SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
assert_eq!(
motion_stable_capture_bounds(
&layer,
&scene,
&[],
requirements,
TranslatedContentAxes { x: false, y: true },
None,
),
Some(Rect {
x: -96.0,
y: -300.0,
width: 296.0,
height: 400.0,
})
);
}
#[test]
fn vertical_motion_stable_capture_uses_external_surface_clip() {
let layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 200.0,
height: 100.0,
},
vec![],
);
let mut shape = test_shape(0, BlendMode::SrcOver);
shape.rect = Rect {
x: 60.0,
y: -80.0,
width: 80.0,
height: 220.0,
};
let mut scene = CompositorScene::new();
scene.shapes.push(shape);
let requirements =
SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
assert_eq!(
motion_stable_capture_bounds(
&layer,
&scene,
&[],
requirements,
TranslatedContentAxes { x: false, y: true },
Some(Rect {
x: 0.0,
y: 0.0,
width: 200.0,
height: 100.0,
}),
),
Some(Rect {
x: -96.0,
y: -300.0,
width: 296.0,
height: 400.0,
})
);
}
#[test]
fn estimate_layer_surface_rect_expands_for_child_layer_shadow() {
let mut child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 12.0,
height: 8.0,
},
vec![],
);
child.transform_to_parent = ProjectiveTransform::translation(20.0, 9.0);
child.graphics_layer.shadow_elevation = 6.0;
let parent = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 4.0,
height: 4.0,
},
vec![RenderNode::Layer(Box::new(child))],
);
let rect = estimate_layer_surface_rect(&parent);
assert!(rect.x < 20.0);
assert!(rect.y < 9.0);
assert!(rect.width > 12.0);
assert!(rect.height > 8.0);
}
#[test]
fn estimate_layer_surface_rect_respects_local_bounds_for_effect_layers() {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 28.0,
height: 28.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: 10.0,
y: 10.0,
width: 10.0,
height: 10.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
layer.graphics_layer.render_effect = Some(RenderEffect::blur(12.0));
assert_eq!(
estimate_layer_surface_rect(&layer),
Rect {
x: 0.0,
y: 0.0,
width: 28.0,
height: 28.0,
}
);
}
#[test]
fn layer_raster_cache_candidate_ignores_parent_transform() {
let primitive = PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
rect: Rect {
x: 2.0,
y: 3.0,
width: 6.0,
height: 4.0,
},
brush: Brush::solid(Color::BLACK),
},
clip: None,
}),
};
let base = cacheable_layer(
41,
Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
},
vec![RenderNode::Primitive(primitive.clone())],
);
let mut moved = base.clone();
moved.transform_to_parent = ProjectiveTransform::translation(32.0, 18.0);
assert_eq!(
layer_raster_cache_candidate(&base, 1.25, false, false),
layer_raster_cache_candidate(&moved, 1.25, false, false)
);
}
#[test]
fn layer_raster_cache_candidate_changes_for_child_transform() {
let mut child = cacheable_layer(
8,
Rect {
x: 0.0,
y: 0.0,
width: 12.0,
height: 10.0,
},
vec![],
);
child.transform_to_parent = ProjectiveTransform::translation(4.0, 6.0);
let base = cacheable_layer(
7,
Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
},
vec![RenderNode::Layer(Box::new(child.clone()))],
);
let mut moved_child = child;
moved_child.transform_to_parent = ProjectiveTransform::translation(9.0, 6.0);
let moved = cacheable_layer(
7,
Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
},
vec![RenderNode::Layer(Box::new(moved_child))],
);
assert_ne!(
layer_raster_cache_candidate(&base, 1.0, false, false),
layer_raster_cache_candidate(&moved, 1.0, false, false)
);
}
#[test]
fn layer_raster_cache_candidate_rejects_external_backdrop_dependency() {
let mut child = cacheable_layer(
12,
Rect {
x: 0.0,
y: 0.0,
width: 8.0,
height: 8.0,
},
vec![],
);
child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
let parent = cacheable_layer(
11,
Rect {
x: 0.0,
y: 0.0,
width: 16.0,
height: 16.0,
},
vec![RenderNode::Layer(Box::new(child))],
);
assert!(layer_raster_cache_candidate(&parent, 1.0, false, false).is_some());
assert!(layer_raster_cache_candidate(&parent, 1.0, true, false).is_none());
}
#[test]
fn layer_raster_cache_candidate_does_not_force_translation_only_text_surfaces() {
let text = RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
node_id: 77,
rect: Rect {
x: 2.0,
y: 3.0,
width: 48.0,
height: 18.0,
},
text: AnnotatedString::from("runtime cache"),
text_style: TextStyle::default(),
font_size: 14.0,
layout_options: TextLayoutOptions::default(),
clip: None,
})),
});
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 64.0,
height: 32.0,
},
vec![text],
);
layer.node_id = Some(77);
layer.recompute_raster_cache_hashes();
assert!(
layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
"root path should not isolate plain translation-only text layers"
);
assert!(
layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
"child path should also render plain translation-only text layers directly"
);
}
#[test]
fn layer_surface_requirements_keep_plain_text_on_direct_path() {
let layer = text_layer_with_style(AnnotatedString::from("plain"), TextStyle::default());
let requirements = layer_surface_requirements(&layer);
assert_eq!(requirements.direct_translation, Some(Point::default()));
assert!(requirements
.surface_requirements
.contains(SurfaceRequirement::PixelStableComposite));
assert!(!requirements
.surface_requirements
.has_isolating_requirement());
}
#[test]
fn layer_surface_requirements_keep_translated_plain_text_leaf_on_direct_path() {
let layer = pure_text_leaf(false, true);
let requirements = layer_surface_requirements(&layer);
assert_eq!(
requirements.direct_translation,
Some(Point::new(11.4, 23.6))
);
assert!(
requirements
.surface_requirements
.contains(SurfaceRequirement::PixelStableComposite)
&& !requirements
.surface_requirements
.has_isolating_requirement(),
"translated plain text should stay on the direct path and isolate only the glyph draw"
);
}
#[test]
fn layer_surface_requirements_keep_translated_text_leaf_with_background_on_direct_path() {
let layer = snapped_text_leaf(false, true);
let requirements = layer_surface_requirements(&layer);
assert_eq!(
requirements.direct_translation,
Some(Point::new(14.25, 16.5))
);
assert!(
requirements
.surface_requirements
.contains(SurfaceRequirement::PixelStableComposite)
&& !requirements
.surface_requirements
.has_isolating_requirement(),
"translated text with direct sibling decoration/background should keep the layer direct"
);
}
#[test]
fn translated_plain_text_uses_bounded_snap_effect_layer() {
let root = pure_text_leaf_root(true, true);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.child_layers.len(), 0);
assert_eq!(collected.scene.texts.len(), 1);
assert_eq!(
collected.scene.texts[0].snap_anchor,
Some(SnapAnchor::rigid(Point::new(11.4, 23.6))),
"translated plain text should use the same content-origin snap phase while active"
);
assert_eq!(collected.scene.effect_layers.len(), 1);
assert_eq!(
collected.scene.effect_layers[0].snap_anchor,
Some(SnapAnchor::rigid(Point::new(11.4, 23.6))),
"translated plain text's bounded local picture should composite at the same snap phase"
);
}
#[test]
fn non_translated_text_local_surface_keeps_linear_composite_resolve() {
let layer = text_layer_with_style(
AnnotatedString::from("gradient"),
TextStyle::from_span_style(SpanStyle {
brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
..SpanStyle::default()
}),
);
let requirements = layer_surface_requirements(&layer);
assert!(requirements
.surface_requirements
.contains(SurfaceRequirement::TextMaterialMask));
assert_eq!(
composite_sample_mode_for_requirements(false, false, requirements),
CompositeSampleMode::Linear
);
}
#[test]
fn inherited_translated_text_local_surface_uses_box4_layer_surface() {
let layer = text_layer_with_style(
AnnotatedString::from("shadow"),
TextStyle::from_span_style(SpanStyle {
shadow: Some(Shadow {
color: Color::BLACK,
offset: Point::new(1.0, 2.0),
blur_radius: 3.0,
}),
..SpanStyle::default()
}),
);
let requirements = layer_surface_requirements(&layer);
assert!(requirements
.surface_requirements
.contains(SurfaceRequirement::TextMaterialMask));
assert_eq!(
composite_sample_mode_for_requirements(true, false, requirements),
CompositeSampleMode::Box4
);
assert_eq!(
layer_surface_target_scale(true, false, requirements, 1.25),
SurfaceRequirementSet::default()
.with(SurfaceRequirement::TextMaterialMask)
.with(SurfaceRequirement::MotionStableCapture)
.target_scale(1.25)
);
}
#[test]
fn translated_text_local_surface_inside_capture_keeps_parent_scale() {
let layer = text_layer_with_style(
AnnotatedString::from("shadow"),
TextStyle::from_span_style(SpanStyle {
shadow: Some(Shadow {
color: Color::BLACK,
offset: Point::new(1.0, 2.0),
blur_radius: 3.0,
}),
..SpanStyle::default()
}),
);
let requirements = layer_surface_requirements(&layer);
assert_eq!(
composite_sample_mode_for_requirements(true, true, requirements),
CompositeSampleMode::Linear
);
assert_eq!(
layer_surface_target_scale(true, true, requirements, 10.0),
SurfaceRequirementSet::default()
.with(SurfaceRequirement::TextMaterialMask)
.target_scale(10.0)
);
}
#[test]
fn layer_surface_requirements_use_local_surface_for_gradient_and_stroke_text() {
let cases = [
(
"draw_style",
AnnotatedString::from("draw_style"),
TextStyle::from_span_style(SpanStyle {
draw_style: Some(TextDrawStyle::Stroke { width: 2.0 }),
..SpanStyle::default()
}),
),
(
"gradient_brush",
AnnotatedString::from("gradient"),
TextStyle::from_span_style(SpanStyle {
brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
..SpanStyle::default()
}),
),
];
for (label, text, text_style) in cases {
let layer = text_layer_with_style(text, text_style);
let requirements = layer_surface_requirements(&layer);
assert!(
requirements
.surface_requirements
.contains(SurfaceRequirement::TextMaterialMask),
"{label} text should use a bounded local surface: {requirements:?}"
);
}
}
#[test]
fn layer_surface_requirements_use_local_surface_for_complex_text_effects() {
let cases = [
(
"shadow",
AnnotatedString::from("shadow"),
TextStyle::from_span_style(SpanStyle {
shadow: Some(Shadow {
color: Color::BLACK,
offset: Point::new(1.0, 2.0),
blur_radius: 3.0,
}),
..SpanStyle::default()
}),
),
(
"background",
AnnotatedString::from("background"),
TextStyle::from_span_style(SpanStyle {
background: Some(Color::BLACK),
..SpanStyle::default()
}),
),
(
"baseline_shift",
AnnotatedString::from("baseline_shift"),
TextStyle::from_span_style(SpanStyle {
baseline_shift: Some(BaselineShift::SUPERSCRIPT),
..SpanStyle::default()
}),
),
(
"geometric_transform",
AnnotatedString::from("geometric_transform"),
TextStyle::from_span_style(SpanStyle {
text_geometric_transform: Some(TextGeometricTransform {
scale_x: 1.2,
skew_x: 0.15,
}),
..SpanStyle::default()
}),
),
(
"letter_spacing",
AnnotatedString::from("letter_spacing"),
TextStyle::from_span_style(SpanStyle {
letter_spacing: TextUnit::Em(0.2),
..SpanStyle::default()
}),
),
];
for (label, text, text_style) in cases {
let layer = text_layer_with_style(text, text_style);
let requirements = layer_surface_requirements(&layer);
assert!(
requirements
.surface_requirements
.contains(SurfaceRequirement::TextMaterialMask),
"{label} text should use a bounded local surface: {requirements:?}"
);
assert_eq!(
requirements.direct_translation,
Some(Point::default()),
"{label} text should still classify as a direct translation"
);
}
}
#[test]
fn layer_surface_requirements_color_only_span_styles_use_direct_path() {
let layer = text_layer_with_style(
AnnotatedString {
text: "styled".to_string(),
span_styles: vec![RangeStyle {
item: SpanStyle {
color: Some(Color::BLACK),
..SpanStyle::default()
},
range: 0..3,
}],
..AnnotatedString::default()
},
TextStyle::default(),
);
let requirements = layer_surface_requirements(&layer);
assert!(
!requirements
.surface_requirements
.contains(SurfaceRequirement::TextMaterialMask),
"color-only span styles should render directly via software text raster colors"
);
}
#[test]
fn layer_surface_requirements_keep_decoration_only_text_on_direct_path() {
let layer = text_layer_with_style(
AnnotatedString::from("decoration"),
TextStyle::from_span_style(SpanStyle {
text_decoration: Some(TextDecoration::UNDERLINE),
..SpanStyle::default()
}),
);
let requirements = layer_surface_requirements(&layer);
assert_eq!(requirements.direct_translation, Some(Point::default()));
assert!(
requirements
.surface_requirements
.contains(SurfaceRequirement::PixelStableComposite)
&& !requirements
.surface_requirements
.has_isolating_requirement(),
"decoration-only text should not force an isolating layer surface: {requirements:?}"
);
}
#[test]
fn direct_text_leaf_snaps_modifier_background_and_text_with_one_anchor() {
let root = snapped_text_leaf_root(false, false);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.scene.shapes.len(), 1);
assert_eq!(collected.scene.images.len(), 1);
assert_eq!(collected.scene.texts.len(), 1);
let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
assert_eq!(collected.scene.shapes[0].snap_anchor, expected_anchor);
assert_eq!(collected.scene.images[0].snap_anchor, expected_anchor);
assert_eq!(collected.scene.texts[0].snap_anchor, expected_anchor);
}
#[test]
fn animated_translated_content_text_leaf_uses_bounded_content_snap() {
let root = snapped_text_leaf_root(true, true);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.child_layers.len(), 0);
assert_eq!(collected.scene.shapes.len(), 1);
assert_eq!(collected.scene.images.len(), 1);
assert_eq!(collected.scene.texts.len(), 1);
assert_eq!(collected.scene.effect_layers.len(), 1);
let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
assert_eq!(
collected.scene.effect_layers[0].snap_anchor, expected_anchor,
"active translated leaf capture should keep the content-origin snap phase"
);
assert_eq!(
collected.scene.shapes[0].snap_anchor, expected_anchor,
"active scroll shapes should render with the same content-origin snap phase as settled content"
);
assert_eq!(
collected.scene.images[0].snap_anchor, expected_anchor,
"active scroll images should render with the same content-origin snap phase as settled content"
);
assert_eq!(
collected.scene.texts[0].snap_anchor, expected_anchor,
"active scroll text should render with the same content-origin snap phase as settled content"
);
}
#[test]
fn rested_translated_content_context_text_leaf_snaps_for_crisp_scroll_rest() {
let root = snapped_text_leaf_root(false, true);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.child_layers.len(), 0);
assert_eq!(collected.scene.shapes.len(), 1);
assert_eq!(collected.scene.images.len(), 1);
assert_eq!(collected.scene.texts.len(), 1);
assert_eq!(collected.scene.effect_layers.len(), 0);
let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
assert_eq!(
collected.scene.shapes[0].snap_anchor, expected_anchor,
"rested scroll content should snap back to device pixels"
);
assert_eq!(
collected.scene.images[0].snap_anchor, expected_anchor,
"rested scroll images should snap back to device pixels"
);
assert_eq!(
collected.scene.texts[0].snap_anchor, expected_anchor,
"rested scroll text should snap back to device pixels"
);
}
#[test]
fn complex_text_uses_local_surface() {
let root = translated_content_local_surface_root();
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert!(
!collected.child_layers.is_empty(),
"translated-content effectful text should render through a bounded local surface"
);
assert!(collected.scene.texts.is_empty());
assert!(collected.scene.shadow_draws.is_empty());
}
#[test]
fn translated_content_surface_composite_uses_scroll_content_snap_anchor() {
let mut root = translated_content_local_surface_root();
let scroll_offset = Point::new(0.0, -18.5);
let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
panic!("expected translated content layer");
};
translated_content.translated_content_offset = scroll_offset;
let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
panic!("expected effectful text layer");
};
effectful_text.transform_to_parent =
effectful_text
.transform_to_parent
.then(ProjectiveTransform::translation(
scroll_offset.x,
scroll_offset.y,
));
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.child_layers.len(), 1);
assert_eq!(
collected.child_layers[0].snap_anchor,
Some(SnapAnchor::rigid(Point::new(14.25, -2.0))),
"isolated scrolled descendants must composite with the same content-origin snap phase"
);
}
#[test]
fn animated_translated_content_surface_composite_uses_scroll_content_snap_anchor() {
let mut root = translated_content_local_surface_root();
let scroll_offset = Point::new(0.0, -18.5);
let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
panic!("expected translated content layer");
};
translated_content.motion_context_animated = true;
translated_content.translated_content_offset = scroll_offset;
let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
panic!("expected effectful text layer");
};
effectful_text.transform_to_parent =
effectful_text
.transform_to_parent
.then(ProjectiveTransform::translation(
scroll_offset.x,
scroll_offset.y,
));
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.child_layers.len(), 1);
assert_eq!(
collected.child_layers[0].snap_anchor,
Some(SnapAnchor::rigid(Point::new(14.25, -2.0))),
"animated scrolled descendants should composite with the same content-origin snap phase"
);
}
#[test]
fn translated_text_material_effect_layer_uses_scroll_content_snap_anchor() {
let mut layer = text_layer_with_style(
AnnotatedString::from("gradient"),
TextStyle::from_span_style(SpanStyle {
brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
..SpanStyle::default()
}),
);
layer.translated_content_context = true;
layer.translated_content_offset = Point::new(0.0, -18.5);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.scene.effect_layers.len(), 1);
assert_eq!(
composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
CompositeSampleMode::Box4
);
assert_eq!(
collected.scene.effect_layers[0].snap_anchor,
Some(SnapAnchor::rigid(Point::new(0.0, -18.5))),
"text material surfaces must composite with the scroll content-origin snap phase"
);
}
#[test]
fn translated_layer_surface_capture_does_not_restart_local_picture_for_shadow_text() {
let mut layer = text_layer_with_style(
AnnotatedString::from("shadow"),
TextStyle::from_span_style(SpanStyle {
shadow: Some(Shadow {
color: Color::BLACK,
offset: Point::new(1.0, 2.0),
blur_radius: 3.0,
}),
..SpanStyle::default()
}),
);
layer.translated_content_context = true;
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected = collect_layer_contents_with_translation_context(
&layer,
None,
None,
TranslationRenderContext {
inherited_content_translation: false,
surface_capture_active: true,
local_picture_capture_active: true,
..TranslationRenderContext::default()
},
&mut rect_cache,
&mut requirements_cache,
);
assert!(
collected.scene.effect_layers.is_empty(),
"a translated layer surface already provides the stable local capture"
);
assert_eq!(collected.scene.shadow_draws.len(), 1);
assert_eq!(collected.scene.texts.len(), 1);
assert!(
!collected.scene.texts[0].translated_content_context,
"text inside an active motion-stable capture must raster in capture-local coordinates"
);
}
#[test]
fn translated_layer_surface_capture_keeps_only_material_effect_layers() {
let mut layer = text_layer_with_style(
AnnotatedString::from("gradient"),
TextStyle::from_span_style(SpanStyle {
brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
..SpanStyle::default()
}),
);
layer.translated_content_context = true;
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected = collect_layer_contents_with_translation_context(
&layer,
None,
None,
TranslationRenderContext {
inherited_content_translation: false,
surface_capture_active: true,
local_picture_capture_active: true,
..TranslationRenderContext::default()
},
&mut rect_cache,
&mut requirements_cache,
);
assert_eq!(collected.scene.effect_layers.len(), 1);
assert!(
collected.scene.effect_layers[0]
.requirements
.contains(SurfaceRequirement::MotionStableCapture),
"translated text materials still need motion-stable resolve semantics inside a stable capture"
);
assert_eq!(
composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
CompositeSampleMode::Box4
);
assert_eq!(
effect_layer_target_scale(&collected.scene.effect_layers[0], 10.0),
10.0
);
assert!(collected.scene.effect_layers[0].effect.is_some());
}
#[test]
fn translated_viewport_surface_does_not_add_plain_local_picture_capture() {
let mut layer = text_layer_with_style(
AnnotatedString::from("shadow"),
TextStyle::from_span_style(SpanStyle {
shadow: Some(Shadow {
color: Color::BLACK,
offset: Point::new(1.0, 2.0),
blur_radius: 3.0,
}),
..SpanStyle::default()
}),
);
layer.translated_content_context = true;
layer.motion_context_animated = true;
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected = collect_layer_contents_with_translation_context(
&layer,
None,
None,
TranslationRenderContext {
surface_capture_active: true,
..TranslationRenderContext::default()
},
&mut rect_cache,
&mut requirements_cache,
);
assert_eq!(
collected.scene.effect_layers.len(),
0,
"plain translated content inside a viewport surface should not be captured again"
);
assert_eq!(collected.scene.shadow_draws.len(), 1);
assert_eq!(collected.scene.texts.len(), 1);
}
#[test]
fn static_pure_text_leaf_snaps_without_sibling_draw_primitives() {
let root = pure_text_leaf_root(false, false);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.scene.texts.len(), 1);
assert!(
collected.scene.texts[0].snap_anchor.is_some(),
"idle pure text leaves should participate in rigid snap anchoring"
);
}
#[test]
fn animated_pure_text_leaf_stays_unsnapped() {
let root = pure_text_leaf_root(true, false);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.scene.texts.len(), 1);
assert_eq!(collected.scene.texts[0].snap_anchor, None);
}
#[test]
fn animated_translated_pure_text_uses_bounded_content_snap() {
let root = pure_text_leaf_root(true, true);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
let expected_anchor = Some(SnapAnchor::rigid(Point::new(11.4, 23.6)));
assert_eq!(collected.child_layers.len(), 0);
assert_eq!(collected.scene.texts.len(), 1);
assert_eq!(collected.scene.effect_layers.len(), 1);
assert_eq!(collected.scene.texts[0].snap_anchor, expected_anchor);
assert_eq!(
collected.scene.effect_layers[0].snap_anchor,
expected_anchor
);
}
#[test]
fn rested_translated_pure_text_leaf_snaps_for_crisp_scroll_rest() {
let root = pure_text_leaf_root(false, true);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.child_layers.len(), 0);
assert_eq!(collected.scene.texts.len(), 1);
assert_eq!(collected.scene.effect_layers.len(), 0);
assert_eq!(
collected.scene.texts[0].snap_anchor,
Some(SnapAnchor::rigid(Point::new(11.4, 23.6))),
"rested translated text should snap to device pixels"
);
}
#[test]
fn static_gpu_effect_text_leaf_stays_unsnapped() {
let root = text_layer_with_style(
AnnotatedString::from("Gradient"),
TextStyle::from_span_style(SpanStyle {
brush: Some(Brush::linear_gradient(vec![
Color(0.2, 0.8, 1.0, 1.0),
Color(1.0, 0.7, 0.4, 1.0),
])),
draw_style: Some(TextDrawStyle::Stroke { width: 2.5 }),
..SpanStyle::default()
}),
);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(collected.scene.texts.len(), 1);
assert_eq!(
collected.scene.texts[0].snap_anchor, None,
"gpu text-effect leaves must not take the rigid text snap path"
);
assert_eq!(
collected.scene.effect_layers.len(),
1,
"gradient stroke text should still emit a runtime shader effect layer"
);
}
#[test]
fn layer_surface_requirements_keep_shape_plus_direct_child_on_direct_path() {
let mut child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 40.0,
height: 20.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
width: 40.0,
height: 20.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
let layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 64.0,
height: 32.0,
},
vec![
RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
width: 64.0,
height: 32.0,
},
brush: Brush::solid(Color::BLACK),
},
clip: None,
}),
}),
RenderNode::Layer(Box::new(child)),
],
);
let requirements = layer_surface_requirements(&layer);
assert_eq!(requirements.direct_translation, Some(Point::default()));
assert!(!requirements
.surface_requirements
.contains(SurfaceRequirement::MixedDirectContent));
assert!(!requirements
.surface_requirements
.has_isolating_requirement());
}
#[test]
fn collect_layer_contents_translates_direct_text_rects_into_parent_space() {
let mut child = text_layer_with_style(
AnnotatedString::from("direct"),
TextStyle::from_span_style(SpanStyle {
text_decoration: Some(TextDecoration::UNDERLINE),
..SpanStyle::default()
}),
);
child.transform_to_parent = ProjectiveTransform::translation(9.0, 7.0);
let parent = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 64.0,
height: 32.0,
},
vec![RenderNode::Layer(Box::new(child))],
);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected = with_test_app_context(|| {
collect_layer_contents(
&parent,
None,
None,
&mut rect_cache,
&mut requirements_cache,
)
});
assert!(
collected.child_layers.is_empty(),
"decoration-only text child should collapse directly into the parent scene"
);
assert_eq!(collected.scene.texts.len(), 1, "expected one text draw");
let text = &collected.scene.texts[0];
assert!(
text.rect.x >= 9.0 && text.rect.y >= 7.0,
"collapsed text rect should be translated into parent space, got {:?}",
text.rect
);
assert!(
collected
.scene
.shapes
.iter()
.any(|shape| shape.rect.y >= 7.0),
"collapsed underline geometry should also be translated into parent space"
);
}
#[test]
fn direct_translation_accepts_nearly_identity_axis_scale_noise() {
let local_bounds = Rect {
x: 0.0,
y: 0.0,
width: 393.3,
height: 16.8,
};
let quad = [
[10.0, 78.399_994],
[403.3, 78.399_994],
[10.0, 95.2],
[403.3, 95.2],
];
let transform = ProjectiveTransform::from_rect_to_quad(local_bounds, quad);
assert_eq!(
direct_translation(transform),
Some(Point::new(10.0, 78.399_994)),
);
}
#[test]
fn layer_surface_requirements_keep_shape_plus_isolating_child_as_mixed_content() {
let mut child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 24.0,
height: 18.0,
},
vec![RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
width: 24.0,
height: 18.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
})],
);
child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
child.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
let layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 64.0,
height: 32.0,
},
vec![
RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
width: 64.0,
height: 32.0,
},
brush: Brush::solid(Color::BLACK),
},
clip: None,
}),
}),
RenderNode::Layer(Box::new(child)),
],
);
let requirements = layer_surface_requirements(&layer);
assert!(requirements
.surface_requirements
.contains(SurfaceRequirement::MixedDirectContent));
assert!(!requirements
.surface_requirements
.has_isolating_requirement());
}
#[test]
fn build_scene_window_filters_and_translates_items() {
let mut shape = test_shape(6, BlendMode::SrcOver);
shape.rect.x = 12.0;
shape.rect.y = 25.0;
shape.local_rect.x = 12.0;
shape.local_rect.y = 25.0;
shape.quad = [[12.0, 25.0], [20.0, 25.0], [12.0, 33.0], [20.0, 33.0]];
shape.clip = Some(Rect {
x: 11.0,
y: 24.0,
width: 10.0,
height: 10.0,
});
let mut image = test_image(8, BlendMode::SrcOver);
image.rect.x = 18.0;
image.rect.y = 27.0;
image.local_rect.x = 18.0;
image.local_rect.y = 27.0;
image.quad = [[18.0, 27.0], [26.0, 27.0], [18.0, 35.0], [26.0, 35.0]];
let mut text = test_text(9);
text.rect.x = 16.0;
text.rect.y = 29.0;
text.clip = Some(Rect {
x: 15.0,
y: 28.0,
width: 9.0,
height: 6.0,
});
let mut shadow_shape = test_shape(7, BlendMode::SrcOver);
shadow_shape.rect.x = 14.0;
shadow_shape.rect.y = 26.0;
shadow_shape.local_rect.x = 14.0;
shadow_shape.local_rect.y = 26.0;
shadow_shape.quad = [[14.0, 26.0], [22.0, 26.0], [14.0, 34.0], [22.0, 34.0]];
let mut shadow = test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]);
shadow.z_index = 7;
let mut nested_effect = effect_layer(6, 10);
nested_effect.rect.x = 13.0;
nested_effect.rect.y = 24.0;
nested_effect.clip = Some(Rect {
x: 15.0,
y: 25.0,
width: 4.0,
height: 5.0,
});
let mut nested_backdrop = backdrop_layer(8);
nested_backdrop.rect.x = 17.0;
nested_backdrop.rect.y = 26.0;
nested_backdrop.clip = Some(Rect {
x: 18.0,
y: 27.0,
width: 3.0,
height: 4.0,
});
let window = build_scene_window(
SceneWindowSource {
shapes: &[test_shape(4, BlendMode::SrcOver), shape],
images: &[image],
texts: &[text],
shadow_draws: &[shadow],
draw_ops: &[],
effect_layers: &[effect_layer(2, 4), nested_effect.clone()],
backdrop_layers: &[backdrop_layer(4), nested_backdrop.clone()],
},
5,
10,
Rect {
x: 10.0,
y: 20.0,
width: 20.0,
height: 20.0,
},
);
assert_eq!(window.shapes.len(), 1);
assert_eq!(
window.shapes[0].rect,
Rect {
x: 2.0,
y: 5.0,
width: 8.0,
height: 8.0,
}
);
assert_eq!(
window.shapes[0].clip,
Some(Rect {
x: 1.0,
y: 4.0,
width: 10.0,
height: 10.0,
})
);
assert_eq!(window.images.len(), 1);
assert_eq!(window.images[0].rect.x, 8.0);
assert_eq!(window.images[0].rect.y, 7.0);
assert_eq!(window.texts.len(), 1);
assert_eq!(window.texts[0].rect.x, 6.0);
assert_eq!(window.texts[0].rect.y, 9.0);
assert_eq!(
window.texts[0].clip,
Some(Rect {
x: 5.0,
y: 8.0,
width: 9.0,
height: 6.0,
})
);
assert_eq!(window.shadow_draws.len(), 1);
assert_eq!(window.shadow_draws[0].shapes[0].0.rect.x, 4.0);
assert_eq!(window.shadow_draws[0].shapes[0].0.rect.y, 6.0);
assert_eq!(window.effect_layers.len(), 1);
assert_eq!(
window.effect_layers[0].rect,
Rect {
x: 3.0,
y: 4.0,
width: 10.0,
height: 10.0,
}
);
assert_eq!(
window.effect_layers[0].clip,
Some(Rect {
x: 5.0,
y: 5.0,
width: 4.0,
height: 5.0,
})
);
assert_eq!(window.backdrop_layers.len(), 1);
assert_eq!(
window.backdrop_layers[0].rect,
Rect {
x: 7.0,
y: 6.0,
width: 10.0,
height: 10.0,
}
);
assert_eq!(
window.backdrop_layers[0].clip,
Some(Rect {
x: 8.0,
y: 7.0,
width: 3.0,
height: 4.0,
})
);
}
#[test]
fn filtered_effect_layer_index_counts_only_window_members() {
let effects = vec![
effect_layer(0, 2),
effect_layer(5, 12),
effect_layer(6, 10),
effect_layer(14, 20),
];
assert_eq!(filtered_effect_layer_index(&effects, 1, 5, 12), Some(0));
assert_eq!(filtered_effect_layer_index(&effects, 2, 5, 12), Some(1));
assert_eq!(filtered_effect_layer_index(&effects, 3, 5, 12), None);
}
#[test]
fn blend_mode_support_matrix_is_explicit() {
assert!(is_blend_mode_supported(BlendMode::SrcOver));
assert!(is_blend_mode_supported(BlendMode::DstOut));
assert!(!is_blend_mode_supported(BlendMode::Clear));
assert!(!is_blend_mode_supported(BlendMode::Multiply));
}
#[test]
fn collect_non_effect_segment_items_preserves_global_z_order() {
let shapes = vec![
test_shape(3, BlendMode::SrcOver),
test_shape(1, BlendMode::DstOut),
];
let images = vec![test_image(2, BlendMode::SrcOver)];
let texts = vec![test_text(0)];
let shadows: Vec<ShadowDraw> = Vec::new();
let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
let mut scratch = Vec::new();
collect_non_effect_segment_items(
&shapes,
&images,
&texts,
&shadows,
&draw_ops,
0,
4,
&[],
&mut scratch,
);
let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
assert_eq!(
items,
vec![
SegmentDrawItem::Text(0),
SegmentDrawItem::Shape(1),
SegmentDrawItem::Image(0),
SegmentDrawItem::Shape(0),
]
);
}
#[test]
fn collect_non_effect_segment_items_filters_effect_ranges() {
let shapes = vec![
test_shape(1, BlendMode::SrcOver),
test_shape(3, BlendMode::DstOut),
];
let images = vec![test_image(2, BlendMode::SrcOver)];
let texts = vec![test_text(4)];
let shadows: Vec<ShadowDraw> = Vec::new();
let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
let effect_ranges = [std::ops::Range { start: 2, end: 4 }];
let mut scratch = Vec::new();
collect_non_effect_segment_items(
&shapes,
&images,
&texts,
&shadows,
&draw_ops,
0,
5,
&effect_ranges,
&mut scratch,
);
let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
assert_eq!(
items,
vec![SegmentDrawItem::Shape(0), SegmentDrawItem::Text(0)]
);
}
#[test]
fn segment_command_iter_merges_non_conflicting_batches_into_one_chunk() {
let ordered_items = vec![
(0, SegmentDrawItem::Shape(0)),
(1, SegmentDrawItem::Image(0)),
(2, SegmentDrawItem::Text(0)),
];
let shapes = vec![test_shape(0, BlendMode::SrcOver)];
let images = vec![test_image(1, BlendMode::DstOut)];
let commands: Vec<_> = SegmentCommandIter::new(&ordered_items, &shapes, &images).collect();
assert_eq!(
commands,
vec![SegmentRenderCommand::DrawChunk(chunk(&[
SegmentBatchPlan::Shape {
start: 0,
end: 1,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Image {
start: 1,
end: 2,
blend_mode: BlendMode::DstOut,
},
SegmentBatchPlan::Text { start: 2, end: 3 },
]))]
);
}
#[test]
fn segment_command_iter_keeps_repeated_batch_kinds_in_one_chunk() {
let ordered_items = vec![
(0, SegmentDrawItem::Shape(0)),
(1, SegmentDrawItem::Image(0)),
(2, SegmentDrawItem::Shape(1)),
];
let shapes = vec![
test_shape(0, BlendMode::SrcOver),
test_shape(2, BlendMode::DstOut),
];
let images = vec![test_image(1, BlendMode::SrcOver)];
let commands: Vec<_> = SegmentCommandIter::new(&ordered_items, &shapes, &images).collect();
assert_eq!(
commands,
vec![SegmentRenderCommand::DrawChunk(chunk(&[
SegmentBatchPlan::Shape {
start: 0,
end: 1,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Image {
start: 1,
end: 2,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Shape {
start: 2,
end: 3,
blend_mode: BlendMode::DstOut,
},
]))]
);
}
#[test]
fn segment_command_iter_splits_contiguous_shape_runs_at_uniform_batch_limit() {
let ordered_items: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
.map(|index| (index, SegmentDrawItem::Shape(index)))
.collect();
let shapes: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
.map(|index| test_shape(index, BlendMode::SrcOver))
.collect();
let images = Vec::new();
let commands: Vec<_> = SegmentCommandIter::new(&ordered_items, &shapes, &images).collect();
assert_eq!(
commands,
vec![SegmentRenderCommand::DrawChunk(chunk(&[
SegmentBatchPlan::Shape {
start: 0,
end: MAX_SHAPES_PER_BATCH,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Shape {
start: MAX_SHAPES_PER_BATCH,
end: MAX_SHAPES_PER_BATCH + 1,
blend_mode: BlendMode::SrcOver,
},
]))]
);
}
#[test]
fn segment_command_iter_keeps_shadows_as_explicit_boundaries() {
let ordered_items = vec![
(0, SegmentDrawItem::Shape(0)),
(1, SegmentDrawItem::Shadow(0)),
(2, SegmentDrawItem::Image(0)),
(3, SegmentDrawItem::Text(0)),
];
let shapes = vec![test_shape(0, BlendMode::SrcOver)];
let images = vec![test_image(2, BlendMode::SrcOver)];
let commands: Vec<_> = SegmentCommandIter::new(&ordered_items, &shapes, &images).collect();
assert_eq!(
commands,
vec![
SegmentRenderCommand::DrawChunk(chunk(&[SegmentBatchPlan::Shape {
start: 0,
end: 1,
blend_mode: BlendMode::SrcOver,
}])),
SegmentRenderCommand::Shadow(0),
SegmentRenderCommand::DrawChunk(chunk(&[
SegmentBatchPlan::Image {
start: 2,
end: 3,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Text { start: 3, end: 4 },
])),
]
);
}
#[test]
fn staged_buffer_uploads_align_new_copies_to_copy_buffer_alignment() {
let mut uploads = StagedBufferUploads::default();
uploads.bytes.extend_from_slice(&[1, 2]);
uploads.stage(UploadTarget::ImageIndex, &[3, 4, 5, 6]);
assert_eq!(uploads.bytes, vec![1, 2, 0, 0, 3, 4, 5, 6]);
assert_eq!(
uploads.copies,
vec![PendingBufferCopy {
source_offset: 4,
size: 4,
target: UploadTarget::ImageIndex,
}]
);
}
#[test]
fn staged_buffer_uploads_ignore_empty_payloads() {
let mut uploads = StagedBufferUploads::default();
uploads.stage(UploadTarget::Uniform, &[]);
assert!(uploads.is_empty());
assert!(uploads.bytes.is_empty());
}
#[test]
fn staged_buffer_uploads_return_exact_payload_slice_for_copy() {
let mut uploads = StagedBufferUploads::default();
uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
assert_eq!(uploads.payload_for_copy(uploads.copies[1]), &[5, 6, 7, 8]);
}
#[test]
fn inner_shadow_composite_mask_uses_fill_shape_and_scale() {
let mut fill = test_shape(0, BlendMode::SrcOver);
fill.local_rect = Rect {
x: 10.0,
y: 12.0,
width: 40.0,
height: 20.0,
};
fill.shape = Some(RoundedCornerShape::uniform(6.0));
let cutout = test_shape(1, BlendMode::DstOut);
let shadow = test_shadow_draw(vec![
(fill, BlendMode::SrcOver),
(cutout, BlendMode::DstOut),
]);
let mask = inner_shadow_composite_mask(&shadow, 1.5).expect("inner mask expected");
assert_eq!(mask.rect, [15.0, 18.0, 60.0, 30.0]);
assert_eq!(mask.radii, [9.0, 9.0, 9.0, 9.0]);
}
#[test]
fn inner_shadow_composite_mask_is_none_without_dst_out() {
let fill = test_shape(0, BlendMode::SrcOver);
let shadow = test_shadow_draw(vec![(fill, BlendMode::SrcOver)]);
assert!(inner_shadow_composite_mask(&shadow, 1.0).is_none());
}
#[test]
fn render_effect_support_matrix_covers_all_variants() {
let blur = RenderEffect::blur(4.0);
let offset = RenderEffect::offset(2.0, 3.0);
let shader = RenderEffect::runtime_shader(cranpose_ui_graphics::RuntimeShader::new(
r#"
@group(0) @binding(0) var input_texture: texture_2d<f32>;
@group(0) @binding(1) var input_sampler: sampler;
@group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex
fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var output: VertexOutput;
let x = f32(i32(vertex_index & 1u) * 2 - 1);
let y = f32(i32(vertex_index >> 1u) * 2 - 1);
output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
output.position = vec4<f32>(x, y, 0.0, 1.0);
return output;
}
@fragment
fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(input_texture, input_sampler, input.uv);
}
"#,
));
let chain = blur.clone().then(offset.clone());
assert!(is_render_effect_supported(&blur));
assert!(is_render_effect_supported(&offset));
assert!(is_render_effect_supported(&shader));
assert!(is_render_effect_supported(&chain));
}
#[test]
fn clip_to_bounds_propagates_visual_clip_to_all_descendant_shapes() {
let container_local_bounds = Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 500.0,
};
let container_clip_in_parent = Rect {
x: 0.0,
y: 50.0,
width: 800.0,
height: 500.0,
};
let shape_above = RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Rect {
rect: Rect {
x: 10.0,
y: -30.0,
width: 100.0,
height: 40.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
});
let shape_inside = RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Rect {
rect: Rect {
x: 10.0,
y: 100.0,
width: 100.0,
height: 40.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
});
let shape_below = RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Rect {
rect: Rect {
x: 10.0,
y: 600.0,
width: 100.0,
height: 40.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
});
let mut content_layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 1000.0,
},
vec![shape_above, shape_inside, shape_below],
);
content_layer.transform_to_parent = ProjectiveTransform::translation(0.0, -30.0);
content_layer.translated_content_context = true;
let mut clip_container = test_layer(
container_local_bounds,
vec![RenderNode::Layer(Box::new(content_layer))],
);
clip_container.clip_to_bounds = true;
clip_container.transform_to_parent = ProjectiveTransform::translation(0.0, 50.0);
let root = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 600.0,
},
vec![RenderNode::Layer(Box::new(clip_container))],
);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(
collected.scene.shapes.len(),
3,
"all three shapes should be flattened into the scene"
);
for (i, shape) in collected.scene.shapes.iter().enumerate() {
assert!(
shape.clip.is_some(),
"shape {} at rect {:?} must have a clip from clip_to_bounds container, but clip is None",
i,
shape.rect
);
let clip = shape.clip.unwrap();
assert_eq!(
clip, container_clip_in_parent,
"shape {} clip should match the clip_to_bounds container bounds in parent space",
i
);
}
}
#[test]
fn clip_to_bounds_culls_child_layers_outside_boundary() {
let clip_container_bounds = Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 500.0,
};
let shape_in_card = RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
width: 300.0,
height: 80.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
});
let mut card_outside = crate::test_support::layer_node(
Rect {
x: 0.0,
y: 0.0,
width: 300.0,
height: 80.0,
},
ProjectiveTransform::identity(),
GraphicsLayer {
clip: true,
..GraphicsLayer::default()
},
vec![shape_in_card.clone()],
);
card_outside.transform_to_parent = ProjectiveTransform::translation(10.0, 600.0);
let mut card_inside = crate::test_support::layer_node(
Rect {
x: 0.0,
y: 0.0,
width: 300.0,
height: 80.0,
},
ProjectiveTransform::identity(),
GraphicsLayer {
clip: true,
..GraphicsLayer::default()
},
vec![shape_in_card],
);
card_inside.transform_to_parent = ProjectiveTransform::translation(10.0, 100.0);
let content = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 1000.0,
},
vec![
RenderNode::Layer(Box::new(card_inside)),
RenderNode::Layer(Box::new(card_outside)),
],
);
let mut clip_container = test_layer(
clip_container_bounds,
vec![RenderNode::Layer(Box::new(content))],
);
clip_container.clip_to_bounds = true;
let root = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 600.0,
},
vec![RenderNode::Layer(Box::new(clip_container))],
);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert_eq!(
collected.scene.shapes.len(),
1,
"only the card inside the clip boundary should produce shapes; \
the card outside must be culled entirely"
);
let shape = &collected.scene.shapes[0];
assert!(
shape.clip.is_some(),
"the visible card's shape must have a clip from clip_to_bounds"
);
}
#[test]
fn flattened_layer_shadow_z_index_is_below_content() {
let shape = RenderNode::Primitive(PrimitiveEntry {
phase: PrimitivePhase::BeforeChildren,
node: PrimitiveNode::Draw(DrawPrimitiveNode {
primitive: DrawPrimitive::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
},
brush: Brush::solid(Color::WHITE),
},
clip: None,
}),
});
let child_bounds = Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let child = crate::test_support::layer_node(
child_bounds,
ProjectiveTransform::translation(50.0, 50.0),
GraphicsLayer {
shadow_elevation: 20.0,
..GraphicsLayer::default()
},
vec![shape],
);
let root = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 800.0,
height: 600.0,
},
vec![RenderNode::Layer(Box::new(child))],
);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected =
collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
assert!(
!collected.scene.shadow_draws.is_empty(),
"shadow_elevation > 0 must produce shadow draws"
);
let max_shadow_z = collected
.scene
.shadow_draws
.iter()
.map(|s| s.z_index)
.max()
.unwrap();
let min_content_z = collected
.scene
.shapes
.iter()
.map(|s| s.z_index)
.min()
.unwrap();
assert!(
max_shadow_z < min_content_z,
"shadow z-index ({}) must be less than content z-index ({}); \
shadows must render behind their content",
max_shadow_z,
min_content_z
);
}
}