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::retained::{
RetainedBudget, RetainedDecision, RetainedEntry, RetainedPages, RetainedPlan,
};
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, GlyphQuad, PhaseClock, RetainedKind, 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::{Backdrop, FilterStep, ServedFilter, served_filter};
use crate::gpu::atlas::{
AtlasPageBuffers, AtlasRenderReport, AtlasRenderer, lower_encoded_image, push_solid_strips,
};
use crate::gpu::bindings::{
ATLAS_SAMPLER_BINDING, ExternalRuns, ExternalTextures, lower_encoded_external,
};
use crate::gpu::config::resource_widths_agree;
use crate::gpu::depth::DepthAttachment;
use crate::gpu::paint_texture::lower_encoded_paint;
use crate::gpu::pipelines::{
EnginePipeline, EngineShaders, INTERMEDIATE_FORMAT, atlas_strip_desc, warm_up_descs_for,
};
use crate::gpu::strips::{
ClassRun, ClassSet, PaintClass, PaintType, merge_class_runs, 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, RetainedLayers, RetainedRole,
RetainedTarget, Round, RoundOp, Schedule,
};
use crate::{EngineTarget, OutputAlpha};
use vello_common::geometry::RectU16;
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 BACKDROP_UNREADABLE_WARNING: Once = Once::new();
const COPY_QUAD_VERTICES: u32 = 4;
const COPY_INSTANCE_BYTES: u64 = 16;
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; 11] = [
"draws",
"strips",
"alphas",
"glyph_draws",
"atlas_glyphs",
"replays",
"dirty_pages",
"atlas_refused",
"glyph_misses",
"glyph_evicted",
"run_keys",
];
#[cfg(feature = "perf-trace")]
const ENCODE_TRACE_FIRST_SUMMED: usize = 5;
#[cfg(feature = "perf-trace")]
const ENCODE_TRACE_MEDIAN_AGAIN: usize = 10;
#[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,
replays: u32,
dirty_pages: u32,
atlas_refused: u32,
glyph_misses: u32,
glyph_evicted: u32,
run_keys: u32,
}
#[cfg(feature = "perf-trace")]
impl EncodeCounts {
fn of(
frame: &CompiledFrame,
replayed: bool,
report: &AtlasRenderReport,
policy: &crate::text::atlas_policy::AtlasTrace,
) -> 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,
replays: u32::from(replayed),
dirty_pages: report.pages,
atlas_refused: report.refused,
glyph_misses: policy.glyph_misses,
glyph_evicted: policy.glyph_evicted,
run_keys: policy.run_keys(),
}
}
}
#[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,
self.counts.replays,
self.counts.dirty_pages,
self.counts.atlas_refused,
self.counts.glyph_misses,
self.counts.glyph_evicted,
self.counts.run_keys,
]
}
}
#[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 column = ENCODE_TRACE_COLUMNS.len() + offset;
let value = if (ENCODE_TRACE_FIRST_SUMMED..ENCODE_TRACE_MEDIAN_AGAIN).contains(&offset)
{
self.window
.iter()
.filter_map(|row| row.get(column))
.fold(0_u64, |acc, v| acc.saturating_add(u64::from(*v)))
} else {
u64::from(self.percentile(column, 50))
};
line.push(' ');
line.push_str(name);
line.push('=');
line.push_str(&value.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,
routing: ClassRouting,
pass_merge: bool,
frame_passes: Vec<FramePass>,
resolved: ResolvedPipelines,
atlas_glyphs: Option<AtlasRenderer>,
atlas_lowering: Option<SceneCompiler>,
atlas_report: AtlasRenderReport,
filters: Option<FilterResources>,
region_copies: Option<RegionCopyResources>,
retained: RetainedPages,
retained_layers: RetainedLayers,
range_scene: Scene,
#[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> {
Self::with_image_sampler(
device,
caps,
format,
pipeline_cache,
config::image_sampler_enabled(),
)
}
pub fn with_image_sampler(
device: &wgpu::Device,
caps: &TierCaps,
format: wgpu::TextureFormat,
pipeline_cache: Option<&wgpu::PipelineCache>,
image_sampler: bool,
) -> 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 routing = ClassRouting::from_config();
let mut library = ShaderLibrary::new();
let shaders = EngineShaders::register_family(&mut library, device, image_sampler);
let mut pipelines = PipelineCache::new(Arc::new(library), pipeline_cache.cloned());
pipelines.warm_up(device, &warm_up_descs_for(&shaders, format, routing.uber));
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: {
let mut compiler = SceneCompiler::for_caps(1, 1, caps);
compiler.set_retained_layers(!config::layers_disabled());
compiler
},
gradients,
depth: DepthAttachment::new(),
targets: IntermediateTargets::new(caps),
pages: PageConfig::default(),
textures: ExternalTextures::new(),
resources: FrameResources::new(device, dim, image_sampler),
scratch: Scratch::default(),
routing,
pass_merge: !config::pass_merge_disabled(),
frame_passes: Vec::new(),
resolved: ResolvedPipelines::default(),
atlas_glyphs: None,
atlas_lowering: None,
atlas_report: AtlasRenderReport::default(),
filters: None,
region_copies: None,
retained: RetainedPages::for_caps(caps),
retained_layers: RetainedLayers::new(),
range_scene: Scene::new(),
#[cfg(feature = "perf-trace")]
encode_trace: EncodeTrace::default(),
})
}
#[must_use]
pub fn format(&self) -> wgpu::TextureFormat {
self.format
}
#[must_use]
pub fn image_sampler(&self) -> bool {
self.shaders.image_sampler
}
#[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 desc in warm_up_descs_for(&self.shaders, self.format, self.routing.uber) {
let _ = self.pipelines.get_or_create(device, &desc);
}
}
pub fn set_uber_program(&mut self, uber: bool) {
self.routing.uber = uber;
}
#[must_use]
pub fn uber_program(&self) -> bool {
self.routing.uber
}
pub fn set_glyph_quads(&mut self, enabled: bool) {
self.compiler.set_glyph_quads(enabled);
}
#[must_use]
pub fn glyph_quads(&self) -> bool {
self.compiler.glyph_quads_enabled()
}
pub fn set_run_split_min(&mut self, min_run: u32) {
self.routing.min_run = min_run;
}
#[must_use]
pub fn run_split_min(&self) -> u32 {
self.routing.min_run
}
pub fn set_pass_merge(&mut self, merge: bool) {
self.pass_merge = merge;
}
#[must_use]
pub fn pass_merge(&self) -> bool {
self.pass_merge
}
#[must_use]
pub fn last_frame_passes(&self) -> &[FramePass] {
&self.frame_passes
}
#[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);
self.retained.clear();
}
pub fn retained_ledger(&self) -> impl Iterator<Item = RetainedEntry> + '_ {
self.retained.entries()
}
#[must_use]
pub fn retained_frame(&self) -> u64 {
self.retained.frame()
}
#[must_use]
pub fn retained_generation(&self) -> u64 {
self.retained.generation()
}
pub fn take_retained_refusal(&mut self) -> bool {
self.retained.take_refusal()
}
pub fn clear_retained(&mut self) {
self.retained.clear();
}
#[must_use]
pub fn retained_budget(&self) -> RetainedBudget {
self.retained.budget()
}
pub fn set_retained_budget(&mut self, budget: RetainedBudget) {
self.retained.set_budget(budget);
}
pub fn set_retained_layers(&mut self, enabled: bool) {
self.compiler.set_retained_layers(enabled);
if !enabled {
self.retained.clear();
}
}
#[must_use]
pub fn retained_layers(&self) -> bool {
self.compiler.retained_layers_enabled()
}
#[cfg(feature = "perf-trace")]
pub fn take_pipeline_wait(&mut self) -> Duration {
self.pipelines.take_resolve_wait()
}
#[must_use]
pub fn warm_up_finished(&self) -> u64 {
self.pipelines.warm_up_finished()
}
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();
self.frame_passes.clear();
let size = grid_size(target.width, target.height)?;
let mut frame = self.compiler.compile(scene, root, size)?;
let compile_total = clock.lap();
let retained_plan = self
.retained
.plan(&frame.retained, &mut frame.recorder.layers)?;
retained_roles(&frame, &retained_plan, &mut self.retained_layers);
let copy_mode = RegionCopyMode::of(target.view.texture());
let serve_backdrops = copy_mode != RegionCopyMode::Unavailable;
if !frame.backdrops.is_empty() && !serve_backdrops {
note_backdrop_unreadable(target.view.texture());
}
let backdrops: &[Backdrop] = if serve_backdrops {
&frame.backdrops
} else {
&[]
};
let staging_extent = backdrops.iter().fold((0_u32, 0_u32), |(w, h), backdrop| {
(
w.max(u32::from(backdrop.region.width())),
h.max(u32::from(backdrop.region.height())),
)
});
let rounds = Schedule::build_frame(
&frame.recorder,
&self.caps,
&self.pages,
&self.retained_layers,
backdrops,
)?;
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.routing = self.routing;
self.scratch.build(
&frame,
&rounds,
depth_view.is_some(),
punches,
&self.resources.paint_slots,
if serve_backdrops {
&frame.backdrops
} else {
&[]
},
);
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)?;
let rendered = retained_rendered(&frame, &rounds);
self.retained.commit(
device,
&frame.retained,
&frame.retained_superseded,
&retained_plan,
&rendered,
);
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();
self.atlas_report = AtlasRenderReport::default();
#[cfg(feature = "perf-trace")]
let mut replayed = false;
if self.compiler.glyph_replay_pending()
&& self.replay_glyph_pages(device, queue, timestamps)
{
self.compiler.acknowledge_glyph_replay();
#[cfg(feature = "perf-trace")]
{
replayed = true;
}
}
let replay_span = clock.lap();
let format = target.format;
let pipelines = self.frame_pipelines(device, format, depth_view.is_some(), copy_mode);
if pipelines.copy.is_some() {
let copies = self
.region_copies
.get_or_insert_with(|| RegionCopyResources::new(device));
if copy_mode == RegionCopyMode::Texture {
copies.ensure_staging(device, staging_extent.0, staging_extent.1);
}
}
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, copy_mode,
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,
replayed,
&self.atlas_report,
self.compiler.glyph_atlas_trace(),
),
});
Ok(())
}
#[expect(
clippy::too_many_arguments,
reason = "[`Self::encode_traced`]'s argument list plus the range, each \
still supplied by a different owner"
)]
pub fn record_range(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
scene: &Scene,
range: Range<usize>,
target: EngineTarget<'_>,
base_color: Color,
root: Affine,
timestamps: FrameTimestamps<'_>,
) -> Result<RangeCut, EngineError> {
let mut projected = std::mem::take(&mut self.range_scene);
let options = RangeOptions {
keep_draw_retained: self.retained_layers(),
};
let cut = project_command_range(scene, range, options, &mut projected);
let result = self.encode_traced(
device, queue, encoder, &projected, target, base_color, root, timestamps,
);
self.range_scene = projected;
result.map(|()| cut)
}
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,
copy_mode: RegionCopyMode,
) -> FramePipelines {
let alpha_variant = if depth {
EnginePipeline::StripDepthAlpha
} else {
EnginePipeline::StripAlpha
};
let punch_variant = if depth {
EnginePipeline::StripDepthDestOut
} else {
EnginePipeline::StripDestOut
};
let uber = ClassSet::only(PaintClass::Uber);
if self.resolved.format != Some(format) {
self.resolved = ResolvedPipelines {
format: Some(format),
..ResolvedPipelines::default()
};
}
let surface_classes = if self.scratch.surface_classes.is_empty() {
uber
} else {
self.scratch.surface_classes
};
let mut frame = FramePipelines {
alpha: (
alpha_variant,
self.class_pipelines(device, format, alpha_variant, surface_classes),
),
opaque: None,
page: None,
punch: None,
filter: None,
copy: None,
};
if depth && !self.scratch.opaque.is_empty() {
frame.opaque = Some(self.class_pipelines(
device,
format,
EnginePipeline::StripOpaque,
self.scratch.opaque_classes,
));
}
if self.scratch.page_rounds() > 0 {
let page_classes = if self.scratch.page_classes.is_empty() {
uber
} else {
self.scratch.page_classes
};
frame.page = Some(self.class_pipelines(
device,
format,
EnginePipeline::StripIntermediate,
page_classes,
));
}
if self.scratch.punches() {
frame.punch = Some((
punch_variant,
self.class_pipelines(
device,
format,
punch_variant,
ClassSet::only(self.routing.punch_class()),
),
));
}
if self.scratch.filter_passes > 0 {
frame.filter = Some(
self.pipelines
.get_or_create(device, &EnginePipeline::Filter.desc(&self.shaders, format))
.clone(),
);
}
if self.scratch.copies > 0 && copy_mode != RegionCopyMode::Unavailable {
frame.copy = Some(
self.pipelines
.get_or_create(device, &EnginePipeline::Copy.desc(&self.shaders, format))
.clone(),
);
}
if let Some(pipeline) = frame.alpha.1.any() {
self.resources
.ensure_bind_groups(device, frame.alpha.0, pipeline, format);
self.resources
.ensure_external_groups(device, frame.alpha.0, pipeline, &self.textures);
}
if let Some(pipeline) = frame.opaque.as_ref().and_then(ClassPipelines::any) {
self.resources.ensure_bind_groups(
device,
EnginePipeline::StripOpaque,
pipeline,
format,
);
}
if let Some(pipeline) = frame.page.as_ref().and_then(ClassPipelines::any) {
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, pipelines)) = frame.punch.as_ref()
&& let Some(pipeline) = pipelines.any()
{
self.resources
.ensure_bind_groups(device, *variant, pipeline, format);
}
self.resources
.ensure_page_configs(device, self.scratch.page_rounds());
frame
}
fn class_pipelines(
&mut self,
device: &wgpu::Device,
format: wgpu::TextureFormat,
state: EnginePipeline,
classes: ClassSet,
) -> ClassPipelines {
let mut out = ClassPipelines::default();
for class in classes.iter() {
let class = state.fold_class(class);
if out.get(class).is_some() {
continue;
}
let slot = ResolvedPipelines::slot(state, class);
let pipeline = match self.resolved.entries.get(slot).and_then(Option::as_ref) {
Some(pipeline) => pipeline.clone(),
None => {
let pipeline = self
.pipelines
.get_or_create(device, &state.class_desc(&self.shaders, format, class))
.clone();
if let Some(entry) = self.resolved.entries.get_mut(slot) {
*entry = Some(pipeline.clone());
}
pipeline
}
};
out.set(class, pipeline);
}
out
}
#[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,
copy_mode: RegionCopyMode,
timestamps: FrameTimestamps<'_>,
) {
let merge = self.pass_merge;
let clear = clear_color(base_color, target.output);
let depth_load = self.depth.load_op(target.depth.is_some());
if !merge {
drop(encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(FramePass::Clear.label()),
color_attachments: &[Some(color_attachment(
target.view,
wgpu::LoadOp::Clear(clear),
))],
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,
}));
self.frame_passes.push(FramePass::Clear);
}
let Some(instances) = self.resources.instances.as_ref() else {
if merge {
SurfaceOpening {
view: target.view,
depth: depth_view,
clear,
depth_load,
opaque: None,
}
.record(
encoder,
None,
timestamps.writes(EngineSpan::Main),
&mut self.frame_passes,
);
}
#[cfg(debug_assertions)]
self.check_pass_list(merge, false, false, false);
return;
};
let dim = self.caps.resource_texture_dim;
let opaque_count = self.scratch.opaque.len() as u32;
let base = opaque_count;
let opaque = if opaque_count > 0
&& let Some(opaque) = pipelines.opaque.as_ref()
&& let Some(opaque_groups) =
self.resources.bind_groups.get(&EnginePipeline::StripOpaque)
{
Some(PassPlan {
pass: FramePass::Opaque,
view: target.view,
load: wgpu::LoadOp::Load,
depth: depth_view,
state: EnginePipeline::StripOpaque,
pipelines: opaque,
groups: opaque_groups,
resources: &opaque_groups.resources,
composites: &[],
external_keys: &[],
instances,
base: 0,
segments: &self.scratch.opaque_segments,
composite_class: self.routing.composite_class(),
})
} else {
None
};
let opaque_drawn = opaque.is_some();
let mut opening = if merge {
Some(SurfaceOpening {
view: target.view,
depth: depth_view,
clear,
depth_load,
opaque,
})
} else {
if let Some(opaque) = opaque.as_ref() {
record_pass(
encoder,
opaque,
timestamps.writes(EngineSpan::Main),
&mut self.frame_passes,
);
}
None
};
let punch_pass = pipelines.punch.as_ref().and_then(|(variant, punch)| {
Some(PunchPass {
view: target.view,
depth: depth_view,
state: *variant,
pipelines: punch,
class: self.routing.punch_class(),
groups: self.resources.bind_groups.get(variant)?,
instances,
base,
})
});
let punch_drawn = punch_pass.is_some();
let mut live: [Option<PooledTexture>; MAX_LIVE_PAGES] = [const { None }; MAX_LIVE_PAGES];
let mut page_slot = 0_usize;
let mut copy_slot = 0_u32;
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(source) = plan.copy {
if let Some((_, pooled)) = own.as_ref() {
record_owed_opening(&mut opening, encoder, timestamps, &mut self.frame_passes);
RegionCopyPass {
mode: copy_mode,
target: target.view,
page: pooled,
source,
pipeline: pipelines.copy.as_ref(),
resources: self.region_copies.as_ref(),
slot: copy_slot,
}
.record(
device,
queue,
encoder,
timestamps.writes(EngineSpan::Composite),
);
copy_slot = copy_slot.saturating_add(1);
self.frame_passes.push(FramePass::Copy);
}
settle_pages(&mut self.targets, &mut live, plan.released, own);
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),
);
self.frame_passes.push(FramePass::Filter);
}
settle_pages(&mut self.targets, &mut live, plan.released, own);
continue;
}
let retained_page = plan
.retained
.and_then(|retained| self.retained.page_for(retained.bracket));
if plan.retained.is_some() && retained_page.is_none() {
settle_pages(&mut self.targets, &mut live, plan.released, own);
continue;
}
let offscreen = own
.as_ref()
.map(|(_, pooled)| (pooled.view(), pooled.size()))
.or(retained_page);
let continued = plan.page.is_some_and(|page| page.continued)
|| plan.retained.is_some_and(|retained| retained.continued);
let config = match offscreen {
None => &self.resources.config,
Some((_, (width, height))) => {
let Some(config) = self.resources.page_configs.get(page_slot) else {
continue;
};
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 offscreen {
None => pipelines.alpha.0,
Some(_) => EnginePipeline::StripIntermediate,
};
let class_pipelines = match (offscreen, pipelines.page.as_ref()) {
(None, _) => &pipelines.alpha.1,
(Some(_), Some(page)) => page,
(Some(_), None) => continue,
};
let Some(pipeline) = class_pipelines.any() else {
continue;
};
let Some(groups) = self.resources.bind_groups.get(&variant) else {
continue;
};
let page_resources = offscreen.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::Backdrop(_, _, 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,
));
}
if let Segment::Composite(_, source) = *segment {
let view = match source {
CompositeSource::Page(parity) => live[parity.index()].as_ref().map_or(
&self.resources.placeholders.layer_input,
PooledTexture::view,
),
CompositeSource::Retained(bracket) => self
.retained
.page_for(bracket)
.map_or(&self.resources.placeholders.layer_input, |(view, _)| view),
};
composites.push(resources_bind_group(
device,
pipeline,
&self.resources.alphas.view,
config,
view,
));
}
}
let span = if offscreen.is_some() {
EngineSpan::Composite
} else {
EngineSpan::Main
};
let (view, pass, load, depth) = match offscreen {
Some((view, _)) => (
view,
FramePass::Page,
if continued {
wgpu::LoadOp::Load
} else {
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
},
None,
),
None => (
target.view,
FramePass::Surface,
wgpu::LoadOp::Load,
depth_view,
),
};
if !segments.is_empty() || offscreen.is_some() {
let round = PassPlan {
pass,
view,
load,
depth,
state: variant,
pipelines: class_pipelines,
groups,
resources,
composites: &composites,
external_keys: self.resources.external_runs.keys(),
instances,
base,
segments,
composite_class: self.routing.composite_class(),
};
let opening = if offscreen.is_none() {
opening.take()
} else {
None
};
match opening {
Some(opening) => opening.record(
encoder,
Some(&round),
timestamps.writes(span),
&mut self.frame_passes,
),
None => record_pass(
encoder,
&round,
timestamps.writes(span),
&mut self.frame_passes,
),
}
}
settle_pages(&mut self.targets, &mut live, plan.released, own);
if let Some(punch) = punch_pass.as_ref()
&& plan.punch.1 > 0
{
record_owed_opening(&mut opening, encoder, timestamps, &mut self.frame_passes);
punch.record(encoder, plan.punch, timestamps, &mut self.frame_passes);
}
}
record_owed_opening(&mut opening, encoder, timestamps, &mut self.frame_passes);
if let Some(punch) = punch_pass.as_ref() {
punch.record(
encoder,
self.scratch.punch,
timestamps,
&mut self.frame_passes,
);
}
for page in live.into_iter().flatten() {
self.targets.release(page);
}
#[cfg(debug_assertions)]
self.check_pass_list(merge, true, opaque_drawn, punch_drawn);
#[cfg(not(debug_assertions))]
let _ = (opaque_drawn, punch_drawn);
}
#[cfg(debug_assertions)]
fn check_pass_list(&self, merge: bool, instances: bool, opaque: bool, punch: bool) {
let mut planned = Vec::new();
self.scratch.plan_passes(
&PassShape {
merge,
instances,
opaque,
punch,
},
&mut planned,
);
debug_assert_eq!(
planned, self.frame_passes,
"the recorded pass list disagrees with the planned one"
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FramePass {
Opening,
Clear,
Opaque,
Surface,
Page,
Filter,
Copy,
Punch,
}
impl FramePass {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
FramePass::Opening => "frust-engine surface opening",
FramePass::Clear => "frust-engine clear",
FramePass::Opaque => "frust-engine opaque strips",
FramePass::Surface => "frust-engine alpha strips",
FramePass::Page => PAGE_LABEL,
FramePass::Filter => FILTER_LABEL,
FramePass::Copy => "frust-engine region copy",
FramePass::Punch => "frust-engine hole punch",
}
}
#[must_use]
pub const fn targets_surface(self) -> bool {
!matches!(self, FramePass::Page | FramePass::Filter | FramePass::Copy)
}
}
#[cfg(any(test, debug_assertions))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PassShape {
merge: bool,
instances: bool,
opaque: bool,
punch: bool,
}
#[derive(Debug)]
struct FramePipelines {
alpha: (EnginePipeline, ClassPipelines),
opaque: Option<ClassPipelines>,
page: Option<ClassPipelines>,
punch: Option<(EnginePipeline, ClassPipelines)>,
filter: Option<wgpu::RenderPipeline>,
copy: Option<wgpu::RenderPipeline>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ClassRouting {
uber: bool,
min_run: u32,
}
impl Default for ClassRouting {
fn default() -> Self {
Self {
uber: false,
min_run: crate::gpu::strips::RUN_SPLIT_MIN,
}
}
}
impl ClassRouting {
fn from_config() -> Self {
Self {
uber: config::uber_program_forced(),
min_run: config::run_split_min(),
}
}
fn composite_class(self) -> PaintClass {
if self.uber {
PaintClass::Uber
} else {
PaintClass::LayerComposite
}
}
fn punch_class(self) -> PaintClass {
if self.uber {
PaintClass::Uber
} else {
PaintClass::SolidCoverage
}
}
}
#[derive(Debug, Default)]
struct ClassPipelines {
by_class: [Option<wgpu::RenderPipeline>; PaintClass::ALL.len()],
}
impl ClassPipelines {
fn get(&self, class: PaintClass) -> Option<&wgpu::RenderPipeline> {
self.by_class.get(class.index()).and_then(Option::as_ref)
}
fn set(&mut self, class: PaintClass, pipeline: wgpu::RenderPipeline) {
if let Some(slot) = self.by_class.get_mut(class.index()) {
*slot = Some(pipeline);
}
}
fn any(&self) -> Option<&wgpu::RenderPipeline> {
self.by_class.iter().flatten().next()
}
}
#[derive(Debug)]
struct ResolvedPipelines {
format: Option<wgpu::TextureFormat>,
entries: [Option<wgpu::RenderPipeline>; RESOLVED_SLOTS],
}
const RESOLVED_SLOTS: usize = EnginePipeline::ALL.len() * PaintClass::ALL.len();
impl Default for ResolvedPipelines {
fn default() -> Self {
Self {
format: None,
entries: [const { None }; RESOLVED_SLOTS],
}
}
}
impl ResolvedPipelines {
const fn slot(state: EnginePipeline, class: PaintClass) -> usize {
state.index() * PaintClass::ALL.len() + class.index()
}
}
struct PassPlan<'a> {
pass: FramePass,
view: &'a wgpu::TextureView,
load: wgpu::LoadOp<wgpu::Color>,
depth: Option<&'a wgpu::TextureView>,
state: EnginePipeline,
pipelines: &'a ClassPipelines,
groups: &'a StripBindGroups,
resources: &'a wgpu::BindGroup,
composites: &'a [wgpu::BindGroup],
external_keys: &'a [u64],
instances: &'a wgpu::Buffer,
base: u32,
segments: &'a [Segment],
composite_class: PaintClass,
}
fn record_pass(
encoder: &mut wgpu::CommandEncoder,
plan: &PassPlan<'_>,
timestamps: Option<wgpu::RenderPassTimestampWrites<'_>>,
log: &mut Vec<FramePass>,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(plan.pass.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: timestamps,
occlusion_query_set: None,
multiview_mask: None,
});
log.push(plan.pass);
draw_segments(&mut pass, plan);
}
fn draw_segments(pass: &mut wgpu::RenderPass<'_>, plan: &PassPlan<'_>) {
pass.set_vertex_buffer(0, plan.instances.slice(..));
let mut composite = 0_usize;
let mut bound: Option<PaintClass> = None;
for segment in plan.segments {
let class = plan.state.fold_class(match *segment {
Segment::Strips(_, _, class) | Segment::External(_, _, _, class) => class,
Segment::Composite(..) | Segment::Backdrop(..) => plan.composite_class,
});
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),
)
}
Segment::Backdrop(first, count, _) => {
let Some(group) = plan.composites.get(composite) else {
continue;
};
composite = composite.saturating_add(1);
if count == 0 {
continue;
}
(
group,
None,
GpuStrip::instance_range(plan.base.saturating_add(first), count),
)
}
};
if bound != Some(class) {
let Some(pipeline) = plan.pipelines.get(class) else {
continue;
};
pass.set_pipeline(pipeline);
bound = Some(class);
}
plan.groups.bind_with(pass, group, images);
pass.draw(GpuStrip::vertex_range(), range);
}
}
struct SurfaceOpening<'a> {
view: &'a wgpu::TextureView,
depth: Option<&'a wgpu::TextureView>,
clear: wgpu::Color,
depth_load: wgpu::LoadOp<f32>,
opaque: Option<PassPlan<'a>>,
}
impl SurfaceOpening<'_> {
fn record(
&self,
encoder: &mut wgpu::CommandEncoder,
round: Option<&PassPlan<'_>>,
timestamps: Option<wgpu::RenderPassTimestampWrites<'_>>,
log: &mut Vec<FramePass>,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(FramePass::Opening.label()),
color_attachments: &[Some(color_attachment(
self.view,
wgpu::LoadOp::Clear(self.clear),
))],
depth_stencil_attachment: self
.depth
.map(|view| depth_attachment(view, self.depth_load)),
timestamp_writes: timestamps,
occlusion_query_set: None,
multiview_mask: None,
});
log.push(FramePass::Opening);
if let Some(opaque) = self.opaque.as_ref() {
draw_segments(&mut pass, opaque);
}
if let Some(round) = round {
draw_segments(&mut pass, round);
}
}
}
fn record_owed_opening(
opening: &mut Option<SurfaceOpening<'_>>,
encoder: &mut wgpu::CommandEncoder,
timestamps: FrameTimestamps<'_>,
log: &mut Vec<FramePass>,
) {
if let Some(opening) = opening.take() {
opening.record(encoder, None, timestamps.writes(EngineSpan::Main), log);
}
}
struct PunchPass<'a> {
view: &'a wgpu::TextureView,
depth: Option<&'a wgpu::TextureView>,
state: EnginePipeline,
pipelines: &'a ClassPipelines,
class: PaintClass,
groups: &'a StripBindGroups,
instances: &'a wgpu::Buffer,
base: u32,
}
impl PunchPass<'_> {
fn record(
&self,
encoder: &mut wgpu::CommandEncoder,
punch: (u32, u32),
timestamps: FrameTimestamps<'_>,
log: &mut Vec<FramePass>,
) {
let (first, count) = punch;
if count == 0 {
return;
}
record_pass(
encoder,
&PassPlan {
pass: FramePass::Punch,
view: self.view,
load: wgpu::LoadOp::Load,
depth: self.depth,
state: self.state,
pipelines: self.pipelines,
groups: self.groups,
resources: &self.groups.resources,
composites: &[],
external_keys: &[],
instances: self.instances,
base: self.base,
segments: &[Segment::Strips(first, count, self.class)],
composite_class: self.class,
},
timestamps.writes(EngineSpan::Main),
log,
);
}
}
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, PaintClass),
External(u32, u32, u32, PaintClass),
Composite(u32, CompositeSource),
Backdrop(u32, u32, PageParity),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CompositeSource {
Page(PageParity),
Retained(u32),
}
#[derive(Debug, Clone, Copy)]
struct RetainedRound {
bracket: u32,
continued: bool,
}
#[derive(Debug, Clone)]
struct RoundPlan {
page: Option<PagePlan>,
retained: Option<RetainedRound>,
segments: Range<usize>,
released: [bool; MAX_LIVE_PAGES],
filter: Option<FilterPlan>,
copy: Option<RectU16>,
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,
copies: u32,
routing: ClassRouting,
opaque_segments: Vec<Segment>,
opaque_classes: ClassSet,
surface_classes: ClassSet,
page_classes: ClassSet,
planning_page: bool,
class_runs: Vec<ClassRun>,
alpha_marks: Vec<(u32, PaintClass)>,
opaque_marks: Vec<(u32, PaintClass)>,
}
impl Scratch {
fn build(
&mut self,
frame: &CompiledFrame,
rounds: &[Round],
depth_active: bool,
punches: bool,
paint_slots: &[Option<ResolvedPaint>],
backdrops: &[Backdrop],
) {
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);
for bracket in &frame.retained {
if let RetainedKind::Draw { depth, .. } = bracket.kind
&& let Some(slot) = self.layer_depth.get_mut(bracket.layer as usize)
{
*slot = depth;
}
}
self.filter_blocks.clear();
self.filter_layers.clear();
self.filter_passes = 0;
self.copies = 0;
self.opaque_segments.clear();
self.alpha_marks.clear();
self.opaque_marks.clear();
self.opaque_classes = ClassSet::EMPTY;
self.surface_classes = ClassSet::EMPTY;
self.page_classes = ClassSet::EMPTY;
let draws = frame.draws();
let strips = frame.strip_buf();
let mut pending: &[ClearPunch] = if punches { &frame.clears } else { &[] };
let opaque_limit = backdrops
.first()
.map_or(u32::MAX, |backdrop| backdrop.depth);
for round in rounds {
let page = round.page();
let retained = round.retained();
let offscreen = page.is_some() || retained.is_some();
if let Some(copy) = round.region_copy() {
if let Some(page) = page {
if !pending.is_empty()
&& let Some(backdrop) = backdrops
.iter()
.find(|backdrop| backdrop.layer == page.layer)
{
let mut first_segment = self.segments.len();
let mut run_start = self.alpha.len() as u32;
let mut run_external = None;
self.cut_for_punches(
&mut pending,
strips,
backdrop.depth,
&mut first_segment,
&mut run_start,
&mut run_external,
);
}
self.copies = self.copies.saturating_add(1);
self.rounds.push(RoundPlan {
page: Some(PagePlan {
parity: page.parity,
size: page.size,
continued: false,
}),
retained: None,
segments: self.segments.len()..self.segments.len(),
released: released_groups(&round.released),
filter: None,
copy: Some(copy.source),
punch: (0, 0),
});
}
continue;
}
let filter = self.plan_filter(frame, round, backdrops);
let window = match (page, retained) {
(Some(page), _) => PageWindow::of(page),
(None, Some(retained)) => PageWindow::of_retained(retained),
(None, None) => PageWindow::ROOT,
};
let split_opaque = !offscreen && depth_active;
self.planning_page = offscreen;
let cuts = !offscreen;
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,
);
}
if let Ok(at) = backdrops
.binary_search_by_key(&draw.depth, |backdrop| backdrop.depth)
{
if let Some(backdrop) = backdrops.get(at)
&& let Some(sample) = round
.samples
.iter()
.find(|sample| sample.layer == backdrop.layer)
&& let Some(run) = strips.get(draw.strip_range.clone())
{
let end = self.alpha.len() as u32;
self.push_run(run_start, end, run_external);
run_external = None;
self.push_backdrop(
backdrop,
sample.parity,
draw.depth,
run,
window,
);
run_start = self.alpha.len() as u32;
}
continue;
}
let Some(paint) = pack_paint(&draw.paint, draw.depth, paint_slots)
else {
continue;
};
if paint.glyph_quad {
let Some(quad) = glyph_quad(&frame.glyph_quads, draw.depth) 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;
}
mark_class(
&mut self.alpha_marks,
self.alpha.len() as u32,
paint.class,
);
if let Some(instance) = window.place_quad(quad, paint) {
self.alpha.push(instance);
}
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 && draw.depth < opaque_limit;
let class = paint.class;
mark_class(&mut self.alpha_marks, self.alpha.len() as u32, class);
if to_opaque {
mark_class(&mut self.opaque_marks, self.opaque.len() as u32, class);
}
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);
let source = match composite.retained {
Some(retained) => CompositeSource::Retained(retained.bracket),
None => CompositeSource::Page(composite.parity),
};
self.segments.push(Segment::Composite(end, source));
self.note_class(self.routing.composite_class());
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);
let own_layer = page
.map(|page| page.layer)
.or(retained.map(|retained| retained.layer));
if let Some(layer) = own_layer
&& let Some(slot) = self.layer_depth.get_mut(layer as usize)
{
*slot = (*slot).max(deepest);
}
let released = released_groups(&round.released);
self.rounds.push(RoundPlan {
page: page.map(|page| PagePlan {
parity: page.parity,
size: page.size,
continued: page.continued,
}),
retained: retained.map(|retained| RetainedRound {
bracket: retained.bracket,
continued: retained.continued,
}),
segments: first_segment..self.segments.len(),
released,
filter,
copy: None,
punch: (0, 0),
});
}
if !pending.is_empty() {
self.punch = self.push_punches(pending, strips);
}
self.class_runs.clear();
merge_class_runs(
marked_runs(&self.opaque_marks, 0, self.opaque.len() as u32),
self.routing.min_run,
self.routing.uber,
&mut self.class_runs,
);
debug_assert!(
runs_draw(&self.class_runs, &self.opaque),
"the opaque marks disagree with the instances they mark"
);
for run in &self.class_runs {
self.opaque_segments
.push(Segment::Strips(run.first, run.count, run.class));
self.opaque_classes.insert(run.class);
}
}
fn note_class(&mut self, class: PaintClass) {
if self.planning_page {
self.page_classes.insert(class);
} else {
self.surface_classes.insert(class);
}
}
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,
retained: None,
segments: *first_segment..self.segments.len(),
released: [false; MAX_LIVE_PAGES],
filter: None,
copy: 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>) {
if end.checked_sub(first).filter(|count| *count > 0).is_none() {
return;
}
let Some(span) = self.alpha.get(first as usize..end as usize) else {
return;
};
self.class_runs.clear();
merge_class_runs(
marked_runs(&self.alpha_marks, first, end),
self.routing.min_run,
self.routing.uber,
&mut self.class_runs,
);
debug_assert!(
runs_draw(&self.class_runs, span),
"the alpha marks disagree with the instances they mark"
);
for index in 0..self.class_runs.len() {
let run = self.class_runs[index];
let at = first.saturating_add(run.first);
self.segments.push(match external {
Some(slot) => Segment::External(at, run.count, slot, run.class),
None => Segment::Strips(at, run.count, run.class),
});
self.note_class(run.class);
}
}
fn push_backdrop(
&mut self,
backdrop: &Backdrop,
parity: PageParity,
depth: u32,
run: &[Strip],
window: PageWindow,
) {
let class = self.routing.composite_class();
let paint = PackedPaint {
payload: PaintPayload::Layer(backdrop.region.x0, backdrop.region.y0),
paint: LAYER_PAINT_SOURCE | u32::from(u8::MAX),
depth_index: depth,
opaque: false,
external: None,
class,
glyph_quad: false,
};
let first = self.alpha.len() as u32;
for pair in run.windows(2) {
self.push_span(&pair[0], &pair[1], paint, false, window);
}
let count = (self.alpha.len() as u32).saturating_sub(first);
if count > 0 {
self.segments.push(Segment::Backdrop(first, count, parity));
self.note_class(class);
}
}
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,
class: PaintClass::SolidCoverage,
glyph_quad: false,
};
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,
backdrops: &[Backdrop],
) -> Option<FilterPlan> {
let pass = round.filter_pass()?;
let page = round.page()?;
let recorded = frame.recorder.layers.get(pass.layer as usize)?;
let data_offset = match backdrops
.iter()
.find(|backdrop| backdrop.layer == pass.layer)
{
Some(backdrop) => self.pack_filter_block(pass.layer, || {
Some(GpuFilterData::from(GpuGaussianBlur::from(&backdrop.blur())))
})?,
None => 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> {
self.pack_filter_block(layer, || {
Some(match served_filter(layer, kind).ok()? {
ServedFilter::Blur(blur) => GpuFilterData::from(GpuGaussianBlur::from(&blur)),
ServedFilter::DropShadow(shadow) => {
GpuFilterData::from(GpuDropShadow::from(&shadow))
}
})
})
}
fn pack_filter_block(
&mut self,
layer: u32,
make: impl FnOnce() -> Option<GpuFilterData>,
) -> Option<u32> {
let index = match self.filter_layers.iter().position(|id| *id == layer) {
Some(index) => index,
None => {
let block = make()?;
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.retained.is_some())
&& plan.filter.is_none()
&& plan.copy.is_none()
})
.count()
}
#[cfg(any(test, debug_assertions))]
fn plan_passes(&self, shape: &PassShape, out: &mut Vec<FramePass>) {
out.clear();
if !shape.merge {
out.push(FramePass::Clear);
}
if !shape.instances {
if shape.merge {
out.push(FramePass::Opening);
}
return;
}
if !shape.merge && shape.opaque {
out.push(FramePass::Opaque);
}
let mut owed = shape.merge;
for plan in &self.rounds {
if plan.copy.is_some() {
if owed {
out.push(FramePass::Opening);
owed = false;
}
out.push(FramePass::Copy);
continue;
}
if plan.filter.is_some() {
out.push(FramePass::Filter);
continue;
}
if plan.page.is_some() || plan.retained.is_some() {
out.push(FramePass::Page);
} else if !plan.segments.is_empty() {
out.push(if owed {
FramePass::Opening
} else {
FramePass::Surface
});
owed = false;
}
if shape.punch && plan.punch.1 > 0 {
if owed {
out.push(FramePass::Opening);
owed = false;
}
out.push(FramePass::Punch);
}
}
if owed {
out.push(FramePass::Opening);
}
if shape.punch && self.punch.1 > 0 {
out.push(FramePass::Punch);
}
}
fn instance_bytes(&self) -> (&[u8], &[u8]) {
(
bytemuck::cast_slice(&self.opaque),
bytemuck::cast_slice(&self.alpha),
)
}
}
fn mark_class(marks: &mut Vec<(u32, PaintClass)>, at: u32, class: PaintClass) {
match marks.last_mut() {
Some(last) if last.1 == class => {}
Some(last) if last.0 == at => {
last.1 = class;
let len = marks.len();
if len >= 2 && marks[len - 2].1 == class {
marks.pop();
}
}
_ => marks.push((at, class)),
}
}
fn marked_runs(
marks: &[(u32, PaintClass)],
first: u32,
end: u32,
) -> impl Iterator<Item = ClassRun> + '_ {
let from = marks
.partition_point(|(at, _)| *at <= first)
.saturating_sub(1);
let tail = marks.get(from..).unwrap_or(&[]);
tail.iter()
.enumerate()
.take_while(move |(_, (at, _))| *at < end)
.filter_map(move |(index, (at, class))| {
let start = (*at).max(first);
let stop = tail.get(index + 1).map_or(end, |next| next.0.min(end));
(stop > start).then(|| ClassRun {
first: start - first,
count: stop - start,
class: *class,
})
})
}
fn runs_draw(runs: &[ClassRun], instances: &[GpuStrip]) -> bool {
let mut next = 0_u32;
for run in runs {
if run.first != next || run.count == 0 {
return false;
}
let Some(span) = instances.get(run.first as usize..(run.first + run.count) as usize) else {
return false;
};
if !span
.iter()
.all(|instance| run.class.admits_flag(instance.paint_and_rect_flag))
{
return false;
}
next += run.count;
}
next as usize == instances.len()
}
#[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 of_retained(target: &RetainedTarget) -> Self {
Self {
x0: target.bounds.x0,
x1: target.bounds.x1,
y0: target.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 place_quad(self, quad: &GlyphQuad, paint: PackedPaint) -> Option<GpuStrip> {
let end = quad.x.saturating_add(quad.width);
if quad.width == 0 || quad.height == 0 || end <= self.x0 || quad.x >= self.x1 {
return None;
}
let cut = self.x0.saturating_sub(quad.x);
let x = quad.x.max(self.x0);
let width = end.min(self.x1).saturating_sub(x);
let instance = GpuStrip::from_rect(
x.saturating_sub(self.x0),
quad.y.saturating_sub(self.y0),
width,
quad.height,
0,
StripDraw {
payload: pack_u16_pair(quad.atlas_x.saturating_add(cut), quad.atlas_y),
paint: paint.paint,
depth_index: paint.depth_index,
},
);
(width > 0).then_some(instance)
}
}
fn glyph_quad(quads: &[GlyphQuad], depth: u32) -> Option<&GlyphQuad> {
let found = quads
.binary_search_by_key(&depth, |quad| quad.depth)
.ok()
.and_then(|index| quads.get(index));
debug_assert!(
found.is_some(),
"a glyph-quad draw with no quad at depth {depth}"
);
found
}
fn retained_roles(frame: &CompiledFrame, plan: &RetainedPlan, roles: &mut RetainedLayers) {
roles.clear();
for (index, (bracket, decision)) in frame.retained.iter().zip(&plan.decisions).enumerate() {
let id = u32::try_from(index).unwrap_or(u32::MAX);
let role = match *decision {
RetainedDecision::Record { .. } => RetainedRole::Record { bracket: id },
RetainedDecision::Draw { dest, origin } => RetainedRole::Draw {
bracket: id,
dest,
origin,
},
RetainedDecision::Hidden => RetainedRole::Hidden,
RetainedDecision::Inline(_) => continue,
};
roles.set(bracket.layer, role);
}
}
fn retained_rendered(frame: &CompiledFrame, rounds: &[Round]) -> Vec<bool> {
let mut rendered = vec![false; frame.retained.len()];
for round in rounds {
if let Some(target) = round.retained()
&& let Some(slot) = rendered.get_mut(target.bracket as usize)
{
*slot = true;
}
}
rendered
}
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,
Layer(u16, u16),
}
#[derive(Debug, Clone, Copy)]
struct PackedPaint {
payload: PaintPayload,
paint: u32,
depth_index: u32,
opaque: bool,
external: Option<u32>,
class: PaintClass,
glyph_quad: bool,
}
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),
PaintPayload::Layer(x0, y0) => {
pack_u16_pair(x.saturating_sub(x0), y.saturating_sub(y0))
}
}
}
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,
class: PaintClass::SolidCoverage,
glyph_quad: false,
}),
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,
class: resolved.class,
glyph_quad: resolved.paint_type == PaintType::Glyph,
})
}
}
}
#[derive(Debug, Clone, Copy)]
struct ResolvedPaint {
paint_type: PaintType,
texel_offset: u32,
opaque: bool,
external: Option<u32>,
class: PaintClass,
}
fn class_of_record(record: &GpuEncodedPaint) -> PaintClass {
match record {
GpuEncodedPaint::Image(image) => PaintClass::of_image(
image.image_params & IMAGE_QUALITY_BITS,
(image.image_params >> IMAGE_SOURCE_KIND_SHIFT) & 1 == 1,
),
_ => PaintClass::of_flag(pack_paint_descriptor(record.paint_type(), 0)),
}
}
const IMAGE_QUALITY_BITS: u32 = 0b11;
const IMAGE_SOURCE_KIND_SHIFT: u32 = 14;
#[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,
atlas_sampler: Option<wgpu::Sampler>,
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, image_sampler: bool) -> 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),
atlas_sampler: image_sampler.then(|| atlas_sampler(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;
let mut glyph_records = frame.glyph_run_paints.iter().copied().peekable();
for (index, paint) in frame.encoded_paints.iter().enumerate() {
let glyph_record = glyph_records.next_if_eq(&index).is_some();
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: if glyph_record {
PaintType::Glyph
} else {
record.paint_type()
},
texel_offset,
opaque: !paint.may_have_transparency(),
external,
class: if glyph_record {
PaintClass::Glyph
} else {
class_of_record(&record)
},
};
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();
}
debug_assert!(
resource_widths_agree(
dim,
self.alphas.width,
self.paints.width,
self.gradients.width
),
"strip resource textures must all be resource_texture_dim ({dim}) wide: \
alphas {}, paints {}, gradients {}",
self.alphas.width,
self.paints.width,
self.gradients.width,
);
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.atlas_sampler.as_ref(),
&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,
atlas_sampler,
..
} = 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, atlas_sampler.as_ref()),
);
}
}
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,
sampler: Option<&wgpu::Sampler>,
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, sampler);
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,
sampler: Option<&wgpu::Sampler>,
) -> wgpu::BindGroup {
let textures = [
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(atlas),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(external),
},
];
let with_sampler;
let entries: &[wgpu::BindGroupEntry<'_>] = match sampler {
Some(sampler) => {
let [atlas, external] = textures;
with_sampler = [
atlas,
external,
wgpu::BindGroupEntry {
binding: ATLAS_SAMPLER_BINDING,
resource: wgpu::BindingResource::Sampler(sampler),
},
];
&with_sampler
}
None => &textures,
};
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frust-engine strip images"),
layout: &pipeline.get_bind_group_layout(1),
entries,
})
}
fn atlas_sampler(device: &wgpu::Device) -> wgpu::Sampler {
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("frust-engine atlas sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
})
}
fn released_groups(released: &[PageParity]) -> [bool; MAX_LIVE_PAGES] {
let mut groups = [false; MAX_LIVE_PAGES];
for parity in released {
if let Some(slot) = groups.get_mut(parity.index()) {
*slot = true;
}
}
groups
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegionCopyMode {
Texture,
Fragment,
Unavailable,
}
impl RegionCopyMode {
#[must_use]
pub fn of(target: &wgpu::Texture) -> Self {
Self::for_usage(target.usage(), target.format(), target.sample_count())
}
#[must_use]
pub fn for_usage(
usage: wgpu::TextureUsages,
format: wgpu::TextureFormat,
sample_count: u32,
) -> Self {
if sample_count != 1 {
Self::Unavailable
} else if usage.contains(wgpu::TextureUsages::TEXTURE_BINDING) {
Self::Fragment
} else if format == INTERMEDIATE_FORMAT && usage.contains(wgpu::TextureUsages::COPY_SRC) {
Self::Texture
} else {
Self::Unavailable
}
}
}
fn note_backdrop_unreadable(target: &wgpu::Texture) {
BACKDROP_UNREADABLE_WARNING.call_once(|| {
log::warn!(
"backdrop blur drawn as nothing: the frame's target ({:?}, {:?}) is neither \
TEXTURE_BINDING nor COPY_SRC in {INTERMEDIATE_FORMAT:?}, so its backdrop cannot be \
read back (logged once)",
target.format(),
target.usage()
);
});
}
#[derive(Debug)]
struct RegionCopyResources {
instances: wgpu::Buffer,
staging: Option<ResourceTexture>,
}
impl RegionCopyResources {
fn new(device: &wgpu::Device) -> Self {
let slots = crate::filters::MAX_BACKDROPS_PER_FRAME as u64;
Self {
instances: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frust-engine region copy instances"),
size: slots.saturating_mul(COPY_INSTANCE_BYTES),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}),
staging: None,
}
}
fn ensure_staging(&mut self, device: &wgpu::Device, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
let (held_width, held_height) = self
.staging
.as_ref()
.map_or((0, 0), |staging| (staging.width, staging.height));
if held_width >= width && held_height >= height {
return;
}
self.staging = Some(ResourceTexture::new(
device,
&wgpu::TextureDescriptor {
label: Some("frust-engine region copy staging"),
size: wgpu::Extent3d {
width: width.max(held_width),
height: height.max(held_height),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: INTERMEDIATE_FORMAT,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
));
}
}
struct RegionCopyPass<'a> {
mode: RegionCopyMode,
target: &'a wgpu::TextureView,
page: &'a PooledTexture,
source: RectU16,
pipeline: Option<&'a wgpu::RenderPipeline>,
resources: Option<&'a RegionCopyResources>,
slot: u32,
}
impl RegionCopyPass<'_> {
fn record(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
timestamps: Option<wgpu::RenderPassTimestampWrites<'_>>,
) {
let width = self.source.width();
let height = self.source.height();
let source = match (self.mode, self.resources) {
(RegionCopyMode::Fragment, Some(_)) => {
Some((self.target, (self.source.x0, self.source.y0)))
}
(RegionCopyMode::Texture, Some(resources)) => match resources.staging.as_ref() {
Some(staging)
if staging.width >= u32::from(width) && staging.height >= u32::from(height) =>
{
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: self.target.texture(),
mip_level: 0,
origin: wgpu::Origin3d {
x: u32::from(self.source.x0),
y: u32::from(self.source.y0),
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
staging.copy_target(),
wgpu::Extent3d {
width: u32::from(width),
height: u32::from(height),
depth_or_array_layers: 1,
},
);
Some((&staging.view, (0, 0)))
}
_ => None,
},
_ => None,
};
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(FramePass::Copy.label()),
color_attachments: &[Some(color_attachment(
self.page.view(),
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
))],
depth_stencil_attachment: None,
timestamp_writes: timestamps,
occlusion_query_set: None,
multiview_mask: None,
});
let (Some((view, origin)), Some(pipeline), Some(resources)) =
(source, self.pipeline, self.resources)
else {
return;
};
let offset = u64::from(self.slot).saturating_mul(COPY_INSTANCE_BYTES);
if offset.saturating_add(COPY_INSTANCE_BYTES) > resources.instances.size() {
return;
}
let (page_width, page_height) = self.page.size();
let instance = [
pack_u16_pair(0, 0),
pack_u16_pair(origin.0, origin.1),
pack_u16_pair(width, height),
pack_u16_pair(
u16::try_from(page_width).unwrap_or(u16::MAX),
u16::try_from(page_height).unwrap_or(u16::MAX),
),
];
queue.write_buffer(
&resources.instances,
offset,
bytemuck::cast_slice(&instance),
);
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frust-engine region copy source"),
layout: &pipeline.get_bind_group_layout(0),
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
}],
});
pass.set_pipeline(pipeline);
pass.set_vertex_buffer(0, resources.instances.slice(..));
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(
0..COPY_QUAD_VERTICES,
self.slot..self.slot.saturating_add(1),
);
}
}
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),
},
],
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RangeOptions {
pub keep_draw_retained: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RangeCut {
pub start: usize,
pub end: usize,
pub reopened: usize,
pub closed: usize,
pub demoted_retained: usize,
pub dropped_draw_retained: usize,
pub dropped_pops: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BracketKind {
Clip,
Layer,
Retained,
}
fn opens(command: &frust_scene::Command) -> Option<BracketKind> {
use frust_scene::Command;
match command {
Command::PushClip { .. }
| Command::PushClipRounded { .. }
| Command::PushClipPath { .. } => Some(BracketKind::Clip),
Command::PushLayer { .. } => Some(BracketKind::Layer),
Command::PushRetained { .. } => Some(BracketKind::Retained),
_ => None,
}
}
fn closes(command: &frust_scene::Command) -> Option<BracketKind> {
use frust_scene::Command;
match command {
Command::PopClip => Some(BracketKind::Clip),
Command::PopLayer => Some(BracketKind::Layer),
Command::PopRetained => Some(BracketKind::Retained),
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
enum OpenBracket {
Plain(BracketKind),
Retained,
Demoted { layer: bool, prefix: Affine },
Absorbed,
}
impl OpenBracket {
fn kind(self) -> BracketKind {
match self {
OpenBracket::Plain(kind) => kind,
OpenBracket::Retained | OpenBracket::Demoted { .. } | OpenBracket::Absorbed => {
BracketKind::Retained
}
}
}
}
fn replay(builder: &mut SceneBuilder<'_>, prefix: Affine, command: &frust_scene::Command) {
use frust_scene::{Command, PathStyle};
fn under(
builder: &mut SceneBuilder<'_>,
transform: Affine,
record: impl FnOnce(&mut SceneBuilder<'_>),
) {
builder.push_transform(transform);
record(builder);
builder.pop_transform();
}
match command {
Command::FillRect {
rect,
brush,
transform,
} => under(builder, prefix * *transform, |b| {
b.fill_rect(*rect, brush.clone())
}),
Command::RoundedRect {
rect,
radii,
brush,
transform,
} => under(builder, prefix * *transform, |b| {
b.fill_rounded_rect_radii(*rect, *radii, brush.clone());
}),
Command::Line {
p0,
p1,
width,
brush,
transform,
} => under(builder, prefix * *transform, |b| {
b.stroke_line(*p0, *p1, *width, brush.clone());
}),
Command::GlyphRun(run) => under(builder, prefix, |b| b.draw_glyph_run(run.clone())),
Command::PushClip { rect, transform } => {
under(builder, prefix * *transform, |b| b.push_clip(*rect));
}
Command::PushClipRounded {
rect,
radii,
transform,
} => under(builder, prefix * *transform, |b| {
b.push_clip_rounded_radii(*rect, *radii);
}),
Command::PushClipPath { path, transform } => {
under(builder, prefix * *transform, |b| {
b.push_clip_path(path.clone())
});
}
Command::PopClip => builder.pop_clip(),
Command::Image {
data,
dest,
transform,
} => under(builder, prefix * *transform, |b| b.draw_image(data, *dest)),
Command::BlurredRoundedRect {
rect,
radii,
std_dev,
color,
transform,
} => under(builder, prefix * *transform, |b| {
b.draw_blurred_rounded_rect_radii(*rect, *radii, *std_dev, *color);
}),
Command::PushLayer {
rect,
alpha,
transform,
} => under(builder, prefix * *transform, |b| {
b.push_layer(*rect, *alpha)
}),
Command::PopLayer => builder.pop_layer(),
Command::ClearRect { rect, transform } => {
under(builder, prefix * *transform, |b| b.clear_rect(*rect));
}
Command::Path {
path,
style,
brush,
transform,
} => under(builder, prefix * *transform, |b| match style {
PathStyle::Fill => b.fill_path(path.clone(), brush.clone()),
PathStyle::Stroke { width, dash: None } => {
b.stroke_path(path.clone(), *width, brush.clone());
}
PathStyle::Stroke {
width,
dash: Some(dash),
} => b.stroke_path_dashed(path.clone(), *width, *dash, brush.clone()),
}),
Command::ShaderQuad {
program,
dest,
transform,
time,
} => under(builder, prefix * *transform, |b| {
b.draw_shader(program, *dest, *time);
}),
Command::PushRetained {
key,
rect,
alpha,
scale,
transform,
} => under(builder, prefix * *transform, |b| {
b.push_retained(*key, *rect, *alpha, *scale);
}),
Command::PopRetained => builder.pop_retained(),
Command::DrawRetained {
key,
rect,
transform,
alpha,
scale,
} => under(builder, prefix * *transform, |b| {
b.draw_retained(*key, *rect, *alpha, *scale);
}),
Command::SceneTexture {
id,
dest,
transform,
} => {
under(builder, prefix * *transform, |b| {
b.scene_texture(*id, *dest)
});
}
Command::BackdropBlur {
rect,
radius,
transform,
} => under(builder, prefix * *transform, |b| {
b.backdrop_blur(*rect, *radius)
}),
}
}
pub fn project_command_range(
scene: &Scene,
range: Range<usize>,
options: RangeOptions,
out: &mut Scene,
) -> RangeCut {
project_command_ranges(scene, std::slice::from_ref(&range), options, out)
}
pub fn project_command_ranges(
scene: &Scene,
ranges: &[Range<usize>],
options: RangeOptions,
out: &mut Scene,
) -> RangeCut {
let commands = scene.commands();
let len = commands.len();
let clamped: Vec<(usize, usize)> = ranges
.iter()
.map(|range| {
let end = range.end.min(len);
(range.start.min(end), end)
})
.collect();
let mut cut = RangeCut {
start: clamped.first().map_or(0, |&(start, _)| start),
end: clamped.last().map_or(0, |&(_, end)| end),
..RangeCut::default()
};
let mut closed_at: Vec<Option<usize>> = vec![None; len];
let mut stack: Vec<(BracketKind, usize)> = Vec::new();
let mut open_at_start: Vec<Vec<usize>> = vec![Vec::new(); clamped.len()];
let snapshot = |index: usize, stack: &[(BracketKind, usize)], open: &mut [Vec<usize>]| {
for (slot, &(start, _)) in open.iter_mut().zip(&clamped) {
if start == index {
slot.extend(stack.iter().map(|&(_, at)| at));
}
}
};
for (index, command) in commands.iter().enumerate() {
snapshot(index, &stack, &mut open_at_start);
if let Some(kind) = opens(command) {
stack.push((kind, index));
} else if let Some(kind) = closes(command)
&& let Some(position) = stack.iter().rposition(|&(open, _)| open == kind)
{
let (_, at) = stack.remove(position);
closed_at[at] = Some(index);
}
}
snapshot(len, &stack, &mut open_at_start);
let spans = |at: usize, start: usize, end: usize| {
at < start || (end < len && closed_at[at].is_none_or(|c| c >= end))
};
out.reset();
let mut builder = SceneBuilder::new(out);
let push = |at: usize,
(start, end): (usize, usize),
builder: &mut SceneBuilder<'_>,
open: &mut Vec<OpenBracket>,
prefix: &mut Affine,
cut: &mut RangeCut| {
let command = &commands[at];
match command {
frust_scene::Command::PushRetained {
rect,
alpha,
scale,
transform,
..
} => {
if open
.iter()
.any(|bracket| matches!(bracket, OpenBracket::Demoted { .. }))
{
open.push(OpenBracket::Absorbed);
} else if spans(at, start, end) {
cut.demoted_retained += 1;
let correction =
crate::compile::layers::retained_correction(*rect, *scale, *transform);
let layer = *alpha < 1.0;
if layer {
let corrected = *prefix * correction * *transform;
builder.push_transform(corrected);
builder.push_layer(*rect, *alpha);
builder.pop_transform();
}
open.push(OpenBracket::Demoted {
layer,
prefix: *prefix,
});
*prefix *= correction;
} else {
replay(builder, *prefix, command);
open.push(OpenBracket::Retained);
}
}
_ => {
if let Some(kind) = opens(command) {
replay(builder, *prefix, command);
open.push(OpenBracket::Plain(kind));
}
}
}
};
let close =
|bracket: OpenBracket, builder: &mut SceneBuilder<'_>, prefix: &mut Affine| match bracket {
OpenBracket::Plain(BracketKind::Clip) => builder.pop_clip(),
OpenBracket::Plain(BracketKind::Layer) => builder.pop_layer(),
OpenBracket::Plain(BracketKind::Retained) | OpenBracket::Retained => {
builder.pop_retained();
}
OpenBracket::Demoted {
layer,
prefix: before,
} => {
if layer {
builder.pop_layer();
}
*prefix = before;
}
OpenBracket::Absorbed => {}
};
let mut open: Vec<OpenBracket> = Vec::new();
for (&bounds, reopen) in clamped.iter().zip(&open_at_start) {
let (start, end) = bounds;
let mut prefix = Affine::IDENTITY;
for &at in reopen {
cut.reopened += 1;
push(at, bounds, &mut builder, &mut open, &mut prefix, &mut cut);
}
for (index, command) in commands.iter().enumerate().take(end).skip(start) {
if opens(command).is_some() {
push(
index,
bounds,
&mut builder,
&mut open,
&mut prefix,
&mut cut,
);
} else if let Some(kind) = closes(command) {
match open.iter().rposition(|bracket| bracket.kind() == kind) {
Some(position) => {
let bracket = open.remove(position);
close(bracket, &mut builder, &mut prefix);
}
None => cut.dropped_pops += 1,
}
} else if matches!(command, frust_scene::Command::DrawRetained { .. })
&& !options.keep_draw_retained
{
cut.dropped_draw_retained += 1;
} else {
replay(&mut builder, prefix, command);
}
}
while let Some(bracket) = open.pop() {
cut.closed += 1;
close(bracket, &mut builder, &mut prefix);
}
}
cut
}
pub fn open_retained_bracket(commands: &[frust_scene::Command], index: usize) -> Option<usize> {
let mut open: Vec<usize> = Vec::new();
for (at, command) in commands.iter().enumerate().take(index) {
match command {
frust_scene::Command::PushRetained { .. } => open.push(at),
frust_scene::Command::PopRetained => {
open.pop();
}
_ => {}
}
}
open.first().copied()
}
#[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_line_sums_events_and_takes_the_median_of_levels() {
let mut trace = EncodeTrace::default();
let base = ENCODE_TRACE_COLUMNS.len();
for frame in 0..3_u32 {
let mut row = [0_u32; ENCODE_TRACE_ROW];
row[base] = 10 + frame; row[base + 5] = 1; row[base + 6] = 2; row[base + 8] = frame; row[base + 10] = 4 + frame; trace.window.push(row);
}
let line = trace.line();
let tail = line.split(" total_p95_us=").nth(1).unwrap();
let counts = tail.split_once(' ').unwrap().1;
assert_eq!(
counts,
"draws=11 strips=0 alphas=0 glyph_draws=0 atlas_glyphs=0 replays=3 \
dirty_pages=6 atlas_refused=0 glyph_misses=3 glyph_evicted=0 run_keys=5"
);
}
#[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_lowered_record_names_its_strip_program_class() {
use crate::gpu::atlas::pack_image_params;
use crate::gpu::paint_texture::GpuEncodedImage;
let image = |quality: u32, external: bool| {
GpuEncodedPaint::Image(GpuEncodedImage {
image_params: pack_image_params(quality, 0, 0, 3, external),
image_size: 0,
image_offset: 0,
transform: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
tint: 0,
tint_mode: 0,
image_padding: 0,
})
};
assert_eq!(class_of_record(&image(0, false)), PaintClass::Image);
assert_eq!(class_of_record(&image(1, false)), PaintClass::Image);
assert_eq!(class_of_record(&image(2, false)), PaintClass::ImageFiltered);
assert_eq!(class_of_record(&image(1, true)), PaintClass::ImageFiltered);
}
#[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,
class: PaintClass::Gradient,
})];
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, _, _)
| Segment::Backdrop(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::Backdrop(first, _, _)
| Segment::Composite(first, _) => first >= punch_first + punch_count,
}),
"and everything recorded after it lands on top of the erase"
);
}
const PLAN_SOFT: Rect = Rect::new(8.5, 8.5, 24.5, 40.5);
fn pass_lists(scratch: &Scratch) -> (Vec<FramePass>, Vec<FramePass>) {
let shape = |merge| PassShape {
merge,
instances: true,
opaque: !scratch.opaque.is_empty(),
punch: scratch.punches(),
};
let mut merged = Vec::new();
scratch.plan_passes(&shape(true), &mut merged);
let mut apart = Vec::new();
scratch.plan_passes(&shape(false), &mut apart);
assert_opened_once(&merged);
assert!(
!apart.contains(&FramePass::Opening),
"an unmerged frame records no opening: {apart:?}"
);
(merged, apart)
}
fn assert_opened_once(passes: &[FramePass]) {
let openings = passes
.iter()
.filter(|pass| **pass == FramePass::Opening)
.count();
assert_eq!(openings, 1, "one opening per frame: {passes:?}");
let first_surface = passes.iter().position(|pass| pass.targets_surface());
assert_eq!(
first_surface,
passes.iter().position(|pass| *pass == FramePass::Opening),
"the opening is the first pass over the surface: {passes:?}"
);
assert!(
!passes.contains(&FramePass::Clear) && !passes.contains(&FramePass::Opaque),
"and the clear and opaque strips live inside it: {passes:?}"
);
}
#[test]
fn an_empty_frame_is_the_opening_alone() {
let (scratch, _) = plan_of(&plan_scene(|_| {}));
let (merged, apart) = pass_lists(&scratch);
assert_eq!(merged, [FramePass::Opening]);
assert_eq!(apart, [FramePass::Clear]);
for merge in [true, false] {
let mut passes = Vec::new();
scratch.plan_passes(
&PassShape {
merge,
instances: false,
opaque: false,
punch: false,
},
&mut passes,
);
assert_eq!(passes.len(), 1, "one clean clear: {passes:?}");
}
}
#[test]
fn a_surface_only_frame_folds_its_clear_and_opaque_strips_into_one_pass() {
let (scratch, _) = plan_of(&plan_scene(|builder| {
plan_backdrop(builder);
builder.fill_rect(PLAN_SOFT, Brush::Solid(BLUE));
}));
assert!(!scratch.opaque.is_empty(), "the backdrop is opaque strips");
let (merged, apart) = pass_lists(&scratch);
assert_eq!(merged, [FramePass::Opening]);
assert_eq!(
apart,
[FramePass::Clear, FramePass::Opaque, FramePass::Surface]
);
}
#[test]
fn without_depth_the_opening_is_the_clear_and_the_surface_round() {
let scene = plan_scene(|builder| {
plan_backdrop(builder);
builder.fill_rect(PLAN_SOFT, Brush::Solid(BLUE));
});
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, false, false, &[], &[]);
assert!(scratch.opaque.is_empty(), "no depth, no opaque strips");
let (merged, apart) = pass_lists(&scratch);
assert_eq!(merged, [FramePass::Opening]);
assert_eq!(apart, [FramePass::Clear, FramePass::Surface]);
}
#[test]
fn a_page_round_waits_for_the_opening_rather_than_splitting_it() {
let (scratch, _) = plan_of(&plan_scene(|builder| {
plan_backdrop(builder);
builder.push_layer(PLAN_CHIP, 0.5);
builder.fill_rect(PLAN_CHIP, Brush::Solid(BLUE));
builder.pop_layer();
}));
let (merged, apart) = pass_lists(&scratch);
assert_eq!(merged, [FramePass::Page, FramePass::Opening]);
assert_eq!(
apart,
[
FramePass::Clear,
FramePass::Opaque,
FramePass::Page,
FramePass::Surface
]
);
}
#[test]
fn a_punch_cut_ends_the_opening_where_the_first_surface_pass_ended() {
let (scratch, _) = plan_of(&plan_scene(|builder| {
plan_backdrop(builder);
builder.fill_rect(PLAN_SOFT, Brush::Solid(BLUE));
builder.clear_rect(PLAN_SLOT);
builder.fill_rect(PLAN_CHIP, Brush::Solid(BLUE.with_alpha(0.5)));
}));
let (merged, apart) = pass_lists(&scratch);
assert_eq!(
merged,
[FramePass::Opening, FramePass::Punch, FramePass::Surface]
);
assert_eq!(
apart,
[
FramePass::Clear,
FramePass::Opaque,
FramePass::Surface,
FramePass::Punch,
FramePass::Surface
]
);
}
#[test]
fn a_punch_ahead_of_every_surface_draw_takes_the_opening_alone() {
let (scratch, _) = plan_of(&plan_scene(|builder| {
builder.clear_rect(PLAN_SLOT);
plan_backdrop(builder);
builder.fill_rect(PLAN_CHIP, Brush::Solid(BLUE.with_alpha(0.5)));
}));
let (merged, apart) = pass_lists(&scratch);
assert_eq!(
merged,
[FramePass::Opening, FramePass::Punch, FramePass::Surface]
);
assert_eq!(
apart,
[
FramePass::Clear,
FramePass::Opaque,
FramePass::Punch,
FramePass::Surface
]
);
}
#[test]
fn a_punch_recorded_last_follows_the_opening() {
let (scratch, _) = plan_of(&plan_scene(|builder| {
plan_backdrop(builder);
builder.fill_rect(PLAN_SOFT, Brush::Solid(BLUE));
builder.clear_rect(PLAN_SLOT);
}));
let (merged, apart) = pass_lists(&scratch);
assert_eq!(merged, [FramePass::Opening, FramePass::Punch]);
assert_eq!(
apart,
[
FramePass::Clear,
FramePass::Opaque,
FramePass::Surface,
FramePass::Punch
]
);
}
#[test]
fn a_filter_frame_records_its_filter_passes_before_the_opening() {
use crate::filters::{LayerFilter, push_filter_layer};
use vello_common::peniko::BlendMode;
use vello_common::record::{CommandRecorder, LayerProps};
let mut frame = SceneCompiler::new(PLAN_VIEWPORT.0, PLAN_VIEWPORT.1)
.compile(
&plan_scene(|builder| {
plan_backdrop(builder);
builder.fill_rect(PLAN_CHIP, Brush::Solid(BLUE));
}),
Affine::IDENTITY,
PLAN_VIEWPORT,
)
.expect("an in-range scene compiles");
let draws = frame.recorder.draws.clone();
assert_eq!(draws.len(), 2, "the backdrop and the chip");
let mut recorder = CommandRecorder::new(PLAN_VIEWPORT.0, PLAN_VIEWPORT.1);
let strips_of = |draw: &crate::EngineDraw| &frame.strip_buf()[draw.strip_range.clone()];
recorder.push_draw(draws[0].clone(), strips_of(&draws[0]));
push_filter_layer(
&mut recorder,
LayerProps {
blend_mode: BlendMode::default(),
opacity: 1.0,
mask: None,
clip_path: None,
},
LayerFilter::Blur { sigma: 2.0 },
Affine::IDENTITY,
)
.expect("a finite σ under the ceiling records");
recorder.push_draw(draws[1].clone(), strips_of(&draws[1]));
recorder.pop_layer();
frame.recorder = recorder;
let rounds = Schedule::build(
&frame.recorder,
&TierCaps::fake(DownlevelProfile::Full),
&PageConfig::default(),
)
.expect("a blurred layer under the surface schedules");
let mut scratch = Scratch::default();
scratch.build(&frame, &rounds, true, false, &[], &[]);
assert!(scratch.filter_passes > 0, "the layer is filtered");
let (merged, apart) = pass_lists(&scratch);
let filters = |passes: &[FramePass]| {
passes
.iter()
.filter(|pass| **pass == FramePass::Filter)
.count()
};
assert_eq!(filters(&merged), scratch.filter_passes as usize);
assert_eq!(filters(&apart), filters(&merged));
assert_eq!(
merged.len() + 2,
apart.len(),
"the clear and the opaque strips fold into the surface round: \
{merged:?} against {apart:?}"
);
assert_eq!(merged.first(), Some(&FramePass::Page));
assert_eq!(
merged.last(),
Some(&FramePass::Opening),
"every filter pass runs before the surface opens: {merged:?}"
);
}
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, _, _)
| Segment::Backdrop(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"
);
}
const PLAN_PANEL: Rect = Rect::new(8.0, 8.0, 40.0, 36.0);
fn backdrop_plan_of(scene: &Scene, punches: bool) -> (Scratch, Vec<Round>, CompiledFrame) {
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(
&frame.recorder,
&TierCaps::fake(DownlevelProfile::Full),
&PageConfig::default(),
&RetainedLayers::new(),
&frame.backdrops,
)
.expect("a backdrop under the surface schedules");
let mut scratch = Scratch::default();
scratch.build(&frame, &rounds, true, punches, &[], &frame.backdrops);
(scratch, rounds, frame)
}
fn backdrop_scene() -> Scene {
plan_scene(|builder| {
builder.fill_rect(
Rect::new(
0.0,
0.0,
f64::from(PLAN_VIEWPORT.0),
f64::from(PLAN_VIEWPORT.1),
),
Brush::Solid(BLUE),
);
builder.backdrop_blur(PLAN_PANEL, 2.0);
builder.fill_rect(Rect::new(12.0, 12.0, 20.0, 20.0), Brush::Solid(RED));
})
}
#[test]
fn a_backdrop_ends_the_opening_before_its_copy_and_draws_after_its_passes() {
let (scratch, _, frame) = backdrop_plan_of(&backdrop_scene(), false);
assert_eq!(frame.backdrops.len(), 1);
let (merged, apart) = pass_lists(&scratch);
assert_eq!(
merged,
[
FramePass::Opening,
FramePass::Copy,
FramePass::Filter,
FramePass::Filter,
FramePass::Surface,
],
"the opening — clear, opaque strips and the field — lands before the copy reads \
the surface back, and the panel and chip draw after the blur"
);
assert_eq!(
apart,
[
FramePass::Clear,
FramePass::Opaque,
FramePass::Surface,
FramePass::Copy,
FramePass::Filter,
FramePass::Filter,
FramePass::Surface,
],
"unmerged, the same order with the clear and the opaque strips apart"
);
}
#[test]
fn a_backdrops_rectangle_samples_the_page_its_round_releases() {
let (scratch, rounds, frame) = backdrop_plan_of(&backdrop_scene(), false);
let backdrop = frame.backdrops[0];
let sampling = rounds
.iter()
.position(|round| !round.samples.is_empty())
.expect("one surface round samples the blurred page");
let sample = rounds[sampling].samples[0];
assert_eq!(sample.layer, backdrop.layer);
assert!(
rounds[sampling].released.contains(&sample.parity),
"the round that samples the page hands it back"
);
let plan = &scratch.rounds[sampling];
let segments = &scratch.segments[plan.segments.clone()];
let (first, count, parity) = segments
.iter()
.find_map(|segment| match *segment {
Segment::Backdrop(first, count, parity) => Some((first, count, parity)),
_ => None,
})
.expect("the panel is drawn as a backdrop segment");
assert_eq!(parity, sample.parity);
assert!(count > 0);
for instance in &scratch.alpha[first as usize..(first + count) as usize] {
assert_eq!(
instance.paint_and_rect_flag & !crate::gpu::strips::RECT_STRIP_FLAG,
LAYER_PAINT_SOURCE | u32::from(u8::MAX),
"every panel instance reads the layer input at full opacity"
);
let (page_x, page_y) = (instance.payload & 0xffff, instance.payload >> 16);
assert_eq!(
(page_x, page_y),
(
u32::from(instance.x - backdrop.region.x0),
u32::from(instance.y - backdrop.region.y0)
),
"and samples the page texel under its own device origin"
);
}
}
#[test]
fn draws_after_a_backdrop_never_take_the_depth_writing_pass() {
let after = plan_scene(|builder| {
builder.backdrop_blur(PLAN_PANEL, 2.0);
builder.fill_rect(
Rect::new(
0.0,
0.0,
f64::from(PLAN_VIEWPORT.0),
f64::from(PLAN_VIEWPORT.1),
),
Brush::Solid(BLUE),
);
});
let (scratch, _, _) = backdrop_plan_of(&after, false);
assert!(
scratch.opaque.is_empty(),
"nothing at or past the backdrop's depth is routed opaque"
);
let (before, _, _) = backdrop_plan_of(&backdrop_scene(), false);
assert!(!before.opaque.is_empty());
}
#[test]
fn a_punch_ahead_of_a_backdrop_is_erased_before_the_copy() {
let scene = plan_scene(|builder| {
builder.fill_rect(PLAN_SLOT, Brush::Solid(BLUE));
builder.clear_rect(PLAN_SLOT);
builder.backdrop_blur(PLAN_PANEL, 2.0);
});
let (scratch, _, _) = backdrop_plan_of(&scene, true);
let (merged, _) = pass_lists(&scratch);
let punch = merged
.iter()
.position(|pass| *pass == FramePass::Punch)
.expect("the punch is recorded");
let copy = merged
.iter()
.position(|pass| *pass == FramePass::Copy)
.expect("the copy is recorded");
assert!(
punch < copy,
"the backdrop reads the erased surface, not the field the punch erases: {merged:?}"
);
}
#[test]
fn a_target_that_cannot_be_read_back_serves_no_backdrop() {
let (scratch, rounds) = plan_of(&backdrop_scene());
assert!(
rounds
.iter()
.all(|round| round.copy.is_none() && round.samples.is_empty())
);
let (merged, _) = pass_lists(&scratch);
assert_eq!(merged, [FramePass::Opening]);
assert_eq!(scratch.copies, 0);
}
#[test]
fn the_region_copy_mode_prefers_sampling_and_needs_a_single_sample() {
use wgpu::TextureFormat::{Bgra8Unorm, Rgba8Unorm, Rgba8UnormSrgb};
use wgpu::TextureUsages as U;
let attach = U::RENDER_ATTACHMENT;
assert_eq!(
RegionCopyMode::for_usage(attach | U::COPY_SRC, Rgba8Unorm, 1),
RegionCopyMode::Texture,
"a headless readback target in the pages' own format is copied"
);
assert_eq!(
RegionCopyMode::for_usage(attach | U::COPY_SRC | U::TEXTURE_BINDING, Rgba8Unorm, 1),
RegionCopyMode::Fragment,
"sampling costs the one pass alone, so it wins whenever it is allowed"
);
assert_eq!(
RegionCopyMode::for_usage(attach | U::TEXTURE_BINDING, Bgra8Unorm, 1),
RegionCopyMode::Fragment,
"any format can be sampled across into a page"
);
assert_eq!(
RegionCopyMode::for_usage(attach | U::COPY_SRC, Rgba8UnormSrgb, 1),
RegionCopyMode::Unavailable,
"a copy would move encoded bytes into a linear page"
);
assert_eq!(
RegionCopyMode::for_usage(attach, Bgra8Unorm, 1),
RegionCopyMode::Unavailable,
"a swapchain is RENDER_ATTACHMENT only"
);
assert_eq!(
RegionCopyMode::for_usage(attach | U::TEXTURE_BINDING | U::COPY_SRC, Rgba8Unorm, 4),
RegionCopyMode::Unavailable,
"a multisampled target is neither"
);
}
}