use core::ops::Range;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Once};
#[cfg(any(test, feature = "perf-trace"))]
use std::time::Duration;
use frust_gpu::{PipelineCache, PooledTexture, SceneTextureId, ShaderLibrary, TierCaps};
use frust_scene::{Scene, SceneBuilder};
use glifo::{AtlasCommand, AtlasCommandRecorder, AtlasPaint};
use kurbo::Affine;
use peniko::{Brush, Color};
use vello_common::encode::{EncodedImage, EncodedPaint};
use vello_common::fearless_simd::Level;
use vello_common::paint::{ImageId, ImageSource, Paint};
use vello_common::strip::Strip;
use crate::cache::images::{ATLAS_PADDING, AtlasRegion};
use crate::cache::{
AtlasBudget, BYTES_PER_TEXEL, CachedRamp, GradientCache, GradientTextureLayout, ResidentImage,
};
#[cfg(any(test, feature = "perf-trace"))]
use crate::compile::CompileSpans;
use crate::compile::paint::resolve_lut_request;
use crate::compile::{ClearPunch, CompiledFrame, PhaseClock, SceneCompiler};
use crate::config;
use crate::diag::{EngineSpan, FrameTimestamps};
use crate::error::EngineError;
use crate::filters::blur::{FilterInstanceData, GpuFilterData, GpuGaussianBlur};
use crate::filters::drop_shadow::GpuDropShadow;
use crate::filters::{FilterStep, ServedFilter, served_filter};
use crate::gpu::atlas::{
AtlasPageBuffers, AtlasRenderReport, AtlasRenderer, lower_encoded_image, push_solid_strips,
};
use crate::gpu::bindings::{ExternalRuns, ExternalTextures, lower_encoded_external};
use crate::gpu::depth::DepthAttachment;
use crate::gpu::paint_texture::lower_encoded_paint;
use crate::gpu::pipelines::{EnginePipeline, EngineShaders, atlas_strip_desc, warm_up_descs};
use crate::gpu::strips::{PaintType, pack_paint_descriptor};
use crate::gpu::targets::{
IntermediateTargets, IntermediateTexture, filter_data_texture_descriptor,
filter_data_texture_height, filter_sampler,
};
use crate::gpu::{self, AtlasArray, GpuConfig, GpuEncodedPaint, GpuStrip, StripDraw};
use crate::schedule::pages::{PageConfig, PageSize};
use crate::schedule::{
Composite, MAX_LIVE_PAGES, PageParity, PageTarget, Round, RoundOp, Schedule,
};
use crate::{EngineTarget, OutputAlpha};
use vello_common::geometry::SizeU16;
use vello_common::record::RecordedLayerKind;
const SOLID_PAINT: u32 = pack_paint_descriptor(PaintType::Solid, 0);
const MIN_RESOURCE_TEXTURE_DIM: u32 = 64;
const MIN_INSTANCE_CAPACITY: u64 = 4096;
const LAYER_PAINT_SOURCE: u32 = 1 << 29;
const PUNCH_SOURCE: u32 = u32::MAX;
const PAGE_LABEL: &str = "frust-engine layer page";
const FILTER_LABEL: &str = "frust-engine filter pass";
const FILTER_QUAD_VERTICES: u32 = 4;
const MIN_FILTER_INSTANCE_CAPACITY: u64 = 16;
static INDEXED_PAINT_WARNING: Once = Once::new();
static ATLAS_REFUSAL_WARNING: Once = Once::new();
#[cfg(feature = "perf-trace")]
#[derive(Debug, Clone, Copy, Default)]
struct EncodeSpans {
compile: CompileSpans,
compile_total: Duration,
schedule: Duration,
paints: Duration,
instances: Duration,
resize: Duration,
upload: Duration,
replay: Duration,
pipelines: Duration,
record: Duration,
counts: EncodeCounts,
}
#[cfg(feature = "perf-trace")]
const ENCODE_TRACE_COLUMNS: [&str; 17] = [
"validate",
"prepare",
"classify",
"admit",
"walk",
"glyphs",
"finish",
"compile",
"schedule",
"paints",
"instances",
"resize",
"upload",
"replay",
"pipelines",
"record",
"total",
];
#[cfg(feature = "perf-trace")]
const ENCODE_TRACE_COUNTS: [&str; 5] = ["draws", "strips", "alphas", "glyph_draws", "atlas_glyphs"];
#[cfg(feature = "perf-trace")]
const ENCODE_TRACE_ROW: usize = ENCODE_TRACE_COLUMNS.len() + ENCODE_TRACE_COUNTS.len();
#[cfg(feature = "perf-trace")]
#[derive(Debug, Clone, Copy, Default)]
struct EncodeCounts {
draws: u32,
strips: u32,
alphas: u32,
glyph_draws: u32,
atlas_glyphs: u32,
}
#[cfg(feature = "perf-trace")]
impl EncodeCounts {
fn of(frame: &CompiledFrame) -> Self {
fn count(len: usize) -> u32 {
u32::try_from(len).unwrap_or(u32::MAX)
}
Self {
draws: count(frame.draws().len()),
strips: count(frame.strip_buf().len()),
alphas: count(frame.alphas().len()),
glyph_draws: frame.glyph_draws,
atlas_glyphs: frame.atlas_glyph_draws,
}
}
}
#[cfg(feature = "perf-trace")]
const ENCODE_TRACE_WINDOW: usize = 60;
#[cfg(feature = "perf-trace")]
impl EncodeSpans {
fn total(&self) -> Duration {
[
self.schedule,
self.paints,
self.instances,
self.resize,
self.upload,
self.replay,
self.pipelines,
self.record,
]
.iter()
.fold(self.compile_total, |acc, span| acc.saturating_add(*span))
}
fn row(&self) -> [u32; ENCODE_TRACE_ROW] {
fn ns(span: Duration) -> u32 {
u32::try_from(span.as_nanos()).unwrap_or(u32::MAX)
}
[
ns(self.compile.validate),
ns(self.compile.prepare),
ns(self.compile.classify),
ns(self.compile.admit),
ns(self.compile.walk),
ns(self.compile.glyphs),
ns(self.compile.finish),
ns(self.compile_total),
ns(self.schedule),
ns(self.paints),
ns(self.instances),
ns(self.resize),
ns(self.upload),
ns(self.replay),
ns(self.pipelines),
ns(self.record),
ns(self.total()),
self.counts.draws,
self.counts.strips,
self.counts.alphas,
self.counts.glyph_draws,
self.counts.atlas_glyphs,
]
}
}
#[cfg(feature = "perf-trace")]
#[derive(Debug, Default)]
struct EncodeTrace {
window: Vec<[u32; ENCODE_TRACE_ROW]>,
ranked: Vec<u32>,
frames: u64,
}
#[cfg(feature = "perf-trace")]
impl EncodeTrace {
fn record(&mut self, spans: &EncodeSpans) {
self.frames = self.frames.saturating_add(1);
self.window.push(spans.row());
if self.window.len() < ENCODE_TRACE_WINDOW {
return;
}
let line = self.line();
self.window.clear();
log::info!("{line}");
}
fn line(&mut self) -> String {
let mut line = String::with_capacity(640);
line.push_str("frust-perf enc n=");
line.push_str(&self.frames.to_string());
line.push_str(" w=");
line.push_str(&self.window.len().to_string());
for (column, name) in ENCODE_TRACE_COLUMNS.iter().enumerate() {
let p50 = self.percentile(column, 50);
line.push(' ');
line.push_str(name);
line.push_str("_us=");
line.push_str(&format_us(p50));
}
let tail = self.percentile(ENCODE_TRACE_COLUMNS.len() - 1, 95);
line.push_str(" total_p95_us=");
line.push_str(&format_us(tail));
for (offset, name) in ENCODE_TRACE_COUNTS.iter().enumerate() {
let p50 = self.percentile(ENCODE_TRACE_COLUMNS.len() + offset, 50);
line.push(' ');
line.push_str(name);
line.push('=');
line.push_str(&p50.to_string());
}
line
}
fn percentile(&mut self, column: usize, pct: usize) -> u32 {
self.ranked.clear();
self.ranked
.extend(self.window.iter().filter_map(|row| row.get(column)));
self.ranked.sort_unstable();
let len = self.ranked.len();
if len == 0 {
return 0;
}
let rank = (len * pct).div_ceil(100).clamp(1, len);
self.ranked.get(rank - 1).copied().unwrap_or(0)
}
}
#[cfg(feature = "perf-trace")]
fn format_us(nanos: u32) -> String {
let tenths = u64::from(nanos).div_ceil(100);
format!("{}.{}", tenths / 10, tenths % 10)
}
#[derive(Debug)]
pub struct EngineRenderer {
caps: TierCaps,
format: wgpu::TextureFormat,
shaders: EngineShaders,
pipelines: PipelineCache,
compiler: SceneCompiler,
gradients: GradientCache,
depth: DepthAttachment,
targets: IntermediateTargets,
pages: PageConfig,
textures: ExternalTextures,
resources: FrameResources,
scratch: Scratch,
atlas_glyphs: Option<AtlasRenderer>,
atlas_lowering: Option<SceneCompiler>,
atlas_report: AtlasRenderReport,
filters: Option<FilterResources>,
#[cfg(feature = "perf-trace")]
encode_trace: EncodeTrace,
}
impl EngineRenderer {
pub fn new(
device: &wgpu::Device,
caps: &TierCaps,
format: wgpu::TextureFormat,
pipeline_cache: Option<&wgpu::PipelineCache>,
) -> Result<Self, EngineError> {
let dim = caps.resource_texture_dim;
if !dim.is_power_of_two() || dim < MIN_RESOURCE_TEXTURE_DIM {
return Err(EngineError::AtlasError);
}
let mut library = ShaderLibrary::new();
let shaders = EngineShaders::register(&mut library, device);
let mut pipelines = PipelineCache::new(Arc::new(library), pipeline_cache.cloned());
pipelines.warm_up(device, &warm_up_descs(&shaders, format));
let level = Level::try_detect().unwrap_or(Level::baseline());
let gradients = GradientCache::for_texture(GradientTextureLayout::square(dim), level);
Ok(Self {
caps: caps.clone(),
format,
shaders,
pipelines,
compiler: SceneCompiler::for_caps(1, 1, caps),
gradients,
depth: DepthAttachment::new(),
targets: IntermediateTargets::new(caps),
pages: PageConfig::default(),
textures: ExternalTextures::new(),
resources: FrameResources::new(device, dim),
scratch: Scratch::default(),
atlas_glyphs: None,
atlas_lowering: None,
atlas_report: AtlasRenderReport::default(),
filters: None,
#[cfg(feature = "perf-trace")]
encode_trace: EncodeTrace::default(),
})
}
#[must_use]
pub fn format(&self) -> wgpu::TextureFormat {
self.format
}
#[must_use]
pub fn caps(&self) -> &TierCaps {
&self.caps
}
#[must_use]
pub fn targets(&self) -> &IntermediateTargets {
&self.targets
}
#[must_use]
pub fn page_config(&self) -> PageConfig {
self.pages
}
#[must_use]
pub fn atlas_budget(&self) -> AtlasBudget {
self.compiler.images().budget()
}
pub fn set_atlas_budget(&mut self, budget: AtlasBudget) {
self.compiler.set_atlas_budget(budget);
self.resources.reset_atlas();
}
pub fn set_image_residency(&mut self, images: crate::cache::images::ImageResidency) {
self.compiler.set_image_residency(images);
self.resources.reset_atlas();
}
#[must_use]
pub fn refused_atlas_regions(&self) -> u64 {
self.resources.refused_regions
}
pub fn finish_warm_up(&mut self, device: &wgpu::Device) {
for pipeline in EnginePipeline::ALL {
let _ = self
.pipelines
.get_or_create(device, &pipeline.desc(&self.shaders, self.format));
}
}
#[must_use]
pub fn compiled_pipelines(&self) -> u64 {
self.pipelines.compiled_variants()
}
pub fn set_depth_pre_cleared(&mut self, pre_cleared: bool) {
self.depth.set_pre_cleared(pre_cleared);
}
#[must_use]
pub fn depth_pre_cleared(&self) -> bool {
self.depth.is_pre_cleared()
}
pub fn bind_texture(
&mut self,
id: SceneTextureId,
size: (u32, u32),
view: wgpu::TextureView,
) -> Option<wgpu::TextureView> {
self.resources.forget_external(id.get());
if !self.compiler.bind_external_texture(id.get(), size) {
return self.textures.unbind(id);
}
self.textures.bind(id, view)
}
pub fn unbind_texture(&mut self, id: SceneTextureId) -> Option<wgpu::TextureView> {
self.compiler.unbind_external_texture(id.get());
self.resources.forget_external(id.get());
self.textures.unbind(id)
}
#[must_use]
pub fn bound_texture(&self, id: SceneTextureId) -> Option<&wgpu::TextureView> {
self.textures.get(id)
}
#[must_use]
pub fn bound_texture_count(&self) -> usize {
self.textures.len()
}
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
self.targets.drop_parked();
self.depth.resize(device, width, height);
}
pub fn end_frame(&mut self, _queue: &wgpu::Queue) {
self.targets.end_frame();
self.gradients.maintain();
}
#[expect(
clippy::too_many_arguments,
reason = "the seam frust-render drives: device, queue, encoder, scene, \
target, base colour and root transform are each supplied by a \
different owner, so bundling them would only move the \
assembly to every call site"
)]
pub fn encode(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
scene: &Scene,
target: EngineTarget<'_>,
base_color: Color,
root: Affine,
) -> Result<(), EngineError> {
self.encode_traced(
device,
queue,
encoder,
scene,
target,
base_color,
root,
FrameTimestamps::inert(),
)
}
#[expect(
clippy::too_many_arguments,
reason = "[`Self::encode`]'s argument list plus the timestamp sink, \
each still supplied by a different owner"
)]
pub fn encode_traced(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
scene: &Scene,
target: EngineTarget<'_>,
base_color: Color,
root: Affine,
timestamps: FrameTimestamps<'_>,
) -> Result<(), EngineError> {
let mut clock = PhaseClock::start();
let size = grid_size(target.width, target.height)?;
let mut frame = self.compiler.compile(scene, root, size)?;
let compile_total = clock.lap();
let rounds = Schedule::build(&frame.recorder, &self.caps, &self.pages)?;
let ceiling = self.targets.max_texture_size();
for round in &rounds {
if let Some(page) = round.page()
&& (page.size.width > ceiling || page.size.height > ceiling)
{
return Err(EngineError::IntermediateTextureTooLarge);
}
}
let depth_enabled = !config::depth_disabled();
if depth_enabled && target.depth.is_none() {
self.depth.ensure(device, target.width, target.height);
}
let depth_view = depth_enabled
.then(|| target.depth.or_else(|| self.depth.owned_view()))
.flatten()
.cloned();
let depth_view = depth_view.as_ref();
let punches = !frame.clears.is_empty() && !is_opaque(base_color);
let schedule_span = clock.lap();
let atlas_budget = self.compiler.images().budget();
self.resources
.resolve_paints(&frame, &mut self.gradients, atlas_budget, &self.textures);
let paints_span = clock.lap();
self.scratch.build(
&frame,
&rounds,
depth_view.is_some(),
punches,
&self.resources.paint_slots,
);
let instances_span = clock.lap();
let dim = self.caps.resource_texture_dim;
let alphas_grown = gpu::grow_alpha_texture_height(
self.resources.alphas.height,
frame.alphas().len(),
dim,
)?;
let paints_grown = gpu::paint_texture::grow_encoded_paints_texture_height(
self.resources.paints.height,
self.resources.paint_texels(),
dim,
)?;
let gradients_grown = self.resources.grown_gradient_height(&self.gradients)?;
self.resources
.ensure_atlas(device, queue, atlas_budget, frame.atlas_layers);
if self.clear_glyph_rects(device, queue, &frame) {
self.compiler.acknowledge_glyph_clears();
}
self.resources.resize_alphas(device, alphas_grown);
self.resources.resize_paints(device, paints_grown);
self.resources.resize_gradients(device, gradients_grown);
let resize_span = clock.lap();
let atlas_serviced =
self.resources
.upload(queue, &mut frame, &mut self.gradients, size, dim);
self.resources
.upload_instances(device, queue, &self.scratch);
if atlas_serviced {
self.compiler.acknowledge_image_plan();
}
let upload_span = clock.lap();
if self.compiler.glyph_replay_pending()
&& self.replay_glyph_pages(device, queue, timestamps)
{
self.compiler.acknowledge_glyph_replay();
}
let replay_span = clock.lap();
let format = target.format;
let pipelines = self.frame_pipelines(device, format, depth_view.is_some());
if let Some(pipeline) = pipelines.filter.as_ref() {
let scratch = &self.scratch;
self.filters
.get_or_insert_with(|| FilterResources::new(device))
.prepare(
device,
queue,
pipeline,
&scratch.filter_blocks,
scratch.filter_passes,
);
}
let pipelines_span = clock.lap();
self.record_frame(
device, queue, encoder, &target, depth_view, base_color, &pipelines, timestamps,
);
#[cfg(not(feature = "perf-trace"))]
let _ = (
compile_total,
schedule_span,
paints_span,
instances_span,
resize_span,
upload_span,
replay_span,
pipelines_span,
clock.lap(),
);
#[cfg(feature = "perf-trace")]
self.encode_trace.record(&EncodeSpans {
compile: frame.compile_spans,
compile_total,
schedule: schedule_span,
paints: paints_span,
instances: instances_span,
resize: resize_span,
upload: upload_span,
replay: replay_span,
pipelines: pipelines_span,
record: clock.lap(),
counts: EncodeCounts::of(&frame),
});
Ok(())
}
fn ensure_atlas_renderer(&mut self, device: &wgpu::Device) {
if self.atlas_glyphs.is_none() {
self.atlas_glyphs = Some(AtlasRenderer::new(device, &self.caps));
}
}
fn clear_glyph_rects(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
frame: &CompiledFrame,
) -> bool {
if frame.glyph_clears.is_empty() {
return true;
}
if self.resources.atlas.is_none() {
return false;
}
self.ensure_atlas_renderer(device);
let refused = {
let Self {
atlas_glyphs,
resources,
..
} = self;
let (Some(glyphs), Some(atlas)) = (atlas_glyphs.as_ref(), resources.atlas.as_ref())
else {
return false;
};
frame
.glyph_clears
.iter()
.filter(|rect| !glyphs.clear_rect(queue, atlas, **rect))
.count()
};
if refused > 0 {
self.resources.note_refused_regions(refused as u64);
}
true
}
#[must_use]
fn replay_glyph_pages(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
timestamps: FrameTimestamps<'_>,
) -> bool {
if self.resources.atlas.is_none() {
return false;
}
self.ensure_atlas_renderer(device);
let pipeline = self
.pipelines
.get_or_create(device, &atlas_strip_desc(&self.shaders))
.clone();
let report = {
let Self {
atlas_glyphs,
atlas_lowering,
resources,
compiler,
..
} = self;
let (Some(glyphs), Some(atlas)) = (atlas_glyphs.as_mut(), resources.atlas.as_ref())
else {
return false;
};
let (width, height) = atlas.size();
let page = (
u16::try_from(width).unwrap_or(u16::MAX),
u16::try_from(height).unwrap_or(u16::MAX),
);
let lowering = atlas_lowering.get_or_insert_with(|| SceneCompiler::new(page.0, page.1));
glyphs.render_pending(
device,
queue,
&pipeline,
atlas,
compiler.glyph_atlas_mut(),
timestamps,
|recorder, buffers| lower_atlas_page(recorder, buffers, lowering, page),
)
};
if report.refused > 0 {
self.resources
.note_refused_regions(u64::from(report.refused));
}
self.atlas_report = report;
true
}
#[must_use]
pub fn atlas_render_report(&self) -> AtlasRenderReport {
self.atlas_report
}
fn frame_pipelines(
&mut self,
device: &wgpu::Device,
format: wgpu::TextureFormat,
depth: bool,
) -> FramePipelines {
let alpha_variant = if depth {
EnginePipeline::StripDepthAlpha
} else {
EnginePipeline::StripAlpha
};
let punch_variant = if depth {
EnginePipeline::StripDepthDestOut
} else {
EnginePipeline::StripDestOut
};
let mut frame = FramePipelines {
alpha: (
alpha_variant,
self.pipelines
.get_or_create(device, &alpha_variant.desc(&self.shaders, format))
.clone(),
),
opaque: None,
page: None,
punch: None,
filter: None,
};
if depth && !self.scratch.opaque.is_empty() {
frame.opaque = Some(
self.pipelines
.get_or_create(
device,
&EnginePipeline::StripOpaque.desc(&self.shaders, format),
)
.clone(),
);
}
if self.scratch.page_rounds() > 0 {
frame.page = Some(
self.pipelines
.get_or_create(
device,
&EnginePipeline::StripIntermediate.desc(&self.shaders, format),
)
.clone(),
);
}
if self.scratch.punches() {
frame.punch = Some((
punch_variant,
self.pipelines
.get_or_create(device, &punch_variant.desc(&self.shaders, format))
.clone(),
));
}
if self.scratch.filter_passes > 0 {
frame.filter = Some(
self.pipelines
.get_or_create(device, &EnginePipeline::Filter.desc(&self.shaders, format))
.clone(),
);
}
self.resources
.ensure_bind_groups(device, frame.alpha.0, &frame.alpha.1, format);
self.resources.ensure_external_groups(
device,
frame.alpha.0,
&frame.alpha.1,
&self.textures,
);
if let Some(pipeline) = frame.opaque.as_ref() {
self.resources.ensure_bind_groups(
device,
EnginePipeline::StripOpaque,
pipeline,
format,
);
}
if let Some(pipeline) = frame.page.as_ref() {
self.resources.ensure_bind_groups(
device,
EnginePipeline::StripIntermediate,
pipeline,
format,
);
self.resources.ensure_external_groups(
device,
EnginePipeline::StripIntermediate,
pipeline,
&self.textures,
);
}
if let Some((variant, pipeline)) = frame.punch.as_ref() {
self.resources
.ensure_bind_groups(device, *variant, pipeline, format);
}
self.resources
.ensure_page_configs(device, self.scratch.page_rounds());
frame
}
#[expect(
clippy::too_many_arguments,
reason = "one frame's full recording state, each piece owned by a \
different part of the renderer; bundling them would move the \
same assembly one call up"
)]
fn record_frame(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target: &EngineTarget<'_>,
depth_view: Option<&wgpu::TextureView>,
base_color: Color,
pipelines: &FramePipelines,
timestamps: FrameTimestamps<'_>,
) {
let depth_load = self.depth.load_op(target.depth.is_some());
drop(encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("frust-engine clear"),
color_attachments: &[Some(color_attachment(
target.view,
wgpu::LoadOp::Clear(clear_color(base_color, target.output)),
))],
depth_stencil_attachment: depth_view.map(|view| depth_attachment(view, depth_load)),
timestamp_writes: timestamps.writes(EngineSpan::Main),
occlusion_query_set: None,
multiview_mask: None,
}));
let Some(instances) = self.resources.instances.as_ref() else {
return;
};
let dim = self.caps.resource_texture_dim;
let opaque_count = self.scratch.opaque.len() as u32;
let base = opaque_count;
if opaque_count > 0
&& let Some(opaque) = pipelines.opaque.as_ref()
&& let Some(opaque_groups) =
self.resources.bind_groups.get(&EnginePipeline::StripOpaque)
{
record_pass(
encoder,
&PassPlan {
label: "frust-engine opaque strips",
view: target.view,
load: wgpu::LoadOp::Load,
depth: depth_view,
pipeline: opaque,
groups: opaque_groups,
resources: &opaque_groups.resources,
composites: &[],
external_keys: &[],
instances,
base: 0,
segments: &[Segment::Strips(0, opaque_count)],
timestamps: timestamps.writes(EngineSpan::Main),
},
);
}
let punch_pass = pipelines.punch.as_ref().and_then(|(variant, pipeline)| {
Some(PunchPass {
view: target.view,
depth: depth_view,
pipeline,
groups: self.resources.bind_groups.get(variant)?,
instances,
base,
})
});
let mut live: [Option<PooledTexture>; MAX_LIVE_PAGES] = [const { None }; MAX_LIVE_PAGES];
let mut page_slot = 0_usize;
for plan in &self.scratch.rounds {
let own = match plan.page {
None => None,
Some(page) if page.continued => match live[page.parity.index()].take() {
Some(pooled) => Some((page.parity, pooled)),
None => continue,
},
Some(page) => match self.targets.acquire(
device,
page.size.width,
page.size.height,
PAGE_LABEL,
) {
IntermediateTexture::Texture(pooled) => Some((page.parity, pooled)),
IntermediateTexture::TooLarge { .. } => continue,
},
};
if let Some(filter) = plan.filter {
if let Some((_, pooled)) = own.as_ref()
&& let Some(pipeline) = pipelines.filter.as_ref()
&& let Some(filters) = self.filters.as_ref()
{
let (width, height) = pooled.size();
filters.write_instance(
queue,
filter.instance,
&FilterInstanceData::new(
&filter.step,
filter.data_offset,
(0, 0),
(0, 0),
SizeU16::from_wh(
u16::try_from(width).unwrap_or(u16::MAX),
u16::try_from(height).unwrap_or(u16::MAX),
),
filter.original,
),
);
let source = live[filter.source.index()].as_ref().map_or(
&self.resources.placeholders.layer_input,
PooledTexture::view,
);
filters.record_pass_timed(
device,
encoder,
&FilterPassPlan {
label: FILTER_LABEL,
pipeline,
dest: pooled.view(),
source,
instance: filter.instance,
},
timestamps.writes(EngineSpan::Composite),
);
}
settle_pages(&mut self.targets, &mut live, plan.released, own);
continue;
}
let config = match &own {
None => &self.resources.config,
Some((_, pooled)) => {
let Some(config) = self.resources.page_configs.get(page_slot) else {
continue;
};
let (width, height) = pooled.size();
queue.write_buffer(
config,
0,
bytemuck::bytes_of(&GpuConfig::new(width, height, dim, dim)),
);
page_slot = page_slot.saturating_add(1);
config
}
};
let variant = match &own {
None => pipelines.alpha.0,
Some(_) => EnginePipeline::StripIntermediate,
};
let pipeline = match (&own, pipelines.page.as_ref()) {
(None, _) => &pipelines.alpha.1,
(Some(_), Some(page)) => page,
(Some(_), None) => continue,
};
let Some(groups) = self.resources.bind_groups.get(&variant) else {
continue;
};
let page_resources = own.as_ref().map(|_| {
resources_bind_group(
device,
pipeline,
&self.resources.alphas.view,
config,
&self.resources.placeholders.layer_input,
)
});
let resources = page_resources.as_ref().unwrap_or(&groups.resources);
let segments = self
.scratch
.segments
.get(plan.segments.clone())
.unwrap_or(&[]);
let mut composites: Vec<wgpu::BindGroup> = Vec::new();
for segment in segments {
if let Segment::Composite(_, parity) = *segment {
let view = live[parity.index()].as_ref().map_or(
&self.resources.placeholders.layer_input,
PooledTexture::view,
);
composites.push(resources_bind_group(
device,
pipeline,
&self.resources.alphas.view,
config,
view,
));
}
}
let span = if own.is_some() {
EngineSpan::Composite
} else {
EngineSpan::Main
};
let (view, label, load, depth) = match &own {
Some((_, pooled)) => (
pooled.view(),
PAGE_LABEL,
if plan.page.is_some_and(|page| page.continued) {
wgpu::LoadOp::Load
} else {
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
},
None,
),
None => (
target.view,
"frust-engine alpha strips",
wgpu::LoadOp::Load,
depth_view,
),
};
if !segments.is_empty() || own.is_some() {
record_pass(
encoder,
&PassPlan {
label,
view,
load,
depth,
pipeline,
groups,
resources,
composites: &composites,
external_keys: self.resources.external_runs.keys(),
instances,
base,
segments,
timestamps: timestamps.writes(span),
},
);
}
settle_pages(&mut self.targets, &mut live, plan.released, own);
if let Some(punch) = punch_pass.as_ref() {
punch.record(encoder, plan.punch, timestamps.writes(EngineSpan::Main));
}
}
if let Some(punch) = punch_pass.as_ref() {
punch.record(
encoder,
self.scratch.punch,
timestamps.writes(EngineSpan::Main),
);
}
for page in live.into_iter().flatten() {
self.targets.release(page);
}
}
}
#[derive(Debug)]
struct FramePipelines {
alpha: (EnginePipeline, wgpu::RenderPipeline),
opaque: Option<wgpu::RenderPipeline>,
page: Option<wgpu::RenderPipeline>,
punch: Option<(EnginePipeline, wgpu::RenderPipeline)>,
filter: Option<wgpu::RenderPipeline>,
}
struct PassPlan<'a> {
label: &'a str,
view: &'a wgpu::TextureView,
load: wgpu::LoadOp<wgpu::Color>,
depth: Option<&'a wgpu::TextureView>,
pipeline: &'a wgpu::RenderPipeline,
groups: &'a StripBindGroups,
resources: &'a wgpu::BindGroup,
composites: &'a [wgpu::BindGroup],
external_keys: &'a [u64],
instances: &'a wgpu::Buffer,
base: u32,
segments: &'a [Segment],
timestamps: Option<wgpu::RenderPassTimestampWrites<'a>>,
}
fn record_pass(encoder: &mut wgpu::CommandEncoder, plan: &PassPlan<'_>) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(plan.label),
color_attachments: &[Some(color_attachment(plan.view, plan.load))],
depth_stencil_attachment: plan
.depth
.map(|view| depth_attachment(view, wgpu::LoadOp::Load)),
timestamp_writes: plan.timestamps.clone(),
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(plan.pipeline);
pass.set_vertex_buffer(0, plan.instances.slice(..));
let mut composite = 0_usize;
for segment in plan.segments {
let (group, images, range) = match *segment {
Segment::Strips(first, count) => {
if count == 0 {
continue;
}
(
plan.resources,
None,
GpuStrip::instance_range(plan.base.saturating_add(first), count),
)
}
Segment::External(first, count, slot) => {
if count == 0 {
continue;
}
let Some(images) = plan
.external_keys
.get(slot as usize)
.and_then(|key| plan.groups.externals.get(key))
else {
continue;
};
(
plan.resources,
Some(images),
GpuStrip::instance_range(plan.base.saturating_add(first), count),
)
}
Segment::Composite(first, _) => {
let Some(group) = plan.composites.get(composite) else {
continue;
};
composite = composite.saturating_add(1);
(
group,
None,
GpuStrip::instance_range(plan.base.saturating_add(first), 1),
)
}
};
plan.groups.bind_with(&mut pass, group, images);
pass.draw(GpuStrip::vertex_range(), range);
}
}
struct PunchPass<'a> {
view: &'a wgpu::TextureView,
depth: Option<&'a wgpu::TextureView>,
pipeline: &'a wgpu::RenderPipeline,
groups: &'a StripBindGroups,
instances: &'a wgpu::Buffer,
base: u32,
}
impl PunchPass<'_> {
fn record(
&self,
encoder: &mut wgpu::CommandEncoder,
punch: (u32, u32),
timestamps: Option<wgpu::RenderPassTimestampWrites<'_>>,
) {
let (first, count) = punch;
if count == 0 {
return;
}
record_pass(
encoder,
&PassPlan {
label: "frust-engine hole punch",
view: self.view,
load: wgpu::LoadOp::Load,
depth: self.depth,
pipeline: self.pipeline,
groups: self.groups,
resources: &self.groups.resources,
composites: &[],
external_keys: &[],
instances: self.instances,
base: self.base,
segments: &[Segment::Strips(first, count)],
timestamps,
},
);
}
}
fn settle_pages(
targets: &mut IntermediateTargets,
live: &mut [Option<PooledTexture>; MAX_LIVE_PAGES],
released: [bool; MAX_LIVE_PAGES],
own: Option<(PageParity, PooledTexture)>,
) {
for (index, slot) in live.iter_mut().enumerate() {
if released.get(index).copied().unwrap_or(false)
&& let Some(page) = slot.take()
{
targets.release(page);
}
}
if let Some((parity, pooled)) = own
&& let Some(previous) = live[parity.index()].replace(pooled)
{
targets.release(previous);
}
}
#[derive(Debug)]
pub struct FilterResources {
data: ResourceTexture,
data_group: Option<wgpu::BindGroup>,
sampler: wgpu::Sampler,
instances: Option<wgpu::Buffer>,
instance_capacity: u64,
staging: Vec<u8>,
}
impl FilterResources {
#[must_use]
pub fn new(device: &wgpu::Device) -> Self {
Self {
data: ResourceTexture::new(
device,
&filter_data_texture_descriptor(gpu::MIN_RESOURCE_TEXTURE_HEIGHT),
),
data_group: None,
sampler: filter_sampler(device),
instances: None,
instance_capacity: 0,
staging: Vec::new(),
}
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
pipeline: &wgpu::RenderPipeline,
blocks: &[GpuFilterData],
passes: u32,
) {
let Some(height) = filter_data_texture_height(blocks.len()) else {
self.data_group = None;
return;
};
if height > self.data.height {
self.data = ResourceTexture::new(device, &filter_data_texture_descriptor(height));
self.data_group = None;
}
if self.data_group.is_none() {
self.data_group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frust-engine filter data"),
layout: &pipeline.get_bind_group_layout(0),
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.data.view),
}],
}));
}
let footprint = gpu::resource_texture_bytes(self.data.width, self.data.height);
self.staging.clear();
self.staging
.resize(usize::try_from(footprint).unwrap_or(usize::MAX), 0);
let bytes: &[u8] = bytemuck::cast_slice(blocks);
if let Some(head) = self.staging.get_mut(..bytes.len()) {
head.copy_from_slice(bytes);
}
queue.write_texture(
self.data.copy_target(),
&self.staging,
resource_layout(
gpu::resource_bytes_per_row(self.data.width),
self.data.height,
),
self.data.extent(),
);
self.reserve_instances(device, passes);
}
fn reserve_instances(&mut self, device: &wgpu::Device, passes: u32) {
let stride = size_of::<FilterInstanceData>() as u64;
let required = u64::from(passes).saturating_mul(stride).max(stride);
if self.instances.is_some() && self.instance_capacity >= required {
return;
}
let capacity = required
.checked_next_power_of_two()
.unwrap_or(required)
.max(MIN_FILTER_INSTANCE_CAPACITY.saturating_mul(stride));
self.instance_capacity = capacity;
self.instances = Some(device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frust-engine filter instances"),
size: capacity,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
}
pub fn write_instance(&self, queue: &wgpu::Queue, index: u32, instance: &FilterInstanceData) {
let Some(buffer) = self.instances.as_ref() else {
return;
};
let stride = size_of::<FilterInstanceData>() as u64;
let offset = u64::from(index).saturating_mul(stride);
if offset.saturating_add(stride) > self.instance_capacity {
return;
}
queue.write_buffer(buffer, offset, bytemuck::bytes_of(instance));
}
pub fn record_pass(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
plan: &FilterPassPlan<'_>,
) {
self.record_pass_timed(device, encoder, plan, None);
}
pub fn record_pass_timed(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
plan: &FilterPassPlan<'_>,
timestamps: Option<wgpu::RenderPassTimestampWrites<'_>>,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(plan.label),
color_attachments: &[Some(color_attachment(
plan.dest,
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
))],
depth_stencil_attachment: None,
timestamp_writes: timestamps,
occlusion_query_set: None,
multiview_mask: None,
});
let (Some(data_group), Some(instances)) =
(self.data_group.as_ref(), self.instances.as_ref())
else {
return;
};
let source = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frust-engine filter source"),
layout: &plan.pipeline.get_bind_group_layout(1),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(plan.source),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
pass.set_pipeline(plan.pipeline);
pass.set_vertex_buffer(0, instances.slice(..));
pass.set_bind_group(0, data_group, &[]);
pass.set_bind_group(1, &source, &[]);
pass.draw(
0..FILTER_QUAD_VERTICES,
plan.instance..plan.instance.saturating_add(1),
);
}
}
#[derive(Debug)]
pub struct FilterPassPlan<'a> {
pub label: &'a str,
pub pipeline: &'a wgpu::RenderPipeline,
pub dest: &'a wgpu::TextureView,
pub source: &'a wgpu::TextureView,
pub instance: u32,
}
fn color_attachment(
view: &wgpu::TextureView,
load: wgpu::LoadOp<wgpu::Color>,
) -> wgpu::RenderPassColorAttachment<'_> {
wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
},
}
}
fn depth_attachment(
view: &wgpu::TextureView,
load: wgpu::LoadOp<f32>,
) -> wgpu::RenderPassDepthStencilAttachment<'_> {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: Some(wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}
}
fn clear_color(base_color: Color, output: OutputAlpha) -> wgpu::Color {
let components = match output {
OutputAlpha::Premultiplied => base_color.premultiply().components,
OutputAlpha::Straight => base_color.components,
};
wgpu::Color {
r: f64::from(components[0]),
g: f64::from(components[1]),
b: f64::from(components[2]),
a: f64::from(components[3]),
}
}
fn is_opaque(base_color: Color) -> bool {
base_color.components[3] >= 1.0
}
fn grid_size(width: u32, height: u32) -> Result<(u16, u16), EngineError> {
let width = u16::try_from(width).map_err(|_| EngineError::TargetTooLarge)?;
let height = u16::try_from(height).map_err(|_| EngineError::TargetTooLarge)?;
Ok((width, height))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Segment {
Strips(u32, u32),
External(u32, u32, u32),
Composite(u32, PageParity),
}
#[derive(Debug, Clone)]
struct RoundPlan {
page: Option<PagePlan>,
segments: Range<usize>,
released: [bool; MAX_LIVE_PAGES],
filter: Option<FilterPlan>,
punch: (u32, u32),
}
#[derive(Debug, Clone, Copy)]
struct FilterPlan {
step: FilterStep,
source: PageParity,
data_offset: u32,
original: SizeU16,
instance: u32,
}
#[derive(Debug, Clone, Copy)]
struct PagePlan {
parity: PageParity,
size: PageSize,
continued: bool,
}
#[derive(Debug, Default)]
struct Scratch {
opaque: Vec<GpuStrip>,
alpha: Vec<GpuStrip>,
segments: Vec<Segment>,
rounds: Vec<RoundPlan>,
punch: (u32, u32),
layer_depth: Vec<u32>,
filter_blocks: Vec<GpuFilterData>,
filter_layers: Vec<u32>,
filter_passes: u32,
}
impl Scratch {
fn build(
&mut self,
frame: &CompiledFrame,
rounds: &[Round],
depth_active: bool,
punches: bool,
paint_slots: &[Option<ResolvedPaint>],
) {
self.opaque.clear();
self.alpha.clear();
self.segments.clear();
self.rounds.clear();
self.punch = (0, 0);
self.layer_depth.clear();
self.layer_depth.resize(frame.recorder.layers.len(), 0);
self.filter_blocks.clear();
self.filter_layers.clear();
self.filter_passes = 0;
let draws = frame.draws();
let strips = frame.strip_buf();
let mut pending: &[ClearPunch] = if punches { &frame.clears } else { &[] };
for round in rounds {
let page = round.page();
let filter = self.plan_filter(frame, round);
let window = page.map_or(PageWindow::ROOT, PageWindow::of);
let split_opaque = page.is_none() && depth_active;
let cuts = page.is_none();
let mut first_segment = self.segments.len();
let mut deepest = 0_u32;
let mut run_start = self.alpha.len() as u32;
let mut run_external: Option<u32> = None;
for op in &round.ops {
match op {
RoundOp::Draws(range) => {
let batch = draws
.get(range.start as usize..range.end as usize)
.unwrap_or(&[]);
for draw in batch {
deepest = deepest.max(draw.depth);
if cuts && !pending.is_empty() {
self.cut_for_punches(
&mut pending,
strips,
draw.depth,
&mut first_segment,
&mut run_start,
&mut run_external,
);
}
let Some(paint) = pack_paint(&draw.paint, draw.depth, paint_slots)
else {
continue;
};
let Some(run) = strips.get(draw.strip_range.clone()) else {
continue;
};
if paint.external != run_external {
let end = self.alpha.len() as u32;
self.push_run(run_start, end, run_external);
run_start = end;
run_external = paint.external;
}
let to_opaque = paint.opaque && split_opaque;
for pair in run.windows(2) {
self.push_span(&pair[0], &pair[1], paint, to_opaque, window);
}
}
}
RoundOp::Composite(composite) => {
let depth = self
.layer_depth
.get(composite.layer as usize)
.copied()
.unwrap_or(0);
if cuts && !pending.is_empty() {
self.cut_for_punches(
&mut pending,
strips,
depth,
&mut first_segment,
&mut run_start,
&mut run_external,
);
}
let end = self.alpha.len() as u32;
self.push_run(run_start, end, run_external);
deepest = deepest.max(depth);
self.segments
.push(Segment::Composite(end, composite.parity));
self.alpha
.push(composite_instance(composite, window.origin(), depth));
run_start = self.alpha.len() as u32;
run_external = None;
}
}
}
let end = self.alpha.len() as u32;
self.push_run(run_start, end, run_external);
if let Some(page) = page
&& let Some(slot) = self.layer_depth.get_mut(page.layer as usize)
{
*slot = (*slot).max(deepest);
}
let mut released = [false; MAX_LIVE_PAGES];
for parity in &round.released {
if let Some(slot) = released.get_mut(parity.index()) {
*slot = true;
}
}
self.rounds.push(RoundPlan {
page: page.map(|page| PagePlan {
parity: page.parity,
size: page.size,
continued: page.continued,
}),
segments: first_segment..self.segments.len(),
released,
filter,
punch: (0, 0),
});
}
if !pending.is_empty() {
self.punch = self.push_punches(pending, strips);
}
}
fn cut_for_punches(
&mut self,
pending: &mut &[ClearPunch],
strips: &[Strip],
depth: u32,
first_segment: &mut usize,
run_start: &mut u32,
run_external: &mut Option<u32>,
) {
let cut = pending
.iter()
.take_while(|punch| punch.depth < depth)
.count();
if cut == 0 {
return;
}
let (issued, rest) = pending.split_at(cut);
*pending = rest;
let end = self.alpha.len() as u32;
self.push_run(*run_start, end, *run_external);
*run_external = None;
let punch = self.push_punches(issued, strips);
self.rounds.push(RoundPlan {
page: None,
segments: *first_segment..self.segments.len(),
released: [false; MAX_LIVE_PAGES],
filter: None,
punch,
});
*first_segment = self.segments.len();
*run_start = self.alpha.len() as u32;
}
fn push_run(&mut self, first: u32, end: u32, external: Option<u32>) {
let Some(count) = end.checked_sub(first).filter(|count| *count > 0) else {
return;
};
self.segments.push(match external {
Some(slot) => Segment::External(first, count, slot),
None => Segment::Strips(first, count),
});
}
fn push_punches(&mut self, punches: &[ClearPunch], strips: &[Strip]) -> (u32, u32) {
let first = self.alpha.len() as u32;
for punch in punches {
let paint = PackedPaint {
payload: PaintPayload::Solid(PUNCH_SOURCE),
paint: SOLID_PAINT,
depth_index: punch.depth,
opaque: false,
external: None,
};
let Some(run) = strips.get(punch.strip_range.clone()) else {
continue;
};
for pair in run.windows(2) {
self.push_span(&pair[0], &pair[1], paint, false, PageWindow::ROOT);
}
}
(first, (self.alpha.len() as u32).saturating_sub(first))
}
fn push_span(
&mut self,
strip: &Strip,
next: &Strip,
paint: PackedPaint,
to_opaque: bool,
window: PageWindow,
) {
let values = paint.values_at(strip.x, strip.y);
let mut span = GpuStrip::from_strip_pair(strip, next, values);
if window.place(&mut span, paint) {
self.alpha.push(span);
}
if let Some(mut gap) = GpuStrip::gap_fill(strip, next, values) {
gap.payload = paint.payload_at(gap.x, gap.y);
if window.place(&mut gap, paint) {
if to_opaque {
self.opaque.push(gap);
} else {
self.alpha.push(gap);
}
}
}
}
fn plan_filter(&mut self, frame: &CompiledFrame, round: &Round) -> Option<FilterPlan> {
let pass = round.filter_pass()?;
let page = round.page()?;
let recorded = frame.recorder.layers.get(pass.layer as usize)?;
let data_offset = self.filter_block(pass.layer, &recorded.kind)?;
let instance = self.filter_passes;
self.filter_passes = self.filter_passes.saturating_add(1);
Some(FilterPlan {
step: pass.step,
source: pass.source,
data_offset,
original: SizeU16::from(page.bounds),
instance,
})
}
fn filter_block(&mut self, layer: u32, kind: &RecordedLayerKind) -> Option<u32> {
let index = match self.filter_layers.iter().position(|id| *id == layer) {
Some(index) => index,
None => {
let block = match served_filter(layer, kind).ok()? {
ServedFilter::Blur(blur) => GpuFilterData::from(GpuGaussianBlur::from(&blur)),
ServedFilter::DropShadow(shadow) => {
GpuFilterData::from(GpuDropShadow::from(&shadow))
}
};
self.filter_layers.push(layer);
self.filter_blocks.push(block);
self.filter_layers.len().saturating_sub(1)
}
};
u32::try_from(index)
.ok()?
.checked_mul(GpuFilterData::SIZE_TEXELS)
}
fn punches(&self) -> bool {
self.punch.1 > 0 || self.rounds.iter().any(|plan| plan.punch.1 > 0)
}
fn page_rounds(&self) -> usize {
self.rounds
.iter()
.filter(|plan| plan.page.is_some() && plan.filter.is_none())
.count()
}
fn instance_bytes(&self) -> (&[u8], &[u8]) {
(
bytemuck::cast_slice(&self.opaque),
bytemuck::cast_slice(&self.alpha),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PageWindow {
x0: u16,
x1: u16,
y0: u16,
}
impl PageWindow {
const ROOT: Self = Self {
x0: 0,
x1: u16::MAX,
y0: 0,
};
fn of(page: &PageTarget) -> Self {
Self {
x0: page.bounds.x0,
x1: page.bounds.x1,
y0: page.bounds.y0,
}
}
fn origin(self) -> (u16, u16) {
(self.x0, self.y0)
}
fn place(self, span: &mut GpuStrip, paint: PackedPaint) -> bool {
let end = span.x.saturating_add(span.width);
if span.width == 0 || end <= self.x0 || span.x >= self.x1 {
return false;
}
let cut = self.x0.saturating_sub(span.x);
if cut > 0 {
span.x = self.x0;
span.width = span.width.saturating_sub(cut);
let dense = cut.min(span.dense_width_or_rect_height);
span.dense_width_or_rect_height = span.dense_width_or_rect_height.saturating_sub(dense);
span.col_idx_or_rect_frac = if span.dense_width_or_rect_height == 0 {
0
} else {
span.col_idx_or_rect_frac.saturating_add(u32::from(dense))
};
span.payload = paint.payload_at(span.x, span.y);
}
let over = end.saturating_sub(self.x1);
if over > 0 {
span.width = span.width.saturating_sub(over);
span.dense_width_or_rect_height = span.dense_width_or_rect_height.min(span.width);
}
span.x = span.x.saturating_sub(self.x0);
span.y = span.y.saturating_sub(self.y0);
span.width > 0
}
}
fn composite_instance(composite: &Composite, origin: (u16, u16), depth: u32) -> GpuStrip {
let bounds = composite.bounds;
let source = composite.source();
GpuStrip::from_rect(
bounds.x0.saturating_sub(origin.0),
bounds.y0.saturating_sub(origin.1),
bounds.width(),
bounds.height(),
0,
StripDraw {
payload: pack_u16_pair(source.x0, source.y0),
paint: LAYER_PAINT_SOURCE | u32::from(pack_opacity(composite.opacity)),
depth_index: depth,
},
)
}
fn pack_opacity(opacity: f32) -> u8 {
(opacity.clamp(0.0, 1.0) * 255.0).round() as u8
}
#[derive(Debug, Clone, Copy)]
enum PaintPayload {
Solid(u32),
Position,
}
#[derive(Debug, Clone, Copy)]
struct PackedPaint {
payload: PaintPayload,
paint: u32,
depth_index: u32,
opaque: bool,
external: Option<u32>,
}
impl PackedPaint {
fn payload_at(self, x: u16, y: u16) -> u32 {
match self.payload {
PaintPayload::Solid(rgba) => rgba,
PaintPayload::Position => pack_u16_pair(x, y),
}
}
fn values_at(self, x: u16, y: u16) -> StripDraw {
StripDraw {
payload: self.payload_at(x, y),
paint: self.paint,
depth_index: self.depth_index,
}
}
}
fn pack_u16_pair(x: u16, y: u16) -> u32 {
u32::from(x) | (u32::from(y) << 16)
}
fn pack_paint(
paint: &Paint,
depth: u32,
paint_slots: &[Option<ResolvedPaint>],
) -> Option<PackedPaint> {
match paint {
Paint::Solid(color) => Some(PackedPaint {
payload: PaintPayload::Solid(color.as_premul_rgba8().to_u32()),
paint: SOLID_PAINT,
depth_index: depth,
opaque: color.is_opaque(),
external: None,
}),
Paint::Indexed(indexed) => {
let Some(Some(resolved)) = paint_slots.get(indexed.index()) else {
INDEXED_PAINT_WARNING.call_once(|| {
log::warn!(
"an indexed paint could not be lowered to a GPU record; those draws are \
skipped (logged once)"
);
});
return None;
};
Some(PackedPaint {
payload: PaintPayload::Position,
paint: pack_paint_descriptor(resolved.paint_type, resolved.texel_offset),
depth_index: depth,
opaque: resolved.opaque,
external: resolved.external,
})
}
}
}
#[derive(Debug, Clone, Copy)]
struct ResolvedPaint {
paint_type: PaintType,
texel_offset: u32,
opaque: bool,
external: Option<u32>,
}
#[derive(Debug)]
struct ResourceTexture {
texture: wgpu::Texture,
view: wgpu::TextureView,
width: u32,
height: u32,
}
impl ResourceTexture {
fn new(device: &wgpu::Device, descriptor: &wgpu::TextureDescriptor<'_>) -> Self {
let texture = device.create_texture(descriptor);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
Self {
texture,
view,
width: descriptor.size.width,
height: descriptor.size.height,
}
}
fn copy_target(&self) -> wgpu::TexelCopyTextureInfo<'_> {
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
}
}
fn extent(&self) -> wgpu::Extent3d {
wgpu::Extent3d {
width: self.width,
height: self.height,
depth_or_array_layers: 1,
}
}
}
#[derive(Debug)]
struct FrameResources {
alphas: ResourceTexture,
paints: ResourceTexture,
gradients: ResourceTexture,
placeholders: Placeholders,
config: wgpu::Buffer,
page_configs: Vec<wgpu::Buffer>,
instances: Option<wgpu::Buffer>,
instance_capacity: u64,
paints_data: Vec<GpuEncodedPaint>,
paint_slots: Vec<Option<ResolvedPaint>>,
paint_ramps: Vec<Option<CachedRamp>>,
paint_staging: Vec<u8>,
external_runs: ExternalRuns,
bind_groups: HashMap<EnginePipeline, StripBindGroups>,
bind_group_format: Option<wgpu::TextureFormat>,
atlas: Option<AtlasArray>,
image_registry: HashMap<ImageId, ResidentImage>,
refused_regions: u64,
}
impl FrameResources {
fn new(device: &wgpu::Device, dim: u32) -> Self {
let min = gpu::MIN_RESOURCE_TEXTURE_HEIGHT;
Self {
alphas: ResourceTexture::new(device, &gpu::alpha_texture_descriptor(dim, min)),
paints: ResourceTexture::new(
device,
&gpu::paint_texture::encoded_paints_texture_descriptor(dim, min),
),
gradients: ResourceTexture::new(device, &gradient_texture_descriptor(dim, min)),
placeholders: Placeholders::new(device),
config: device.create_buffer(&config_descriptor("frust-engine config uniform")),
page_configs: Vec::new(),
instances: None,
instance_capacity: 0,
paints_data: Vec::new(),
paint_slots: Vec::new(),
paint_ramps: Vec::new(),
paint_staging: Vec::new(),
external_runs: ExternalRuns::new(),
bind_groups: HashMap::new(),
bind_group_format: None,
atlas: None,
image_registry: HashMap::new(),
refused_regions: 0,
}
}
fn reset_atlas(&mut self) {
self.atlas = None;
self.image_registry.clear();
self.bind_groups.clear();
}
fn note_refused_regions(&mut self, regions: u64) {
self.refused_regions = self.refused_regions.saturating_add(regions);
ATLAS_REFUSAL_WARNING.call_once(|| {
log::warn!(
"an atlas region was refused by the image atlas array; those images are skipped \
and their uploads re-offered on a later frame (logged once — see \
EngineRenderer::refused_atlas_regions for the count)"
);
});
}
fn resolve_paints(
&mut self,
frame: &CompiledFrame,
cache: &mut GradientCache,
budget: AtlasBudget,
externals: &ExternalTextures,
) {
self.paints_data.clear();
self.paint_slots.clear();
self.external_runs.clear();
self.update_image_registry(frame, budget);
if frame.encoded_paints.is_empty() {
return;
}
self.paint_ramps.clear();
self.paint_ramps.resize(frame.encoded_paints.len(), None);
for request in &frame.lut_requests {
let ramp = resolve_lut_request(*request, &frame.encoded_paints, cache);
if let Some(slot) = self.paint_ramps.get_mut(request.paint_index) {
*slot = ramp;
}
}
let mut texel_offset = 0;
for (index, paint) in frame.encoded_paints.iter().enumerate() {
let mut external = None;
let lowered = match paint {
EncodedPaint::Image(image) => image_id(image)
.and_then(|id| self.image_registry.get(&id))
.and_then(|resident| {
lower_encoded_image(image, resident)
.map(|record| fit_minified(record, resident))
}),
EncodedPaint::ExternalTexture(entry) => externals
.view(entry.texture_id.0)
.and_then(|_| self.external_runs.slot_of(entry.texture_id.0))
.map(|slot| {
external = Some(slot);
lower_encoded_external(entry)
}),
_ => {
let ramp = self.paint_ramps.get(index).copied().flatten();
lower_encoded_paint(paint, ramp)
}
};
let slot = lowered.map(|record| {
let resolved = ResolvedPaint {
paint_type: record.paint_type(),
texel_offset,
opaque: !paint.may_have_transparency(),
external,
};
texel_offset += record.texel_len();
self.paints_data.push(record);
resolved
});
self.paint_slots.push(slot);
}
}
fn update_image_registry(&mut self, frame: &CompiledFrame, budget: AtlasBudget) {
if !frame.image_evictions.is_empty() {
let cleared: HashSet<AtlasRegion> = frame.image_evictions.iter().copied().collect();
self.image_registry
.retain(|_, resident| !cleared.contains(&resident.region));
}
let mut refused = 0_u64;
for upload in &frame.image_uploads {
if !budget.contains(upload.region, frame.atlas_layers) {
refused = refused.saturating_add(1);
continue;
}
self.image_registry.insert(
upload.id,
ResidentImage {
id: upload.id,
region: upload.region,
natural: upload.natural,
padding: u32::from(ATLAS_PADDING),
may_have_transparency: upload.may_have_transparency,
},
);
}
for slot in &frame.glyph_slots {
if !budget.contains(slot.region, frame.atlas_layers) {
refused = refused.saturating_add(1);
continue;
}
self.image_registry.insert(
slot.id,
ResidentImage {
id: slot.id,
region: slot.region,
natural: slot.region.size,
padding: slot.padding,
may_have_transparency: true,
},
);
}
if refused > 0 {
self.note_refused_regions(refused);
}
}
fn ensure_atlas(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
budget: AtlasBudget,
layers: u32,
) -> bool {
let grew = match &mut self.atlas {
None if layers == 0 => false,
None => {
self.atlas = Some(AtlasArray::with_layers(
device,
budget.atlas_size.0,
budget.atlas_size.1,
layers,
));
true
}
Some(atlas) => atlas.ensure_layers(device, queue, layers),
};
if grew {
self.bind_groups.clear();
}
grew
}
fn upload_atlas(&mut self, queue: &wgpu::Queue, frame: &CompiledFrame) -> bool {
let refused = match self.atlas.as_ref() {
None => (frame.image_evictions.len() + frame.image_uploads.len()) as u64,
Some(atlas) => {
let mut refused = 0_u64;
for region in &frame.image_evictions {
if !atlas.clear_region(queue, *region) {
refused = refused.saturating_add(1);
}
}
for upload in &frame.image_uploads {
if !atlas.write_region(queue, upload.region, upload.pixels.data_as_u8_slice()) {
refused = refused.saturating_add(1);
}
}
refused
}
};
if refused == 0 {
return true;
}
self.note_refused_regions(refused);
false
}
fn ensure_page_configs(&mut self, device: &wgpu::Device, rounds: usize) {
while self.page_configs.len() < rounds {
self.page_configs
.push(device.create_buffer(&config_descriptor("frust-engine page config uniform")));
}
}
fn paint_texels(&self) -> u32 {
self.paints_data
.iter()
.map(GpuEncodedPaint::texel_len)
.sum()
}
fn grown_gradient_height(&self, cache: &GradientCache) -> Result<Option<u32>, EngineError> {
let width = self.gradients.width;
let texels = u32::try_from(cache.luts_size() / BYTES_PER_TEXEL as usize)
.map_err(|_| EngineError::PaintCapacity)?;
let required = texels.div_ceil(width).max(gpu::MIN_RESOURCE_TEXTURE_HEIGHT);
if required > width {
return Err(EngineError::PaintCapacity);
}
Ok((required > self.gradients.height).then_some(required))
}
fn resize_alphas(&mut self, device: &wgpu::Device, height: Option<u32>) {
if let Some(height) = height {
self.alphas = ResourceTexture::new(
device,
&gpu::alpha_texture_descriptor(self.alphas.width, height),
);
self.bind_groups.clear();
}
}
fn resize_paints(&mut self, device: &wgpu::Device, height: Option<u32>) {
if let Some(height) = height {
self.paints = ResourceTexture::new(
device,
&gpu::paint_texture::encoded_paints_texture_descriptor(self.paints.width, height),
);
self.bind_groups.clear();
}
}
fn resize_gradients(&mut self, device: &wgpu::Device, height: Option<u32>) {
if let Some(height) = height {
self.gradients = ResourceTexture::new(
device,
&gradient_texture_descriptor(self.gradients.width, height),
);
self.bind_groups.clear();
}
}
fn upload(
&mut self,
queue: &wgpu::Queue,
frame: &mut CompiledFrame,
cache: &mut GradientCache,
size: (u16, u16),
dim: u32,
) -> bool {
let atlas_serviced = self.upload_atlas(queue, frame);
let alphas = &self.alphas;
gpu::with_padded_alphas(
&mut frame.strips.alphas,
alphas.width,
alphas.height,
|bytes| {
queue.write_texture(
alphas.copy_target(),
bytes,
resource_layout(gpu::resource_bytes_per_row(alphas.width), alphas.height),
alphas.extent(),
);
},
);
if !self.paints_data.is_empty() {
let footprint = gpu::resource_texture_bytes(self.paints.width, self.paints.height);
self.paint_staging
.resize(usize::try_from(footprint).unwrap_or(usize::MAX), 0);
if GpuEncodedPaint::serialize_to_buffer(&self.paints_data, &mut self.paint_staging)
.is_ok()
{
queue.write_texture(
self.paints.copy_target(),
&self.paint_staging,
resource_layout(
gpu::resource_bytes_per_row(self.paints.width),
self.paints.height,
),
self.paints.extent(),
);
}
}
if cache.has_changed() {
let layout = GradientTextureLayout {
width: self.gradients.width,
height: self.gradients.height,
};
if let Some(upload) = cache.begin_upload(layout) {
queue.write_texture(
self.gradients.copy_target(),
&upload,
resource_layout(upload.bytes_per_row(), self.gradients.height),
self.gradients.extent(),
);
}
cache.mark_synced();
}
let config = GpuConfig::new(u32::from(size.0), u32::from(size.1), dim, dim);
queue.write_buffer(&self.config, 0, bytemuck::bytes_of(&config));
atlas_serviced
}
fn upload_instances(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, scratch: &Scratch) {
let (opaque, alpha) = scratch.instance_bytes();
let required = (opaque.len() + alpha.len()) as u64;
if required == 0 {
return;
}
let buffer = match &mut self.instances {
Some(buffer) if self.instance_capacity >= required => buffer,
slot => {
let floor = MIN_INSTANCE_CAPACITY * size_of::<GpuStrip>() as u64;
let capacity = required
.checked_next_power_of_two()
.unwrap_or(required)
.max(floor);
self.instance_capacity = capacity;
slot.insert(device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frust-engine strip instances"),
size: capacity,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}))
}
};
if !opaque.is_empty() {
queue.write_buffer(buffer, 0, opaque);
}
if !alpha.is_empty() {
queue.write_buffer(buffer, opaque.len() as u64, alpha);
}
}
fn ensure_bind_groups(
&mut self,
device: &wgpu::Device,
variant: EnginePipeline,
pipeline: &wgpu::RenderPipeline,
format: wgpu::TextureFormat,
) {
if self.bind_group_format != Some(format) {
self.bind_groups.clear();
self.bind_group_format = Some(format);
}
if self.bind_groups.contains_key(&variant) {
return;
}
let atlas_view = self
.atlas
.as_ref()
.map(AtlasArray::view)
.unwrap_or(&self.placeholders.atlas_array);
let groups = StripBindGroups::new(
device,
pipeline,
&self.alphas.view,
&self.config,
&self.placeholders,
atlas_view,
&self.paints.view,
&self.gradients.view,
);
self.bind_groups.insert(variant, groups);
}
fn ensure_external_groups(
&mut self,
device: &wgpu::Device,
variant: EnginePipeline,
pipeline: &wgpu::RenderPipeline,
externals: &ExternalTextures,
) {
if self.external_runs.is_empty() {
return;
}
let Self {
bind_groups,
external_runs,
atlas,
placeholders,
..
} = self;
let atlas_view = atlas
.as_ref()
.map(AtlasArray::view)
.unwrap_or(&placeholders.atlas_array);
let Some(groups) = bind_groups.get_mut(&variant) else {
return;
};
for key in external_runs.keys() {
if groups.externals.contains_key(key) {
continue;
}
let Some(view) = externals.view(*key) else {
continue;
};
groups
.externals
.insert(*key, images_bind_group(device, pipeline, atlas_view, view));
}
}
fn forget_external(&mut self, key: u64) {
for groups in self.bind_groups.values_mut() {
groups.externals.remove(&key);
}
}
}
fn lower_atlas_page(
recorder: &AtlasCommandRecorder,
buffers: &mut AtlasPageBuffers,
lowering: &mut SceneCompiler,
page: (u16, u16),
) -> bool {
let mut scene = Scene::new();
{
let mut builder = SceneBuilder::new(&mut scene);
let mut transform = Affine::IDENTITY;
let mut brush = Brush::Solid(Color::BLACK);
for command in &recorder.commands {
match command {
AtlasCommand::SetTransform(next) => transform = *next,
AtlasCommand::SetPaint(AtlasPaint::Solid(color)) => brush = Brush::Solid(*color),
AtlasCommand::FillPath(path) => {
builder.push_transform(transform);
builder.fill_path((**path).clone(), brush.clone());
builder.pop_transform();
}
AtlasCommand::FillRect(rect) => {
builder.push_transform(transform);
builder.fill_rect(*rect, brush.clone());
builder.pop_transform();
}
_ => return false,
}
}
}
let Ok(frame) = lowering.compile(&scene, Affine::IDENTITY, page) else {
return false;
};
let strips = frame.strip_buf();
for draw in frame.draws() {
let Paint::Solid(color) = &draw.paint else {
return false;
};
let Some(run) = strips.get(draw.strip_range.clone()) else {
continue;
};
push_solid_strips(
run,
color.as_premul_rgba8().to_u32(),
draw.depth,
&mut buffers.instances,
);
}
buffers.alphas.extend_from_slice(frame.alphas());
true
}
fn image_id(image: &EncodedImage) -> Option<ImageId> {
match image.source {
ImageSource::OpaqueId { id, .. } => Some(id),
ImageSource::Pixmap(_) => None,
}
}
fn fit_minified(record: GpuEncodedPaint, resident: &ResidentImage) -> GpuEncodedPaint {
let Some((x, y)) = resident.minify_scale() else {
return record;
};
let GpuEncodedPaint::Image(mut image) = record else {
return record;
};
image.transform[0] *= x;
image.transform[2] *= x;
image.transform[4] *= x;
image.transform[1] *= y;
image.transform[3] *= y;
image.transform[5] *= y;
GpuEncodedPaint::Image(image)
}
fn config_descriptor(label: &str) -> wgpu::BufferDescriptor<'_> {
wgpu::BufferDescriptor {
label: Some(label),
size: GpuConfig::SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}
}
fn resource_layout(bytes_per_row: u32, rows: u32) -> wgpu::TexelCopyBufferLayout {
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(rows),
}
}
fn gradient_texture_descriptor(width: u32, height: u32) -> wgpu::TextureDescriptor<'static> {
wgpu::TextureDescriptor {
label: Some("frust-engine gradient texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: GradientTextureLayout::FORMAT,
usage: gpu::RESOURCE_TEXTURE_USAGES,
view_formats: &[],
}
}
#[derive(Debug)]
struct Placeholders {
layer_input: wgpu::TextureView,
atlas_array: wgpu::TextureView,
external: wgpu::TextureView,
}
impl Placeholders {
fn new(device: &wgpu::Device) -> Self {
Self {
layer_input: placeholder_view(device, "frust-engine layer input placeholder", false),
atlas_array: placeholder_view(device, "frust-engine atlas placeholder", true),
external: placeholder_view(device, "frust-engine external placeholder", false),
}
}
}
fn placeholder_view(device: &wgpu::Device, label: &str, array: bool) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: if array { 2 } else { 1 },
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor {
label: Some(label),
dimension: Some(if array {
wgpu::TextureViewDimension::D2Array
} else {
wgpu::TextureViewDimension::D2
}),
..Default::default()
})
}
#[derive(Debug)]
struct StripBindGroups {
resources: wgpu::BindGroup,
images: wgpu::BindGroup,
paints: wgpu::BindGroup,
gradients: wgpu::BindGroup,
externals: HashMap<u64, wgpu::BindGroup>,
}
impl StripBindGroups {
#[expect(
clippy::too_many_arguments,
reason = "one bind-group build's full resource list; a struct would \
only rename the same borrows the caller already holds \
mutably in `FrameResources`"
)]
fn new(
device: &wgpu::Device,
pipeline: &wgpu::RenderPipeline,
alphas: &wgpu::TextureView,
config: &wgpu::Buffer,
placeholders: &Placeholders,
atlas: &wgpu::TextureView,
paints: &wgpu::TextureView,
gradients: &wgpu::TextureView,
) -> Self {
let resources_group =
resources_bind_group(device, pipeline, alphas, config, &placeholders.layer_input);
let images = images_bind_group(device, pipeline, atlas, &placeholders.external);
let paints = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frust-engine strip paints"),
layout: &pipeline.get_bind_group_layout(2),
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(paints),
}],
});
let gradients = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frust-engine strip gradients"),
layout: &pipeline.get_bind_group_layout(3),
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(gradients),
}],
});
Self {
resources: resources_group,
images,
paints,
gradients,
externals: HashMap::new(),
}
}
fn bind_with(
&self,
pass: &mut wgpu::RenderPass<'_>,
resources: &wgpu::BindGroup,
images: Option<&wgpu::BindGroup>,
) {
pass.set_bind_group(0, resources, &[]);
pass.set_bind_group(1, images.unwrap_or(&self.images), &[]);
pass.set_bind_group(2, &self.paints, &[]);
pass.set_bind_group(3, &self.gradients, &[]);
}
}
fn images_bind_group(
device: &wgpu::Device,
pipeline: &wgpu::RenderPipeline,
atlas: &wgpu::TextureView,
external: &wgpu::TextureView,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frust-engine strip images"),
layout: &pipeline.get_bind_group_layout(1),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(atlas),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(external),
},
],
})
}
fn resources_bind_group(
device: &wgpu::Device,
pipeline: &wgpu::RenderPipeline,
alphas: &wgpu::TextureView,
config: &wgpu::Buffer,
layer_input: &wgpu::TextureView,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frust-engine strip resources"),
layout: &pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(alphas),
},
wgpu::BindGroupEntry {
binding: 1,
resource: config.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(layer_input),
},
],
})
}
#[cfg(test)]
mod tests {
use super::*;
use frust_gpu::DownlevelProfile;
use kurbo::Rect;
use peniko::color::palette::css::{BLUE, RED};
use std::collections::BTreeMap;
use vello_common::geometry::RectU16;
use vello_common::paint::IndexedPaint;
#[cfg(feature = "perf-trace")]
#[test]
fn an_encode_trace_line_reports_every_column_in_order() {
let mut trace = EncodeTrace {
frames: 7,
..EncodeTrace::default()
};
let mut row = [0_u32; ENCODE_TRACE_ROW];
for (column, slot) in row.iter_mut().enumerate() {
*slot = (column as u32 + 1) * 1_000;
}
trace.window.push(row);
let line = trace.line();
let phases: Vec<String> = ENCODE_TRACE_COLUMNS
.iter()
.enumerate()
.map(|(column, name)| format!("{name}_us={}.0", column + 1))
.collect();
let counts: Vec<String> = ENCODE_TRACE_COUNTS
.iter()
.enumerate()
.map(|(offset, name)| {
format!(
"{name}={}",
(ENCODE_TRACE_COLUMNS.len() + offset + 1) * 1_000
)
})
.collect();
assert_eq!(
line,
format!(
"frust-perf enc n=7 w=1 {} total_p95_us={}.0 {}",
phases.join(" "),
ENCODE_TRACE_COLUMNS.len(),
counts.join(" "),
),
);
}
#[cfg(feature = "perf-trace")]
#[test]
fn an_encode_trace_percentile_is_nearest_rank_per_column() {
let mut trace = EncodeTrace::default();
for value in [4_000_u32, 1_000, 3_000, 2_000] {
let mut row = [0_u32; ENCODE_TRACE_ROW];
row[0] = value;
trace.window.push(row);
}
assert_eq!(trace.percentile(0, 50), 2_000);
assert_eq!(trace.percentile(0, 95), 4_000);
trace.window.clear();
assert_eq!(trace.percentile(0, 50), 0);
}
#[cfg(feature = "perf-trace")]
#[test]
fn a_span_that_cost_anything_never_formats_as_zero() {
assert_eq!(format_us(0), "0.0");
assert_eq!(format_us(1), "0.1");
assert_eq!(format_us(4_170_000), "4170.0");
}
#[test]
fn compile_phases_sum_without_overflowing() {
let spans = CompileSpans {
validate: Duration::from_micros(10),
prepare: Duration::from_micros(20),
classify: Duration::from_micros(30),
admit: Duration::from_micros(40),
walk: Duration::from_micros(50),
glyphs: Duration::from_micros(45),
finish: Duration::from_micros(60),
};
assert_eq!(spans.total(), Duration::from_micros(210));
assert_eq!(
CompileSpans {
validate: Duration::MAX,
walk: Duration::MAX,
..CompileSpans::default()
}
.total(),
Duration::MAX,
);
}
#[test]
fn a_target_past_the_u16_grid_is_refused() {
assert_eq!(
grid_size(1920, 1080).expect("a normal surface"),
(1920, 1080)
);
assert_eq!(
grid_size(u32::from(u16::MAX), 16).expect("the grid ceiling itself"),
(u16::MAX, 16)
);
assert!(matches!(
grid_size(u32::from(u16::MAX) + 1, 16),
Err(EngineError::TargetTooLarge)
));
assert!(matches!(
grid_size(16, u32::from(u16::MAX) + 1),
Err(EngineError::TargetTooLarge)
));
}
#[test]
fn a_premultiplied_clear_folds_alpha_into_the_colour() {
let half = RED.with_alpha(0.5);
let premultiplied = clear_color(half, OutputAlpha::Premultiplied);
let straight = clear_color(half, OutputAlpha::Straight);
assert!((premultiplied.r - 0.5).abs() < 1e-6);
assert!((premultiplied.a - 0.5).abs() < 1e-6);
assert!((straight.r - 1.0).abs() < 1e-6);
assert!((straight.a - 0.5).abs() < 1e-6);
assert_eq!(
clear_color(BLUE, OutputAlpha::Premultiplied),
clear_color(BLUE, OutputAlpha::Straight)
);
}
#[test]
fn a_solid_paint_travels_premultiplied_in_the_instance_payload() {
let paint = pack_paint(&Paint::from(RED), 3, &[]).expect("a solid paint always resolves");
let values = paint.values_at(40, 12);
assert_eq!(values.paint, SOLID_PAINT);
assert_eq!(values.depth_index, 3);
assert_eq!(values.payload, RED.premultiply().to_rgba8().to_u32());
assert!(paint.opaque);
assert_eq!(
paint.values_at(0, 0).payload,
values.payload,
"a solid colour is the same wherever it is stamped"
);
let paint = pack_paint(&Paint::from(RED.with_alpha(0.5)), 0, &[])
.expect("a translucent solid paint still resolves");
assert!(
!paint.opaque,
"a translucent paint never reaches the opaque pass"
);
}
#[test]
fn an_unresolvable_indexed_paint_skips_its_draw() {
let indexed = Paint::Indexed(IndexedPaint::new(0));
assert!(
pack_paint(&indexed, 0, &[]).is_none(),
"an index past the frame's slots resolves to nothing"
);
assert!(
pack_paint(&indexed, 0, &[None]).is_none(),
"so does a slot the lowering refused"
);
}
#[test]
fn a_resolved_indexed_paint_samples_at_each_instances_own_position() {
let slots = [Some(ResolvedPaint {
paint_type: PaintType::LinearGradient,
texel_offset: 6,
opaque: true,
external: None,
})];
let paint = pack_paint(&Paint::Indexed(IndexedPaint::new(0)), 2, &slots)
.expect("a lowered paint resolves");
assert_eq!(
paint.paint,
pack_paint_descriptor(PaintType::LinearGradient, 6)
);
assert_eq!(paint.depth_index, 2);
assert_eq!(paint.payload_at(8, 4), 8 | (4 << 16));
assert_eq!(paint.payload_at(40, 4), 40 | (4 << 16));
}
#[test]
fn a_u16_pair_packs_low_half_first() {
assert_eq!(pack_u16_pair(0, 0), 0);
assert_eq!(pack_u16_pair(1, 0), 1);
assert_eq!(pack_u16_pair(0, 1), 1 << 16);
assert_eq!(pack_u16_pair(u16::MAX, u16::MAX), u32::MAX);
}
#[test]
fn the_minimum_resource_dimension_keeps_every_upload_row_legal() {
assert_eq!(
MIN_RESOURCE_TEXTURE_DIM * BYTES_PER_TEXEL,
wgpu::COPY_BYTES_PER_ROW_ALIGNMENT
);
assert!(MIN_RESOURCE_TEXTURE_DIM.is_power_of_two());
}
const PLAN_VIEWPORT: (u16, u16) = (64, 48);
const PLAN_SLOT: Rect = Rect::new(4.0, 4.0, 32.0, 44.0);
const PLAN_CHIP: Rect = Rect::new(16.0, 12.0, 56.0, 32.0);
fn plan_of(scene: &Scene) -> (Scratch, Vec<Round>) {
let frame = SceneCompiler::new(PLAN_VIEWPORT.0, PLAN_VIEWPORT.1)
.compile(scene, Affine::IDENTITY, PLAN_VIEWPORT)
.expect("an in-range scene compiles");
let rounds = Schedule::build(
&frame.recorder,
&TierCaps::fake(DownlevelProfile::Full),
&PageConfig::default(),
)
.expect("a scene of solid fills schedules");
let mut scratch = Scratch::default();
scratch.build(&frame, &rounds, true, !frame.clears.is_empty(), &[]);
(scratch, rounds)
}
fn plan_scene(record: impl FnOnce(&mut SceneBuilder<'_>)) -> Scene {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
record(&mut builder);
scene
}
fn plan_backdrop(builder: &mut SceneBuilder<'_>) {
builder.fill_rect(
Rect::new(
0.0,
0.0,
f64::from(PLAN_VIEWPORT.0),
f64::from(PLAN_VIEWPORT.1),
),
Brush::Solid(RED),
);
}
#[test]
fn a_frame_that_punches_nothing_is_planned_exactly_as_its_rounds() {
let (scratch, rounds) = plan_of(&plan_scene(|builder| {
plan_backdrop(builder);
builder.fill_rect(PLAN_CHIP, Brush::Solid(BLUE));
}));
assert_eq!(
scratch.rounds.len(),
rounds.len(),
"a frame with no clear is cut nowhere, so it costs exactly its \
scheduled rounds' passes"
);
assert!(!scratch.punches(), "and records no punch pass at all");
}
#[test]
fn a_clear_recorded_last_keeps_its_pass_after_every_round() {
let (scratch, rounds) = plan_of(&plan_scene(|builder| {
plan_backdrop(builder);
builder.clear_rect(PLAN_SLOT);
}));
assert_eq!(
scratch.rounds.len(),
rounds.len(),
"nothing is recorded after the clear, so nothing is cut"
);
assert!(
scratch.rounds.iter().all(|plan| plan.punch.1 == 0),
"no round carries the punch"
);
assert!(
scratch.punch.1 > 0,
"it is issued after the last round instead — the position an \
unconditionally-trailing pass would have put it in, which is why \
a frame shaped like this renders byte-identically"
);
}
#[test]
fn a_draw_over_a_clear_cuts_the_surface_round_and_the_punch_goes_in_the_cut() {
let (scratch, rounds) = plan_of(&plan_scene(|builder| {
plan_backdrop(builder);
builder.clear_rect(PLAN_SLOT);
builder.fill_rect(PLAN_CHIP, Brush::Solid(BLUE));
}));
assert_eq!(
scratch.rounds.len(),
rounds.len() + 1,
"the surface round is cut in two — one extra pass, and one only"
);
assert_eq!(
scratch.punch,
(0, 0),
"with nothing left over for a trailing pass"
);
let cut = scratch
.rounds
.iter()
.position(|plan| plan.punch.1 > 0)
.expect("the cut plan carries the punch");
assert_eq!(cut, scratch.rounds.len() - 2, "and it is the cut plan");
let before = scratch.rounds[cut].segments.clone();
let after = scratch.rounds[cut + 1].segments.clone();
let end_of = |range: Range<usize>| {
scratch.segments[range]
.iter()
.map(|segment| match *segment {
Segment::Strips(first, count) | Segment::External(first, count, _) => {
first + count
}
Segment::Composite(first, _) => first + 1,
})
.max()
.expect("a plan of this frame draws something")
};
let (punch_first, punch_count) = scratch.rounds[cut].punch;
assert!(
end_of(before) <= punch_first,
"everything recorded before the clear is erased by the punch"
);
assert!(
scratch.segments[after]
.iter()
.all(|segment| match *segment {
Segment::Strips(first, _)
| Segment::External(first, _, _)
| Segment::Composite(first, _) => first >= punch_first + punch_count,
}),
"and everything recorded after it lands on top of the erase"
);
}
const BAND_VIEWPORT: (u16, u16) = (200, 8);
const BAND_PAGES: PageConfig = PageConfig {
min_page_size: 64,
max_page_size: 64,
};
fn band_plan_of(scene: &Scene, config: &PageConfig) -> (Scratch, Vec<Round>) {
let frame = SceneCompiler::new(BAND_VIEWPORT.0, BAND_VIEWPORT.1)
.compile(scene, Affine::IDENTITY, BAND_VIEWPORT)
.expect("an in-range scene compiles");
let rounds = Schedule::build(
&frame.recorder,
&TierCaps::fake(DownlevelProfile::Full),
config,
)
.expect("a layer wider than the ceiling bands rather than refusing");
let mut scratch = Scratch::default();
scratch.build(&frame, &rounds, false, false, &[]);
assert_eq!(
scratch.rounds.len(),
rounds.len(),
"a frame with no clear is cut nowhere, so the plans and the rounds \
line up one for one"
);
(scratch, rounds)
}
fn band_scene(rects: &[(Rect, Color)]) -> Scene {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.push_layer(
Rect::new(
0.0,
0.0,
f64::from(BAND_VIEWPORT.0),
f64::from(BAND_VIEWPORT.1),
),
0.5,
);
for (rect, color) in rects {
builder.fill_rect(*rect, Brush::Solid(*color));
}
builder.pop_layer();
scene
}
fn band_bounds(rounds: &[Round]) -> Vec<RectU16> {
rounds
.iter()
.filter_map(|round| round.page().map(|page| page.bounds))
.collect()
}
fn plan_strips(scratch: &Scratch, plan: &RoundPlan) -> Vec<GpuStrip> {
scratch.segments[plan.segments.clone()]
.iter()
.filter_map(|segment| match *segment {
Segment::Strips(first, count) | Segment::External(first, count, _) => {
Some((first as usize, count as usize))
}
Segment::Composite(..) => None,
})
.flat_map(|(first, count)| scratch.alpha[first..first + count].iter().copied())
.collect()
}
fn painted_pixels(
scratch: &Scratch,
rounds: &[Round],
) -> BTreeMap<(u32, u16, u16), (u32, Option<u32>)> {
let mut painted = BTreeMap::new();
for (plan, round) in scratch.rounds.iter().zip(rounds) {
let Some(page) = round.page() else {
continue;
};
for span in plan_strips(scratch, plan) {
for offset in 0..span.width {
let x = page.bounds.x0 + span.x + offset;
let y = page.bounds.y0 + span.y;
let column = (offset < span.dense_width_or_rect_height)
.then(|| span.col_idx_or_rect_frac + u32::from(offset));
assert!(
painted
.insert((span.depth_index, y, x), (span.payload, column))
.is_none(),
"one draw covers a device pixel at most once, however the \
layer holding it was split"
);
}
}
}
painted
}
#[test]
fn a_banded_layer_paints_exactly_what_one_page_would_have() {
let scene = band_scene(&[
(Rect::new(0.0, 0.0, 40.0, 8.0), RED),
(Rect::new(49.0, 0.0, 99.0, 8.0), BLUE),
(Rect::new(160.0, 0.0, 200.0, 8.0), RED),
]);
let (one_page, one_page_rounds) = band_plan_of(&scene, &PageConfig::default());
let (banded, banded_rounds) = band_plan_of(&scene, &BAND_PAGES);
assert_eq!(
band_bounds(&one_page_rounds).len(),
1,
"the reference plan really does hold the layer on one page"
);
assert!(
band_bounds(&banded_rounds).len() > 1,
"and the case only means anything while the other one is banded"
);
assert_eq!(
painted_pixels(&banded, &banded_rounds),
painted_pixels(&one_page, &one_page_rounds),
"a banded layer paints what one whole-layer page would have"
);
}
#[test]
fn a_band_holds_no_instance_of_a_strip_outside_its_own_column() {
let scene = band_scene(&[
(Rect::new(0.0, 0.0, 40.0, 8.0), RED),
(Rect::new(160.0, 0.0, 200.0, 8.0), BLUE),
]);
let (scratch, rounds) = band_plan_of(&scene, &BAND_PAGES);
let bands = band_bounds(&rounds);
assert!(bands.len() > 2, "the layer bands: {bands:?}");
let mut empty = 0_usize;
for (plan, band) in scratch
.rounds
.iter()
.zip(&rounds)
.filter_map(|(plan, round)| round.page().map(|page| (plan, page.bounds)))
{
let strips = plan_strips(&scratch, plan);
if strips.is_empty() {
empty += 1;
}
for span in strips {
let end = span.x + span.width;
assert!(
end <= band.width(),
"an instance of {span:?} reaches past the {band:?} band's own \
column, which its composite never samples"
);
let x = band.x0 + span.x;
assert!(
x < 44 || band.x0 + end > 160,
"an instance covers device x {x}..{}, which neither \
rectangle reaches",
band.x0 + end
);
}
}
assert!(
empty > 0,
"a band whose column holds nothing renders nothing: {bands:?}"
);
}
#[test]
fn a_strip_straddling_a_band_edge_advances_its_alpha_column_with_its_geometry() {
let scene = band_scene(&[
(Rect::new(0.0, 0.0, 40.0, 8.0), RED),
(Rect::new(49.0, 0.0, 99.0, 8.0), BLUE),
(Rect::new(160.0, 0.0, 200.0, 8.0), RED),
]);
let (scratch, rounds) = band_plan_of(&scene, &BAND_PAGES);
let edges: Vec<u16> = band_bounds(&rounds)
.iter()
.map(|bounds| bounds.x0)
.collect();
let mut dense: Vec<(u16, u16, u32)> = Vec::new();
for (plan, band) in scratch
.rounds
.iter()
.zip(&rounds)
.filter_map(|(plan, round)| round.page().map(|page| (plan, page.bounds)))
{
for span in plan_strips(&scratch, plan) {
for offset in 0..span.dense_width_or_rect_height {
dense.push((
band.y0 + span.y,
band.x0 + span.x + offset,
span.col_idx_or_rect_frac + u32::from(offset),
));
}
}
}
dense.sort_unstable();
let neighbours = || {
dense
.windows(2)
.map(|pair| (pair[0], pair[1]))
.filter(|((row, x, _), (next_row, next_x, _))| row == next_row && x + 1 == *next_x)
};
assert!(
neighbours().any(|(_, (_, x, _))| edges.contains(&x)),
"the case only means anything while an alpha-sampled instance \
really is cut by a band edge {edges:?}: {dense:?}"
);
for ((_, _, column), (_, x, next_column)) in neighbours() {
assert_eq!(
next_column,
column + 1,
"neighbouring pixels of one strip row sample neighbouring alpha \
columns, band edge at {x} or not: {dense:?}"
);
}
}
#[test]
fn a_translucent_layer_over_a_clear_is_composited_after_the_punch() {
let (scratch, rounds) = plan_of(&plan_scene(|builder| {
plan_backdrop(builder);
builder.clear_rect(PLAN_SLOT);
builder.push_layer(PLAN_CHIP, 0.5);
builder.fill_rect(PLAN_CHIP, Brush::Solid(BLUE));
builder.pop_layer();
}));
assert_eq!(
scratch.rounds.len(),
rounds.len() + 1,
"the layer's page round, then the surface round cut in two"
);
let cut = scratch
.rounds
.iter()
.position(|plan| plan.punch.1 > 0)
.expect("the cut plan carries the punch");
let composited = scratch.segments[scratch.rounds[cut + 1].segments.clone()]
.iter()
.any(|segment| matches!(segment, Segment::Composite(..)));
assert!(
composited,
"the layer composites in the plan AFTER the punch, not before it"
);
assert!(
!scratch.segments[scratch.rounds[cut].segments.clone()]
.iter()
.any(|segment| matches!(segment, Segment::Composite(..))),
"and nothing composites into the plan the punch closes"
);
}
}