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, QuadPlacement,
RoundedCompositeMask, ShaderCompositeBatchItem, SubstrateRegions,
},
frame_graph::FrameCommandRecorder,
geometry::SegmentTransform,
offscreen::OffscreenTarget,
render::{
GpuRenderer, PassFrame, RunStage, StoreRunBatch, TargetRect, ViewportUniformParams,
image_draw_bounds, run_draw_bounds, scissor_rect_for_rect, segment_scene_rect,
supported_blend_mode, text_draw_bounds, text_draw_geometry_in_rect,
},
rrect_shadow::{ShadowInstance, append_shadow_instances, rrect_shadow_bounds},
run_store::{RunDrawCall, run_has_shapes},
scene::{CompositorScene, DrawOp, DrawOpKind, RRectShadowDraw, RunDraw, TextDraw},
shape_pipelines::PipelineDemand,
};
#[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>>, Rect),
Image(usize),
Text(&'a TextDraw, (Rect, f32)),
RRectShadow(&'a RRectShadowDraw),
Composite(&'a ResolvedComposite),
}
enum Batch<'a> {
StoreRun {
batch: StoreRunBatch,
scissor: Option<(u32, u32, u32, u32)>,
},
Arena {
chunk: usize,
uniform_slot: usize,
paint: std::ops::Range<usize>,
interiors: Option<std::ops::Range<usize>>,
scissor: Option<(u32, u32, u32, u32)>,
clip: 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)>,
},
Shadows {
instances: std::ops::Range<u32>,
uniform_slot: usize,
scissor: Option<(u32, u32, u32, u32)>,
},
Composite(PreparedCompositeDraw<'a>),
Shader(PreparedShaderDraw<'a>),
Projective(PreparedProjectiveComposite<'a>),
}
pub(crate) struct TrackedPass<'a, 'p> {
pass: &'a mut wgpu::RenderPass<'p>,
scissor: Option<TargetRect>,
index: Option<(usize, wgpu::BufferAddress, wgpu::BufferAddress)>,
pipeline: Option<wgpu::RenderPipeline>,
groups: [Option<BoundGroup>; TRACKED_GROUPS],
vertices: [Option<(wgpu::Buffer, wgpu::BufferAddress, wgpu::BufferAddress)>; TRACKED_SLOTS],
}
const TRACKED_GROUPS: usize = 2;
const TRACKED_SLOTS: usize = 2;
const TRACKED_OFFSETS: usize = 4;
#[derive(PartialEq)]
struct BoundGroup {
group: wgpu::BindGroup,
offsets: [wgpu::DynamicOffset; TRACKED_OFFSETS],
offset_count: usize,
}
impl BoundGroup {
fn new(group: &wgpu::BindGroup, offsets: &[wgpu::DynamicOffset]) -> Option<Self> {
let mut tracked = [0; TRACKED_OFFSETS];
tracked.get_mut(..offsets.len())?.copy_from_slice(offsets);
Some(Self {
group: group.clone(),
offsets: tracked,
offset_count: offsets.len(),
})
}
fn is(&self, group: &wgpu::BindGroup, offsets: &[wgpu::DynamicOffset]) -> bool {
self.group == *group && self.offsets.get(..self.offset_count) == Some(offsets)
}
}
impl<'a, 'p> TrackedPass<'a, 'p> {
pub(crate) fn new(pass: &'a mut wgpu::RenderPass<'p>) -> Self {
Self {
pass,
scissor: None,
index: None,
pipeline: None,
groups: [const { None }; TRACKED_GROUPS],
vertices: [const { None }; TRACKED_SLOTS],
}
}
pub(crate) fn set_scissor_rect(&mut self, x: u32, y: u32, width: u32, height: u32) {
if self.scissor != Some((x, y, width, height)) {
self.pass.set_scissor_rect(x, y, width, height);
self.scissor = Some((x, y, width, height));
}
}
pub(crate) fn set_index_buffer(&mut self, slice: wgpu::BufferSlice<'_>) {
let bound = (
std::ptr::from_ref(slice.buffer()).addr(),
slice.offset(),
slice.size(),
);
if self.index != Some(bound) {
self.pass.set_index_buffer(slice, wgpu::IndexFormat::Uint32);
self.index = Some(bound);
}
}
pub(crate) fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
if self.pipeline.as_ref() != Some(pipeline) {
self.pass.set_pipeline(pipeline);
self.pipeline = Some(pipeline.clone());
}
}
pub(crate) fn set_bind_group(
&mut self,
index: u32,
bind_group: &wgpu::BindGroup,
offsets: &[wgpu::DynamicOffset],
) {
let tracked = usize::try_from(index)
.ok()
.and_then(|index| self.groups.get_mut(index));
match tracked {
Some(bound) => {
if !bound
.as_ref()
.is_some_and(|bound| bound.is(bind_group, offsets))
{
self.pass.set_bind_group(index, bind_group, offsets);
*bound = BoundGroup::new(bind_group, offsets);
}
}
None => self.pass.set_bind_group(index, bind_group, offsets),
}
}
pub(crate) fn set_vertex_buffer(&mut self, slot: u32, slice: wgpu::BufferSlice<'_>) {
let bound = (slice.buffer(), slice.offset(), slice.size());
let tracked = usize::try_from(slot)
.ok()
.and_then(|slot| self.vertices.get_mut(slot));
match tracked {
Some(tracked) => {
if tracked
.as_ref()
.map(|(buffer, offset, size)| (buffer, *offset, *size))
!= Some(bound)
{
self.pass.set_vertex_buffer(slot, slice);
*tracked = Some((bound.0.clone(), bound.1, bound.2));
}
}
None => self.pass.set_vertex_buffer(slot, slice),
}
}
pub(crate) fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
self.pass.draw(vertices, instances);
}
pub(crate) fn draw_indexed(
&mut self,
indices: std::ops::Range<u32>,
base_vertex: i32,
instances: std::ops::Range<u32>,
) {
self.pass.draw_indexed(indices, base_vertex, instances);
}
pub(crate) fn untracked(&mut self) -> &mut wgpu::RenderPass<'p> {
self.scissor = None;
self.index = None;
self.pipeline = None;
self.groups = [const { None }; TRACKED_GROUPS];
self.vertices = [const { None }; TRACKED_SLOTS];
self.pass
}
}
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 stage_scissor(
scissor: TargetRect,
clip: Option<TargetRect>,
stage: RunStage,
) -> Option<TargetRect> {
match (stage, clip) {
(RunStage::Paint, Some(clip)) => crate::render::intersect_target_rects(scissor, clip),
_ => Some(scissor),
}
}
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 {
fn upload_pass_buffers<C: FrameCommandRecorder>(
&self,
recorder: &mut C,
scratch: &PassScratch,
) -> PassBuffers {
PassBuffers {
images: (!scratch.image_indices.is_empty()).then(|| {
self.upload_image_slot(
recorder,
&scratch.image_vertices,
&scratch.image_indices,
&scratch.image_clips,
)
}),
shadows: (!scratch.shadow_instances.is_empty())
.then(|| self.upload_shadow_instances(recorder, &scratch.shadow_instances)),
glyphs: (!scratch.glyph_instances.plain.is_empty())
.then(|| self.upload_glyph_instances(recorder, &scratch.glyph_instances.plain)),
turned_glyphs: (!scratch.glyph_instances.turned.is_empty())
.then(|| self.upload_turned_glyphs(recorder, &scratch.glyph_instances.turned)),
}
}
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 = self.interiors_first && takes_depth(segments);
let mut prep = PassPrep {
recorder,
device: &device,
target,
load_op,
batches: Vec::new(),
chunk: None,
held: std::mem::take(&mut scratch.held),
depth,
mixed_turns: turns_mixed(segments),
overlay_segment: None,
overlay_images: None,
depth_seq: 0,
open: None,
arena_draws: std::mem::take(&mut scratch.arena_draws),
pipelines: std::mem::take(&mut scratch.pipelines),
chunk_start: 0,
chunk_base: 0,
chunk_slot: None,
chunk_clip: None,
};
let prepared = segments
.iter()
.try_for_each(|segment| prep.segment(self, segment, &mut scratch));
prep.finish(self);
self.ensure_run_pipelines(&mut prep.pipelines);
let batches = prep.batches;
scratch.arena_draws = prep.arena_draws;
scratch.pipelines = prep.pipelines;
scratch.held = prep.held;
let buffers = if prepared.is_ok() {
self.upload_pass_buffers(recorder, &scratch)
} else {
PassBuffers::default()
};
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);
}
group_glyph_batches(&batches, &mut scratch);
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 TrackedPass::new(&mut pass),
frame,
&batches,
PassCmds {
images: &scratch.image_cmds,
glyphs: &scratch.glyph_cmds,
arena: &scratch.arena_draws,
},
&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),
image_clips: std::mem::take(&mut self.scratch_image_clips),
glyph_instances: std::mem::take(&mut self.scratch_glyph_instances),
glyph_cmds: std::mem::take(&mut self.scratch_glyph_cmds),
glyph_moved: std::mem::take(&mut self.scratch_glyph_moved),
shadow_instances: std::mem::take(&mut self.scratch_shadow_instances),
arena_draws: std::mem::take(&mut self.scratch_arena_draws),
pipelines: std::mem::take(&mut self.scratch_pipelines),
held: std::mem::take(&mut self.scratch_held),
};
scratch.arena_draws.clear();
scratch.image_vertices.clear();
scratch.image_indices.clear();
scratch.image_cmds.clear();
scratch.image_clips.clear();
scratch.glyph_instances.clear();
scratch.glyph_cmds.clear();
scratch.shadow_instances.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_image_clips = scratch.image_clips;
self.scratch_glyph_instances = scratch.glyph_instances;
self.scratch_glyph_cmds = scratch.glyph_cmds;
self.scratch_glyph_moved = scratch.glyph_moved;
self.scratch_shadow_instances = scratch.shadow_instances;
self.scratch_arena_draws = scratch.arena_draws;
self.scratch_pipelines = scratch.pipelines;
self.scratch_held = scratch.held;
}
fn draw_batches(
&mut self,
pass: &mut TrackedPass<'_, '_>,
frame: PassFrame,
batches: &[Batch<'_>],
cmds: PassCmds<'_>,
buffers: &PassBuffers,
) -> Result<(), String> {
let target_size = frame.size;
if frame.depth {
for batch in batches.iter().rev() {
self.draw_shape_batch(pass, batch, cmds.arena, frame, RunStage::Interiors)?;
}
}
for batch in batches {
match batch {
Batch::StoreRun { .. } | Batch::Arena { .. } => {
self.draw_shape_batch(pass, batch, cmds.arena, frame, RunStage::Paint)?;
}
Batch::Images {
cmds: range,
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,
&cmds.images[range.clone()],
(*blend_mode, frame.depth),
*scissor,
)?;
}
Batch::Glyphs {
cmds: range,
uniform_slot,
scissor,
} => {
self.draw_glyph_cmds(
pass,
(buffers.glyphs.as_ref(), buffers.turned_glyphs.as_ref()),
*uniform_slot,
&cmds.glyphs[range.clone()],
*scissor,
frame,
)?;
}
Batch::Shadows {
instances,
uniform_slot,
scissor,
} => {
let buffer = buffers
.shadows
.as_ref()
.ok_or_else(|| "shadow batch without shadow instances".to_string())?;
self.draw_shadow_instances(
pass,
buffer,
*uniform_slot,
instances.clone(),
*scissor,
frame,
)?;
}
Batch::Composite(prepared) => {
self.effect_renderer.draw_prepared_composite(
pass.untracked(),
target_size,
prepared,
);
}
Batch::Shader(prepared) => {
self.effect_renderer.draw_prepared_shader_src_over(
pass.untracked(),
target_size,
prepared,
);
}
Batch::Projective(prepared) => {
self.effect_renderer.draw_prepared_projective_composite(
pass.untracked(),
target_size,
prepared,
);
}
}
}
Ok(())
}
}
impl GpuRenderer {
fn draw_shape_batch(
&self,
pass: &mut TrackedPass<'_, '_>,
batch: &Batch<'_>,
arena: &[RunDrawCall],
frame: PassFrame,
stage: RunStage,
) -> Result<(), String> {
match batch {
Batch::StoreRun { batch, scissor } => {
match stage_scissor(frame.scissor(*scissor), batch.clip, stage) {
Some(scissor) => self.draw_store_run(pass, batch, arena, scissor, stage),
None => Ok(()),
}
}
Batch::Arena {
chunk,
uniform_slot,
paint,
interiors,
scissor,
clip,
} => {
let draws = match stage {
RunStage::Paint => paint.clone(),
RunStage::Interiors => match interiors {
Some(interiors) => interiors.clone(),
None => return Ok(()),
},
};
let Some(scissor) = stage_scissor(frame.scissor(*scissor), *clip, stage) else {
return Ok(());
};
self.draw_arena(pass, *chunk, *uniform_slot, &arena[draws], 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 {
self.frame_stats
.depth_passes
.set(self.frame_stats.depth_passes.get() + 1);
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)
}
}
}
const MIXED_TURN_CHANGES: usize = 8;
fn turns_mixed(segments: &[PassSegment<'_>]) -> bool {
segments
.windows(2)
.filter(|pair| pair[0].transform.is_identity() != pair[1].transform.is_identity())
.count()
>= MIXED_TURN_CHANGES
}
pub(crate) fn lays_interiors_first(backend: wgpu::Backend) -> bool {
!matches!(backend, wgpu::Backend::Gl | wgpu::Backend::BrowserWebGpu)
}
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()
.enumerate()
.any(|(op_index, op)| match op.kind {
DrawOpKind::Run(index) => {
let window = (op_index == 0)
.then(|| segment.first_run_window.clone())
.flatten()
.unwrap_or(0..u32::MAX);
let run = &segment.scene.runs[index];
let shapes = &run.tables().shapes;
let mut first = 0;
run.segment_records().any(|records| {
let start = first;
first += records.count;
let mut drawn = start.max(window.start)..first.min(window.end);
if !records.occluders || drawn.is_empty() {
return false;
}
drawn == (start..first)
|| drawn.any(|ordinal| {
shapes.occludes((records.start + ordinal - start) as usize)
})
})
}
_ => 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::RRectShadow(index) => {
rrect_shadow_bounds(&scene.rrect_shadows[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 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(item) = shadow_texts.find_map(|text| {
text_draw_geometry_in_rect(text, viewport_rect, root_scale)
.map(|geometry| Item::Text(text, geometry))
}) {
return Some(item);
}
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()?;
let window = (op_index == 0).then(|| first_run_window.clone()).flatten();
match op_item(scene, op, window, (viewport_rect, root_scale, skip_text)) {
OpItem::Draw(item) => return Some(item),
OpItem::Shadow(texts, casters) => {
shadow_texts = texts;
if let Some(item) = casters {
return Some(item);
}
}
OpItem::Nothing => {}
}
}
})
}
enum OpItem<'a> {
Draw(Item<'a>),
Shadow(std::slice::Iter<'a, TextDraw>, Option<Item<'a>>),
Nothing,
}
fn op_item<'a>(
scene: &'a CompositorScene,
op: &DrawOp,
window: Option<std::ops::Range<u32>>,
(viewport_rect, root_scale, skip_text): (Rect, f32, bool),
) -> OpItem<'a> {
let item = match op.kind {
DrawOpKind::Text(index) => {
let text = &scene.texts[index];
text_draw_geometry_in_rect(text, viewport_rect, root_scale)
.filter(|_| !skip_text)
.map(|geometry| Item::Text(text, geometry))
}
DrawOpKind::Run(index) => {
let run = &scene.runs[index];
visible_run_bounds(run, viewport_rect, root_scale)
.filter(|_| run_has_shapes(run))
.map(|bounds| Item::Run(run, window, bounds))
}
_ if !op_is_visible_in_rect(scene, op, viewport_rect, root_scale) => None,
DrawOpKind::Shadow(index) => {
let shadow = &scene.shadow_draws[index];
let casters = shadow.shapes.as_ref().and_then(|run| {
visible_run_bounds(run, viewport_rect, root_scale)
.map(|bounds| Item::Run(run, None, bounds))
});
return OpItem::Shadow(shadow.texts.iter(), casters);
}
DrawOpKind::Image(index) => Some(Item::Image(index)),
DrawOpKind::RRectShadow(index) => Some(Item::RRectShadow(&scene.rrect_shadows[index])),
};
item.map_or(OpItem::Nothing, OpItem::Draw)
}
fn visible_run_bounds(run: &RunDraw, viewport_rect: Rect, root_scale: f32) -> Option<Rect> {
run_draw_bounds(run, root_scale).filter(|bounds| bounds.intersect(viewport_rect).is_some())
}
#[derive(Default)]
struct PassBuffers {
images: Option<crate::render::ImageSlot>,
shadows: Option<crate::frame_graph::BufferUpload>,
glyphs: Option<crate::frame_graph::BufferUpload>,
turned_glyphs: Option<crate::frame_graph::BufferUpload>,
}
struct PassScratch {
image_vertices: Vec<crate::render::Vertex>,
image_indices: Vec<u32>,
image_cmds: Vec<crate::render::ImageDrawCmd>,
image_clips: Vec<crate::render::DeviceRoundedClip>,
glyph_instances: crate::render::GlyphInstances,
glyph_cmds: Vec<crate::render::GlyphDrawCmd>,
glyph_moved: Vec<crate::render::GlyphDrawCmd>,
shadow_instances: Vec<ShadowInstance>,
arena_draws: Vec<RunDrawCall>,
pipelines: PipelineDemand,
held: HeldDraws,
}
#[derive(Clone, Copy)]
struct PassCmds<'a> {
images: &'a [crate::render::ImageDrawCmd],
glyphs: &'a [crate::render::GlyphDrawCmd],
arena: &'a [RunDrawCall],
}
fn group_glyph_batches(batches: &[Batch<'_>], scratch: &mut PassScratch) {
for batch in batches {
if let Batch::Glyphs { cmds, .. } = batch {
let cmds = &mut scratch.glyph_cmds[cmds.clone()];
let shared = crate::render::defer_retained_glyph_runs(cmds, &mut scratch.glyph_moved);
crate::render::group_glyph_kinds(&mut cmds[..shared], &mut scratch.glyph_moved);
}
}
}
const MAX_HELD_DRAWS: usize = 256;
pub(crate) 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)
}
const HELD_CELL_SHIFT: u32 = 6;
fn held_cells(rect: TargetRect) -> (std::ops::Range<usize>, u64) {
let (x, y, width, height) = rect;
let last_column = (x.saturating_add(width.max(1) - 1) >> HELD_CELL_SHIFT).min(63);
let first_column = (x >> HELD_CELL_SHIFT).min(63);
let columns = last_column - first_column + 1;
let mask = if columns >= 64 {
u64::MAX
} else {
((1_u64 << columns) - 1) << first_column
};
let first_row = (y >> HELD_CELL_SHIFT) as usize;
let last_row = (y.saturating_add(height.max(1) - 1) >> HELD_CELL_SHIFT) as usize;
(first_row..last_row + 1, mask)
}
#[derive(Default)]
pub(crate) struct HeldDraws {
cmds: Option<std::ops::Range<usize>>,
images: Option<(std::ops::Range<usize>, ViewportUniformParams)>,
held: usize,
image_bounds: Vec<TargetRect>,
union: Option<TargetRect>,
cells: Vec<u64>,
rows: Vec<Vec<TargetRect>>,
}
impl HeldDraws {
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.mark(rect);
}
}
fn hold_image(
&mut self,
cmds: std::ops::Range<usize>,
viewport: ViewportUniformParams,
bounds: TargetRect,
) {
let cmds = match self.images.take() {
Some((held, _)) => held.start..cmds.end,
None => cmds,
};
self.images = Some((cmds, viewport));
self.image_bounds.push(bounds);
self.mark(bounds);
}
fn mark(&mut self, rect: TargetRect) {
self.union = Some(
self.union
.map_or(rect, |union| target_rect_union(union, rect)),
);
self.held += 1;
let (rows, mask) = held_cells(rect);
if self.cells.len() < rows.end {
self.cells.resize(rows.end, 0);
}
if self.rows.len() < rows.end {
self.rows.resize_with(rows.end, Vec::new);
}
for (cells, held) in self.cells[rows.clone()]
.iter_mut()
.zip(&mut self.rows[rows])
{
*cells |= mask;
held.push(rect);
}
}
fn is_empty(&self) -> bool {
self.union.is_none()
}
fn overlaps(&self, rect: TargetRect) -> bool {
if !self
.union
.is_some_and(|union| target_rects_overlap(union, rect))
{
return false;
}
let (rows, mask) = held_cells(rect);
let end = rows.end.min(self.cells.len());
let start = rows.start.min(end);
self.cells[start..end]
.iter()
.zip(&self.rows[start..end])
.any(|(cells, held)| {
cells & mask != 0 && held.iter().any(|held| target_rects_overlap(*held, rect))
})
}
fn covers_image(&self, rect: TargetRect) -> bool {
self.image_bounds
.iter()
.any(|held| target_rects_overlap(*held, rect))
}
fn image_viewport(&self) -> Option<ViewportUniformParams> {
self.images.as_ref().map(|(_, viewport)| *viewport)
}
fn full(&self) -> bool {
self.held >= MAX_HELD_DRAWS
}
fn take(&mut self) -> HeldCmds {
self.held = 0;
self.image_bounds.clear();
for held in &mut self.rows[..self.cells.len()] {
held.clear();
}
self.cells.clear();
self.union = None;
HeldCmds {
glyphs: self.cmds.take(),
images: self.images.take(),
}
}
}
struct HeldCmds {
glyphs: Option<std::ops::Range<usize>>,
images: Option<(std::ops::Range<usize>, ViewportUniformParams)>,
}
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>,
held: HeldDraws,
depth: bool,
mixed_turns: bool,
overlay_segment: Option<usize>,
overlay_images: Option<ViewportUniformParams>,
depth_seq: u32,
open: Option<SegmentBinding>,
arena_draws: Vec<RunDrawCall>,
pipelines: PipelineDemand,
chunk_start: usize,
chunk_base: u32,
chunk_slot: Option<usize>,
chunk_clip: Option<TargetRect>,
}
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 bound = ViewportUniformParams {
transform: SegmentTransform::IDENTITY,
..viewport
};
let binding = match self.open {
Some(open) if open.bound == bound && open.scissor == segment.scissor => open,
_ => {
self.finish(renderer);
let open = SegmentBinding {
uniform_slot: renderer.claim_uniform_slot(bound),
bound,
scissor: segment.scissor,
};
self.open = Some(open);
self.overlay_segment = None;
self.overlay_images = None;
open
}
};
let mut items = merge_items(
segment,
viewport_rect,
self.target_size(),
renderer.ablation.text,
)
.peekable();
let run = SegmentRun {
segment,
viewport,
binding,
};
while let Some(item) = items.peek() {
match item {
Item::Run(..) => {
self.run_items(renderer, &mut items, &run);
continue;
}
Item::Image(_) => {
self.image_items(renderer, &mut items, &run, scratch)?;
continue;
}
Item::Text(text, geometry) => {
self.text_item(renderer, (text, *geometry), &run, scratch)?;
}
Item::RRectShadow(_) => {
self.flush(renderer, run.binding);
self.shadow_run(renderer, &mut items, &run, scratch);
continue;
}
Item::Composite(composite) => {
self.flush(renderer, run.binding);
self.composite_item(renderer, composite, &run)?;
}
}
items.next();
}
Ok(())
}
fn finish(&mut self, renderer: &mut GpuRenderer) {
if let Some(open) = self.open.take() {
self.flush(renderer, open);
}
}
fn open_chunk(&mut self, renderer: &mut GpuRenderer) -> usize {
let open = renderer.open_arena();
self.chunk = Some(open);
self.chunk_start = self.arena_draws.len();
self.chunk_base = self.depth_seq;
self.chunk_slot = None;
open
}
fn chunk_uniform_slot(&mut self, renderer: &mut GpuRenderer, binding: SegmentBinding) -> usize {
if let Some(slot) = self.chunk_slot {
return slot;
}
let slot = if self.depth {
renderer.claim_uniform_slot(ViewportUniformParams {
depth_base: self.chunk_base as f32,
..binding.bound
})
} else {
binding.uniform_slot
};
self.chunk_slot = Some(slot);
slot
}
fn close_chunk(&mut self, renderer: &mut GpuRenderer, binding: SegmentBinding) {
let Some(open) = self.chunk.take() else {
return;
};
let records = renderer.open_arena_records();
let paint_start = self.arena_draws.len();
renderer.close_arena(open, &mut self.arena_draws);
let end = self.arena_draws.len();
if end == self.chunk_start {
return;
}
let uniform_slot = self.chunk_uniform_slot(renderer, binding);
if self.depth {
self.depth_seq = self.chunk_base.saturating_add(records);
}
self.batches.push(Batch::Arena {
chunk: open,
uniform_slot,
paint: paint_start..end,
interiors: Some(self.chunk_start..end),
scissor: binding.scissor,
clip: self.chunk_clip,
});
}
fn draw_held(&mut self, renderer: &mut GpuRenderer, binding: SegmentBinding) {
let Some(open) = self.chunk else {
self.flush(renderer, binding);
return;
};
let held = self.held.take();
if held.glyphs.is_none() && held.images.is_none() {
return;
}
self.cut_paint(renderer, binding, open);
let base = self
.chunk_base
.saturating_add(renderer.open_arena_records());
self.push_held(renderer, binding, held, Some(base));
}
fn cut_paint(&mut self, renderer: &mut GpuRenderer, binding: SegmentBinding, open: usize) {
let paint_start = self.arena_draws.len();
renderer.cut_arena(open, &mut self.arena_draws);
let end = self.arena_draws.len();
if end > paint_start {
let uniform_slot = self.chunk_uniform_slot(renderer, binding);
self.batches.push(Batch::Arena {
chunk: open,
uniform_slot,
paint: paint_start..end,
interiors: None,
scissor: binding.scissor,
clip: self.chunk_clip,
});
}
}
fn placed_slot(
&mut self,
renderer: &mut GpuRenderer,
binding: SegmentBinding,
viewport: ViewportUniformParams,
base: Option<u32>,
) -> usize {
if self.depth {
let base = base.unwrap_or_else(|| {
let next = self.depth_seq;
self.depth_seq = next.saturating_add(1);
next
});
renderer.claim_uniform_slot(ViewportUniformParams {
depth_base: base as f32,
..viewport
})
} else if viewport == binding.bound {
binding.uniform_slot
} else {
renderer.claim_uniform_slot(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: std::ops::Range<usize>) {
let records = self.arena_draws[draws]
.iter()
.map(|draw| draw.records.end)
.max()
.unwrap_or(0);
self.depth_seq = self.depth_seq.saturating_add(records);
}
fn flush(&mut self, renderer: &mut GpuRenderer, binding: SegmentBinding) {
self.close_chunk(renderer, binding);
let held = self.held.take();
self.push_held(renderer, binding, held, None);
}
fn push_held(
&mut self,
renderer: &mut GpuRenderer,
binding: SegmentBinding,
held: HeldCmds,
base: Option<u32>,
) {
if let Some(cmds) = held.glyphs {
self.push_glyphs(renderer, binding, cmds, base);
}
if let Some((cmds, viewport)) = held.images {
self.push_images(
renderer,
(binding, viewport),
cmds,
(BlendMode::SrcOver, base),
);
}
}
fn push_glyphs(
&mut self,
renderer: &mut GpuRenderer,
binding: SegmentBinding,
cmds: std::ops::Range<usize>,
base: Option<u32>,
) {
let continues = base.is_none() && self.overlay_segment == Some(binding.uniform_slot);
if let Some(Batch::Glyphs { cmds: last, .. }) = self.batches.last_mut()
&& continues
&& last.end == cmds.start
{
last.end = cmds.end;
return;
}
let uniform_slot = self.placed_slot(renderer, binding, binding.bound, base);
self.overlay_segment = Some(binding.uniform_slot);
self.batches.push(Batch::Glyphs {
cmds,
uniform_slot,
scissor: binding.scissor,
});
}
fn push_images(
&mut self,
renderer: &mut GpuRenderer,
(binding, viewport): (SegmentBinding, ViewportUniformParams),
cmds: std::ops::Range<usize>,
(blend_mode, base): (BlendMode, Option<u32>),
) {
if cmds.is_empty() {
return;
}
let continues = base.is_none()
&& blend_mode == BlendMode::SrcOver
&& self.overlay_images == Some(viewport);
if let Some(Batch::Images {
cmds: last,
blend_mode: BlendMode::SrcOver,
..
}) = self.batches.last_mut()
&& continues
&& last.end == cmds.start
{
last.end = cmds.end;
return;
}
let uniform_slot = self.placed_slot(renderer, binding, viewport, base);
self.overlay_images = Some(viewport);
self.batches.push(Batch::Images {
cmds,
blend_mode,
uniform_slot,
scissor: binding.scissor,
});
}
fn run_target_bounds(&self, bounds: Rect, run: &SegmentRun<'s, '_>) -> Option<TargetRect> {
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, bounds)) =
items.next_if(|item| matches!(item, Item::Run(..)))
{
if !self.held.is_empty()
&& self
.run_target_bounds(bounds, run)
.is_some_and(|bounds| self.held.overlaps(bounds))
{
self.draw_held(renderer, run.binding);
}
let window = window.unwrap_or(0..u32::MAX);
if renderer.run_is_stored(draw) {
self.close_chunk(renderer, run.binding);
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,
(&mut self.arena_draws, &mut self.pipelines),
);
if self.depth {
self.take_depth_range(batch.draws.clone());
}
self.batches.push(Batch::StoreRun {
batch,
scissor: run.segment.scissor,
});
} else {
let total = draw.record_count().min(window.end);
let mut from = window.start;
let clip =
crate::render::clip_scissor(&draw.placement, run.segment.scale, run.viewport);
if let Some(open) = self.chunk
&& clip != self.chunk_clip
{
self.cut_paint(renderer, run.binding, open);
}
self.chunk_clip = clip;
let segment_clip = crate::render::SegmentClip::of(&draw.placement, clip.is_some());
while from < total {
if self
.chunk
.is_some_and(|open| !renderer.arena_accepts(open, draw))
{
self.close_chunk(renderer, run.binding);
}
let open = match self.chunk {
Some(open) => open,
None => self.open_chunk(renderer),
};
let taken = renderer.append_arena_run(
open,
draw,
from..total,
run.segment.scale,
run.viewport,
self.mixed_turns,
(self.depth, segment_clip),
&mut self.pipelines,
);
if taken == 0 {
self.close_chunk(renderer, run.binding);
continue;
}
from += taken;
}
}
}
}
fn image_items(
&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(index)) = items.peek() else {
unreachable!("an image run starts at an image");
};
let image = &run.segment.scene.images[*index];
if supported_blend_mode(image.blend_mode) != BlendMode::SrcOver {
self.flush(renderer, run.binding);
return self.image_run(renderer, items, run, scratch);
}
items.next();
if self.held.full()
|| self
.held
.image_viewport()
.is_some_and(|viewport| viewport != run.viewport)
{
self.draw_held(renderer, run.binding);
}
let start = scratch.image_cmds.len();
renderer.append_image_draw_cmd(
image,
run.viewport,
run.segment.scale,
(&mut scratch.image_vertices, &mut scratch.image_indices),
&mut scratch.image_clips,
&mut scratch.image_cmds,
)?;
if let Some(bounds) = scratch
.image_cmds
.get(start)
.map(crate::render::ImageDrawCmd::bounds)
{
self.held
.hold_image(start..scratch.image_cmds.len(), run.viewport, bounds);
}
Ok(())
}
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_clips,
&mut scratch.image_cmds,
)?;
}
self.push_images(
renderer,
(run.binding, run.viewport),
cmd_start..scratch.image_cmds.len(),
(blend_mode, None),
);
Ok(())
}
fn shadow_run(
&mut self,
renderer: &mut GpuRenderer,
items: &mut Peekable<impl Iterator<Item = Item<'s>>>,
run: &SegmentRun<'s, '_>,
scratch: &mut PassScratch,
) {
let start = scratch.shadow_instances.len();
while let Some(Item::RRectShadow(draw)) =
items.next_if(|item| matches!(item, Item::RRectShadow(_)))
{
append_shadow_instances(draw, run.segment.scale, &mut scratch.shadow_instances);
}
let end = scratch.shadow_instances.len();
if start == end {
return;
}
let uniform_slot = self.placed_slot(renderer, run.binding, run.viewport, None);
self.batches.push(Batch::Shadows {
instances: start as u32..end as u32,
uniform_slot,
scissor: run.segment.scissor,
});
}
fn text_item(
&mut self,
renderer: &mut GpuRenderer,
(text, geometry): (&'s TextDraw, (Rect, f32)),
run: &SegmentRun<'s, '_>,
scratch: &mut PassScratch,
) -> Result<(), String> {
let glyph_start = scratch.glyph_cmds.len();
let drew_glyphs = renderer.append_text_glyph_draws(
(text, geometry),
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 {
let cmds = &scratch.glyph_cmds[glyph_start..glyph_end];
if self.held.full() || cmds.iter().any(|cmd| self.held.covers_image(cmd.bounds())) {
self.draw_held(renderer, run.binding);
}
self.held.hold(
glyph_start..glyph_end,
cmds.iter().map(crate::render::GlyphDrawCmd::bounds),
);
}
return Ok(());
}
self.flush(renderer, run.binding);
let cmd_start = scratch.image_cmds.len();
renderer.append_text_image_draw_cmds(
text,
run.viewport,
run.segment.scale,
&mut scratch.image_vertices,
&mut scratch.image_indices,
&mut scratch.image_cmds,
)?;
self.push_images(
renderer,
(run.binding, run.viewport),
cmd_start..scratch.image_cmds.len(),
(BlendMode::SrcOver, None),
);
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 Some(placement) = QuadPlacement::in_target(dest, self.target_size()) else {
return Ok(());
};
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,
placement,
};
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,
binding: SegmentBinding,
}
#[derive(Clone, Copy)]
struct SegmentBinding {
uniform_slot: usize,
bound: ViewportUniformParams,
scissor: Option<(u32, u32, u32, u32)>,
}
#[cfg(test)]
#[path = "tests/draw_pass_tests.rs"]
mod tests;