use crate::pipeline::CompositorTexturePlan;
use anyhow::Result;
use bytemuck::{Pod, Zeroable};
use fission_layout::LayoutPoint;
use fission_render::LayerClip;
use fission_render::Renderer as _;
use fission_render_vello::{RetainedSceneCache, VelloRenderer, VelloTextMeasurer};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use vello::wgpu;
use vello::{RenderParams, Renderer as VelloSceneRenderer, Scene};
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Pod, Zeroable)]
struct LayerUniform {
rect: [f32; 4],
clip: [f32; 4],
clip_local: [f32; 4],
clip_shape: [f32; 4],
viewport_and_opacity: [f32; 4],
transform: [[f32; 4]; 4],
}
struct CachedLayerTexture {
texture: wgpu::Texture,
view: wgpu::TextureView,
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
width: u32,
height: u32,
content_key: u64,
base: Option<CachedBaseTexture>,
last_uniform: Option<LayerUniform>,
last_used_frame: u64,
}
struct CachedBaseTexture {
texture: wgpu::Texture,
view: wgpu::TextureView,
width: u32,
height: u32,
scene_cache_key: Option<u64>,
}
struct DrawBatch {
layer_key: u64,
clip: (u32, u32, u32, u32),
}
#[derive(Clone)]
struct RetainedCompositorLayer {
has_texture: bool,
bounds: fission_layout::LayoutRect,
clip: Option<LayerClip>,
opacity: f32,
transform: Option<[f32; 16]>,
transform_clip: bool,
scene_cache_key: Option<u64>,
content_key: u64,
local_dynamic: bool,
composite_dynamic: bool,
children: Vec<u64>,
local_dirty: bool,
structure_dirty: bool,
composite_dirty: bool,
last_draw_rect: Option<fission_layout::LayoutRect>,
last_used_frame: u64,
}
impl RetainedCompositorLayer {
fn new(plan: &CompositorTexturePlan, frame: u64) -> Self {
Self {
has_texture: plan.scene.is_some(),
bounds: plan.bounds,
clip: plan.clip.clone(),
opacity: plan.opacity,
transform: plan.transform,
transform_clip: plan.transform_clip,
scene_cache_key: plan.scene_cache_key,
content_key: plan.content_key,
local_dynamic: plan.local_dynamic,
composite_dynamic: plan.composite_dynamic,
children: plan.children.iter().map(|child| child.key).collect(),
local_dirty: true,
structure_dirty: true,
composite_dirty: true,
last_draw_rect: None,
last_used_frame: frame,
}
}
}
#[derive(Default, Clone, Copy)]
pub struct CompositorFrameStats {
pub damage_rect: Option<fission_layout::LayoutRect>,
pub rendered_layers: usize,
pub partial_recomposites: usize,
pub full_recomposites: usize,
pub reused_base_textures: usize,
pub evicted_layers: usize,
pub resident_texture_bytes: usize,
}
#[derive(Clone, Copy)]
struct RenderOutcome {
changed: bool,
damage_rect: Option<fission_layout::LayoutRect>,
}
#[derive(Clone, Copy)]
struct TargetContext {
origin: LayoutPoint,
inherited_transform: Option<[f32; 16]>,
inherited_scissor: Option<(u32, u32, u32, u32)>,
viewport_width: u32,
viewport_height: u32,
}
pub struct TextureLayerCompositor {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
textures: HashMap<u64, CachedLayerTexture>,
layers: HashMap<u64, RetainedCompositorLayer>,
root_layers: Vec<u64>,
frame_index: u64,
texture_budget_bytes: usize,
last_target_size: Option<(u32, u32)>,
}
impl TextureLayerCompositor {
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("fission-compositor shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("compositor.wgsl").into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("fission-compositor 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,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("fission-compositor pipeline layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("fission-compositor pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
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: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("fission-compositor sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
Self {
pipeline,
bind_group_layout,
sampler,
textures: HashMap::new(),
layers: HashMap::new(),
root_layers: Vec::new(),
frame_index: 0,
texture_budget_bytes: compositor_cache_budget_bytes(),
last_target_size: None,
}
}
pub fn prune(
&mut self,
live_keys: &HashSet<u64>,
) -> (Option<fission_layout::LayoutRect>, bool) {
let mut removed_damage = None;
let mut removed_any = false;
self.textures.retain(|key, _| live_keys.contains(key));
self.layers.retain(|key, layer| {
if live_keys.contains(key) {
true
} else {
removed_any = true;
removed_damage = union_layout_rects(removed_damage, layer.last_draw_rect);
false
}
});
self.root_layers.retain(|key| live_keys.contains(key));
(removed_damage, removed_any)
}
pub fn render_layers(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
vello_renderer: &mut VelloSceneRenderer,
retained_scene_cache: &mut RetainedSceneCache,
measurer: Arc<VelloTextMeasurer>,
scale_factor: f64,
viewport_width: u32,
viewport_height: u32,
root_transform: Option<[f32; 16]>,
plans: &[CompositorTexturePlan],
force_full_redraw: bool,
clear_color: wgpu::Color,
target_view: &wgpu::TextureView,
) -> Result<CompositorFrameStats> {
self.frame_index = self.frame_index.saturating_add(1);
let live_keys = self.sync_plans(plans);
let (pruned_damage, removed_layers) = self.prune(&live_keys);
let mut stats = CompositorFrameStats::default();
let root_ctx = TargetContext {
origin: LayoutPoint::ZERO,
inherited_transform: root_transform,
inherited_scissor: None,
viewport_width,
viewport_height,
};
for plan in plans {
let outcome = self.render_plan_layer(
device,
queue,
vello_renderer,
retained_scene_cache,
Arc::clone(&measurer),
scale_factor,
plan,
root_ctx,
&mut stats,
)?;
stats.damage_rect = union_layout_rects(stats.damage_rect, outcome.damage_rect);
}
stats.damage_rect = union_layout_rects(stats.damage_rect, pruned_damage);
let target_size_changed = self.last_target_size != Some((viewport_width, viewport_height));
let full_target_redraw = force_full_redraw
|| target_size_changed
|| removed_layers
|| stats.damage_rect.is_none();
let damage_scissor = if full_target_redraw {
None
} else {
stats.damage_rect.map(|rect| {
logical_rect_to_physical_scissor(
rect,
scale_factor,
viewport_width,
viewport_height,
)
})
};
if full_target_redraw || damage_scissor.is_some() {
let root_layers = self.root_layers.clone();
let draw_batches = self.build_layer_draw_batches(
queue,
device,
scale_factor,
viewport_width,
viewport_height,
LayoutPoint::ZERO,
root_transform,
1.0,
None,
&root_layers,
damage_scissor,
);
self.draw_batches_to_view(
device,
queue,
target_view,
draw_batches,
if full_target_redraw {
wgpu::LoadOp::Clear(clear_color)
} else {
wgpu::LoadOp::Load
},
);
}
self.last_target_size = Some((viewport_width, viewport_height));
stats.evicted_layers = self.enforce_texture_budget();
stats.resident_texture_bytes = self.resident_texture_bytes();
Ok(stats)
}
fn sync_plans(&mut self, plans: &[CompositorTexturePlan]) -> HashSet<u64> {
let mut live = HashSet::new();
self.root_layers.clear();
for plan in plans {
self.sync_plan(plan, &mut live);
self.root_layers.push(plan.key);
}
live
}
fn sync_plan(&mut self, plan: &CompositorTexturePlan, live: &mut HashSet<u64>) {
live.insert(plan.key);
for child in &plan.children {
self.sync_plan(child, live);
}
let next_children: Vec<u64> = plan.children.iter().map(|child| child.key).collect();
match self.layers.get_mut(&plan.key) {
Some(layer) => {
layer.last_used_frame = self.frame_index;
let had_texture = layer.has_texture;
let texture_changed = had_texture != plan.scene.is_some();
let size_changed = layer.bounds.size != plan.bounds.size;
let scene_changed = layer.scene_cache_key != plan.scene_cache_key;
let structure_changed = layer.children != next_children;
let style_changed = layer.clip != plan.clip
|| (layer.opacity - plan.opacity).abs() > 0.001
|| layer.transform != plan.transform
|| layer.transform_clip != plan.transform_clip;
layer.has_texture = plan.scene.is_some();
layer.bounds = plan.bounds;
layer.clip = plan.clip.clone();
layer.opacity = plan.opacity;
layer.transform = plan.transform;
layer.transform_clip = plan.transform_clip;
layer.scene_cache_key = plan.scene_cache_key;
layer.content_key = plan.content_key;
layer.local_dynamic = plan.local_dynamic;
layer.composite_dynamic = plan.composite_dynamic;
layer.children = next_children;
layer.local_dirty |=
texture_changed || size_changed || scene_changed || plan.local_dynamic;
layer.structure_dirty |= structure_changed;
layer.composite_dirty |= style_changed || structure_changed;
}
None => {
self.layers.insert(
plan.key,
RetainedCompositorLayer::new(plan, self.frame_index),
);
}
}
}
fn render_plan_layer(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
vello_renderer: &mut VelloSceneRenderer,
retained_scene_cache: &mut RetainedSceneCache,
measurer: Arc<VelloTextMeasurer>,
scale_factor: f64,
plan: &CompositorTexturePlan,
target_ctx: TargetContext,
stats: &mut CompositorFrameStats,
) -> Result<RenderOutcome> {
let state = self
.layers
.get(&plan.key)
.cloned()
.expect("missing retained compositor layer state");
let localized = localize_world_transform(plan.transform, target_ctx.origin);
let combined = combine_transforms(target_ctx.inherited_transform, localized);
let scoped_scissor = intersect_scissor(
target_ctx.inherited_scissor,
plan.clip.as_ref(),
if state.transform_clip {
combined
} else {
target_ctx.inherited_transform
},
target_ctx.origin,
scale_factor,
target_ctx.viewport_width,
target_ctx.viewport_height,
);
let current_draw_rect = logical_draw_rect_for_plan(
plan,
target_ctx.origin,
if state.transform_clip {
combined
} else {
target_ctx.inherited_transform
},
scale_factor,
target_ctx.viewport_width,
target_ctx.viewport_height,
);
if plan.scene.is_none() {
let child_ctx = TargetContext {
origin: target_ctx.origin,
inherited_transform: combined,
inherited_scissor: Some(scoped_scissor),
viewport_width: target_ctx.viewport_width,
viewport_height: target_ctx.viewport_height,
};
let mut child_damage = None;
let mut child_changed = false;
for child in &plan.children {
let outcome = self.render_plan_layer(
device,
queue,
vello_renderer,
retained_scene_cache,
Arc::clone(&measurer),
scale_factor,
child,
child_ctx,
stats,
)?;
child_changed |= outcome.changed;
child_damage = union_layout_rects(child_damage, outcome.damage_rect);
}
let wrapper_damage =
if state.composite_dirty || state.structure_dirty || state.composite_dynamic {
union_layout_rects(state.last_draw_rect, current_draw_rect)
} else {
child_damage
};
if let Some(layer) = self.layers.get_mut(&plan.key) {
layer.last_draw_rect = current_draw_rect;
layer.local_dirty = false;
layer.structure_dirty = false;
layer.composite_dirty = false;
}
return Ok(RenderOutcome {
changed: child_changed || wrapper_damage.is_some(),
damage_rect: wrapper_damage,
});
}
let texture_width = logical_to_physical(plan.bounds.size.width, scale_factor);
let texture_height = logical_to_physical(plan.bounds.size.height, scale_factor);
let local_ctx = TargetContext {
origin: plan.bounds.origin,
inherited_transform: None,
inherited_scissor: None,
viewport_width: texture_width,
viewport_height: texture_height,
};
let mut child_damage_local = None;
let mut child_changed = false;
for child in &plan.children {
let outcome = self.render_plan_layer(
device,
queue,
vello_renderer,
retained_scene_cache,
Arc::clone(&measurer),
scale_factor,
child,
local_ctx,
stats,
)?;
child_changed |= outcome.changed;
child_damage_local = union_layout_rects(child_damage_local, outcome.damage_rect);
}
let mut created = false;
{
let cached = self.textures.entry(plan.key).or_insert_with(|| {
created = true;
create_cached_texture(
device,
&self.bind_group_layout,
&self.sampler,
texture_width,
texture_height,
)
});
if cached.width != texture_width || cached.height != texture_height {
*cached = create_cached_texture(
device,
&self.bind_group_layout,
&self.sampler,
texture_width,
texture_height,
);
created = true;
}
cached.last_used_frame = self.frame_index;
}
let full_recompose = created
|| state.local_dirty
|| state.structure_dirty
|| state.local_dynamic
|| child_damage_local.is_none() && child_changed;
let can_partial_recompose = !full_recompose && child_damage_local.is_some();
let needs_recompose = full_recompose || can_partial_recompose;
if needs_recompose {
stats.rendered_layers += 1;
if full_recompose {
render_or_seed_layer_base(
self.textures
.get_mut(&plan.key)
.expect("missing cached compositor layer"),
device,
queue,
vello_renderer,
retained_scene_cache,
Arc::clone(&measurer),
scale_factor,
plan.scene.as_ref(),
plan.scene_cache_key.filter(|_| !plan.local_dynamic),
texture_width,
texture_height,
None,
)?;
stats.full_recomposites += 1;
} else if let Some(dirty_rect) = child_damage_local {
let dirty_scissor = logical_rect_to_physical_scissor(
dirty_rect,
scale_factor,
texture_width,
texture_height,
);
render_or_seed_layer_base(
self.textures
.get_mut(&plan.key)
.expect("missing cached compositor layer"),
device,
queue,
vello_renderer,
retained_scene_cache,
Arc::clone(&measurer),
scale_factor,
plan.scene.as_ref(),
plan.scene_cache_key.filter(|_| !plan.local_dynamic),
texture_width,
texture_height,
Some(dirty_scissor),
)?;
stats.partial_recomposites += 1;
stats.reused_base_textures += 1;
}
if !plan.children.is_empty() {
let local_clip = if can_partial_recompose {
child_damage_local.map(|rect| {
logical_rect_to_physical_scissor(
rect,
scale_factor,
texture_width,
texture_height,
)
})
} else {
None
};
let draw_batches = self.build_layer_draw_batches(
queue,
device,
scale_factor,
texture_width,
texture_height,
plan.bounds.origin,
None,
1.0,
local_clip,
&state.children,
None,
);
let target_view = &self
.textures
.get(&plan.key)
.expect("missing cached compositor layer")
.view;
self.draw_batches_to_view(
device,
queue,
target_view,
draw_batches,
wgpu::LoadOp::Load,
);
}
if let Some(cached) = self.textures.get_mut(&plan.key) {
cached.content_key = plan.content_key;
}
}
let parent_damage = if needs_recompose || state.composite_dirty || state.composite_dynamic {
union_layout_rects(state.last_draw_rect, current_draw_rect)
} else {
None
};
if let Some(layer) = self.layers.get_mut(&plan.key) {
layer.last_draw_rect = current_draw_rect;
layer.local_dirty = false;
layer.structure_dirty = false;
layer.composite_dirty = false;
layer.last_used_frame = self.frame_index;
}
Ok(RenderOutcome {
changed: needs_recompose || state.composite_dirty || state.composite_dynamic,
damage_rect: parent_damage,
})
}
fn build_layer_draw_batches(
&mut self,
queue: &wgpu::Queue,
device: &wgpu::Device,
scale_factor: f64,
viewport_width: u32,
viewport_height: u32,
target_origin: LayoutPoint,
inherited_transform: Option<[f32; 16]>,
inherited_opacity: f32,
inherited_scissor: Option<(u32, u32, u32, u32)>,
layer_keys: &[u64],
extra_scissor: Option<(u32, u32, u32, u32)>,
) -> Vec<DrawBatch> {
let mut batches = Vec::with_capacity(layer_keys.len());
for key in layer_keys {
let Some(layer) = self.layers.get(key).cloned() else {
continue;
};
if let (Some(extra), Some(last_draw_rect)) = (extra_scissor, layer.last_draw_rect) {
let draw_scissor = logical_rect_to_physical_scissor(
last_draw_rect,
scale_factor,
viewport_width,
viewport_height,
);
if intersect_scissor_optional(Some(draw_scissor), Some(extra)).is_none() {
continue;
}
}
let localized = localize_world_transform(layer.transform, target_origin);
let combined = combine_transforms(inherited_transform, localized);
let clip_transform = if layer.transform_clip {
combined
} else {
inherited_transform
};
let clip = intersect_scissor(
inherited_scissor,
layer.clip.as_ref(),
clip_transform,
target_origin,
scale_factor,
viewport_width,
viewport_height,
);
let Some(clip) = intersect_scissor_optional(Some(clip), extra_scissor) else {
continue;
};
if clip.2 == 0 || clip.3 == 0 || clip.0 >= viewport_width || clip.1 >= viewport_height {
continue;
}
let opacity = inherited_opacity * layer.opacity;
if !layer.has_texture {
batches.extend(self.build_layer_draw_batches(
queue,
device,
scale_factor,
viewport_width,
viewport_height,
target_origin,
combined,
opacity,
Some(clip),
&layer.children,
extra_scissor,
));
continue;
}
let (clip_local, clip_shape) =
local_clip_mask(layer.clip.as_ref(), layer.bounds, scale_factor);
let Some(cached) = self.textures.get_mut(key) else {
continue;
};
let rect = [
((layer.bounds.origin.x - target_origin.x) as f64 * scale_factor) as f32,
((layer.bounds.origin.y - target_origin.y) as f64 * scale_factor) as f32,
cached.width as f32,
cached.height as f32,
];
let uniform = LayerUniform {
rect,
clip: [clip.0 as f32, clip.1 as f32, clip.2 as f32, clip.3 as f32],
clip_local,
clip_shape,
viewport_and_opacity: [viewport_width as f32, viewport_height as f32, opacity, 0.0],
transform: matrix_to_rows(scale_transform(combined, scale_factor)),
};
if cached.last_uniform != Some(uniform) {
queue.write_buffer(&cached.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
cached.last_uniform = Some(uniform);
}
batches.push(DrawBatch {
layer_key: *key,
clip,
});
}
batches
}
fn draw_batches_to_view(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
target_view: &wgpu::TextureView,
draw_batches: Vec<DrawBatch>,
load: wgpu::LoadOp<wgpu::Color>,
) {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("fission-compositor encoder"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("fission-compositor pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.pipeline);
for batch in &draw_batches {
pass.set_scissor_rect(
batch.clip.0,
batch.clip.1,
batch.clip.2.max(1),
batch.clip.3.max(1),
);
let Some(cached) = self.textures.get(&batch.layer_key) else {
continue;
};
pass.set_bind_group(0, &cached.bind_group, &[]);
pass.draw(0..4, 0..1);
}
}
queue.submit(Some(encoder.finish()));
}
fn resident_texture_bytes(&self) -> usize {
self.textures
.values()
.map(|cached| {
let main = texture_bytes(cached.width, cached.height);
let base = cached
.base
.as_ref()
.map(|base| texture_bytes(base.width, base.height))
.unwrap_or(0);
main + base
})
.sum()
}
fn enforce_texture_budget(&mut self) -> usize {
let mut current = self.resident_texture_bytes();
if current <= self.texture_budget_bytes {
return 0;
}
let mut victims: Vec<(u64, u64)> = self
.textures
.iter()
.map(|(key, cached)| (*key, cached.last_used_frame))
.collect();
victims.sort_by_key(|(_, last_used)| *last_used);
let mut evicted = 0;
for (key, _) in victims {
if current <= self.texture_budget_bytes {
break;
}
if let Some(cached) = self.textures.remove(&key) {
current = current.saturating_sub(texture_bytes(cached.width, cached.height));
if let Some(base) = cached.base {
current = current.saturating_sub(texture_bytes(base.width, base.height));
}
evicted += 1;
}
}
evicted
}
}
fn union_layout_rects(
lhs: Option<fission_layout::LayoutRect>,
rhs: Option<fission_layout::LayoutRect>,
) -> Option<fission_layout::LayoutRect> {
match (lhs, rhs) {
(Some(a), Some(b)) => Some(rect_union(a, b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
fn logical_to_physical(value: f32, scale_factor: f64) -> u32 {
((value as f64 * scale_factor).round() as u32).max(1)
}
fn texture_bytes(width: u32, height: u32) -> usize {
width as usize * height as usize * 4
}
fn compositor_cache_budget_bytes() -> usize {
std::env::var("FISSION_COMPOSITOR_CACHE_MB")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(256)
.saturating_mul(1024 * 1024)
}
fn matrix_to_rows(matrix: [f32; 16]) -> [[f32; 4]; 4] {
[
[matrix[0], matrix[1], matrix[2], matrix[3]],
[matrix[4], matrix[5], matrix[6], matrix[7]],
[matrix[8], matrix[9], matrix[10], matrix[11]],
[matrix[12], matrix[13], matrix[14], matrix[15]],
]
}
fn scale_transform(transform: Option<[f32; 16]>, scale_factor: f64) -> [f32; 16] {
let mut matrix = transform.unwrap_or(identity_matrix());
matrix[12] *= scale_factor as f32;
matrix[13] *= scale_factor as f32;
matrix
}
fn localize_world_transform(
transform: Option<[f32; 16]>,
target_origin: LayoutPoint,
) -> Option<[f32; 16]> {
let transform = transform?;
let to_local = translation_matrix(-target_origin.x, -target_origin.y);
let from_local = translation_matrix(target_origin.x, target_origin.y);
Some(multiply_matrix(
to_local,
multiply_matrix(transform, from_local),
))
}
fn combine_transforms(inherited: Option<[f32; 16]>, local: Option<[f32; 16]>) -> Option<[f32; 16]> {
match (inherited, local) {
(Some(lhs), Some(rhs)) => Some(multiply_matrix(lhs, rhs)),
(Some(lhs), None) => Some(lhs),
(None, Some(rhs)) => Some(rhs),
(None, None) => None,
}
}
fn intersect_scissor(
inherited_scissor: Option<(u32, u32, u32, u32)>,
clip: Option<&LayerClip>,
transform: Option<[f32; 16]>,
target_origin: LayoutPoint,
scale_factor: f64,
viewport_width: u32,
viewport_height: u32,
) -> (u32, u32, u32, u32) {
let base = clip
.map(|_| {
clip_rect_to_physical(
clip,
transform,
target_origin,
scale_factor,
viewport_width,
viewport_height,
)
})
.unwrap_or((0, 0, viewport_width.max(1), viewport_height.max(1)));
match inherited_scissor {
Some(parent) => intersect_scissor_rects(parent, base),
None => base,
}
}
fn clip_rect_to_physical(
clip: Option<&LayerClip>,
transform: Option<[f32; 16]>,
target_origin: LayoutPoint,
scale_factor: f64,
viewport_width: u32,
viewport_height: u32,
) -> (u32, u32, u32, u32) {
let Some(rect) = clip_rect(clip) else {
return (0, 0, viewport_width.max(1), viewport_height.max(1));
};
let mut points = [
LayoutPoint::new(
rect.origin.x - target_origin.x,
rect.origin.y - target_origin.y,
),
LayoutPoint::new(
rect.origin.x + rect.size.width - target_origin.x,
rect.origin.y - target_origin.y,
),
LayoutPoint::new(
rect.origin.x - target_origin.x,
rect.origin.y + rect.size.height - target_origin.y,
),
LayoutPoint::new(
rect.origin.x + rect.size.width - target_origin.x,
rect.origin.y + rect.size.height - target_origin.y,
),
];
if let Some(matrix) = transform {
for point in &mut points {
*point = transform_point(matrix, *point);
}
}
let min_x = points
.iter()
.map(|p| p.x)
.fold(f32::INFINITY, f32::min)
.max(0.0);
let min_y = points
.iter()
.map(|p| p.y)
.fold(f32::INFINITY, f32::min)
.max(0.0);
let max_x = points
.iter()
.map(|p| p.x)
.fold(f32::NEG_INFINITY, f32::max)
.min(viewport_width as f32 / scale_factor as f32);
let max_y = points
.iter()
.map(|p| p.y)
.fold(f32::NEG_INFINITY, f32::max)
.min(viewport_height as f32 / scale_factor as f32);
let x = (min_x as f64 * scale_factor).round().max(0.0) as u32;
let y = (min_y as f64 * scale_factor).round().max(0.0) as u32;
let max_x = (max_x as f64 * scale_factor)
.round()
.clamp(0.0, viewport_width as f64) as u32;
let max_y = (max_y as f64 * scale_factor)
.round()
.clamp(0.0, viewport_height as f64) as u32;
if x >= viewport_width || y >= viewport_height || max_x <= x || max_y <= y {
return (viewport_width, viewport_height, 0, 0);
}
(
x.min(viewport_width),
y.min(viewport_height),
max_x.saturating_sub(x),
max_y.saturating_sub(y),
)
}
fn intersect_scissor_rects(
lhs: (u32, u32, u32, u32),
rhs: (u32, u32, u32, u32),
) -> (u32, u32, u32, u32) {
let left = lhs.0.max(rhs.0);
let top = lhs.1.max(rhs.1);
let right = lhs.0.saturating_add(lhs.2).min(rhs.0.saturating_add(rhs.2));
let bottom = lhs.1.saturating_add(lhs.3).min(rhs.1.saturating_add(rhs.3));
(
left,
top,
right.saturating_sub(left),
bottom.saturating_sub(top),
)
}
fn intersect_scissor_optional(
lhs: Option<(u32, u32, u32, u32)>,
rhs: Option<(u32, u32, u32, u32)>,
) -> Option<(u32, u32, u32, u32)> {
match (lhs, rhs) {
(Some(lhs), Some(rhs)) => {
let left = lhs.0.max(rhs.0);
let top = lhs.1.max(rhs.1);
let right = lhs.0.saturating_add(lhs.2).min(rhs.0.saturating_add(rhs.2));
let bottom = lhs.1.saturating_add(lhs.3).min(rhs.1.saturating_add(rhs.3));
let width = right.saturating_sub(left);
let height = bottom.saturating_sub(top);
if width == 0 || height == 0 {
None
} else {
Some((left, top, width, height))
}
}
(Some(rect), None) | (None, Some(rect)) => Some(rect),
(None, None) => None,
}
}
fn clip_rect(clip: Option<&LayerClip>) -> Option<fission_layout::LayoutRect> {
match clip {
Some(LayerClip::Rect(rect)) => Some(*rect),
Some(LayerClip::RoundedRect { rect, .. }) => Some(*rect),
None => None,
}
}
fn transform_point(matrix: [f32; 16], point: LayoutPoint) -> LayoutPoint {
LayoutPoint::new(
matrix[0] * point.x + matrix[4] * point.y + matrix[12],
matrix[1] * point.x + matrix[5] * point.y + matrix[13],
)
}
fn logical_draw_rect_for_plan(
plan: &CompositorTexturePlan,
target_origin: LayoutPoint,
transform: Option<[f32; 16]>,
scale_factor: f64,
viewport_width: u32,
viewport_height: u32,
) -> Option<fission_layout::LayoutRect> {
let rect = clip_rect(plan.clip.as_ref()).unwrap_or(plan.bounds);
let scissor = clip_rect_to_physical(
Some(&LayerClip::Rect(rect)),
transform,
target_origin,
scale_factor,
viewport_width,
viewport_height,
);
physical_scissor_to_logical_rect(scissor, scale_factor)
}
fn physical_scissor_to_logical_rect(
scissor: (u32, u32, u32, u32),
scale_factor: f64,
) -> Option<fission_layout::LayoutRect> {
if scissor.2 == 0 || scissor.3 == 0 {
return None;
}
let scale = scale_factor as f32;
Some(fission_layout::LayoutRect::new(
scissor.0 as f32 / scale,
scissor.1 as f32 / scale,
scissor.2 as f32 / scale,
scissor.3 as f32 / scale,
))
}
fn rect_union(
lhs: fission_layout::LayoutRect,
rhs: fission_layout::LayoutRect,
) -> fission_layout::LayoutRect {
let left = lhs.origin.x.min(rhs.origin.x);
let top = lhs.origin.y.min(rhs.origin.y);
let right = (lhs.origin.x + lhs.size.width).max(rhs.origin.x + rhs.size.width);
let bottom = (lhs.origin.y + lhs.size.height).max(rhs.origin.y + rhs.size.height);
fission_layout::LayoutRect::new(left, top, right - left, bottom - top)
}
fn logical_rect_to_physical_scissor(
rect: fission_layout::LayoutRect,
scale_factor: f64,
viewport_width: u32,
viewport_height: u32,
) -> (u32, u32, u32, u32) {
let x = ((rect.origin.x as f64) * scale_factor).round().max(0.0) as u32;
let y = ((rect.origin.y as f64) * scale_factor).round().max(0.0) as u32;
if x >= viewport_width || y >= viewport_height {
return (viewport_width, viewport_height, 0, 0);
}
let w = ((rect.size.width as f64) * scale_factor).round().max(0.0) as u32;
let h = ((rect.size.height as f64) * scale_factor).round().max(0.0) as u32;
(
x.min(viewport_width),
y.min(viewport_height),
w.min(viewport_width.saturating_sub(x)),
h.min(viewport_height.saturating_sub(y)),
)
}
fn local_clip_mask(
clip: Option<&LayerClip>,
bounds: fission_layout::LayoutRect,
scale_factor: f64,
) -> ([f32; 4], [f32; 4]) {
match clip {
Some(LayerClip::Rect(rect)) => (
[
((rect.origin.x - bounds.origin.x) as f64 * scale_factor) as f32,
((rect.origin.y - bounds.origin.y) as f64 * scale_factor) as f32,
(rect.size.width as f64 * scale_factor) as f32,
(rect.size.height as f64 * scale_factor) as f32,
],
[1.0, 0.0, 0.0, 0.0],
),
Some(LayerClip::RoundedRect { rect, radius }) => (
[
((rect.origin.x - bounds.origin.x) as f64 * scale_factor) as f32,
((rect.origin.y - bounds.origin.y) as f64 * scale_factor) as f32,
(rect.size.width as f64 * scale_factor) as f32,
(rect.size.height as f64 * scale_factor) as f32,
],
[2.0, (*radius as f64 * scale_factor) as f32, 0.0, 0.0],
),
None => (
[
0.0,
0.0,
(bounds.size.width as f64 * scale_factor) as f32,
(bounds.size.height as f64 * scale_factor) as f32,
],
[0.0, 0.0, 0.0, 0.0],
),
}
}
fn identity_matrix() -> [f32; 16] {
[
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]
}
fn translation_matrix(tx: f32, ty: f32) -> [f32; 16] {
[
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, tx, ty, 0.0, 1.0,
]
}
fn multiply_matrix(a: [f32; 16], b: [f32; 16]) -> [f32; 16] {
let mut out = [0.0; 16];
for row in 0..4 {
for col in 0..4 {
let mut sum = 0.0;
for k in 0..4 {
sum += a[row * 4 + k] * b[k * 4 + col];
}
out[row * 4 + col] = sum;
}
}
out
}
fn create_cached_texture(
device: &wgpu::Device,
bind_group_layout: &wgpu::BindGroupLayout,
sampler: &wgpu::Sampler,
width: u32,
height: u32,
) -> CachedLayerTexture {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("fission-compositor layer texture"),
size: wgpu::Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::STORAGE_BINDING
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("fission-compositor uniform"),
size: std::mem::size_of::<LayerUniform>() 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("fission-compositor bind group"),
layout: bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: uniform_buffer.as_entire_binding(),
},
],
});
CachedLayerTexture {
texture,
view,
uniform_buffer,
bind_group,
width,
height,
content_key: 0,
base: None,
last_uniform: None,
last_used_frame: 0,
}
}
fn create_base_texture(device: &wgpu::Device, width: u32, height: u32) -> CachedBaseTexture {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("fission-compositor base texture"),
size: wgpu::Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::STORAGE_BINDING
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
CachedBaseTexture {
texture,
view,
width,
height,
scene_cache_key: None,
}
}
fn ensure_base_texture(
cached: &mut CachedLayerTexture,
device: &wgpu::Device,
queue: &wgpu::Queue,
vello_renderer: &mut VelloSceneRenderer,
retained_scene_cache: &mut RetainedSceneCache,
measurer: Arc<VelloTextMeasurer>,
scale_factor: f64,
scene: Option<&fission_render::RenderScene>,
scene_cache_key: Option<u64>,
width: u32,
height: u32,
) -> Result<()> {
let needs_render = {
let base = cached
.base
.get_or_insert_with(|| create_base_texture(device, width, height));
if base.width != width || base.height != height {
*base = create_base_texture(device, width, height);
}
base.scene_cache_key != scene_cache_key
};
if needs_render {
let base = cached
.base
.as_mut()
.expect("missing base texture after creation");
if let Some(scene) = scene {
render_plan_scene(
device,
queue,
vello_renderer,
retained_scene_cache,
measurer,
scale_factor,
scene,
scene_cache_key,
&base.view,
width,
height,
)?;
} else {
clear_target_view(device, queue, &base.view, wgpu::Color::TRANSPARENT);
}
base.scene_cache_key = scene_cache_key;
}
Ok(())
}
fn copy_texture_to_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
src: &wgpu::Texture,
dst: &wgpu::Texture,
width: u32,
height: u32,
region: Option<(u32, u32, u32, u32)>,
) {
let (origin_x, origin_y, copy_width, copy_height) = region.unwrap_or((0, 0, width, height));
if copy_width == 0 || copy_height == 0 {
return;
}
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("fission-compositor copy encoder"),
});
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: src,
mip_level: 0,
origin: wgpu::Origin3d {
x: origin_x,
y: origin_y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: dst,
mip_level: 0,
origin: wgpu::Origin3d {
x: origin_x,
y: origin_y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d {
width: copy_width.max(1),
height: copy_height.max(1),
depth_or_array_layers: 1,
},
);
queue.submit(Some(encoder.finish()));
}
fn render_or_seed_layer_base(
cached: &mut CachedLayerTexture,
device: &wgpu::Device,
queue: &wgpu::Queue,
vello_renderer: &mut VelloSceneRenderer,
retained_scene_cache: &mut RetainedSceneCache,
measurer: Arc<VelloTextMeasurer>,
scale_factor: f64,
scene: Option<&fission_render::RenderScene>,
scene_cache_key: Option<u64>,
width: u32,
height: u32,
region: Option<(u32, u32, u32, u32)>,
) -> Result<()> {
ensure_base_texture(
cached,
device,
queue,
vello_renderer,
retained_scene_cache,
measurer,
scale_factor,
scene,
scene_cache_key,
width,
height,
)?;
if let Some(base) = &cached.base {
copy_texture_to_texture(
device,
queue,
&base.texture,
&cached.texture,
width,
height,
region,
);
}
Ok(())
}
fn clear_target_view(
device: &wgpu::Device,
queue: &wgpu::Queue,
target_view: &wgpu::TextureView,
color: wgpu::Color,
) {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("fission-compositor clear encoder"),
});
{
let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("fission-compositor clear pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(color),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
}
queue.submit(Some(encoder.finish()));
}
fn render_plan_scene(
device: &wgpu::Device,
queue: &wgpu::Queue,
vello_renderer: &mut VelloSceneRenderer,
retained_scene_cache: &mut RetainedSceneCache,
measurer: Arc<VelloTextMeasurer>,
scale_factor: f64,
scene: &fission_render::RenderScene,
scene_cache_key: Option<u64>,
target_view: &wgpu::TextureView,
width: u32,
height: u32,
) -> Result<()> {
let params = RenderParams {
base_color: vello::peniko::Color::from_rgba8(0, 0, 0, 0),
width,
height,
antialiasing_method: vello::AaConfig::Area,
};
if let Some(cache_key) = scene_cache_key {
let cached_scene = retained_scene_cache.get_or_insert_with(cache_key, |scene_cache| {
let mut encoded = Scene::new();
let mut renderer = VelloRenderer::new(
&mut encoded,
Arc::clone(&measurer),
scene_cache,
scale_factor,
);
renderer.render_scene(scene)?;
Ok(encoded)
})?;
vello_renderer
.render_to_texture(device, queue, cached_scene, target_view, ¶ms)
.map_err(|error| anyhow::anyhow!("failed to render cached scene: {error}"))?;
return Ok(());
}
let mut encoded = Scene::new();
let mut renderer =
VelloRenderer::new(&mut encoded, measurer, retained_scene_cache, scale_factor);
renderer.render_scene(scene)?;
vello_renderer
.render_to_texture(device, queue, &encoded, target_view, ¶ms)
.map_err(|error| anyhow::anyhow!("failed to render compositor scene: {error}"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use fission_layout::LayoutRect;
fn plan_with_clip(
clip: Option<LayerClip>,
transform: Option<[f32; 16]>,
) -> CompositorTexturePlan {
CompositorTexturePlan {
key: 1,
bounds: LayoutRect::new(10.0, 20.0, 120.0, 80.0),
scene: None,
scene_cache_key: None,
content_key: 1,
local_dynamic: false,
composite_dynamic: false,
opacity: 1.0,
transform,
transform_clip: true,
clip,
children: Vec::new(),
source_layer_path: None,
}
}
#[test]
fn rounded_clip_mask_preserves_radius() {
let (clip_local, clip_shape) = local_clip_mask(
Some(&LayerClip::RoundedRect {
rect: LayoutRect::new(14.0, 26.0, 64.0, 32.0),
radius: 8.0,
}),
LayoutRect::new(10.0, 20.0, 120.0, 80.0),
2.0,
);
assert_eq!(clip_shape[0], 2.0);
assert_eq!(clip_shape[1], 16.0);
assert_eq!(clip_local, [8.0, 12.0, 128.0, 64.0]);
}
#[test]
fn draw_rect_respects_transform() {
let plan = plan_with_clip(
Some(LayerClip::Rect(LayoutRect::new(10.0, 20.0, 40.0, 20.0))),
Some(translation_matrix(12.0, 6.0)),
);
let rect =
logical_draw_rect_for_plan(&plan, LayoutPoint::ZERO, plan.transform, 1.0, 400, 300)
.expect("draw rect");
assert_eq!(rect.origin.x, 22.0);
assert_eq!(rect.origin.y, 26.0);
assert_eq!(rect.size.width, 40.0);
assert_eq!(rect.size.height, 20.0);
}
#[test]
fn disjoint_damage_scissor_skips_batch() {
let clip = intersect_scissor_optional(Some((0, 0, 20, 20)), Some((40, 40, 10, 10)));
assert!(clip.is_none());
}
}