use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use azul_core::dom::DomId;
use azul_core::geom::{LogicalRect, LogicalSize};
use azul_core::resources::RendererResources;
use azul_layout::cpurender;
use azul_layout::solver3::display_list::DisplayList;
use azul_layout::window::{FrameDamage, LayoutWindow};
pub(super) struct CpuBackend {
pub(super) last_frame: Option<cpurender::AzulPixmap>,
pub(super) compositor: Option<cpurender::CompositorState>,
pub(super) glyph_cache: azul_layout::glyph_cache::GlyphCache,
pub(super) previous_display_list: Option<DisplayList>,
pub(super) last_frame_damage: FrameDamage,
pub(super) last_present_damage: FrameDamage,
pub(super) previous_scroll_offsets: cpurender::ScrollOffsetMap,
pub(super) previous_vview_dls: BTreeMap<DomId, Arc<DisplayList>>,
pub(super) previous_gpu_transforms:
std::collections::HashMap<usize, azul_core::transform::ComputedTransform3D>,
pub(super) previous_gpu_opacities: std::collections::HashMap<usize, f32>,
}
impl Default for CpuBackend {
fn default() -> Self {
Self::new()
}
}
impl CpuBackend {
#[must_use]
pub(super) fn new() -> Self {
Self {
last_frame: None,
compositor: None,
glyph_cache: azul_layout::glyph_cache::GlyphCache::new(),
previous_display_list: None,
last_frame_damage: FrameDamage::None,
last_present_damage: FrameDamage::None,
previous_scroll_offsets: cpurender::ScrollOffsetMap::new(),
previous_vview_dls: BTreeMap::new(),
previous_gpu_transforms: std::collections::HashMap::new(),
previous_gpu_opacities: std::collections::HashMap::new(),
}
}
#[allow(clippy::too_many_lines)]
pub(super) fn render_frame(
&mut self,
layout_window: &LayoutWindow,
renderer_resources: &RendererResources,
width: f32,
height: f32,
dpi_factor: f32,
) -> Vec<LogicalRect> {
let dom_id = DomId { inner: 0 };
let Some(result) = layout_window.layout_results.get(&dom_id) else {
return Vec::new();
};
let display_list = &result.display_list;
let pixel_w = (width * dpi_factor).ceil() as u32;
let pixel_h = (height * dpi_factor).ceil() as u32;
if pixel_w == 0 || pixel_h == 0 {
return Vec::new();
}
let compositor = self
.compositor
.get_or_insert_with(|| cpurender::CompositorState::new(pixel_w, pixel_h));
let root = compositor.layers.get(&compositor.root_layer);
let (old_pw, old_ph) = match root {
Some(layer) => (layer.pixbuf.width(), layer.pixbuf.height()),
None => (0, 0),
};
let needs_resize = old_pw != pixel_w || old_ph != pixel_h;
let mut resize_damage = Vec::new();
let mut resize_preserved_pixels = false;
if needs_resize {
let is_grow = pixel_w >= old_pw && pixel_h >= old_ph && old_pw > 0 && old_ph > 0;
if is_grow {
resize_preserved_pixels = true;
if let Some(root_layer) = compositor.layers.get_mut(&compositor.root_layer) {
let _ = root_layer.pixbuf.resize_grow_only(pixel_w, pixel_h, 255, 255, 255, 255);
root_layer.bounds.size = LogicalSize {
width: pixel_w as f32,
height: pixel_h as f32,
};
}
resize_damage = cpurender::compute_resize_damage(
old_pw as f32 / dpi_factor,
old_ph as f32 / dpi_factor,
width,
height,
);
} else {
*compositor = cpurender::CompositorState::new(pixel_w, pixel_h);
}
}
let scroll_offsets = layout_window
.scroll_manager
.build_scroll_offset_map(dom_id, &result.scroll_ids);
let gpu_cache_early = layout_window.gpu_state_manager.get_cache(dom_id);
let (gpu_transforms, gpu_opacities) =
cpurender::extract_gpu_values(gpu_cache_early, dom_id);
let gpu_damage = cpurender::gpu_value_damage(
display_list,
&self.previous_gpu_transforms,
&self.previous_gpu_opacities,
&gpu_transforms,
&gpu_opacities,
);
let has_gpu_damage = !gpu_damage.rects.is_empty() || gpu_damage.needs_full;
self.previous_gpu_transforms = gpu_transforms;
self.previous_gpu_opacities = gpu_opacities;
let can_reuse_previous_frame = !needs_resize || resize_preserved_pixels;
let dl_damage = match &self.previous_display_list {
Some(old_dl) if can_reuse_previous_frame && !gpu_damage.needs_full => {
cpurender::compute_display_list_damage(
old_dl,
display_list,
&self.previous_scroll_offsets,
&scroll_offsets,
)
}
_ => None, };
let vview_dls: BTreeMap<DomId, Arc<DisplayList>> = layout_window
.layout_results
.iter()
.filter(|(id, _)| id.inner != dom_id.inner)
.map(|(id, r)| (*id, Arc::new(r.display_list.clone())))
.collect();
let vview_damage = cpurender::compute_virtual_view_damage(
display_list,
&vview_dls,
&self.previous_vview_dls,
);
let has_vview_damage = !vview_damage.is_empty();
self.previous_vview_dls = vview_dls.clone();
let mut scroll_shifts: Vec<(u64, LogicalRect, (f32, f32), (f32, f32))> = Vec::new();
for (scroll_id, offset) in &scroll_offsets {
let prev = self
.previous_scroll_offsets
.get(scroll_id)
.copied()
.unwrap_or((0.0, 0.0));
let delta = (offset.0 - prev.0, offset.1 - prev.1);
if (delta.0 * dpi_factor).abs() > 0.5 || (delta.1 * dpi_factor).abs() > 0.5 {
for item in display_list.items.iter() {
if let azul_layout::solver3::display_list::DisplayListItem::PushScrollFrame {
clip_bounds,
scroll_id: sid,
..
} = item
{
if sid == scroll_id {
scroll_shifts.push((*sid, *clip_bounds.inner(), delta, *offset));
}
}
}
}
}
let has_scroll = !scroll_shifts.is_empty();
let shifted_ids: BTreeSet<u64> = scroll_shifts.iter().map(|(sid, ..)| *sid).collect();
let next_scroll_baseline: cpurender::ScrollOffsetMap = scroll_offsets
.iter()
.map(|(id, off)| {
if shifted_ids.contains(id) {
(*id, *off)
} else {
(
*id,
self.previous_scroll_offsets.get(id).copied().unwrap_or(*off),
)
}
})
.collect();
let mut all_damage: Vec<LogicalRect>;
let is_incremental;
match dl_damage {
Some(rects)
if rects.is_empty()
&& !needs_resize
&& resize_damage.is_empty()
&& !has_scroll
&& !has_vview_damage
&& !has_gpu_damage =>
{
self.previous_display_list = Some(display_list.clone());
self.previous_scroll_offsets = next_scroll_baseline;
self.last_frame_damage = FrameDamage::None;
self.last_present_damage = FrameDamage::None;
return Vec::new();
}
Some(mut rects) if can_reuse_previous_frame => {
rects.extend(resize_damage);
all_damage = rects;
is_incremental = true;
}
_ => {
all_damage = resize_damage;
is_incremental = false;
}
}
if is_incremental && has_vview_damage {
all_damage.extend(vview_damage);
}
if is_incremental && !gpu_damage.rects.is_empty() {
all_damage.extend(gpu_damage.rects.iter().copied());
}
let mut output = match self.last_frame.take() {
Some(p) if p.width() == pixel_w && p.height() == pixel_h => p,
Some(mut p) => {
p.resize_reuse(pixel_w, pixel_h, 255, 255, 255, 255);
p
}
None => match cpurender::AzulPixmap::new(pixel_w, pixel_h) {
Some(mut p) => {
p.fill(255, 255, 255, 255);
p
}
None => return Vec::new(),
},
};
let mut present_extra: Vec<LogicalRect> = Vec::new();
if is_incremental {
for (scroll_id, clip, delta, offset) in &scroll_shifts {
let prev_offset = (offset.0 - delta.0, offset.1 - delta.1);
if cpurender::scroll_fast_path_eligible(
display_list,
*scroll_id,
clip,
*offset,
prev_offset,
) {
let strips = cpurender::scroll_shift_region(
&mut output,
clip,
*delta,
*offset,
dpi_factor,
);
all_damage.extend(strips);
all_damage.extend(cpurender::overlay_rects_after_frame(
display_list,
*scroll_id,
clip,
));
present_extra.push(*clip);
} else {
all_damage.push(*clip);
}
}
}
if is_incremental {
cpurender::coalesce_damage_rects(&mut all_damage);
}
let gpu_cache = layout_window.gpu_state_manager.get_cache(dom_id);
let render_offsets = if is_incremental {
&next_scroll_baseline
} else {
&scroll_offsets
};
let render_state =
cpurender::CpuRenderState::from_gpu_cache(gpu_cache, dom_id, render_offsets)
.with_system_style(layout_window.system_style.clone())
.with_virtual_view_display_lists(vview_dls)
.with_image_callback_results(layout_window.cpu_image_callback_results.clone());
if is_incremental && !all_damage.is_empty() {
drop(cpurender::render_display_list_damaged(
display_list,
&mut output,
dpi_factor,
renderer_resources,
&layout_window.font_manager,
&mut self.glyph_cache,
&render_state,
&all_damage,
));
} else {
output.fill(255, 255, 255, 255);
compositor.allocate_layers_from_display_list(display_list, dpi_factor);
drop(compositor.render_layers(
display_list,
dpi_factor,
renderer_resources,
&layout_window.font_manager,
&mut self.glyph_cache,
&render_state,
));
compositor.composite_frame(&mut output, dpi_factor);
}
self.previous_display_list = Some(display_list.clone());
self.previous_scroll_offsets = if is_incremental {
next_scroll_baseline
} else {
scroll_offsets.clone()
};
self.last_frame = Some(output);
self.last_frame_damage = if is_incremental {
FrameDamage::Rects(all_damage.clone())
} else {
FrameDamage::Full
};
self.last_present_damage = if is_incremental {
let mut present = all_damage.clone();
present.extend(present_extra);
FrameDamage::Rects(present)
} else {
FrameDamage::Full
};
all_damage
}
}