1use std::collections::HashSet;
21use std::ops::Range;
22
23use ailloli_ui_core::math::Scale;
24use ailloli_ui_core::{BorderStyle, ClipShape, Color, Point, Radius, Rect};
25use ailloli_ui_runtime::scene::ClipEntry;
26use ailloli_ui_runtime::{
27 BlendMode, DrawBorder, DrawBoxShadow, DrawCmd, DrawImage, DrawPolyline, DrawRRect, DrawRect,
28 DrawRingProgress,
29};
30
31use crate::clip::{ClipParamsGpu, ClipRenderMode};
32use crate::cmd_bounds::{
33 inflate_for_effects, push_composite_quad, scissor_to_local, snap_and_clamp_bounds,
34 union_cmd_bounds_prepared,
35};
36use crate::frame_prep::PreparedResources;
37use crate::icons::IconKey;
38use crate::isolated_budget::IsolatedBudgetPolicy;
39use crate::isolated_plan::{
40 BackdropCapturePoint, CompositeParams, IsolatedEffectChain, OffscreenPassId,
41 PlannedIsolatedComposite, PlannedIsolatedPass,
42};
43use crate::passes::primitives::text_origin_from_baseline;
44use crate::passes::{
45 make_tex_rect_scaled, push_border_rrect_scaled, push_box_shadow_scaled, push_polyline_scaled,
46 push_rect_scaled, push_ring_progress_scaled, push_rrect_scaled, to_ndc,
47};
48use crate::renderer::LayerPass;
49use crate::text::GlyphKey;
50use crate::vertices::{
51 BorderRRectVertex, BoxShadowVertex, RRectVertex, RingProgressVertex, StrokeVertex, TexVertex,
52 Vertex,
53};
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum PipelineKind {
65 Rect,
67 RRect,
69 BorderRRect,
71 BoxShadow,
73 RingProgress,
75 Stroke,
77 Textured,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum ClipBindKind {
94 None,
96 Shape,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum TextureBindKind {
113 None,
115 TextPage(u8),
117 IconPage(IconKey),
119}
120
121#[derive(Debug, Clone, PartialEq)]
133pub enum PlannedBatch {
134 Primitives {
136 pipeline: PipelineKind,
138 clip_bind: ClipBindKind,
140 texture: TextureBindKind,
142 vertex_range: Range<u32>,
144 },
145 IsolatedComposite(PlannedIsolatedComposite),
147}
148
149#[derive(Debug, Clone)]
162pub struct PlannedLayer {
163 pub scissor: Option<Rect>,
165 pub clip_mode: ClipRenderMode,
167 pub stencil_ref: Option<u32>,
169 pub stencil_mask_range: Option<Range<u32>>,
171 pub clip_params_none: ClipParamsGpu,
173 pub clip_params_shape: Option<ClipParamsGpu>,
175 pub use_clip_alpha_for_content: bool,
177 pub batch_range: Range<usize>,
179}
180
181pub type IsolatedPass = PlannedIsolatedPass;
190
191#[derive(Debug, Default)]
201pub struct FrameRenderPlan {
202 pub vertex_arena: Vec<Vertex>,
204 pub rrect_vertex_arena: Vec<RRectVertex>,
206 pub border_vertex_arena: Vec<BorderRRectVertex>,
208 pub shadow_vertex_arena: Vec<BoxShadowVertex>,
210 pub ring_progress_vertex_arena: Vec<RingProgressVertex>,
212 pub stroke_vertex_arena: Vec<StrokeVertex>,
214 pub tex_vertex_arena: Vec<TexVertex>,
216 pub stencil_mask_arena: Vec<RRectVertex>,
218 pub composite_vertex_arena: Vec<TexVertex>,
220 pub layers: Vec<PlannedLayer>,
222 pub batches: Vec<PlannedBatch>,
224 pub planned_isolated: Vec<PlannedIsolatedPass>,
226 pub backdrop_captures: Vec<BackdropCapturePoint>,
228 pub needs_stencil_attachment: bool,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
242pub enum FramePlanError {
243 NestedDepthExceeded {
245 depth: u8,
247 max: u8,
249 },
250 EmptyIsolatedBounds {
252 layer_idx: usize,
254 },
255 TooManyIsolatedPasses {
257 count: u32,
259 max: u32,
261 },
262 OffscreenBudgetExceeded {
264 layer_idx: usize,
266 },
267}
268
269struct IsoScheduleParams {
271 parent_id: Option<OffscreenPassId>,
273 isolated_depth: u8,
275 composites_to_main: bool,
277 forced_pass_id: Option<OffscreenPassId>,
279}
280
281fn compute_backdrop_capture_rect(layer: &LayerPass<'_>, render_bounds: Rect) -> Rect {
283 let Some(scissor) = layer.clip_plan.scissor else {
284 return render_bounds;
285 };
286 let x0 = render_bounds.x.max(scissor.x);
287 let y0 = render_bounds.y.max(scissor.y);
288 let x1 = (render_bounds.x + render_bounds.w).min(scissor.x + scissor.w);
289 let y1 = (render_bounds.y + render_bounds.h).min(scissor.y + scissor.h);
290 Rect::new(x0, y0, (x1 - x0).max(1.0), (y1 - y0).max(1.0))
291}
292
293fn attach_blend_planning(
295 plan: &mut FrameRenderPlan,
296 layer: &LayerPass<'_>,
297 pass_id: OffscreenPassId,
298 render_bounds: Rect,
299 composites_to_main: bool,
300 budget: &mut IsolatedBudgetPolicy,
301) {
302 let requested = layer.effects.blend_mode;
303 let mut effective = requested;
304 let mut needs = false;
305 let mut capture_rect = None;
306
307 if composites_to_main && requested != BlendMode::Normal {
308 let rect = compute_backdrop_capture_rect(layer, render_bounds);
309 let w = rect.w.max(1.0).ceil() as u32;
310 let h = rect.h.max(1.0).ceil() as u32;
311 if budget.can_schedule_blend() && !budget.would_exceed_bytes(w, h, false) {
312 budget.record_blend_scheduled(w, h);
313 needs = true;
314 capture_rect = Some(rect);
315 } else {
316 budget.record_blend_skip();
317 effective = BlendMode::Normal;
318 }
319 }
320
321 if let Some(p) = plan.planned_isolated.iter_mut().find(|p| p.id == pass_id) {
322 p.composite_blend_mode = effective;
323 p.needs_blend_dst_capture = needs;
324 p.blend_capture_rect_px = capture_rect;
325 p.composite.blend_mode = effective;
326 }
327}
328
329fn attach_backdrop_planning(
331 plan: &mut FrameRenderPlan,
332 layer: &LayerPass<'_>,
333 layer_idx: usize,
334 pass_id: OffscreenPassId,
335 render_bounds: Rect,
336 composites_to_main: bool,
337 budget: &mut IsolatedBudgetPolicy,
338) {
339 let mut radius = layer.effects.backdrop_blur_radius_px;
340 let mut needs = false;
341 let mut capture_rect = None;
342
343 if composites_to_main && radius > 0.0 {
344 radius = budget.clamp_backdrop_radius(radius);
345 let rect = compute_backdrop_capture_rect(layer, render_bounds);
346 let w = rect.w.max(1.0).ceil() as u32;
347 let h = rect.h.max(1.0).ceil() as u32;
348 if budget.can_schedule_backdrop() && !budget.would_exceed_bytes(w, h, false) {
349 budget.record_backdrop_scheduled(w, h);
350 let split = plan.layers.len();
351 plan.backdrop_captures.push(BackdropCapturePoint {
352 pass_id,
353 source_layer_idx: layer_idx,
354 capture_rect_px: rect,
355 split_planned_layer_idx: split,
356 });
357 needs = true;
358 capture_rect = Some(rect);
359 } else {
360 budget.record_backdrop_skip();
361 }
362 }
363
364 if let Some(p) = plan.planned_isolated.iter_mut().find(|p| p.id == pass_id) {
365 p.backdrop_blur_radius_px = radius;
366 p.needs_backdrop_capture = needs;
367 p.backdrop_capture_rect_px = capture_rect;
368 }
369}
370
371fn link_child_to_parent(
373 plan: &mut FrameRenderPlan,
374 parent_id: OffscreenPassId,
375 child_id: OffscreenPassId,
376) {
377 if let Some(parent) = plan.planned_isolated.iter_mut().find(|p| p.id == parent_id) {
378 parent.child_pass_ids.push(child_id);
379 }
380}
381
382struct LayerPlanCtx {
384 surface: [f32; 2],
386 scale: Scale,
388 origin_px: Option<[f32; 2]>,
390}
391
392#[allow(clippy::too_many_arguments)]
393fn schedule_isolated_layer(
407 plan: &mut FrameRenderPlan,
408 layers: &[LayerPass<'_>],
409 layer_idx: usize,
410 prepared: &PreparedResources,
411 surface: [f32; 2],
412 scale: Scale,
413 stencil_supported: bool,
414 budget: &mut IsolatedBudgetPolicy,
415 next_iso_id: &mut u16,
416 params: IsoScheduleParams,
417) -> Result<Option<OffscreenPassId>, FramePlanError> {
418 let layer = &layers[layer_idx];
419 if !budget.can_schedule_pass() {
420 return Err(FramePlanError::TooManyIsolatedPasses {
421 count: budget.isolated_pass_count + 1,
422 max: budget.config.max_isolated_passes_per_frame,
423 });
424 }
425
426 let mut effect_chain = IsolatedEffectChain::from_effects(&layer.effects);
427 budget.clamp_blur_chain(&mut effect_chain);
428 let content_bounds = union_cmd_bounds_prepared(layer.cmds, scale, prepared)
429 .ok_or(FramePlanError::EmptyIsolatedBounds { layer_idx })?;
430 let render_bounds = inflate_for_effects(content_bounds, &effect_chain);
431 let render_bounds = budget.clamp_surface_bounds(render_bounds);
432 let (render_bounds, origin, local_size) = snap_and_clamp_bounds(render_bounds, surface);
433
434 let mut clip_mode = layer.clip_plan.clip_mode;
435 if clip_mode == ClipRenderMode::Stencil && !stencil_supported {
436 clip_mode = ClipRenderMode::ShaderMask;
437 }
438 let needs_stencil = clip_mode == ClipRenderMode::Stencil;
439
440 if budget.would_exceed_bytes(local_size[0], local_size[1], needs_stencil) {
441 budget.record_bytes_skip();
442 return Ok(None);
443 }
444
445 budget.record_pass_scheduled(local_size[0], local_size[1], needs_stencil);
446
447 let pass_id = if let Some(id) = params.forced_pass_id {
448 id
449 } else {
450 let id = OffscreenPassId(*next_iso_id);
451 *next_iso_id = next_iso_id.saturating_add(1);
452 id
453 };
454
455 plan.planned_isolated.push(PlannedIsolatedPass {
456 id: pass_id,
457 source_layer_idx: layer_idx,
458 content_bounds_px: content_bounds,
459 render_bounds_px: render_bounds,
460 content_origin_px: origin,
461 local_size_px: local_size,
462 needs_stencil,
463 clear_color: Color::new(0.0, 0.0, 0.0, 0.0),
464 effects: effect_chain,
465 composite: CompositeParams {
466 dest_rect_px: render_bounds,
467 opacity: layer.effects.opacity,
468 blend_mode: layer.effects.blend_mode,
469 },
470 parent_id: params.parent_id,
471 child_pass_ids: Vec::new(),
472 isolated_depth: params.isolated_depth,
473 composites_to_main: params.composites_to_main,
474 backdrop_blur_radius_px: 0.0,
475 backdrop_capture_rect_px: None,
476 needs_backdrop_capture: false,
477 composite_blend_mode: layer.effects.blend_mode,
478 needs_blend_dst_capture: false,
479 blend_capture_rect_px: None,
480 });
481
482 attach_backdrop_planning(
483 plan,
484 layer,
485 layer_idx,
486 pass_id,
487 render_bounds,
488 params.composites_to_main,
489 budget,
490 );
491
492 attach_blend_planning(
493 plan,
494 layer,
495 pass_id,
496 render_bounds,
497 params.composites_to_main,
498 budget,
499 );
500
501 if params.composites_to_main {
502 let tint = [1.0, 1.0, 1.0, layer.effects.opacity.clamp(0.0, 1.0)];
503 let vrange = push_composite_quad(
504 &mut plan.composite_vertex_arena,
505 surface,
506 render_bounds,
507 tint,
508 );
509 let batch_start = plan.batches.len();
510 plan.batches
511 .push(PlannedBatch::IsolatedComposite(PlannedIsolatedComposite {
512 pass_id,
513 dest_rect_px: render_bounds,
514 opacity: layer.effects.opacity,
515 blend_mode: plan
516 .planned_isolated
517 .iter()
518 .find(|p| p.id == pass_id)
519 .map(|p| p.composite_blend_mode)
520 .unwrap_or(BlendMode::Normal),
521 needs_dst_capture: plan
522 .planned_isolated
523 .iter()
524 .find(|p| p.id == pass_id)
525 .map(|p| p.needs_blend_dst_capture)
526 .unwrap_or(false),
527 dst_capture_rect_px: plan
528 .planned_isolated
529 .iter()
530 .find(|p| p.id == pass_id)
531 .and_then(|p| p.blend_capture_rect_px),
532 vertex_range: vrange,
533 }));
534 plan.layers.push(PlannedLayer {
535 scissor: Some(render_bounds),
536 clip_mode: ClipRenderMode::Scissor,
537 stencil_ref: None,
538 stencil_mask_range: None,
539 clip_params_none: ClipParamsGpu::none(),
540 clip_params_shape: None,
541 use_clip_alpha_for_content: false,
542 batch_range: batch_start..plan.batches.len(),
543 });
544 }
545
546 Ok(Some(pass_id))
547}
548
549#[allow(clippy::too_many_arguments)]
550fn flush_depth_zero_segment(
564 plan: &mut FrameRenderPlan,
565 layers: &[LayerPass<'_>],
566 segment: &mut Vec<usize>,
567 prepared: &PreparedResources,
568 surface: [f32; 2],
569 scale: Scale,
570 stencil_supported: bool,
571 budget: &mut IsolatedBudgetPolicy,
572 next_iso_id: &mut u16,
573 pass_at_depth: &mut Vec<Option<OffscreenPassId>>,
574) -> Result<(), FramePlanError> {
575 if segment.is_empty() {
576 return Ok(());
577 }
578 if segment.len() == 1 {
579 let layer_idx = segment[0];
580 let depth = layers[layer_idx].isolated_depth;
581 let parent_id = (depth > 0)
582 .then(|| pass_at_depth.get((depth - 1) as usize))
583 .flatten()
584 .copied()
585 .flatten();
586 if let Some(pass_id) = schedule_isolated_layer(
587 plan,
588 layers,
589 layer_idx,
590 prepared,
591 surface,
592 scale,
593 stencil_supported,
594 budget,
595 next_iso_id,
596 IsoScheduleParams {
597 parent_id,
598 isolated_depth: depth,
599 composites_to_main: parent_id.is_none(),
600 forced_pass_id: None,
601 },
602 )? {
603 let d = depth as usize;
604 if pass_at_depth.len() <= d {
605 pass_at_depth.resize(d + 1, None);
606 }
607 pass_at_depth[d] = Some(pass_id);
608 pass_at_depth.truncate(d + 1);
609 if let Some(pid) = parent_id {
610 link_child_to_parent(plan, pid, pass_id);
611 }
612 }
613 segment.clear();
614 return Ok(());
615 }
616
617 let n = segment.len();
618 let root_layer_idx = segment[n - 1];
619 let root_id = OffscreenPassId(*next_iso_id + (n - 1) as u16);
620 let mut child_ids = Vec::with_capacity(n - 1);
621 for (i, &child_layer_idx) in segment.iter().take(n - 1).enumerate() {
622 let child_id = OffscreenPassId(*next_iso_id + i as u16);
623 if schedule_isolated_layer(
624 plan,
625 layers,
626 child_layer_idx,
627 prepared,
628 surface,
629 scale,
630 stencil_supported,
631 budget,
632 next_iso_id,
633 IsoScheduleParams {
634 parent_id: Some(root_id),
635 isolated_depth: 1,
636 composites_to_main: false,
637 forced_pass_id: Some(child_id),
638 },
639 )?
640 .is_some()
641 {
642 child_ids.push(child_id);
643 link_child_to_parent(plan, root_id, child_id);
644 }
645 }
646 if let Some(pass_id) = schedule_isolated_layer(
647 plan,
648 layers,
649 root_layer_idx,
650 prepared,
651 surface,
652 scale,
653 stencil_supported,
654 budget,
655 next_iso_id,
656 IsoScheduleParams {
657 parent_id: None,
658 isolated_depth: 0,
659 composites_to_main: true,
660 forced_pass_id: Some(root_id),
661 },
662 )? {
663 if let Some(root_pass) = plan.planned_isolated.iter_mut().find(|p| p.id == pass_id) {
664 root_pass.child_pass_ids = child_ids;
665 }
666 pass_at_depth.clear();
667 pass_at_depth.push(Some(pass_id));
668 }
669 *next_iso_id = next_iso_id.saturating_add(n as u16);
670 segment.clear();
671 Ok(())
672}
673
674impl FrameRenderPlan {
675 pub fn build_cpu(
701 layers: &[LayerPass<'_>],
702 prepared: &PreparedResources,
703 surface: [f32; 2],
704 scale: Scale,
705 stencil_supported: bool,
706 ) -> Self {
707 Self::try_build_cpu(
708 layers,
709 prepared,
710 surface,
711 scale,
712 stencil_supported,
713 &mut IsolatedBudgetPolicy::with_defaults(),
714 )
715 .unwrap_or_else(|e| panic!("FrameRenderPlan::build_cpu: {e:?}"))
716 }
717
718 pub fn try_build_cpu(
742 layers: &[LayerPass<'_>],
743 prepared: &PreparedResources,
744 surface: [f32; 2],
745 scale: Scale,
746 stencil_supported: bool,
747 budget: &mut IsolatedBudgetPolicy,
748 ) -> Result<Self, FramePlanError> {
749 budget.reset_frame();
750 let mut plan = FrameRenderPlan::default();
751 let ctx = LayerPlanCtx {
752 surface,
753 scale,
754 origin_px: None,
755 };
756 let scale_100 = (scale.dpr * 100.0).round().clamp(1.0, u16::MAX as f32) as u16;
757
758 let mut stencil_ref_counter: u32 = 0;
759 let mut next_iso_id: u16 = 0;
760 let mut pass_at_depth: Vec<Option<OffscreenPassId>> = Vec::new();
761 let mut depth_zero_segment: Vec<usize> = Vec::new();
762
763 for (layer_idx, layer) in layers.iter().enumerate() {
764 let wants_isolated = layer.isolated && layer.effects.needs_offscreen();
765
766 if layer.isolated && !wants_isolated {
767 flush_depth_zero_segment(
768 &mut plan,
769 layers,
770 &mut depth_zero_segment,
771 prepared,
772 surface,
773 scale,
774 stencil_supported,
775 budget,
776 &mut next_iso_id,
777 &mut pass_at_depth,
778 )?;
779 } else if wants_isolated {
781 if !budget.nesting_depth_ok(layer.isolated_depth) {
782 return Err(FramePlanError::NestedDepthExceeded {
783 depth: layer.isolated_depth,
784 max: budget.config.max_isolated_nesting_depth,
785 });
786 }
787
788 if layer.isolated_depth > 0 {
789 flush_depth_zero_segment(
790 &mut plan,
791 layers,
792 &mut depth_zero_segment,
793 prepared,
794 surface,
795 scale,
796 stencil_supported,
797 budget,
798 &mut next_iso_id,
799 &mut pass_at_depth,
800 )?;
801 let depth = layer.isolated_depth;
802 let parent_id = (depth > 0)
803 .then(|| pass_at_depth.get((depth - 1) as usize))
804 .flatten()
805 .copied()
806 .flatten();
807 if let Some(pass_id) = schedule_isolated_layer(
808 &mut plan,
809 layers,
810 layer_idx,
811 prepared,
812 surface,
813 scale,
814 stencil_supported,
815 budget,
816 &mut next_iso_id,
817 IsoScheduleParams {
818 parent_id,
819 isolated_depth: depth,
820 composites_to_main: parent_id.is_none(),
821 forced_pass_id: None,
822 },
823 )? {
824 let d = depth as usize;
825 if pass_at_depth.len() <= d {
826 pass_at_depth.resize(d + 1, None);
827 }
828 pass_at_depth[d] = Some(pass_id);
829 pass_at_depth.truncate(d + 1);
830 if let Some(pid) = parent_id {
831 link_child_to_parent(&mut plan, pid, pass_id);
832 }
833 }
834 continue;
835 }
836
837 if depth_zero_segment.is_empty() {
838 let parent_id = pass_at_depth.first().copied().flatten();
839 match schedule_isolated_layer(
840 &mut plan,
841 layers,
842 layer_idx,
843 prepared,
844 surface,
845 scale,
846 stencil_supported,
847 budget,
848 &mut next_iso_id,
849 IsoScheduleParams {
850 parent_id,
851 isolated_depth: 0,
852 composites_to_main: parent_id.is_none(),
853 forced_pass_id: None,
854 },
855 )? {
856 Some(pass_id) => {
857 pass_at_depth.clear();
858 pass_at_depth.push(Some(pass_id));
859 if let Some(pid) = parent_id {
860 link_child_to_parent(&mut plan, pid, pass_id);
861 }
862 continue;
863 }
864 None => {
865 }
867 }
868 } else {
869 depth_zero_segment.push(layer_idx);
870 continue;
871 }
872 } else {
873 flush_depth_zero_segment(
874 &mut plan,
875 layers,
876 &mut depth_zero_segment,
877 prepared,
878 surface,
879 scale,
880 stencil_supported,
881 budget,
882 &mut next_iso_id,
883 &mut pass_at_depth,
884 )?;
885 }
886
887 let mut clip_mode = layer.clip_plan.clip_mode;
888 if clip_mode == ClipRenderMode::Stencil && !stencil_supported {
889 clip_mode = ClipRenderMode::ShaderMask;
890 }
891 let stencil_ref = if clip_mode == ClipRenderMode::Stencil {
892 stencil_ref_counter = stencil_ref_counter.saturating_add(1);
893 if stencil_ref_counter > 255 {
894 stencil_ref_counter = 1;
897 }
898 Some(stencil_ref_counter)
899 } else {
900 None
901 };
902
903 let stencil_mask_range = if clip_mode == ClipRenderMode::Stencil {
904 if let Some(ClipEntry {
905 shape: ClipShape::RoundRect { rect, radius },
906 ..
907 }) = layer.clip_plan.primary_round_mask
908 {
909 let start = plan.stencil_mask_arena.len() as u32;
910 push_rrect_scaled(
911 &mut plan.stencil_mask_arena,
912 surface[0],
913 surface[1],
914 scale,
915 DrawRRect {
916 rect,
917 radius,
918 color: Color::new(1.0, 1.0, 1.0, 1.0),
919 },
920 );
921 let end = plan.stencil_mask_arena.len() as u32;
922 Some(start..end)
923 } else {
924 None
925 }
926 } else {
927 None
928 };
929
930 let clip_params_none = ClipParamsGpu::none();
931 let clip_params_shape = layer
932 .clip_plan
933 .primary_round_mask
934 .map(|entry| ClipParamsGpu::from_shape(&entry.shape, scale.dpr));
935 let use_clip_alpha_for_content = matches!(clip_mode, ClipRenderMode::ShaderMask)
936 || (matches!(clip_mode, ClipRenderMode::Stencil)
937 && crate::clip::stencil_aa_enabled());
938
939 let content_clip_bind = if use_clip_alpha_for_content && clip_params_shape.is_some() {
940 ClipBindKind::Shape
941 } else {
942 ClipBindKind::None
943 };
944
945 let batch_start = plan.batches.len();
946 append_layer_draws(
947 &mut plan,
948 layer,
949 prepared,
950 &ctx,
951 scale_100,
952 content_clip_bind,
953 batch_start,
954 );
955
956 let batch_end = plan.batches.len();
957
958 plan.layers.push(PlannedLayer {
959 scissor: layer.clip_plan.scissor,
960 clip_mode,
961 stencil_ref,
962 stencil_mask_range,
963 clip_params_none,
964 clip_params_shape,
965 use_clip_alpha_for_content,
966 batch_range: batch_start..batch_end,
967 });
968
969 if clip_mode == ClipRenderMode::Stencil {
970 plan.needs_stencil_attachment = true;
971 }
972 }
973
974 flush_depth_zero_segment(
975 &mut plan,
976 layers,
977 &mut depth_zero_segment,
978 prepared,
979 surface,
980 scale,
981 stencil_supported,
982 budget,
983 &mut next_iso_id,
984 &mut pass_at_depth,
985 )?;
986
987 let _ = HashSet::<IconKey>::new();
989
990 Ok(plan)
991 }
992
993 pub fn build_isolated_subplan(
1012 layer: &LayerPass<'_>,
1013 prepared: &PreparedResources,
1014 iso: &PlannedIsolatedPass,
1015 scale: Scale,
1016 stencil_supported: bool,
1017 ) -> Self {
1018 let local_surface = [iso.local_size_px[0] as f32, iso.local_size_px[1] as f32];
1019 let mut plan = FrameRenderPlan::default();
1020 let ctx = LayerPlanCtx {
1021 surface: local_surface,
1022 scale,
1023 origin_px: Some(iso.content_origin_px),
1024 };
1025 let scale_100 = (scale.dpr * 100.0).round().clamp(1.0, u16::MAX as f32) as u16;
1026 let mut clip_mode = layer.clip_plan.clip_mode;
1027 if clip_mode == ClipRenderMode::Stencil && !stencil_supported {
1028 clip_mode = ClipRenderMode::ShaderMask;
1029 }
1030 let stencil_ref = if clip_mode == ClipRenderMode::Stencil {
1031 Some(1u32)
1032 } else {
1033 None
1034 };
1035
1036 let stencil_mask_range = if clip_mode == ClipRenderMode::Stencil {
1037 if let Some(ClipEntry {
1038 shape: ClipShape::RoundRect { rect, radius },
1039 ..
1040 }) = layer.clip_plan.primary_round_mask
1041 {
1042 let rect = translate_rect(rect, iso.content_origin_px);
1043 let start = plan.stencil_mask_arena.len() as u32;
1044 push_rrect_scaled(
1045 &mut plan.stencil_mask_arena,
1046 local_surface[0],
1047 local_surface[1],
1048 scale,
1049 DrawRRect {
1050 rect,
1051 radius,
1052 color: Color::new(1.0, 1.0, 1.0, 1.0),
1053 },
1054 );
1055 let end = plan.stencil_mask_arena.len() as u32;
1056 Some(start..end)
1057 } else {
1058 None
1059 }
1060 } else {
1061 None
1062 };
1063
1064 let clip_params_none = ClipParamsGpu::none();
1065 let clip_params_shape = layer
1066 .clip_plan
1067 .primary_round_mask
1068 .map(|entry| ClipParamsGpu::from_shape(&entry.shape, scale.dpr));
1069 let use_clip_alpha_for_content = matches!(clip_mode, ClipRenderMode::ShaderMask)
1070 || (matches!(clip_mode, ClipRenderMode::Stencil) && crate::clip::stencil_aa_enabled());
1071
1072 let content_clip_bind = if use_clip_alpha_for_content && clip_params_shape.is_some() {
1073 ClipBindKind::Shape
1074 } else {
1075 ClipBindKind::None
1076 };
1077
1078 let batch_start = plan.batches.len();
1079 append_layer_draws(
1080 &mut plan,
1081 layer,
1082 prepared,
1083 &ctx,
1084 scale_100,
1085 content_clip_bind,
1086 batch_start,
1087 );
1088
1089 let local_scissor = scissor_to_local(
1090 layer.clip_plan.scissor,
1091 iso.content_origin_px,
1092 iso.local_size_px,
1093 );
1094
1095 plan.layers.push(PlannedLayer {
1096 scissor: local_scissor,
1097 clip_mode,
1098 stencil_ref,
1099 stencil_mask_range,
1100 clip_params_none,
1101 clip_params_shape,
1102 use_clip_alpha_for_content,
1103 batch_range: batch_start..plan.batches.len(),
1104 });
1105
1106 if clip_mode == ClipRenderMode::Stencil {
1107 plan.needs_stencil_attachment = true;
1108 }
1109 plan
1110 }
1111}
1112
1113fn translate_rect(r: Rect, origin: [f32; 2]) -> Rect {
1115 Rect::new(r.x - origin[0], r.y - origin[1], r.w, r.h)
1116}
1117
1118fn translate_dr(dr: DrawRect, origin: Option<[f32; 2]>) -> DrawRect {
1120 let Some(o) = origin else {
1121 return dr;
1122 };
1123 DrawRect {
1124 rect: translate_rect(dr.rect, o),
1125 color: dr.color,
1126 }
1127}
1128
1129fn translate_rr(rr: DrawRRect, origin: Option<[f32; 2]>) -> DrawRRect {
1131 let Some(o) = origin else {
1132 return rr;
1133 };
1134 DrawRRect {
1135 rect: translate_rect(rr.rect, o),
1136 radius: rr.radius,
1137 color: rr.color,
1138 }
1139}
1140
1141fn translate_border(border: DrawBorder, origin: Option<[f32; 2]>) -> DrawBorder {
1143 let Some(o) = origin else {
1144 return border;
1145 };
1146 DrawBorder {
1147 rect: translate_rect(border.rect, o),
1148 radius: border.radius,
1149 border: border.border,
1150 }
1151}
1152
1153fn translate_box_shadow(shadow: DrawBoxShadow, origin: Option<[f32; 2]>) -> DrawBoxShadow {
1155 let Some(o) = origin else {
1156 return shadow;
1157 };
1158 DrawBoxShadow {
1159 rect: translate_rect(shadow.rect, o),
1160 radius: shadow.radius,
1161 shadow: shadow.shadow,
1162 }
1163}
1164
1165fn translate_ring_progress(ring: DrawRingProgress, origin: Option<[f32; 2]>) -> DrawRingProgress {
1167 let Some(o) = origin else {
1168 return ring;
1169 };
1170 DrawRingProgress {
1171 rect: translate_rect(ring.rect, o),
1172 thickness: ring.thickness,
1173 fraction: ring.fraction,
1174 track_color: ring.track_color,
1175 fill_color: ring.fill_color,
1176 start_angle: ring.start_angle,
1177 }
1178}
1179
1180fn translate_polyline(polyline: DrawPolyline, origin: Option<[f32; 2]>) -> DrawPolyline {
1182 let Some(o) = origin else {
1183 return polyline;
1184 };
1185 DrawPolyline {
1186 points: polyline
1187 .points
1188 .into_iter()
1189 .map(|point| Point::new(point.x - o[0], point.y - o[1]))
1190 .collect(),
1191 stroke: polyline.stroke,
1192 }
1193}
1194
1195fn translate_img(img: DrawImage, origin: Option<[f32; 2]>) -> DrawImage {
1197 let Some(o) = origin else {
1198 return img;
1199 };
1200 let mut img = img;
1201 img.rect = translate_rect(img.rect, o);
1202 img
1203}
1204
1205fn radius_is_uniform(radius: Radius) -> bool {
1209 radius.tl == radius.tr && radius.tr == radius.br && radius.br == radius.bl
1210}
1211
1212fn push_border_rect_batch(
1214 plan: &mut FrameRenderPlan,
1215 batch_start: usize,
1216 clip_bind: ClipBindKind,
1217 surface: [f32; 2],
1218 scale: Scale,
1219 rect: Rect,
1220 color: Color,
1221) {
1222 if rect.w <= 0.0 || rect.h <= 0.0 || color.a <= 0.0 {
1223 return;
1224 }
1225 let start = plan.vertex_arena.len() as u32;
1226 push_rect_scaled(
1227 &mut plan.vertex_arena,
1228 surface[0],
1229 surface[1],
1230 scale,
1231 rect,
1232 color,
1233 );
1234 let end = plan.vertex_arena.len() as u32;
1235 if end > start {
1236 push_planned_batch(
1237 &mut plan.batches,
1238 batch_start,
1239 PlannedBatch::Primitives {
1240 pipeline: PipelineKind::Rect,
1241 clip_bind,
1242 texture: TextureBindKind::None,
1243 vertex_range: start..end,
1244 },
1245 );
1246 }
1247}
1248
1249fn emit_rect_border_batches(
1251 plan: &mut FrameRenderPlan,
1252 batch_start: usize,
1253 clip_bind: ClipBindKind,
1254 surface: [f32; 2],
1255 scale: Scale,
1256 border: DrawBorder,
1257) {
1258 let rect = border.rect;
1259 let w = border.border.layout_widths();
1260 if rect.w <= 0.0 || rect.h <= 0.0 {
1261 return;
1262 }
1263
1264 let top = w.top.min(rect.h).max(0.0);
1265 let bottom = w.bottom.min((rect.h - top).max(0.0)).max(0.0);
1266 let left = w.left.min(rect.w).max(0.0);
1267 let right = w.right.min((rect.w - left).max(0.0)).max(0.0);
1268 let middle_y = rect.y + top;
1269 let middle_h = (rect.h - top - bottom).max(0.0);
1270
1271 push_border_rect_batch(
1272 plan,
1273 batch_start,
1274 clip_bind,
1275 surface,
1276 scale,
1277 Rect::new(rect.x, rect.y, rect.w, top),
1278 border.border.colors.top,
1279 );
1280 push_border_rect_batch(
1281 plan,
1282 batch_start,
1283 clip_bind,
1284 surface,
1285 scale,
1286 Rect::new(rect.x, rect.y + rect.h - bottom, rect.w, bottom),
1287 border.border.colors.bottom,
1288 );
1289 push_border_rect_batch(
1290 plan,
1291 batch_start,
1292 clip_bind,
1293 surface,
1294 scale,
1295 Rect::new(rect.x, middle_y, left, middle_h),
1296 border.border.colors.left,
1297 );
1298 push_border_rect_batch(
1299 plan,
1300 batch_start,
1301 clip_bind,
1302 surface,
1303 scale,
1304 Rect::new(rect.x + rect.w - right, middle_y, right, middle_h),
1305 border.border.colors.right,
1306 );
1307}
1308
1309fn emit_border_batches(
1311 plan: &mut FrameRenderPlan,
1312 batch_start: usize,
1313 clip_bind: ClipBindKind,
1314 surface: [f32; 2],
1315 scale: Scale,
1316 border: DrawBorder,
1317) {
1318 if !border.border.is_visible() || border.border.style != BorderStyle::Solid {
1319 return;
1320 }
1321
1322 if radius_is_uniform(border.radius) && border.radius.tl > 0.0 && border.border.is_uniform() {
1323 let Some(width) = border.border.uniform_width() else {
1324 return;
1325 };
1326 let Some(color) = border.border.uniform_color() else {
1327 return;
1328 };
1329 if width <= 0.0 || color.a <= 0.0 {
1330 return;
1331 }
1332 let start = plan.border_vertex_arena.len() as u32;
1333 push_border_rrect_scaled(
1334 &mut plan.border_vertex_arena,
1335 surface[0],
1336 surface[1],
1337 scale,
1338 border,
1339 width,
1340 color,
1341 );
1342 let end = plan.border_vertex_arena.len() as u32;
1343 if end > start {
1344 push_planned_batch(
1345 &mut plan.batches,
1346 batch_start,
1347 PlannedBatch::Primitives {
1348 pipeline: PipelineKind::BorderRRect,
1349 clip_bind,
1350 texture: TextureBindKind::None,
1351 vertex_range: start..end,
1352 },
1353 );
1354 }
1355 return;
1356 }
1357
1358 emit_rect_border_batches(plan, batch_start, clip_bind, surface, scale, border);
1359}
1360
1361#[allow(clippy::too_many_arguments)]
1362fn append_layer_draws(
1364 plan: &mut FrameRenderPlan,
1365 layer: &LayerPass<'_>,
1366 prepared: &PreparedResources,
1367 ctx: &LayerPlanCtx,
1368 scale_100: u16,
1369 content_clip_bind: ClipBindKind,
1370 batch_start: usize,
1371) {
1372 let [w, h] = ctx.surface;
1373 let scale = ctx.scale;
1374 let origin = ctx.origin_px;
1375
1376 for cmd in layer.cmds {
1377 match cmd {
1378 DrawCmd::Rect(dr) => {
1379 let dr = translate_dr(*dr, origin);
1380 let start = plan.vertex_arena.len() as u32;
1381 push_rect_scaled(&mut plan.vertex_arena, w, h, scale, dr.rect, dr.color);
1382 let end = plan.vertex_arena.len() as u32;
1383 if end > start {
1384 push_planned_batch(
1385 &mut plan.batches,
1386 batch_start,
1387 PlannedBatch::Primitives {
1388 pipeline: PipelineKind::Rect,
1389 clip_bind: content_clip_bind,
1390 texture: TextureBindKind::None,
1391 vertex_range: start..end,
1392 },
1393 );
1394 }
1395 }
1396 DrawCmd::RRect(rr) => {
1397 let rr = translate_rr(*rr, origin);
1398 let start = plan.rrect_vertex_arena.len() as u32;
1399 push_rrect_scaled(&mut plan.rrect_vertex_arena, w, h, scale, rr);
1400 let end = plan.rrect_vertex_arena.len() as u32;
1401 if end > start {
1402 push_planned_batch(
1403 &mut plan.batches,
1404 batch_start,
1405 PlannedBatch::Primitives {
1406 pipeline: PipelineKind::RRect,
1407 clip_bind: content_clip_bind,
1408 texture: TextureBindKind::None,
1409 vertex_range: start..end,
1410 },
1411 );
1412 }
1413 }
1414 DrawCmd::Border(border) => {
1415 let border = translate_border(*border, origin);
1416 emit_border_batches(
1417 plan,
1418 batch_start,
1419 content_clip_bind,
1420 ctx.surface,
1421 scale,
1422 border,
1423 );
1424 }
1425 DrawCmd::BoxShadow(shadow) => {
1426 let shadow = translate_box_shadow(*shadow, origin);
1427 let start = plan.shadow_vertex_arena.len() as u32;
1428 push_box_shadow_scaled(&mut plan.shadow_vertex_arena, w, h, scale, shadow);
1429 let end = plan.shadow_vertex_arena.len() as u32;
1430 if end > start {
1431 push_planned_batch(
1432 &mut plan.batches,
1433 batch_start,
1434 PlannedBatch::Primitives {
1435 pipeline: PipelineKind::BoxShadow,
1436 clip_bind: content_clip_bind,
1437 texture: TextureBindKind::None,
1438 vertex_range: start..end,
1439 },
1440 );
1441 }
1442 }
1443 DrawCmd::RingProgress(ring) => {
1444 let ring = translate_ring_progress(*ring, origin);
1445 let start = plan.ring_progress_vertex_arena.len() as u32;
1446 push_ring_progress_scaled(&mut plan.ring_progress_vertex_arena, w, h, scale, ring);
1447 let end = plan.ring_progress_vertex_arena.len() as u32;
1448 if end > start {
1449 push_planned_batch(
1450 &mut plan.batches,
1451 batch_start,
1452 PlannedBatch::Primitives {
1453 pipeline: PipelineKind::RingProgress,
1454 clip_bind: content_clip_bind,
1455 texture: TextureBindKind::None,
1456 vertex_range: start..end,
1457 },
1458 );
1459 }
1460 }
1461 DrawCmd::Polyline(polyline) => {
1462 let polyline = translate_polyline(polyline.clone(), origin);
1463 let start = plan.stroke_vertex_arena.len() as u32;
1464 push_polyline_scaled(&mut plan.stroke_vertex_arena, w, h, scale, &polyline);
1465 let end = plan.stroke_vertex_arena.len() as u32;
1466 if end > start {
1467 push_planned_batch(
1468 &mut plan.batches,
1469 batch_start,
1470 PlannedBatch::Primitives {
1471 pipeline: PipelineKind::Stroke,
1472 clip_bind: content_clip_bind,
1473 texture: TextureBindKind::None,
1474 vertex_range: start..end,
1475 },
1476 );
1477 }
1478 }
1479 DrawCmd::Text(dt) => {
1480 for rect in dt.decoration_rects(scale.dpr) {
1481 let rect = origin.map_or(rect, |origin| {
1482 translate_rect(rect, [origin[0] / scale.dpr, origin[1] / scale.dpr])
1483 });
1484 push_border_rect_batch(
1485 plan,
1486 batch_start,
1487 content_clip_bind,
1488 ctx.surface,
1489 scale,
1490 rect,
1491 dt.color,
1492 );
1493 }
1494 emit_text_batches_local(
1495 &mut plan.tex_vertex_arena,
1496 &mut plan.batches,
1497 batch_start,
1498 content_clip_bind,
1499 prepared,
1500 w,
1501 h,
1502 scale,
1503 scale_100,
1504 dt,
1505 origin,
1506 );
1507 }
1508 DrawCmd::Image(img) => {
1509 let img = translate_img(img.clone(), origin);
1510 let physical_px_size = img.rect.w.max(img.rect.h) * scale.dpr;
1511 let key = IconKey {
1512 icon: img.icon.clone(),
1513 px_size: physical_px_size.round().clamp(8.0, 256.0) as u16,
1514 scale_100,
1515 };
1516 if !prepared.icons.contains(&key) {
1517 continue;
1518 }
1519 let start = plan.tex_vertex_arena.len() as u32;
1520 plan.tex_vertex_arena
1521 .extend_from_slice(&make_tex_rect_scaled(w, h, scale, img));
1522 let end = plan.tex_vertex_arena.len() as u32;
1523 if end > start {
1524 push_planned_batch(
1525 &mut plan.batches,
1526 batch_start,
1527 PlannedBatch::Primitives {
1528 pipeline: PipelineKind::Textured,
1529 clip_bind: content_clip_bind,
1530 texture: TextureBindKind::IconPage(key),
1531 vertex_range: start..end,
1532 },
1533 );
1534 }
1535 }
1536 }
1537 }
1538}
1539
1540#[allow(clippy::too_many_arguments)]
1541#[allow(dead_code)]
1542fn emit_text_batches(
1544 arena: &mut Vec<TexVertex>,
1545 batches: &mut Vec<PlannedBatch>,
1546 layer_batch_start: usize,
1547 clip_bind: ClipBindKind,
1548 prepared: &PreparedResources,
1549 w: f32,
1550 h: f32,
1551 scale: Scale,
1552 scale_100: u16,
1553 dt: &ailloli_ui_runtime::DrawText,
1554) {
1555 let (origin_x, origin_y) = text_origin_from_baseline(dt);
1556 let fallback_color = dt.color;
1557 let mut current_page: Option<u8> = None;
1558 let mut current_start: u32 = arena.len() as u32;
1559
1560 for gi in dt.layout.glyphs() {
1561 let color = gi.color.unwrap_or(fallback_color).to_array();
1562 let physical_px_size = ((gi.px_size as f32) * scale.dpr).round();
1563 let key = GlyphKey {
1564 face_id: gi.face_id,
1565 font_index: gi.font_index,
1566 px_size: physical_px_size.clamp(8.0, 128.0) as u16,
1567 glyph_id: gi.glyph_id,
1568 scale_100,
1569 };
1570 let Some(&(page_idx, g)) = prepared.glyphs.get(&key) else {
1571 continue;
1572 };
1573 if g.size_px[0] <= 0.0 || g.size_px[1] <= 0.0 {
1574 continue;
1575 }
1576
1577 if Some(page_idx) != current_page {
1578 if let Some(p) = current_page {
1579 let end = arena.len() as u32;
1580 if end > current_start {
1581 push_planned_batch(
1582 batches,
1583 layer_batch_start,
1584 PlannedBatch::Primitives {
1585 pipeline: PipelineKind::Textured,
1586 clip_bind,
1587 texture: TextureBindKind::TextPage(p),
1588 vertex_range: current_start..end,
1589 },
1590 );
1591 }
1592 }
1593 current_page = Some(page_idx);
1594 current_start = arena.len() as u32;
1595 }
1596
1597 let pen_x = (origin_x + gi.x) * scale.dpr;
1598 let pen_y = (origin_y + gi.y) * scale.dpr;
1599 let x0 = (pen_x + g.offset_px[0]).round();
1600 let y0 = (pen_y + g.offset_px[1]).round();
1601 let x1 = x0 + g.size_px[0];
1602 let y1 = y0 + g.size_px[1];
1603
1604 let p0 = to_ndc(w, h, x0, y0);
1605 let p1 = to_ndc(w, h, x1, y0);
1606 let p2 = to_ndc(w, h, x1, y1);
1607 let p3 = to_ndc(w, h, x0, y1);
1608
1609 let uv0 = g.uv_min;
1610 let uv2 = g.uv_max;
1611 let uv1 = [uv2[0], uv0[1]];
1612 let uv3 = [uv0[0], uv2[1]];
1613
1614 arena.push(TexVertex {
1615 pos: p0,
1616 uv: uv0,
1617 tint: color,
1618 });
1619 arena.push(TexVertex {
1620 pos: p1,
1621 uv: uv1,
1622 tint: color,
1623 });
1624 arena.push(TexVertex {
1625 pos: p2,
1626 uv: uv2,
1627 tint: color,
1628 });
1629 arena.push(TexVertex {
1630 pos: p0,
1631 uv: uv0,
1632 tint: color,
1633 });
1634 arena.push(TexVertex {
1635 pos: p2,
1636 uv: uv2,
1637 tint: color,
1638 });
1639 arena.push(TexVertex {
1640 pos: p3,
1641 uv: uv3,
1642 tint: color,
1643 });
1644 }
1645
1646 if let Some(p) = current_page {
1647 let end = arena.len() as u32;
1648 if end > current_start {
1649 push_planned_batch(
1650 batches,
1651 layer_batch_start,
1652 PlannedBatch::Primitives {
1653 pipeline: PipelineKind::Textured,
1654 clip_bind,
1655 texture: TextureBindKind::TextPage(p),
1656 vertex_range: current_start..end,
1657 },
1658 );
1659 }
1660 }
1661}
1662
1663fn push_planned_batch(
1668 batches: &mut Vec<PlannedBatch>,
1669 layer_batch_start: usize,
1670 next: PlannedBatch,
1671) {
1672 let PlannedBatch::Primitives {
1673 pipeline,
1674 clip_bind,
1675 texture,
1676 vertex_range,
1677 } = next
1678 else {
1679 batches.push(next);
1680 return;
1681 };
1682 if batches.len() > layer_batch_start {
1683 if let Some(PlannedBatch::Primitives {
1684 pipeline: lp,
1685 clip_bind: lc,
1686 texture: lt,
1687 vertex_range: ref mut lr,
1688 }) = batches.last_mut()
1689 {
1690 if *lp == pipeline && *lc == clip_bind && *lt == texture && lr.end == vertex_range.start
1691 {
1692 lr.end = vertex_range.end;
1693 return;
1694 }
1695 }
1696 }
1697 batches.push(PlannedBatch::Primitives {
1698 pipeline,
1699 clip_bind,
1700 texture,
1701 vertex_range,
1702 });
1703}
1704
1705#[allow(clippy::too_many_arguments)]
1706fn emit_text_batches_local(
1708 arena: &mut Vec<TexVertex>,
1709 batches: &mut Vec<PlannedBatch>,
1710 layer_batch_start: usize,
1711 clip_bind: ClipBindKind,
1712 prepared: &PreparedResources,
1713 w: f32,
1714 h: f32,
1715 scale: Scale,
1716 scale_100: u16,
1717 dt: &ailloli_ui_runtime::DrawText,
1718 origin: Option<[f32; 2]>,
1719) {
1720 let (origin_x, origin_y) = text_origin_from_baseline(dt);
1721 let ox = origin.map(|o| o[0]).unwrap_or(0.0);
1722 let oy = origin.map(|o| o[1]).unwrap_or(0.0);
1723 let fallback_color = dt.color;
1724 let mut current_page: Option<u8> = None;
1725 let mut current_start: u32 = arena.len() as u32;
1726
1727 for gi in dt.layout.glyphs() {
1728 let color = gi.color.unwrap_or(fallback_color).to_array();
1729 let physical_px_size = ((gi.px_size as f32) * scale.dpr).round();
1730 let key = GlyphKey {
1731 face_id: gi.face_id,
1732 font_index: gi.font_index,
1733 px_size: physical_px_size.clamp(8.0, 128.0) as u16,
1734 glyph_id: gi.glyph_id,
1735 scale_100,
1736 };
1737 let Some(&(page_idx, g)) = prepared.glyphs.get(&key) else {
1738 continue;
1739 };
1740 if g.size_px[0] <= 0.0 || g.size_px[1] <= 0.0 {
1741 continue;
1742 }
1743
1744 if Some(page_idx) != current_page {
1745 if let Some(p) = current_page {
1746 let end = arena.len() as u32;
1747 if end > current_start {
1748 push_planned_batch(
1749 batches,
1750 layer_batch_start,
1751 PlannedBatch::Primitives {
1752 pipeline: PipelineKind::Textured,
1753 clip_bind,
1754 texture: TextureBindKind::TextPage(p),
1755 vertex_range: current_start..end,
1756 },
1757 );
1758 }
1759 }
1760 current_page = Some(page_idx);
1761 current_start = arena.len() as u32;
1762 }
1763
1764 let pen_x = (origin_x + gi.x) * scale.dpr - ox;
1765 let pen_y = (origin_y + gi.y) * scale.dpr - oy;
1766 let x0 = (pen_x + g.offset_px[0]).round();
1767 let y0 = (pen_y + g.offset_px[1]).round();
1768 let x1 = x0 + g.size_px[0];
1769 let y1 = y0 + g.size_px[1];
1770
1771 let p0 = to_ndc(w, h, x0, y0);
1772 let p1 = to_ndc(w, h, x1, y0);
1773 let p2 = to_ndc(w, h, x1, y1);
1774 let p3 = to_ndc(w, h, x0, y1);
1775
1776 let uv0 = g.uv_min;
1777 let uv2 = g.uv_max;
1778 let uv1 = [uv2[0], uv0[1]];
1779 let uv3 = [uv0[0], uv2[1]];
1780
1781 arena.push(TexVertex {
1782 pos: p0,
1783 uv: uv0,
1784 tint: color,
1785 });
1786 arena.push(TexVertex {
1787 pos: p1,
1788 uv: uv1,
1789 tint: color,
1790 });
1791 arena.push(TexVertex {
1792 pos: p2,
1793 uv: uv2,
1794 tint: color,
1795 });
1796 arena.push(TexVertex {
1797 pos: p0,
1798 uv: uv0,
1799 tint: color,
1800 });
1801 arena.push(TexVertex {
1802 pos: p2,
1803 uv: uv2,
1804 tint: color,
1805 });
1806 arena.push(TexVertex {
1807 pos: p3,
1808 uv: uv3,
1809 tint: color,
1810 });
1811 }
1812
1813 if let Some(p) = current_page {
1814 let end = arena.len() as u32;
1815 if end > current_start {
1816 push_planned_batch(
1817 batches,
1818 layer_batch_start,
1819 PlannedBatch::Primitives {
1820 pipeline: PipelineKind::Textured,
1821 clip_bind,
1822 texture: TextureBindKind::TextPage(p),
1823 vertex_range: current_start..end,
1824 },
1825 );
1826 }
1827 }
1828}
1829
1830#[cfg(test)]
1831mod tests {
1834 use super::*;
1835 use crate::isolated_budget::IsolatedBudgetPolicy;
1836 use ailloli_ui_core::{
1837 Border, BorderStyle, BoxShadow, EdgeColors, EdgeInsets, FontId, IconId, Point, Radius,
1838 StrokeStyle, TextDecoration, TextStyle,
1839 };
1840 use ailloli_ui_runtime::scene::ClipStackSnapshot;
1841 use ailloli_ui_runtime::BlendMode;
1842 use ailloli_ui_runtime::DrawBorder;
1843 use ailloli_ui_runtime::DrawBoxShadow;
1844 use ailloli_ui_runtime::DrawPolyline;
1845 use ailloli_ui_runtime::DrawRect;
1846 use ailloli_ui_runtime::DrawRingProgress;
1847 use ailloli_ui_runtime::DrawText;
1848 use ailloli_ui_text::{TextLayoutParams, TextSystem, WrapMode};
1849
1850 fn batch_range(b: &PlannedBatch) -> std::ops::Range<u32> {
1852 match b {
1853 PlannedBatch::Primitives { vertex_range, .. } => vertex_range.clone(),
1854 PlannedBatch::IsolatedComposite(c) => c.vertex_range.clone(),
1855 }
1856 }
1857
1858 fn batch_texture(b: &PlannedBatch) -> TextureBindKind {
1860 match b {
1861 PlannedBatch::Primitives { texture, .. } => texture.clone(),
1862 PlannedBatch::IsolatedComposite(_) => TextureBindKind::None,
1863 }
1864 }
1865
1866 fn batch_pipeline(b: &PlannedBatch) -> Option<PipelineKind> {
1868 match b {
1869 PlannedBatch::Primitives { pipeline, .. } => Some(*pipeline),
1870 PlannedBatch::IsolatedComposite(_) => None,
1871 }
1872 }
1873
1874 fn make_rect_layer<'a>(cmds: &'a [DrawCmd], clip: Option<ClipShape>) -> LayerPass<'a> {
1876 match clip {
1877 Some(c) => LayerPass::with_clip(cmds, c),
1878 None => LayerPass::new(cmds),
1879 }
1880 }
1881
1882 fn empty_prepared() -> PreparedResources {
1884 PreparedResources::default()
1885 }
1886
1887 #[test]
1888 fn underlined_wrapped_text_emits_one_rect_per_visual_line() {
1889 let mut text_system = TextSystem::new();
1890 let style = TextStyle::new(FontId::Ui, 14, Color::WHITE).underline();
1891 let layout = text_system.layout_cached(TextLayoutParams {
1892 text: "one two three four",
1893 style,
1894 max_width: Some(35.0),
1895 wrap_mode: WrapMode::WordOrAnywhere,
1896 });
1897 assert!(layout.lines.len() > 1);
1898 let expected_vertices = layout.lines.len() * 6;
1899 let baseline = layout.lines[0].baseline_y;
1900 let cmds = vec![DrawCmd::Text(DrawText {
1901 pos: [4.0, 4.0 + baseline],
1902 color: style.color,
1903 decoration: TextDecoration::Underline,
1904 layout,
1905 })];
1906 let layers = vec![LayerPass::new(&cmds)];
1907
1908 let plan = FrameRenderPlan::build_cpu(
1909 &layers,
1910 &empty_prepared(),
1911 [160.0, 120.0],
1912 Scale::new(2.0),
1913 true,
1914 );
1915
1916 assert_eq!(plan.vertex_arena.len(), expected_vertices);
1917 assert!(plan
1918 .batches
1919 .iter()
1920 .any(|batch| batch_pipeline(batch) == Some(PipelineKind::Rect)));
1921 }
1922
1923 #[test]
1924 fn arena_ranges_do_not_overlap_across_layers() {
1925 let l1 = vec![
1926 DrawCmd::Rect(DrawRect {
1927 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
1928 color: Color::new(1.0, 0.0, 0.0, 1.0),
1929 }),
1930 DrawCmd::Rect(DrawRect {
1931 rect: Rect::new(20.0, 0.0, 10.0, 10.0),
1932 color: Color::new(0.0, 1.0, 0.0, 1.0),
1933 }),
1934 ];
1935 let l2 = vec![
1936 DrawCmd::Rect(DrawRect {
1937 rect: Rect::new(40.0, 0.0, 10.0, 10.0),
1938 color: Color::new(0.0, 0.0, 1.0, 1.0),
1939 }),
1940 DrawCmd::Rect(DrawRect {
1941 rect: Rect::new(60.0, 0.0, 10.0, 10.0),
1942 color: Color::new(1.0, 1.0, 0.0, 1.0),
1943 }),
1944 ];
1945 let l3 = vec![
1946 DrawCmd::Rect(DrawRect {
1947 rect: Rect::new(80.0, 0.0, 10.0, 10.0),
1948 color: Color::new(0.0, 1.0, 1.0, 1.0),
1949 }),
1950 DrawCmd::Rect(DrawRect {
1951 rect: Rect::new(100.0, 0.0, 10.0, 10.0),
1952 color: Color::new(1.0, 0.0, 1.0, 1.0),
1953 }),
1954 ];
1955 let layers = vec![
1956 make_rect_layer(&l1, None),
1957 make_rect_layer(&l2, None),
1958 make_rect_layer(&l3, None),
1959 ];
1960
1961 let plan = FrameRenderPlan::build_cpu(
1962 &layers,
1963 &empty_prepared(),
1964 [128.0, 128.0],
1965 Scale::new(1.0),
1966 true,
1967 );
1968
1969 assert_eq!(plan.vertex_arena.len(), 36);
1971 assert_eq!(plan.batches.len(), 3);
1973 assert_eq!(plan.layers.len(), 3);
1974
1975 assert_eq!(batch_range(&plan.batches[0]), 0..12);
1977 assert_eq!(batch_range(&plan.batches[1]), 12..24);
1978 assert_eq!(batch_range(&plan.batches[2]), 24..36);
1979 assert!(!plan.needs_stencil_attachment);
1980 assert!(plan.planned_isolated.is_empty());
1981 }
1982
1983 #[test]
1984 fn batches_keep_intra_layer_merge_when_compatible() {
1985 let l1 = vec![
1986 DrawCmd::Rect(DrawRect {
1987 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
1988 color: Color::new(1.0, 0.0, 0.0, 1.0),
1989 }),
1990 DrawCmd::Rect(DrawRect {
1991 rect: Rect::new(20.0, 0.0, 10.0, 10.0),
1992 color: Color::new(0.0, 1.0, 0.0, 1.0),
1993 }),
1994 ];
1995 let layers = vec![make_rect_layer(&l1, None)];
1996 let plan = FrameRenderPlan::build_cpu(
1997 &layers,
1998 &empty_prepared(),
1999 [64.0, 64.0],
2000 Scale::new(1.0),
2001 true,
2002 );
2003 assert_eq!(plan.batches.len(), 1);
2004 assert_eq!(batch_range(&plan.batches[0]), 0..12);
2005 }
2006
2007 #[test]
2008 fn rect_border_lowers_to_four_rect_quads() {
2009 let cmds = vec![DrawCmd::Border(DrawBorder {
2010 rect: Rect::new(4.0, 6.0, 30.0, 20.0),
2011 radius: Radius::zero(),
2012 border: Border {
2013 widths: EdgeInsets::new(1.0, 2.0, 3.0, 4.0),
2014 colors: EdgeColors::all(Color::WHITE),
2015 style: BorderStyle::Solid,
2016 },
2017 })];
2018 let layers = vec![make_rect_layer(&cmds, None)];
2019
2020 let plan = FrameRenderPlan::build_cpu(
2021 &layers,
2022 &empty_prepared(),
2023 [64.0, 64.0],
2024 Scale::new(1.0),
2025 true,
2026 );
2027
2028 assert_eq!(plan.vertex_arena.len(), 24);
2029 assert!(plan.border_vertex_arena.is_empty());
2030 assert_eq!(plan.batches.len(), 1);
2031 assert_eq!(batch_pipeline(&plan.batches[0]), Some(PipelineKind::Rect));
2032 }
2033
2034 #[test]
2035 fn rounded_uniform_border_uses_sdf_ring_pipeline() {
2036 let cmds = vec![DrawCmd::Border(DrawBorder {
2037 rect: Rect::new(4.0, 6.0, 30.0, 20.0),
2038 radius: Radius::uniform(8.0),
2039 border: Border::new(2.0, Color::WHITE),
2040 })];
2041 let layers = vec![make_rect_layer(&cmds, None)];
2042
2043 let plan = FrameRenderPlan::build_cpu(
2044 &layers,
2045 &empty_prepared(),
2046 [64.0, 64.0],
2047 Scale::new(1.0),
2048 true,
2049 );
2050
2051 assert!(plan.vertex_arena.is_empty());
2052 assert_eq!(plan.border_vertex_arena.len(), 6);
2053 assert_eq!(plan.batches.len(), 1);
2054 assert_eq!(
2055 batch_pipeline(&plan.batches[0]),
2056 Some(PipelineKind::BorderRRect)
2057 );
2058 }
2059
2060 #[test]
2061 fn box_shadow_uses_shadow_pipeline_and_arena() {
2062 let cmds = vec![DrawCmd::BoxShadow(DrawBoxShadow {
2063 rect: Rect::new(10.0, 12.0, 30.0, 20.0),
2064 radius: Radius::uniform(6.0),
2065 shadow: BoxShadow::new(0.0, 4.0, 8.0, 2.0, Color::BLACK),
2066 })];
2067 let layers = vec![make_rect_layer(&cmds, None)];
2068
2069 let plan = FrameRenderPlan::build_cpu(
2070 &layers,
2071 &empty_prepared(),
2072 [96.0, 96.0],
2073 Scale::new(1.0),
2074 true,
2075 );
2076
2077 assert!(plan.vertex_arena.is_empty());
2078 assert_eq!(plan.shadow_vertex_arena.len(), 6);
2079 assert_eq!(plan.batches.len(), 1);
2080 assert_eq!(
2081 batch_pipeline(&plan.batches[0]),
2082 Some(PipelineKind::BoxShadow)
2083 );
2084 }
2085
2086 #[test]
2087 fn ring_progress_uses_ring_pipeline_and_arena() {
2088 let cmds = vec![DrawCmd::RingProgress(DrawRingProgress {
2089 rect: Rect::new(10.0, 12.0, 40.0, 40.0),
2090 thickness: 6.0,
2091 fraction: 0.66,
2092 track_color: Color::new(0.2, 0.2, 0.2, 1.0),
2093 fill_color: Color::new(1.0, 0.35, 0.0, 1.0),
2094 start_angle: -std::f32::consts::FRAC_PI_2,
2095 })];
2096 let layers = vec![make_rect_layer(&cmds, None)];
2097
2098 let plan = FrameRenderPlan::build_cpu(
2099 &layers,
2100 &empty_prepared(),
2101 [96.0, 96.0],
2102 Scale::new(1.0),
2103 true,
2104 );
2105
2106 assert!(plan.vertex_arena.is_empty());
2107 assert_eq!(plan.ring_progress_vertex_arena.len(), 6);
2108 assert_eq!(plan.batches.len(), 1);
2109 assert_eq!(
2110 batch_pipeline(&plan.batches[0]),
2111 Some(PipelineKind::RingProgress)
2112 );
2113 }
2114
2115 #[test]
2116 fn polyline_under_window_root_stencil_has_mask_and_vertices() {
2117 let cmds = vec![DrawCmd::Polyline(DrawPolyline {
2118 points: vec![Point::new(20.0, 100.0), Point::new(300.0, 100.0)],
2119 stroke: StrokeStyle::new(3.0, Color::WHITE),
2120 })];
2121 let root = ClipShape::RoundRect {
2122 rect: Rect::new(0.0, 0.0, 320.0, 200.0),
2123 radius: 10.0,
2124 };
2125 let layers = vec![LayerPass::with_window_root_clip(&cmds, root)];
2126
2127 let plan = FrameRenderPlan::build_cpu(
2128 &layers,
2129 &empty_prepared(),
2130 [320.0, 200.0],
2131 Scale::new(1.0),
2132 true,
2133 );
2134
2135 assert_eq!(plan.layers.len(), 1);
2136 let layer = &plan.layers[0];
2137 assert_eq!(layer.clip_mode, ClipRenderMode::Stencil);
2138 assert!(layer.stencil_mask_range.is_some());
2139 assert!(!plan.stroke_vertex_arena.is_empty());
2140 assert_eq!(batch_pipeline(&plan.batches[0]), Some(PipelineKind::Stroke));
2141
2142 for v in &plan.stroke_vertex_arena {
2143 assert!(
2144 v.pos[0].is_finite()
2145 && v.pos[1].is_finite()
2146 && v.pos[0] >= -1.05
2147 && v.pos[0] <= 1.05
2148 && v.pos[1] >= -1.05
2149 && v.pos[1] <= 1.05,
2150 "stroke vertex ndc out of range: {:?}",
2151 v.pos
2152 );
2153 }
2154 }
2155
2156 #[test]
2157 fn polyline_uses_stroke_pipeline_and_arena() {
2158 let cmds = vec![DrawCmd::Polyline(DrawPolyline {
2159 points: vec![Point::new(10.0, 12.0), Point::new(40.0, 30.0)],
2160 stroke: StrokeStyle::new(3.0, Color::WHITE),
2161 })];
2162 let layers = vec![make_rect_layer(&cmds, None)];
2163
2164 let plan = FrameRenderPlan::build_cpu(
2165 &layers,
2166 &empty_prepared(),
2167 [96.0, 96.0],
2168 Scale::new(1.0),
2169 true,
2170 );
2171
2172 assert!(plan.vertex_arena.is_empty());
2173 assert!(!plan.stroke_vertex_arena.is_empty());
2174 assert_eq!(plan.batches.len(), 1);
2175 assert_eq!(batch_pipeline(&plan.batches[0]), Some(PipelineKind::Stroke));
2176 }
2177
2178 #[test]
2179 fn batches_do_not_merge_across_layers() {
2180 let l1 = vec![DrawCmd::Rect(DrawRect {
2181 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
2182 color: Color::new(1.0, 0.0, 0.0, 1.0),
2183 })];
2184 let l2 = vec![DrawCmd::Rect(DrawRect {
2185 rect: Rect::new(20.0, 0.0, 10.0, 10.0),
2186 color: Color::new(0.0, 1.0, 0.0, 1.0),
2187 })];
2188 let layers = vec![make_rect_layer(&l1, None), make_rect_layer(&l2, None)];
2189 let plan = FrameRenderPlan::build_cpu(
2190 &layers,
2191 &empty_prepared(),
2192 [64.0, 64.0],
2193 Scale::new(1.0),
2194 true,
2195 );
2196 assert_eq!(plan.batches.len(), 2);
2197 assert_eq!(batch_range(&plan.batches[0]), 0..6);
2198 assert_eq!(batch_range(&plan.batches[1]), 6..12);
2199 }
2200
2201 #[test]
2202 fn batches_do_not_merge_when_texture_differs() {
2203 let mut batches: Vec<PlannedBatch> = Vec::new();
2204 push_planned_batch(
2205 &mut batches,
2206 0,
2207 PlannedBatch::Primitives {
2208 pipeline: PipelineKind::Textured,
2209 clip_bind: ClipBindKind::None,
2210 texture: TextureBindKind::TextPage(0),
2211 vertex_range: 0..6,
2212 },
2213 );
2214 push_planned_batch(
2215 &mut batches,
2216 0,
2217 PlannedBatch::Primitives {
2218 pipeline: PipelineKind::Textured,
2219 clip_bind: ClipBindKind::None,
2220 texture: TextureBindKind::TextPage(1),
2221 vertex_range: 6..12,
2222 },
2223 );
2224 assert_eq!(batches.len(), 2);
2225 assert_eq!(batch_texture(&batches[0]), TextureBindKind::TextPage(0));
2226 assert_eq!(batch_texture(&batches[1]), TextureBindKind::TextPage(1));
2227 }
2228
2229 #[test]
2230 fn batches_do_not_merge_when_icon_differs() {
2231 let mut batches: Vec<PlannedBatch> = Vec::new();
2232 let k1 = IconKey {
2233 icon: IconId::Plus,
2234 px_size: 16,
2235 scale_100: 100,
2236 };
2237 let k2 = IconKey {
2238 icon: IconId::Trash,
2239 px_size: 16,
2240 scale_100: 100,
2241 };
2242 push_planned_batch(
2243 &mut batches,
2244 0,
2245 PlannedBatch::Primitives {
2246 pipeline: PipelineKind::Textured,
2247 clip_bind: ClipBindKind::None,
2248 texture: TextureBindKind::IconPage(k1.clone()),
2249 vertex_range: 0..6,
2250 },
2251 );
2252 push_planned_batch(
2253 &mut batches,
2254 0,
2255 PlannedBatch::Primitives {
2256 pipeline: PipelineKind::Textured,
2257 clip_bind: ClipBindKind::None,
2258 texture: TextureBindKind::IconPage(k2.clone()),
2259 vertex_range: 6..12,
2260 },
2261 );
2262 assert_eq!(batches.len(), 2);
2263 assert_eq!(batch_texture(&batches[0]), TextureBindKind::IconPage(k1));
2264 assert_eq!(batch_texture(&batches[1]), TextureBindKind::IconPage(k2));
2265 }
2266
2267 #[test]
2268 fn intra_layer_fusion_does_not_cross_layer_boundary_via_layer_batch_start() {
2269 let mut batches: Vec<PlannedBatch> = Vec::new();
2270 push_planned_batch(
2272 &mut batches,
2273 0,
2274 PlannedBatch::Primitives {
2275 pipeline: PipelineKind::Rect,
2276 clip_bind: ClipBindKind::None,
2277 texture: TextureBindKind::None,
2278 vertex_range: 0..6,
2279 },
2280 );
2281 let layer_b_start = batches.len();
2283 push_planned_batch(
2285 &mut batches,
2286 layer_b_start,
2287 PlannedBatch::Primitives {
2288 pipeline: PipelineKind::Rect,
2289 clip_bind: ClipBindKind::None,
2290 texture: TextureBindKind::None,
2291 vertex_range: 6..12,
2292 },
2293 );
2294 assert_eq!(
2295 batches.len(),
2296 2,
2297 "fusion must not cross layer boundary (layer_batch_start)"
2298 );
2299 }
2300
2301 #[test]
2302 fn stencil_mask_emitted_before_content_for_layer() {
2303 let l1 = vec![DrawCmd::Rect(DrawRect {
2304 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
2305 color: Color::new(0.5, 0.5, 0.5, 1.0),
2306 })];
2307 let root_round = ClipShape::RoundRect {
2308 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
2309 radius: 20.0,
2310 };
2311 let snap = ClipStackSnapshot::from_clip(Some(root_round), true);
2313 let layers = vec![LayerPass::with_clip_stack(&l1, snap)];
2314
2315 let plan = FrameRenderPlan::build_cpu(
2316 &layers,
2317 &empty_prepared(),
2318 [128.0, 128.0],
2319 Scale::new(1.0),
2320 true,
2321 );
2322
2323 assert_eq!(plan.layers.len(), 1);
2324 let layer = &plan.layers[0];
2325 assert_eq!(layer.clip_mode, ClipRenderMode::Stencil);
2326 assert!(layer.stencil_ref.is_some());
2327 let mask_range = layer
2328 .stencil_mask_range
2329 .as_ref()
2330 .expect("stencil mask range must be present");
2331 assert_eq!(*mask_range, 0..6);
2333 assert_eq!(plan.stencil_mask_arena.len(), 6);
2334 assert!(plan.needs_stencil_attachment);
2335 }
2336
2337 #[test]
2338 fn isolated_pass_segmentation_splits_layer_run() {
2339 let l1 = vec![DrawCmd::Rect(DrawRect {
2340 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
2341 color: Color::new(1.0, 0.0, 0.0, 1.0),
2342 })];
2343 let l2 = vec![DrawCmd::Rect(DrawRect {
2344 rect: Rect::new(20.0, 0.0, 10.0, 10.0),
2345 color: Color::new(0.0, 1.0, 0.0, 1.0),
2346 })];
2347 let l3 = vec![DrawCmd::Rect(DrawRect {
2348 rect: Rect::new(40.0, 0.0, 10.0, 10.0),
2349 color: Color::new(0.0, 0.0, 1.0, 1.0),
2350 })];
2351 let layers = vec![
2352 LayerPass::new(&l1),
2353 LayerPass::new_isolated(&l2),
2354 LayerPass::new(&l3),
2355 ];
2356
2357 let plan = FrameRenderPlan::build_cpu(
2358 &layers,
2359 &empty_prepared(),
2360 [128.0, 128.0],
2361 Scale::new(1.0),
2362 true,
2363 );
2364
2365 assert_eq!(plan.layers.len(), 3);
2367 assert!(plan.planned_isolated.is_empty());
2368 }
2369
2370 #[test]
2371 fn isolated_with_opacity_creates_planned_isolated_and_composite() {
2372 let l1 = vec![DrawCmd::Rect(DrawRect {
2373 rect: Rect::new(0.0, 0.0, 40.0, 40.0),
2374 color: Color::new(1.0, 0.0, 0.0, 1.0),
2375 })];
2376 let mut layer = LayerPass::new_isolated(&l1);
2377 layer.effects.opacity = 0.5;
2378 let plan = FrameRenderPlan::build_cpu(
2379 &[LayerPass::new(&[]), layer],
2380 &empty_prepared(),
2381 [128.0, 128.0],
2382 Scale::new(1.0),
2383 true,
2384 );
2385 assert_eq!(plan.planned_isolated.len(), 1);
2386 assert_eq!(plan.planned_isolated[0].source_layer_idx, 1);
2387 assert!(matches!(
2388 plan.batches.last(),
2389 Some(PlannedBatch::IsolatedComposite(_))
2390 ));
2391 }
2392
2393 #[test]
2394 fn backdrop_root_schedules_capture_split() {
2395 let bg = vec![DrawCmd::Rect(DrawRect {
2396 rect: Rect::new(0.0, 0.0, 128.0, 128.0),
2397 color: Color::new(0.0, 0.0, 1.0, 1.0),
2398 })];
2399 let iso = vec![DrawCmd::Rect(DrawRect {
2400 rect: Rect::new(32.0, 32.0, 64.0, 64.0),
2401 color: Color::new(1.0, 0.0, 0.0, 1.0),
2402 })];
2403 let mut layer = LayerPass::new_isolated(&iso);
2404 layer.effects.opacity = 0.9;
2405 layer.effects.backdrop_blur_radius_px = 16.0;
2406 let top = vec![DrawCmd::Rect(DrawRect {
2407 rect: Rect::new(0.0, 0.0, 8.0, 8.0),
2408 color: Color::new(1.0, 1.0, 1.0, 1.0),
2409 })];
2410 let mut budget = IsolatedBudgetPolicy::with_defaults();
2411 let plan = FrameRenderPlan::try_build_cpu(
2412 &[LayerPass::new(&bg), layer, LayerPass::new(&top)],
2413 &empty_prepared(),
2414 [128.0, 128.0],
2415 Scale::new(1.0),
2416 true,
2417 &mut budget,
2418 )
2419 .expect("backdrop plan");
2420 assert_eq!(plan.backdrop_captures.len(), 1);
2421 assert_eq!(plan.backdrop_captures[0].split_planned_layer_idx, 1);
2422 let iso_pass = &plan.planned_isolated[0];
2423 assert!(iso_pass.needs_backdrop_capture);
2424 assert!(iso_pass.backdrop_capture_rect_px.is_some());
2425 assert!(iso_pass.backdrop_blur_radius_px > 0.0);
2426 }
2427
2428 #[test]
2429 fn backdrop_budget_skip_downgrades() {
2430 let bg = vec![DrawCmd::Rect(DrawRect {
2431 rect: Rect::new(0.0, 0.0, 128.0, 128.0),
2432 color: Color::new(0.0, 0.0, 1.0, 1.0),
2433 })];
2434 let iso = vec![DrawCmd::Rect(DrawRect {
2435 rect: Rect::new(0.0, 0.0, 128.0, 128.0),
2436 color: Color::new(1.0, 0.0, 0.0, 1.0),
2437 })];
2438 let mut layer = LayerPass::new_isolated(&iso);
2439 layer.effects.backdrop_blur_radius_px = 8.0;
2440 layer.effects.opacity = 0.5;
2441 let mut budget = IsolatedBudgetPolicy::with_defaults();
2442 budget.config.max_backdrop_captures_per_frame = 0;
2443 let plan = FrameRenderPlan::try_build_cpu(
2444 &[LayerPass::new(&bg), layer],
2445 &empty_prepared(),
2446 [128.0, 128.0],
2447 Scale::new(1.0),
2448 true,
2449 &mut budget,
2450 )
2451 .expect("plan with backdrop budget skip");
2452 assert!(plan.backdrop_captures.is_empty());
2453 let iso_pass = &plan.planned_isolated[0];
2454 assert!(!iso_pass.needs_backdrop_capture);
2455 assert_eq!(budget.downgrades.backdrop_budget_skipped, 1);
2456 }
2457
2458 #[test]
2459 fn nested_depth_exceeded() {
2460 let cmds = vec![DrawCmd::Rect(DrawRect {
2461 rect: Rect::new(0.0, 0.0, 8.0, 8.0),
2462 color: Color::new(1.0, 0.0, 0.0, 1.0),
2463 })];
2464 let mut layers: Vec<LayerPass<'_>> = Vec::new();
2465 for depth in 0..4u8 {
2466 let mut l = LayerPass::new_isolated(&cmds);
2467 l.isolated_depth = depth;
2468 l.effects.opacity = 0.9;
2469 layers.push(l);
2470 }
2471 let mut budget = IsolatedBudgetPolicy::with_defaults();
2472 budget.config.max_isolated_nesting_depth = 3;
2473 let err = FrameRenderPlan::try_build_cpu(
2474 &layers,
2475 &empty_prepared(),
2476 [64.0, 64.0],
2477 Scale::new(1.0),
2478 true,
2479 &mut budget,
2480 )
2481 .unwrap_err();
2482 assert!(matches!(
2483 err,
2484 FramePlanError::NestedDepthExceeded { depth: 3, max: 3 }
2485 ));
2486 }
2487
2488 #[test]
2489 fn nested_isolated_builds_dag() {
2490 let l1 = vec![DrawCmd::Rect(DrawRect {
2491 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
2492 color: Color::new(1.0, 0.0, 0.0, 1.0),
2493 })];
2494 let l2 = vec![DrawCmd::Rect(DrawRect {
2495 rect: Rect::new(20.0, 0.0, 10.0, 10.0),
2496 color: Color::new(0.0, 1.0, 0.0, 1.0),
2497 })];
2498 let mut a = LayerPass::new_isolated(&l1);
2499 a.effects.opacity = 0.5;
2500 let mut b = LayerPass::new_isolated(&l2);
2501 b.effects.opacity = 0.5;
2502 let mut budget = IsolatedBudgetPolicy::with_defaults();
2503 let plan = FrameRenderPlan::try_build_cpu(
2504 &[a, b],
2505 &empty_prepared(),
2506 [64.0, 64.0],
2507 Scale::new(1.0),
2508 true,
2509 &mut budget,
2510 )
2511 .expect("nested consecutive isolated should plan");
2512 assert_eq!(plan.planned_isolated.len(), 2);
2513 let root = plan
2514 .planned_isolated
2515 .iter()
2516 .find(|p| p.composites_to_main)
2517 .expect("root pass");
2518 let child = plan
2519 .planned_isolated
2520 .iter()
2521 .find(|p| !p.composites_to_main)
2522 .expect("child pass");
2523 assert_eq!(root.child_pass_ids, vec![child.id]);
2524 assert_eq!(child.parent_id, Some(root.id));
2525 assert_eq!(
2526 plan.batches
2527 .iter()
2528 .filter(|b| matches!(b, PlannedBatch::IsolatedComposite(_)))
2529 .count(),
2530 1
2531 );
2532 }
2533
2534 #[test]
2535 fn blur_radius_clamped_to_max() {
2536 let l1 = vec![DrawCmd::Rect(DrawRect {
2537 rect: Rect::new(0.0, 0.0, 40.0, 40.0),
2538 color: Color::new(1.0, 0.0, 0.0, 1.0),
2539 })];
2540 let mut layer = LayerPass::new_isolated(&l1);
2541 layer.effects.blur_radius_px = 200.0;
2542 let mut budget = IsolatedBudgetPolicy::with_defaults();
2543 budget.config.max_blur_radius_px = 16.0;
2544 let plan = FrameRenderPlan::try_build_cpu(
2545 &[layer],
2546 &empty_prepared(),
2547 [128.0, 128.0],
2548 Scale::new(1.0),
2549 true,
2550 &mut budget,
2551 )
2552 .unwrap();
2553 let blur = plan.planned_isolated[0]
2554 .effects
2555 .effects
2556 .iter()
2557 .find_map(|e| {
2558 if let crate::isolated_plan::IsolatedEffect::Blur { radius_px } = e {
2559 Some(*radius_px)
2560 } else {
2561 None
2562 }
2563 });
2564 assert_eq!(blur, Some(16.0));
2565 assert_eq!(budget.downgrades.blur_radius_clamped, 1);
2566 }
2567
2568 #[test]
2569 fn too_many_isolated_passes_rejected() {
2570 let mut layer_bufs: Vec<Vec<DrawCmd>> = Vec::new();
2571 for _ in 0..10 {
2572 layer_bufs.push(vec![DrawCmd::Rect(DrawRect {
2573 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
2574 color: Color::new(1.0, 0.0, 0.0, 1.0),
2575 })]);
2576 layer_bufs.push(vec![DrawCmd::Rect(DrawRect {
2577 rect: Rect::new(0.0, 0.0, 1.0, 1.0),
2578 color: Color::WHITE,
2579 })]);
2580 }
2581 let mut layers: Vec<LayerPass<'_>> = Vec::new();
2582 for (i, buf) in layer_bufs.iter().enumerate() {
2583 if i % 2 == 0 {
2584 let mut l = LayerPass::new_isolated(buf);
2585 l.effects.opacity = 0.5;
2586 layers.push(l);
2587 } else {
2588 layers.push(LayerPass::new(buf));
2589 }
2590 }
2591 let mut budget = IsolatedBudgetPolicy::with_defaults();
2592 budget.config.max_isolated_passes_per_frame = 4;
2593 let err = FrameRenderPlan::try_build_cpu(
2594 &layers,
2595 &empty_prepared(),
2596 [256.0, 256.0],
2597 Scale::new(1.0),
2598 true,
2599 &mut budget,
2600 )
2601 .unwrap_err();
2602 assert!(matches!(err, FramePlanError::TooManyIsolatedPasses { .. }));
2603 }
2604
2605 #[test]
2606 fn bytes_budget_skips_offscreen_pass() {
2607 let l1 = vec![DrawCmd::Rect(DrawRect {
2608 rect: Rect::new(0.0, 0.0, 200.0, 200.0),
2609 color: Color::new(1.0, 0.0, 0.0, 1.0),
2610 })];
2611 let mut layer = LayerPass::new_isolated(&l1);
2612 layer.effects.opacity = 0.5;
2613 let mut budget = IsolatedBudgetPolicy::with_defaults();
2614 budget.config.max_offscreen_bytes_per_frame = 1024;
2615 let plan = FrameRenderPlan::try_build_cpu(
2616 &[layer],
2617 &empty_prepared(),
2618 [256.0, 256.0],
2619 Scale::new(1.0),
2620 true,
2621 &mut budget,
2622 )
2623 .unwrap();
2624 assert!(plan.planned_isolated.is_empty());
2625 assert_eq!(budget.downgrades.bytes_budget_skipped, 1);
2626 assert!(!plan.layers.is_empty());
2627 }
2628
2629 #[test]
2630 fn isolated_composite_batch_between_normal_layers() {
2631 let bg = vec![DrawCmd::Rect(DrawRect {
2632 rect: Rect::new(0.0, 0.0, 128.0, 128.0),
2633 color: Color::new(1.0, 0.0, 0.0, 1.0),
2634 })];
2635 let iso = vec![DrawCmd::Rect(DrawRect {
2636 rect: Rect::new(32.0, 32.0, 40.0, 40.0),
2637 color: Color::new(0.0, 1.0, 0.0, 1.0),
2638 })];
2639 let top = vec![DrawCmd::Rect(DrawRect {
2640 rect: Rect::new(80.0, 80.0, 20.0, 20.0),
2641 color: Color::new(0.0, 0.0, 1.0, 1.0),
2642 })];
2643 let mut mid = LayerPass::new_isolated(&iso);
2644 mid.effects.opacity = 0.5;
2645 let plan = FrameRenderPlan::build_cpu(
2646 &[LayerPass::new(&bg), mid, LayerPass::new(&top)],
2647 &empty_prepared(),
2648 [128.0, 128.0],
2649 Scale::new(1.0),
2650 true,
2651 );
2652 assert_eq!(plan.layers.len(), 3);
2653 assert_eq!(plan.planned_isolated.len(), 1);
2654 assert!(plan
2655 .batches
2656 .iter()
2657 .any(|b| matches!(b, PlannedBatch::IsolatedComposite(_))));
2658 let prim_count = plan
2659 .batches
2660 .iter()
2661 .filter(|b| matches!(b, PlannedBatch::Primitives { .. }))
2662 .count();
2663 assert_eq!(prim_count, 2);
2664 }
2665
2666 #[test]
2667 fn local_remap_shifts_rect_to_origin() {
2668 let cmds = vec![DrawCmd::Rect(DrawRect {
2669 rect: Rect::new(40.0, 40.0, 20.0, 20.0),
2670 color: Color::new(1.0, 0.0, 0.0, 1.0),
2671 })];
2672 let mut layer = LayerPass::new_isolated(&cmds);
2673 layer.effects.opacity = 0.5;
2674 let prepared = empty_prepared();
2675 let plan =
2676 FrameRenderPlan::build_cpu(&[layer], &prepared, [128.0, 128.0], Scale::new(1.0), true);
2677 let iso = &plan.planned_isolated[0];
2678 let sub = FrameRenderPlan::build_isolated_subplan(
2679 &LayerPass::new_isolated(&cmds),
2680 &prepared,
2681 iso,
2682 Scale::new(1.0),
2683 true,
2684 );
2685 assert!(!sub.vertex_arena.is_empty());
2686 let min_x = sub
2687 .vertex_arena
2688 .iter()
2689 .map(|v| v.pos[0])
2690 .fold(f32::INFINITY, f32::min);
2691 assert!(
2692 min_x < 0.0,
2693 "local NDC x should start near left edge, got {min_x}"
2694 );
2695 }
2696
2697 #[test]
2698 fn stencil_attachment_required_only_when_any_layer_stencil() {
2699 let l1 = vec![DrawCmd::Rect(DrawRect {
2700 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
2701 color: Color::new(1.0, 0.0, 0.0, 1.0),
2702 })];
2703 let no_stencil = FrameRenderPlan::build_cpu(
2704 &[LayerPass::new(&l1)],
2705 &empty_prepared(),
2706 [64.0, 64.0],
2707 Scale::new(1.0),
2708 true,
2709 );
2710 assert!(!no_stencil.needs_stencil_attachment);
2711
2712 let root_round = ClipShape::RoundRect {
2713 rect: Rect::new(0.0, 0.0, 64.0, 64.0),
2714 radius: 12.0,
2715 };
2716 let snap = ClipStackSnapshot::from_clip(Some(root_round), true);
2717 let with_stencil = FrameRenderPlan::build_cpu(
2718 &[LayerPass::with_clip_stack(&l1, snap)],
2719 &empty_prepared(),
2720 [64.0, 64.0],
2721 Scale::new(1.0),
2722 true,
2723 );
2724 assert!(with_stencil.needs_stencil_attachment);
2725 }
2726
2727 #[test]
2728 fn stencil_downgrades_to_shader_mask_when_unsupported() {
2729 let l1 = vec![DrawCmd::Rect(DrawRect {
2730 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
2731 color: Color::new(1.0, 0.0, 0.0, 1.0),
2732 })];
2733 let root_round = ClipShape::RoundRect {
2734 rect: Rect::new(0.0, 0.0, 64.0, 64.0),
2735 radius: 12.0,
2736 };
2737 let snap = ClipStackSnapshot::from_clip(Some(root_round), true);
2738 let plan = FrameRenderPlan::build_cpu(
2739 &[LayerPass::with_clip_stack(&l1, snap)],
2740 &empty_prepared(),
2741 [64.0, 64.0],
2742 Scale::new(1.0),
2743 false, );
2745 assert_eq!(plan.layers.len(), 1);
2746 assert_eq!(plan.layers[0].clip_mode, ClipRenderMode::ShaderMask);
2747 assert!(plan.layers[0].stencil_ref.is_none());
2748 assert!(plan.layers[0].stencil_mask_range.is_none());
2749 assert!(!plan.needs_stencil_attachment);
2750 }
2751
2752 #[test]
2753 fn blend_multiply_schedules_dst_capture() {
2754 let bg = vec![DrawCmd::Rect(DrawRect {
2755 rect: Rect::new(0.0, 0.0, 128.0, 128.0),
2756 color: Color::new(1.0, 1.0, 0.0, 1.0),
2757 })];
2758 let iso = vec![DrawCmd::Rect(DrawRect {
2759 rect: Rect::new(32.0, 32.0, 64.0, 64.0),
2760 color: Color::new(1.0, 0.0, 0.0, 1.0),
2761 })];
2762 let mut layer = LayerPass::new_isolated(&iso);
2763 layer.effects.blend_mode = BlendMode::Multiply;
2764 let plan = FrameRenderPlan::build_cpu(
2765 &[LayerPass::new(&bg), layer],
2766 &empty_prepared(),
2767 [128.0, 128.0],
2768 Scale::new(1.0),
2769 true,
2770 );
2771 let iso_pass = &plan.planned_isolated[0];
2772 assert_eq!(iso_pass.composite_blend_mode, BlendMode::Multiply);
2773 assert!(iso_pass.needs_blend_dst_capture);
2774 let comp = plan
2775 .batches
2776 .iter()
2777 .find_map(|b| {
2778 if let PlannedBatch::IsolatedComposite(c) = b {
2779 Some(c)
2780 } else {
2781 None
2782 }
2783 })
2784 .expect("composite batch");
2785 assert_eq!(comp.blend_mode, BlendMode::Multiply);
2786 assert!(comp.needs_dst_capture);
2787 assert!(comp.dst_capture_rect_px.is_some());
2788 }
2789
2790 #[test]
2791 fn blend_budget_skip_downgrades_to_normal() {
2792 let iso = vec![DrawCmd::Rect(DrawRect {
2793 rect: Rect::new(0.0, 0.0, 128.0, 128.0),
2794 color: Color::new(1.0, 0.0, 0.0, 1.0),
2795 })];
2796 let mut layer = LayerPass::new_isolated(&iso);
2797 layer.effects.blend_mode = BlendMode::Screen;
2798 let mut budget = IsolatedBudgetPolicy::with_defaults();
2799 budget.config.max_blend_captures_per_frame = 0;
2800 let plan = FrameRenderPlan::try_build_cpu(
2801 &[layer],
2802 &empty_prepared(),
2803 [128.0, 128.0],
2804 Scale::new(1.0),
2805 true,
2806 &mut budget,
2807 )
2808 .expect("build");
2809 assert_eq!(budget.downgrades.blend_capture_budget_skipped, 1);
2810 let iso_pass = &plan.planned_isolated[0];
2811 assert_eq!(iso_pass.composite_blend_mode, BlendMode::Normal);
2812 assert!(!iso_pass.needs_blend_dst_capture);
2813 }
2814
2815 #[test]
2816 fn image_skipped_when_not_in_prepared() {
2817 let l1 = vec![DrawCmd::Image(DrawImage {
2818 rect: Rect::new(0.0, 0.0, 16.0, 16.0),
2819 icon: IconId::Plus,
2820 tint: Color::new(1.0, 1.0, 1.0, 1.0),
2821 rotation_rad: 0.0,
2822 })];
2823 let plan = FrameRenderPlan::build_cpu(
2825 &[LayerPass::new(&l1)],
2826 &empty_prepared(),
2827 [64.0, 64.0],
2828 Scale::new(1.0),
2829 true,
2830 );
2831 assert_eq!(plan.batches.len(), 0);
2832 assert_eq!(plan.tex_vertex_arena.len(), 0);
2833 }
2834}