1
//! CPU render backend for the headless E2E runner.
2
//!
3
//! Verbatim port of the DLL's `desktop::shell2::headless::CpuBackend`
4
//! (`dll/src/desktop/shell2/headless/mod.rs`) minus the pieces that need a
5
//! `PlatformWindow` (the hit tester, the `AZ_MAP_DEBUG` / `AZ_DUMP_FRAME_DIR`
6
//! dumps). Everything it calls lives in `azul_layout::cpurender`, so the port is
7
//! mechanical.
8
//!
9
//! WHY THIS EXISTS: the damage assertions (`assert_changed`,
10
//! `assert_damage_covers_changes`, `assert_damage_incremental`,
11
//! `assert_idle_stable`) read `LayoutWindow::frame_report`, which is written by
12
//! `FrameReport::record_frame` — and the ONLY producer of the paint/present
13
//! damage it records is this render pass. A runner that never renders a frame
14
//! reports `FrameDamage::None` forever, so every damage assertion fails with
15
//! "nothing was repainted (stale screen)" no matter what the engine did.
16

            
17
use std::collections::{BTreeMap, BTreeSet};
18
use std::sync::Arc;
19

            
20
use azul_core::dom::DomId;
21
use azul_core::geom::{LogicalRect, LogicalSize};
22
use azul_core::resources::RendererResources;
23

            
24
use azul_layout::cpurender;
25
use azul_layout::solver3::display_list::DisplayList;
26
use azul_layout::window::{FrameDamage, LayoutWindow};
27

            
28
/// CPU rendering backend (the headless replacement for WebRender).
29
///
30
/// Holds the retained compositor state, the previous frame's display list /
31
/// scroll offsets / GPU values — everything the frame-to-frame damage diff
32
/// needs — and the damage of the most recent `render_frame`.
33
pub(super) struct CpuBackend {
34
    /// Last rendered pixmap.
35
    pub(super) last_frame: Option<cpurender::AzulPixmap>,
36
    /// Retained compositor state with per-layer pixbufs.
37
    pub(super) compositor: Option<cpurender::CompositorState>,
38
    /// Glyph cache — persists across frames for text rendering.
39
    pub(super) glyph_cache: azul_layout::glyph_cache::GlyphCache,
40
    /// Previous display list for damage-rect computation.
41
    pub(super) previous_display_list: Option<Arc<DisplayList>>,
42
    /// PAINT damage of the most recent `render_frame` — the region actually
43
    /// re-rasterised.
44
    pub(super) last_frame_damage: FrameDamage,
45
    /// PRESENT damage of the most recent `render_frame` — the region that
46
    /// visually CHANGED on screen (⊇ paint damage; a scroll memmoves a large
47
    /// region but paints a strip).
48
    pub(super) last_present_damage: FrameDamage,
49
    /// Scroll offsets of the previous frame (`scroll_id` → (x,y)).
50
    pub(super) previous_scroll_offsets: cpurender::ScrollOffsetMap,
51
    /// Where zombie exits painted LAST frame (logical px). Per frame the
52
    /// zombie contribution to damage is `previous ∪ current`: restore the
53
    /// live pixels where an exit was, paint it where it is — the reap frame
54
    /// (zombies gone, previous non-empty) erases the leftovers the same way.
55
    pub(super) previous_zombie_rects: Vec<azul_core::geom::LogicalRect>,
56
    /// Previous frame's `VirtualView` child-DOM display lists.
57
    pub(super) previous_vview_dls: BTreeMap<DomId, Arc<DisplayList>>,
58
    /// GPU-animated values of the previous frame, for the frame-to-frame diff.
59
    pub(super) previous_gpu_transforms:
60
        std::collections::HashMap<usize, azul_core::transform::ComputedTransform3D>,
61
    pub(super) previous_gpu_opacities: std::collections::HashMap<usize, f32>,
62
}
63

            
64
impl Default for CpuBackend {
65
    fn default() -> Self {
66
        Self::new()
67
    }
68
}
69

            
70
impl CpuBackend {
71
    #[must_use]
72
57
    pub(super) fn new() -> Self {
73
57
        Self {
74
57
            last_frame: None,
75
57
            compositor: None,
76
57
            glyph_cache: azul_layout::glyph_cache::GlyphCache::new(),
77
57
            previous_display_list: None,
78
57
            last_frame_damage: FrameDamage::None,
79
57
            last_present_damage: FrameDamage::None,
80
57
            previous_scroll_offsets: cpurender::ScrollOffsetMap::new(),
81
57
            previous_zombie_rects: Vec::new(),
82
57
            previous_vview_dls: BTreeMap::new(),
83
57
            previous_gpu_transforms: std::collections::HashMap::new(),
84
57
            previous_gpu_opacities: std::collections::HashMap::new(),
85
57
        }
86
57
    }
87

            
88
    /// Render the current display list into `last_frame`, recording the paint /
89
    /// present damage of the frame.
90
    ///
91
    /// Uses damage-rect-based incremental rendering when possible: the current
92
    /// display list is diffed against `previous_display_list`, and only the
93
    /// changed regions are repainted. Returns the damage rects that were
94
    /// rendered (empty = nothing changed, or a full repaint).
95
    #[allow(clippy::too_many_lines)]
96
16674
    pub(super) fn render_frame(
97
16674
        &mut self,
98
16674
        layout_window: &LayoutWindow,
99
16674
        renderer_resources: &RendererResources,
100
16674
        width: f32,
101
16674
        height: f32,
102
16674
        dpi_factor: f32,
103
16674
    ) -> Vec<LogicalRect> {
104
        // Engine observability: every e2e/headless frame reports its
105
        // duration + probe spans (drop-guard covers all return paths).
106
        #[cfg(feature = "telemetry")]
107
        let _frame_pump = crate::telemetry::FramePump::begin("present");
108

            
109
16674
        let dom_id = DomId { inner: 0 };
110
16674
        let Some(result) = layout_window.layout_results.get(&dom_id) else {
111
            return Vec::new();
112
        };
113
16674
        let display_list = &result.display_list;
114

            
115
16674
        let pixel_w = (width * dpi_factor).ceil() as u32;
116
16674
        let pixel_h = (height * dpi_factor).ceil() as u32;
117
16674
        if pixel_w == 0 || pixel_h == 0 {
118
            return Vec::new();
119
16674
        }
120

            
121
        // Allocate or resize compositor
122
16674
        let compositor = self
123
16674
            .compositor
124
16674
            .get_or_insert_with(|| cpurender::CompositorState::new(pixel_w, pixel_h));
125

            
126
16674
        let root = compositor.layers.get(&compositor.root_layer);
127
16674
        let (old_pw, old_ph) = match root {
128
16674
            Some(layer) => (layer.pixbuf.width(), layer.pixbuf.height()),
129
            None => (0, 0),
130
        };
131
16674
        let needs_resize = old_pw != pixel_w || old_ph != pixel_h;
132

            
133
16674
        let mut resize_damage = Vec::new();
134
        // A GROW preserves the previous frame: `resize_grow_only` copies the old
135
        // pixels into the top-left of the enlarged buffer (and `resize_reuse`
136
        // does the same for `last_frame` below), so the frame stays a valid base
137
        // for an incremental repaint and only the newly-exposed L is unknown.
138
        // A SHRINK throws the whole compositor away, so nothing may be reused.
139
16674
        let mut resize_preserved_pixels = false;
140
16674
        if needs_resize {
141
11
            let is_grow = pixel_w >= old_pw && pixel_h >= old_ph && old_pw > 0 && old_ph > 0;
142
11
            if is_grow {
143
6
                resize_preserved_pixels = true;
144
6
                if let Some(root_layer) = compositor.layers.get_mut(&compositor.root_layer) {
145
6
                    let _ = root_layer.pixbuf.resize_grow_only(pixel_w, pixel_h, 255, 255, 255, 255);
146
6
                    root_layer.bounds.size = LogicalSize {
147
6
                        width: pixel_w as f32,
148
6
                        height: pixel_h as f32,
149
6
                    };
150
6
                }
151
                // Damage rects are LOGICAL everywhere downstream.
152
6
                resize_damage = cpurender::compute_resize_damage(
153
6
                    old_pw as f32 / dpi_factor,
154
6
                    old_ph as f32 / dpi_factor,
155
6
                    width,
156
6
                    height,
157
                );
158
5
            } else {
159
5
                // Shrink (or a MIXED resize — wider but shorter lands here too,
160
5
                // `is_grow` demands both axes). This branch stays a FULL
161
5
                // repaint, and that is a measured decision, not an oversight: it
162
5
                // recreates the compositor AND never calls
163
5
                // `compute_resize_damage`, so letting it reuse the previous
164
5
                // frame under-paints. Measured with the resize probe at
165
5
                // 500x600 -> 700x400: 53200 changed pixels uncovered by any
166
5
                // damage rect, the first at (500, 134) — i.e. the whole
167
5
                // newly-exposed right strip, stale on a real screen. A shrink
168
5
                // also exposes nothing new, so a full repaint here costs at most
169
5
                // the NEW (smaller) buffer.
170
5
                *compositor = cpurender::CompositorState::new(pixel_w, pixel_h);
171
5
            }
172
16663
        }
173

            
174
        // Real scroll offsets for this frame — needed by the damage diff (items
175
        // inside scroll frames are stored at CONTENT coords) and by the
176
        // scroll-shift machinery further down.
177
16674
        let scroll_offsets = layout_window
178
16674
            .scroll_manager
179
16674
            .build_scroll_offset_map(dom_id, &result.scroll_id_to_node_id);
180

            
181
        // GPU-value diff: thumb position / fade opacity / transforms change
182
        // WITHOUT any display-list item changing (items only carry the keys).
183
16674
        let gpu_cache_early = layout_window.gpu_state_manager.get_cache(dom_id);
184
16674
        let (gpu_transforms, gpu_opacities) =
185
16674
            cpurender::extract_gpu_values(gpu_cache_early, dom_id);
186
16674
        let gpu_damage = cpurender::gpu_value_damage(
187
16674
            display_list,
188
16674
            &self.previous_gpu_transforms,
189
16674
            &self.previous_gpu_opacities,
190
16674
            &gpu_transforms,
191
16674
            &gpu_opacities,
192
        );
193
16674
        let has_gpu_damage = !gpu_damage.rects.is_empty() || gpu_damage.needs_full;
194
16674
        if has_gpu_damage && std::env::var_os("AZ_PATCH_DEBUG").is_some() {
195
            let td: Vec<_> = gpu_transforms
196
                .iter()
197
                .filter(|(k, v)| self.previous_gpu_transforms.get(k) != Some(v))
198
                .map(|(k, v)| (*k, v.m[3][0], v.m[3][1]))
199
                .collect();
200
            let od: Vec<_> = gpu_opacities
201
                .iter()
202
                .filter(|(k, v)| self.previous_gpu_opacities.get(k) != Some(v))
203
                .map(|(k, v)| (*k, *v))
204
                .collect();
205
            eprintln!(
206
                "[GPUDMG] prev_t={} cur_t={} prev_o={} cur_o={} changed_t={td:?} changed_o={od:?} rects={:?}",
207
                self.previous_gpu_transforms.len(),
208
                gpu_transforms.len(),
209
                self.previous_gpu_opacities.len(),
210
                gpu_opacities.len(),
211
                gpu_damage.rects,
212
            );
213
16674
        }
214
        // Retained exits repaint every tick without any display-list item
215
        // changing — their per-frame truth is `previous ∪ current` painted
216
        // rects: restore the live frame where the exit WAS, paint it where
217
        // it IS. That keeps the incremental path (and even the reap frame's
218
        // cleanup) on bounded damage instead of forcing full composites for
219
        // the whole exit duration.
220
16674
        let zombies_active = layout_window.has_zombies();
221
16674
        let zombie_rects = if zombies_active {
222
18
            layout_window.zombie_paint_rects()
223
        } else {
224
16656
            Vec::new()
225
        };
226
16674
        let zombie_damage: Vec<azul_core::geom::LogicalRect> = self
227
16674
            .previous_zombie_rects
228
16674
            .iter()
229
16674
            .chain(zombie_rects.iter())
230
16674
            .copied()
231
16674
            .collect();
232
16674
        self.previous_gpu_transforms = gpu_transforms;
233
16674
        self.previous_gpu_opacities = gpu_opacities;
234

            
235
        // Can the pixels of the previous frame still be trusted? Yes when the
236
        // buffer did not change size at all, and yes on a GROW (the old pixels
237
        // were copied over verbatim). No on a shrink / first allocation.
238
16674
        let can_reuse_previous_frame = !needs_resize || resize_preserved_pixels;
239

            
240
        // Display-list damage (incremental path)
241
16674
        let dl_damage = match &self.previous_display_list {
242
16617
            Some(old_dl) if can_reuse_previous_frame && !gpu_damage.needs_full => {
243
16612
                cpurender::compute_display_list_damage(
244
16612
                    old_dl,
245
16612
                    display_list,
246
16612
                    &self.previous_scroll_offsets,
247
16612
                    &scroll_offsets,
248
                )
249
            }
250
62
            _ => None, // first frame, shrink or ref-frame transform → full repaint
251
        };
252

            
253
        // VirtualView child-DOM damage.
254
16674
        let vview_dls: BTreeMap<DomId, Arc<DisplayList>> = layout_window
255
16674
            .layout_results
256
16674
            .iter()
257
16674
            .filter(|(id, _)| id.inner != dom_id.inner)
258
16674
            .map(|(id, r)| (*id, r.display_list.clone()))
259
16674
            .collect();
260
16674
        let vview_damage = cpurender::compute_virtual_view_damage(
261
16674
            display_list,
262
16674
            &vview_dls,
263
16674
            &self.previous_vview_dls,
264
        );
265
16674
        let has_vview_damage = !vview_damage.is_empty();
266
16674
        self.previous_vview_dls = vview_dls.clone();
267

            
268
        // Scroll: the display list is UNCHANGED on scroll, so the diff above
269
        // only ever catches the scrollbar. Collect (clip, delta) per frame whose
270
        // offset changed so the still-visible pixels can be MOVED and only the
271
        // exposed strip repainted.
272
16674
        let mut scroll_shifts: Vec<(u64, LogicalRect, (f32, f32), (f32, f32))> = Vec::new();
273
33126
        for (scroll_id, offset) in &scroll_offsets {
274
16452
            let prev = self
275
16452
                .previous_scroll_offsets
276
16452
                .get(scroll_id)
277
16452
                .copied()
278
16452
                .unwrap_or((0.0, 0.0));
279
16452
            let delta = (offset.0 - prev.0, offset.1 - prev.1);
280
            // Threshold in PHYSICAL pixels.
281
16452
            if (delta.0 * dpi_factor).abs() > 0.5 || (delta.1 * dpi_factor).abs() > 0.5 {
282
438
                for item in display_list.items.iter() {
283
                    if let azul_layout::solver3::display_list::DisplayListItem::PushScrollFrame {
284
14
                        clip_bounds,
285
14
                        scroll_id: sid,
286
                        ..
287
438
                    } = item
288
                    {
289
14
                        if sid == scroll_id {
290
14
                            scroll_shifts.push((*sid, *clip_bounds.inner(), delta, *offset));
291
14
                        }
292
424
                    }
293
                }
294
16438
            }
295
        }
296
16674
        let has_scroll = !scroll_shifts.is_empty();
297
        // Advance the scroll baseline ONLY for frames actually painted at their
298
        // new offset this call, so sub-device-pixel deltas ACCUMULATE instead of
299
        // being swallowed frame after frame.
300
16674
        let shifted_ids: BTreeSet<u64> = scroll_shifts.iter().map(|(sid, ..)| *sid).collect();
301
16674
        let next_scroll_baseline: cpurender::ScrollOffsetMap = scroll_offsets
302
16674
            .iter()
303
16674
            .map(|(id, off)| {
304
16452
                if shifted_ids.contains(id) {
305
14
                    (*id, *off)
306
                } else {
307
16438
                    (
308
16438
                        *id,
309
16438
                        self.previous_scroll_offsets.get(id).copied().unwrap_or(*off),
310
16438
                    )
311
                }
312
16452
            })
313
16674
            .collect();
314

            
315
        // Determine render path.
316
        let mut all_damage: Vec<LogicalRect>;
317
        let is_incremental;
318

            
319
        // A PATCHED build may change the item count, which the old-vs-new
320
        // item diff reads as structural (None -> full). The patch recorded
321
        // its own precise damage at build time — and on a patched build it
322
        // is AUTHORITATIVE, not a fallback: the index-pairing diff
323
        // under-damages a same-count splice (re-emitted node + translated
324
        // neighbours mis-pair). Guarded to the same conditions the diff ran
325
        // under, so gpu needs_full / shrink / first frame stay full repaints.
326
16674
        let diff_path_ran = self.previous_display_list.is_some()
327
16617
            && can_reuse_previous_frame
328
16612
            && !gpu_damage.needs_full;
329
16674
        let dl_damage = if diff_path_ran && layout_window.layout_cache.last_build_was_patched {
330
            // On a PATCHED build the patch's own damage AUGMENTS the item
331
            // diff: the index-pairing diff under-damages a same-count splice
332
            // (one stale rect where a reflow moved three nodes), so union
333
            // the two when the diff produced rects, and use the patch's
334
            // damage alone when the diff gave up (count change -> None).
335
            // Never REPLACE a Some(diff) wholesale: unpatched-equal frames
336
            // must keep their baseline damage exactly (an empty diff on a
337
            // quiet frame stays the idle skip).
338
18
            match (dl_damage, layout_window.layout_cache.last_patch_damage.clone()) {
339
                // An EMPTY diff on a patched build means the splice produced a
340
                // byte-identical list (same-text re-shape) — the frame is IDLE
341
                // and must stay idle; painting patch rects here flips the
342
                // idle-skip and drifts the frame scheduling (scrollbar-fade
343
                // clock) off the baseline.
344
15
                (Some(d), Some(_)) if d.is_empty() => Some(d),
345
5
                (Some(mut d), Some(p)) => {
346
5
                    d.extend(p);
347
5
                    Some(d)
348
                }
349
3
                (None, p) => p,
350
                (d, None) => d,
351
            }
352
        } else {
353
16656
            dl_damage
354
        };
355
16674
        if std::env::var_os("AZ_PATCH_DEBUG").is_some() {
356
            eprintln!(
357
                "[E2EDMG] dl_damage={:?} diff_ran={} patched={} resize={:?} gpu_full={} gpu_rects={} zombie={}",
358
                dl_damage.as_ref().map(|r| r.len()),
359
                diff_path_ran,
360
                layout_window.layout_cache.last_build_was_patched,
361
                resize_damage.len(),
362
                gpu_damage.needs_full,
363
                gpu_damage.rects.len(),
364
                zombie_damage.len(),
365
            );
366
16674
        }
367
73
        match dl_damage {
368
16505
            Some(rects)
369
16578
                if rects.is_empty()
370
16518
                    && !needs_resize
371
16517
                    && resize_damage.is_empty()
372
16517
                    && !has_scroll
373
16515
                    && !has_vview_damage
374
16515
                    && !has_gpu_damage
375
16508
                    && zombie_damage.is_empty() =>
376
            {
377
                // Nothing changed — skip rendering entirely.
378
                //
379
                // `!needs_resize` is load-bearing now that a resize can reach
380
                // this match at all: skipping leaves `last_frame` at the OLD
381
                // dimensions while the compositor is already at the new ones, so
382
                // the host would publish (and present) a wrongly-sized buffer.
383
                // A frame whose backing store changed size is never "nothing".
384
16505
                self.previous_display_list = Some(display_list.clone());
385
16505
                self.previous_scroll_offsets = next_scroll_baseline;
386
16505
                self.last_frame_damage = FrameDamage::None;
387
16505
                self.last_present_damage = FrameDamage::None;
388
16505
                return Vec::new();
389
            }
390
            // The display-list diff plus, on a grow, the newly-exposed L. The
391
            // guard used to be `!needs_resize`, which meant a grow BUILT the
392
            // bounded repaint (`compute_resize_damage` + `resize_grow_only`
393
            // preserving the old pixels) and then threw it away: `dl_damage` was
394
            // forced to `None`, the match fell through to `_`, the buffer was
395
            // filled white and everything was repainted — `FrameDamage::Full`
396
            // for a window that only grew by a strip.
397
73
            Some(mut rects) if can_reuse_previous_frame => {
398
73
                rects.extend(resize_damage);
399
73
                all_damage = rects;
400
73
                is_incremental = true;
401
73
            }
402
96
            _ => {
403
96
                all_damage = resize_damage;
404
96
                is_incremental = false;
405
96
            }
406
        }
407

            
408
169
        if is_incremental && has_vview_damage {
409
            all_damage.extend(vview_damage);
410
169
        }
411
169
        if is_incremental && !gpu_damage.rects.is_empty() {
412
40
            all_damage.extend(gpu_damage.rects.iter().copied());
413
129
        }
414
169
        if is_incremental && !zombie_damage.is_empty() {
415
12
            all_damage.extend(zombie_damage.iter().copied());
416
157
        }
417

            
418
        // Acquire output pixmap — reuse buffer for both grow and shrink
419
169
        let mut output = match self.last_frame.take() {
420
112
            Some(p) if p.width() == pixel_w && p.height() == pixel_h => p,
421
11
            Some(mut p) => {
422
11
                p.resize_reuse(pixel_w, pixel_h, 255, 255, 255, 255);
423
11
                p
424
            }
425
57
            None => match cpurender::AzulPixmap::new(pixel_w, pixel_h) {
426
57
                Some(mut p) => {
427
57
                    p.fill(255, 255, 255, 255);
428
57
                    p
429
                }
430
                None => return Vec::new(),
431
            },
432
        };
433

            
434
        // Thin-strip scroll: MOVE the still-visible pixels and repaint only the
435
        // strip that scrolled into view. Regions that were pixel-SHIFTED belong
436
        // to PRESENT damage (the whole clip changed on screen) but not to paint
437
        // damage (only a strip was rasterised).
438
169
        let mut present_extra: Vec<LogicalRect> = Vec::new();
439
169
        if is_incremental {
440
87
            for (scroll_id, clip, delta, offset) in &scroll_shifts {
441
14
                let prev_offset = (offset.0 - delta.0, offset.1 - delta.1);
442
14
                if cpurender::scroll_fast_path_eligible(
443
14
                    display_list,
444
14
                    *scroll_id,
445
14
                    clip,
446
14
                    *offset,
447
14
                    prev_offset,
448
14
                ) {
449
14
                    let strips = cpurender::scroll_shift_region(
450
14
                        &mut output,
451
14
                        clip,
452
14
                        *delta,
453
14
                        *offset,
454
14
                        dpi_factor,
455
14
                    );
456
14
                    all_damage.extend(strips);
457
14
                    all_damage.extend(cpurender::overlay_rects_after_frame(
458
14
                        display_list,
459
14
                        *scroll_id,
460
14
                        clip,
461
14
                    ));
462
14
                    present_extra.push(*clip);
463
14
                } else {
464
                    all_damage.push(*clip);
465
                }
466
            }
467
96
        }
468

            
469
        // The recorded paint/present damage must not double-count a region.
470
169
        if is_incremental {
471
73
            cpurender::coalesce_damage_rects(&mut all_damage);
472
96
        }
473

            
474
169
        let gpu_cache = layout_window.gpu_state_manager.get_cache(dom_id);
475
        // Incremental repaints must raster at the offsets the surrounding
476
        // (un-repainted) pixels are ALREADY at — the baseline.
477
169
        let render_offsets = if is_incremental {
478
73
            &next_scroll_baseline
479
        } else {
480
96
            &scroll_offsets
481
        };
482
169
        let render_state =
483
169
            cpurender::CpuRenderState::from_gpu_cache(gpu_cache, dom_id, render_offsets)
484
169
                .with_system_style(layout_window.system_style.clone())
485
169
                .with_virtual_view_display_lists(vview_dls);
486

            
487
169
        if is_incremental && !all_damage.is_empty() {
488
73
            drop(cpurender::render_display_list_damaged(
489
73
                display_list,
490
73
                &mut output,
491
73
                dpi_factor,
492
73
                renderer_resources,
493
73
                &layout_window.font_manager,
494
73
                &mut self.glyph_cache,
495
73
                &render_state,
496
73
                &all_damage,
497
            ));
498
            // Exits paint ON TOP of the restored live pixels; their current
499
            // rects are inside `all_damage` by construction.
500
73
            if zombies_active {
501
11
                layout_window.composite_zombies_cpu(
502
11
                    &mut output,
503
11
                    dpi_factor,
504
11
                    renderer_resources,
505
11
                    &mut self.glyph_cache,
506
11
                );
507
62
            }
508
96
        } else {
509
96
            output.fill(255, 255, 255, 255);
510
96
            compositor.allocate_layers_from_display_list(
511
96
                display_list,
512
96
                dpi_factor,
513
96
                &render_state.transforms,
514
96
                &render_state.opacities,
515
96
            );
516
96
            drop(compositor.render_layers(
517
96
                display_list,
518
96
                dpi_factor,
519
96
                renderer_resources,
520
96
                &layout_window.font_manager,
521
96
                &mut self.glyph_cache,
522
96
                &render_state,
523
96
            ));
524
96
            compositor.composite_frame(&mut output, dpi_factor);
525
96
            // The design doc's invariant: the rendered frame is B ∪ zombies.
526
96
            layout_window.composite_zombies_cpu(
527
96
                &mut output,
528
96
                dpi_factor,
529
96
                renderer_resources,
530
96
                &mut self.glyph_cache,
531
96
            );
532
96
        }
533

            
534
169
        self.previous_zombie_rects = zombie_rects;
535
169
        self.previous_display_list = Some(display_list.clone());
536
169
        self.previous_scroll_offsets = if is_incremental {
537
73
            next_scroll_baseline
538
        } else {
539
96
            scroll_offsets.clone()
540
        };
541
169
        self.last_frame = Some(output);
542
169
        self.last_frame_damage = if is_incremental {
543
73
            FrameDamage::Rects(all_damage.clone())
544
        } else {
545
96
            FrameDamage::Full
546
        };
547
169
        self.last_present_damage = if is_incremental {
548
73
            let mut present = all_damage.clone();
549
73
            present.extend(present_extra);
550
73
            FrameDamage::Rects(present)
551
        } else {
552
96
            FrameDamage::Full
553
        };
554
169
        all_damage
555
16674
    }
556
}