use std::{iter::Peekable, rc::Rc, sync::Arc};
use cranpose_ui_graphics::{BlendMode, Rect, RuntimeShader};
use crate::{
effect_renderer::{
CompositeBatchItem, CompositeSampleMode, PreparedCompositeDraw,
PreparedProjectiveComposite, PreparedShaderDraw, ProjectiveCompositeItem,
RoundedCompositeMask, ShaderCompositeBatchItem, SubstrateRegions,
},
frame_graph::FrameCommandRecorder,
geometry::SegmentTransform,
offscreen::OffscreenTarget,
render::{
GpuRenderer, PassFrame, RunStage, StoreRunBatch, TargetRect, ViewportUniformParams,
image_draw_bounds, run_draw_bounds, run_draw_is_visible_in_rect, scissor_rect_for_rect,
segment_scene_rect, supported_blend_mode, text_draw_bounds, text_draw_is_visible_in_rect,
},
run_store::{RunDrawCall, run_has_shapes},
scene::{CompositorScene, DrawOp, DrawOpKind, RunDraw, TextDraw},
};
#[derive(Clone, Copy)]
pub(crate) struct PassTarget<'a> {
pub(crate) view: &'a wgpu::TextureView,
pub(crate) width: u32,
pub(crate) height: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SourceContent {
Retained(u64),
Transient,
}
impl SourceContent {
pub(crate) fn retained(key: &impl std::hash::Hash) -> Self {
let mut hasher = cranpose_ui_graphics::FxHasher::default();
key.hash(&mut hasher);
Self::Retained(std::hash::Hasher::finish(&hasher))
}
pub(crate) fn retained_hash(self) -> Option<u64> {
match self {
Self::Retained(hash) => Some(hash),
Self::Transient => None,
}
}
pub(crate) fn derived(self, step: &impl std::hash::Hash) -> Self {
match self {
Self::Retained(hash) => Self::retained(&(hash, step)),
Self::Transient => Self::Transient,
}
}
}
#[derive(Clone)]
pub(crate) struct ResolvedComposite {
pub(crate) z_index: usize,
pub(crate) source: Rc<OffscreenTarget>,
pub(crate) content: SourceContent,
pub(crate) dest: (f32, f32, f32, f32),
pub(crate) scissor: Option<(f32, f32, f32, f32)>,
pub(crate) kind: ResolvedCompositeKind,
}
#[derive(Clone)]
pub(crate) enum ResolvedCompositeKind {
Blit {
alpha: f32,
blend_mode: BlendMode,
rounded_mask: Option<RoundedCompositeMask>,
sample_mode: CompositeSampleMode,
source_viewport: Option<(f32, f32, f32, f32)>,
},
Shader {
shader: Arc<RuntimeShader>,
layer_pixel_rect: [f32; 4],
source_region: Option<(f32, f32, f32, f32)>,
source_logical_size: Option<(f32, f32)>,
substrate_regions: SubstrateRegions,
rounded_mask: Option<RoundedCompositeMask>,
alpha: f32,
},
Projective {
dest_quad: [[f32; 2]; 4],
inverse: [[f32; 3]; 3],
alpha: f32,
blend_mode: BlendMode,
sample_mode: CompositeSampleMode,
source_region: Option<(f32, f32, f32, f32)>,
},
}
pub(crate) struct PassSegment<'a> {
pub(crate) scene: &'a CompositorScene,
pub(crate) ops: &'a [DrawOp],
pub(crate) composites: &'a [ResolvedComposite],
pub(crate) offset: [f32; 2],
pub(crate) scissor: Option<(u32, u32, u32, u32)>,
pub(crate) first_run_window: Option<std::ops::Range<u32>>,
pub(crate) transform: SegmentTransform,
pub(crate) scale: f32,
}
enum Item<'a> {
Run(&'a RunDraw, Option<std::ops::Range<u32>>),
Image(usize),
Text(&'a TextDraw),
Composite(&'a ResolvedComposite),
}
enum Batch<'a> {
StoreRun {
batch: StoreRunBatch,
scissor: Option<(u32, u32, u32, u32)>,
},
Arena {
chunk: usize,
uniform_slot: usize,
draws: Vec<RunDrawCall>,
scissor: Option<(u32, u32, u32, u32)>,
},
Images {
cmds: std::ops::Range<usize>,
blend_mode: BlendMode,
uniform_slot: usize,
scissor: Option<(u32, u32, u32, u32)>,
},
Glyphs {
cmds: std::ops::Range<usize>,
uniform_slot: usize,
scissor: Option<(u32, u32, u32, u32)>,
},
Composite(PreparedCompositeDraw<'a>),
Shader(PreparedShaderDraw<'a>),
Projective(PreparedProjectiveComposite<'a>),
}
pub(crate) fn scissor_in_target(
scissor: (f32, f32, f32, f32),
target_size: (u32, u32),
segment_offset: [f32; 2],
) -> Option<(u32, u32, u32, u32)> {
let (x, y, width, height) = scissor;
let left = (x - segment_offset[0]).floor().max(0.0);
let top = (y - segment_offset[1]).floor().max(0.0);
let right = (x + width - segment_offset[0])
.ceil()
.min(target_size.0 as f32);
let bottom = (y + height - segment_offset[1])
.ceil()
.min(target_size.1 as f32);
if right <= left || bottom <= top {
return None;
}
Some((
left as u32,
top as u32,
(right - left) as u32,
(bottom - top) as u32,
))
}
fn intersect_scissors(
a: Option<(u32, u32, u32, u32)>,
b: Option<(u32, u32, u32, u32)>,
) -> Option<Option<(u32, u32, u32, u32)>> {
match (a, b) {
(None, None) => Some(None),
(Some(rect), None) | (None, Some(rect)) => Some(Some(rect)),
(Some((ax, ay, aw, ah)), Some((bx, by, bw, bh))) => {
let left = ax.max(bx);
let top = ay.max(by);
let right = (ax + aw).min(bx + bw);
let bottom = (ay + ah).min(by + bh);
(right > left && bottom > top).then(|| Some((left, top, right - left, bottom - top)))
}
}
}
fn dest_in_target(dest: (f32, f32, f32, f32), segment_offset: [f32; 2]) -> (f32, f32, f32, f32) {
(
dest.0 - segment_offset[0],
dest.1 - segment_offset[1],
dest.2,
dest.3,
)
}
fn mask_in_target(
mask: Option<RoundedCompositeMask>,
segment_offset: [f32; 2],
) -> Option<RoundedCompositeMask> {
mask.map(|mask| RoundedCompositeMask {
rect: [
mask.rect[0] - segment_offset[0],
mask.rect[1] - segment_offset[1],
mask.rect[2],
mask.rect[3],
],
radii: mask.radii,
})
}
fn composite_visible(
composite: &ResolvedComposite,
target_size: (u32, u32),
segment_offset: [f32; 2],
segment_scissor: Option<(u32, u32, u32, u32)>,
) -> bool {
let (x, y, width, height) = dest_in_target(composite.dest, segment_offset);
let (left, top, right, bottom) = match segment_scissor {
Some((sx, sy, sw, sh)) => (sx as f32, sy as f32, (sx + sw) as f32, (sy + sh) as f32),
None => (0.0, 0.0, target_size.0 as f32, target_size.1 as f32),
};
if x >= right || y >= bottom || x + width <= left || y + height <= top {
return false;
}
composite
.scissor
.is_none_or(|scissor| scissor_in_target(scissor, target_size, segment_offset).is_some())
}
impl GpuRenderer {
pub(crate) fn encode_pass<'s, C: FrameCommandRecorder>(
&mut self,
recorder: &mut C,
target: PassTarget<'_>,
segments: &'s [PassSegment<'s>],
load_op: wgpu::LoadOp<wgpu::Color>,
label: &'static str,
) -> Result<bool, String> {
let mut scratch = self.take_pass_scratch();
let device = self.device.clone();
let depth = takes_depth(segments);
let mut prep = PassPrep {
recorder,
device: &device,
target,
load_op,
batches: Vec::new(),
chunk: None,
pending_glyphs: PendingGlyphs::default(),
depth,
overlay_segment: None,
depth_seq: 0,
};
let prepared = segments
.iter()
.try_for_each(|segment| prep.segment(self, segment, &mut scratch));
let batches = prep.batches;
let buffers = PassBuffers {
images: match &prepared {
Ok(()) if !scratch.image_indices.is_empty() => Some(self.upload_image_slot(
recorder,
&scratch.image_vertices,
&scratch.image_indices,
)),
_ => None,
},
glyphs: match &prepared {
Ok(()) if !scratch.glyph_instances.is_empty() => {
Some(self.upload_glyph_instances(recorder, &scratch.glyph_instances))
}
_ => None,
},
};
let result = match prepared {
Err(error) => Err(error),
Ok(()) if batches.is_empty() => {
if matches!(load_op, wgpu::LoadOp::Clear(_)) {
self.clear_target(recorder, target.view, load_op);
}
Ok(false)
}
Ok(()) => {
let composite_draws = batches
.iter()
.filter(|batch| matches!(batch, Batch::Composite(_) | Batch::Shader(_)))
.count() as u32;
if composite_draws > 0 {
self.effect_renderer.record_composite_pass();
self.frame_stats.add_draw_calls(composite_draws);
}
let draw_result = {
let frame = PassFrame {
size: (target.width, target.height),
depth,
};
let mut pass = self.begin_scene_pass(recorder, label, target, load_op, depth);
self.draw_batches(
&mut pass,
frame,
&batches,
&scratch.image_cmds,
&scratch.glyph_cmds,
&buffers,
)
};
recorder.record_pass();
draw_result.map(|()| true)
}
};
drop(batches);
self.return_pass_scratch(scratch);
result
}
fn take_pass_scratch(&mut self) -> PassScratch {
let mut scratch = PassScratch {
image_vertices: std::mem::take(&mut self.scratch_image_vertices),
image_indices: std::mem::take(&mut self.scratch_image_indices),
image_cmds: std::mem::take(&mut self.scratch_image_cmds),
glyph_instances: std::mem::take(&mut self.scratch_glyph_instances),
glyph_cmds: std::mem::take(&mut self.scratch_glyph_cmds),
};
scratch.image_vertices.clear();
scratch.image_indices.clear();
scratch.image_cmds.clear();
scratch.glyph_instances.clear();
scratch.glyph_cmds.clear();
scratch
}
fn return_pass_scratch(&mut self, scratch: PassScratch) {
self.scratch_image_vertices = scratch.image_vertices;
self.scratch_image_indices = scratch.image_indices;
self.scratch_image_cmds = scratch.image_cmds;
self.scratch_glyph_instances = scratch.glyph_instances;
self.scratch_glyph_cmds = scratch.glyph_cmds;
}
fn draw_batches(
&mut self,
pass: &mut wgpu::RenderPass<'_>,
frame: PassFrame,
batches: &[Batch<'_>],
image_cmds: &[crate::render::ImageDrawCmd],
glyph_cmds: &[crate::render::GlyphDrawCmd],
buffers: &PassBuffers,
) -> Result<(), String> {
let target_size = frame.size;
if frame.depth {
for batch in batches.iter().rev() {
self.draw_shape_batch(pass, batch, frame, RunStage::Interiors)?;
}
}
for batch in batches {
match batch {
Batch::StoreRun { .. } | Batch::Arena { .. } => {
self.draw_shape_batch(pass, batch, frame, RunStage::Paint)?;
}
Batch::Images {
cmds,
blend_mode,
uniform_slot,
scissor,
} => {
let slot = buffers
.images
.as_ref()
.ok_or_else(|| "image batch without an image slot".to_string())?;
self.draw_image_cmds(
pass,
slot,
*uniform_slot,
&image_cmds[cmds.clone()],
self.image_pipeline(*blend_mode, frame.depth),
*scissor,
)?;
}
Batch::Glyphs {
cmds,
uniform_slot,
scissor,
} => {
self.draw_glyph_cmds(
pass,
buffers.glyphs.as_ref(),
*uniform_slot,
&glyph_cmds[cmds.clone()],
*scissor,
frame,
)?;
}
Batch::Composite(prepared) => {
self.effect_renderer
.draw_prepared_composite(pass, target_size, prepared);
}
Batch::Shader(prepared) => {
self.effect_renderer
.draw_prepared_shader_src_over(pass, target_size, prepared);
}
Batch::Projective(prepared) => {
self.effect_renderer.draw_prepared_projective_composite(
pass,
target_size,
prepared,
);
}
}
}
Ok(())
}
}
impl GpuRenderer {
fn draw_shape_batch(
&self,
pass: &mut wgpu::RenderPass<'_>,
batch: &Batch<'_>,
frame: PassFrame,
stage: RunStage,
) -> Result<(), String> {
match batch {
Batch::StoreRun { batch, scissor } => {
self.draw_store_run(pass, batch, frame.scissor(*scissor), stage)
}
Batch::Arena {
chunk,
uniform_slot,
draws,
scissor,
} => self.draw_arena(
pass,
*chunk,
*uniform_slot,
draws,
frame.scissor(*scissor),
stage,
),
_ => Ok(()),
}
}
}
impl GpuRenderer {
fn begin_scene_pass<'p, C: FrameCommandRecorder>(
&mut self,
recorder: &'p mut C,
label: &'static str,
target: PassTarget<'_>,
load_op: wgpu::LoadOp<wgpu::Color>,
depth: bool,
) -> wgpu::RenderPass<'p> {
if depth {
let depth_view = self.depth_target((target.width, target.height));
recorder.begin_depth_pass(label, target.view, load_op, &depth_view)
} else {
recorder.begin_color_pass(label, target.view, load_op)
}
}
}
static NO_INTERIORS_FIRST: crate::debug_toggles::DebugToggle =
crate::debug_toggles::DebugToggle::new("CRANPOSE_NO_INTERIORS_FIRST");
fn takes_depth(segments: &[PassSegment<'_>]) -> bool {
!NO_INTERIORS_FIRST.equals("1")
&& segments.iter().all(|segment| segment.composites.is_empty())
&& segments.iter().any(segment_has_occluders)
}
fn segment_has_occluders(segment: &PassSegment<'_>) -> bool {
segment.ops.iter().any(|op| match op.kind {
DrawOpKind::Run(index) => segment.scene.runs[index]
.segment_records()
.any(|records| records.occluders),
_ => false,
})
}
pub(crate) fn op_draw_bounds(
scene: &CompositorScene,
op: &DrawOp,
root_scale: f32,
) -> Option<Rect> {
match op.kind {
DrawOpKind::Run(index) => run_draw_bounds(&scene.runs[index], root_scale),
DrawOpKind::Image(index) => image_draw_bounds(&scene.images[index], root_scale),
DrawOpKind::Text(index) => text_draw_bounds(&scene.texts[index], root_scale),
DrawOpKind::Shadow(index) => {
let shadow = &scene.shadow_draws[index];
if shadow.requires_surface() {
return None;
}
shadow_caster_bounds(shadow, root_scale)
.into_iter()
.chain(
shadow
.texts
.iter()
.filter_map(|text| text_draw_bounds(text, root_scale)),
)
.reduce(|a, b| a.union(b))
}
}
}
fn shadow_caster_bounds(shadow: &crate::scene::ShadowDraw, root_scale: f32) -> Option<Rect> {
shadow
.shapes
.as_ref()
.and_then(|run| run_draw_bounds(run, root_scale))
}
pub(crate) fn op_is_visible_in_rect(
scene: &CompositorScene,
op: &DrawOp,
viewport_rect: Rect,
root_scale: f32,
) -> bool {
op_draw_bounds(scene, op, root_scale)
.is_some_and(|bounds| bounds.intersect(viewport_rect).is_some())
}
fn segment_viewport_rect(target: PassTarget<'_>, segment: &PassSegment<'_>) -> Rect {
let (x, y, width, height) = segment
.scissor
.unwrap_or((0, 0, target.width, target.height));
segment_scene_rect(
segment.transform,
Rect {
x: segment.offset[0] + x as f32,
y: segment.offset[1] + y as f32,
width: width as f32,
height: height as f32,
},
segment.scale,
)
}
pub(crate) fn segment_draws_anything(target: PassTarget<'_>, segment: &PassSegment<'_>) -> bool {
let viewport_rect = segment_viewport_rect(target, segment);
merge_items(segment, viewport_rect, (target.width, target.height), false)
.next()
.is_some()
}
fn translate_inverse(inverse: [[f32; 3]; 3], offset: [f32; 2]) -> [[f32; 3]; 3] {
let mut shifted = inverse;
for row in &mut shifted {
row[2] += row[0] * offset[0] + row[1] * offset[1];
}
shifted
}
fn unshadowed_item<'a>(
scene: &'a CompositorScene,
kind: DrawOpKind,
op_index: usize,
first_run_window: &Option<std::ops::Range<u32>>,
skip_text: bool,
) -> Option<Item<'a>> {
match kind {
DrawOpKind::Run(index) => {
let run = &scene.runs[index];
run_has_shapes(run).then(|| {
let window = (op_index == 0).then(|| first_run_window.clone()).flatten();
Item::Run(run, window)
})
}
DrawOpKind::Image(index) => Some(Item::Image(index)),
DrawOpKind::Text(_) if skip_text => None,
DrawOpKind::Text(index) => Some(Item::Text(&scene.texts[index])),
DrawOpKind::Shadow(_) => None,
}
}
fn merge_items<'a>(
segment: &PassSegment<'a>,
viewport_rect: Rect,
target_size: (u32, u32),
skip_text: bool,
) -> impl Iterator<Item = Item<'a>> + use<'a> {
let scene = segment.scene;
let root_scale = segment.scale;
let mut ops = segment.ops.iter().enumerate().peekable();
let mut composites = segment.composites.iter().peekable();
let mut shadow_texts: std::slice::Iter<'a, TextDraw> = [].iter();
let offset = segment.offset;
let scissor = segment.scissor;
let first_run_window = segment.first_run_window.clone();
std::iter::from_fn(move || {
loop {
if let Some(text) = shadow_texts
.find(|text| text_draw_is_visible_in_rect(text, viewport_rect, root_scale))
{
return Some(Item::Text(text));
}
let next_z = ops.peek().map(|(_, op)| op.z_index);
if composites
.peek()
.is_some_and(|composite| next_z.is_none_or(|z| composite.z_index <= z))
{
let composite = composites.next().expect("peeked composite");
if composite_visible(composite, target_size, offset, scissor) {
return Some(Item::Composite(composite));
}
continue;
}
let (op_index, op) = ops.next()?;
if !op_is_visible_in_rect(scene, op, viewport_rect, root_scale) {
continue;
}
if let DrawOpKind::Shadow(index) = op.kind {
let shadow = &scene.shadow_draws[index];
shadow_texts = shadow.texts.iter();
if let Some(run) = unblurred_shadow_run(shadow, viewport_rect, root_scale) {
return Some(Item::Run(run, None));
}
continue;
}
if let Some(item) =
unshadowed_item(scene, op.kind, op_index, &first_run_window, skip_text)
{
return Some(item);
}
}
})
}
fn unblurred_shadow_run(
shadow: &crate::scene::ShadowDraw,
viewport_rect: Rect,
root_scale: f32,
) -> Option<&RunDraw> {
let run = shadow.shapes.as_ref()?;
run_draw_is_visible_in_rect(run, viewport_rect, root_scale).then_some(run)
}
struct PassBuffers {
images: Option<crate::render::ImageSlot>,
glyphs: Option<crate::frame_graph::BufferUpload>,
}
struct PassScratch {
image_vertices: Vec<crate::render::Vertex>,
image_indices: Vec<u32>,
image_cmds: Vec<crate::render::ImageDrawCmd>,
glyph_instances: Vec<crate::render::GlyphInstance>,
glyph_cmds: Vec<crate::render::GlyphDrawCmd>,
}
const MAX_PENDING_GLYPHS: usize = 256;
fn target_rects_overlap(a: TargetRect, b: TargetRect) -> bool {
a.0 < b.0 + b.2 && b.0 < a.0 + a.2 && a.1 < b.1 + b.3 && b.1 < a.1 + a.3
}
fn target_rect_union(a: TargetRect, b: TargetRect) -> TargetRect {
let left = a.0.min(b.0);
let top = a.1.min(b.1);
let right = (a.0 + a.2).max(b.0 + b.2);
let bottom = (a.1 + a.3).max(b.1 + b.3);
(left, top, right - left, bottom - top)
}
#[derive(Default)]
struct PendingGlyphs {
cmds: Option<std::ops::Range<usize>>,
bounds: Vec<TargetRect>,
union: Option<TargetRect>,
}
impl PendingGlyphs {
fn hold(&mut self, cmds: std::ops::Range<usize>, bounds: impl IntoIterator<Item = TargetRect>) {
self.cmds = Some(match self.cmds.take() {
Some(held) => held.start..cmds.end,
None => cmds,
});
for rect in bounds {
self.union = Some(
self.union
.map_or(rect, |union| target_rect_union(union, rect)),
);
self.bounds.push(rect);
}
}
fn overlaps(&self, rect: TargetRect) -> bool {
self.union
.is_some_and(|union| target_rects_overlap(union, rect))
&& self
.bounds
.iter()
.any(|held| target_rects_overlap(*held, rect))
}
fn full(&self) -> bool {
self.bounds.len() >= MAX_PENDING_GLYPHS
}
fn take(&mut self) -> Option<std::ops::Range<usize>> {
self.bounds.clear();
self.union = None;
self.cmds.take()
}
}
struct PassPrep<'a, 's, C> {
recorder: &'a mut C,
device: &'a wgpu::Device,
target: PassTarget<'a>,
load_op: wgpu::LoadOp<wgpu::Color>,
batches: Vec<Batch<'s>>,
chunk: Option<usize>,
pending_glyphs: PendingGlyphs,
depth: bool,
overlay_segment: Option<usize>,
depth_seq: u32,
}
impl<'s, C: FrameCommandRecorder> PassPrep<'_, 's, C> {
fn target_size(&self) -> (u32, u32) {
(self.target.width, self.target.height)
}
fn segment(
&mut self,
renderer: &mut GpuRenderer,
segment: &PassSegment<'s>,
scratch: &mut PassScratch,
) -> Result<(), String> {
debug_assert!(
segment.transform.is_identity() || segment.composites.is_empty(),
"a transformed segment places its composites nowhere"
);
let viewport = ViewportUniformParams {
width: self.target.width,
height: self.target.height,
offset: segment.offset,
transform: segment.transform,
origin: [0.0; 2],
depth_base: 0.0,
};
let viewport_rect = segment_viewport_rect(self.target, segment);
let uniform_slot = renderer.claim_uniform_slot(viewport);
let mut items = merge_items(
segment,
viewport_rect,
self.target_size(),
renderer.ablation.text,
)
.peekable();
let run = SegmentRun {
segment,
viewport,
uniform_slot,
};
while let Some(item) = items.peek() {
match item {
Item::Run(..) => {
self.run_items(renderer, &mut items, &run);
continue;
}
Item::Image(_) => {
self.flush(renderer, &run);
self.image_run(renderer, &mut items, &run, scratch)?;
continue;
}
Item::Text(text) => self.text_item(renderer, text, &run, scratch)?,
Item::Composite(composite) => {
self.flush(renderer, &run);
self.composite_item(renderer, composite, &run)?;
}
}
items.next();
}
self.flush(renderer, &run);
Ok(())
}
fn close_chunk(&mut self, renderer: &mut GpuRenderer, run: &SegmentRun<'s, '_>) {
let Some(open) = self.chunk.take() else {
return;
};
let draws = renderer.close_arena(open);
if !draws.is_empty() {
let uniform_slot = self.depth_slot(renderer, run, &draws);
self.batches.push(Batch::Arena {
chunk: open,
uniform_slot,
draws,
scissor: run.segment.scissor,
});
}
}
fn depth_slot(
&mut self,
renderer: &mut GpuRenderer,
run: &SegmentRun<'s, '_>,
draws: &[RunDrawCall],
) -> usize {
if !self.depth {
return run.uniform_slot;
}
let base = self.take_depth_range(draws);
renderer.claim_uniform_slot(ViewportUniformParams {
depth_base: base,
..run.viewport
})
}
fn overlay_slot(&mut self, renderer: &mut GpuRenderer, run: &SegmentRun<'s, '_>) -> usize {
self.overlay_segment = Some(run.uniform_slot);
if !self.depth {
return run.uniform_slot;
}
let base = self.depth_seq;
self.depth_seq = base.saturating_add(1);
renderer.claim_uniform_slot(ViewportUniformParams {
depth_base: base as f32,
..run.viewport
})
}
fn text_viewport(
&self,
renderer: &GpuRenderer,
run: &SegmentRun<'s, '_>,
) -> ViewportUniformParams {
if !self.depth {
return run.viewport;
}
let open = self.chunk.map_or(0, |_| renderer.open_arena_records());
ViewportUniformParams {
depth_base: self.depth_seq.saturating_add(open) as f32,
..run.viewport
}
}
fn take_depth_range(&mut self, draws: &[RunDrawCall]) -> f32 {
let base = self.depth_seq;
let records = draws.iter().map(|draw| draw.records.end).max().unwrap_or(0);
self.depth_seq = base.saturating_add(records);
base as f32
}
fn flush(&mut self, renderer: &mut GpuRenderer, run: &SegmentRun<'s, '_>) {
self.close_chunk(renderer, run);
let Some(cmds) = self.pending_glyphs.take() else {
return;
};
let continues = self.overlay_segment == Some(run.uniform_slot);
match self.batches.last_mut() {
Some(Batch::Glyphs { cmds: last, .. }) if last.end == cmds.start && continues => {
last.end = cmds.end;
}
_ => {
let uniform_slot = self.overlay_slot(renderer, run);
self.batches.push(Batch::Glyphs {
cmds,
uniform_slot,
scissor: run.segment.scissor,
});
}
}
}
fn run_target_bounds(&self, draw: &RunDraw, run: &SegmentRun<'s, '_>) -> Option<TargetRect> {
let bounds = run_draw_bounds(draw, run.segment.scale)?;
let pixel = 1.0 / run.segment.scale;
scissor_rect_for_rect(
Rect {
x: bounds.x - pixel,
y: bounds.y - pixel,
width: bounds.width + 2.0 * pixel,
height: bounds.height + 2.0 * pixel,
},
run.segment.scale,
run.viewport,
)
}
fn run_items(
&mut self,
renderer: &mut GpuRenderer,
items: &mut Peekable<impl Iterator<Item = Item<'s>>>,
run: &SegmentRun<'s, '_>,
) {
while let Some(Item::Run(draw, window)) =
items.next_if(|item| matches!(item, Item::Run(..)))
{
if self
.run_target_bounds(draw, run)
.is_some_and(|bounds| self.pending_glyphs.overlaps(bounds))
{
self.flush(renderer, run);
}
let window = window.unwrap_or(0..u32::MAX);
if renderer.run_is_stored(draw) {
self.close_chunk(renderer, run);
let viewport = ViewportUniformParams {
depth_base: self.depth_seq as f32,
..run.viewport
};
let batch = renderer.prepare_store_run(
self.recorder,
draw,
viewport,
run.segment.scale,
&window,
self.depth,
);
if self.depth {
self.take_depth_range(&batch.draws);
}
self.batches.push(Batch::StoreRun {
batch,
scissor: run.segment.scissor,
});
} else {
let total = draw.record_count().min(window.end);
let mut from = window.start;
while from < total {
if self
.chunk
.is_some_and(|open| !renderer.arena_accepts(open, draw))
{
self.close_chunk(renderer, run);
}
let open = *self.chunk.get_or_insert_with(|| renderer.open_arena());
let taken = renderer.append_arena_run(
open,
draw,
from..total,
run.segment.scale,
!run.viewport.transform.is_identity(),
self.depth,
);
if taken == 0 {
self.close_chunk(renderer, run);
continue;
}
from += taken;
}
}
}
}
fn image_run(
&mut self,
renderer: &mut GpuRenderer,
items: &mut Peekable<impl Iterator<Item = Item<'s>>>,
run: &SegmentRun<'s, '_>,
scratch: &mut PassScratch,
) -> Result<(), String> {
let Some(Item::Image(first)) = items.peek() else {
unreachable!("image run starts at an image");
};
let images = &run.segment.scene.images;
let blend_mode = supported_blend_mode(images[*first].blend_mode);
let cmd_start = scratch.image_cmds.len();
while let Some(Item::Image(index)) = items.next_if(|item| {
matches!(item, Item::Image(index) if supported_blend_mode(images[*index].blend_mode) == blend_mode)
}) {
let image = &images[index];
renderer.append_image_draw_cmd(
image,
run.viewport,
run.segment.scale,
&mut scratch.image_vertices,
&mut scratch.image_indices,
&mut scratch.image_cmds,
)?;
}
if cmd_start < scratch.image_cmds.len() {
let uniform_slot = self.overlay_slot(renderer, run);
self.batches.push(Batch::Images {
cmds: cmd_start..scratch.image_cmds.len(),
blend_mode,
uniform_slot,
scissor: run.segment.scissor,
});
}
Ok(())
}
fn text_item(
&mut self,
renderer: &mut GpuRenderer,
text: &'s TextDraw,
run: &SegmentRun<'s, '_>,
scratch: &mut PassScratch,
) -> Result<(), String> {
let glyph_start = scratch.glyph_cmds.len();
let drew_glyphs = renderer.append_text_glyph_draws(
std::iter::once(text),
self.text_viewport(renderer, run),
run.segment.scale,
&mut scratch.glyph_instances,
&mut scratch.glyph_cmds,
)?;
if drew_glyphs {
let glyph_end = scratch.glyph_cmds.len();
if glyph_start < glyph_end {
if self.pending_glyphs.full() {
self.flush(renderer, run);
}
self.pending_glyphs.hold(
glyph_start..glyph_end,
scratch.glyph_cmds[glyph_start..glyph_end]
.iter()
.map(crate::render::GlyphDrawCmd::bounds),
);
}
return Ok(());
}
self.flush(renderer, run);
let cmd_start = scratch.image_cmds.len();
renderer.append_text_image_draw_cmds(
std::iter::once(text),
run.viewport,
run.segment.scale,
&mut scratch.image_vertices,
&mut scratch.image_indices,
&mut scratch.image_cmds,
)?;
if cmd_start < scratch.image_cmds.len() {
let continues = self.overlay_segment == Some(run.uniform_slot);
match self.batches.last_mut() {
Some(Batch::Images {
cmds, blend_mode, ..
}) if cmds.end == cmd_start && *blend_mode == BlendMode::SrcOver && continues => {
cmds.end = scratch.image_cmds.len();
}
_ => {
let uniform_slot = self.overlay_slot(renderer, run);
self.batches.push(Batch::Images {
cmds: cmd_start..scratch.image_cmds.len(),
blend_mode: BlendMode::SrcOver,
uniform_slot,
scissor: run.segment.scissor,
});
}
}
}
Ok(())
}
fn composite_item(
&mut self,
renderer: &mut GpuRenderer,
composite: &'s ResolvedComposite,
run: &SegmentRun<'s, '_>,
) -> Result<(), String> {
let offset = run.segment.offset;
let own_scissor = composite
.scissor
.and_then(|scissor| scissor_in_target(scissor, self.target_size(), offset));
if composite.scissor.is_some() && own_scissor.is_none() {
return Ok(());
}
let Some(scissor) = intersect_scissors(own_scissor, run.segment.scissor) else {
return Ok(());
};
let dest = dest_in_target(composite.dest, offset);
match &composite.kind {
ResolvedCompositeKind::Blit {
alpha,
blend_mode,
rounded_mask,
sample_mode,
source_viewport,
} => {
let item = CompositeBatchItem {
source: composite.source.as_ref(),
alpha: *alpha,
scissor,
rounded_mask: mask_in_target(*rounded_mask, offset),
blend_mode: supported_blend_mode(*blend_mode),
dest_viewport: Some(dest),
source_viewport: *source_viewport,
sample_mode: *sample_mode,
};
let prepared = renderer.effect_renderer.prepare_composite_draw(
self.recorder,
self.device,
self.load_op,
&item,
);
self.batches.push(Batch::Composite(prepared));
}
ResolvedCompositeKind::Shader {
shader,
layer_pixel_rect,
source_region,
source_logical_size,
substrate_regions,
rounded_mask,
alpha,
} => {
let item = ShaderCompositeBatchItem {
source: composite.source.as_ref(),
shader: shader.as_ref(),
layer_pixel_rect: *layer_pixel_rect,
source_region: *source_region,
source_logical_size: *source_logical_size,
substrate_regions: *substrate_regions,
rounded_mask: mask_in_target(*rounded_mask, offset),
alpha: *alpha,
scissor,
dest_viewport: dest,
};
let prepared = renderer
.effect_renderer
.prepare_shader_draw(self.recorder, self.device, &item)
.ok_or_else(|| "shader composite preparation failed".to_string())?;
self.batches.push(Batch::Shader(prepared));
}
ResolvedCompositeKind::Projective {
dest_quad,
inverse,
alpha,
blend_mode,
sample_mode,
source_region,
} => {
let item = ProjectiveCompositeItem {
source: composite.source.as_ref(),
source_region: *source_region,
viewport: self.target_size(),
dest_quad: dest_quad.map(|[x, y]| [x - offset[0], y - offset[1]]),
inverse: translate_inverse(*inverse, offset),
alpha: *alpha,
blend_mode: supported_blend_mode(*blend_mode),
sample_mode: *sample_mode,
scissor,
};
let prepared = renderer.effect_renderer.prepare_projective_composite_draw(
self.recorder,
self.device,
&item,
);
self.batches.push(Batch::Projective(prepared));
}
}
Ok(())
}
}
struct SegmentRun<'s, 'a> {
segment: &'a PassSegment<'s>,
viewport: ViewportUniformParams,
uniform_slot: usize,
}
#[cfg(test)]
#[path = "tests/draw_pass_tests.rs"]
mod tests;