use crate::effect_renderer::{
projective_dest_bounds_rect, CompositeBatchItem, CompositeSampleMode, EffectRenderer,
EffectScratchTargetProvider, ProjectiveSurfaceComposite, RoundedCompositeMask,
ShaderCompositeBatchItem,
};
use crate::frame_graph::{
FrameCommandRecorder, FrameTextureDescriptor, WgpuFrameGraph, WgpuFrameGraphExecutor,
};
use crate::layer_events::{
collect_effect_ranges, collect_layer_events, LayerEvent, LayerEventKind,
};
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, SceneWindowSource,
};
use crate::normalized_scene::{
collect_layer_contents_with_translation_context_and_text_layout,
estimate_layer_surface_rect_cached, translate_quad, ChildLayerComposite, 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, backdrop_underlay_is_covered_by_local_content,
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, root_direct_scene_events_are_supported,
scaled_quad, snap_delta_for_anchor, snap_motion_stable_dest_quad, surface_target_size,
translation_stable_device_pixel_bounds, 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::{
collect_solid_text_atlas_run, measure_text_with_font,
rasterize_annotated_text_to_image_with_glyph_cache, rasterize_text_to_image_with_glyph_cache,
SoftwareGlyphAtlasGlyph, SoftwareGlyphAtlasKey, SoftwareGlyphAtlasPlacement,
SoftwareGlyphAtlasRunGlyph, SoftwareGlyphRasterCache, 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::borrow::Cow;
use std::cell::Cell;
use std::collections::{hash_map::DefaultHasher, HashMap};
use std::hash::{Hash, Hasher};
use std::ops::Range;
use std::rc::{Rc, Weak};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{mpsc, Arc};
use std::time::Duration;
use web_time::Instant;
use crate::gpu_stats;
use crate::gpu_stats::gpu_stats_enabled;
use crate::pipeline::push_layer_shadow;
#[cfg(target_arch = "wasm32")]
const MAX_SHAPES_PER_BATCH: usize = 200;
#[cfg(not(target_arch = "wasm32"))]
const MAX_SHAPES_PER_BATCH: usize = 768;
#[cfg(target_arch = "wasm32")]
const MAX_GRADIENT_STOPS: usize = 256;
#[cfg(not(target_arch = "wasm32"))]
const MAX_GRADIENT_STOPS: usize = 1024;
const HARD_MAX_BUFFER_MB: usize = 64; const MAX_SHADOW_SURFACE_CACHE_ITEMS: usize = 512;
const MAX_SHADOW_SURFACE_CACHE_BYTES: u64 = 192 * 1024 * 1024;
const MAX_TEXT_IMAGE_CACHE_ITEMS: usize = 1024;
const MAX_TEXT_GLYPH_MASK_CACHE_ITEMS: usize = 8192;
const MAX_TEXT_GLYPH_ATLAS_ITEMS: usize = 8192;
const MAX_TEXT_GLYPH_RUN_CACHE_ITEMS: usize = 1024;
#[cfg(not(target_arch = "wasm32"))]
const MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS: usize = 1024;
#[cfg(not(target_arch = "wasm32"))]
const MIN_RETAINED_TEXT_GLYPH_QUADS: usize = 192;
#[cfg(not(target_arch = "wasm32"))]
const OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS: f64 = 0.75;
#[cfg(not(target_arch = "wasm32"))]
const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES: usize = 2;
#[cfg(not(target_arch = "wasm32"))]
const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS: usize = 160;
#[cfg(not(target_arch = "wasm32"))]
const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS: usize = 160;
const TEXT_GLYPH_ATLAS_WIDTH: u32 = 4096;
const TEXT_GLYPH_ATLAS_HEIGHT: u32 = 4096;
const TEXT_GLYPH_ATLAS_PADDING: u32 = 1;
const MAX_TEXT_LINE_INDEX_CACHE_ITEMS: usize = 512;
const MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER: usize = 2;
const MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES: usize = 128;
const CACHE_MISS_WARMUP_FRAMES: u8 = 1;
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;
#[cfg(not(target_arch = "wasm32"))]
const INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS: usize = 128;
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;
const DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
#[cfg(not(target_arch = "wasm32"))]
static SEGMENT_DIAG_LINES: AtomicUsize = AtomicUsize::new(0);
fn wgpu_render_stage_telemetry_threshold_ms() -> Option<f64> {
static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
*THRESHOLD_MS.get_or_init(|| {
let explicit = std::env::var("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY_MS")
.ok()
.and_then(|value| value.parse::<f64>().ok())
.filter(|value| value.is_finite() && *value >= 0.0);
explicit.or_else(|| {
std::env::var_os("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY")
.is_some()
.then_some(DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS)
})
})
}
fn instant_ms(start: Instant, end: Instant) -> f64 {
end.duration_since(start).as_secs_f64() * 1000.0
}
fn should_log_wgpu_render_stage(start: Instant, end: Instant) -> Option<f64> {
let threshold_ms = wgpu_render_stage_telemetry_threshold_ms()?;
let total_ms = instant_ms(start, end);
(total_ms >= threshold_ms).then_some(total_ms)
}
fn admit_layer_surface_cache_miss_impl(
key: &LayerRasterCacheKey,
observed_scene_range_misses: &mut BoundedLruCache<LayerRasterCacheKey, ()>,
) -> bool {
if !key.is_scene_range() {
return true;
}
if observed_scene_range_misses.contains(key) {
return true;
}
observed_scene_range_misses.put(*key, ());
false
}
#[cfg(test)]
fn first_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
let mut observed_scene_range_misses =
BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
}
#[cfg(test)]
fn repeated_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
let mut observed_scene_range_misses =
BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
let _ = admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses);
admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
}
fn frame_stats_need_warmup_frame(snapshot: &gpu_stats::FrameStatsSnapshot) -> bool {
snapshot.layer_cache_misses > 0
|| snapshot.shadow_shape_cache_misses > 0
|| snapshot.text_image_cache_misses > 0
|| snapshot.text_glyph_atlas_misses > 0
}
fn text_atlas_fallback_diag_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_TEXT_ATLAS_FALLBACK_DIAG").is_some())
}
fn text_glyph_run_diag_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_TEXT_GLYPH_RUN_DIAG").is_some())
}
fn root_direct_diag_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_ROOT_DIRECT_DIAG").is_some())
}
fn scene_layer_events_precede_z(scene: &CompositorScene, z_index: usize) -> bool {
scene
.effect_layers
.iter()
.any(|layer| layer.z_start < z_index && 0 < layer.z_end)
|| scene
.backdrop_layers
.iter()
.any(|layer| layer.z_index < z_index)
}
fn direct_root_child_can_be_replayed_into_later_underlay(child: &ChildLayerComposite<'_>) -> bool {
child.layer.backdrop().is_none()
&& child.layer.effect().is_none()
&& child.shadow_draws.is_empty()
&& axis_aligned_quad_rect(child.dest_quad).is_some()
}
fn rects_overlap(a: Rect, b: Rect) -> bool {
let a_right = a.x + a.width;
let a_bottom = a.y + a.height;
let b_right = b.x + b.width;
let b_bottom = b.y + b.height;
a.x < b_right && b.x < a_right && a.y < b_bottom && b.y < a_bottom
}
fn direct_root_child_underlays_are_supported(collected: &CollectedLayer<'_>) -> bool {
for (child_index, child) in collected.child_layers.iter().enumerate() {
if child.layer.backdrop().is_some() {
if root_direct_diag_enabled() {
log::warn!(
"[root-direct-diag] reject self-backdrop child node={:?}",
child.layer.node_id
);
}
return false;
}
if child.needs_nested_underlay {
let Some(dest_rect) = axis_aligned_quad_rect(child.dest_quad) else {
if root_direct_diag_enabled() {
log::warn!(
"[root-direct-diag] reject projective underlay child node={:?}",
child.layer.node_id
);
}
return false;
};
let translation_only = (dest_rect.width - child.logical_rect.width).abs() <= 0.001
&& (dest_rect.height - child.logical_rect.height).abs() <= 0.001;
let unsupported_preceding_child_layer = collected.child_layers[..child_index]
.iter()
.any(|preceding| {
if direct_root_child_can_be_replayed_into_later_underlay(preceding) {
return false;
}
axis_aligned_quad_rect(preceding.dest_quad)
.is_none_or(|preceding_rect| rects_overlap(preceding_rect, dest_rect))
});
let preceding_scene_events =
scene_layer_events_precede_z(&collected.scene, child.z_index);
if unsupported_preceding_child_layer || preceding_scene_events || !translation_only {
if root_direct_diag_enabled() {
log::warn!(
"[root-direct-diag] reject underlay child node={:?} unsupported_preceding_child_layer={} preceding_scene_events={} translation_only={} dest=({:.1},{:.1},{:.1},{:.1}) logical=({:.1},{:.1},{:.1},{:.1})",
child.layer.node_id,
unsupported_preceding_child_layer,
preceding_scene_events,
translation_only,
dest_rect.x,
dest_rect.y,
dest_rect.width,
dest_rect.height,
child.logical_rect.x,
child.logical_rect.y,
child.logical_rect.width,
child.logical_rect.height
);
}
return false;
}
}
}
true
}
#[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,
}
struct CachedShadowComposite {
source: Rc<OffscreenTarget>,
scissor: Option<(u32, u32, u32, u32)>,
rounded_mask: Option<RoundedCompositeMask>,
dest_viewport: Option<(f32, f32, f32, f32)>,
}
impl CachedShadowComposite {
fn batch_item(&self) -> CompositeBatchItem<'_> {
CompositeBatchItem {
source: &self.source,
alpha: 1.0,
scissor: self.scissor,
rounded_mask: self.rounded_mask,
blend_mode: BlendMode::SrcOver,
dest_viewport: self.dest_viewport,
source_viewport: None,
sample_mode: CompositeSampleMode::Linear,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct TextImageCacheKey(u64);
struct CachedTextImage {
image: ImageBitmap,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct TextGlyphRunCacheKey(u64);
#[derive(Clone, Copy)]
struct CachedTextGlyphQuad {
x: i32,
y: i32,
width: usize,
height: usize,
color: (f32, f32, f32, f32),
uv: ImageUvRect,
}
struct CachedTextGlyphRun {
glyphs: Rc<[SoftwareGlyphAtlasPlacement]>,
quads: Option<Rc<[CachedTextGlyphQuad]>>,
atlas_generation: u64,
}
const TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER: f32 = 2.0;
#[cfg(not(target_arch = "wasm32"))]
struct CachedGpuTextGlyphRun {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_count: u32,
atlas_generation: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct TextLineIndexCacheKey(usize);
struct CachedTextLineIndex {
text: Weak<cranpose_ui::text::AnnotatedString>,
len: usize,
starts: Rc<[usize]>,
}
struct TextLineIndexCache {
entries: BoundedLruCache<TextLineIndexCacheKey, CachedTextLineIndex>,
}
impl TextLineIndexCache {
fn new(capacity: usize) -> Self {
Self {
entries: BoundedLruCache::with_capacity_at_least_one(capacity),
}
}
fn line_starts(&mut self, text: &Rc<cranpose_ui::text::AnnotatedString>) -> Rc<[usize]> {
let key = TextLineIndexCacheKey(Rc::as_ptr(text) as usize);
if let Some(cached) = self.entries.get(&key) {
if cached.len == text.text.len()
&& cached
.text
.upgrade()
.is_some_and(|cached_text| Rc::ptr_eq(&cached_text, text))
{
return cached.starts.clone();
}
}
let starts = Rc::<[usize]>::from(line_start_offsets(text.text.as_str()));
self.entries.put(
key,
CachedTextLineIndex {
text: Rc::downgrade(text),
len: text.text.len(),
starts: starts.clone(),
},
);
starts
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct ShapeShadowSurfacePlan {
source_device_bounds: DevicePixelBounds,
processing_scissor: Option<(u32, u32, u32, u32)>,
pixel_radius: f32,
}
#[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 direct_shader_composite_viewport(
alpha: f32,
blend_mode: BlendMode,
dest_viewport: Option<(f32, f32, f32, f32)>,
sample_mode: CompositeSampleMode,
source_size: (u32, u32),
) -> Option<(f32, f32, f32, f32)> {
if alpha != 1.0 || supported_blend_mode(blend_mode) != BlendMode::SrcOver {
return None;
}
let viewport = dest_viewport?;
if viewport.2 <= 0.0 || viewport.3 <= 0.0 {
return None;
}
match sample_mode {
CompositeSampleMode::Linear => Some(viewport),
CompositeSampleMode::Box4
if shader_composite_preserves_source_pixel_grid(viewport, source_size) =>
{
Some(viewport)
}
CompositeSampleMode::Box4 => None,
}
}
fn shader_composite_preserves_source_pixel_grid(
viewport: (f32, f32, f32, f32),
source_size: (u32, u32),
) -> bool {
const EPSILON: f32 = 0.01;
let (x, y, width, height) = viewport;
let (source_width, source_height) = source_size;
(x - x.round()).abs() <= EPSILON
&& (y - y.round()).abs() <= EPSILON
&& (width - source_width as f32).abs() <= EPSILON
&& (height - source_height as f32).abs() <= EPSILON
}
type DirectShaderTailComposite<'a> = (&'a RenderEffect, &'a RuntimeShader, (f32, f32, f32, f32));
fn direct_shader_tail_composite(
effect: &RenderEffect,
alpha: f32,
blend_mode: BlendMode,
dest_viewport: Option<(f32, f32, f32, f32)>,
sample_mode: CompositeSampleMode,
source_size: (u32, u32),
) -> Option<DirectShaderTailComposite<'_>> {
let viewport = direct_shader_composite_viewport(
alpha,
blend_mode,
dest_viewport,
sample_mode,
source_size,
)?;
let RenderEffect::Chain { first, second } = effect else {
return None;
};
let RenderEffect::Shader { shader } = second.as_ref() else {
return None;
};
Some((first.as_ref(), shader, viewport))
}
fn hash_f32_for_cache<H: Hasher>(value: f32, state: &mut H) {
value.to_bits().hash(state);
}
fn hash_text_raster_geometry_for_cache<H: Hasher>(
rect: Rect,
static_text_motion: bool,
state: &mut H,
) {
hash_f32_for_cache(rect.width, state);
hash_f32_for_cache(rect.height, state);
static_text_motion.hash(state);
if !static_text_motion {
hash_f32_for_cache(rect.x.fract(), state);
hash_f32_for_cache(rect.y.fract(), state);
}
}
fn text_raster_geometry_for_draw(
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 = text_draw
.snap_anchor
.map(|anchor| snap_delta_for_anchor(anchor, root_scale))
.unwrap_or_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_draw_is_visible_in_viewport(
logical_rect: Rect,
clip: Option<Rect>,
viewport: ViewportUniformParams,
root_scale: f32,
) -> bool {
draw_rect_is_visible_in_viewport(logical_rect, clip, viewport, root_scale)
}
fn text_draw_should_prewarm_in_viewport(
logical_rect: Rect,
clip: Option<Rect>,
viewport: ViewportUniformParams,
root_scale: f32,
) -> bool {
if !root_scale.is_finite() || root_scale <= 0.0 {
return false;
}
let viewport_rect = Rect {
x: viewport.offset[0] / root_scale,
y: viewport.offset[1] / root_scale,
width: viewport.width as f32 / root_scale,
height: viewport.height as f32 / root_scale,
};
let margin_x = viewport_rect.width * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
let margin_y = viewport_rect.height * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
let prewarm_viewport = expand_rect(viewport_rect, margin_x, margin_y);
let prewarm_rect = match clip {
Some(clip) => expand_rect(clip, margin_x, margin_y).intersect(prewarm_viewport),
None => Some(prewarm_viewport),
};
prewarm_rect.is_some_and(|rect| logical_rect.intersect(rect).is_some())
}
fn expand_rect(rect: Rect, margin_x: f32, margin_y: f32) -> Rect {
Rect {
x: rect.x - margin_x,
y: rect.y - margin_y,
width: rect.width + margin_x * 2.0,
height: rect.height + margin_y * 2.0,
}
}
fn draw_rect_is_visible_in_viewport(
rect: Rect,
clip: Option<Rect>,
viewport: ViewportUniformParams,
root_scale: f32,
) -> bool {
if !root_scale.is_finite() || root_scale <= 0.0 {
return false;
}
let viewport_rect = Rect {
x: viewport.offset[0] / root_scale,
y: viewport.offset[1] / root_scale,
width: viewport.width as f32 / root_scale,
height: viewport.height as f32 / root_scale,
};
let visible_rect = match clip {
Some(clip) => clip.intersect(viewport_rect),
None => Some(viewport_rect),
};
visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
}
fn shape_draw_is_visible_in_viewport(
shape: &DrawShape,
viewport: ViewportUniformParams,
root_scale: f32,
) -> bool {
let snap_delta = shape
.snap_anchor
.map(|anchor| snap_delta_for_anchor(anchor, root_scale))
.unwrap_or_default();
let rect = quad_bounds(translate_quad(shape.quad, snap_delta));
let clip = shape
.clip
.map(|clip| clip.translate(snap_delta.x, snap_delta.y));
draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale)
}
fn cached_text_glyph_quad(
glyph: &SoftwareGlyphAtlasPlacement,
entry: GlyphAtlasEntry,
) -> CachedTextGlyphQuad {
CachedTextGlyphQuad {
x: glyph.x,
y: glyph.y,
width: glyph.width,
height: glyph.height,
color: (
glyph.color.0.clamp(0.0, 1.0),
glyph.color.1.clamp(0.0, 1.0),
glyph.color.2.clamp(0.0, 1.0),
glyph.color.3.clamp(0.0, 1.0),
),
uv: glyph_atlas_uv_rect(entry),
}
}
fn append_cached_text_glyph_quad(
source_raster_rect: Rect,
quad: &CachedTextGlyphQuad,
image_vertices: &mut Vec<Vertex>,
image_indices: &mut Vec<u32>,
) -> bool {
if quad.width == 0 || quad.height == 0 || quad.color.3 <= 0.0 {
return false;
}
let base_vertex = image_vertices.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,
]);
let x0 = source_raster_rect.x + quad.x as f32;
let y0 = source_raster_rect.y + quad.y as f32;
let x1 = x0 + quad.width as f32;
let y1 = y0 + quad.height as f32;
let color = [quad.color.0, quad.color.1, quad.color.2, quad.color.3];
image_vertices.extend_from_slice(&[
Vertex {
position: [x0, y0],
color,
uv: [quad.uv.min[0], quad.uv.min[1]],
uv_bounds: quad.uv.sample_bounds,
},
Vertex {
position: [x1, y0],
color,
uv: [quad.uv.max[0], quad.uv.min[1]],
uv_bounds: quad.uv.sample_bounds,
},
Vertex {
position: [x0, y1],
color,
uv: [quad.uv.min[0], quad.uv.max[1]],
uv_bounds: quad.uv.sample_bounds,
},
Vertex {
position: [x1, y1],
color,
uv: [quad.uv.max[0], quad.uv.max[1]],
uv_bounds: quad.uv.sample_bounds,
},
]);
true
}
fn cached_text_glyph_quad_logical_rect(
source_raster_rect: Rect,
quad: &CachedTextGlyphQuad,
root_scale: f32,
) -> Option<Rect> {
if !root_scale.is_finite() || root_scale <= 0.0 {
return None;
}
Some(Rect {
x: (source_raster_rect.x + quad.x as f32) / root_scale,
y: (source_raster_rect.y + quad.y as f32) / root_scale,
width: quad.width as f32 / root_scale,
height: quad.height as f32 / root_scale,
})
}
fn cached_text_glyph_quad_is_visible_in_viewport(
source_raster_rect: Rect,
quad: &CachedTextGlyphQuad,
clip: Option<Rect>,
viewport: ViewportUniformParams,
root_scale: f32,
) -> bool {
cached_text_glyph_quad_logical_rect(source_raster_rect, quad, root_scale)
.is_some_and(|rect| draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TextGlyphDrawAction {
DrawVisible,
PrewarmOffscreen,
Skip,
}
fn text_glyph_draw_action(
is_visible: bool,
is_prewarm_candidate: bool,
allow_offscreen_prewarm: bool,
) -> TextGlyphDrawAction {
if is_visible {
TextGlyphDrawAction::DrawVisible
} else if allow_offscreen_prewarm && is_prewarm_candidate {
TextGlyphDrawAction::PrewarmOffscreen
} else {
TextGlyphDrawAction::Skip
}
}
#[cfg(not(target_arch = "wasm32"))]
fn should_use_retained_text_glyph_run(quads_len: usize, clip: Option<Rect>) -> bool {
clip.is_none() && quads_len >= MIN_RETAINED_TEXT_GLYPH_QUADS
}
#[cfg(not(target_arch = "wasm32"))]
fn offscreen_text_glyph_prewarm_work_is_bounded(
cached_glyphs: Option<usize>,
text_len: usize,
) -> bool {
match cached_glyphs {
Some(glyphs) => glyphs <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS,
None => text_len <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn offscreen_text_glyph_prewarm_budget_exhausted(
start: Instant,
admitted_candidates: usize,
) -> bool {
admitted_candidates >= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES
|| instant_ms(start, Instant::now()) >= OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS
}
fn text_draws_for_ordered_range<'a>(
ordered_items: &'a [(usize, SegmentDrawItem)],
texts: &'a [TextDraw],
start: usize,
end: usize,
) -> Result<impl Iterator<Item = &'a TextDraw>, String> {
let range_items = ordered_items
.get(start..end)
.ok_or_else(|| format!("text batch range {start}..{end} is outside ordered draw items"))?;
for (_, item) in range_items {
match item {
SegmentDrawItem::Text(text_index) if *text_index < texts.len() => {}
SegmentDrawItem::Text(text_index) => {
return Err(format!(
"text batch references missing text draw index: {text_index}"
));
}
_ => return Err(format!("text batch contains non-text draw item: {item:?}")),
}
}
Ok(range_items.iter().filter_map(move |(_, item)| match item {
SegmentDrawItem::Text(text_index) => texts.get(*text_index),
_ => None,
}))
}
const SHADOW_CACHE_DEVICE_QUANT: f32 = 16.0;
fn hash_shadow_device_offset<H: Hasher>(value: f32, origin: f32, root_scale: f32, state: &mut H) {
let quantized = ((value - origin) * root_scale * SHADOW_CACHE_DEVICE_QUANT).round();
(quantized as i64).hash(state);
}
fn hash_shadow_device_rect<H: Hasher>(
rect: Rect,
origin_x: f32,
origin_y: f32,
root_scale: f32,
state: &mut H,
) {
hash_shadow_device_offset(rect.x, origin_x, root_scale, state);
hash_shadow_device_offset(rect.y, origin_y, root_scale, state);
hash_shadow_device_offset(rect.width, 0.0, root_scale, state);
hash_shadow_device_offset(rect.height, 0.0, root_scale, state);
}
fn hash_shape_shadow_item<H: Hasher>(
shape: &DrawShape,
blend_mode: BlendMode,
origin_x: f32,
origin_y: f32,
root_scale: f32,
state: &mut H,
) {
hash_shadow_device_rect(shape.rect, origin_x, origin_y, root_scale, state);
hash_shadow_device_rect(shape.local_rect, origin_x, origin_y, root_scale, state);
for point in shape.quad {
hash_shadow_device_offset(point[0], origin_x, root_scale, state);
hash_shadow_device_offset(point[1], origin_y, root_scale, state);
}
match shape.snap_anchor {
Some(anchor) => {
1u8.hash(state);
hash_shadow_device_offset(anchor.origin.x, origin_x, root_scale, state);
hash_shadow_device_offset(anchor.origin.y, origin_y, root_scale, 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_shadow_device_rect(clip, origin_x, origin_y, root_scale, state);
}
None => 0u8.hash(state),
}
blend_mode.hash(state);
shape.blend_mode.hash(state);
}
fn shape_shadow_content_hash(shapes: &[(DrawShape, BlendMode)], root_scale: f32) -> u64 {
let mut hasher = DefaultHasher::new();
let origin = shape_shadow_bounds(shapes).unwrap_or(Rect {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
});
shapes.len().hash(&mut hasher);
for (shape, blend_mode) in shapes {
hash_shape_shadow_item(
shape,
*blend_mode,
origin.x,
origin.y,
root_scale,
&mut hasher,
);
}
hasher.finish()
}
fn shape_shadow_surface_cache_key(
shapes: &[(DrawShape, BlendMode)],
device_bounds: DevicePixelBounds,
pixel_radius: f32,
root_scale: f32,
) -> Option<ShadowSurfaceCacheKey> {
(root_scale.is_finite() && root_scale > 0.0).then(|| ShadowSurfaceCacheKey {
content_hash: shape_shadow_content_hash(shapes, root_scale),
pixel_size: [device_bounds.width, device_bounds.height],
root_scale_bits: root_scale.to_bits(),
blur_radius_bits: pixel_radius.to_bits(),
})
}
fn shape_shadow_bounds(shapes: &[(DrawShape, BlendMode)]) -> Option<Rect> {
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),
})
}
fn shadow_draw_bounds(shadow: &ShadowDraw) -> Option<Rect> {
shadow
.shapes
.iter()
.map(|(shape, _)| shape.rect)
.chain(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),
})
}
fn shadow_draw_may_render(
shadow: &ShadowDraw,
width: u32,
height: u32,
root_scale: f32,
max_texture_dim: u32,
) -> bool {
if shadow.texts.is_empty() && !shadow.shapes.is_empty() && shadow.blur_radius > 0.0 {
return shape_shadow_surface_plan(
&shadow.shapes,
shadow.clip,
shadow.blur_radius,
width,
height,
root_scale,
max_texture_dim,
)
.is_some();
}
let Some(bounds) = shadow_draw_bounds(shadow) else {
return false;
};
let blur_margin = blur_extent_margin(shadow.blur_radius);
let mut visible_bounds = Rect {
x: bounds.x - blur_margin,
y: bounds.y - blur_margin,
width: bounds.width + blur_margin * 2.0,
height: 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) = visible_bounds.intersect(clip_expanded) else {
return false;
};
visible_bounds = intersection;
}
scissor_rect_for_rect(visible_bounds, root_scale, width, height).is_some()
}
fn shape_shadow_surface_plan(
shapes: &[(DrawShape, BlendMode)],
clip: Option<Rect>,
blur_radius: f32,
width: u32,
height: u32,
root_scale: f32,
max_texture_dim: u32,
) -> Option<ShapeShadowSurfacePlan> {
let shape_bounds = shape_shadow_bounds(shapes)?;
let blur_margin = blur_extent_margin(blur_radius);
let source_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,
};
let mut visible_blur_bounds = source_blur_bounds;
if let Some(clip) = 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,
};
visible_blur_bounds = visible_blur_bounds.intersect(clip_expanded)?;
}
let processing_scissor = scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
processing_scissor?;
let visible_device_bounds =
device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)?;
let source_device_bounds =
translation_stable_device_pixel_bounds(source_blur_bounds, root_scale, max_texture_dim)
.unwrap_or(visible_device_bounds);
Some(ShapeShadowSurfacePlan {
source_device_bounds,
processing_scissor,
pixel_radius: blur_radius * root_scale,
})
}
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,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn shape_shader_source() -> Cow<'static, str> {
Cow::Owned(
shaders::SHADER
.replace(
"array<ShapeData, 200>",
&format!("array<ShapeData, {MAX_SHAPES_PER_BATCH}>"),
)
.replace(
"array<GradientStop, 256>",
&format!("array<GradientStop, {MAX_GRADIENT_STOPS}>"),
),
)
}
#[cfg(target_arch = "wasm32")]
fn shape_shader_source() -> Cow<'static, str> {
Cow::Borrowed(shaders::SHADER)
}
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(shape_shader_source()),
});
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,
})
}
fn create_glyph_atlas_pipeline(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
uniform_layout: &wgpu::BindGroupLayout,
image_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Glyph Atlas Shader"),
source: wgpu::ShaderSource::Wgsl(shaders::GLYPH_ATLAS_SHADER.into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Glyph Atlas Pipeline Layout"),
bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Glyph Atlas Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("glyph_atlas_vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[Vertex::desc()],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("glyph_atlas_fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
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,
}
}
}
#[derive(Clone, Copy)]
struct GlyphAtlasEntry {
x: u32,
y: u32,
width: u32,
height: u32,
}
struct TextGlyphAtlas {
texture: wgpu::Texture,
_view: wgpu::TextureView,
bind_group: wgpu::BindGroup,
entries: BoundedLruCache<SoftwareGlyphAtlasKey, GlyphAtlasEntry>,
generation: u64,
cursor_x: u32,
cursor_y: u32,
row_height: u32,
upload_scratch: Vec<u8>,
}
impl TextGlyphAtlas {
fn new(
device: &wgpu::Device,
image_layout: &wgpu::BindGroupLayout,
sampler: &wgpu::Sampler,
) -> Self {
let texture = Self::create_texture(device);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Text Glyph Atlas Bind Group"),
layout: image_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
});
Self {
texture,
_view: view,
bind_group,
entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
generation: 0,
cursor_x: TEXT_GLYPH_ATLAS_PADDING,
cursor_y: TEXT_GLYPH_ATLAS_PADDING,
row_height: 0,
upload_scratch: Vec::new(),
}
}
fn create_texture(device: &wgpu::Device) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("Text Glyph Atlas Texture"),
size: wgpu::Extent3d {
width: TEXT_GLYPH_ATLAS_WIDTH,
height: TEXT_GLYPH_ATLAS_HEIGHT,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
})
}
fn reset(
&mut self,
device: &wgpu::Device,
image_layout: &wgpu::BindGroupLayout,
sampler: &wgpu::Sampler,
) {
let generation = self.generation.wrapping_add(1);
let mut next = Self::new(device, image_layout, sampler);
next.generation = generation;
*self = next;
}
fn generation(&self) -> u64 {
self.generation
}
fn entry(&mut self, key: &SoftwareGlyphAtlasKey) -> Option<GlyphAtlasEntry> {
self.entries.get(key).copied()
}
fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
if width == 0
|| height == 0
|| width + TEXT_GLYPH_ATLAS_PADDING * 2 > TEXT_GLYPH_ATLAS_WIDTH
|| height + TEXT_GLYPH_ATLAS_PADDING * 2 > TEXT_GLYPH_ATLAS_HEIGHT
{
return None;
}
if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > TEXT_GLYPH_ATLAS_WIDTH {
self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
self.cursor_y = self
.cursor_y
.saturating_add(self.row_height)
.saturating_add(TEXT_GLYPH_ATLAS_PADDING);
self.row_height = 0;
}
if self.cursor_y + height + TEXT_GLYPH_ATLAS_PADDING > TEXT_GLYPH_ATLAS_HEIGHT {
return None;
}
let entry = GlyphAtlasEntry {
x: self.cursor_x,
y: self.cursor_y,
width,
height,
};
self.cursor_x = self
.cursor_x
.saturating_add(width)
.saturating_add(TEXT_GLYPH_ATLAS_PADDING);
self.row_height = self.row_height.max(height);
Some(entry)
}
fn upload_glyph(
&mut self,
key: SoftwareGlyphAtlasKey,
glyph: &SoftwareGlyphAtlasGlyph,
queue: &wgpu::Queue,
executor: &mut WgpuFrameGraphExecutor,
frame_stats: &mut gpu_stats::FrameStats,
) -> Option<GlyphAtlasEntry> {
if let Some(entry) = self.entry(&key) {
frame_stats.record_text_glyph_atlas_hit();
return Some(entry);
}
let width = u32::try_from(glyph.mask.width).ok()?;
let height = u32::try_from(glyph.mask.height).ok()?;
let entry = self.allocate(width, height)?;
self.upload_scratch.clear();
self.upload_scratch.reserve(
glyph
.mask
.alpha
.len()
.saturating_sub(self.upload_scratch.capacity()),
);
self.upload_scratch.extend(
glyph
.mask
.alpha
.iter()
.map(|alpha| (alpha.clamp(0.0, 1.0) * 255.0).round() as u8),
);
let upload_stats = executor.upload_texture(
queue,
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: entry.x,
y: entry.y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
&self.upload_scratch,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(entry.width),
rows_per_image: Some(entry.height),
},
wgpu::Extent3d {
width: entry.width,
height: entry.height,
depth_or_array_layers: 1,
},
);
frame_stats.record_command_stats(upload_stats);
frame_stats.record_text_glyph_atlas_miss(entry.width, entry.height);
self.entries.put(key, entry);
Some(entry)
}
}
struct ImageDrawCmd {
index_start: u32,
scissor: (u32, u32, u32, u32),
image_id: u64,
sampling: ImageSampling,
}
#[derive(Clone, Copy)]
enum GlyphDrawSource {
Shared {
index_start: u32,
index_count: u32,
},
#[cfg(not(target_arch = "wasm32"))]
Retained {
cache_key: TextGlyphRunCacheKey,
uniform_slot: usize,
},
}
#[derive(Clone, Copy)]
struct GlyphDrawCmd {
source: GlyphDrawSource,
scissor: (u32, u32, u32, u32),
}
impl GlyphDrawCmd {
fn shared(index_start: u32, index_count: u32, scissor: (u32, u32, u32, u32)) -> Self {
Self {
source: GlyphDrawSource::Shared {
index_start,
index_count,
},
scissor,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn retained(
cache_key: TextGlyphRunCacheKey,
uniform_slot: usize,
scissor: (u32, u32, u32, u32),
) -> Self {
Self {
source: GlyphDrawSource::Retained {
cache_key,
uniform_slot,
},
scissor,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct ImageUvRect {
min: [f32; 2],
max: [f32; 2],
sample_bounds: [f32; 4],
}
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,
#[cfg(not(target_arch = "wasm32"))]
RetainedGlyphUniform,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
struct PendingBufferCopy {
source_offset: u64,
target_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]) {
self.stage_at(target, 0, bytes);
}
#[cfg(not(target_arch = "wasm32"))]
fn stage_at(&mut self, target: UploadTarget, target_offset: u64, 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,
target_offset,
size: bytes.len() as u64,
target,
});
}
fn truncate(&mut self, bytes_len: usize, copies_len: usize) {
self.bytes.truncate(bytes_len);
self.copies.truncate(copies_len);
}
}
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,
glyph_atlas_pipeline: wgpu::RenderPipeline,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_atlas_pipeline: wgpu::RenderPipeline,
image_bind_group_layout: wgpu::BindGroupLayout,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_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(not(target_arch = "wasm32"))]
retained_glyph_uniform_buffer: wgpu::Buffer,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_bind_group: wgpu::BindGroup,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_stride: u64,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_capacity: usize,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_cursor: usize,
#[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>,
text_glyph_atlas: TextGlyphAtlas,
text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
#[cfg(not(target_arch = "wasm32"))]
text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedGpuTextGlyphRun>,
text_glyph_mask_cache: SoftwareGlyphRasterCache,
text_line_index_cache: TextLineIndexCache,
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_glyph_cmds: Vec<GlyphDrawCmd>,
scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
scratch_text_glyph_placements: Vec<SoftwareGlyphAtlasPlacement>,
scratch_text_glyph_quads: Vec<CachedTextGlyphQuad>,
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,
observed_scene_range_cache_misses: BoundedLruCache<LayerRasterCacheKey, ()>,
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>,
pending_frame_warmup_frames: u8,
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 runtime_cache_is_safe = allow_runtime_cache
&& surface_requirements
.surface_requirements
.has_isolating_requirement()
&& !layer
.effect()
.is_some_and(RenderEffect::contains_runtime_shader);
let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
|| (allow_runtime_cache && surface_requirements.has_renderer_forced_surface())
|| runtime_cache_is_safe;
if !cache_is_allowed {
return None;
}
if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
return None;
}
if layer
.effect()
.is_some_and(RenderEffect::contains_runtime_shader)
{
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,
}],
});
#[cfg(not(target_arch = "wasm32"))]
let retained_glyph_uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Retained Glyph Dynamic Uniform Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<Uniforms>() as u64
),
},
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,
);
let glyph_atlas_pipeline = create_glyph_atlas_pipeline(
&device,
surface_format,
&uniform_bind_group_layout,
&image_bind_group_layout,
);
#[cfg(not(target_arch = "wasm32"))]
let retained_glyph_atlas_pipeline = create_glyph_atlas_pipeline(
&device,
surface_format,
&retained_glyph_uniform_bind_group_layout,
&image_bind_group_layout,
);
#[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));
let text_glyph_atlas =
TextGlyphAtlas::new(&device, &image_bind_group_layout, &image_nearest_sampler);
#[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,
});
#[cfg(not(target_arch = "wasm32"))]
let retained_glyph_uniform_stride = align_usize_to(
std::mem::size_of::<Uniforms>(),
(device.limits().min_uniform_buffer_offset_alignment as usize)
.max(wgpu::COPY_BUFFER_ALIGNMENT as usize),
) as u64;
#[cfg(not(target_arch = "wasm32"))]
let retained_glyph_uniform_capacity = INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS;
#[cfg(not(target_arch = "wasm32"))]
let retained_glyph_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Retained Glyph Uniform Buffer"),
size: retained_glyph_uniform_stride * retained_glyph_uniform_capacity as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
#[cfg(not(target_arch = "wasm32"))]
let retained_glyph_uniform_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Retained Glyph Uniform Bind Group"),
layout: &retained_glyph_uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &retained_glyph_uniform_buffer,
offset: 0,
size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
}),
}],
});
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,
glyph_atlas_pipeline,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_atlas_pipeline,
image_bind_group_layout,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_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(not(target_arch = "wasm32"))]
retained_glyph_uniform_buffer,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_bind_group,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_stride,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_capacity,
#[cfg(not(target_arch = "wasm32"))]
retained_glyph_uniform_cursor: 0,
#[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,
),
text_glyph_atlas,
text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
MAX_TEXT_GLYPH_RUN_CACHE_ITEMS,
),
#[cfg(not(target_arch = "wasm32"))]
text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
),
text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
),
text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_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_glyph_cmds: Vec::new(),
scratch_text_glyph_run: Vec::new(),
scratch_text_glyph_placements: Vec::new(),
scratch_text_glyph_quads: 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(),
observed_scene_range_cache_misses: BoundedLruCache::with_capacity_at_least_one(
MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES,
),
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,
pending_frame_warmup_frames: 0,
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 admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
admit_layer_surface_cache_miss_impl(key, &mut self.observed_scene_range_cache_misses)
}
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 cached_shape_shadow_composite(
&mut self,
shadow: &ShadowDraw,
width: u32,
height: u32,
root_scale: f32,
) -> Option<CachedShadowComposite> {
if shadow.blur_radius <= 0.0 || shadow.shapes.is_empty() || !shadow.texts.is_empty() {
return None;
}
let plan = shape_shadow_surface_plan(
&shadow.shapes,
shadow.clip,
shadow.blur_radius,
width,
height,
root_scale,
self.max_texture_dim(),
)?;
let key = shape_shadow_surface_cache_key(
&shadow.shapes,
plan.source_device_bounds,
plan.pixel_radius,
root_scale,
)?;
let cached = self.cached_shadow_surface(&key)?;
let viewport_offset = [plan.source_device_bounds.x, plan.source_device_bounds.y];
self.frame_stats.record_shadow_shape_cache_hit(
plan.source_device_bounds.width,
plan.source_device_bounds.height,
);
let clip_scissor = shadow
.clip
.and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
let scissor = clip_scissor.or(plan.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],
plan.source_device_bounds.width as f32,
plan.source_device_bounds.height as f32,
));
Some(CachedShadowComposite {
source: cached,
scissor,
rounded_mask,
dest_viewport,
})
}
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 runtime_cache_is_safe = allow_runtime_cache
&& surface_requirements
.surface_requirements
.has_isolating_requirement()
&& !layer
.effect()
.is_some_and(RenderEffect::contains_runtime_shader);
let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
|| (allow_runtime_cache && surface_requirements.has_renderer_forced_surface())
|| runtime_cache_is_safe;
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) => {
let layer = &backdrop_layers[index];
let effective_backdrop_underlay = if backdrop_underlay.is_some()
&& backdrop_underlay_is_covered_by_local_content(
shapes,
images,
shadow_draws,
draw_ops,
effect_layers,
backdrop_layers,
layer,
) {
None
} else {
backdrop_underlay
};
execute_apply_backdrop_layer_to_target(
self,
target,
layer,
effective_backdrop_underlay,
width,
height,
root_scale,
None,
)?;
}
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();
if let Some(viewport) = direct_shader_composite_viewport(
alpha,
blend_mode,
dest_viewport,
sample_mode,
(source.width, source.height),
) {
let shader_applied = self
.renderer
.effect_renderer
.encode_shader_src_over_to_view(
self.recorder,
&device,
source,
dest_view,
shader,
effect_rect,
load_op,
scissor,
viewport,
);
if shader_applied {
self.renderer
.effect_renderer
.debug_effects
.set(self.renderer.effect_renderer.debug_effects.get() + 1);
self.recorder.record_pass();
self.renderer.effect_renderer.record_composite_pass();
return;
}
}
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_with_direct_shader_tail_composite(
&mut self,
source: &OffscreenTarget,
first_effect: &RenderEffect,
shader: &RuntimeShader,
effect_rect: [f32; 4],
dest_view: &wgpu::TextureView,
load_op: wgpu::LoadOp<wgpu::Color>,
scissor: Option<(u32, u32, u32, u32)>,
dest_viewport: (f32, f32, f32, f32),
) -> Result<bool, String> {
let device = self.renderer.device.clone();
let intermediate_descriptor = self.renderer.transient_offscreen_descriptor(
"Render Effect Direct Shader Tail Intermediate",
source.width,
source.height,
);
let intermediate = self
.recorder
.acquire_transient_offscreen(&device, intermediate_descriptor);
let effect_scratch_targets = self
.renderer
.effect_renderer
.acquire_recorded_effect_scratch_targets(
self.recorder,
&device,
first_effect,
source.width,
source.height,
self.renderer.surface_format,
);
let first_passes = {
let mut effect_scratch_refs = effect_scratch_targets.refs();
let pass_count = self.renderer.effect_renderer.encode_effect(
self.recorder,
&device,
source,
&intermediate.view,
first_effect,
effect_rect,
&mut effect_scratch_refs,
);
match pass_count {
Ok(pass_count) => effect_scratch_refs.assert_consumed().map(|()| pass_count),
Err(error) => Err(error),
}
};
let first_passes = match first_passes {
Ok(pass_count) => pass_count,
Err(error) => {
effect_scratch_targets.release_into(self.recorder);
self.recorder
.release_transient_offscreen(intermediate_descriptor, intermediate);
return Err(error);
}
};
let shader_applied = self
.renderer
.effect_renderer
.encode_shader_src_over_to_view(
self.recorder,
&device,
&intermediate,
dest_view,
shader,
effect_rect,
load_op,
scissor,
dest_viewport,
);
self.recorder
.record_passes(first_passes.saturating_add(u32::from(shader_applied)));
effect_scratch_targets.release_into(self.recorder);
self.recorder
.release_transient_offscreen(intermediate_descriptor, intermediate);
if !shader_applied {
return Ok(false);
}
self.renderer
.effect_renderer
.debug_effects
.set(self.renderer.effect_renderer.debug_effects.get() + 1);
self.renderer.effect_renderer.record_composite_pass();
Ok(true)
}
#[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> {
if let (
RenderEffect::Chain { first, second },
Some(viewport),
BlendMode::SrcOver,
CompositeSampleMode::Linear,
) = (
effect,
dest_viewport,
supported_blend_mode(blend_mode),
sample_mode,
) {
if let (
RenderEffect::Blur {
radius_x,
radius_y,
edge_treatment,
},
RenderEffect::Shader { shader },
) = (first.as_ref(), second.as_ref())
{
if *radius_x > 0.0 || *radius_y > 0.0 {
let device = self.renderer.device.clone();
let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
"Blur Rounded Mask Scratch",
source.width,
source.height,
);
let scratch = self
.recorder
.acquire_transient_offscreen(&device, scratch_descriptor);
let fused = self
.renderer
.effect_renderer
.encode_blur_then_rounded_mask_src_over_to_view(
self.recorder,
&device,
source,
&scratch,
dest_view,
*radius_x,
*radius_y,
*edge_treatment,
shader,
effect_rect,
load_op,
scissor,
viewport,
);
if fused {
self.recorder.record_passes(2);
self.renderer.effect_renderer.record_blur_pass();
self.renderer
.effect_renderer
.debug_effects
.set(self.renderer.effect_renderer.debug_effects.get() + 1);
self.renderer.effect_renderer.record_composite_pass();
self.recorder
.release_transient_offscreen(scratch_descriptor, scratch);
return Ok(());
}
self.recorder
.release_transient_offscreen(scratch_descriptor, scratch);
}
}
}
if let Some((first_effect, shader, viewport)) = direct_shader_tail_composite(
effect,
alpha,
blend_mode,
dest_viewport,
sample_mode,
(source.width, source.height),
) {
if self.record_effect_with_direct_shader_tail_composite(
source,
first_effect,
shader,
effect_rect,
dest_view,
load_op,
scissor,
viewport,
)? {
return Ok(());
}
}
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 admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
self.renderer.admit_layer_surface_cache_miss(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();
}
#[allow(clippy::too_many_arguments)]
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> {
self.render_non_effect_segment_with_composites(
text_state,
target_view,
shapes,
images,
texts,
shadow_draws,
draw_ops,
z_start,
z_end,
effect_z_ranges,
&[],
&[],
width,
height,
root_scale,
initial_load_op,
)
}
#[allow(clippy::too_many_arguments)]
fn render_non_effect_segment_with_composites(
&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>],
composites: &[(usize, CompositeBatchItem<'_>)],
shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
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,
width,
height,
root_scale,
&mut ordered_items,
);
#[cfg(not(target_arch = "wasm32"))]
let raw_shadow_items = ordered_items
.iter()
.filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
.count();
let culled_shadow_items = retain_renderable_shadow_items(
&mut ordered_items,
shadow_draws,
width,
height,
root_scale,
self.renderer.max_texture_dim(),
);
#[cfg(target_arch = "wasm32")]
let _ = culled_shadow_items;
let mut cached_shadow_composites: Vec<(usize, CachedShadowComposite)> = Vec::new();
ordered_items.extend(
composites
.iter()
.enumerate()
.map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::Composite(index))),
);
ordered_items.extend(
shader_composites
.iter()
.enumerate()
.map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::ShaderComposite(index))),
);
for (z_index, item) in &mut ordered_items {
let SegmentDrawItem::Shadow(shadow_index) = *item else {
continue;
};
let Some(composite) = self.renderer.cached_shape_shadow_composite(
&shadow_draws[shadow_index],
width,
height,
root_scale,
) else {
continue;
};
let composite_index = composites.len() + cached_shadow_composites.len();
cached_shadow_composites.push((*z_index, composite));
*item = SegmentDrawItem::Composite(composite_index);
}
let mut merged_composites = Vec::with_capacity(
composites
.len()
.saturating_add(cached_shadow_composites.len()),
);
merged_composites.extend(composites.iter().copied());
merged_composites.extend(
cached_shadow_composites
.iter()
.map(|(z_index, composite)| (*z_index, composite.batch_item())),
);
ordered_items.sort_by_key(|(z_index, _)| *z_index);
#[cfg(not(target_arch = "wasm32"))]
maybe_print_segment_diag(
z_start..z_end,
&ordered_items,
shapes,
images,
SegmentDiagCounts {
raw_shadow_items,
culled_shadow_items,
cached_shadow_composites: cached_shadow_composites.len(),
composite_items: merged_composites.len(),
shader_composite_items: shader_composites.len(),
},
);
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,
&merged_composites,
shader_composites,
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_surface_batch_to_view(
&mut self,
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
load_op: wgpu::LoadOp<wgpu::Color>,
composites: &[CompositeBatchItem<'_>],
) {
if composites.is_empty() {
return;
}
let device = self.renderer.device.clone();
self.renderer
.effect_renderer
.encode_composite_batch_to_view_pass(
self.recorder,
&device,
dest_view,
viewport,
load_op,
composites,
);
self.recorder.record_pass();
self.renderer.effect_renderer.record_composite_pass();
}
fn copy_texture_region_to_target(
&mut self,
source: &OffscreenTarget,
source_origin: (u32, u32),
target: &OffscreenTarget,
size: (u32, u32),
) -> bool {
let (width, height) = size;
if width == 0 || height == 0 || width > target.width || height > target.height {
return false;
}
let Some(source_right) = source_origin.0.checked_add(width) else {
return false;
};
let Some(source_bottom) = source_origin.1.checked_add(height) else {
return false;
};
if source_right > source.width || source_bottom > source.height {
return false;
}
self.recorder.encoder().copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: source.texture(),
mip_level: 0,
origin: wgpu::Origin3d {
x: source_origin.0,
y: source_origin.1,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: target.texture(),
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
true
}
fn shader_composite_batch_to_view(
&mut self,
dest_view: &wgpu::TextureView,
viewport: (u32, u32),
load_op: wgpu::LoadOp<wgpu::Color>,
composites: &[ShaderCompositeBatchItem<'_>],
) -> bool {
if composites.is_empty() {
return true;
}
let device = self.renderer.device.clone();
let encoded = self
.renderer
.effect_renderer
.encode_shader_batch_src_over_to_view(
self.recorder,
&device,
dest_view,
viewport,
load_op,
composites,
);
if encoded {
self.recorder.record_pass();
self.renderer.effect_renderer.record_composite_pass();
self.renderer
.effect_renderer
.debug_effects
.set(self.renderer.effect_renderer.debug_effects.get() + composites.len() as u32);
}
encoded
}
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,
overlay_graph: Option<&RenderGraph>,
width: u32,
height: u32,
root_scale: f32,
) -> Result<(), String> {
log::trace!("🎨 Rendering graph to {}x{}", width, height);
let render_start = Instant::now();
#[cfg(target_arch = "wasm32")]
{
self.wasm_uniform_batch_cursor = 0;
self.wasm_shape_batch_cursor = 0;
self.wasm_image_batch_cursor = 0;
}
#[cfg(not(target_arch = "wasm32"))]
{
self.retained_glyph_uniform_cursor = 0;
}
let result = self.render_graph(
text_state,
view,
graph,
overlay_graph,
width,
height,
root_scale,
);
let after_graph = Instant::now();
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);
if frame_stats_need_warmup_frame(&snapshot) {
self.pending_frame_warmup_frames = CACHE_MISS_WARMUP_FRAMES;
} else {
self.pending_frame_warmup_frames = self.pending_frame_warmup_frames.saturating_sub(1);
}
self.frame_stats.maybe_print_snapshot(
snapshot,
&mut self.frame_count,
self.gpu_stats_enabled,
);
self.frame_stats.reset();
let after_stats = Instant::now();
if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
log::warn!(
"[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
instant_ms(render_start, after_graph),
instant_ms(after_graph, after_stats),
);
}
result
}
pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
self.last_frame_stats
}
pub fn needs_frame_warmup(&self) -> bool {
self.pending_frame_warmup_frames > 0
}
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,
overlay_graph: Option<&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,
overlay_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)
}
#[allow(clippy::too_many_arguments)]
fn render_graph(
&mut self,
text_state: &mut TextSystemState,
surface_view: &wgpu::TextureView,
graph: &RenderGraph,
overlay_graph: Option<&RenderGraph>,
width: u32,
height: u32,
root_scale: f32,
) -> Result<(), String> {
let device = self.device.clone();
let queue = self.queue.clone();
let graph_start = Instant::now();
#[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,
overlay_graph,
width,
height,
root_scale,
frame_encoder,
)
},
);
let after_build = Instant::now();
let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
let after_execute = Instant::now();
self.frame_graph_executor = executor;
if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
log::warn!(
"[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
instant_ms(graph_start, after_build),
instant_ms(after_build, after_execute),
);
}
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,
overlay_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)
};
let after_execute = Instant::now();
self.frame_graph_executor = executor;
if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
}
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,
overlay_graph: Option<&RenderGraph>,
width: u32,
height: u32,
root_scale: f32,
frame_encoder: &mut C,
) -> Result<(), String> {
let recorded_start = Instant::now();
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 root_direct_scene_events_are_supported(&collected.scene) {
direct_root_child_underlays_are_supported(&collected).then_some(collected)
} else {
None
}
} else {
None
};
let after_root_collect = Instant::now();
let mut backend = RecordingSurfaceBackend {
renderer: self,
recorder: frame_encoder,
};
if let Some(collected) = direct_root {
let direct_render_start = Instant::now();
let result = execute_render_root_direct(
&mut backend,
text_state,
surface_view,
collected,
width,
height,
root_scale,
wgpu::LoadOp::Clear(CLEAR_COLOR),
);
if result.is_ok() {
if let Some(overlay_graph) = overlay_graph {
Self::render_overlay_graph_recorded(
&mut backend,
text_state,
surface_view,
overlay_graph,
width,
height,
root_scale,
)?;
}
}
let after_direct_render = Instant::now();
if let Some(total_ms) =
should_log_wgpu_render_stage(recorded_start, after_direct_render)
{
log::warn!(
"[wgpu-render-stage:recorded-direct-root] total_ms={total_ms:.2} collect_ms={:.2} render_ms={:.2}",
instant_ms(recorded_start, after_root_collect),
instant_ms(direct_render_start, after_direct_render),
);
}
return result;
}
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 {
node_id: graph.root.node_id,
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,
None,
)?;
}
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);
if let Some(overlay_graph) = overlay_graph {
Self::render_overlay_graph_recorded(
&mut backend,
text_state,
surface_view,
overlay_graph,
width,
height,
root_scale,
)?;
}
let after_layer_render = Instant::now();
if let Some(total_ms) = should_log_wgpu_render_stage(recorded_start, after_layer_render) {
log::warn!(
"[wgpu-render-stage:recorded-layer-root] total_ms={total_ms:.2} collect_ms={:.2} render_ms={:.2}",
instant_ms(recorded_start, after_root_collect),
instant_ms(after_root_collect, after_layer_render),
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn render_overlay_graph_recorded<C: FrameCommandRecorder>(
backend: &mut RecordingSurfaceBackend<'_, '_, C>,
text_state: &mut TextSystemState,
surface_view: &wgpu::TextureView,
graph: &RenderGraph,
width: u32,
height: u32,
root_scale: f32,
) -> Result<(), String> {
backend.renderer.layer_surface_rect_cache.clear();
backend.renderer.layer_surface_requirements_cache.clear();
let collected = collect_layer_contents_with_translation_context_and_text_layout(
&graph.root,
text_state,
None,
None,
TranslationRenderContext::default(),
&mut backend.renderer.layer_surface_rect_cache,
&mut backend.renderer.layer_surface_requirements_cache,
);
if !collected.child_layers.is_empty()
|| !root_direct_scene_events_are_supported(&collected.scene)
|| !direct_root_child_underlays_are_supported(&collected)
{
return Err("dev overlay graph must stay directly renderable".to_string());
}
execute_render_root_direct(
backend,
text_state,
surface_view,
collected,
width,
height,
root_scale,
wgpu::LoadOp::Load,
)
}
#[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)],
composites: &[(usize, CompositeBatchItem<'_>)],
shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
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,
composites,
shader_composites,
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 })
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::too_many_arguments)]
fn render_segment_draw_chunk_fused_native<C: FrameCommandRecorder>(
&mut self,
frame_encoder: &mut C,
target_view: &wgpu::TextureView,
ordered_items: &[(usize, SegmentDrawItem)],
composites: &[(usize, CompositeBatchItem<'_>)],
shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
shapes: &[DrawShape],
images: &[ImageDraw],
texts: &[TextDraw],
chunk: &SegmentDrawChunkPlan,
width: u32,
height: u32,
root_scale: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
) -> Result<Option<SegmentRenderOutcome>, String> {
let Some(partitions) = native_segment_fusion_partitions(ordered_items, shapes, chunk)?
else {
return Ok(None);
};
let mut rendered_any = false;
let mut pass_count = 0_u32;
let mut next_load_op = load_op;
for partition in partitions {
let outcome = self.render_segment_draw_chunk_fused_native_partition(
frame_encoder,
target_view,
ordered_items,
composites,
shader_composites,
shapes,
images,
texts,
&partition.chunk,
partition.budget,
width,
height,
root_scale,
next_load_op,
)?;
if outcome.rendered_any {
rendered_any = true;
pass_count = pass_count.saturating_add(outcome.pass_count);
next_load_op = wgpu::LoadOp::Load;
}
}
Ok(Some(SegmentRenderOutcome {
rendered_any,
pass_count,
}))
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::too_many_arguments)]
fn render_segment_draw_chunk_fused_native_partition<C: FrameCommandRecorder>(
&mut self,
frame_encoder: &mut C,
target_view: &wgpu::TextureView,
ordered_items: &[(usize, SegmentDrawItem)],
composites: &[(usize, CompositeBatchItem<'_>)],
shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
shapes: &[DrawShape],
images: &[ImageDraw],
texts: &[TextDraw],
chunk: &SegmentDrawChunkPlan,
budget: NativeSegmentFusionBudget,
width: u32,
height: u32,
root_scale: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
) -> Result<SegmentRenderOutcome, String> {
let partition_start = Instant::now();
let mut staged_uploads = self.take_staged_uploads();
staged_uploads.clear();
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);
let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
image_vertices.clear();
image_indices.clear();
image_cmds.clear();
glyph_cmds.clear();
let result = (|| {
let viewport = ViewportUniformParams {
width,
height,
offset: [0.0, 0.0],
};
self.prewarm_offscreen_text_glyph_draws_in_chunk(
ordered_items,
texts,
chunk,
viewport,
root_scale,
&mut staged_uploads,
&mut image_vertices,
&mut image_indices,
&mut glyph_cmds,
)?;
let mut shape_refs = Vec::with_capacity(budget.shape_count);
for batch in chunk.iter() {
let SegmentBatchPlan::Shape { start, end, .. } = batch else {
continue;
};
for (_, item) in &ordered_items[start..end] {
let SegmentDrawItem::Shape(shape_index) = item else {
return Err(format!(
"shape batch contains non-shape draw item: {item:?}"
));
};
shape_refs.push(&shapes[*shape_index]);
}
}
let after_shape_refs = Instant::now();
if !shape_refs.is_empty() {
let Some(_) = self.prepare_shapes_batch(
shape_refs.iter().copied(),
root_scale,
viewport,
&mut staged_uploads,
) else {
return Err(
"native fused segment shape preparation produced no draw batch".to_string(),
);
};
}
let after_shape_prepare = Instant::now();
let mut fused_batches = Vec::with_capacity(chunk.batches.len());
let mut shape_cursor = 0_u32;
let mut composite_cursor = 0usize;
let mut shader_composite_cursor = 0usize;
for batch in chunk.iter() {
match batch {
SegmentBatchPlan::Shape {
start,
end,
blend_mode,
} => {
for (_, item) in &ordered_items[start..end] {
if !matches!(item, SegmentDrawItem::Shape(_)) {
return Err(format!(
"shape batch contains non-shape draw item: {item:?}"
));
}
}
let shape_count = end - start;
if shape_count > 0 {
let index_start = shape_cursor * 6;
let index_count = shape_count as u32 * 6;
fused_batches.push(FusedSegmentBatch::Shape {
batch: PreparedShapeBatch {
index_start,
index_count,
},
blend_mode,
});
shape_cursor += shape_count as u32;
}
}
SegmentBatchPlan::Image {
start,
end,
blend_mode,
} => {
let cmd_start = image_cmds.len();
for (_, item) in &ordered_items[start..end] {
let SegmentDrawItem::Image(image_index) = item else {
return Err(format!(
"image batch contains non-image draw item: {item:?}"
));
};
self.append_image_draw_cmd(
&images[*image_index],
viewport,
root_scale,
&mut image_vertices,
&mut image_indices,
&mut image_cmds,
)?;
}
let cmd_end = image_cmds.len();
if cmd_start < cmd_end {
fused_batches.push(FusedSegmentBatch::Image {
cmd_range: cmd_start..cmd_end,
blend_mode,
});
}
}
SegmentBatchPlan::Text { start, end } => {
let glyph_cmd_start = glyph_cmds.len();
let image_cmd_start = image_cmds.len();
let text_draws =
text_draws_for_ordered_range(ordered_items, texts, start, end)?;
if !self.append_text_glyph_draws(
text_draws,
viewport,
root_scale,
false,
&mut staged_uploads,
&mut image_vertices,
&mut image_indices,
&mut glyph_cmds,
)? {
let text_draws =
text_draws_for_ordered_range(ordered_items, texts, start, end)?;
self.append_text_image_draw_cmds(
text_draws,
viewport,
root_scale,
&mut image_vertices,
&mut image_indices,
&mut image_cmds,
)?;
}
let image_cmd_end = image_cmds.len();
let glyph_cmd_end = glyph_cmds.len();
if image_cmd_start < image_cmd_end || glyph_cmd_start < glyph_cmd_end {
fused_batches.push(FusedSegmentBatch::Text {
image_cmd_range: image_cmd_start..image_cmd_end,
glyph_cmd_range: glyph_cmd_start..glyph_cmd_end,
});
}
}
SegmentBatchPlan::Composite { start, end } => {
for (_, item) in &ordered_items[start..end] {
if !matches!(item, SegmentDrawItem::Composite(_)) {
return Err(format!(
"composite batch contains non-composite draw item: {item:?}"
));
}
}
let draw_count = end - start;
if draw_count > 0 {
let draw_start = composite_cursor;
composite_cursor += draw_count;
fused_batches.push(FusedSegmentBatch::Composite {
draw_range: draw_start..composite_cursor,
});
}
}
SegmentBatchPlan::ShaderComposite { start, end } => {
for (_, item) in &ordered_items[start..end] {
if !matches!(item, SegmentDrawItem::ShaderComposite(_)) {
return Err(format!(
"shader composite batch contains non-shader-composite draw item: {item:?}"
));
}
}
let draw_count = end - start;
if draw_count > 0 {
let draw_start = shader_composite_cursor;
shader_composite_cursor += draw_count;
fused_batches.push(FusedSegmentBatch::ShaderComposite {
draw_range: draw_start..shader_composite_cursor,
});
}
}
}
}
let after_batch_prepare = Instant::now();
if !image_indices.is_empty() {
self.stage_native_image_buffers(
&mut staged_uploads,
viewport,
&image_vertices,
&image_indices,
);
}
let device = self.device.clone();
let composite_items: Vec<_> = chunk
.iter()
.filter_map(|batch| match batch {
SegmentBatchPlan::Composite { start, end } => Some((start, end)),
_ => None,
})
.flat_map(|(start, end)| {
ordered_items[start..end].iter().filter_map(|(_, item)| {
let SegmentDrawItem::Composite(composite_index) = item else {
return None;
};
composites
.get(*composite_index)
.map(|(_, composite)| *composite)
})
})
.collect();
let prepared_composites = self.effect_renderer.prepare_composite_batch_draws(
frame_encoder,
&device,
load_op,
&composite_items,
);
let shader_items: Vec<_> = chunk
.iter()
.filter_map(|batch| match batch {
SegmentBatchPlan::ShaderComposite { start, end } => Some((start, end)),
_ => None,
})
.flat_map(|(start, end)| {
ordered_items[start..end].iter().filter_map(|(_, item)| {
let SegmentDrawItem::ShaderComposite(composite_index) = item else {
return None;
};
shader_composites
.get(*composite_index)
.map(|(_, composite)| *composite)
})
})
.collect();
let prepared_shaders = self
.effect_renderer
.prepare_shader_batch_draws(frame_encoder, &device, &shader_items)
.ok_or_else(|| "shader composite batch preparation failed".to_string())?;
if !shader_items.is_empty() {
self.effect_renderer.record_composite_pass();
self.effect_renderer
.debug_effects
.set(self.effect_renderer.debug_effects.get() + shader_items.len() as u32);
}
let after_composite_prepare = Instant::now();
if fused_batches.is_empty() {
return Ok(SegmentRenderOutcome {
rendered_any: false,
pass_count: 0,
});
}
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 after_upload = Instant::now();
{
let mut render_pass =
frame_encoder
.encoder()
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Fused Segment Draw 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,
});
for batch in &fused_batches {
match batch {
FusedSegmentBatch::Shape { batch, blend_mode } => {
self.draw_prepared_shapes(
&mut render_pass,
*blend_mode,
*batch,
width,
height,
);
}
FusedSegmentBatch::Image {
cmd_range,
blend_mode,
} => {
self.draw_native_prepared_image_cmd_range(
&mut render_pass,
&image_cmds,
cmd_range.clone(),
*blend_mode,
)?;
}
FusedSegmentBatch::Text {
image_cmd_range,
glyph_cmd_range,
} => {
if !image_cmd_range.is_empty() {
self.draw_native_prepared_image_cmd_range(
&mut render_pass,
&image_cmds,
image_cmd_range.clone(),
BlendMode::SrcOver,
)?;
self.frame_stats.bump_text();
}
if !glyph_cmd_range.is_empty() {
self.draw_native_prepared_glyph_cmd_range(
&mut render_pass,
&glyph_cmds,
glyph_cmd_range.clone(),
)?;
}
}
FusedSegmentBatch::Composite { draw_range } => {
for draw in
prepared_composites.get(draw_range.clone()).ok_or_else(|| {
"composite draw range is outside the prepared command buffer"
.to_string()
})?
{
self.effect_renderer.draw_prepared_composite(
&mut render_pass,
(width, height),
draw,
);
}
}
FusedSegmentBatch::ShaderComposite { draw_range } => {
for draw in prepared_shaders.get(draw_range.clone()).ok_or_else(|| {
"shader composite draw range is outside the prepared command buffer"
.to_string()
})? {
self.effect_renderer.draw_prepared_shader_src_over(
&device,
&mut render_pass,
(width, height),
draw,
);
}
}
}
}
}
let after_pass = Instant::now();
if let Some(total_ms) = should_log_wgpu_render_stage(partition_start, after_pass) {
log::warn!(
"[wgpu-render-stage:fused-segment] total_ms={total_ms:.2} shape_refs_ms={:.2} shape_prepare_ms={:.2} batch_prepare_ms={:.2} composite_prepare_ms={:.2} upload_ms={:.2} pass_ms={:.2} batches={} shapes={} image_cmds={} glyph_cmds={} staged_bytes={}",
instant_ms(partition_start, after_shape_refs),
instant_ms(after_shape_refs, after_shape_prepare),
instant_ms(after_shape_prepare, after_batch_prepare),
instant_ms(after_batch_prepare, after_composite_prepare),
instant_ms(after_composite_prepare, after_upload),
instant_ms(after_upload, after_pass),
fused_batches.len(),
budget.shape_count,
image_cmds.len(),
glyph_cmds.len(),
staged_uploads.bytes.len(),
);
}
Ok(SegmentRenderOutcome {
rendered_any: true,
pass_count: 1,
})
})();
self.scratch_image_vertices = image_vertices;
self.scratch_image_indices = image_indices;
self.scratch_image_cmds = image_cmds;
self.scratch_glyph_cmds = glyph_cmds;
self.restore_staged_uploads(staged_uploads);
result
}
#[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)],
composites: &[(usize, CompositeBatchItem<'_>)],
shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
shapes: &[DrawShape],
images: &[ImageDraw],
texts: &[TextDraw],
chunk: SegmentDrawChunkPlan,
width: u32,
height: u32,
root_scale: f32,
load_op: wgpu::LoadOp<wgpu::Color>,
) -> Result<SegmentRenderOutcome, String> {
#[cfg(not(target_arch = "wasm32"))]
if let Some(outcome) = self.render_segment_draw_chunk_fused_native(
frame_encoder,
target_view,
ordered_items,
composites,
shader_composites,
shapes,
images,
texts,
&chunk,
width,
height,
root_scale,
load_op,
)? {
return Ok(outcome);
}
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,
width,
height,
);
}
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],
};
let text_draws =
text_draws_for_ordered_range(ordered_items, texts, start, end)?;
if let Some(prepared_glyphs) = self.prepare_text_glyph_draw_cmds(
text_draws,
viewport,
root_scale,
&mut staged_uploads,
)? {
if prepared_glyphs.is_empty() {
self.scratch_glyph_cmds = prepared_glyphs.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 Glyph Atlas 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_glyphs(&mut render_pass, &prepared_glyphs)?;
}
pass_count = pass_count.saturating_add(1);
self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
rendered_any = true;
next_load_op = wgpu::LoadOp::Load;
} else {
let text_draws =
text_draws_for_ordered_range(ordered_items, texts, start, end)?;
let prepared_images = self.prepare_text_image_draw_cmds(
text_draws,
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;
}
}
SegmentBatchPlan::Composite { start, end } => {
let batch_items: Vec<_> = ordered_items[start..end]
.iter()
.map(|(_, item)| match item {
SegmentDrawItem::Composite(composite_index) => composites
.get(*composite_index)
.map(|(_, composite)| *composite)
.ok_or_else(|| {
"composite item index is outside the composite buffer"
.to_string()
}),
other => Err(format!(
"composite batch contains non-composite draw item: {other:?}"
)),
})
.collect::<Result<_, _>>()?;
let device = self.device.clone();
self.effect_renderer.encode_composite_batch_to_view_pass(
frame_encoder,
&device,
target_view,
(width, height),
next_load_op,
&batch_items,
);
self.effect_renderer.record_composite_pass();
pass_count = pass_count.saturating_add(1);
rendered_any = true;
next_load_op = wgpu::LoadOp::Load;
}
SegmentBatchPlan::ShaderComposite { start, end } => {
let batch_items: Vec<_> = ordered_items[start..end]
.iter()
.map(|(_, item)| match item {
SegmentDrawItem::ShaderComposite(composite_index) => {
shader_composites
.get(*composite_index)
.map(|(_, composite)| *composite)
.ok_or_else(|| {
"shader composite item index is outside the shader composite buffer"
.to_string()
})
}
other => Err(format!(
"shader composite batch contains non-shader-composite draw item: {other:?}"
)),
})
.collect::<Result<Vec<_>, _>>()?;
let device = self.device.clone();
let encoded = self.effect_renderer.encode_shader_batch_src_over_to_view(
frame_encoder,
&device,
target_view,
(width, height),
next_load_op,
&batch_items,
);
if !encoded {
return Err("shader composite batch failed to encode".to_string());
}
self.effect_renderer.record_composite_pass();
self.effect_renderer.debug_effects.set(
self.effect_renderer.debug_effects.get() + batch_items.len() as u32,
);
pass_count = pass_count.saturating_add(1);
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(not(target_arch = "wasm32"))]
fn stage_retained_glyph_viewport_uniforms(
&mut self,
staged_uploads: &mut StagedBufferUploads,
params: ViewportUniformParams,
) -> usize {
let slot = self.claim_retained_glyph_uniform_slot();
let uniforms = Self::viewport_uniforms(params);
staged_uploads.stage_at(
UploadTarget::RetainedGlyphUniform,
self.retained_glyph_uniform_offset(slot),
bytemuck::bytes_of(&uniforms),
);
slot
}
#[cfg(not(target_arch = "wasm32"))]
fn claim_retained_glyph_uniform_slot(&mut self) -> usize {
let slot = self.retained_glyph_uniform_cursor;
self.retained_glyph_uniform_cursor = self.retained_glyph_uniform_cursor.saturating_add(1);
self.ensure_retained_glyph_uniform_capacity(slot.saturating_add(1));
slot
}
#[cfg(not(target_arch = "wasm32"))]
fn retained_glyph_uniform_offset(&self, slot: usize) -> u64 {
self.retained_glyph_uniform_stride * slot as u64
}
#[cfg(not(target_arch = "wasm32"))]
fn retained_glyph_uniform_dynamic_offset(&self, slot: usize) -> Result<u32, String> {
let offset = self.retained_glyph_uniform_offset(slot);
u32::try_from(offset).map_err(|_| {
"retained glyph uniform offset exceeded WGPU dynamic offset range".to_string()
})
}
#[cfg(not(target_arch = "wasm32"))]
fn ensure_retained_glyph_uniform_capacity(&mut self, required_slots: usize) {
if required_slots <= self.retained_glyph_uniform_capacity {
return;
}
let new_capacity = required_slots
.next_power_of_two()
.max(INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS);
self.retained_glyph_uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Retained Glyph Uniform Buffer"),
size: self.retained_glyph_uniform_stride * new_capacity as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.retained_glyph_uniform_bind_group =
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Retained Glyph Uniform Bind Group"),
layout: &self.retained_glyph_uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &self.retained_glyph_uniform_buffer,
offset: 0,
size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
}),
}],
});
self.retained_glyph_uniform_capacity = new_capacity;
}
#[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,
UploadTarget::RetainedGlyphUniform => &self.retained_glyph_uniform_buffer,
};
encoder.copy_buffer_to_buffer(
&self.upload_buffer,
upload_buffer_offset + copy.source_offset,
target_buffer,
copy.target_offset,
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 source_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,
};
let mut visible_blur_bounds = source_blur_bounds;
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) = visible_blur_bounds.intersect(clip_expanded) else {
return;
};
visible_blur_bounds = intersection;
}
let processing_scissor =
scissor_rect_for_rect(visible_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(visible_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() {
if let Some(plan) = shape_shadow_surface_plan(
&shadow.shapes,
shadow.clip,
shadow.blur_radius,
width,
height,
root_scale,
self.max_texture_dim(),
) {
if self.encode_shape_only_blurred_shadow_draw(
frame_encoder,
target_view,
shadow,
plan.source_device_bounds,
plan.pixel_radius,
plan.processing_scissor,
width,
height,
root_scale,
) {
return;
}
}
}
if !shadow.texts.is_empty() {
self.frame_stats.record_shadow_text_blur_fallback();
}
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,
width,
height,
);
}
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 =
shape_shadow_surface_cache_key(&shadow.shapes, device_bounds, pixel_radius, root_scale);
if let Some(key) = cache_key {
if let Some(cached) = self.cached_shadow_surface(&key) {
self.frame_stats
.record_shadow_shape_cache_hit(bounds_w, bounds_h);
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;
}
self.frame_stats
.record_shadow_shape_cache_miss(bounds_w, bounds_h);
self.frame_stats.maybe_print_shadow_shape_cache_miss(
bounds_w,
bounds_h,
key.content_hash,
pixel_radius,
viewport_offset,
shadow.shapes.len(),
shadow.clip,
);
}
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
.filter(|shape| shape_draw_is_visible_in_viewport(shape, viewport, root_scale))
.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_start: 0,
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,
width: u32,
height: u32,
) {
self.frame_stats.bump_shapes();
render_pass.set_scissor_rect(0, 0, width, height);
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(
batch.index_start..batch.index_start + 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, width, height);
}
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 draw_prepared_glyphs(
&mut self,
render_pass: &mut wgpu::RenderPass<'_>,
batch: &PreparedGlyphBatch,
) -> Result<(), String> {
if batch.cmds.is_empty() {
return Ok(());
}
#[cfg(not(target_arch = "wasm32"))]
{
self.draw_native_prepared_glyph_cmd_range(
render_pass,
&batch.cmds,
0..batch.cmds.len(),
)?;
}
#[cfg(target_arch = "wasm32")]
{
self.frame_stats.bump_text();
render_pass.set_pipeline(&self.glyph_atlas_pipeline);
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_bind_group(1, &self.text_glyph_atlas.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 GlyphDrawSource::Shared {
index_start,
index_count,
} = cmd.source;
render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
}
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn draw_native_prepared_image_cmd_range(
&mut self,
render_pass: &mut wgpu::RenderPass<'_>,
cmds: &[ImageDrawCmd],
cmd_range: Range<usize>,
blend_mode: BlendMode,
) -> Result<(), String> {
let Some(cmds) = cmds.get(cmd_range) else {
return Err("image command range is outside the prepared command buffer".to_string());
};
if 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,
});
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_index_buffer(self.image_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
for cmd in 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(())
}
#[cfg(not(target_arch = "wasm32"))]
fn draw_native_prepared_glyph_cmd_range(
&mut self,
render_pass: &mut wgpu::RenderPass<'_>,
cmds: &[GlyphDrawCmd],
cmd_range: Range<usize>,
) -> Result<(), String> {
let Some(cmds) = cmds.get(cmd_range) else {
return Err("glyph command range is outside the prepared command buffer".to_string());
};
if cmds.is_empty() {
return Ok(());
}
self.frame_stats.bump_text();
let mut shared_buffers_bound = false;
let mut retained_pipeline_bound = false;
for cmd in cmds {
let (sx, sy, sw, sh) = cmd.scissor;
render_pass.set_scissor_rect(sx, sy, sw, sh);
match cmd.source {
GlyphDrawSource::Shared {
index_start,
index_count,
} => {
if retained_pipeline_bound || !shared_buffers_bound {
render_pass.set_pipeline(&self.glyph_atlas_pipeline);
render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
retained_pipeline_bound = false;
}
if !shared_buffers_bound {
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_index_buffer(
self.image_index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
shared_buffers_bound = true;
}
render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
}
GlyphDrawSource::Retained {
cache_key,
uniform_slot,
} => {
shared_buffers_bound = false;
if !retained_pipeline_bound {
render_pass.set_pipeline(&self.retained_glyph_atlas_pipeline);
render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
retained_pipeline_bound = true;
}
let cached = self
.text_glyph_gpu_run_cache
.peek(&cache_key)
.ok_or_else(|| "retained glyph buffer missing from cache".to_string())?;
let dynamic_offset =
self.retained_glyph_uniform_dynamic_offset(uniform_slot)?;
render_pass.set_bind_group(
0,
&self.retained_glyph_uniform_bind_group,
&[dynamic_offset],
);
render_pass
.set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
render_pass.draw_indexed(0..cached.index_count, 0, 0..1);
}
}
}
Ok(())
}
fn append_image_draw_cmd(
&mut self,
image_draw: &ImageDraw,
viewport: ViewportUniformParams,
root_scale: f32,
image_vertices: &mut Vec<Vertex>,
image_indices: &mut Vec<u32>,
image_cmds: &mut Vec<ImageDrawCmd>,
) -> Result<(), String> {
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 {
return Ok(());
}
let (tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
if tint[3] <= 0.0 {
return Ok(());
}
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 Some(scissor) =
scissor_rect_for_image(&adjusted_image, root_scale, viewport.width, viewport.height)
else {
return Ok(());
};
let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
return Ok(());
};
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,
});
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn stage_native_image_buffers(
&mut self,
staged_uploads: &mut StagedBufferUploads,
viewport: ViewportUniformParams,
image_vertices: &[Vertex],
image_indices: &[u32],
) {
if image_indices.is_empty() {
return;
}
self.stage_viewport_uniforms(staged_uploads, viewport);
let needed_bytes = std::mem::size_of_val(image_vertices) 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 = std::mem::size_of_val(image_indices) 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),
);
}
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 {
self.append_image_draw_cmd(
image_draw,
viewport,
root_scale,
&mut image_vertices,
&mut image_indices,
&mut image_cmds,
)?;
}
#[cfg(not(target_arch = "wasm32"))]
if !image_cmds.is_empty() {
self.stage_native_image_buffers(
staged_uploads,
viewport,
&image_vertices,
&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 glyph_atlas_entry_for(
&mut self,
glyph: &SoftwareGlyphAtlasGlyph,
) -> Result<GlyphAtlasEntry, String> {
if let Some(entry) = self.text_glyph_atlas.upload_glyph(
glyph.key,
glyph,
&self.queue,
&mut self.frame_graph_executor,
&mut self.frame_stats,
) {
return Ok(entry);
}
self.text_glyph_atlas.reset(
&self.device,
&self.image_bind_group_layout,
&self.image_nearest_sampler,
);
Err("text glyph atlas filled and was reset".to_string())
}
fn glyph_atlas_entry_for_cached(
&mut self,
glyph: &SoftwareGlyphAtlasPlacement,
) -> Option<GlyphAtlasEntry> {
let entry = self.text_glyph_atlas.entry(&glyph.key)?;
self.frame_stats.record_text_glyph_atlas_hit();
Some(entry)
}
fn glyph_atlas_entry_for_placement(
&mut self,
glyph: &SoftwareGlyphAtlasPlacement,
) -> Result<GlyphAtlasEntry, String> {
if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
return Ok(entry);
}
let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
return Err("text glyph placement has no retained raster mask".to_string());
};
self.glyph_atlas_entry_for(&upload_glyph)
}
fn prepare_text_glyph_quads(
&mut self,
run_key: TextGlyphRunCacheKey,
atlas_generation: u64,
cached_glyph_run: Option<&[SoftwareGlyphAtlasPlacement]>,
collected_run: &[SoftwareGlyphAtlasRunGlyph],
generated_quads: &mut Vec<CachedTextGlyphQuad>,
) -> Result<Rc<[CachedTextGlyphQuad]>, String> {
generated_quads.clear();
if let Some(glyph_run) = cached_glyph_run {
for glyph in glyph_run {
if glyph.width == 0 || glyph.height == 0 || glyph.color.3 <= 0.0 {
continue;
}
let entry = self.glyph_atlas_entry_for_placement(glyph)?;
generated_quads.push(cached_text_glyph_quad(glyph, entry));
}
} else {
for run_glyph in collected_run {
let placement = run_glyph.placement();
if placement.width == 0 || placement.height == 0 || placement.color.3 <= 0.0 {
continue;
}
let entry = match run_glyph {
SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
self.glyph_atlas_entry_for_placement(placement)?
}
SoftwareGlyphAtlasRunGlyph::New(glyph) => self.glyph_atlas_entry_for(glyph)?,
};
generated_quads.push(cached_text_glyph_quad(&placement, entry));
}
}
let quads: Rc<[CachedTextGlyphQuad]> = Rc::from(generated_quads.clone().into_boxed_slice());
if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
cached.quads = Some(Rc::clone(&quads));
cached.atlas_generation = atlas_generation;
}
Ok(quads)
}
#[allow(clippy::too_many_arguments)]
fn append_text_glyph_quad_run(
&mut self,
source_raster_rect: Rect,
quads: &[CachedTextGlyphQuad],
clip: Option<Rect>,
viewport: ViewportUniformParams,
root_scale: f32,
image_vertices: &mut Vec<Vertex>,
image_indices: &mut Vec<u32>,
record_cached_hits: bool,
) -> usize {
let mut appended = 0usize;
for quad in quads {
if !cached_text_glyph_quad_is_visible_in_viewport(
source_raster_rect,
quad,
clip,
viewport,
root_scale,
) {
continue;
}
if append_cached_text_glyph_quad(
source_raster_rect,
quad,
image_vertices,
image_indices,
) {
if record_cached_hits {
self.frame_stats.record_text_glyph_atlas_hit();
}
appended = appended.saturating_add(1);
}
}
appended
}
#[cfg(not(target_arch = "wasm32"))]
fn retained_glyph_viewport(
viewport: ViewportUniformParams,
source_raster_rect: Rect,
) -> ViewportUniformParams {
ViewportUniformParams {
width: viewport.width,
height: viewport.height,
offset: [
viewport.offset[0] - source_raster_rect.x,
viewport.offset[1] - source_raster_rect.y,
],
}
}
#[cfg(not(target_arch = "wasm32"))]
fn retained_text_glyph_run_ready(&mut self, cache_key: TextGlyphRunCacheKey) -> bool {
let atlas_generation = self.text_glyph_atlas.generation();
self.text_glyph_gpu_run_cache
.peek(&cache_key)
.is_some_and(|cached| cached.atlas_generation == atlas_generation)
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::too_many_arguments)]
fn emit_retained_text_glyph_run_if_ready(
&mut self,
cache_key: TextGlyphRunCacheKey,
quads: &[CachedTextGlyphQuad],
clip: Option<Rect>,
viewport: ViewportUniformParams,
source_raster_rect: Rect,
scissor: (u32, u32, u32, u32),
staged_uploads: &mut StagedBufferUploads,
glyph_cmds: &mut Vec<GlyphDrawCmd>,
) -> bool {
if !should_use_retained_text_glyph_run(quads.len(), clip) {
return false;
}
if !self.retained_text_glyph_run_ready(cache_key)
&& !self.ensure_retained_text_glyph_run(cache_key, quads)
{
return false;
}
let uniform_slot = self.stage_retained_glyph_viewport_uniforms(
staged_uploads,
Self::retained_glyph_viewport(viewport, source_raster_rect),
);
glyph_cmds.push(GlyphDrawCmd::retained(cache_key, uniform_slot, scissor));
true
}
#[cfg(not(target_arch = "wasm32"))]
fn ensure_retained_text_glyph_run(
&mut self,
cache_key: TextGlyphRunCacheKey,
quads: &[CachedTextGlyphQuad],
) -> bool {
let atlas_generation = self.text_glyph_atlas.generation();
if self
.text_glyph_gpu_run_cache
.peek(&cache_key)
.is_some_and(|cached| cached.atlas_generation == atlas_generation)
{
return true;
}
let mut vertices = Vec::with_capacity(quads.len().saturating_mul(4));
let mut indices = Vec::with_capacity(quads.len().saturating_mul(6));
let origin = Rect {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
};
for quad in quads {
append_cached_text_glyph_quad(origin, quad, &mut vertices, &mut indices);
}
if indices.is_empty() {
return false;
}
let vertex_bytes = bytemuck::cast_slice(&vertices);
let index_bytes = bytemuck::cast_slice(&indices);
let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Retained Text Glyph Vertex Buffer"),
size: vertex_bytes.len() as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Retained Text Glyph Index Buffer"),
size: index_bytes.len() as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let vertex_upload =
self.frame_graph_executor
.upload_buffer(&self.queue, &vertex_buffer, 0, vertex_bytes);
self.frame_stats.record_command_stats(vertex_upload);
let index_upload =
self.frame_graph_executor
.upload_buffer(&self.queue, &index_buffer, 0, index_bytes);
self.frame_stats.record_command_stats(index_upload);
self.text_glyph_gpu_run_cache.put(
cache_key,
CachedGpuTextGlyphRun {
vertex_buffer,
index_buffer,
index_count: indices.len() as u32,
atlas_generation,
},
);
true
}
#[allow(clippy::too_many_arguments)]
fn append_text_glyph_draws<'a, I>(
&mut self,
layer_texts: I,
viewport: ViewportUniformParams,
root_scale: f32,
allow_offscreen_prewarm: bool,
staged_uploads: &mut StagedBufferUploads,
image_vertices: &mut Vec<Vertex>,
image_indices: &mut Vec<u32>,
glyph_cmds: &mut Vec<GlyphDrawCmd>,
) -> Result<bool, String>
where
I: IntoIterator<Item = &'a TextDraw>,
{
let append_start = Instant::now();
let initial_vertex_len = image_vertices.len();
let initial_index_len = image_indices.len();
let initial_cmd_len = glyph_cmds.len();
let initial_staged_bytes_len = staged_uploads.bytes.len();
let initial_staged_copies_len = staged_uploads.copies.len();
let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
let mut collected_placements = std::mem::take(&mut self.scratch_text_glyph_placements);
let mut generated_quads = std::mem::take(&mut self.scratch_text_glyph_quads);
generated_quads.clear();
let mut visited = 0usize;
let mut emitted_glyphs = 0usize;
let mut prewarmed_glyphs = 0usize;
let mut run_hits = 0usize;
let mut run_misses = 0usize;
for text_draw in layer_texts {
visited = visited.saturating_add(1);
let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
self.text_raster_geometry(text_draw, root_scale)
else {
continue;
};
if !static_text_motion {
image_vertices.truncate(initial_vertex_len);
image_indices.truncate(initial_index_len);
glyph_cmds.truncate(initial_cmd_len);
staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
self.scratch_text_glyph_run = collected_run;
self.scratch_text_glyph_placements = collected_placements;
self.scratch_text_glyph_quads = generated_quads;
return Ok(false);
}
let is_visible =
text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale);
let draw_action = text_glyph_draw_action(
is_visible,
text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale),
allow_offscreen_prewarm,
);
if draw_action == TextGlyphDrawAction::Skip {
continue;
}
let raster_source = text_glyph_raster_source(text_draw, raster_rect);
let source_draw = raster_source.draw.as_ref();
let source_raster_rect = raster_source.raster_rect;
let run_key = Self::text_glyph_run_cache_key(
source_draw,
source_raster_rect,
text_scale,
static_text_motion,
);
let atlas_generation = self.text_glyph_atlas.generation();
let mut cached_quad_run = None;
let mut miss_collect_ms = None;
let mut miss_cached_glyphs = 0usize;
let mut miss_new_glyphs = 0usize;
let cached_glyph_run = if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
run_hits = run_hits.saturating_add(1);
if cached.atlas_generation == atlas_generation {
cached_quad_run = cached.quads.as_ref().map(Rc::clone);
}
Some(Rc::clone(&cached.glyphs))
} else {
run_misses = run_misses.saturating_add(1);
collected_run.clear();
let collect_start = Instant::now();
let collect_result = collect_solid_text_atlas_run(
&source_draw.text,
source_raster_rect,
&source_draw.text_style,
source_draw.color,
source_draw.font_size,
text_scale,
&self.text_fonts,
&mut self.text_glyph_mask_cache,
&mut collected_run,
);
miss_collect_ms = Some(instant_ms(collect_start, Instant::now()));
if collect_result.is_none() {
if text_atlas_fallback_diag_enabled() {
let preview: String = source_draw.text.text.chars().take(96).collect();
log::warn!(
"[text-atlas-fallback] node={:?} visible={} prewarm={} spans={} links={} text_len={} preview={:?} span_style={:?} paragraph_style={:?}",
source_draw.node_id,
is_visible,
draw_action == TextGlyphDrawAction::PrewarmOffscreen,
source_draw.text.span_styles.len(),
source_draw.text.link_annotations.len(),
source_draw.text.text.len(),
preview,
source_draw.text_style.span_style,
source_draw.text_style.paragraph_style,
);
}
if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
continue;
}
image_vertices.truncate(initial_vertex_len);
image_indices.truncate(initial_index_len);
glyph_cmds.truncate(initial_cmd_len);
staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
self.scratch_text_glyph_run = collected_run;
self.scratch_text_glyph_placements = collected_placements;
self.scratch_text_glyph_quads = generated_quads;
return Ok(false);
}
if text_glyph_run_diag_enabled() {
miss_cached_glyphs = collected_run
.iter()
.filter(|glyph| matches!(glyph, SoftwareGlyphAtlasRunGlyph::Cached(_)))
.count();
miss_new_glyphs = collected_run.len().saturating_sub(miss_cached_glyphs);
}
collected_placements.clear();
collected_placements.extend(
collected_run
.iter()
.map(SoftwareGlyphAtlasRunGlyph::placement),
);
let glyphs: Rc<[SoftwareGlyphAtlasPlacement]> =
Rc::from(collected_placements.clone().into_boxed_slice());
self.text_glyph_run_cache.put(
run_key,
CachedTextGlyphRun {
glyphs,
quads: None,
atlas_generation: 0,
},
);
None
};
if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
let prewarm_quads = if let Some(quad_run) = cached_quad_run {
quad_run
} else {
let prepare_start = Instant::now();
match self.prepare_text_glyph_quads(
run_key,
atlas_generation,
cached_glyph_run.as_deref(),
&collected_run,
&mut generated_quads,
) {
Ok(quads) => {
if let Some(collect_ms) = miss_collect_ms {
if text_glyph_run_diag_enabled() {
log::warn!(
"[text-glyph-run-diag] visible=false glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
quads.len(),
miss_cached_glyphs,
miss_new_glyphs,
collect_ms,
instant_ms(prepare_start, Instant::now()),
);
}
}
quads
}
Err(_) => continue,
}
};
#[cfg(not(target_arch = "wasm32"))]
if should_use_retained_text_glyph_run(prewarm_quads.len(), source_draw.clip) {
self.ensure_retained_text_glyph_run(run_key, prewarm_quads.as_ref());
}
prewarmed_glyphs = prewarmed_glyphs.saturating_add(prewarm_quads.len());
continue;
}
let draw_rect = Rect {
x: source_raster_rect.x / root_scale,
y: source_raster_rect.y / root_scale,
width: source_raster_rect.width / root_scale,
height: source_raster_rect.height / root_scale,
};
let Some(scissor) = scissor_rect_for_layer(
draw_rect,
source_draw.clip,
root_scale,
viewport.width,
viewport.height,
) else {
continue;
};
#[cfg(not(target_arch = "wasm32"))]
if let Some(quad_run) = cached_quad_run.as_ref() {
if should_use_retained_text_glyph_run(quad_run.len(), source_draw.clip)
&& self.emit_retained_text_glyph_run_if_ready(
run_key,
quad_run.as_ref(),
source_draw.clip,
viewport,
source_raster_rect,
scissor,
staged_uploads,
glyph_cmds,
)
{
emitted_glyphs = emitted_glyphs.saturating_add(quad_run.len());
continue;
}
}
let index_start = image_indices.len() as u32;
if let Some(quad_run) = cached_quad_run {
emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
source_raster_rect,
quad_run.as_ref(),
source_draw.clip,
viewport,
root_scale,
image_vertices,
image_indices,
true,
));
} else {
let prepare_start = Instant::now();
let Ok(quad_run) = self.prepare_text_glyph_quads(
run_key,
atlas_generation,
cached_glyph_run.as_deref(),
&collected_run,
&mut generated_quads,
) else {
image_vertices.truncate(initial_vertex_len);
image_indices.truncate(initial_index_len);
glyph_cmds.truncate(initial_cmd_len);
staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
self.scratch_text_glyph_run = collected_run;
self.scratch_text_glyph_placements = collected_placements;
self.scratch_text_glyph_quads = generated_quads;
return Ok(false);
};
if let Some(collect_ms) = miss_collect_ms {
if text_glyph_run_diag_enabled() {
log::warn!(
"[text-glyph-run-diag] visible=true glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
quad_run.len(),
miss_cached_glyphs,
miss_new_glyphs,
collect_ms,
instant_ms(prepare_start, Instant::now()),
);
}
}
emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
source_raster_rect,
quad_run.as_ref(),
source_draw.clip,
viewport,
root_scale,
image_vertices,
image_indices,
false,
));
}
let index_count = image_indices.len() as u32 - index_start;
if index_count > 0 {
glyph_cmds.push(GlyphDrawCmd::shared(index_start, index_count, scissor));
}
}
self.scratch_text_glyph_run = collected_run;
self.scratch_text_glyph_placements = collected_placements;
self.scratch_text_glyph_quads = generated_quads;
let append_end = Instant::now();
if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
log::warn!(
"[wgpu-render-stage:text-glyph-atlas] total_ms={total_ms:.2} visited={} cmds={} glyphs={} prewarmed={} run_hits={} run_misses={}",
visited,
glyph_cmds.len().saturating_sub(initial_cmd_len),
emitted_glyphs,
prewarmed_glyphs,
run_hits,
run_misses,
);
}
Ok(true)
}
#[cfg(not(target_arch = "wasm32"))]
fn text_glyph_prewarm_decision(
&self,
text_draw: &TextDraw,
viewport: ViewportUniformParams,
root_scale: f32,
) -> TextGlyphPrewarmDecision {
let Some((logical_rect, _, clip, _, static_text_motion)) =
self.text_raster_geometry(text_draw, root_scale)
else {
return TextGlyphPrewarmDecision::MissingGeometry;
};
if !static_text_motion {
return TextGlyphPrewarmDecision::DynamicMotion;
}
if text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
return TextGlyphPrewarmDecision::Visible;
}
if text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale) {
TextGlyphPrewarmDecision::Candidate
} else {
TextGlyphPrewarmDecision::OutsidePrewarmWindow
}
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::too_many_arguments)]
fn prewarm_offscreen_text_glyph_draws_in_chunk(
&mut self,
ordered_items: &[(usize, SegmentDrawItem)],
texts: &[TextDraw],
chunk: &SegmentDrawChunkPlan,
viewport: ViewportUniformParams,
root_scale: f32,
staged_uploads: &mut StagedBufferUploads,
image_vertices: &mut Vec<Vertex>,
image_indices: &mut Vec<u32>,
glyph_cmds: &mut Vec<GlyphDrawCmd>,
) -> Result<(), String> {
let prewarm_start = Instant::now();
let diag_enabled = std::env::var_os("CRANPOSE_TEXT_PREWARM_DIAG").is_some();
let mut text_items = 0usize;
let mut candidates = 0usize;
let mut missing_geometry = 0usize;
let mut dynamic_motion = 0usize;
let mut visible = 0usize;
let mut outside = 0usize;
let mut already_prepared = 0usize;
let mut admitted_candidates = 0usize;
let mut skipped_unbounded = 0usize;
let mut skipped_budget = 0usize;
let initial_vertex_len = image_vertices.len();
let initial_index_len = image_indices.len();
let initial_cmd_len = glyph_cmds.len();
let initial_staged_bytes_len = staged_uploads.bytes.len();
let initial_staged_copies_len = staged_uploads.copies.len();
'batches: for batch in chunk.iter() {
let SegmentBatchPlan::Text { start, end } = batch else {
continue;
};
for (_, item) in &ordered_items[start..end] {
if offscreen_text_glyph_prewarm_budget_exhausted(prewarm_start, admitted_candidates)
{
skipped_budget = skipped_budget.saturating_add(1);
break 'batches;
}
let SegmentDrawItem::Text(text_index) = item else {
return Err(format!(
"text prewarm batch contains non-text draw item: {item:?}"
));
};
let Some(text_draw) = texts.get(*text_index) else {
continue;
};
text_items = text_items.saturating_add(1);
match self.text_glyph_prewarm_decision(text_draw, viewport, root_scale) {
TextGlyphPrewarmDecision::Candidate => {}
TextGlyphPrewarmDecision::MissingGeometry => {
missing_geometry = missing_geometry.saturating_add(1);
continue;
}
TextGlyphPrewarmDecision::DynamicMotion => {
dynamic_motion = dynamic_motion.saturating_add(1);
continue;
}
TextGlyphPrewarmDecision::Visible => {
visible = visible.saturating_add(1);
continue;
}
TextGlyphPrewarmDecision::OutsidePrewarmWindow => {
outside = outside.saturating_add(1);
continue;
}
}
candidates = candidates.saturating_add(1);
let Some((_, raster_rect, _, text_scale, static_text_motion)) =
self.text_raster_geometry(text_draw, root_scale)
else {
missing_geometry = missing_geometry.saturating_add(1);
continue;
};
let raster_source = text_glyph_raster_source(text_draw, raster_rect);
let source_draw = raster_source.draw.as_ref();
let run_key = Self::text_glyph_run_cache_key(
source_draw,
raster_source.raster_rect,
text_scale,
static_text_motion,
);
let atlas_generation = self.text_glyph_atlas.generation();
let cached_glyphs = if let Some(cached) = self.text_glyph_run_cache.peek(&run_key) {
if cached.atlas_generation == atlas_generation && cached.quads.is_some() {
already_prepared = already_prepared.saturating_add(1);
continue;
}
Some(cached.glyphs.len())
} else {
None
};
if !offscreen_text_glyph_prewarm_work_is_bounded(
cached_glyphs,
source_draw.text.text.len(),
) {
skipped_unbounded = skipped_unbounded.saturating_add(1);
continue;
}
admitted_candidates = admitted_candidates.saturating_add(1);
self.append_text_glyph_draws(
std::iter::once(text_draw),
viewport,
root_scale,
true,
staged_uploads,
image_vertices,
image_indices,
glyph_cmds,
)?;
image_vertices.truncate(initial_vertex_len);
image_indices.truncate(initial_index_len);
glyph_cmds.truncate(initial_cmd_len);
staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
}
}
if diag_enabled && text_items > 0 {
log::warn!(
"[text-glyph-prewarm-diag] texts={text_items} candidates={candidates} admitted={admitted_candidates} cached={already_prepared} skipped_unbounded={skipped_unbounded} skipped_budget={skipped_budget} visible={visible} outside={outside} dynamic={dynamic_motion} missing={missing_geometry}"
);
}
if admitted_candidates > 0 {
if let Some(total_ms) = should_log_wgpu_render_stage(prewarm_start, Instant::now()) {
log::warn!(
"[wgpu-render-stage:text-glyph-prewarm] total_ms={total_ms:.2} candidates={candidates} admitted={admitted_candidates} cached={already_prepared} skipped_unbounded={skipped_unbounded} skipped_budget={skipped_budget}"
);
}
}
Ok(())
}
fn prepare_text_glyph_draw_cmds<'a, I>(
&mut self,
layer_texts: I,
viewport: ViewportUniformParams,
root_scale: f32,
staged_uploads: &mut StagedBufferUploads,
) -> Result<Option<PreparedGlyphBatch>, String>
where
I: IntoIterator<Item = &'a TextDraw>,
{
#[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 glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
image_vertices.clear();
image_indices.clear();
glyph_cmds.clear();
if !self.append_text_glyph_draws(
layer_texts,
viewport,
root_scale,
false,
staged_uploads,
&mut image_vertices,
&mut image_indices,
&mut glyph_cmds,
)? {
self.scratch_image_vertices = image_vertices;
self.scratch_image_indices = image_indices;
self.scratch_glyph_cmds = glyph_cmds;
return Ok(None);
}
#[cfg(not(target_arch = "wasm32"))]
if !image_indices.is_empty() {
self.stage_native_image_buffers(
staged_uploads,
viewport,
&image_vertices,
&image_indices,
);
}
#[cfg(target_arch = "wasm32")]
let image_slot = if glyph_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 glyph_cmds.is_empty() {
0
} else {
self.prepare_wasm_viewport_uniforms(viewport)
};
self.scratch_image_vertices = image_vertices;
self.scratch_image_indices = image_indices;
Ok(Some(PreparedGlyphBatch {
cmds: glyph_cmds,
#[cfg(target_arch = "wasm32")]
image_slot,
#[cfg(target_arch = "wasm32")]
uniform_slot,
}))
}
#[allow(clippy::too_many_arguments)]
fn append_image_bitmap_draw_cmd(
&mut self,
image: &ImageBitmap,
rect: Rect,
clip: Option<Rect>,
sampling: ImageSampling,
viewport: ViewportUniformParams,
root_scale: f32,
image_vertices: &mut Vec<Vertex>,
image_indices: &mut Vec<u32>,
image_cmds: &mut Vec<ImageDrawCmd>,
) -> Result<(), String> {
if rect.width <= 0.0 || rect.height <= 0.0 {
return Ok(());
}
self.ensure_image_cached(image)?;
let (device_quad, scissor_rect) =
if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
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 = Rect {
x: left_px / root_scale,
y: top_px / root_scale,
width: width_px / root_scale,
height: height_px / root_scale,
};
let right_px = left_px + width_px;
let bottom_px = top_px + height_px;
(
[
[left_px, top_px],
[right_px, top_px],
[left_px, bottom_px],
[right_px, bottom_px],
],
snapped_rect,
)
} else {
(
rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
rect,
)
};
let Some(scissor) = scissor_rect_for_layer(
scissor_rect,
clip,
root_scale,
viewport.width,
viewport.height,
) else {
return Ok(());
};
let Some(uv_rect) = image_uv_rect(image, None) else {
return Ok(());
};
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,
]);
let color = [1.0, 1.0, 1.0, 1.0];
image_vertices.extend_from_slice(&[
Vertex {
position: device_quad[0],
color,
uv: [uv_rect.min[0], uv_rect.min[1]],
uv_bounds: uv_rect.sample_bounds,
},
Vertex {
position: device_quad[1],
color,
uv: [uv_rect.max[0], uv_rect.min[1]],
uv_bounds: uv_rect.sample_bounds,
},
Vertex {
position: device_quad[2],
color,
uv: [uv_rect.min[0], uv_rect.max[1]],
uv_bounds: uv_rect.sample_bounds,
},
Vertex {
position: device_quad[3],
color,
uv: [uv_rect.max[0], uv_rect.max[1]],
uv_bounds: uv_rect.sample_bounds,
},
]);
image_cmds.push(ImageDrawCmd {
index_start,
scissor,
image_id: image.id(),
sampling,
});
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn append_text_image_draw_cmds<'a, I>(
&mut self,
layer_texts: I,
viewport: ViewportUniformParams,
root_scale: f32,
image_vertices: &mut Vec<Vertex>,
image_indices: &mut Vec<u32>,
image_cmds: &mut Vec<ImageDrawCmd>,
) -> Result<(), String>
where
I: Iterator<Item = &'a TextDraw>,
{
let append_start = Instant::now();
let initial_len = image_cmds.len();
let mut visited = 0usize;
let mut hit_count = 0usize;
let mut miss_count = 0usize;
for text_draw in layer_texts {
visited = visited.saturating_add(1);
let _ = text_draw.node_id;
let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
self.text_raster_geometry(text_draw, root_scale)
else {
continue;
};
if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
continue;
}
let raster_source = self.text_image_raster_source(
text_draw,
logical_rect,
raster_rect,
clip,
root_scale,
static_text_motion,
);
let source_draw = raster_source.draw.as_ref();
let source_raster_rect = raster_source.raster_rect;
let cache_key = Self::text_image_cache_key(
source_draw,
source_raster_rect,
text_scale,
static_text_motion,
);
let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
self.frame_stats
.record_text_image_cache_hit(cached.image.width(), cached.image.height());
hit_count = hit_count.saturating_add(1);
cached.image.clone()
} else {
let Some(image) =
self.rasterize_text_draw_to_image(source_draw, source_raster_rect, text_scale)
else {
continue;
};
self.frame_stats
.record_text_image_cache_miss(image.width(), image.height());
miss_count = miss_count.saturating_add(1);
self.text_image_cache.put(
cache_key,
CachedTextImage {
image: image.clone(),
},
);
image
};
let draw_origin = if static_text_motion {
Point::new(
source_raster_rect.x / root_scale,
source_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,
};
self.append_image_bitmap_draw_cmd(
&image,
draw_rect,
clip,
ImageSampling::Nearest,
viewport,
root_scale,
image_vertices,
image_indices,
image_cmds,
)?;
}
let append_end = Instant::now();
if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
log::warn!(
"[wgpu-render-stage:text-images] total_ms={total_ms:.2} visited={} emitted={} hits={} misses={}",
visited,
image_cmds.len().saturating_sub(initial_len),
hit_count,
miss_count,
);
}
Ok(())
}
fn text_image_raster_source<'a>(
&mut self,
text_draw: &'a TextDraw,
logical_rect: Rect,
raster_rect: Rect,
clip: Option<Rect>,
root_scale: f32,
static_text_motion: bool,
) -> TextRasterSource<'a> {
let Some(clip) = clip else {
return TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
};
};
if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
return TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
};
}
let line_starts = self.text_line_index_cache.line_starts(&text_draw.text);
clipped_text_raster_source_with_line_starts(
text_draw,
logical_rect,
raster_rect,
clip,
root_scale,
line_starts.as_ref(),
)
}
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>,
{
#[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();
self.append_text_image_draw_cmds(
layer_texts,
viewport,
root_scale,
&mut image_vertices,
&mut image_indices,
&mut image_cmds,
)?;
#[cfg(not(target_arch = "wasm32"))]
if !image_cmds.is_empty() {
self.stage_native_image_buffers(
staged_uploads,
viewport,
&image_vertices,
&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 text_raster_geometry(
&self,
text_draw: &TextDraw,
root_scale: f32,
) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
text_raster_geometry_for_draw(text_draw, root_scale)
}
fn text_image_cache_key(
text_draw: &TextDraw,
raster_rect: Rect,
text_scale: f32,
static_text_motion: bool,
) -> TextImageCacheKey {
let mut state = default_hash::new();
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_text_raster_geometry_for_cache(raster_rect, static_text_motion, &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 text_glyph_run_cache_key(
text_draw: &TextDraw,
raster_rect: Rect,
text_scale: f32,
static_text_motion: bool,
) -> TextGlyphRunCacheKey {
TextGlyphRunCacheKey(
Self::text_image_cache_key(text_draw, raster_rect, text_scale, static_text_motion).0,
)
}
fn rasterize_text_draw_to_image(
&mut 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_with_glyph_cache(
text_draw.text.text.as_str(),
raster_rect,
&text_draw.text_style,
text_draw.color,
text_draw.font_size,
text_scale,
font,
&mut self.text_glyph_mask_cache,
);
}
if let Some(image) = rasterize_annotated_text_to_image_with_glyph_cache(
&text_draw.text,
raster_rect,
&text_draw.text_style,
text_draw.color,
text_draw.font_size,
text_scale,
&self.text_fonts,
&mut self.text_glyph_mask_cache,
) {
return Some(image);
}
rasterize_spanned_text_to_image(
text_draw,
raster_rect,
text_scale,
&self.text_fonts,
&mut self.text_glyph_mask_cache,
)
}
}
fn rasterize_spanned_text_to_image(
text_draw: &TextDraw,
raster_rect: Rect,
text_scale: f32,
fonts: &SoftwareTextFontSet,
glyph_cache: &mut SoftwareGlyphRasterCache,
) -> 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_with_glyph_cache(
content,
segment_rect,
&chunk_style,
chunk_style.resolve_text_color(text_draw.color),
chunk_font_size,
text_scale,
font,
glyph_cache,
) {
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()
}
struct TextRasterSource<'a> {
draw: Cow<'a, TextDraw>,
raster_rect: Rect,
}
fn text_glyph_raster_source(text_draw: &TextDraw, raster_rect: Rect) -> TextRasterSource<'_> {
TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
}
}
#[cfg(test)]
fn clipped_text_raster_source<'a>(
text_draw: &'a TextDraw,
logical_rect: Rect,
raster_rect: Rect,
clip: Option<Rect>,
root_scale: f32,
static_text_motion: bool,
) -> TextRasterSource<'a> {
let Some(clip) = clip else {
return TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
};
};
if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
return TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
};
}
let line_starts = line_start_offsets(text_draw.text.text.as_str());
clipped_text_raster_source_with_line_starts(
text_draw,
logical_rect,
raster_rect,
clip,
root_scale,
&line_starts,
)
}
fn clipped_text_raster_source_with_line_starts<'a>(
text_draw: &'a TextDraw,
logical_rect: Rect,
raster_rect: Rect,
clip: Rect,
root_scale: f32,
line_starts: &[usize],
) -> TextRasterSource<'a> {
if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
return TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
};
}
let Some(visible_rect) = logical_rect.intersect(clip) else {
return TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
};
};
let line_count = line_starts.len().max(1);
let line_height = logical_rect.height / line_count as f32;
if !line_height.is_finite() || line_height <= 0.0 {
return TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
};
}
let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
let visible_bottom =
((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
let start_line = visible_top.saturating_sub(1).max(0) as usize;
let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
let end_line = end_line.min(line_count);
if start_line == 0 && end_line >= line_count {
return TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
};
}
let byte_start = line_starts[start_line];
let byte_end = line_end_offset(text_draw.text.text.as_str(), line_starts, end_line - 1);
if byte_start >= byte_end {
return TextRasterSource {
draw: Cow::Borrowed(text_draw),
raster_rect,
};
}
let slice_y = logical_rect.y + start_line as f32 * line_height;
let slice_height = (end_line - start_line) as f32 * line_height;
let mut slice_raster_rect = Rect {
x: logical_rect.x * root_scale,
y: slice_y * root_scale,
width: logical_rect.width * root_scale,
height: slice_height * root_scale,
};
slice_raster_rect.x = slice_raster_rect.x.round();
slice_raster_rect.y = slice_raster_rect.y.round();
slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
let mut sliced_draw = text_draw.clone();
sliced_draw.rect = Rect {
x: logical_rect.x,
y: slice_y,
width: logical_rect.width,
height: slice_height,
};
sliced_draw.text = Rc::new(text_draw.text.subsequence(byte_start..byte_end));
TextRasterSource {
draw: Cow::Owned(sliced_draw),
raster_rect: slice_raster_rect,
}
}
fn line_start_offsets(text: &str) -> Vec<usize> {
let mut starts =
Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
starts.push(0);
starts.extend(
text.char_indices()
.filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
);
starts
}
fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
line_starts.get(line + 1).copied().unwrap_or(text.len())
}
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),
Composite(usize),
ShaderComposite(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,
},
Composite {
start: usize,
end: usize,
},
ShaderComposite {
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,
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TextGlyphPrewarmDecision {
Candidate,
MissingGeometry,
DynamicMotion,
Visible,
OutsidePrewarmWindow,
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct NativeSegmentFusionBudget {
shape_count: usize,
gradient_stop_count: usize,
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, PartialEq, Eq)]
struct NativeSegmentFusionPartition {
chunk: SegmentDrawChunkPlan,
budget: NativeSegmentFusionBudget,
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, PartialEq, Eq)]
enum FusedSegmentBatch {
Shape {
batch: PreparedShapeBatch,
blend_mode: BlendMode,
},
Image {
cmd_range: Range<usize>,
blend_mode: BlendMode,
},
Text {
image_cmd_range: Range<usize>,
glyph_cmd_range: Range<usize>,
},
Composite {
draw_range: Range<usize>,
},
ShaderComposite {
draw_range: Range<usize>,
},
}
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_start: u32,
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
}
}
struct PreparedGlyphBatch {
cmds: Vec<GlyphDrawCmd>,
#[cfg(target_arch = "wasm32")]
image_slot: usize,
#[cfg(target_arch = "wasm32")]
uniform_slot: usize,
}
impl PreparedGlyphBatch {
fn is_empty(&self) -> bool {
self.cmds.is_empty()
}
fn into_cmds(self) -> Vec<GlyphDrawCmd> {
self.cmds
}
}
#[cfg(not(target_arch = "wasm32"))]
fn gradient_stop_count_for_shape(shape: &DrawShape) -> usize {
match &shape.brush {
Brush::Solid(_) => 0,
Brush::LinearGradient { colors, .. }
| Brush::RadialGradient { colors, .. }
| Brush::SweepGradient { colors, .. } => colors.len(),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn native_segment_fusion_budget(
ordered_items: &[(usize, SegmentDrawItem)],
shapes: &[DrawShape],
chunk: &SegmentDrawChunkPlan,
) -> Result<Option<NativeSegmentFusionBudget>, String> {
let mut shape_count = 0usize;
let mut gradient_stop_count = 0usize;
for batch in chunk.iter() {
let SegmentBatchPlan::Shape { start, end, .. } = batch else {
continue;
};
for (_, item) in &ordered_items[start..end] {
let SegmentDrawItem::Shape(shape_index) = item else {
return Err(format!(
"shape batch contains non-shape draw item: {item:?}"
));
};
let shape = &shapes[*shape_index];
shape_count = shape_count.saturating_add(1);
gradient_stop_count =
gradient_stop_count.saturating_add(gradient_stop_count_for_shape(shape));
}
}
if shape_count > MAX_SHAPES_PER_BATCH || gradient_stop_count > MAX_GRADIENT_STOPS {
return Ok(None);
}
Ok(Some(NativeSegmentFusionBudget {
shape_count,
gradient_stop_count,
}))
}
#[cfg(not(target_arch = "wasm32"))]
fn push_native_segment_fusion_partition(
partitions: &mut Vec<NativeSegmentFusionPartition>,
current: &mut SegmentDrawChunkPlan,
current_budget: &mut NativeSegmentFusionBudget,
) {
if current.is_empty() {
return;
}
partitions.push(NativeSegmentFusionPartition {
chunk: std::mem::take(current),
budget: *current_budget,
});
*current_budget = NativeSegmentFusionBudget {
shape_count: 0,
gradient_stop_count: 0,
};
}
#[cfg(not(target_arch = "wasm32"))]
fn native_segment_fusion_partitions(
ordered_items: &[(usize, SegmentDrawItem)],
shapes: &[DrawShape],
chunk: &SegmentDrawChunkPlan,
) -> Result<Option<Vec<NativeSegmentFusionPartition>>, String> {
if let Some(budget) = native_segment_fusion_budget(ordered_items, shapes, chunk)? {
return Ok(Some(vec![NativeSegmentFusionPartition {
chunk: chunk.clone(),
budget,
}]));
}
let mut partitions = Vec::new();
let mut current = SegmentDrawChunkPlan::default();
let mut current_budget = NativeSegmentFusionBudget {
shape_count: 0,
gradient_stop_count: 0,
};
for batch in chunk.iter() {
let SegmentBatchPlan::Shape {
start,
end,
blend_mode,
} = batch
else {
current.push(batch);
continue;
};
let mut run_start = start;
for (item_cursor, (_, item)) in ordered_items.iter().enumerate().take(end).skip(start) {
let SegmentDrawItem::Shape(shape_index) = *item else {
return Err(format!(
"shape batch contains non-shape draw item: {:?}",
item
));
};
let gradient_stop_count = gradient_stop_count_for_shape(&shapes[shape_index]);
if gradient_stop_count > MAX_GRADIENT_STOPS {
return Ok(None);
}
let fits_shape_count =
current_budget.shape_count.saturating_add(1) <= MAX_SHAPES_PER_BATCH;
let fits_gradient_count = current_budget
.gradient_stop_count
.saturating_add(gradient_stop_count)
<= MAX_GRADIENT_STOPS;
if !fits_shape_count || !fits_gradient_count {
if run_start < item_cursor {
current.push(SegmentBatchPlan::Shape {
start: run_start,
end: item_cursor,
blend_mode,
});
}
push_native_segment_fusion_partition(
&mut partitions,
&mut current,
&mut current_budget,
);
run_start = item_cursor;
}
current_budget.shape_count = current_budget.shape_count.saturating_add(1);
current_budget.gradient_stop_count = current_budget
.gradient_stop_count
.saturating_add(gradient_stop_count);
}
if run_start < end {
current.push(SegmentBatchPlan::Shape {
start: run_start,
end,
blend_mode,
});
}
}
push_native_segment_fusion_partition(&mut partitions, &mut current, &mut current_budget);
Ok(Some(partitions))
}
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::Composite(_) => {
let mut end = start + 1;
while end < ordered_items.len() {
if matches!(ordered_items[end].1, SegmentDrawItem::Composite(_)) {
end += 1;
} else {
break;
}
}
Some((SegmentBatchPlan::Composite { start, end }, end))
}
SegmentDrawItem::ShaderComposite(_) => {
let mut end = start + 1;
while end < ordered_items.len() {
if matches!(ordered_items[end].1, SegmentDrawItem::ShaderComposite(_)) {
end += 1;
} else {
break;
}
}
Some((SegmentBatchPlan::ShaderComposite { 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>],
width: u32,
height: u32,
root_scale: f32,
scratch: &mut Vec<(usize, SegmentDrawItem)>,
) {
scratch.clear();
let viewport = ViewportUniformParams {
width,
height,
offset: [0.0, 0.0],
};
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) => {
let shape = shapes.get(index)?;
if !shape_draw_is_visible_in_viewport(shape, viewport, root_scale) {
return None;
}
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 retain_renderable_shadow_items(
ordered_items: &mut Vec<(usize, SegmentDrawItem)>,
shadow_draws: &[ShadowDraw],
width: u32,
height: u32,
root_scale: f32,
max_texture_dim: u32,
) -> usize {
let original_len = ordered_items.len();
ordered_items.retain(|(_, item)| match item {
SegmentDrawItem::Shadow(index) => shadow_draws.get(*index).is_some_and(|shadow| {
shadow_draw_may_render(shadow, width, height, root_scale, max_texture_dim)
}),
_ => true,
});
original_len.saturating_sub(ordered_items.len())
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy)]
struct SegmentDiagCounts {
raw_shadow_items: usize,
culled_shadow_items: usize,
cached_shadow_composites: usize,
composite_items: usize,
shader_composite_items: usize,
}
#[cfg(not(target_arch = "wasm32"))]
fn maybe_print_segment_diag(
z_range: Range<usize>,
ordered_items: &[(usize, SegmentDrawItem)],
shapes: &[DrawShape],
images: &[ImageDraw],
counts: SegmentDiagCounts,
) {
if std::env::var_os("CRANPOSE_SEGMENT_DIAG").is_none() {
return;
}
let line = SEGMENT_DIAG_LINES.fetch_add(1, Ordering::Relaxed);
if line >= 64 {
return;
}
let remaining_shadow_items = ordered_items
.iter()
.filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
.count();
let commands: Vec<_> = SegmentCommandIter::new(ordered_items, shapes, images).collect();
let draw_chunks = commands
.iter()
.filter(|command| matches!(command, SegmentRenderCommand::DrawChunk(_)))
.count();
let shadow_commands = commands
.iter()
.filter(|command| matches!(command, SegmentRenderCommand::Shadow(_)))
.count();
let mut native_partitions = 0usize;
let mut native_unfused_chunks = 0usize;
for command in &commands {
let SegmentRenderCommand::DrawChunk(chunk) = command else {
continue;
};
match native_segment_fusion_partitions(ordered_items, shapes, chunk) {
Ok(Some(partitions)) => native_partitions += partitions.len(),
Ok(None) | Err(_) => native_unfused_chunks += 1,
}
}
eprintln!(
"[segment-diag #{line}] z={}..{} items={} raw_shadows={} culled_shadows={} cached_shadows={} remaining_shadows={} composites={} shader_composites={} draw_chunks={} shadow_commands={} native_partitions={} native_unfused_chunks={}",
z_range.start,
z_range.end,
ordered_items.len(),
counts.raw_shadow_items,
counts.culled_shadow_items,
counts.cached_shadow_composites,
remaining_shadow_items,
counts.composite_items,
counts.shader_composite_items,
draw_chunks,
shadow_commands,
native_partitions,
native_unfused_chunks,
);
}
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 glyph_atlas_uv_rect(entry: GlyphAtlasEntry) -> ImageUvRect {
let atlas_width = TEXT_GLYPH_ATLAS_WIDTH as f32;
let atlas_height = TEXT_GLYPH_ATLAS_HEIGHT as f32;
let min = [entry.x as f32 / atlas_width, entry.y as f32 / atlas_height];
let max = [
(entry.x + entry.width) as f32 / atlas_width,
(entry.y + entry.height) as f32 / atlas_height,
];
let center_min = [
(entry.x as f32 + 0.5) / atlas_width,
(entry.y as f32 + 0.5) / atlas_height,
];
let center_max = [
(entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) / atlas_width,
(entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) / atlas_height,
];
ImageUvRect {
min,
max,
sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
}
}
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 crate::normalized_scene::visible_draw_rect;
use cranpose_foundation::lazy::{remember_lazy_list_state, LazyListScope, LazyListState};
use cranpose_render_common::graph::{DrawPrimitiveNode, IsolationReasons, TextPrimitiveNode};
use cranpose_render_common::raster_cache::LayerRasterCacheHashes;
use cranpose_render_common::scene_builder::build_graph_from_applier;
use cranpose_ui::text::{
AnnotatedString, BaselineShift, RangeStyle, Shadow, SpanStyle, TextDecoration,
TextDrawStyle, TextGeometricTransform, TextMotion, TextUnit,
};
use cranpose_ui::{
LayoutEngine, LazyColumn, LazyColumnSpec, Modifier, Size, Text, TextLayoutOptions,
TextStyle,
};
use cranpose_ui_graphics::{
Brush, Color, CornerRadii, DrawPrimitive, Rect, RenderEffect, RoundedCornerShape,
RuntimeShader,
};
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 assert_snap_anchor_close(actual: Option<SnapAnchor>, expected_origin: Point, message: &str) {
let Some(actual) = actual else {
panic!("{message}: missing snap anchor");
};
let expected = SnapAnchor::rigid(expected_origin);
assert_eq!(
actual.device_pixel_step, expected.device_pixel_step,
"{message}: device pixel step changed"
);
assert!(
(actual.origin.x - expected.origin.x).abs() <= 1e-4
&& (actual.origin.y - expected.origin.y).abs() <= 1e-4,
"{message}: expected origin {:?}, got {:?}",
expected.origin,
actual.origin
);
}
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 direct_shader_composite_accepts_box4_when_viewport_preserves_source_pixels() {
assert_eq!(
direct_shader_composite_viewport(
1.0,
BlendMode::SrcOver,
Some((12.0, 18.0, 64.0, 32.0)),
CompositeSampleMode::Box4,
(64, 32),
),
Some((12.0, 18.0, 64.0, 32.0))
);
}
#[test]
fn direct_shader_composite_rejects_box4_when_viewport_resamples_source() {
assert_eq!(
direct_shader_composite_viewport(
1.0,
BlendMode::SrcOver,
Some((12.0, 18.0, 64.5, 32.0)),
CompositeSampleMode::Box4,
(64, 32),
),
None
);
assert_eq!(
direct_shader_composite_viewport(
1.0,
BlendMode::SrcOver,
Some((12.25, 18.0, 64.0, 32.0)),
CompositeSampleMode::Box4,
(64, 32),
),
None
);
}
fn test_text_draw(rect: Rect, text_motion: TextMotion) -> TextDraw {
let mut text_style = TextStyle::default();
text_style.paragraph_style.text_motion = Some(text_motion);
TextDraw {
node_id: 42,
rect,
snap_anchor: None,
translated_content_context: false,
text: Rc::new(AnnotatedString::new("stable markdown row".to_string())),
color: Color::WHITE,
text_style,
font_size: 14.0,
scale: 1.0,
layout_options: TextLayoutOptions::default(),
z_index: 0,
clip: None,
}
}
#[test]
fn static_text_image_cache_key_ignores_absolute_scroll_position() {
let base = test_text_draw(
Rect {
x: 12.25,
y: 40.75,
width: 220.0,
height: 24.0,
},
TextMotion::Static,
);
let scrolled = test_text_draw(
Rect {
x: 12.75,
y: -318.5,
width: 220.0,
height: 24.0,
},
TextMotion::Static,
);
let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, true);
let scrolled_key = GpuRenderer::text_image_cache_key(&scrolled, scrolled.rect, 1.0, true);
assert_eq!(
base_key, scrolled_key,
"scrolling static text must reuse the same raster cache entry"
);
}
#[test]
fn static_text_glyph_run_cache_key_ignores_absolute_scroll_position() {
let base = test_text_draw(
Rect {
x: 12.25,
y: 40.75,
width: 220.0,
height: 24.0,
},
TextMotion::Static,
);
let scrolled = test_text_draw(
Rect {
x: 12.75,
y: -318.5,
width: 220.0,
height: 24.0,
},
TextMotion::Static,
);
let base_key = GpuRenderer::text_glyph_run_cache_key(&base, base.rect, 1.0, true);
let scrolled_key =
GpuRenderer::text_glyph_run_cache_key(&scrolled, scrolled.rect, 1.0, true);
assert_eq!(
base_key, scrolled_key,
"scrolling static text must reuse the same retained glyph run"
);
}
#[test]
fn static_multiline_text_glyph_source_keeps_full_text_when_image_source_slices() {
let rect = Rect {
x: 8.0,
y: 100.0,
width: 240.0,
height: 1_000.0,
};
let mut draw = test_text_draw(rect, TextMotion::Static);
let lines = (0..100)
.map(|line| format!("line-{line:03}"))
.collect::<Vec<_>>()
.join("\n");
draw.text = Rc::new(AnnotatedString::from(lines));
let raster_rect = Rect {
x: 16.0,
y: 200.0,
width: 480.0,
height: 2_000.0,
};
let clipped = clipped_text_raster_source(
&draw,
rect,
raster_rect,
Some(Rect {
x: 0.0,
y: 610.0,
width: 800.0,
height: 40.0,
}),
2.0,
true,
);
let glyph = text_glyph_raster_source(&draw, raster_rect);
assert!(
matches!(clipped.draw, Cow::Owned(_)),
"the image source should still slice large clipped multiline text"
);
assert!(
matches!(glyph.draw, Cow::Borrowed(_)),
"the glyph source must keep a stable full-text run key while scrolling"
);
let clipped_key = GpuRenderer::text_glyph_run_cache_key(
clipped.draw.as_ref(),
clipped.raster_rect,
2.0,
true,
);
let glyph_key = GpuRenderer::text_glyph_run_cache_key(
glyph.draw.as_ref(),
glyph.raster_rect,
2.0,
true,
);
assert_ne!(
clipped_key, glyph_key,
"image slicing must not force glyph rendering onto per-scroll line-window cache keys"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn retained_glyph_viewport_offsets_relative_vertices_by_source_origin() {
let viewport = ViewportUniformParams {
width: 800,
height: 600,
offset: [10.0, 20.0],
};
let source = Rect {
x: 40.0,
y: 90.0,
width: 120.0,
height: 48.0,
};
let retained = GpuRenderer::retained_glyph_viewport(viewport, source);
assert_eq!(retained.width, viewport.width);
assert_eq!(retained.height, viewport.height);
assert_eq!(retained.offset, [-30.0, -70.0]);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn tiny_text_glyph_runs_stay_in_shared_uploads() {
assert!(
!should_use_retained_text_glyph_run(8, None),
"tiny labels must stay in the shared fused batch"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn line_sized_text_glyph_runs_stay_in_shared_uploads() {
assert!(
!should_use_retained_text_glyph_run(64, None),
"Markdown scroll frames contain many line-sized text runs; retaining each one creates per-run buffer binds instead of one shared glyph batch"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn large_clipped_text_glyph_runs_stay_in_shared_uploads() {
assert!(
!should_use_retained_text_glyph_run(
MIN_RETAINED_TEXT_GLYPH_QUADS.saturating_mul(2),
Some(Rect {
x: 0.0,
y: 0.0,
width: 200.0,
height: 100.0,
}),
),
"clipped lazy-list text must not draw a full retained run outside the viewport"
);
}
#[test]
fn normal_text_glyph_draw_skips_offscreen_prewarm_candidates() {
assert_eq!(
text_glyph_draw_action(false, true, false),
TextGlyphDrawAction::Skip,
"normal draw traversal must not prepare offscreen text"
);
}
#[test]
fn bounded_text_glyph_prewarm_admits_offscreen_candidates() {
assert_eq!(
text_glyph_draw_action(false, true, true),
TextGlyphDrawAction::PrewarmOffscreen,
"only the bounded prewarm path may prepare offscreen text"
);
}
#[test]
fn visible_text_glyph_draws_are_always_admitted() {
assert_eq!(
text_glyph_draw_action(true, false, false),
TextGlyphDrawAction::DrawVisible
);
assert_eq!(
text_glyph_draw_action(true, true, true),
TextGlyphDrawAction::DrawVisible
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn offscreen_text_prewarm_skips_large_uncached_text_runs() {
assert!(
!offscreen_text_glyph_prewarm_work_is_bounded(
None,
MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS + 1,
),
"offscreen prewarm must not collect large uncached text runs in an input frame"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn offscreen_text_prewarm_admits_small_uncached_text_runs() {
assert!(
offscreen_text_glyph_prewarm_work_is_bounded(
None,
MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
),
"small labels can be warmed without risking a frame-budget spike"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn offscreen_text_prewarm_skips_large_cached_runs_without_quads() {
assert!(
!offscreen_text_glyph_prewarm_work_is_bounded(
Some(MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS + 1),
0,
),
"cached glyph placements can still be too large to prepare during input frames"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn offscreen_text_prewarm_stops_after_candidate_budget() {
assert!(
offscreen_text_glyph_prewarm_budget_exhausted(
Instant::now(),
MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES,
),
"prewarm must be bounded by candidate count even when each candidate is cheap"
);
}
#[test]
fn clipped_cached_glyph_quads_are_filtered_to_viewport() {
fn quad(y: i32) -> CachedTextGlyphQuad {
CachedTextGlyphQuad {
x: 8,
y,
width: 20,
height: 10,
color: (1.0, 1.0, 1.0, 1.0),
uv: ImageUvRect {
min: [0.0, 0.0],
max: [1.0, 1.0],
sample_bounds: [0.0, 0.0, 1.0, 1.0],
},
}
}
let source = Rect {
x: 0.0,
y: 0.0,
width: 320.0,
height: 400.0,
};
let clip = Some(Rect {
x: 0.0,
y: 0.0,
width: 320.0,
height: 80.0,
});
let viewport = ViewportUniformParams {
width: 320,
height: 80,
offset: [0.0, 0.0],
};
assert!(cached_text_glyph_quad_is_visible_in_viewport(
source,
&quad(40),
clip,
viewport,
1.0,
));
assert!(
!cached_text_glyph_quad_is_visible_in_viewport(source, &quad(140), clip, viewport, 1.0,),
"glyphs outside the effective clip should not enter the frame command stream"
);
}
#[test]
fn small_scene_range_cache_miss_observes_first_render() {
let key = LayerRasterCacheKey::scene_range(
0xCACE,
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 80.0,
},
(120, 80),
ScaleBucket::from_scale(1.0),
);
assert!(
!first_cache_miss_admission(&key),
"a small scene-range miss should render directly first instead of materializing a tiny one-frame retained target"
);
assert!(
repeated_cache_miss_admission(&key),
"a repeated small scene-range miss is stable enough to materialize into the retained cache"
);
}
#[test]
fn large_scene_range_cache_miss_requires_repeated_stable_key() {
let key = LayerRasterCacheKey::scene_range(
0xCACE,
Rect {
x: 0.0,
y: 0.0,
width: 1200.0,
height: 900.0,
},
(1200, 900),
ScaleBucket::from_scale(1.0),
);
assert!(
!first_cache_miss_admission(&key),
"a large first scene-range miss should render directly instead of materializing a multi-MB one-frame cache entry"
);
assert!(
repeated_cache_miss_admission(&key),
"a repeated scene-range miss is stable enough to materialize into the retained cache"
);
}
#[test]
fn renderer_warmup_frame_is_requested_for_cache_miss_stats_only() {
let stats = gpu_stats::FrameStats::default();
let mut snapshot = stats.snapshot();
assert!(
!frame_stats_need_warmup_frame(&snapshot),
"a clean frame must not keep a static scene redrawing"
);
snapshot.layer_cache_misses = 1;
assert!(frame_stats_need_warmup_frame(&snapshot));
snapshot.layer_cache_misses = 0;
snapshot.shadow_shape_cache_misses = 1;
assert!(frame_stats_need_warmup_frame(&snapshot));
snapshot.shadow_shape_cache_misses = 0;
snapshot.text_image_cache_misses = 1;
assert!(frame_stats_need_warmup_frame(&snapshot));
snapshot.text_image_cache_misses = 0;
snapshot.text_glyph_atlas_misses = 1;
assert!(frame_stats_need_warmup_frame(&snapshot));
}
#[test]
fn non_scene_layer_surface_cache_miss_admits_first_render() {
let key = LayerRasterCacheKey::new(
Some(77),
0xC0FFEE,
0,
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 80.0,
},
(120, 80),
ScaleBucket::from_scale(1.0),
);
assert!(
first_cache_miss_admission(&key),
"ordinary retained layer surfaces should still cache on first miss"
);
}
#[test]
fn text_image_cache_key_is_content_addressed_not_node_addressed() {
let first = test_text_draw(
Rect {
x: 12.25,
y: 40.75,
width: 220.0,
height: 24.0,
},
TextMotion::Static,
);
let mut second = first.clone();
second.node_id = first.node_id + 1;
let first_key = GpuRenderer::text_image_cache_key(&first, first.rect, 1.0, true);
let second_key = GpuRenderer::text_image_cache_key(&second, second.rect, 1.0, true);
assert_eq!(
first_key, second_key,
"text raster cache keys must be based on rendered pixels, not node identity"
);
}
#[test]
fn animated_text_image_cache_key_keeps_fractional_phase_only() {
let base = test_text_draw(
Rect {
x: 12.25,
y: 40.75,
width: 220.0,
height: 24.0,
},
TextMotion::Animated,
);
let integer_translated = test_text_draw(
Rect {
x: 44.25,
y: 88.75,
width: 220.0,
height: 24.0,
},
TextMotion::Animated,
);
let phase_shifted = test_text_draw(
Rect {
x: 44.5,
y: 88.75,
width: 220.0,
height: 24.0,
},
TextMotion::Animated,
);
let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, false);
let translated_key = GpuRenderer::text_image_cache_key(
&integer_translated,
integer_translated.rect,
1.0,
false,
);
let phase_shifted_key =
GpuRenderer::text_image_cache_key(&phase_shifted, phase_shifted.rect, 1.0, false);
assert_eq!(
base_key, translated_key,
"integer translation should not invalidate animated text raster cache entries"
);
assert_ne!(
base_key, phase_shifted_key,
"fractional phase affects animated text rasterization and must stay in the key"
);
}
#[test]
fn animated_translated_text_raster_geometry_applies_snap_anchor() {
let mut base = test_text_draw(
Rect {
x: 14.25,
y: 16.50,
width: 220.0,
height: 24.0,
},
TextMotion::Animated,
);
base.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.50)));
let mut scrolled = test_text_draw(
Rect {
x: 14.25,
y: 15.80,
width: 220.0,
height: 24.0,
},
TextMotion::Animated,
);
scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 15.80)));
let (base_logical, base_raster, _, _, base_static) =
text_raster_geometry_for_draw(&base, 1.0).expect("base text geometry");
let (scrolled_logical, scrolled_raster, _, _, scrolled_static) =
text_raster_geometry_for_draw(&scrolled, 1.0).expect("scrolled text geometry");
assert!(!base_static);
assert!(!scrolled_static);
assert!((base_logical.x - 14.0).abs() < f32::EPSILON);
assert!((base_logical.y - 17.0).abs() < f32::EPSILON);
assert!((scrolled_logical.x - 14.0).abs() < f32::EPSILON);
assert!((scrolled_logical.y - 16.0).abs() < f32::EPSILON);
assert_eq!(base_raster.x.fract(), 0.0);
assert_eq!(base_raster.y.fract(), 0.0);
assert_eq!(scrolled_raster.x.fract(), 0.0);
assert_eq!(scrolled_raster.y.fract(), 0.0);
let base_key = GpuRenderer::text_image_cache_key(&base, base_raster, 1.0, false);
let scrolled_key =
GpuRenderer::text_image_cache_key(&scrolled, scrolled_raster, 1.0, false);
assert_eq!(
base_key, scrolled_key,
"translated animated text should keep a stable raster phase while scrolling"
);
}
#[test]
fn clipped_static_multiline_text_raster_source_limits_visible_line_window() {
let rect = Rect {
x: 8.0,
y: 100.0,
width: 240.0,
height: 1_000.0,
};
let mut draw = test_text_draw(rect, TextMotion::Static);
let lines = (0..100)
.map(|line| format!("line-{line:03}"))
.collect::<Vec<_>>()
.join("\n");
draw.text = Rc::new(AnnotatedString::from(lines));
let raster_rect = Rect {
x: 16.0,
y: 200.0,
width: 480.0,
height: 2_000.0,
};
let source = clipped_text_raster_source(
&draw,
rect,
raster_rect,
Some(Rect {
x: 0.0,
y: 610.0,
width: 800.0,
height: 40.0,
}),
2.0,
true,
);
let Cow::Owned(sliced_draw) = source.draw else {
panic!("clipped static multiline text should rasterize only the visible line window");
};
let sliced_text = sliced_draw.text.text.as_str();
assert!(sliced_text.contains("line-050"));
assert!(sliced_text.contains("line-055"));
assert!(!sliced_text.contains("line-000"));
assert!(!sliced_text.contains("line-099"));
assert_eq!(source.raster_rect.x, raster_rect.x);
assert!(source.raster_rect.y > raster_rect.y);
assert!(source.raster_rect.height < raster_rect.height);
}
#[test]
fn clipped_static_multiline_text_raster_source_slices_short_multiline_text() {
let rect = Rect {
x: 8.0,
y: 100.0,
width: 240.0,
height: 320.0,
};
let mut draw = test_text_draw(rect, TextMotion::Static);
let lines = (0..24)
.map(|line| format!("code-line-{line:02}"))
.collect::<Vec<_>>()
.join("\n");
draw.text = Rc::new(AnnotatedString::from(lines));
let raster_rect = Rect {
x: 16.0,
y: 200.0,
width: 480.0,
height: 640.0,
};
let source = clipped_text_raster_source(
&draw,
rect,
raster_rect,
Some(Rect {
x: 0.0,
y: 190.0,
width: 800.0,
height: 120.0,
}),
2.0,
true,
);
let Cow::Owned(sliced_draw) = source.draw else {
panic!("clipped multiline text should rasterize only the visible line window");
};
assert!(sliced_draw.text.text.as_str().contains("code-line-06"));
assert!(!sliced_draw.text.text.as_str().contains("code-line-00"));
assert!(!sliced_draw.text.text.as_str().contains("code-line-23"));
assert_eq!(source.raster_rect.x, raster_rect.x);
assert!(source.raster_rect.y > raster_rect.y);
assert!(source.raster_rect.height < raster_rect.height);
}
#[test]
fn text_line_index_cache_reuses_retained_index_for_same_text_instance() {
let mut cache = TextLineIndexCache::new(4);
let text = Rc::new(AnnotatedString::from("a\nb\nc"));
let first = cache.line_starts(&text);
let second = cache.line_starts(&text);
assert_eq!(first.as_ref(), &[0, 2, 4]);
assert!(
Rc::ptr_eq(&first, &second),
"retained text should not rebuild its line index on every clipped frame"
);
}
#[test]
fn text_line_index_cache_is_retained_text_instance_local() {
let mut cache = TextLineIndexCache::new(4);
let first_text = Rc::new(AnnotatedString::from("a\nb\nc"));
let second_text = Rc::new(AnnotatedString::from("a\nb\nc"));
let first = cache.line_starts(&first_text);
let second = cache.line_starts(&second_text);
assert_eq!(first.as_ref(), second.as_ref());
assert!(
!Rc::ptr_eq(&first, &second),
"line index lookup should not hash large text contents to find unrelated retained nodes"
);
}
#[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 {
node_id: Some(700 + z_index),
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,
motion_context_animated: false,
}
}
#[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_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, root_scale);
let translated_hash = shape_shadow_content_hash(&translated_shapes, 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, root_scale);
assert_ne!(first_hash, changed_hash);
}
#[test]
fn shape_shadow_content_hash_is_stable_under_fractional_scale_scroll() {
fn shadow_shapes_at(y: f32) -> Vec<(DrawShape, BlendMode)> {
let mut shape = test_shape(1, BlendMode::SrcOver);
shape.rect = Rect {
x: 24.0,
y,
width: 180.0,
height: 90.0,
};
shape.local_rect = shape.rect;
shape.quad = crate::rect_to_quad(shape.rect);
shape.shape = Some(RoundedCornerShape::uniform(14.0));
vec![(shape, BlendMode::SrcOver)]
}
let root_scale = 130.0f32 / 96.0;
let blur_radius = 18.0f32;
let pixel_radius = blur_radius * root_scale;
let key_at = |y: f32| {
let shapes = shadow_shapes_at(y);
let plan =
shape_shadow_surface_plan(&shapes, None, blur_radius, 1600, 1600, root_scale, 8192)
.expect("surface plan");
shape_shadow_surface_cache_key(
&shapes,
plan.source_device_bounds,
pixel_radius,
root_scale,
)
.expect("cache key")
};
let base = key_at(640.0);
for step in 1..=12 {
let scrolled = key_at(640.0 - step as f32 * 4.0);
assert_eq!(
base, scrolled,
"scrolled shadow cache key must stay stable at fractional scale (step {step})"
);
}
}
#[test]
fn shape_shadow_cache_key_uses_unclipped_source_bounds_for_scrolled_clip() {
fn translated_card_shadow(y: f32) -> Vec<(DrawShape, BlendMode)> {
let mut shape = test_shape(1, BlendMode::SrcOver);
shape.rect = Rect {
x: 24.0,
y,
width: 280.0,
height: 120.0,
};
shape.local_rect = shape.rect;
shape.quad = [[24.0, y], [304.0, y], [24.0, y + 120.0], [304.0, y + 120.0]];
shape.shape = Some(RoundedCornerShape::uniform(18.0));
vec![(shape, BlendMode::SrcOver)]
}
let root_scale = 1.0;
let blur_radius = 18.0;
let viewport_clip = Rect {
x: 0.0,
y: 96.0,
width: 360.0,
height: 720.0,
};
let key_for = |y: f32| {
let shapes = translated_card_shadow(y);
let plan = shape_shadow_surface_plan(
&shapes,
Some(viewport_clip),
blur_radius,
360,
900,
root_scale,
4096,
)
.expect("surface plan");
shape_shadow_surface_cache_key(
&shapes,
plan.source_device_bounds,
plan.pixel_radius,
root_scale,
)
.expect("cache key")
};
assert_eq!(key_for(740.0), key_for(756.0));
}
#[test]
fn shape_visibility_uses_nonzero_viewport_offset_for_cropped_offscreen() {
let mut shape = test_shape(1, BlendMode::SrcOver);
shape.rect = Rect {
x: 24.0,
y: 740.0,
width: 280.0,
height: 120.0,
};
shape.local_rect = shape.rect;
shape.quad = [[24.0, 740.0], [304.0, 740.0], [24.0, 860.0], [304.0, 860.0]];
let viewport = ViewportUniformParams {
width: 316,
height: 228,
offset: [6.0, 686.0],
};
assert!(shape_draw_is_visible_in_viewport(&shape, viewport, 1.0));
}
#[test]
fn text_prewarm_uses_nonzero_viewport_offset_for_cropped_offscreen() {
let viewport = ViewportUniformParams {
width: 316,
height: 228,
offset: [6.0, 686.0],
};
let text_rect = Rect {
x: 24.0,
y: 740.0,
width: 280.0,
height: 40.0,
};
assert!(text_draw_is_visible_in_viewport(
text_rect, None, viewport, 1.0
));
assert!(text_draw_should_prewarm_in_viewport(
text_rect, None, viewport, 1.0
));
}
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,
}
}
#[test]
fn text_draw_visibility_rejects_text_outside_clip_before_rasterization() {
let viewport = ViewportUniformParams {
width: 320,
height: 240,
offset: [0.0, 0.0],
};
let text_rect = Rect {
x: 0.0,
y: 260.0,
width: 200.0,
height: 40.0,
};
let clip = Some(Rect {
x: 0.0,
y: 0.0,
width: 320.0,
height: 200.0,
});
assert!(
!text_draw_is_visible_in_viewport(text_rect, clip, viewport, 1.0),
"lazy-list beyond-bound text outside the clip must not be rasterized"
);
}
#[test]
fn text_draw_prewarm_accepts_clipped_text_near_viewport() {
let viewport = ViewportUniformParams {
width: 320,
height: 240,
offset: [0.0, 0.0],
};
let text_rect = Rect {
x: 0.0,
y: 260.0,
width: 200.0,
height: 40.0,
};
let clip = Some(Rect {
x: 0.0,
y: 0.0,
width: 320.0,
height: 200.0,
});
assert!(!text_draw_is_visible_in_viewport(
text_rect, clip, viewport, 1.0
));
assert!(text_draw_should_prewarm_in_viewport(
text_rect, clip, viewport, 1.0
));
}
#[test]
fn text_draw_prewarm_rejects_far_clipped_text() {
let viewport = ViewportUniformParams {
width: 320,
height: 240,
offset: [0.0, 0.0],
};
let text_rect = Rect {
x: 0.0,
y: 1600.0,
width: 200.0,
height: 40.0,
};
let clip = Some(Rect {
x: 0.0,
y: 0.0,
width: 320.0,
height: 200.0,
});
assert!(!text_draw_should_prewarm_in_viewport(
text_rect, clip, viewport, 1.0
));
}
#[test]
fn text_draw_visibility_rejects_unclipped_text_outside_viewport() {
let viewport = ViewportUniformParams {
width: 320,
height: 240,
offset: [0.0, 0.0],
};
let text_rect = Rect {
x: 0.0,
y: 241.0,
width: 200.0,
height: 40.0,
};
assert!(
!text_draw_is_visible_in_viewport(text_rect, None, viewport, 1.0),
"unclipped text outside the target viewport must not be rasterized"
);
}
#[test]
fn text_draw_visibility_keeps_partially_visible_text() {
let viewport = ViewportUniformParams {
width: 320,
height: 240,
offset: [0.0, 0.0],
};
let text_rect = Rect {
x: 0.0,
y: 220.0,
width: 200.0,
height: 40.0,
};
assert!(text_draw_is_visible_in_viewport(
text_rect, None, viewport, 1.0
));
}
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(),
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(),
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(),
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));
}
fn child_layer_composite<'a>(
layer: &'a LayerNode,
z_index: usize,
rect: Rect,
needs_nested_underlay: bool,
) -> crate::normalized_scene::ChildLayerComposite<'a> {
crate::normalized_scene::ChildLayerComposite {
z_index,
layer,
logical_rect: Rect {
x: 0.0,
y: 0.0,
width: rect.width,
height: rect.height,
},
dest_quad: rect_to_quad(rect),
snap_anchor: None,
backdrop_rect: rect,
visual_clip: None,
surface_clip: None,
shadow_draws: Vec::new(),
needs_nested_underlay,
}
}
#[test]
fn root_direct_preflight_allows_first_translated_child_underlay() {
let child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 400.0,
height: 280.0,
},
vec![],
);
let collected = CollectedLayer {
scene: CompositorScene::new(),
child_layers: vec![child_layer_composite(
&child,
3,
Rect {
x: 48.0,
y: 96.0,
width: 400.0,
height: 280.0,
},
true,
)],
};
assert!(direct_root_child_underlays_are_supported(&collected));
}
#[test]
fn root_direct_preflight_allows_axis_aligned_prior_child_underlay() {
let first = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 80.0,
height: 40.0,
},
vec![],
);
let backdrop_child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 400.0,
height: 280.0,
},
vec![],
);
let collected = CollectedLayer {
scene: CompositorScene::new(),
child_layers: vec![
child_layer_composite(
&first,
1,
Rect {
x: 8.0,
y: 16.0,
width: 80.0,
height: 40.0,
},
false,
),
child_layer_composite(
&backdrop_child,
4,
Rect {
x: 48.0,
y: 96.0,
width: 400.0,
height: 280.0,
},
true,
),
],
};
assert!(direct_root_child_underlays_are_supported(&collected));
}
#[test]
fn root_direct_preflight_rejects_effectful_prior_child_underlay() {
let mut first = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 80.0,
height: 40.0,
},
vec![],
);
first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
let backdrop_child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 400.0,
height: 280.0,
},
vec![],
);
let collected = CollectedLayer {
scene: CompositorScene::new(),
child_layers: vec![
child_layer_composite(
&first,
1,
Rect {
x: 64.0,
y: 112.0,
width: 80.0,
height: 40.0,
},
false,
),
child_layer_composite(
&backdrop_child,
4,
Rect {
x: 48.0,
y: 96.0,
width: 400.0,
height: 280.0,
},
true,
),
],
};
assert!(!direct_root_child_underlays_are_supported(&collected));
}
#[test]
fn root_direct_preflight_ignores_non_overlapping_effectful_prior_child_underlay() {
let mut first = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 80.0,
height: 40.0,
},
vec![],
);
first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
let backdrop_child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 400.0,
height: 280.0,
},
vec![],
);
let collected = CollectedLayer {
scene: CompositorScene::new(),
child_layers: vec![
child_layer_composite(
&first,
1,
Rect {
x: 8.0,
y: 16.0,
width: 80.0,
height: 40.0,
},
false,
),
child_layer_composite(
&backdrop_child,
4,
Rect {
x: 48.0,
y: 96.0,
width: 400.0,
height: 280.0,
},
true,
),
],
};
assert!(direct_root_child_underlays_are_supported(&collected));
}
#[test]
fn root_direct_preflight_rejects_underlay_that_would_replay_prior_scene_effects() {
let backdrop_child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 400.0,
height: 280.0,
},
vec![],
);
let mut scene = CompositorScene::new();
scene.next_z = 1;
scene.push_effect_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 120.0,
},
None,
Some(RenderEffect::blur(2.0)),
BlendMode::SrcOver,
1.0,
0,
1,
);
let collected = CollectedLayer {
scene,
child_layers: vec![child_layer_composite(
&backdrop_child,
4,
Rect {
x: 48.0,
y: 96.0,
width: 400.0,
height: 280.0,
},
true,
)],
};
assert!(!direct_root_child_underlays_are_supported(&collected));
}
#[test]
fn root_direct_eligibility_does_not_reject_descendant_backdrop() {
let mut backdrop = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 40.0,
height: 40.0,
},
vec![],
);
backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
let child = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 120.0,
height: 96.0,
},
vec![RenderNode::Layer(Box::new(backdrop))],
);
let root = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 240.0,
height: 160.0,
},
vec![RenderNode::Layer(Box::new(child))],
);
let mut cache = HashMap::new();
assert!(root_can_render_directly_cached(&root, &mut cache));
}
#[test]
fn root_direct_scene_events_allow_root_local_effects() {
let mut scene = CompositorScene::new();
scene.effect_layers.push(EffectLayer {
rect: Rect {
x: 20.0,
y: 30.0,
width: 120.0,
height: 80.0,
},
clip: None,
snap_anchor: None,
effect: Some(RenderEffect::blur(6.0)),
blend_mode: BlendMode::SrcOver,
composite_alpha: 1.0,
z_start: 0,
z_end: 1,
requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
});
assert!(root_direct_scene_events_are_supported(&scene));
}
#[test]
fn root_direct_scene_events_reject_root_local_backdrops() {
let mut scene = CompositorScene::new();
scene.backdrop_layers.push(BackdropLayer {
node_id: Some(99),
rect: Rect {
x: 20.0,
y: 30.0,
width: 120.0,
height: 80.0,
},
clip: None,
effect: RenderEffect::blur(6.0),
z_index: 1,
});
assert!(!root_direct_scene_events_are_supported(&scene));
}
#[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: -64.0,
width: 296.0,
height: 164.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: -64.0,
width: 296.0,
height: 164.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_translated_content_offset() {
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 mut base = cacheable_layer(
42,
Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
},
vec![RenderNode::Primitive(primitive)],
);
base.translated_content_context = true;
base.translated_content_offset = Point::new(0.0, -8.0);
base.recompute_raster_cache_hashes();
let mut moved = base.clone();
moved.translated_content_offset = Point::new(0.0, -16.0);
moved.recompute_raster_cache_hashes();
assert_ne!(
layer_raster_cache_candidate(&base, 1.25, false, false),
layer_raster_cache_candidate(&moved, 1.25, false, false),
"full-surface layer cache candidates must not alias different scroll offsets"
);
}
#[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_raster_cache_candidate_allows_stable_runtime_child_effect_surfaces() {
let mut 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::WHITE),
},
clip: None,
}),
})],
);
layer.node_id = Some(78);
layer.graphics_layer.render_effect = Some(RenderEffect::blur(4.0));
layer.recompute_raster_cache_hashes();
assert!(
layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
"root direct path should not force-cache ordinary stable effects"
);
assert!(
layer_raster_cache_candidate(&layer, 1.0, false, true).is_some(),
"child surface rendering should retain stable non-runtime effects"
);
}
#[test]
fn layer_raster_cache_candidate_rejects_runtime_shader_child_effect_surfaces() {
let mut layer = test_layer(
Rect {
x: 0.0,
y: 0.0,
width: 64.0,
height: 32.0,
},
vec![],
);
layer.node_id = Some(79);
layer.graphics_layer.render_effect = Some(RenderEffect::runtime_shader(
RuntimeShader::new("runtime shader"),
));
layer.recompute_raster_cache_hashes();
assert!(
layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
"runtime shaders must not fill the retained layer cache with per-frame uniform variants"
);
}
#[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_surface() {
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(), 1);
assert!(collected.scene.texts.is_empty());
assert!(collected.scene.effect_layers.is_empty());
assert_snap_anchor_close(
collected.child_layers[0].snap_anchor,
Point::new(11.4, 23.6),
"translated plain text's bounded local surface should composite at the content-origin 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(), 1);
assert!(collected.scene.shapes.is_empty());
assert!(collected.scene.images.is_empty());
assert!(collected.scene.texts.is_empty());
assert!(collected.scene.effect_layers.is_empty());
let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
assert_eq!(
collected.child_layers[0].snap_anchor, expected_anchor,
"active translated leaf surface should keep the content-origin snap phase"
);
}
#[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, 16.5))),
"animated translated content should composite the stable local surface at the viewport-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);
assert_eq!(collected.child_layers.len(), 1);
assert!(collected.scene.texts.is_empty());
assert!(collected.scene.effect_layers.is_empty());
assert_snap_anchor_close(
collected.child_layers[0].snap_anchor,
Point::new(11.4, 23.6),
"animated translated pure text should use the bounded content snap phase",
);
}
#[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_snap_anchor_close(
collected.scene.texts[0].snap_anchor,
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 normalized_scene_keeps_lazy_after_bound_text_for_prewarm() {
use std::cell::RefCell;
fn collect_graph_text_labels(layer: &LayerNode, labels: &mut Vec<String>) {
for child in &layer.children {
match child {
RenderNode::Primitive(PrimitiveEntry {
node: PrimitiveNode::Text(text),
..
}) => labels.push(text.text.text.clone()),
RenderNode::Layer(child_layer) => {
collect_graph_text_labels(child_layer, labels)
}
RenderNode::Primitive(_) => {}
}
}
}
let state_holder: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
let state_holder_for_comp = state_holder.clone();
let mut composition = cranpose_ui::run_test_composition(move || {
let list_state = remember_lazy_list_state();
*state_holder_for_comp.borrow_mut() = Some(list_state);
let mut spec = LazyColumnSpec::new()
.vertical_arrangement(cranpose_ui::LinearArrangement::SpacedBy(6.0));
spec.beyond_bounds_item_count = 0;
LazyColumn(Modifier::empty().height(96.0), list_state, spec, |scope| {
scope.items(
12,
None::<fn(usize) -> u64>,
None::<fn(usize) -> u64>,
|index| {
Text(
format!("WarmRow {index}"),
Modifier::empty().height(32.0),
TextStyle::default(),
);
},
);
});
});
let list_state = (*state_holder.borrow()).expect("lazy list state should be captured");
list_state.scroll_to_item(4, 0.0);
let root = composition.root().expect("lazy column root");
let handle = composition.runtime_handle();
let mut applier = composition.applier_mut();
applier.set_runtime_handle(handle);
let _ = applier
.compute_layout(
root,
Size {
width: 240.0,
height: 240.0,
},
)
.expect("lazy column layout");
let graph = build_graph_from_applier(&mut applier, root, 1.0).expect("lazy column graph");
applier.clear_runtime_handle();
let mut graph_labels = Vec::new();
collect_graph_text_labels(&graph.root, &mut graph_labels);
let visible_indices: Vec<_> = list_state
.layout_info()
.visible_items_info
.iter()
.map(|item| item.index)
.collect();
assert_eq!(
visible_indices,
vec![4, 5, 6],
"test setup expects exactly three viewport-visible rows"
);
let mut rect_cache = HashMap::new();
let mut requirements_cache = HashMap::new();
let collected = with_test_app_context(|| {
collect_layer_contents(
&graph.root,
None,
None,
&mut rect_cache,
&mut requirements_cache,
)
});
let root_text_labels: Vec<_> = collected
.scene
.texts
.iter()
.map(|text| text.text.text.clone())
.collect();
let child_layer_count = collected.child_layers.len();
let warm_text = collected
.scene
.texts
.iter()
.find(|text| text.text.text == "WarmRow 7")
.unwrap_or_else(|| {
panic!(
"after-bound lazy text should reach WGPU scene collection; graph_texts={graph_labels:?} root_texts={root_text_labels:?} child_layers={child_layer_count}"
)
});
assert!(
warm_text.rect.y >= 96.0,
"after-bound text should be below the viewport, got {:?}",
warm_text.rect
);
assert_eq!(
visible_draw_rect(warm_text.rect, warm_text.clip),
None,
"after-bound text should remain clipped away for drawing while staying available for glyph prewarm"
);
assert!(
text_draw_should_prewarm_in_viewport(
warm_text.rect,
warm_text.clip,
ViewportUniformParams {
width: 240,
height: 96,
offset: [0.0, 0.0],
},
1.0,
),
"after-bound text inside the warm window must be selected by WGPU prewarm"
);
}
#[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,
&[],
100,
100,
1.0,
&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,
100,
100,
1.0,
&mut scratch,
);
let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
assert_eq!(
items,
vec![SegmentDrawItem::Shape(0), SegmentDrawItem::Text(0)]
);
}
#[test]
fn collect_non_effect_segment_items_culls_offscreen_shapes_but_keeps_text_prewarm() {
let mut shape = test_shape(0, BlendMode::SrcOver);
shape.rect.y = 160.0;
shape.local_rect.y = 160.0;
shape.quad = [[0.0, 160.0], [8.0, 160.0], [0.0, 168.0], [8.0, 168.0]];
let shapes = vec![shape];
let images = Vec::new();
let mut text = test_text(1);
text.rect.y = 160.0;
let texts = vec![text];
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,
2,
&[],
100,
100,
1.0,
&mut scratch,
);
let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
assert_eq!(items, vec![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_layer_composites_in_ordered_draw_chunk() {
let ordered_items = vec![
(0, SegmentDrawItem::Shape(0)),
(1, SegmentDrawItem::Composite(0)),
(2, SegmentDrawItem::Image(0)),
(3, SegmentDrawItem::Composite(1)),
(4, 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,
},
SegmentBatchPlan::Composite { start: 1, end: 2 },
SegmentBatchPlan::Image {
start: 2,
end: 3,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Composite { start: 3, end: 4 },
SegmentBatchPlan::Text { start: 4, end: 5 },
]))]
);
}
#[test]
fn retain_renderable_shadow_items_culls_invisible_shadow_boundaries() {
let shapes = vec![test_shape(0, BlendMode::SrcOver)];
let images = vec![test_image(2, BlendMode::SrcOver)];
let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
shadow_shape.rect = Rect {
x: 500.0,
y: 500.0,
width: 12.0,
height: 12.0,
};
let shadow_draws = vec![ShadowDraw {
shapes: vec![(shadow_shape, BlendMode::SrcOver)],
texts: Vec::new(),
blur_radius: 8.0,
clip: None,
z_index: 1,
}];
let mut ordered_items = vec![
(0, SegmentDrawItem::Shape(0)),
(1, SegmentDrawItem::Shadow(0)),
(2, SegmentDrawItem::Image(0)),
];
let culled =
retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
let commands: Vec<_> = SegmentCommandIter::new(&ordered_items, &shapes, &images).collect();
assert_eq!(culled, 1);
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,
},
]))]
);
}
#[test]
fn retain_renderable_shadow_items_keeps_visible_shadow_boundaries() {
let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
shadow_shape.rect = Rect {
x: 20.0,
y: 20.0,
width: 12.0,
height: 12.0,
};
let shadow_draws = vec![ShadowDraw {
shapes: vec![(shadow_shape, BlendMode::SrcOver)],
texts: Vec::new(),
blur_radius: 8.0,
clip: None,
z_index: 1,
}];
let mut ordered_items = vec![(1, SegmentDrawItem::Shadow(0))];
let culled =
retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
assert_eq!(culled, 0);
assert_eq!(ordered_items, vec![(1, SegmentDrawItem::Shadow(0))]);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_shape_shader_source_uses_native_batch_limits() {
let source = shape_shader_source();
assert!(source.contains(&format!("array<ShapeData, {MAX_SHAPES_PER_BATCH}>")));
assert!(source.contains(&format!("array<GradientStop, {MAX_GRADIENT_STOPS}>")));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_segment_fusion_budget_allows_small_interleaved_chunks() {
let ordered_items = vec![
(0, SegmentDrawItem::Shape(0)),
(1, SegmentDrawItem::Image(0)),
(2, SegmentDrawItem::Text(0)),
(3, SegmentDrawItem::Shape(1)),
];
let shapes = vec![
test_shape(0, BlendMode::SrcOver),
test_shape(3, BlendMode::DstOut),
];
let segment = chunk(&[
SegmentBatchPlan::Shape {
start: 0,
end: 1,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Image {
start: 1,
end: 2,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Text { start: 2, end: 3 },
SegmentBatchPlan::Shape {
start: 3,
end: 4,
blend_mode: BlendMode::DstOut,
},
]);
let budget = native_segment_fusion_budget(&ordered_items, &shapes, &segment)
.expect("budget should be valid")
.expect("chunk should fit native fusion budget");
assert_eq!(
budget,
NativeSegmentFusionBudget {
shape_count: 2,
gradient_stop_count: 0,
}
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_segment_fusion_budget_rejects_shape_uniform_overflow() {
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 segment = 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,
},
]);
let budget =
native_segment_fusion_budget(&ordered_items, &shapes, &segment).expect("valid plan");
assert_eq!(budget, None);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_segment_fusion_budget_rejects_gradient_uniform_overflow() {
let ordered_items = vec![(0, SegmentDrawItem::Shape(0))];
let mut shape = test_shape(0, BlendMode::SrcOver);
shape.brush = Brush::linear_gradient(vec![Color::BLACK; MAX_GRADIENT_STOPS + 1]);
let shapes = vec![shape];
let segment = chunk(&[SegmentBatchPlan::Shape {
start: 0,
end: 1,
blend_mode: BlendMode::SrcOver,
}]);
let budget =
native_segment_fusion_budget(&ordered_items, &shapes, &segment).expect("valid plan");
assert_eq!(budget, None);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_segment_fusion_partitions_shape_uniform_overflow() {
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 segment = 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,
},
]);
let partitions = native_segment_fusion_partitions(&ordered_items, &shapes, &segment)
.expect("valid plan")
.expect("overflowing segment should be partitionable");
assert_eq!(partitions.len(), 2);
assert_eq!(
partitions[0],
NativeSegmentFusionPartition {
chunk: chunk(&[SegmentBatchPlan::Shape {
start: 0,
end: MAX_SHAPES_PER_BATCH,
blend_mode: BlendMode::SrcOver,
}]),
budget: NativeSegmentFusionBudget {
shape_count: MAX_SHAPES_PER_BATCH,
gradient_stop_count: 0,
},
}
);
assert_eq!(
partitions[1],
NativeSegmentFusionPartition {
chunk: chunk(&[SegmentBatchPlan::Shape {
start: MAX_SHAPES_PER_BATCH,
end: MAX_SHAPES_PER_BATCH + 1,
blend_mode: BlendMode::SrcOver,
}]),
budget: NativeSegmentFusionBudget {
shape_count: 1,
gradient_stop_count: 0,
},
}
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_segment_fusion_partitions_gradient_uniform_overflow() {
const STOPS_PER_SHAPE: usize = MAX_GRADIENT_STOPS / 2;
let ordered_items = vec![
(0, SegmentDrawItem::Shape(0)),
(1, SegmentDrawItem::Shape(1)),
(2, SegmentDrawItem::Shape(2)),
];
let mut shapes = Vec::new();
for index in 0..3 {
let mut shape = test_shape(index, BlendMode::SrcOver);
shape.brush = Brush::linear_gradient(vec![Color::BLACK; STOPS_PER_SHAPE]);
shapes.push(shape);
}
let segment = chunk(&[SegmentBatchPlan::Shape {
start: 0,
end: 3,
blend_mode: BlendMode::SrcOver,
}]);
let partitions = native_segment_fusion_partitions(&ordered_items, &shapes, &segment)
.expect("valid plan")
.expect("overflowing gradient segment should be partitionable");
assert_eq!(partitions.len(), 2);
assert_eq!(
partitions[0],
NativeSegmentFusionPartition {
chunk: chunk(&[SegmentBatchPlan::Shape {
start: 0,
end: 2,
blend_mode: BlendMode::SrcOver,
}]),
budget: NativeSegmentFusionBudget {
shape_count: 2,
gradient_stop_count: MAX_GRADIENT_STOPS,
},
}
);
assert_eq!(
partitions[1],
NativeSegmentFusionPartition {
chunk: chunk(&[SegmentBatchPlan::Shape {
start: 2,
end: 3,
blend_mode: BlendMode::SrcOver,
}]),
budget: NativeSegmentFusionBudget {
shape_count: 1,
gradient_stop_count: STOPS_PER_SHAPE,
},
}
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_segment_fusion_accepts_layer_composite_chunks() {
let ordered_items = vec![
(0, SegmentDrawItem::Shape(0)),
(1, SegmentDrawItem::Composite(0)),
(2, SegmentDrawItem::ShaderComposite(0)),
(3, SegmentDrawItem::Shape(1)),
];
let shapes = vec![
test_shape(0, BlendMode::SrcOver),
test_shape(1, BlendMode::SrcOver),
];
let segment = chunk(&[
SegmentBatchPlan::Shape {
start: 0,
end: 1,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Composite { start: 1, end: 2 },
SegmentBatchPlan::ShaderComposite { start: 2, end: 3 },
SegmentBatchPlan::Shape {
start: 3,
end: 4,
blend_mode: BlendMode::SrcOver,
},
]);
let partitions = native_segment_fusion_partitions(&ordered_items, &shapes, &segment)
.expect("valid plan")
.expect("composites are drawable inside the native fused pass");
assert_eq!(
partitions,
vec![NativeSegmentFusionPartition {
chunk: segment,
budget: NativeSegmentFusionBudget {
shape_count: 2,
gradient_stop_count: 0,
},
}],
"layer composites and shader composites must preserve order without forcing separate render passes"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_segment_fusion_partitions_preserve_non_shape_order_at_budget_boundary() {
let ordered_items: Vec<_> = (0..MAX_SHAPES_PER_BATCH)
.map(|index| (index, SegmentDrawItem::Shape(index)))
.chain([
(MAX_SHAPES_PER_BATCH, SegmentDrawItem::Image(0)),
(
MAX_SHAPES_PER_BATCH + 1,
SegmentDrawItem::Shape(MAX_SHAPES_PER_BATCH),
),
])
.collect();
let shapes: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
.map(|index| test_shape(index, BlendMode::SrcOver))
.collect();
let segment = chunk(&[
SegmentBatchPlan::Shape {
start: 0,
end: MAX_SHAPES_PER_BATCH,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Image {
start: MAX_SHAPES_PER_BATCH,
end: MAX_SHAPES_PER_BATCH + 1,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Shape {
start: MAX_SHAPES_PER_BATCH + 1,
end: MAX_SHAPES_PER_BATCH + 2,
blend_mode: BlendMode::SrcOver,
},
]);
let partitions = native_segment_fusion_partitions(&ordered_items, &shapes, &segment)
.expect("valid plan")
.expect("overflowing segment should be partitionable");
assert_eq!(partitions.len(), 2);
assert_eq!(
partitions[0].chunk,
chunk(&[
SegmentBatchPlan::Shape {
start: 0,
end: MAX_SHAPES_PER_BATCH,
blend_mode: BlendMode::SrcOver,
},
SegmentBatchPlan::Image {
start: MAX_SHAPES_PER_BATCH,
end: MAX_SHAPES_PER_BATCH + 1,
blend_mode: BlendMode::SrcOver,
},
])
);
assert_eq!(
partitions[1].chunk,
chunk(&[SegmentBatchPlan::Shape {
start: MAX_SHAPES_PER_BATCH + 1,
end: MAX_SHAPES_PER_BATCH + 2,
blend_mode: BlendMode::SrcOver,
}])
);
}
#[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,
target_offset: 0,
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 staged_buffer_uploads_record_destination_offsets() {
let mut uploads = StagedBufferUploads::default();
uploads.stage_at(UploadTarget::ImageIndex, 256, &[1, 2, 3, 4]);
assert_eq!(uploads.copies[0].target_offset, 256);
assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
}
#[test]
fn staged_buffer_uploads_truncate_restores_previous_state() {
let mut uploads = StagedBufferUploads::default();
uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
let bytes_len = uploads.bytes.len();
let copies_len = uploads.copies.len();
uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
uploads.truncate(bytes_len, copies_len);
assert_eq!(uploads.bytes, vec![1, 2, 3, 4]);
assert_eq!(uploads.copies.len(), 1);
}
#[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
);
}
}