use std::any::TypeId;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::num::NonZeroU64;
#[cfg(feature = "winit-surface")]
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::{Arc, Weak};
use repose_core::color::{ChromaSiting, ColorInfo, PixelFormat};
use repose_core::{
Brush, FontStyle, ImageAlignment, ImageFilter, ImageFit, ImagePaintStyle, ImageSourceRect,
Scene, SceneNode, StrokeCap, TextPaintStyle, Transform, Vec2,
};
#[cfg(feature = "winit-surface")]
use repose_core::{GlyphRasterConfig, PresentModePref, RenderBackend, request_frame};
#[cfg(feature = "winit-surface")]
use wgpu::Instance;
mod slug;
fn align_up(value: u64, alignment: u64) -> anyhow::Result<u64> {
if !alignment.is_power_of_two() {
anyhow::bail!("alignment must be a power of two");
}
value
.checked_add(alignment - 1)
.map(|v| v & !(alignment - 1))
.ok_or_else(|| anyhow::anyhow!("alignment overflow"))
}
mod commands;
pub use commands::apply_render_commands;
pub mod offscreen;
mod callback;
pub use callback::{
Callback, CallbackRenderPass, CallbackResources, ScreenDescriptor, WgpuCallback,
};
mod depth_composite;
pub use depth_composite::{BlitRenderPass, DepthComposite};
#[derive(Clone)]
struct UploadRing {
buf: wgpu::Buffer,
cap: u64,
head: u64,
usage: wgpu::BufferUsages,
}
impl UploadRing {
fn new(device: &wgpu::Device, label: &str, cap: u64, usage: wgpu::BufferUsages) -> Self {
let max = device.limits().max_buffer_size;
let cap = cap.max(4).min(max).min(MAX_UPLOAD_RING_BYTES).max(4);
let buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: cap,
usage,
mapped_at_creation: false,
});
Self {
buf,
cap,
head: 0,
usage,
}
}
fn reset(&mut self) {
self.head = 0;
}
fn grow_to_fit(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
needed: u64,
) -> anyhow::Result<()> {
if needed == 0 {
return Ok(());
}
let start = align_up(self.head, 4)?;
let end = start
.checked_add(needed)
.ok_or_else(|| anyhow::anyhow!("upload ring offset overflow"))?;
if end <= self.cap {
return Ok(());
}
let required = align_up(end, 4)?;
let doubled = self.cap.checked_mul(2).unwrap_or(0);
let new_cap = required.max(doubled).max(256);
let new_cap = align_up(new_cap, 4)?;
if new_cap > device.limits().max_buffer_size || new_cap > MAX_UPLOAD_RING_BYTES {
anyhow::bail!("upload ring exceeds resource budget");
}
if !self.usage.contains(wgpu::BufferUsages::COPY_SRC) {
anyhow::bail!("upload ring growth requires COPY_SRC");
}
let old = self.buf.clone();
self.buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("upload ring (grown)"),
size: new_cap,
usage: self.usage,
mapped_at_creation: false,
});
self.cap = new_cap;
if self.head > 0 {
let copy_size = align_up(self.head, 4)?;
if copy_size > old.size() {
anyhow::bail!("upload ring old data exceeds old buffer");
}
encoder.copy_buffer_to_buffer(&old, 0, &self.buf, 0, copy_size);
}
Ok(())
}
fn alloc_write(&mut self, queue: &wgpu::Queue, bytes: &[u8]) -> anyhow::Result<u64> {
let len = u64::try_from(bytes.len())
.map_err(|_| anyhow::anyhow!("upload byte length exceeds u64"))?;
let start = align_up(self.head, 4)?;
let end = start
.checked_add(len)
.ok_or_else(|| anyhow::anyhow!("upload ring offset overflow"))?;
if end > self.cap {
anyhow::bail!(
"upload ring overflow: start={start} len={len} cap={}",
self.cap
);
}
queue.write_buffer(&self.buf, start, bytes);
self.head = end;
Ok(start)
}
}
struct InstancedPipe<I: bytemuck::Pod> {
ring: UploadRing,
_marker: std::marker::PhantomData<I>,
}
impl<I: bytemuck::Pod> InstancedPipe<I> {
fn new(ring: UploadRing) -> Self {
Self {
ring,
_marker: std::marker::PhantomData,
}
}
fn upload(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
data: &[I],
) -> Option<(u64, u32)> {
if data.is_empty() {
return None;
}
let count = u32::try_from(data.len()).ok()?;
let bytes = bytemuck::cast_slice(data);
let byte_len = u64::try_from(bytes.len()).ok()?;
if byte_len == 0 || byte_len > device.limits().max_buffer_size {
return None;
}
self.ring
.grow_to_fit(device, encoder, byte_len)
.map_err(|error| log::error!("{error:#}"))
.ok()?;
let off = self
.ring
.alloc_write(queue, bytes)
.map_err(|error| log::error!("{error:#}"))
.ok()?;
Some((off, count))
}
fn reset(&mut self) {
self.ring.reset();
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Globals {
ndc_to_px: [f32; 2],
_pad: [f32; 2],
}
fn make_globals(target_w: f32, target_h: f32) -> Globals {
Globals {
ndc_to_px: [target_w * 0.5, target_h * 0.5],
_pad: [0.0, 0.0],
}
}
pub struct WgpuSceneRenderer {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub output_format: wgpu::TextureFormat,
pub output_width: u32,
pub output_height: u32,
pub pixels_per_point: f32,
surface_pipes: Pipelines,
working_space_pipes: Pipelines,
layer_pipes: Pipelines,
working_space_layer_pipes: Pipelines,
rects: InstancedPipe<RectInstance>,
borders: InstancedPipe<BorderInstance>,
ellipses: InstancedPipe<EllipseInstance>,
ellipse_borders: InstancedPipe<EllipseBorderInstance>,
arcs: InstancedPipe<ArcInstance>,
glyph_mask: InstancedPipe<GlyphInstance>,
glyph_color: InstancedPipe<GlyphInstance>,
image_bind_layout_rgba: wgpu::BindGroupLayout,
image_bind_layout_nv12: wgpu::BindGroupLayout,
image_sampler: wgpu::Sampler,
image_sampler_nearest: wgpu::Sampler,
image_sampler_repeat: wgpu::Sampler,
image_sampler_repeat_nearest: wgpu::Sampler,
layer_sampler: wgpu::Sampler,
layer_sampler_linear: wgpu::Sampler,
blur_ring: UploadRing,
text_bind_layout: wgpu::BindGroupLayout,
clip_ring: UploadRing,
projective_ring: UploadRing,
blend_ring: UploadRing,
slug_enabled: bool,
slug_ring: UploadRing,
slug_cache: slug::GlyphSlugCache,
slug_draw_cache: HashMap<SlugDrawKey, SlugDrawCacheEntry>,
slug_draw_cache_bytes_total: u64,
nv12: InstancedPipe<Nv12Instance>,
mesh_cache: HashMap<MeshCacheKey, MeshCacheEntry>,
mesh_cache_bytes_total: u64,
mesh_uniform_buf: wgpu::Buffer,
mesh_bind: wgpu::BindGroup,
mesh_uniform_head: u64,
mesh_uniform_alignment: u64,
mesh_uniform_cap: u64,
flatten_layer_ids: Vec<u32>,
blend_snapshots: std::collections::HashMap<u32, BlendSnapshot>,
blend_snapshot_pool: HashMap<BlendSnapshotKey, BlendSnapshot>,
blend_copies: Vec<BlendCopy>,
msaa_samples: u32,
working_space_msaa_samples: u32,
depth_stencil_tex: wgpu::Texture,
depth_stencil_view: wgpu::TextureView,
msaa_tex: Option<wgpu::Texture>,
msaa_view: Option<wgpu::TextureView>,
ws_msaa_tex: Option<wgpu::Texture>,
ws_msaa_view: Option<wgpu::TextureView>,
surface_resolve_tex: Option<wgpu::Texture>,
surface_resolve_view: Option<wgpu::TextureView>,
surface_resolve_bytes: u64,
msaa_bytes: u64,
ws_msaa_bytes: u64,
depth_stencil_bytes: u64,
working_space_bytes: u64,
gpu_budget_bytes: u64,
globals_buf: wgpu::Buffer,
globals_bind: wgpu::BindGroup,
globals_staging: wgpu::Buffer,
globals_staging_cap: u64,
atlas_mask: AtlasA8,
atlas_color: AtlasRGBA,
atlas_mask_bind: wgpu::BindGroup,
atlas_color_bind: wgpu::BindGroup,
atlas_generation: u64,
atlas_mask_pending: AtlasPending,
atlas_color_pending: AtlasPending,
atlas_last_compact_frame: u64,
atlas_mask_failures: HashSet<repose_text::CacheKey>,
atlas_color_failures: HashSet<repose_text::CacheKey>,
atlas_mask_deferred: HashSet<repose_text::CacheKey>,
atlas_color_deferred: HashSet<repose_text::CacheKey>,
glyph_outline_cache: HashMap<repose_text::CacheKey, GlyphOutlineEntry>,
legacy_shaped_text_cache: HashMap<TextShapeKey, TextShapeCacheEntry>,
next_image_handle: u64,
images: HashMap<u64, ImageTex>,
retained: HashMap<u64, RetainedImage>,
retained_bytes_total: u64,
next_coverage_handle: u64,
coverages: HashMap<u64, CoverageTex>,
frame_index: u64,
cache_touch_clock: u64,
image_bytes_total: u64,
image_evict_after_frames: u64,
image_budget_bytes: u64,
layer_pool: HashMap<u32, LayerTarget>,
producer_layer_ids: HashSet<u32>,
layer_bytes_total: u64,
transient_layer_bytes_total: u64,
working_space: bool,
ws_tex: Option<wgpu::Texture>,
ws_view: Option<wgpu::TextureView>,
ws_bind: Option<wgpu::BindGroup>,
display_pipeline: Option<wgpu::RenderPipeline>,
display_layout: Option<wgpu::BindGroupLayout>,
pub callback_resources: CallbackResources,
callback_scoped_resources: HashMap<CallbackScopeKey, CallbackResources>,
callback_scope_uses: HashMap<CallbackScopeKey, CallbackScopeUse>,
callback_scope_payloads: HashMap<CallbackScopeKey, Weak<Callback>>,
callback_scope_clock: u64,
blend_snapshot_bytes_total: u64,
frame_active: bool,
last_render_error: Option<String>,
}
pub struct WgpuSurfaceBackend {
#[cfg(feature = "winit-surface")]
instance: Option<wgpu::Instance>,
#[cfg(feature = "winit-surface")]
window: Option<Arc<winit::window::Window>>,
pub surface: Option<wgpu::Surface<'static>>,
pub surface_config: Option<wgpu::SurfaceConfiguration>,
#[cfg_attr(not(feature = "winit-surface"), allow(dead_code))]
pending_reconfigure: bool,
pub renderer: WgpuSceneRenderer,
}
impl std::ops::Deref for WgpuSurfaceBackend {
type Target = WgpuSceneRenderer;
fn deref(&self) -> &Self::Target {
&self.renderer
}
}
impl std::ops::DerefMut for WgpuSurfaceBackend {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.renderer
}
}
#[cfg(feature = "winit-surface")]
pub type WgpuBackend = WgpuSurfaceBackend;
impl Drop for WgpuSceneRenderer {
fn drop(&mut self) {
let _ = self.device.poll(wgpu::PollType::Poll);
#[cfg(not(target_arch = "wasm32"))]
{
let _ = self.device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(std::time::Duration::from_millis(100)),
});
}
}
}
#[derive(Clone)]
struct LayerTarget {
texture: wgpu::Texture,
view: wgpu::TextureView,
bind: wgpu::BindGroup,
bind_linear: wgpu::BindGroup,
depth_stencil_view: wgpu::TextureView,
width: u32,
height: u32,
format: wgpu::TextureFormat,
bytes: u64,
rect_px: (f32, f32, f32, f32),
transient: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct BlendSnapshotKey {
format: wgpu::TextureFormat,
width: u32,
height: u32,
}
#[derive(Clone)]
struct BlendSnapshot {
key: BlendSnapshotKey,
texture: wgpu::Texture,
bind: wgpu::BindGroup,
bytes: u64,
}
struct BlendCopy {
pass_id: u64,
blend_id: u32,
target: PassTarget,
region: repose_core::Rect,
}
#[derive(Clone)]
enum ActiveClip {
Rect {
off: u64,
cnt: u32,
rect: repose_core::Rect,
radii: [f32; 4],
scissor: (u32, u32, u32, u32),
difference: bool,
applied: bool,
blocked: bool,
},
Vector {
mesh: Option<MeshGpu>,
source: Arc<repose_core::VectorMeshData>,
uoff: u64,
affine: [f32; 6],
scissor: (u32, u32, u32, u32),
difference: bool,
applied: bool,
blocked: bool,
},
}
impl ActiveClip {
fn difference(&self) -> bool {
match self {
Self::Rect { difference, .. } | Self::Vector { difference, .. } => *difference,
}
}
fn blocked(&self) -> bool {
match self {
Self::Rect { blocked, .. } | Self::Vector { blocked, .. } => *blocked,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum PassTarget {
Surface,
Layer(u32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct CallbackIdentity {
type_id: TypeId,
value: u64,
reusable: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct CallbackScopeKey {
callback: CallbackIdentity,
target: PassTarget,
width: u32,
height: u32,
target_format: wgpu::TextureFormat,
sample_count: u32,
pixels_per_point_bits: u32,
}
#[derive(Clone, Copy)]
struct CallbackScopeUse {
tick: u64,
frame: u64,
}
fn callback_identity(callback: &Callback, fallback: usize) -> CallbackIdentity {
callback
.0
.resource_key()
.map(|key| {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
CallbackIdentity {
type_id: callback.0.resource_type_id(),
value: hasher.finish(),
reusable: true,
}
})
.unwrap_or(CallbackIdentity {
type_id: TypeId::of::<Callback>(),
value: fallback as u64,
reusable: false,
})
}
fn callback_scope_key(
callback: &Callback,
fallback: usize,
target: PassTarget,
descriptor: &ScreenDescriptor,
) -> CallbackScopeKey {
CallbackScopeKey {
callback: callback_identity(callback, fallback),
target,
width: descriptor.size_in_pixels[0],
height: descriptor.size_in_pixels[1],
target_format: descriptor.target_format,
sample_count: descriptor.sample_count,
pixels_per_point_bits: descriptor.pixels_per_point.to_bits(),
}
}
#[derive(Clone)]
struct Pipelines {
rects: wgpu::RenderPipeline,
borders: wgpu::RenderPipeline,
ellipses: wgpu::RenderPipeline,
ellipse_borders: wgpu::RenderPipeline,
arcs: wgpu::RenderPipeline,
text_mask: wgpu::RenderPipeline,
text_color: wgpu::RenderPipeline,
image_rgba: wgpu::RenderPipeline,
coverage: wgpu::RenderPipeline,
image_nv12: wgpu::RenderPipeline,
blur: wgpu::RenderPipeline,
blur_content: wgpu::RenderPipeline,
clip_bin: wgpu::RenderPipeline,
clip_dec: wgpu::RenderPipeline,
slug: Option<wgpu::RenderPipeline>,
mesh: wgpu::RenderPipeline,
mesh_overlay: wgpu::RenderPipeline,
mesh_add: wgpu::RenderPipeline,
mesh_multiply: wgpu::RenderPipeline,
mesh_screen: wgpu::RenderPipeline,
mesh_darken: wgpu::RenderPipeline,
mesh_lighten: wgpu::RenderPipeline,
blend_layer: wgpu::RenderPipeline,
mesh_clip_inc: wgpu::RenderPipeline,
mesh_clip_dec: wgpu::RenderPipeline,
projective_layer: wgpu::RenderPipeline,
}
impl Pipelines {
fn create(
device: &wgpu::Device,
format: wgpu::TextureFormat,
sample_count: u32,
globals_layout: &wgpu::BindGroupLayout,
text_bind_layout: &wgpu::BindGroupLayout,
image_bind_layout_nv12: &wgpu::BindGroupLayout,
clip_pipeline_layout: &wgpu::PipelineLayout,
stencil_for_content: &wgpu::DepthStencilState,
stencil_for_clip_inc: &wgpu::DepthStencilState,
stencil_for_clip_dec: &wgpu::DepthStencilState,
clip_vertex_layout: &wgpu::VertexBufferLayout,
mesh_bind_layout: &wgpu::BindGroupLayout,
) -> Self {
let msaa_state = wgpu::MultisampleState {
count: sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
};
macro_rules! make_content_pipeline {
($name:ident, $shader:literal, $inst_type:ty, $attrs:expr) => {
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(concat!($shader, ".wgsl")),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(concat!(
"shaders/", $shader, ".wgsl"
)))),
});
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(concat!($shader, " pipeline layout")),
bind_group_layouts: &[Some(globals_layout)],
immediate_size: 0,
});
let $name = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(concat!($shader, " pipeline")),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader_module,
entry_point: Some("vs_main"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<$inst_type>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: $attrs,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader_module,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_content.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
});
};
}
let rect_attrs: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 32,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 36,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 48,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 5,
offset: 64,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 6,
offset: 80,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 7,
offset: 88,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 8,
offset: 96,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 9,
offset: 112,
format: wgpu::VertexFormat::Float32x4,
},
];
let border_attrs: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 32,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 36,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 48,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 5,
offset: 52,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 6,
offset: 68,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 7,
offset: 84,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 8,
offset: 92,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 9,
offset: 100,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 10,
offset: 116,
format: wgpu::VertexFormat::Float32x4,
},
];
let ellipse_attrs: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 20,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 32,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 48,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 5,
offset: 64,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 6,
offset: 72,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 7,
offset: 80,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 8,
offset: 96,
format: wgpu::VertexFormat::Float32x4,
},
];
let ellipse_border_attrs: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 20,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 24,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 28,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 5,
offset: 32,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 6,
offset: 48,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 7,
offset: 64,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 8,
offset: 72,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 9,
offset: 80,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 10,
offset: 96,
format: wgpu::VertexFormat::Float32x4,
},
];
make_content_pipeline!(rects, "rect", RectInstance, rect_attrs);
make_content_pipeline!(borders, "border", BorderInstance, border_attrs);
make_content_pipeline!(ellipses, "ellipse", EllipseInstance, ellipse_attrs);
make_content_pipeline!(
ellipse_borders,
"ellipse_border",
EllipseBorderInstance,
ellipse_border_attrs
);
let arc_attrs: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 20,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 24,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 28,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 5,
offset: 32,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 6,
offset: 36,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 7,
offset: 48,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 8,
offset: 64,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 9,
offset: 80,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 10,
offset: 88,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 11,
offset: 96,
format: wgpu::VertexFormat::Uint32,
},
wgpu::VertexAttribute {
shader_location: 12,
offset: 100,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 13,
offset: 112,
format: wgpu::VertexFormat::Float32x4,
},
];
make_content_pipeline!(arcs, "arc", ArcInstance, arc_attrs);
let text_mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("text.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/text.wgsl"))),
});
let text_color_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("text_color.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"shaders/text_color.wgsl"
))),
});
let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("text pipeline layout"),
bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
immediate_size: 0,
});
let glyph_vertex = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<GlyphInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 32,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 48,
format: wgpu::VertexFormat::Float32x4,
},
],
};
let text_mask = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("text pipeline (mask)"),
layout: Some(&text_pipeline_layout),
vertex: wgpu::VertexState {
module: &text_mask_shader,
entry_point: Some("vs_main"),
buffers: &[Some(glyph_vertex.clone())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &text_mask_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_content.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
});
let text_color = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("text pipeline (color)"),
layout: Some(&text_pipeline_layout),
vertex: wgpu::VertexState {
module: &text_color_shader,
entry_point: Some("vs_main"),
buffers: &[Some(glyph_vertex.clone())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &text_color_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_content.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
});
let image_rgba = text_color.clone();
let coverage_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("coverage.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/coverage.wgsl"))),
});
let coverage = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("coverage pipeline (tinted a8)"),
layout: Some(&text_pipeline_layout),
vertex: wgpu::VertexState {
module: &coverage_shader,
entry_point: Some("vs_main"),
buffers: &[Some(glyph_vertex.clone())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &coverage_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_content.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
});
let blur_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("blur_shadow.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"shaders/blur_shadow.wgsl"
))),
});
let blur_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("blur pipeline layout"),
bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
immediate_size: 0,
});
let blur = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("blur pipeline"),
layout: Some(&blur_pipeline_layout),
vertex: wgpu::VertexState {
module: &blur_shader,
entry_point: Some("vs_main"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<BlurInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 32,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 48,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 56,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 5,
offset: 72,
format: wgpu::VertexFormat::Uint32,
},
],
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blur_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_content.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
});
let blur_content_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("blur_content.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"shaders/blur_content.wgsl"
))),
});
let blur_content = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("blur content pipeline"),
layout: Some(&blur_pipeline_layout),
vertex: wgpu::VertexState {
module: &blur_content_shader,
entry_point: Some("vs_main"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<BlurInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 32,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 48,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 56,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 5,
offset: 72,
format: wgpu::VertexFormat::Uint32,
},
],
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blur_content_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_content.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
});
let image_nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("image_nv12.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"shaders/image_nv12.wgsl"
))),
});
let image_nv12_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("image nv12 pipeline layout"),
bind_group_layouts: &[Some(globals_layout), Some(image_bind_layout_nv12)],
immediate_size: 0,
});
let image_nv12 = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("image nv12 pipeline"),
layout: Some(&image_nv12_layout),
vertex: wgpu::VertexState {
module: &image_nv12_shader,
entry_point: Some("vs_main"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Nv12Instance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 32,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 48,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 52,
format: wgpu::VertexFormat::Float32,
},
wgpu::VertexAttribute {
shader_location: 5,
offset: 56,
format: wgpu::VertexFormat::Float32x4,
},
],
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &image_nv12_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_content.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
});
let clip_color_target = wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::empty(),
};
let clip_shader_bin = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("clip_round_rect_bin.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"shaders/clip_round_rect_bin.wgsl"
))),
});
let clip_bin = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("clip pipeline (bin)"),
layout: Some(clip_pipeline_layout),
vertex: wgpu::VertexState {
module: &clip_shader_bin,
entry_point: Some("vs_main"),
buffers: &[Some(clip_vertex_layout.clone())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &clip_shader_bin,
entry_point: Some("fs_main"),
targets: &[Some(clip_color_target.clone())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_clip_inc.clone()),
multisample: wgpu::MultisampleState {
count: sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
let clip_dec = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("clip pipeline (dec)"),
layout: Some(clip_pipeline_layout),
vertex: wgpu::VertexState {
module: &clip_shader_bin,
entry_point: Some("vs_main"),
buffers: &[Some(clip_vertex_layout.clone())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &clip_shader_bin,
entry_point: Some("fs_main"),
targets: &[Some(clip_color_target.clone())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_clip_dec.clone()),
multisample: wgpu::MultisampleState {
count: sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
let slug = Some(slug::create_pipeline(
device,
format,
sample_count,
stencil_for_content,
));
let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mesh.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shaders/mesh.wgsl"))),
});
let mesh_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("mesh pipeline layout"),
bind_group_layouts: &[Some(globals_layout), Some(mesh_bind_layout)],
immediate_size: 0,
});
let mesh_vertex_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<MeshVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 8,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 24,
format: wgpu::VertexFormat::Float32x2,
},
],
};
let make_mesh_pipeline =
|label: &str, depth: &wgpu::DepthStencilState, color: &wgpu::ColorTargetState| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&mesh_pipeline_layout),
vertex: wgpu::VertexState {
module: &mesh_shader,
entry_point: Some("vs_main"),
buffers: &[Some(mesh_vertex_layout.clone())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &mesh_shader,
entry_point: Some("fs_main"),
targets: &[Some(color.clone())],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: Some(depth.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
})
};
let mesh_color_target = wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
};
let mut stencil_for_mesh = stencil_for_content.clone();
stencil_for_mesh.stencil.front.compare = wgpu::CompareFunction::Equal;
stencil_for_mesh.stencil.back.compare = wgpu::CompareFunction::Equal;
let mut stencil_for_overlay = stencil_for_content.clone();
stencil_for_overlay.stencil.front.compare = wgpu::CompareFunction::LessEqual;
stencil_for_overlay.stencil.back.compare = wgpu::CompareFunction::LessEqual;
let mesh = make_mesh_pipeline("mesh pipeline", &stencil_for_mesh, &mesh_color_target);
let mesh_overlay = make_mesh_pipeline(
"mesh overlay pipeline",
&stencil_for_overlay,
&mesh_color_target,
);
let mesh_clip_inc = make_mesh_pipeline(
"mesh clip (inc) pipeline",
stencil_for_clip_inc,
&clip_color_target,
);
let mesh_clip_dec = make_mesh_pipeline(
"mesh clip (dec) pipeline",
stencil_for_clip_dec,
&clip_color_target,
);
let mesh_blend_target = |blend: wgpu::BlendState| wgpu::ColorTargetState {
format,
blend: Some(blend),
write_mask: wgpu::ColorWrites::ALL,
};
let premult_alpha = wgpu::BlendComponent::OVER;
let mesh_add = make_mesh_pipeline(
"mesh pipeline (add)",
&stencil_for_mesh,
&mesh_blend_target(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
}),
);
let mesh_multiply = make_mesh_pipeline(
"mesh pipeline (multiply)",
&stencil_for_mesh,
&mesh_blend_target(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Dst,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: premult_alpha,
}),
);
let mesh_screen = make_mesh_pipeline(
"mesh pipeline (screen)",
&stencil_for_mesh,
&mesh_blend_target(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrc,
operation: wgpu::BlendOperation::Add,
},
alpha: premult_alpha,
}),
);
let mesh_darken = make_mesh_pipeline(
"mesh pipeline (darken)",
&stencil_for_mesh,
&mesh_blend_target(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Min,
},
alpha: premult_alpha,
}),
);
let mesh_lighten = make_mesh_pipeline(
"mesh pipeline (lighten)",
&stencil_for_mesh,
&mesh_blend_target(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Max,
},
alpha: premult_alpha,
}),
);
let projective_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("projective_layer.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"shaders/projective_layer.wgsl"
))),
});
let projective_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("projective layer pipeline layout"),
bind_group_layouts: &[Some(globals_layout), Some(text_bind_layout)],
immediate_size: 0,
});
let projective_vertex_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<ProjectiveInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 8,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 16,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 24,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 32,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 5,
offset: 48,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 6,
offset: 64,
format: wgpu::VertexFormat::Float32,
},
],
};
let projective_layer = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("projective layer composite pipeline"),
layout: Some(&projective_pipeline_layout),
vertex: wgpu::VertexState {
module: &projective_shader,
entry_point: Some("vs_main"),
buffers: &[Some(projective_vertex_layout)],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &projective_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_content.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
});
let blend_layer_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("blend_layer.wgsl"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"shaders/blend_layer.wgsl"
))),
});
let blend_layer_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("blend layer pipeline layout"),
bind_group_layouts: &[
Some(globals_layout),
Some(text_bind_layout),
Some(text_bind_layout),
],
immediate_size: 0,
});
let blend_vertex_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<BlendInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 32,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 3,
offset: 48,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 4,
offset: 64,
format: wgpu::VertexFormat::Uint32,
},
],
};
let blend_layer = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("blend layer pipeline"),
layout: Some(&blend_layer_layout),
vertex: wgpu::VertexState {
module: &blend_layer_shader,
entry_point: Some("vs_main"),
buffers: &[Some(blend_vertex_layout)],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blend_layer_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(stencil_for_content.clone()),
multisample: msaa_state,
multiview_mask: None,
cache: None,
});
Self {
rects,
borders,
ellipses,
ellipse_borders,
arcs,
text_mask,
text_color,
image_rgba,
image_nv12,
coverage,
blur,
blur_content,
clip_bin,
clip_dec,
slug,
mesh,
mesh_add,
mesh_multiply,
mesh_screen,
mesh_darken,
mesh_lighten,
blend_layer,
mesh_overlay,
mesh_clip_inc,
mesh_clip_dec,
projective_layer,
}
}
}
struct Pass {
id: u64,
target: PassTarget,
initial_scissor: (u32, u32, u32, u32),
active_scissor: Option<(u32, u32, u32, u32)>,
clear_color: Option<[f32; 4]>,
cmds: Vec<Cmd>,
}
struct LayerState {
layer_id: u32,
parent_target: PassTarget,
parent_scissors: Vec<repose_core::Rect>,
parent_root_clip: repose_core::Rect,
parent_size: (f32, f32),
parent_transform: Transform,
parent_scissor: Option<(u32, u32, u32, u32)>,
parent_clips: Vec<ActiveClip>,
alpha: f32,
blur: (f32, f32),
rectangle_edge: bool,
}
struct FlattenRecord {
stack_len: usize,
layer_id: u32,
map: [f32; 9],
layer_rect: repose_core::Rect,
saved_scissor_stack: Vec<repose_core::Rect>,
saved_root: repose_core::Rect,
saved_size: (f32, f32),
saved_active_scissor: Option<(u32, u32, u32, u32)>,
saved_clips: Vec<ActiveClip>,
}
const FLATTEN_ID_BASE: u32 = 0xF000_0000;
const MAX_GRAPHICS_LAYERS: usize = 128;
const MAX_GRAPHICS_LAYER_BYTES: u64 = 256 * 1024 * 1024;
const MAX_TRANSIENT_GRAPHICS_LAYERS: usize = 64;
const MAX_TRANSIENT_GRAPHICS_LAYER_BYTES: u64 = 128 * 1024 * 1024;
const STENCIL_BASE: u32 = 128;
const STENCIL_MAX_DEPTH: u32 = 127;
#[allow(non_snake_case)]
enum Cmd {
ClipPush {
off: u64,
cnt: u32,
scissor: (u32, u32, u32, u32),
difference: bool,
applied: bool,
},
ClipPop {
off: u64,
cnt: u32,
scissor: (u32, u32, u32, u32),
stencil_scissor: (u32, u32, u32, u32),
difference: bool,
applied: bool,
},
Rect {
off: u64,
cnt: u32,
},
Border {
off: u64,
cnt: u32,
},
Ellipse {
off: u64,
cnt: u32,
},
EllipseBorder {
off: u64,
cnt: u32,
},
Arc {
off: u64,
cnt: u32,
},
GlyphsMask {
off: u64,
cnt: u32,
},
GlyphsColor {
off: u64,
cnt: u32,
},
GlyphsVector {
off: u64,
cnt: u32,
},
ImageRgba {
off: u64,
cnt: u32,
handle: u64,
filter: ImageFilter,
tile: bool,
},
Coverage {
off: u64,
cnt: u32,
handle: u64,
},
ImageNv12 {
off: u64,
cnt: u32,
handle: u64,
filter: ImageFilter,
},
CompositeLayer {
off: u64,
cnt: u32,
layer_id: u32,
},
CompositeShadow {
off: u64,
cnt: u32,
layer_id: u32,
},
CompositeBlur {
off: u64,
cnt: u32,
layer_id: u32,
},
CompositeProjective {
off: u64,
cnt: u32,
layer_id: u32,
},
VectorMesh {
mesh: MeshGpu,
uoff: u64,
blend: repose_core::BlendMode,
},
BlendLayer {
off: u64,
cnt: u32,
src_layer: u32,
dst_layer: Option<u32>,
parent: PassTarget,
scissor: (u32, u32, u32, u32),
},
VectorOverlay {
mesh: MeshGpu,
uoff: u64,
},
VectorClipPush {
mesh: MeshGpu,
uoff: u64,
scissor: (u32, u32, u32, u32),
difference: bool,
applied: bool,
},
VectorClipPop {
mesh: MeshGpu,
uoff: u64,
scissor: (u32, u32, u32, u32),
stencil_scissor: (u32, u32, u32, u32),
difference: bool,
applied: bool,
},
Callback {
rect: repose_core::Rect,
clip_rect: repose_core::Rect,
scissor: (u32, u32, u32, u32),
restore_scissor: (u32, u32, u32, u32),
callback_id: usize,
payload: repose_core::PaintCallbackPayload,
},
}
struct CoverageTex {
#[allow(dead_code)]
tex: wgpu::Texture,
bind: wgpu::BindGroup,
w: u32,
h: u32,
last_used_frame: u64,
bytes: u64,
}
struct ImageBinds {
linear: wgpu::BindGroup,
nearest: wgpu::BindGroup,
tile_linear: wgpu::BindGroup,
tile_nearest: wgpu::BindGroup,
}
impl ImageBinds {
fn get(&self, filter: ImageFilter, tile: bool) -> &wgpu::BindGroup {
match (filter, tile) {
(ImageFilter::Linear, false) => &self.linear,
(ImageFilter::Nearest, false) => &self.nearest,
(ImageFilter::Linear, true) => &self.tile_linear,
(ImageFilter::Nearest, true) => &self.tile_nearest,
}
}
}
enum ImageTex {
Rgba {
tex: wgpu::Texture,
binds: ImageBinds,
w: u32,
h: u32,
format: wgpu::TextureFormat,
last_used_frame: u64,
bytes: u64,
},
User {
binds: ImageBinds,
w: u32,
h: u32,
last_used_frame: u64,
bytes: u64,
},
Nv12 {
tex_y: wgpu::Texture,
tex_uv: wgpu::Texture,
binds: ImageBinds,
yuv_buf: wgpu::Buffer,
w: u32,
h: u32,
pixel_format: PixelFormat,
fourcc: Option<u32>,
color_info: ColorInfo,
external: bool,
last_used_frame: u64,
bytes: u64,
},
}
impl ImageTex {
fn evictable(&self) -> bool {
match self {
Self::Rgba { .. } => true,
Self::User { .. } => false,
Self::Nv12 { external, .. } => !*external,
}
}
}
#[derive(Clone)]
struct RetainedImage {
w: u32,
h: u32,
format: wgpu::TextureFormat,
rgba: Vec<u8>,
last_used_frame: u64,
}
struct ResolvedImageSource {
width: f32,
height: f32,
uv_min: [f32; 2],
uv_max: [f32; 2],
}
fn resolve_image_source(
source_rect: Option<ImageSourceRect>,
image_width: u32,
image_height: u32,
) -> Option<ResolvedImageSource> {
if image_width == 0 || image_height == 0 {
return None;
}
let Some(source) = source_rect else {
return Some(ResolvedImageSource {
width: image_width as f32,
height: image_height as f32,
uv_min: [0.0, 0.0],
uv_max: [1.0, 1.0],
});
};
if source.is_empty() {
return None;
}
let right = source.x.checked_add(source.width)?;
let bottom = source.y.checked_add(source.height)?;
if right > image_width || bottom > image_height {
return None;
}
Some(ResolvedImageSource {
width: source.width as f32,
height: source.height as f32,
uv_min: [
source.x as f32 / image_width as f32,
source.y as f32 / image_height as f32,
],
uv_max: [
right as f32 / image_width as f32,
bottom as f32 / image_height as f32,
],
})
}
fn image_fit_geometry(
rect: repose_core::Rect,
fit: ImageFit,
alignment: ImageAlignment,
source: &ResolvedImageSource,
) -> Option<(repose_core::Rect, [f32; 4])> {
let src_w = source.width;
let src_h = source.height;
let dst_w = rect.w.max(0.0);
let dst_h = rect.h.max(0.0);
if src_w <= 0.0 || src_h <= 0.0 || dst_w <= 0.0 || dst_h <= 0.0 {
return None;
}
let slack = |content: f32, extent: f32| match alignment {
ImageAlignment::Center => (extent - content) * 0.5,
ImageAlignment::Begin => 0.0,
ImageAlignment::End => extent - content,
};
let tiled = matches!(fit, ImageFit::Tile);
let (scale_x, scale_y) = fit.scale_factor((src_w, src_h), (dst_w, dst_h));
let drawn = if tiled {
repose_core::Rect {
x: rect.x,
y: rect.y,
w: dst_w,
h: dst_h,
}
} else {
let (w, h) = (src_w * scale_x, src_h * scale_y);
repose_core::Rect {
x: rect.x + slack(w, dst_w),
y: rect.y + slack(h, dst_h),
w,
h,
}
};
let uv_min = source.uv_min;
let uv_max = source.uv_max;
let repeats = if tiled {
[dst_w / src_w, dst_h / src_h]
} else {
[1.0, 1.0]
};
let mut uv = [
uv_min[0],
uv_max[1],
uv_min[0] + (uv_max[0] - uv_min[0]) * repeats[0],
uv_max[1] - (uv_max[1] - uv_min[1]) * repeats[1],
];
let clipped = intersect(drawn, rect);
if clipped.w <= 0.0 || clipped.h <= 0.0 {
return None;
}
let du = uv[2] - uv[0];
let dv = uv[3] - uv[1];
uv = [
uv[0] + du * (clipped.x - drawn.x) / drawn.w,
uv[1] + dv * (clipped.y - drawn.y) / drawn.h,
uv[0] + du * (clipped.x + clipped.w - drawn.x) / drawn.w,
uv[1] + dv * (clipped.y + clipped.h - drawn.y) / drawn.h,
];
Some((clipped, uv))
}
struct AtlasA8 {
tex: wgpu::Texture,
view: wgpu::TextureView,
sampler: wgpu::Sampler,
size: u32,
next_x: u32,
next_y: u32,
row_h: u32,
map: HashMap<repose_text::CacheKey, GlyphInfo>,
used_area: u64,
}
struct AtlasRGBA {
tex: wgpu::Texture,
view: wgpu::TextureView,
sampler: wgpu::Sampler,
size: u32,
next_x: u32,
next_y: u32,
row_h: u32,
map: HashMap<repose_text::CacheKey, GlyphInfo>,
used_area: u64,
}
#[derive(Default, Clone, Copy)]
struct AtlasPending {
grow: bool,
compact: bool,
}
#[derive(Clone, Copy)]
struct GlyphInfo {
u0: f32,
v0: f32,
u1: f32,
v1: f32,
w: f32,
h: f32,
last_touch: u64,
last_used_frame: u64,
}
#[derive(Clone, Hash, PartialEq, Eq)]
struct TextShapeKey {
text: Arc<str>,
px_bits: u32,
line_height_bits: u32,
font_family: Option<&'static str>,
font_weight: u16,
font_style: u8,
letter_spacing_bits: u32,
variation: Option<Arc<str>>,
font_generation: u64,
}
struct TextShapeCacheEntry {
glyphs: Arc<[repose_text::ShapedGlyph]>,
last_used_frame: u64,
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct RectInstance {
xywh: [f32; 4],
radii: [f32; 4],
brush_type: u32,
grad_kind: u32,
_pad: [f32; 2],
color0: [f32; 4],
color1: [f32; 4],
grad_p0: [f32; 2],
grad_p1: [f32; 2],
tile_mode: u32,
_pad2: [f32; 3],
fwd_mat: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct BorderInstance {
xywh: [f32; 4],
radii: [f32; 4],
stroke: f32,
brush_type: u32,
_pad: [f32; 2],
grad_kind: u32,
color0: [f32; 4],
color1: [f32; 4],
grad_p0: [f32; 2],
grad_p1: [f32; 2],
tile_mode: u32,
_pad2: [f32; 3],
fwd_mat: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct EllipseInstance {
xywh: [f32; 4],
brush_type: u32,
grad_kind: u32,
_pad: [f32; 2],
color0: [f32; 4],
color1: [f32; 4],
grad_p0: [f32; 2],
grad_p1: [f32; 2],
tile_mode: u32,
_pad2: [f32; 3],
fwd_mat: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct EllipseBorderInstance {
xywh: [f32; 4],
stroke: f32,
pad: f32,
brush_type: u32,
grad_kind: u32,
color0: [f32; 4],
color1: [f32; 4],
grad_p0: [f32; 2],
grad_p1: [f32; 2],
tile_mode: u32,
_pad2: [f32; 3],
fwd_mat: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct ArcInstance {
xywh: [f32; 4],
start_angle: f32,
sweep_angle: f32,
stroke: f32,
pad: f32,
brush_type: u32,
grad_kind: u32,
_pad0: [f32; 2],
color0: [f32; 4],
color1: [f32; 4],
grad_p0: [f32; 2],
grad_p1: [f32; 2],
tile_mode: u32,
cap: f32, _pad1: [f32; 2],
fwd_mat: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct GlyphInstance {
xywh: [f32; 4],
uv: [f32; 4],
color: [f32; 4],
fwd_mat: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct BlurInstance {
xywh: [f32; 4],
uv: [f32; 4],
color: [f32; 4],
blur_uv: [f32; 2],
fwd_mat: [f32; 4],
edge_mode: u32,
_pad: [f32; 3],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct ProjectiveInstance {
c0: [f32; 2],
c1: [f32; 2],
c2: [f32; 2],
c3: [f32; 2],
uv: [f32; 4],
w: [f32; 4],
alpha: f32,
_pad: [f32; 3],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct YuvTransformRaw {
row0: [f32; 4],
row1: [f32; 4],
row2: [f32; 4],
b: [f32; 4],
transfer: [f32; 4],
}
fn make_yuv_transform_raw(color_info: ColorInfo, pixel_format: PixelFormat) -> YuvTransformRaw {
let yuv = color_info.to_yuv_transform();
let sample_scale = match pixel_format {
PixelFormat::P010 => 65535.0 / (64.0 * 1020.0),
_ => 1.0,
};
let transfer_mode = match color_info.transfer {
repose_core::color::Transfer::Srgb => 0.0,
repose_core::color::Transfer::Bt709 => 1.0,
repose_core::color::Transfer::Linear => 2.0,
repose_core::color::Transfer::Pq => 3.0,
repose_core::color::Transfer::Hlg => 4.0,
};
YuvTransformRaw {
row0: [yuv.m[0][0], yuv.m[0][1], yuv.m[0][2], 0.0],
row1: [yuv.m[1][0], yuv.m[1][1], yuv.m[1][2], 0.0],
row2: [yuv.m[2][0], yuv.m[2][1], yuv.m[2][2], 0.0],
b: [yuv.b[0], yuv.b[1], yuv.b[2], sample_scale],
transfer: [transfer_mode, 0.0, 0.0, 0.0],
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Nv12Instance {
xywh: [f32; 4],
uv: [f32; 4],
color: [f32; 4],
uv_x_offset: f32,
uv_y_offset: f32,
fwd_mat: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct ClipInstance {
xywh: [f32; 4],
radii: [f32; 4],
fwd_mat: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct BlendInstance {
xywh: [f32; 4],
uv: [f32; 4],
color: [f32; 4],
fwd_mat: [f32; 4],
mode: u32,
_pad: [f32; 3],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct MeshVertex {
pos: [f32; 2],
color: [f32; 4],
uv: [f32; 2],
}
#[derive(Clone)]
struct MeshGpu {
vertices: wgpu::Buffer,
indices: wgpu::Buffer,
vertex_count: u32,
index_count: u32,
bytes: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct MeshCacheKey {
source: usize,
index: usize,
kind: u8,
vertex_len: usize,
index_len: usize,
}
#[derive(Clone)]
enum MeshOwner {
Object(Arc<repose_core::VectorMeshData>),
Overlay(Arc<[repose_core::VectorMeshData]>),
}
struct MeshCacheEntry {
owner: MeshOwner,
gpu: MeshGpu,
last_used_frame: u64,
last_touch_frame: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct SlugDrawKey {
cache_key: repose_text::CacheKey,
fill: bool,
stroke_hash: u64,
transform: [u32; 11],
origin: [u32; 2],
px_bits: u32,
target: [u32; 2],
color: [u32; 4],
}
struct SlugDrawCacheEntry {
vertices: Arc<[slug::TessVertex]>,
bytes: u64,
last_used_frame: u64,
last_touch_frame: u64,
}
struct GlyphOutlineEntry {
commands: Option<Arc<[repose_text::Command]>>,
last_touch: u64,
}
fn evict_atlas_lru(
map: &mut HashMap<repose_text::CacheKey, GlyphInfo>,
used_area: &mut u64,
frame_index: u64,
target_area: u64,
) -> bool {
let mut victims: Vec<(u64, repose_text::CacheKey)> = map
.iter()
.filter(|(_, info)| info.last_used_frame != frame_index)
.map(|(key, info)| (info.last_touch, *key))
.collect();
if victims.is_empty() {
return false;
}
victims.sort_unstable_by_key(|victim| victim.0);
let mut evicted = false;
for (_, key) in victims {
if *used_area <= target_area || map.len() <= 1 {
break;
}
if let Some(info) = map.remove(&key) {
*used_area = used_area.saturating_sub(glyph_cell_area(&info));
evicted = true;
}
}
evicted
}
fn glyph_cell_area(info: &GlyphInfo) -> u64 {
(info.w as u64).saturating_mul(info.h as u64)
}
fn defer_atlas_glyph(deferred: &mut HashSet<repose_text::CacheKey>, key: repose_text::CacheKey) {
if deferred.len() >= MAX_GLYPH_ATLAS_ENTRIES {
deferred.clear();
}
deferred.insert(key);
}
fn slug_draw_cache_entry_bytes(vertices: usize) -> u64 {
let per_entry = (std::mem::size_of::<SlugDrawKey>()
+ std::mem::size_of::<SlugDrawCacheEntry>()
+ std::mem::size_of::<(SlugDrawKey, SlugDrawCacheEntry)>()) as u64;
u64::try_from(vertices)
.ok()
.and_then(|count| count.checked_mul(std::mem::size_of::<slug::TessVertex>() as u64))
.and_then(|payload| payload.checked_add(per_entry))
.unwrap_or(u64::MAX)
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct MeshUniform {
m0: [f32; 4],
m1: [f32; 4],
paint: [u32; 4],
color0: [f32; 4],
color1: [f32; 4],
grad_start: [f32; 2],
_p3: [f32; 2],
grad_end: [f32; 2],
_p4: [f32; 2],
}
const MESH_UNIFORM_CAP: u64 = 4 * 1024 * 1024;
const MAX_RETAINED_IMAGE_BYTES: u64 = 256 * 1024 * 1024;
const MAX_RETAINED_IMAGES: usize = 512;
const MAX_BLEND_SNAPSHOT_BYTES: u64 = 64 * 1024 * 1024;
const MAX_BLEND_SNAPSHOT_POOL_ENTRIES: usize = 64;
const MAX_GLYPH_ATLAS_ENTRIES: usize = 32768;
const MAX_GLYPH_ATLAS_SIZE: u32 = 4096;
const ATLAS_COMPACT_KEEP_NUM: u64 = 3;
const ATLAS_COMPACT_KEEP_DEN: u64 = 4;
const ATLAS_COMPACT_MIN_FRAME_INTERVAL: u64 = 30;
const MAX_GPU_RESOURCE_BYTES: u64 = 1024 * 1024 * 1024;
const MAX_UPLOAD_RING_BYTES: u64 = 64 * 1024 * 1024;
const MAX_MESH_CACHE_BYTES: u64 = 256 * 1024 * 1024;
const MAX_MESH_CACHE_ENTRIES: usize = 512;
const MAX_SLUG_DRAW_CACHE_ENTRIES: usize = 2048;
const MAX_SLUG_DRAW_CACHE_BYTES: u64 = 32 * 1024 * 1024;
const MAX_LEGACY_SHAPED_TEXT_CACHE_ENTRIES: usize = 256;
const MAX_GLYPH_OUTLINE_CACHE_ENTRIES: usize = 4096;
const MAX_CALLBACK_SCOPES: usize = 256;
const NV12_FOURCC: u32 = 0x3231_564e;
const P010_FOURCC: u32 = 0x3031_3050;
fn canonical_fourcc(format: PixelFormat) -> u32 {
match format {
PixelFormat::P010 => P010_FOURCC,
_ => NV12_FOURCC,
}
}
impl MeshUniform {
fn identity() -> Self {
Self {
m0: [1.0, 0.0, 0.0, 0.0],
m1: [0.0, 1.0, 0.0, 0.0],
paint: [0; 4],
color0: [0.0; 4],
color1: [0.0; 4],
grad_start: [0.0; 2],
_p3: [0.0; 2],
grad_end: [0.0; 2],
_p4: [0.0; 2],
}
}
}
fn mesh_uniform_from_paint(affine: [f32; 6], paint: &repose_core::PaintDesc) -> MeshUniform {
let (paint_type, paint_kind, color0, color1, grad_start, grad_end) = match paint {
repose_core::PaintDesc::Solid => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
repose_core::PaintDesc::Linear {
start,
end,
start_color,
end_color,
} => (
1u32,
0u32,
start_color.to_linear(),
end_color.to_linear(),
[start.x, start.y],
[end.x, end.y],
),
repose_core::PaintDesc::Radial {
center,
radius,
start_color,
end_color,
} => (
1u32,
1u32,
start_color.to_linear(),
end_color.to_linear(),
[center.x, center.y],
[radius.max(0.0), 0.0],
),
repose_core::PaintDesc::Sweep {
center,
start_color,
end_color,
} => (
1u32,
2u32,
start_color.to_linear(),
end_color.to_linear(),
[center.x, center.y],
[0.0, 0.0],
),
_ => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
};
MeshUniform {
m0: [affine[0], affine[1], affine[2], 0.0],
m1: [affine[3], affine[4], affine[5], 0.0],
paint: [paint_type, paint_kind, 0, 0],
color0,
color1,
grad_start,
_p3: [0.0; 2],
grad_end,
_p4: [0.0; 2],
}
}
fn combine_mesh_affine(current: &Transform, mesh: [f32; 6]) -> [f32; 6] {
let cm = current.linear();
let (cm00, cm01, cm10, cm11) = (cm[0], cm[1], cm[2], cm[3]);
let mm00 = mesh[0];
let mm01 = mesh[1];
let mm10 = mesh[2];
let mm11 = mesh[3];
let mtx = mesh[4];
let mty = mesh[5];
let r00 = cm00 * mm00 + cm01 * mm10;
let r01 = cm00 * mm01 + cm01 * mm11;
let r10 = cm10 * mm00 + cm11 * mm10;
let r11 = cm10 * mm01 + cm11 * mm11;
let tx = cm00 * mtx + cm01 * mty + current.translate_x;
let ty = cm10 * mtx + cm11 * mty + current.translate_y;
[r00, r01, tx, r10, r11, ty]
}
fn mesh_aabb(mesh: &repose_core::VectorMeshData, affine: [f32; 6]) -> repose_core::Rect {
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for v in mesh.vertices.iter() {
let x = affine[0] * v.pos[0] + affine[1] * v.pos[1] + affine[2];
let y = affine[3] * v.pos[0] + affine[4] * v.pos[1] + affine[5];
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
let w = (max_x - min_x).max(0.0);
let h = (max_y - min_y).max(0.0);
if !min_x.is_finite() || !min_y.is_finite() {
return repose_core::Rect {
x: 0.0,
y: 0.0,
w: 0.0,
h: 0.0,
};
}
repose_core::Rect {
x: min_x,
y: min_y,
w,
h,
}
}
fn quantize_glyph_px(px: f32) -> f32 {
if !px.is_finite() || px <= 0.0 {
return 1.0;
}
((px * 4.0).round() / 4.0).max(0.25)
}
fn swash_to_a8_coverage(content: repose_text::SwashContent, data: &[u8]) -> Option<Vec<u8>> {
match content {
repose_text::SwashContent::Mask => Some(data.to_vec()),
repose_text::SwashContent::SubpixelMask => {
let mut out = Vec::with_capacity(data.len() / 4);
for px in data.as_chunks::<4>().0 {
let r = px[0];
let g = px[1];
let b = px[2];
out.push(r.max(g).max(b));
}
Some(out)
}
repose_text::SwashContent::Color => None,
}
}
impl WgpuSceneRenderer {
pub fn from_device(
device: wgpu::Device,
queue: wgpu::Queue,
output_format: wgpu::TextureFormat,
msaa_samples: u32,
) -> Self {
Self::from_device_with_working_space_msaa(
device,
queue,
output_format,
msaa_samples,
msaa_samples,
)
}
pub fn from_device_with_working_space_msaa(
device: wgpu::Device,
queue: wgpu::Queue,
output_format: wgpu::TextureFormat,
msaa_samples: u32,
working_space_msaa_samples: u32,
) -> Self {
let msaa_samples = msaa_samples.max(1);
let working_space_msaa_samples = working_space_msaa_samples.max(1);
let mesh_uniform_size = std::mem::size_of::<MeshUniform>() as u64;
let base_mesh_alignment =
u64::from(device.limits().min_uniform_buffer_offset_alignment).max(4);
let mesh_uniform_alignment =
align_up(mesh_uniform_size, base_mesh_alignment).unwrap_or(base_mesh_alignment);
let mesh_uniform_cap = align_up(
MESH_UNIFORM_CAP
.min(device.limits().max_buffer_size)
.max(mesh_uniform_alignment),
mesh_uniform_alignment,
)
.unwrap_or(mesh_uniform_alignment);
let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("globals layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("globals buf"),
size: std::mem::size_of::<Globals>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let globals_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("globals bind"),
layout: &globals_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: globals_buf.as_entire_binding(),
}],
});
let globals_staging_cap =
(64 * std::mem::size_of::<Globals>() as u64).max(std::mem::size_of::<Globals>() as u64);
let globals_staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("globals staging"),
size: globals_staging_cap,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let ds_format = wgpu::TextureFormat::Depth24PlusStencil8;
let stencil_for_content = wgpu::DepthStencilState {
format: ds_format,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Equal,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Equal,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
},
read_mask: 0xFF,
write_mask: 0x00,
},
bias: wgpu::DepthBiasState::default(),
};
let stencil_for_clip_inc = wgpu::DepthStencilState {
format: ds_format,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::IncrementClamp,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::IncrementClamp,
},
read_mask: 0xFF,
write_mask: 0xFF,
},
bias: wgpu::DepthBiasState::default(),
};
let stencil_for_clip_dec = wgpu::DepthStencilState {
format: ds_format,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::DecrementClamp,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::DecrementClamp,
},
read_mask: 0xFF,
write_mask: 0xFF,
},
bias: wgpu::DepthBiasState::default(),
};
let _multisample_state = wgpu::MultisampleState {
count: msaa_samples,
mask: !0,
alpha_to_coverage_enabled: false,
};
let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("image/text sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
let image_sampler_nearest = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("image nearest sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let image_sampler_repeat = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("image repeat sampler"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
let image_sampler_repeat_nearest = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("image repeat nearest sampler"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let layer_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("layer nearest sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let layer_sampler_linear = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("layer linear sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
let text_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("text/rgba bind layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let image_bind_layout_rgba = text_bind_layout.clone();
let image_bind_layout_nv12 =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("image bind layout nv12"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let clip_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("clip pipeline layout"),
bind_group_layouts: &[Some(&globals_layout)],
immediate_size: 0,
});
let clip_vertex_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<ClipInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
shader_location: 0,
offset: 0,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: 16,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
shader_location: 2,
offset: 32,
format: wgpu::VertexFormat::Float32x4,
},
],
};
let mesh_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mesh uniform layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: NonZeroU64::new(mesh_uniform_size),
},
count: None,
}],
});
let mesh_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mesh uniform buffer"),
size: mesh_uniform_cap,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mesh_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mesh uniform bind"),
layout: &mesh_bind_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &mesh_uniform_buf,
offset: 0,
size: NonZeroU64::new(mesh_uniform_size),
}),
}],
});
let surface_pipes = Pipelines::create(
&device,
output_format,
msaa_samples,
&globals_layout,
&text_bind_layout,
&image_bind_layout_nv12,
&clip_pipeline_layout,
&stencil_for_content,
&stencil_for_clip_inc,
&stencil_for_clip_dec,
&clip_vertex_layout,
&mesh_bind_layout,
);
let working_space_pipes = Pipelines::create(
&device,
wgpu::TextureFormat::Rgba16Float,
working_space_msaa_samples,
&globals_layout,
&text_bind_layout,
&image_bind_layout_nv12,
&clip_pipeline_layout,
&stencil_for_content,
&stencil_for_clip_inc,
&stencil_for_clip_dec,
&clip_vertex_layout,
&mesh_bind_layout,
);
let layer_pipes = if msaa_samples == 1 {
surface_pipes.clone()
} else {
Pipelines::create(
&device,
output_format,
1,
&globals_layout,
&text_bind_layout,
&image_bind_layout_nv12,
&clip_pipeline_layout,
&stencil_for_content,
&stencil_for_clip_inc,
&stencil_for_clip_dec,
&clip_vertex_layout,
&mesh_bind_layout,
)
};
let working_space_layer_pipes = if working_space_msaa_samples == 1 {
working_space_pipes.clone()
} else {
Pipelines::create(
&device,
wgpu::TextureFormat::Rgba16Float,
1,
&globals_layout,
&text_bind_layout,
&image_bind_layout_nv12,
&clip_pipeline_layout,
&stencil_for_content,
&stencil_for_clip_inc,
&stencil_for_clip_dec,
&clip_vertex_layout,
&mesh_bind_layout,
)
};
let slug_enabled = true;
let blur_ring = UploadRing::new(
&device,
"blur ring",
1024 * 1024,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let atlas_mask = init_atlas_mask(&device);
let atlas_color = init_atlas_color(&device);
let atlas_mask_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("atlas mask bind"),
layout: &text_bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&atlas_mask.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&atlas_mask.sampler),
},
],
});
let atlas_color_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("atlas color bind"),
layout: &text_bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&atlas_color.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&atlas_color.sampler),
},
],
});
let ring_rect = UploadRing::new(
&device,
"ring rect",
1 << 20,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_border = UploadRing::new(
&device,
"ring border",
1 << 20,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_ellipse = UploadRing::new(
&device,
"ring ellipse",
1 << 20,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_ellipse_border = UploadRing::new(
&device,
"ring ellipse border",
1 << 20,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_arc = UploadRing::new(
&device,
"ring arc",
1 << 20,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_glyph_mask = UploadRing::new(
&device,
"ring glyph mask",
1 << 20,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_glyph_color = UploadRing::new(
&device,
"ring glyph color",
1 << 20,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_slug = UploadRing::new(
&device,
"ring slug",
1 << 22,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_clip = UploadRing::new(
&device,
"ring clip",
1 << 16,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let blend_ring = UploadRing::new(
&device,
"ring blend",
1 << 16,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_projective = UploadRing::new(
&device,
"ring projective",
1 << 16,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let ring_nv12 = UploadRing::new(
&device,
"ring nv12",
1 << 20,
wgpu::BufferUsages::VERTEX
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
);
let depth_stencil_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("temp ds"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth24PlusStencil8,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let depth_stencil_view =
depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = WgpuSceneRenderer {
device,
queue,
output_format,
output_width: 0,
output_height: 0,
pixels_per_point: 1.0,
surface_pipes,
working_space_pipes,
layer_pipes,
working_space_layer_pipes,
rects: InstancedPipe::new(ring_rect),
borders: InstancedPipe::new(ring_border),
ellipses: InstancedPipe::new(ring_ellipse),
ellipse_borders: InstancedPipe::new(ring_ellipse_border),
arcs: InstancedPipe::new(ring_arc),
glyph_mask: InstancedPipe::new(ring_glyph_mask),
glyph_color: InstancedPipe::new(ring_glyph_color),
text_bind_layout,
image_bind_layout_rgba,
image_bind_layout_nv12,
image_sampler,
image_sampler_nearest,
image_sampler_repeat,
image_sampler_repeat_nearest,
layer_sampler,
layer_sampler_linear,
blur_ring,
slug_enabled,
slug_ring: ring_slug,
slug_cache: slug::GlyphSlugCache::new(),
slug_draw_cache: HashMap::new(),
slug_draw_cache_bytes_total: 0,
clip_ring: ring_clip,
nv12: InstancedPipe::new(ring_nv12),
mesh_cache: HashMap::new(),
mesh_cache_bytes_total: 0,
mesh_uniform_buf,
mesh_bind,
mesh_uniform_head: 0,
mesh_uniform_alignment,
mesh_uniform_cap,
projective_ring: ring_projective,
blend_ring,
flatten_layer_ids: Vec::new(),
blend_snapshots: std::collections::HashMap::new(),
blend_snapshot_pool: HashMap::new(),
blend_copies: Vec::new(),
msaa_samples,
working_space_msaa_samples,
depth_stencil_tex,
depth_stencil_view,
msaa_tex: None,
msaa_view: None,
ws_msaa_tex: None,
ws_msaa_view: None,
surface_resolve_tex: None,
surface_resolve_view: None,
surface_resolve_bytes: 0,
msaa_bytes: 0,
ws_msaa_bytes: 0,
depth_stencil_bytes: 0,
working_space_bytes: 0,
gpu_budget_bytes: MAX_GPU_RESOURCE_BYTES,
globals_bind,
globals_buf,
globals_staging,
globals_staging_cap,
atlas_mask,
atlas_color,
atlas_mask_bind,
atlas_color_bind,
atlas_generation: repose_text::font_generation(),
atlas_mask_pending: AtlasPending::default(),
atlas_color_pending: AtlasPending::default(),
atlas_last_compact_frame: 0,
atlas_mask_failures: HashSet::new(),
atlas_color_failures: HashSet::new(),
atlas_mask_deferred: HashSet::new(),
atlas_color_deferred: HashSet::new(),
glyph_outline_cache: HashMap::new(),
legacy_shaped_text_cache: HashMap::new(),
next_image_handle: 1,
images: HashMap::new(),
retained: HashMap::new(),
retained_bytes_total: 0,
next_coverage_handle: 1,
coverages: HashMap::new(),
frame_index: 0,
cache_touch_clock: 0,
image_bytes_total: 0,
image_evict_after_frames: 600, image_budget_bytes: 512 * 1024 * 1024, layer_pool: HashMap::new(),
producer_layer_ids: HashSet::new(),
layer_bytes_total: 0,
transient_layer_bytes_total: 0,
working_space: false,
ws_tex: None,
ws_view: None,
ws_bind: None,
display_pipeline: None,
display_layout: None,
callback_resources: CallbackResources::default(),
callback_scoped_resources: HashMap::new(),
callback_scope_uses: HashMap::new(),
callback_scope_payloads: HashMap::new(),
callback_scope_clock: 0,
blend_snapshot_bytes_total: 0,
frame_active: false,
last_render_error: None,
};
renderer.recreate_msaa_and_depth_stencil();
renderer
}
}
impl WgpuSurfaceBackend {
#[cfg(feature = "winit-surface")]
pub async fn new_async(
window: Arc<winit::window::Window>,
) -> anyhow::Result<WgpuSurfaceBackend> {
Self::new_async_with_options(window, 4, PresentModePref::Auto).await
}
#[cfg(feature = "winit-surface")]
pub async fn new_async_with_msaa(
window: Arc<winit::window::Window>,
msaa_samples: u32,
) -> anyhow::Result<WgpuSurfaceBackend> {
Self::new_async_with_options(window, msaa_samples, PresentModePref::Auto).await
}
#[cfg(feature = "winit-surface")]
pub async fn new_async_with_options(
window: Arc<winit::window::Window>,
msaa_samples: u32,
present_mode: PresentModePref,
) -> anyhow::Result<WgpuSurfaceBackend> {
let instance: Instance = if cfg!(target_arch = "wasm32") {
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
wgpu::util::new_instance_with_webgpu_detection(desc).await
} else {
wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle())
};
let surface = instance.create_surface(window.clone())?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
apply_limit_buckets: false,
})
.await
.map_err(|e| anyhow::anyhow!("No suitable adapter: {e:?}"))?;
let limits = adapter.limits();
let features = {
let available = adapter.features();
let mut features = wgpu::Features::empty();
if available.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM) {
features |= wgpu::Features::TEXTURE_FORMAT_16BIT_NORM;
}
#[cfg(target_os = "linux")]
{
if available.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD) {
features |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_FD;
}
if available.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF) {
features |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF;
}
}
features
};
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("repose-rs device"),
required_features: features,
required_limits: limits,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
})
.await
.map_err(|e| anyhow::anyhow!("request_device failed: {e:?}"))?;
let size = window.inner_size();
let caps = surface.get_capabilities(&adapter);
let (format, view_format) = if cfg!(target_arch = "wasm32")
&& adapter
.get_downlevel_capabilities()
.flags
.contains(wgpu::DownlevelFlags::SURFACE_VIEW_FORMATS)
{
let non_srgb = caps
.formats
.iter()
.copied()
.find(|f| !f.is_srgb())
.unwrap_or(caps.formats[0]);
(non_srgb, Some(non_srgb.add_srgb_suffix()))
} else if cfg!(target_arch = "wasm32") {
let fmt = caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(caps.formats[0]);
(fmt, None)
} else {
let fmt = caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(caps.formats[0]);
(fmt, None)
};
let present_mode = pick_present_mode(&caps, present_mode);
let alpha_mode = caps.alpha_modes[0];
let render_format = view_format.unwrap_or(format);
let msaa_samples = pick_surface_msaa(&adapter, render_format, msaa_samples);
let working_space_msaa_samples = pick_surface_msaa_for_mode(
&adapter,
wgpu::TextureFormat::Rgba16Float,
msaa_samples,
true,
);
let mut renderer = WgpuSceneRenderer::from_device_with_working_space_msaa(
device,
queue,
render_format,
msaa_samples,
working_space_msaa_samples,
);
renderer.resize(size.width, size.height);
let view_formats = view_format.into_iter().collect::<Vec<_>>();
let surface_usage = if cfg!(target_arch = "wasm32") {
wgpu::TextureUsages::RENDER_ATTACHMENT
} else {
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
};
let config = wgpu::SurfaceConfiguration {
usage: surface_usage,
format,
width: size.width.max(1),
height: size.height.max(1),
present_mode,
alpha_mode,
color_space: wgpu::SurfaceColorSpace::Auto,
view_formats,
desired_maximum_frame_latency: 1,
};
surface.configure(&renderer.device, &config);
Ok(WgpuSurfaceBackend {
#[cfg(feature = "winit-surface")]
instance: Some(instance),
#[cfg(feature = "winit-surface")]
window: Some(window),
surface: Some(surface),
surface_config: Some(config),
pending_reconfigure: false,
renderer,
})
}
#[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
pub fn new(window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
pollster::block_on(Self::new_async(window))
}
#[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
pub fn new_with_msaa(
window: Arc<winit::window::Window>,
msaa_samples: u32,
) -> anyhow::Result<WgpuSurfaceBackend> {
pollster::block_on(Self::new_async_with_msaa(window, msaa_samples))
}
#[cfg(all(feature = "winit-surface", not(target_arch = "wasm32")))]
pub fn new_with_options(
window: Arc<winit::window::Window>,
msaa_samples: u32,
present_mode: PresentModePref,
) -> anyhow::Result<WgpuSurfaceBackend> {
pollster::block_on(Self::new_async_with_options(
window,
msaa_samples,
present_mode,
))
}
#[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
pub fn new(_window: Arc<winit::window::Window>) -> anyhow::Result<WgpuSurfaceBackend> {
anyhow::bail!("Use WgpuSurfaceBackend::new_async(window).await on wasm32")
}
#[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
pub fn new_with_msaa(
_window: Arc<winit::window::Window>,
_msaa_samples: u32,
) -> anyhow::Result<WgpuSurfaceBackend> {
anyhow::bail!("Use WgpuSurfaceBackend::new_async_with_msaa(window, msaa).await on wasm32")
}
#[cfg(all(feature = "winit-surface", target_arch = "wasm32"))]
pub fn new_with_options(
_window: Arc<winit::window::Window>,
_msaa_samples: u32,
_present_mode: PresentModePref,
) -> anyhow::Result<WgpuSurfaceBackend> {
anyhow::bail!(
"Use WgpuSurfaceBackend::new_async_with_options(window, msaa, mode).await on wasm32"
)
}
}
#[cfg(feature = "winit-surface")]
fn pick_present_mode(caps: &wgpu::SurfaceCapabilities, pref: PresentModePref) -> wgpu::PresentMode {
let auto = || {
caps.present_modes
.iter()
.copied()
.find(|m| *m == wgpu::PresentMode::Fifo)
.or_else(|| {
caps.present_modes
.iter()
.copied()
.find(|m| *m == wgpu::PresentMode::Mailbox)
})
.unwrap_or(wgpu::PresentMode::Immediate)
};
match pref {
PresentModePref::Auto => auto(),
PresentModePref::Fifo if caps.present_modes.contains(&wgpu::PresentMode::Fifo) => {
wgpu::PresentMode::Fifo
}
PresentModePref::Mailbox if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) => {
wgpu::PresentMode::Mailbox
}
PresentModePref::Immediate
if caps.present_modes.contains(&wgpu::PresentMode::Immediate) =>
{
wgpu::PresentMode::Immediate
}
_ => auto(),
}
}
pub fn pick_surface_msaa(
adapter: &wgpu::Adapter,
format: wgpu::TextureFormat,
requested: u32,
) -> u32 {
pick_surface_msaa_for_mode(adapter, format, requested, false)
}
pub fn pick_surface_msaa_for_mode(
adapter: &wgpu::Adapter,
format: wgpu::TextureFormat,
requested: u32,
working_space: bool,
) -> u32 {
let requested = requested.max(1);
let color_feat = adapter.get_texture_format_features(format);
let working_space_feat = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba16Float);
let depth_feat = adapter.get_texture_format_features(wgpu::TextureFormat::Depth24PlusStencil8);
let supported = |n: u32| {
color_feat
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
&& color_feat.flags.sample_count_supported(n)
&& (!working_space
|| (working_space_feat
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
&& working_space_feat.flags.sample_count_supported(n)
&& (n == 1
|| working_space_feat
.flags
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE))))
&& (n == 1
|| color_feat
.flags
.contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE))
&& depth_feat
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
&& depth_feat.flags.sample_count_supported(n)
};
let mut candidates = vec![requested];
for n in [8, 4, 2, 1] {
if n < requested {
candidates.push(n);
}
}
let chosen = candidates.into_iter().find(|&n| supported(n)).unwrap_or(1);
if chosen != requested {
log::info!("requested MSAA x{requested}, using x{chosen}");
}
chosen
}
impl WgpuSceneRenderer {
pub fn set_image_from_bytes(
&mut self,
handle: u64,
data: &[u8],
srgb: bool,
) -> anyhow::Result<()> {
let img = image::load_from_memory(data)?;
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
repose_core::set_image_intrinsic_size(handle, w, h);
self.set_image_rgba8(handle, w, h, &rgba, srgb)
}
pub fn set_image_rgba8(
&mut self,
handle: u64,
w: u32,
h: u32,
rgba: &[u8],
srgb: bool,
) -> anyhow::Result<()> {
validate_image_handle(handle)?;
validate_texture_dimensions(&self.device, w, h)?;
let expected = checked_image_bytes_usize(w, h, 4)?;
if rgba.len() < expected {
return Err(anyhow::anyhow!(
"RGBA buffer too small: {} < {}",
rgba.len(),
expected
));
}
let format = if srgb {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
};
let needs_recreate = match self.images.get(&handle) {
Some(ImageTex::Rgba {
w: cw,
h: ch,
format: cf,
..
}) => *cw != w || *ch != h || *cf != format,
_ => true,
};
if needs_recreate {
self.remove_image(handle);
let (tex, binds) = self.create_rgba_tex(w, h, format);
let bytes = checked_image_bytes(w, h, 4)?;
self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
self.images.insert(
handle,
ImageTex::Rgba {
tex,
binds,
w,
h,
format,
last_used_frame: self.frame_index,
bytes,
},
);
}
if let Some(old) = self.retained.remove(&handle) {
self.retained_bytes_total = self
.retained_bytes_total
.saturating_sub(old.rgba.len() as u64);
}
self.retained_bytes_total = self.retained_bytes_total.saturating_add(expected as u64);
self.retained.insert(
handle,
RetainedImage {
w,
h,
format,
rgba: rgba[..expected].to_vec(),
last_used_frame: self.frame_index,
},
);
self.evict_retained_excess();
let tex = match self.images.get(&handle) {
Some(ImageTex::Rgba { tex, .. }) => tex,
_ => unreachable!(),
};
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&rgba[..expected],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * w),
rows_per_image: Some(h),
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
self.evict_budget_excess();
Ok(())
}
fn create_image_binds(
&self,
view: &wgpu::TextureView,
label: &str,
sampler: Option<&wgpu::Sampler>,
) -> ImageBinds {
let make = |name: &str, sampler: &wgpu::Sampler| {
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(name),
layout: &self.image_bind_layout_rgba,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
})
};
match sampler {
Some(sampler) => {
let bind = make(label, sampler);
ImageBinds {
linear: make(label, sampler),
nearest: bind.clone(),
tile_linear: bind.clone(),
tile_nearest: bind,
}
}
None => ImageBinds {
linear: make(label, &self.image_sampler),
nearest: make(label, &self.image_sampler_nearest),
tile_linear: make(label, &self.image_sampler_repeat),
tile_nearest: make(label, &self.image_sampler_repeat_nearest),
},
}
}
fn create_nv12_binds(
&self,
view_y: &wgpu::TextureView,
view_uv: &wgpu::TextureView,
yuv_buf: &wgpu::Buffer,
label: &str,
) -> ImageBinds {
let make = |name: &str, sampler: &wgpu::Sampler| {
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(name),
layout: &self.image_bind_layout_nv12,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view_y),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(view_uv),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: yuv_buf,
offset: 0,
size: None,
}),
},
],
})
};
ImageBinds {
linear: make(label, &self.image_sampler),
nearest: make(label, &self.image_sampler_nearest),
tile_linear: make(label, &self.image_sampler_repeat),
tile_nearest: make(label, &self.image_sampler_repeat_nearest),
}
}
fn create_rgba_tex(
&self,
w: u32,
h: u32,
format: wgpu::TextureFormat,
) -> (wgpu::Texture, ImageBinds) {
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("user image rgba"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let binds = self.create_image_binds(&view, "image bind rgba", None);
(tex, binds)
}
fn validate_native_texture_view(
&self,
view: &wgpu::TextureView,
width: u32,
height: u32,
) -> anyhow::Result<()> {
if width == 0 || height == 0 {
anyhow::bail!("native texture dimensions must be non-zero");
}
let texture = view.texture();
if texture.sample_count() != 1
|| texture.dimension() != wgpu::TextureDimension::D2
|| texture.depth_or_array_layers() != 1
|| texture.mip_level_count() == 0
{
anyhow::bail!("native texture must be a single-sample 2D texture with mip levels");
}
if width > self.device.limits().max_texture_dimension_2d
|| height > self.device.limits().max_texture_dimension_2d
{
anyhow::bail!("native texture dimensions exceed the device limit");
}
if !texture
.usage()
.contains(wgpu::TextureUsages::TEXTURE_BINDING)
{
anyhow::bail!("native texture lacks TEXTURE_BINDING usage");
}
if !native_format_supported(texture.format(), self.device.features()) {
anyhow::bail!(
"native texture format {:?} is not a filterable alpha color format",
texture.format()
);
}
if texture.width() < width || texture.height() < height {
anyhow::bail!(
"native texture is {}x{}, smaller than requested {width}x{height}",
texture.width(),
texture.height()
);
}
Ok(())
}
pub fn register_native_texture(
&mut self,
view: &wgpu::TextureView,
width: u32,
height: u32,
) -> u64 {
if let Err(error) = self.validate_native_texture_view(view, width, height) {
log::warn!("register_native_texture: {error:#}");
return 0;
}
let handle = self.next_image_handle;
self.next_image_handle += 1;
let binds = self.create_image_binds(view, "user native image", None);
self.images.insert(
handle,
ImageTex::User {
binds,
w: width,
h: height,
last_used_frame: self.frame_index,
bytes: 0,
},
);
handle
}
pub fn register_native_texture_with_sampler(
&mut self,
view: &wgpu::TextureView,
sampler_desc: wgpu::SamplerDescriptor<'_>,
width: u32,
height: u32,
) -> u64 {
if let Err(error) = self.validate_native_texture_view(view, width, height) {
log::warn!("register_native_texture: {error:#}");
return 0;
}
if let Err(error) = validate_sampler_descriptor(&self.device, &sampler_desc) {
log::warn!("register_native_texture: {error:#}");
return 0;
}
let handle = self.next_image_handle;
self.next_image_handle += 1;
let sampler = self.device.create_sampler(&sampler_desc);
let binds = self.create_image_binds(view, "user native image sampleropts", Some(&sampler));
self.images.insert(
handle,
ImageTex::User {
binds,
w: width,
h: height,
last_used_frame: self.frame_index,
bytes: 0,
},
);
handle
}
pub fn update_native_texture(&mut self, handle: u64, view: &wgpu::TextureView) {
if handle == 0 {
log::warn!("update_native_texture: reserved handle");
return;
}
let Some((w, h)) = self.images.get(&handle).and_then(|entry| match entry {
ImageTex::User { w, h, .. } => Some((*w, *h)),
_ => None,
}) else {
log::warn!("update_native_texture: handle {handle} is not a native image");
return;
};
if let Err(error) = self.validate_native_texture_view(view, w, h) {
log::warn!("update_native_texture: {error:#}");
return;
}
let binds = self.create_image_binds(view, "user native image update", None);
if let Some(entry) = self.images.get_mut(&handle) {
*entry = ImageTex::User {
binds,
w,
h,
last_used_frame: self.frame_index,
bytes: 0,
};
}
}
pub fn set_image_nv12(
&mut self,
handle: u64,
w: u32,
h: u32,
y: &[u8],
uv: &[u8],
color_info: ColorInfo,
) -> anyhow::Result<()> {
validate_image_handle(handle)?;
validate_texture_dimensions(&self.device, w, h)?;
if self
.images
.get(&handle)
.is_some_and(|image| matches!(image, ImageTex::Nv12 { external: true, .. }))
{
anyhow::bail!("cannot overwrite an external DMA-BUF image");
}
let y_expected = checked_image_bytes_usize(w, h, 1)?;
let uv_w = w.div_ceil(2);
let uv_h = h.div_ceil(2);
let uv_expected = checked_image_bytes_usize(uv_w, uv_h, 2)?;
if y.len() < y_expected {
return Err(anyhow::anyhow!("Y plane too small"));
}
if uv.len() < uv_expected {
return Err(anyhow::anyhow!("UV plane too small"));
}
let needs_recreate = match self.images.get(&handle) {
Some(ImageTex::Nv12 {
w: ww,
h: hh,
pixel_format,
..
}) => *ww != w || *hh != h || *pixel_format != PixelFormat::Nv12,
_ => true,
};
let yuv_raw = make_yuv_transform_raw(color_info, PixelFormat::Nv12);
if needs_recreate {
self.remove_image(handle);
let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("nv12 Y"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("nv12 UV"),
size: wgpu::Extent3d {
width: uv_w,
height: uv_h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("nv12 yuv transform"),
size: std::mem::size_of::<YuvTransformRaw>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue
.write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
let binds = self.create_nv12_binds(&view_y, &view_uv, &yuv_buf, "nv12 bind");
let bytes = checked_image_bytes(w, h, 1)?
.checked_add(checked_image_bytes(uv_w, uv_h, 2)?)
.and_then(|bytes| bytes.checked_add(std::mem::size_of::<YuvTransformRaw>() as u64))
.ok_or_else(|| anyhow::anyhow!("NV12 image byte size overflow"))?;
self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
self.images.insert(
handle,
ImageTex::Nv12 {
tex_y,
tex_uv,
binds,
yuv_buf,
w,
h,
pixel_format: PixelFormat::Nv12,
fourcc: Some(NV12_FOURCC),
color_info,
external: false,
last_used_frame: self.frame_index,
bytes,
},
);
} else {
if let Some(ImageTex::Nv12 {
yuv_buf,
color_info: stored_color,
last_used_frame,
..
}) = self.images.get_mut(&handle)
{
*stored_color = color_info;
*last_used_frame = self.frame_index;
self.queue
.write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
}
}
let (tex_y, tex_uv, _binds) = match self.images.get(&handle) {
Some(ImageTex::Nv12 {
tex_y,
tex_uv,
binds,
..
}) => (tex_y, tex_uv, binds),
_ => return Err(anyhow::anyhow!("Handle is not NV12")),
};
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: tex_y,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&y[..y_expected],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w),
rows_per_image: Some(h),
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: tex_uv,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&uv[..uv_expected],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(2 * uv_w),
rows_per_image: Some(uv_h),
},
wgpu::Extent3d {
width: uv_w,
height: uv_h,
depth_or_array_layers: 1,
},
);
self.evict_budget_excess();
Ok(())
}
pub fn set_image_planes(
&mut self,
handle: u64,
w: u32,
h: u32,
pixel_format: PixelFormat,
planes: &[&[u8]],
color_info: ColorInfo,
) -> anyhow::Result<()> {
validate_image_handle(handle)?;
if planes.len() != pixel_format.num_planes() as usize {
anyhow::bail!(
"{} requires {} planes, got {}",
match pixel_format {
PixelFormat::Nv12 => "NV12",
PixelFormat::P010 => "P010",
PixelFormat::I420 => "I420",
PixelFormat::I444 => "I444",
PixelFormat::Rgba => "RGBA",
},
pixel_format.num_planes(),
planes.len()
);
}
match pixel_format {
PixelFormat::Nv12 => {
let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
self.set_image_nv12(handle, w, h, y, uv, color_info)
}
PixelFormat::P010 => {
let y = planes.first().ok_or(anyhow::anyhow!("missing Y plane"))?;
let uv = planes.get(1).ok_or(anyhow::anyhow!("missing UV plane"))?;
self.set_image_p010(handle, w, h, y, uv, color_info)
}
PixelFormat::I420 | PixelFormat::I444 => Err(anyhow::anyhow!(
"I420/I444 not implemented and unlikely -> cheap to convert to NV12 (better for the GPU too)"
)),
PixelFormat::Rgba => {
let rgba = planes
.first()
.ok_or(anyhow::anyhow!("missing RGBA plane"))?;
self.set_image_rgba8(handle, w, h, rgba, false)
}
}
}
fn set_image_p010(
&mut self,
handle: u64,
w: u32,
h: u32,
y: &[u8],
uv: &[u8],
color_info: ColorInfo,
) -> anyhow::Result<()> {
validate_image_handle(handle)?;
validate_texture_dimensions(&self.device, w, h)?;
if self
.images
.get(&handle)
.is_some_and(|image| matches!(image, ImageTex::Nv12 { external: true, .. }))
{
anyhow::bail!("cannot overwrite an external DMA-BUF image");
}
if !self
.device
.features()
.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
{
anyhow::bail!("P010 requires TEXTURE_FORMAT_16BIT_NORM");
}
let uv_w = w.div_ceil(2);
let uv_h = h.div_ceil(2);
let y_expected = checked_image_bytes_usize(w, h, 2)?;
let uv_expected = checked_image_bytes_usize(uv_w, uv_h, 4)?;
if y.len() < y_expected {
return Err(anyhow::anyhow!("P010 Y plane too small"));
}
if uv.len() < uv_expected {
return Err(anyhow::anyhow!("P010 UV plane too small"));
}
let needs_recreate = match self.images.get(&handle) {
Some(ImageTex::Nv12 {
w: ww,
h: hh,
pixel_format,
..
}) => *ww != w || *hh != h || *pixel_format != PixelFormat::P010,
_ => true,
};
let yuv_raw = make_yuv_transform_raw(color_info, PixelFormat::P010);
if needs_recreate {
self.remove_image(handle);
let tex_y = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("p010 Y"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R16Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
let tex_uv = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("p010 UV"),
size: wgpu::Extent3d {
width: uv_w,
height: uv_h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg16Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("p010 yuv transform"),
size: std::mem::size_of::<YuvTransformRaw>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue
.write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
let binds = self.create_nv12_binds(&view_y, &view_uv, &yuv_buf, "p010 bind");
let bytes = checked_image_bytes(w, h, 2)?
.checked_add(checked_image_bytes(uv_w, uv_h, 4)?)
.and_then(|bytes| bytes.checked_add(std::mem::size_of::<YuvTransformRaw>() as u64))
.ok_or_else(|| anyhow::anyhow!("P010 image byte size overflow"))?;
self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
self.images.insert(
handle,
ImageTex::Nv12 {
tex_y,
tex_uv,
binds,
yuv_buf,
w,
h,
pixel_format: PixelFormat::P010,
fourcc: Some(P010_FOURCC),
color_info,
external: false,
last_used_frame: self.frame_index,
bytes,
},
);
} else {
if let Some(ImageTex::Nv12 {
yuv_buf,
color_info: stored_color,
last_used_frame,
..
}) = self.images.get_mut(&handle)
{
*stored_color = color_info;
*last_used_frame = self.frame_index;
self.queue
.write_buffer(yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
}
}
let (tex_y, tex_uv, _binds) = match self.images.get(&handle) {
Some(ImageTex::Nv12 {
tex_y,
tex_uv,
binds,
..
}) => (tex_y, tex_uv, binds),
_ => return Err(anyhow::anyhow!("Handle is not P010/NV12")),
};
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: tex_y,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&y[..y_expected],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(
w.checked_mul(2)
.ok_or_else(|| anyhow::anyhow!("P010 row size overflow"))?,
),
rows_per_image: Some(h),
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: tex_uv,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&uv[..uv_expected],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(
uv_w.checked_mul(4)
.ok_or_else(|| anyhow::anyhow!("P010 UV row size overflow"))?,
),
rows_per_image: Some(uv_h),
},
wgpu::Extent3d {
width: uv_w,
height: uv_h,
depth_or_array_layers: 1,
},
);
self.evict_budget_excess();
Ok(())
}
#[cfg(target_os = "linux")]
pub fn set_image_dmabuf(
&mut self,
handle: u64,
w: u32,
h: u32,
fds: Vec<std::os::unix::io::OwnedFd>,
modifier: u64,
strides: Vec<u32>,
offsets: Vec<u64>,
color_info: ColorInfo,
) -> anyhow::Result<()> {
self.set_image_dmabuf_fourcc(
handle,
w,
h,
fds,
NV12_FOURCC,
modifier,
strides,
offsets,
color_info,
)
}
#[cfg(target_os = "linux")]
pub fn set_image_dmabuf_fourcc(
&mut self,
handle: u64,
w: u32,
h: u32,
fds: Vec<std::os::unix::io::OwnedFd>,
fourcc: u32,
modifier: u64,
strides: Vec<u32>,
offsets: Vec<u64>,
color_info: ColorInfo,
) -> anyhow::Result<()> {
validate_image_handle(handle)?;
checked_image_bytes(w, h, 1)?;
if w == 0
|| h == 0
|| w > self.device.limits().max_texture_dimension_2d
|| h > self.device.limits().max_texture_dimension_2d
{
anyhow::bail!("DMA-BUF dimensions are outside the device texture limit");
}
if modifier == u64::MAX {
anyhow::bail!("DMA-BUF modifier is invalid");
}
if fds.len() != 2 || strides.len() != 2 || offsets.len() != 2 {
anyhow::bail!(
"DMA-BUF import requires two fds, strides, and offsets (got {}, {}, {})",
fds.len(),
strides.len(),
offsets.len()
);
}
let pixel_format = match fourcc {
0x3231_564e | 0x4e56_3132 => PixelFormat::Nv12,
0x3031_3050 | 0x5030_3130 => PixelFormat::P010,
_ => anyhow::bail!("unsupported DMA-BUF fourcc 0x{fourcc:08x}"),
};
if !self
.device
.features()
.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF)
{
anyhow::bail!("DMA-BUF import requires VULKAN_EXTERNAL_MEMORY_DMA_BUF");
}
if pixel_format == PixelFormat::P010
&& !self
.device
.features()
.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
{
anyhow::bail!("P010 DMA-BUF requires TEXTURE_FORMAT_16BIT_NORM");
}
let bytes_per_pixel = if pixel_format == PixelFormat::P010 {
2
} else {
1
};
let uv_w = w.div_ceil(2);
let uv_h = h.div_ceil(2);
let y_file_size =
validate_dmabuf_plane(&fds[0], offsets[0], strides[0], w, h, bytes_per_pixel)?;
let uv_file_size = validate_dmabuf_plane(
&fds[1],
offsets[1],
strides[1],
uv_w,
uv_h,
bytes_per_pixel * 2,
)?;
let yuv_raw = make_yuv_transform_raw(color_info, pixel_format);
let (y_format, uv_format) = if pixel_format == PixelFormat::P010 {
(
wgpu::TextureFormat::R16Unorm,
wgpu::TextureFormat::Rg16Unorm,
)
} else {
(wgpu::TextureFormat::R8Unorm, wgpu::TextureFormat::Rg8Unorm)
};
let hal_y_desc = wgpu::hal::TextureDescriptor {
label: Some("dmabuf y"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: y_format,
usage: wgpu::wgt::TextureUses::RESOURCE,
memory_flags: wgpu::hal::MemoryFlags::empty(),
view_formats: vec![],
};
let hal_uv_desc = wgpu::hal::TextureDescriptor {
label: Some("dmabuf uv"),
size: wgpu::Extent3d {
width: uv_w,
height: uv_h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: uv_format,
usage: wgpu::wgt::TextureUses::RESOURCE,
memory_flags: wgpu::hal::MemoryFlags::empty(),
view_formats: vec![],
};
let wgpu_y_desc = wgpu::TextureDescriptor {
label: Some("dmabuf y"),
size: hal_y_desc.size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: y_format,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
let wgpu_uv_desc = wgpu::TextureDescriptor {
label: Some("dmabuf uv"),
size: hal_uv_desc.size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: uv_format,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
let (tex_y, view_y, tex_uv, view_uv) = unsafe {
let hal_guard = self
.device
.as_hal::<wgpu::hal::vulkan::Api>()
.ok_or_else(|| anyhow::anyhow!("device is not Vulkan"))?;
let mut fds = fds.into_iter();
let y_fd = fds.next().expect("validated fd count");
let uv_fd = fds.next().expect("validated fd count");
let y_texture = hal_guard.texture_from_dmabuf_fd(
y_fd,
&hal_y_desc,
modifier,
strides[0] as u64,
offsets[0],
);
let y_texture =
y_texture.map_err(|error| anyhow::anyhow!("import Y DMA-BUF: {error:?}"))?;
let uv_texture = hal_guard.texture_from_dmabuf_fd(
uv_fd,
&hal_uv_desc,
modifier,
strides[1] as u64,
offsets[1],
);
let uv_texture =
uv_texture.map_err(|error| anyhow::anyhow!("import UV DMA-BUF: {error:?}"))?;
drop(hal_guard);
let tex_y = self
.device
.create_texture_from_hal::<wgpu::hal::vulkan::Api>(
y_texture,
&wgpu_y_desc,
wgpu::wgt::TextureUses::UNINITIALIZED,
);
let tex_uv = self
.device
.create_texture_from_hal::<wgpu::hal::vulkan::Api>(
uv_texture,
&wgpu_uv_desc,
wgpu::wgt::TextureUses::UNINITIALIZED,
);
let view_y = tex_y.create_view(&wgpu::TextureViewDescriptor::default());
let view_uv = tex_uv.create_view(&wgpu::TextureViewDescriptor::default());
(tex_y, view_y, tex_uv, view_uv)
};
let yuv_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("dmabuf yuv transform"),
size: std::mem::size_of::<YuvTransformRaw>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue
.write_buffer(&yuv_buf, 0, bytemuck::bytes_of(&yuv_raw));
let binds = self.create_nv12_binds(&view_y, &view_uv, &yuv_buf, "dmabuf yuv bind");
let bytes = y_file_size
.checked_add(uv_file_size)
.and_then(|value| value.checked_add(std::mem::size_of::<YuvTransformRaw>() as u64))
.ok_or_else(|| anyhow::anyhow!("DMA-BUF byte size overflow"))?;
self.evict_budget_excess();
let replaced_bytes = self
.images
.get(&handle)
.map(|image| match image {
ImageTex::Rgba { bytes, .. }
| ImageTex::Nv12 { bytes, .. }
| ImageTex::User { bytes, .. } => *bytes,
})
.unwrap_or(0);
let projected = self
.image_bytes_total
.saturating_sub(replaced_bytes)
.checked_add(bytes)
.ok_or_else(|| anyhow::anyhow!("DMA-BUF image budget overflow"))?;
if projected > self.image_budget_bytes {
anyhow::bail!("DMA-BUF image budget exceeded");
}
self.remove_image(handle);
self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
self.images.insert(
handle,
ImageTex::Nv12 {
tex_y,
tex_uv,
binds,
yuv_buf,
w,
h,
pixel_format,
fourcc: Some(canonical_fourcc(pixel_format)),
color_info,
external: true,
last_used_frame: self.frame_index,
bytes,
},
);
self.evict_budget_excess();
Ok(())
}
pub fn image_fourcc(&self, handle: u64) -> Option<u32> {
match self.images.get(&handle) {
Some(ImageTex::Nv12 { fourcc, .. }) => *fourcc,
_ => None,
}
}
pub fn remove_image(&mut self, handle: u64) {
if let Some(img) = self.images.remove(&handle) {
let b = match &img {
ImageTex::Rgba { bytes, .. } => *bytes,
ImageTex::Nv12 { bytes, .. } => *bytes,
ImageTex::User { bytes, .. } => *bytes,
};
self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
}
if let Some(retained) = self.retained.remove(&handle) {
self.retained_bytes_total = self
.retained_bytes_total
.saturating_sub(retained.rgba.len() as u64);
}
}
fn evict_image_gpu(&mut self, handle: u64) -> u64 {
let Some(img) = self.images.remove(&handle) else {
return 0;
};
let b = match &img {
ImageTex::Rgba { bytes, .. } => *bytes,
ImageTex::Nv12 { bytes, .. } => *bytes,
ImageTex::User { bytes, .. } => *bytes,
};
self.image_bytes_total = self.image_bytes_total.saturating_sub(b);
b
}
fn revive_retained_image(&mut self, handle: u64) -> anyhow::Result<bool> {
if self.images.contains_key(&handle) {
return Ok(true);
}
let Some(r) = self.retained.get_mut(&handle) else {
return Ok(false);
};
r.last_used_frame = self.frame_index;
let r = r.clone();
let (tex, binds) = self.create_rgba_tex(r.w, r.h, r.format);
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&r.rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * r.w),
rows_per_image: Some(r.h),
},
wgpu::Extent3d {
width: r.w,
height: r.h,
depth_or_array_layers: 1,
},
);
let bytes = checked_image_bytes(r.w, r.h, 4)?;
self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
self.images.insert(
handle,
ImageTex::Rgba {
tex,
binds,
w: r.w,
h: r.h,
format: r.format,
last_used_frame: self.frame_index,
bytes,
},
);
Ok(true)
}
fn resolve_image_for_draw(&mut self, handle: u64) -> Option<(u32, u32, bool)> {
if let Some(retained) = self.retained.get_mut(&handle) {
retained.last_used_frame = self.frame_index;
}
if let Some(t) = self.images.get_mut(&handle) {
return match t {
ImageTex::Rgba {
w,
h,
last_used_frame,
..
} => {
*last_used_frame = self.frame_index;
Some((*w, *h, false))
}
ImageTex::User {
w,
h,
last_used_frame,
..
} => {
*last_used_frame = self.frame_index;
Some((*w, *h, false))
}
ImageTex::Nv12 {
w,
h,
last_used_frame,
..
} => {
*last_used_frame = self.frame_index;
Some((*w, *h, true))
}
};
}
if self.revive_retained_image(handle).unwrap_or(false)
&& let Some(ImageTex::Rgba {
w,
h,
last_used_frame,
..
}) = self.images.get_mut(&handle)
{
*last_used_frame = self.frame_index;
return Some((*w, *h, false));
}
None
}
pub fn register_image_from_bytes(&mut self, data: &[u8], srgb: bool) -> u64 {
let handle = self.next_image_handle;
self.next_image_handle += 1;
if let Err(e) = self.set_image_from_bytes(handle, data, srgb) {
log::error!("Failed to register image: {e}");
}
handle
}
pub fn register_image_rgba8(&mut self, w: u32, h: u32, rgba: &[u8], srgb: bool) -> u64 {
let handle = self.next_image_handle;
self.next_image_handle += 1;
if let Err(e) = self.set_image_rgba8(handle, w, h, rgba, srgb) {
log::error!("Failed to register image: {e}");
}
handle
}
pub fn register_coverage_a8(&mut self, w: u32, h: u32, coverage: &[u8]) -> u64 {
if validate_texture_dimensions(&self.device, w, h).is_err() {
log::error!("Coverage dimensions are outside the device limit");
return 0;
}
let Ok(expected_bytes) = checked_image_bytes(w, h, 1) else {
log::error!("Coverage dimensions overflow");
return 0;
};
let Ok(expected) = usize::try_from(expected_bytes) else {
log::error!("Coverage dimensions exceed addressable memory");
return 0;
};
if coverage.len() < expected {
log::error!("Coverage buffer too small: {} < {expected}", coverage.len());
return 0;
}
let handle = self.next_coverage_handle;
self.next_coverage_handle += 1;
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("coverage tile a8"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("coverage bind a8"),
layout: &self.image_bind_layout_rgba,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.image_sampler),
},
],
});
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&coverage[..expected],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w),
rows_per_image: Some(h),
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
let bytes = checked_image_bytes(w, h, 1).unwrap_or(u64::MAX);
self.image_bytes_total = self.image_bytes_total.saturating_add(bytes);
self.coverages.insert(
handle,
CoverageTex {
tex,
bind,
w,
h,
last_used_frame: self.frame_index,
bytes,
},
);
self.evict_budget_excess();
handle
}
pub fn remove_coverage(&mut self, handle: u64) {
if let Some(tile) = self.coverages.remove(&handle) {
self.image_bytes_total = self.image_bytes_total.saturating_sub(tile.bytes);
}
}
pub fn coverage_dimensions(&mut self, handle: u64) -> Option<(u32, u32)> {
if let Some(tile) = self.coverages.get_mut(&handle) {
tile.last_used_frame = self.frame_index;
return Some((tile.w, tile.h));
}
None
}
fn evict_retained_excess(&mut self) {
let mut candidates: Vec<(u64, u64, u64)> = self
.retained
.iter()
.map(|(handle, image)| (*handle, image.last_used_frame, image.rgba.len() as u64))
.collect();
candidates.sort_by_key(|candidate| candidate.1);
for (handle, _last_used, bytes) in candidates {
if self.retained.len() <= MAX_RETAINED_IMAGES
&& self.retained_bytes_total <= MAX_RETAINED_IMAGE_BYTES
{
break;
}
if let Some(image) = self.retained.remove(&handle) {
self.retained_bytes_total = self
.retained_bytes_total
.saturating_sub(bytes.min(image.rgba.len() as u64));
}
}
}
fn evict_unused_images(&mut self) {
let now = self.frame_index;
let evict_after = self.image_evict_after_frames;
let mut to_evict = Vec::new();
for (h, t) in self.images.iter() {
if !t.evictable() {
continue;
}
let last = match t {
ImageTex::Rgba {
last_used_frame, ..
} => *last_used_frame,
ImageTex::User {
last_used_frame, ..
} => *last_used_frame,
ImageTex::Nv12 {
last_used_frame, ..
} => *last_used_frame,
};
if now.saturating_sub(last) > evict_after {
to_evict.push(*h);
}
}
for h in to_evict {
if self.retained.contains_key(&h) {
self.evict_image_gpu(h);
} else {
self.remove_image(h);
}
}
let mut stale = Vec::new();
for (h, t) in self.coverages.iter() {
if now.saturating_sub(t.last_used_frame) > evict_after {
stale.push(*h);
}
}
for h in stale {
self.remove_coverage(h);
}
self.evict_budget_excess();
}
fn next_cache_touch(&mut self) -> u64 {
let touch = self.cache_touch_clock;
self.cache_touch_clock = self.cache_touch_clock.wrapping_add(1);
touch
}
fn gpu_bytes_total(&self) -> u64 {
self.image_bytes_total
.saturating_add(self.layer_bytes_total)
.saturating_add(self.blend_snapshot_bytes_total)
.saturating_add(self.mesh_cache_bytes_total)
.saturating_add(self.working_space_bytes)
.saturating_add(self.surface_resolve_bytes)
.saturating_add(self.msaa_bytes)
.saturating_add(self.ws_msaa_bytes)
.saturating_add(self.depth_stencil_bytes)
}
fn budget_exceeded(&self) -> bool {
self.image_bytes_total > self.image_budget_bytes
|| self.gpu_bytes_total() > self.gpu_budget_bytes
}
fn evict_budget_excess(&mut self) {
let image_over_budget = self.image_bytes_total > self.image_budget_bytes;
let gpu_over_budget = self.gpu_bytes_total() > self.gpu_budget_bytes;
if !image_over_budget && !gpu_over_budget {
return;
}
let mut coverage_candidates: Vec<(u64, u64)> = self
.coverages
.iter()
.map(|(handle, tile)| (*handle, tile.last_used_frame))
.collect();
coverage_candidates.sort_by_key(|candidate| candidate.1);
let now = self.frame_index;
for (handle, last_used) in coverage_candidates {
if !self.budget_exceeded() {
break;
}
if last_used == now {
continue;
}
self.remove_coverage(handle);
}
if !self.budget_exceeded() {
return;
}
let mut candidates: Vec<(u64, u64, u64)> = self
.images
.iter()
.filter_map(|(h, t)| {
if !t.evictable() {
return None;
}
let (last, bytes) = match t {
ImageTex::Rgba {
last_used_frame,
bytes,
..
} => (*last_used_frame, *bytes),
ImageTex::User {
last_used_frame,
bytes,
..
} => (*last_used_frame, *bytes),
ImageTex::Nv12 {
last_used_frame,
bytes,
..
} => (*last_used_frame, *bytes),
};
Some((*h, last, bytes))
})
.collect();
candidates.sort_by_key(|k| k.1);
for (h, last, _bytes) in candidates {
if !self.budget_exceeded() {
break;
}
if last == now {
continue;
}
if self.retained.contains_key(&h) {
self.evict_image_gpu(h);
} else {
self.remove_image(h);
}
}
while self.gpu_bytes_total() > self.gpu_budget_bytes {
if !self.evict_mesh_cache() {
break;
}
}
if self.gpu_bytes_total() > self.gpu_budget_bytes {
log::warn!(
"renderer GPU resource budget exceeded: {} > {}",
self.gpu_bytes_total(),
self.gpu_budget_bytes
);
}
}
pub fn set_pixels_per_point(&mut self, ppp: f32) {
if ppp.is_finite() {
self.pixels_per_point = ppp.clamp(0.5, 8.0);
}
}
pub fn set_working_space(&mut self, enabled: bool) {
if enabled == self.working_space {
return;
}
self.working_space = enabled;
if enabled {
self.ensure_display_pipeline();
self.recreate_msaa_and_depth_stencil();
self.recreate_working_space_texture();
} else {
self.ws_tex = None;
self.ws_view = None;
self.ws_bind = None;
self.ws_msaa_tex = None;
self.ws_msaa_view = None;
self.surface_resolve_tex = None;
self.surface_resolve_view = None;
self.surface_resolve_bytes = 0;
self.working_space_bytes = 0;
self.recreate_msaa_and_depth_stencil();
}
self.evict_budget_excess();
}
fn ensure_display_pipeline(&mut self) {
if self.display_pipeline.is_some() {
return;
}
let layout = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("display transform layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
self.display_layout = Some(layout);
let display_source: Cow<'static, str> = if self.output_format.is_srgb() {
Cow::Borrowed(include_str!("shaders/display_transform_passthrough.wgsl"))
} else {
Cow::Borrowed(include_str!("shaders/display_transform.wgsl"))
};
let shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("display transform"),
source: wgpu::ShaderSource::Wgsl(display_source),
});
let pipeline_layout = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("display transform pipeline layout"),
bind_group_layouts: &[None, self.display_layout.as_ref()],
immediate_size: 0,
});
let pipeline = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("display transform pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: self.output_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
self.display_pipeline = Some(pipeline);
}
pub fn resize(&mut self, width: u32, height: u32) {
let max = self.device.limits().max_texture_dimension_2d;
let width = if width == 0 { 0 } else { width.min(max) };
let height = if height == 0 { 0 } else { height.min(max) };
if self.output_width == width && self.output_height == height {
return;
}
self.output_width = width;
self.output_height = height;
if width == 0 || height == 0 {
self.msaa_tex = None;
self.msaa_view = None;
self.ws_msaa_tex = None;
self.ws_msaa_view = None;
self.surface_resolve_tex = None;
self.surface_resolve_view = None;
self.surface_resolve_bytes = 0;
self.msaa_bytes = 0;
self.ws_msaa_bytes = 0;
self.depth_stencil_bytes = 0;
self.working_space_bytes = 0;
self.ws_tex = None;
self.ws_view = None;
self.ws_bind = None;
self.evict_budget_excess();
return;
}
self.recreate_msaa_and_depth_stencil();
self.recreate_working_space_texture();
self.evict_budget_excess();
}
fn recreate_working_space_texture(&mut self) {
if !self.working_space || self.output_width == 0 || self.output_height == 0 {
return;
}
self.working_space_bytes = 0;
let w = self
.output_width
.clamp(1, self.device.limits().max_texture_dimension_2d);
let h = self
.output_height
.clamp(1, self.device.limits().max_texture_dimension_2d);
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("working space"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba16Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("working space bind"),
layout: self.display_layout.as_ref().unwrap(),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.image_sampler),
},
],
});
self.working_space_bytes =
texture_storage_bytes(wgpu::TextureFormat::Rgba16Float, w, h).unwrap_or(0);
self.ws_tex = Some(tex);
self.ws_view = Some(view);
self.ws_bind = Some(bind);
}
fn recreate_msaa_and_depth_stencil(&mut self) {
if self.output_width == 0 || self.output_height == 0 {
self.msaa_tex = None;
self.msaa_view = None;
self.ws_msaa_tex = None;
self.ws_msaa_view = None;
self.surface_resolve_tex = None;
self.surface_resolve_view = None;
self.surface_resolve_bytes = 0;
self.msaa_bytes = 0;
self.ws_msaa_bytes = 0;
self.depth_stencil_bytes = 0;
return;
}
self.msaa_bytes = 0;
self.ws_msaa_bytes = 0;
self.depth_stencil_bytes = 0;
if self.msaa_samples > 1 && !self.working_space {
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("msaa color"),
size: wgpu::Extent3d {
width: self.output_width.max(1),
height: self.output_height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.msaa_samples,
dimension: wgpu::TextureDimension::D2,
format: self.output_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
self.msaa_bytes = texture_storage_bytes_with_samples(
self.output_format,
self.output_width.max(1),
self.output_height.max(1),
self.msaa_samples,
)
.unwrap_or(0);
self.msaa_tex = Some(tex);
self.msaa_view = Some(view);
} else {
self.msaa_tex = None;
self.msaa_view = None;
}
if self.working_space && self.working_space_msaa_samples > 1 {
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("working space msaa color"),
size: wgpu::Extent3d {
width: self.output_width.max(1),
height: self.output_height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.working_space_msaa_samples,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba16Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
self.ws_msaa_bytes = texture_storage_bytes_with_samples(
wgpu::TextureFormat::Rgba16Float,
self.output_width.max(1),
self.output_height.max(1),
self.working_space_msaa_samples,
)
.unwrap_or(0);
self.ws_msaa_tex = Some(tex);
self.ws_msaa_view = Some(view);
} else {
self.ws_msaa_tex = None;
self.ws_msaa_view = None;
}
self.surface_resolve_tex = None;
self.surface_resolve_view = None;
self.surface_resolve_bytes = 0;
if self.msaa_samples > 1 && !self.working_space {
let width = self.output_width.max(1);
let height = self.output_height.max(1);
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("surface resolve"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.output_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
self.surface_resolve_bytes =
texture_storage_bytes(self.output_format, width, height).unwrap_or(0);
self.surface_resolve_tex = Some(tex);
self.surface_resolve_view = Some(view);
}
self.depth_stencil_bytes = texture_storage_bytes_with_samples(
wgpu::TextureFormat::Depth24PlusStencil8,
self.output_width.max(1),
self.output_height.max(1),
self.active_surface_msaa_samples(),
)
.unwrap_or(0);
self.depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("depth-stencil (stencil clips)"),
size: wgpu::Extent3d {
width: self.output_width.max(1),
height: self.output_height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.active_surface_msaa_samples(),
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth24PlusStencil8,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
self.depth_stencil_view = self
.depth_stencil_tex
.create_view(&wgpu::TextureViewDescriptor::default());
}
fn active_surface_msaa_samples(&self) -> u32 {
if self.working_space {
self.working_space_msaa_samples
} else {
self.msaa_samples
}
}
fn layer_target_format(&self) -> wgpu::TextureFormat {
if self.working_space {
wgpu::TextureFormat::Rgba16Float
} else {
self.output_format
}
}
fn get_or_create_layer(
&mut self,
layer_id: u32,
width: u32,
height: u32,
rect: repose_core::Rect,
transient: bool,
) -> bool {
if width == 0
|| height == 0
|| width > self.device.limits().max_texture_dimension_2d
|| height > self.device.limits().max_texture_dimension_2d
{
return false;
}
let needs_alloc = match self.layer_pool.get(&layer_id) {
Some(lt) => {
lt.width != width || lt.height != height || lt.format != self.layer_target_format()
}
None => true,
};
if !needs_alloc {
if let Some(lt) = self.layer_pool.get_mut(&layer_id) {
lt.rect_px = (rect.x, rect.y, rect.w, rect.h);
}
return true;
}
let color_bytes =
texture_storage_bytes(self.layer_target_format(), width.max(1), height.max(1))
.unwrap_or(MAX_GRAPHICS_LAYER_BYTES + 1);
let depth_bytes = checked_image_bytes(width.max(1), height.max(1), 4)
.unwrap_or(MAX_GRAPHICS_LAYER_BYTES + 1);
let bytes = color_bytes.saturating_add(depth_bytes);
let old = self.layer_pool.get(&layer_id);
let old_bytes = old.map_or(0, |layer| layer.bytes);
let old_transient_bytes =
old.map_or(0, |layer| if layer.transient { layer.bytes } else { 0 });
let transient_count = self
.layer_pool
.values()
.filter(|layer| layer.transient)
.count();
let transient_count = transient_count
.saturating_sub(usize::from(old.is_some_and(|layer| layer.transient)))
.saturating_add(usize::from(transient));
let count = self.layer_pool.len() + usize::from(!self.layer_pool.contains_key(&layer_id));
let total = self
.layer_bytes_total
.saturating_sub(old_bytes)
.saturating_add(bytes);
let projected_gpu = self
.gpu_bytes_total()
.saturating_sub(old_bytes)
.checked_add(bytes)
.unwrap_or(u64::MAX);
let projected_transient = self
.transient_layer_bytes_total
.saturating_sub(old_transient_bytes)
.saturating_add(if transient { bytes } else { 0 });
if count > MAX_GRAPHICS_LAYERS
|| (transient
&& (transient_count > MAX_TRANSIENT_GRAPHICS_LAYERS
|| projected_transient > MAX_TRANSIENT_GRAPHICS_LAYER_BYTES))
|| bytes > MAX_GRAPHICS_LAYER_BYTES
|| total > MAX_GRAPHICS_LAYER_BYTES
|| projected_gpu > self.gpu_budget_bytes
{
log::warn!("graphics layer budget exhausted; layer {layer_id} skipped");
if let Some(old) = self.layer_pool.remove(&layer_id) {
self.layer_bytes_total = self.layer_bytes_total.saturating_sub(old.bytes);
if old.transient {
self.transient_layer_bytes_total =
self.transient_layer_bytes_total.saturating_sub(old.bytes);
}
}
return false;
}
if let Some(old) = self.layer_pool.remove(&layer_id) {
self.layer_bytes_total = self.layer_bytes_total.saturating_sub(old.bytes);
if old.transient {
self.transient_layer_bytes_total =
self.transient_layer_bytes_total.saturating_sub(old.bytes);
}
}
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("graphics layer"),
size: wgpu::Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.layer_target_format(),
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("layer bind"),
layout: &self.image_bind_layout_rgba,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.layer_sampler),
},
],
});
let bind_linear = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("layer bind linear"),
layout: &self.image_bind_layout_rgba,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.layer_sampler_linear),
},
],
});
let depth_stencil_tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("graphics layer depth-stencil"),
size: wgpu::Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth24PlusStencil8,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let depth_stencil_view =
depth_stencil_tex.create_view(&wgpu::TextureViewDescriptor::default());
self.layer_pool.insert(
layer_id,
LayerTarget {
texture: tex,
view,
bind,
bind_linear,
depth_stencil_view,
width,
height,
format: self.layer_target_format(),
bytes,
rect_px: (rect.x, rect.y, rect.w, rect.h),
transient,
},
);
self.layer_bytes_total = self.layer_bytes_total.saturating_add(bytes);
if transient {
self.transient_layer_bytes_total =
self.transient_layer_bytes_total.saturating_add(bytes);
}
true
}
fn ensure_globals_staging(&mut self, pass_count: usize) -> bool {
let size = std::mem::size_of::<Globals>() as u64;
let needed = match (pass_count.max(1) as u64).checked_mul(size) {
Some(needed) => needed,
None => return false,
};
if needed <= self.globals_staging_cap {
return true;
}
let cap = needed
.next_power_of_two()
.min(self.device.limits().max_buffer_size);
if cap < needed {
return false;
}
self.globals_staging = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("globals staging"),
size: cap,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
self.globals_staging_cap = cap;
true
}
fn reset_atlas_storage(&mut self) {
self.atlas_mask = init_atlas_mask(&self.device);
self.atlas_color = init_atlas_color(&self.device);
self.atlas_mask_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("atlas mask bind"),
layout: &self.text_bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
},
],
});
self.atlas_color_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("atlas color bind"),
layout: &self.text_bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
},
],
});
self.atlas_mask_pending = AtlasPending::default();
self.atlas_color_pending = AtlasPending::default();
self.atlas_mask_deferred.clear();
self.atlas_color_deferred.clear();
}
fn apply_atlas_pending(&mut self) {
if self.atlas_mask_pending.grow {
self.atlas_mask_pending.grow = false;
let size = (self.atlas_mask.size * 2).min(MAX_GLYPH_ATLAS_SIZE);
self.repack_mask(size);
}
if self.atlas_color_pending.grow {
self.atlas_color_pending.grow = false;
let size = (self.atlas_color.size * 2).min(MAX_GLYPH_ATLAS_SIZE);
self.repack_color(size);
}
if self.frame_index.wrapping_sub(self.atlas_last_compact_frame)
< ATLAS_COMPACT_MIN_FRAME_INTERVAL
{
return;
}
if self.atlas_mask_pending.compact && self.compact_atlas_mask() {
self.atlas_mask_pending.compact = false;
}
if self.atlas_color_pending.compact && self.compact_atlas_color() {
self.atlas_color_pending.compact = false;
}
}
fn compact_atlas_mask(&mut self) -> bool {
let target = self.atlas_mask.used_area / ATLAS_COMPACT_KEEP_DEN * ATLAS_COMPACT_KEEP_NUM;
if !evict_atlas_lru(
&mut self.atlas_mask.map,
&mut self.atlas_mask.used_area,
self.frame_index,
target,
) {
return false;
}
let size = self.atlas_mask.size;
self.repack_mask(size);
self.atlas_last_compact_frame = self.frame_index;
true
}
fn compact_atlas_color(&mut self) -> bool {
let target = self.atlas_color.used_area / ATLAS_COMPACT_KEEP_DEN * ATLAS_COMPACT_KEEP_NUM;
if !evict_atlas_lru(
&mut self.atlas_color.map,
&mut self.atlas_color.used_area,
self.frame_index,
target,
) {
return false;
}
let size = self.atlas_color.size;
self.repack_color(size);
self.atlas_last_compact_frame = self.frame_index;
true
}
fn repack_mask(&mut self, new_size: u32) {
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("glyph atlas A8 (repacked)"),
size: wgpu::Extent3d {
width: new_size,
height: new_size,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
self.atlas_mask.tex = tex;
self.atlas_mask.view = self
.atlas_mask
.tex
.create_view(&wgpu::TextureViewDescriptor::default());
self.atlas_mask.size = new_size;
self.atlas_mask.next_x = 1;
self.atlas_mask.next_y = 1;
self.atlas_mask.row_h = 0;
self.atlas_mask.used_area = 0;
self.atlas_mask_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("atlas mask bind"),
layout: &self.text_bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.atlas_mask.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.atlas_mask.sampler),
},
],
});
let survivors: Vec<_> = self.atlas_mask.map.drain().collect();
self.atlas_mask_deferred.clear();
for (key, previous) in survivors {
let _ = self.upload_glyph_mask(key, f32::from_bits(key.font_size_bits));
if let Some(info) = self.atlas_mask.map.get_mut(&key) {
info.last_touch = previous.last_touch;
info.last_used_frame = previous.last_used_frame;
}
}
}
fn repack_color(&mut self, new_size: u32) {
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("glyph atlas RGBA (repacked)"),
size: wgpu::Extent3d {
width: new_size,
height: new_size,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
self.atlas_color.tex = tex;
self.atlas_color.view = self
.atlas_color
.tex
.create_view(&wgpu::TextureViewDescriptor::default());
self.atlas_color.size = new_size;
self.atlas_color.next_x = 1;
self.atlas_color.next_y = 1;
self.atlas_color.row_h = 0;
self.atlas_color.used_area = 0;
self.atlas_color_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("atlas color bind"),
layout: &self.text_bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.atlas_color.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.atlas_color.sampler),
},
],
});
let survivors: Vec<_> = self.atlas_color.map.drain().collect();
self.atlas_color_deferred.clear();
for (key, previous) in survivors {
let _ = self.upload_glyph_color(key, f32::from_bits(key.font_size_bits));
if let Some(info) = self.atlas_color.map.get_mut(&key) {
info.last_touch = previous.last_touch;
info.last_used_frame = previous.last_used_frame;
}
}
}
fn sync_text_caches(&mut self) {
let generation = repose_text::font_generation();
if generation == self.atlas_generation {
return;
}
self.atlas_generation = generation;
self.reset_atlas_storage();
self.atlas_mask_failures.clear();
self.atlas_color_failures.clear();
self.glyph_outline_cache.clear();
self.legacy_shaped_text_cache.clear();
self.slug_draw_cache.clear();
self.slug_draw_cache_bytes_total = 0;
self.slug_cache = slug::GlyphSlugCache::new();
}
fn shape_text_cached(
&mut self,
text: &Arc<str>,
px: f32,
line_height_ratio: f32,
font_family: Option<&'static str>,
font_weight: u16,
font_style: u8,
letter_spacing: f32,
variation: Option<&Arc<str>>,
) -> Arc<repose_text::SharedShapedText> {
repose_text::shape_line_cached(
text.as_ref(),
px,
line_height_ratio,
font_family,
font_weight,
font_style,
letter_spacing,
variation.map(AsRef::as_ref),
)
}
fn legacy_shape_text_cached(
&mut self,
text: &Arc<str>,
px: f32,
line_height_ratio: f32,
font_family: Option<&'static str>,
font_weight: u16,
font_style: u8,
letter_spacing: f32,
variation: Option<&Arc<str>>,
) -> Arc<[repose_text::ShapedGlyph]> {
let key = TextShapeKey {
text: text.clone(),
px_bits: px.to_bits(),
line_height_bits: line_height_ratio.to_bits(),
font_family,
font_weight,
font_style,
letter_spacing_bits: letter_spacing.to_bits(),
variation: variation.cloned(),
font_generation: self.atlas_generation,
};
if let Some(entry) = self.legacy_shaped_text_cache.get_mut(&key) {
entry.last_used_frame = self.frame_index;
return entry.glyphs.clone();
}
let glyphs: Arc<[repose_text::ShapedGlyph]> = Arc::from(repose_text::shape_line(
text.as_ref(),
px,
line_height_ratio,
font_family,
font_weight,
font_style,
letter_spacing,
variation.map(AsRef::as_ref),
));
self.legacy_shaped_text_cache.insert(
key,
TextShapeCacheEntry {
glyphs: glyphs.clone(),
last_used_frame: self.frame_index,
},
);
while self.legacy_shaped_text_cache.len() > MAX_LEGACY_SHAPED_TEXT_CACHE_ENTRIES {
let Some(victim) = self
.legacy_shaped_text_cache
.iter()
.min_by_key(|(_, entry)| entry.last_used_frame)
.map(|(key, _)| key.clone())
else {
break;
};
self.legacy_shaped_text_cache.remove(&victim);
}
glyphs
}
fn slug_draw_key(
cache_key: repose_text::CacheKey,
fill: bool,
stroke_key: Option<&slug::StrokeTessKey>,
transform: &Transform,
origin: (f32, f32),
px: f32,
target: (f32, f32),
color: [f32; 4],
) -> SlugDrawKey {
let linear = transform.linear();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
stroke_key.hash(&mut hasher);
SlugDrawKey {
cache_key,
fill,
stroke_hash: hasher.finish(),
transform: [
linear[0].to_bits(),
linear[1].to_bits(),
linear[2].to_bits(),
linear[3].to_bits(),
transform.scale_x.to_bits(),
transform.scale_y.to_bits(),
transform.translate_x.to_bits(),
transform.translate_y.to_bits(),
transform.rotate.to_bits(),
transform.shear_x.to_bits(),
transform.shear_y.to_bits(),
],
origin: [origin.0.to_bits(), origin.1.to_bits()],
px_bits: px.to_bits(),
target: [target.0.to_bits(), target.1.to_bits()],
color: color.map(f32::to_bits),
}
}
fn evict_slug_draw_cache(&mut self) -> bool {
let victim = self
.slug_draw_cache
.iter()
.filter(|(_, entry)| entry.last_used_frame != self.frame_index)
.min_by_key(|(_, entry)| entry.last_touch_frame)
.map(|(key, _)| *key);
let Some(victim) = victim else {
return false;
};
let Some(entry) = self.slug_draw_cache.remove(&victim) else {
return false;
};
self.slug_draw_cache_bytes_total =
self.slug_draw_cache_bytes_total.saturating_sub(entry.bytes);
true
}
fn reserve_slug_draw_cache(&mut self, bytes: u64) -> bool {
while self.slug_draw_cache.len() >= MAX_SLUG_DRAW_CACHE_ENTRIES
|| self.slug_draw_cache_bytes_total.saturating_add(bytes) > MAX_SLUG_DRAW_CACHE_BYTES
{
if !self.evict_slug_draw_cache() {
return false;
}
}
true
}
fn get_cached_slug_vertices(&mut self, key: &SlugDrawKey) -> Option<Arc<[slug::TessVertex]>> {
let touch = self.next_cache_touch();
self.slug_draw_cache.get_mut(key).map(|entry| {
entry.last_used_frame = self.frame_index;
entry.last_touch_frame = touch;
entry.vertices.clone()
})
}
fn cached_slug_vertices(
&mut self,
key: SlugDrawKey,
vertices: Vec<slug::TessVertex>,
) -> Arc<[slug::TessVertex]> {
let touch = self.next_cache_touch();
if let Some(entry) = self.slug_draw_cache.get_mut(&key) {
entry.last_used_frame = self.frame_index;
entry.last_touch_frame = touch;
return entry.vertices.clone();
}
let bytes = slug_draw_cache_entry_bytes(vertices.len());
if bytes > MAX_SLUG_DRAW_CACHE_BYTES || !self.reserve_slug_draw_cache(bytes) {
return Arc::from(vertices);
}
let touch = self.next_cache_touch();
let vertices: Arc<[slug::TessVertex]> = Arc::from(vertices);
self.slug_draw_cache.insert(
key,
SlugDrawCacheEntry {
vertices: vertices.clone(),
bytes,
last_used_frame: self.frame_index,
last_touch_frame: touch,
},
);
self.slug_draw_cache_bytes_total = self.slug_draw_cache_bytes_total.saturating_add(bytes);
vertices
}
fn cached_glyph_outline(
&mut self,
cache_key: repose_text::CacheKey,
) -> Option<Arc<[repose_text::Command]>> {
let touch = self.next_cache_touch();
if let Some(entry) = self.glyph_outline_cache.get_mut(&cache_key) {
entry.last_touch = touch;
return entry.commands.clone();
}
let commands = repose_text::extract_outline_commands(cache_key).map(Arc::from);
if self.glyph_outline_cache.len() >= MAX_GLYPH_OUTLINE_CACHE_ENTRIES {
self.trim_glyph_outline_cache();
}
self.glyph_outline_cache.insert(
cache_key,
GlyphOutlineEntry {
commands: commands.clone(),
last_touch: touch,
},
);
commands
}
fn trim_glyph_outline_cache(&mut self) {
let target = (MAX_GLYPH_OUTLINE_CACHE_ENTRIES / ATLAS_COMPACT_KEEP_DEN as usize)
* ATLAS_COMPACT_KEEP_NUM as usize;
let mut victims: Vec<(u64, repose_text::CacheKey)> = self
.glyph_outline_cache
.iter()
.map(|(key, entry)| (entry.last_touch, *key))
.collect();
victims.sort_unstable_by_key(|victim| victim.0);
for (_, key) in victims {
if self.glyph_outline_cache.len() <= target {
break;
}
self.glyph_outline_cache.remove(&key);
}
}
fn atlas_bind_group_mask(&self) -> wgpu::BindGroup {
self.atlas_mask_bind.clone()
}
fn atlas_bind_group_color(&self) -> wgpu::BindGroup {
self.atlas_color_bind.clone()
}
fn mark_atlas_mask_failure(&mut self, key: repose_text::CacheKey) {
if self.atlas_mask_failures.len() >= MAX_GLYPH_ATLAS_ENTRIES {
self.atlas_mask_failures.clear();
}
self.atlas_mask_failures.insert(key);
}
fn mark_atlas_color_failure(&mut self, key: repose_text::CacheKey) {
if self.atlas_color_failures.len() >= MAX_GLYPH_ATLAS_ENTRIES {
self.atlas_color_failures.clear();
}
self.atlas_color_failures.insert(key);
}
fn upload_glyph_mask(
&mut self,
cache_key: repose_text::CacheKey,
px: f32,
) -> Option<GlyphInfo> {
let px = quantize_glyph_px(px);
let keyp = repose_text::CacheKey {
font_size_bits: px.to_bits(),
..cache_key
};
let touch = self.next_cache_touch();
if let Some(info) = self.atlas_mask.map.get_mut(&keyp) {
info.last_touch = touch;
info.last_used_frame = self.frame_index;
return Some(*info);
}
if self.atlas_mask_failures.contains(&keyp) || self.atlas_mask_deferred.contains(&keyp) {
return None;
}
let Some(gb) = repose_text::rasterize_cache_key(keyp) else {
self.mark_atlas_mask_failure(keyp);
return None;
};
if gb.w == 0 || gb.h == 0 || gb.data.is_empty() {
self.mark_atlas_mask_failure(keyp);
return None;
}
let Some(coverage) = swash_to_a8_coverage(gb.content, &gb.data) else {
self.mark_atlas_mask_failure(keyp);
return None;
};
let w = gb.w.max(1);
let h = gb.h.max(1);
let expected = checked_image_bytes_usize(w, h, 1).ok()?;
if coverage.len() < expected {
self.mark_atlas_mask_failure(keyp);
return None;
}
if w >= MAX_GLYPH_ATLAS_SIZE || h >= MAX_GLYPH_ATLAS_SIZE {
self.mark_atlas_mask_failure(keyp);
return None;
}
if !self.alloc_or_schedule_mask(w, h) {
defer_atlas_glyph(&mut self.atlas_mask_deferred, keyp);
return None;
}
let x = self.atlas_mask.next_x;
let y = self.atlas_mask.next_y;
self.atlas_mask.next_x += w + 1;
self.atlas_mask.row_h = self.atlas_mask.row_h.max(h + 1);
let layout = wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w),
rows_per_image: Some(h),
};
let size = wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
};
self.queue.write_texture(
wgpu::TexelCopyTextureInfoBase {
texture: &self.atlas_mask.tex,
mip_level: 0,
origin: wgpu::Origin3d { x, y, z: 0 },
aspect: wgpu::TextureAspect::All,
},
&coverage,
layout,
size,
);
let info = GlyphInfo {
u0: x as f32 / self.atlas_mask.size as f32,
v0: y as f32 / self.atlas_mask.size as f32,
u1: (x + w) as f32 / self.atlas_mask.size as f32,
v1: (y + h) as f32 / self.atlas_mask.size as f32,
w: w as f32,
h: h as f32,
last_touch: touch,
last_used_frame: self.frame_index,
};
self.atlas_mask.used_area = self
.atlas_mask
.used_area
.saturating_add(u64::from(w).saturating_mul(u64::from(h)));
self.atlas_mask.map.insert(keyp, info);
Some(info)
}
fn upload_glyph_color(
&mut self,
cache_key: repose_text::CacheKey,
px: f32,
) -> Option<GlyphInfo> {
let px = quantize_glyph_px(px);
let keyp = repose_text::CacheKey {
font_size_bits: px.to_bits(),
..cache_key
};
let touch = self.next_cache_touch();
if let Some(info) = self.atlas_color.map.get_mut(&keyp) {
info.last_touch = touch;
info.last_used_frame = self.frame_index;
return Some(*info);
}
if self.atlas_color_failures.contains(&keyp) || self.atlas_color_deferred.contains(&keyp) {
return None;
}
let Some(gb) = repose_text::rasterize_cache_key(keyp) else {
self.mark_atlas_color_failure(keyp);
return None;
};
if !matches!(gb.content, repose_text::SwashContent::Color) {
self.mark_atlas_color_failure(keyp);
return None;
}
let w = gb.w.max(1);
let h = gb.h.max(1);
let expected = checked_image_bytes_usize(w, h, 4).ok()?;
if gb.data.len() < expected {
self.mark_atlas_color_failure(keyp);
return None;
}
if w >= MAX_GLYPH_ATLAS_SIZE || h >= MAX_GLYPH_ATLAS_SIZE {
self.mark_atlas_color_failure(keyp);
return None;
}
if !self.alloc_or_schedule_color(w, h) {
defer_atlas_glyph(&mut self.atlas_color_deferred, keyp);
return None;
}
let x = self.atlas_color.next_x;
let y = self.atlas_color.next_y;
self.atlas_color.next_x += w + 1;
self.atlas_color.row_h = self.atlas_color.row_h.max(h + 1);
let layout = wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w * 4),
rows_per_image: Some(h),
};
let size = wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
};
self.queue.write_texture(
wgpu::TexelCopyTextureInfoBase {
texture: &self.atlas_color.tex,
mip_level: 0,
origin: wgpu::Origin3d { x, y, z: 0 },
aspect: wgpu::TextureAspect::All,
},
&gb.data,
layout,
size,
);
let info = GlyphInfo {
u0: x as f32 / self.atlas_color.size as f32,
v0: y as f32 / self.atlas_color.size as f32,
u1: (x + w) as f32 / self.atlas_color.size as f32,
v1: (y + h) as f32 / self.atlas_color.size as f32,
w: w as f32,
h: h as f32,
last_touch: touch,
last_used_frame: self.frame_index,
};
self.atlas_color.used_area = self
.atlas_color
.used_area
.saturating_add(u64::from(w).saturating_mul(u64::from(h)));
self.atlas_color.map.insert(keyp, info);
Some(info)
}
fn alloc_space_mask(&mut self, w: u32, h: u32) -> bool {
let Some(x_end) = self
.atlas_mask
.next_x
.checked_add(w)
.and_then(|v| v.checked_add(1))
else {
return false;
};
if x_end >= self.atlas_mask.size {
self.atlas_mask.next_x = 1;
let Some(next_y) = self
.atlas_mask
.next_y
.checked_add(self.atlas_mask.row_h)
.and_then(|v| v.checked_add(1))
else {
return false;
};
self.atlas_mask.next_y = next_y;
self.atlas_mask.row_h = 0;
}
let Some(y_end) = self
.atlas_mask
.next_y
.checked_add(h)
.and_then(|v| v.checked_add(1))
else {
return false;
};
y_end < self.atlas_mask.size
}
fn alloc_space_color(&mut self, w: u32, h: u32) -> bool {
let Some(x_end) = self
.atlas_color
.next_x
.checked_add(w)
.and_then(|v| v.checked_add(1))
else {
return false;
};
if x_end >= self.atlas_color.size {
self.atlas_color.next_x = 1;
let Some(next_y) = self
.atlas_color
.next_y
.checked_add(self.atlas_color.row_h)
.and_then(|v| v.checked_add(1))
else {
return false;
};
self.atlas_color.next_y = next_y;
self.atlas_color.row_h = 0;
}
let Some(y_end) = self
.atlas_color
.next_y
.checked_add(h)
.and_then(|v| v.checked_add(1))
else {
return false;
};
y_end < self.atlas_color.size
}
fn alloc_or_schedule_mask(&mut self, w: u32, h: u32) -> bool {
if self.atlas_mask.map.len() >= MAX_GLYPH_ATLAS_ENTRIES
|| self
.atlas_mask
.used_area
.saturating_add(u64::from(w) * u64::from(h))
>= self.atlas_mask.size as u64 * self.atlas_mask.size as u64
{
self.atlas_mask_pending.compact = true;
return false;
}
if self.alloc_space_mask(w, h) {
return true;
}
if self.atlas_mask.size < MAX_GLYPH_ATLAS_SIZE {
self.atlas_mask_pending.grow = true;
} else {
self.atlas_mask_pending.compact = true;
}
false
}
fn alloc_or_schedule_color(&mut self, w: u32, h: u32) -> bool {
if self.atlas_color.map.len() >= MAX_GLYPH_ATLAS_ENTRIES
|| self
.atlas_color
.used_area
.saturating_add(u64::from(w) * u64::from(h))
>= self.atlas_color.size as u64 * self.atlas_color.size as u64
{
self.atlas_color_pending.compact = true;
return false;
}
if self.alloc_space_color(w, h) {
return true;
}
if self.atlas_color.size < MAX_GLYPH_ATLAS_SIZE {
self.atlas_color_pending.grow = true;
} else {
self.atlas_color_pending.compact = true;
}
false
}
}
fn brush_to_shape_fields(
brush: &Brush,
rect: &repose_core::Rect,
transform: &Transform,
) -> (u32, u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2], u32) {
let to_local = |p: Vec2| {
let m = transform.linear();
let det = m[0] * m[3] - m[1] * m[2];
if det.abs() < 1e-12 {
return [p.x, p.y];
}
[
(m[3] * p.x - m[1] * p.y) / det,
(-m[2] * p.x + m[0] * p.y) / det,
]
};
match brush {
Brush::Solid(c) => (
0u32,
0u32,
c.to_linear(),
[0.0; 4],
[0.0; 2],
[0.0; 2],
0u32,
),
Brush::Linear {
start,
end,
start_color,
end_color,
} => (
1u32,
0u32,
start_color.to_linear(),
end_color.to_linear(),
to_local(*start),
to_local(*end),
0u32,
),
Brush::LinearNormalized {
start,
end,
start_color,
end_color,
} => (
1u32,
0u32,
start_color.to_linear(),
end_color.to_linear(),
to_local(Vec2 {
x: start.x * rect.w,
y: start.y * rect.h,
}),
to_local(Vec2 {
x: end.x * rect.w,
y: end.y * rect.h,
}),
0u32,
),
Brush::Radial {
center,
radius,
start_color,
end_color,
} => (
1u32,
1u32,
start_color.to_linear(),
end_color.to_linear(),
to_local(*center),
[radius.max(0.0), 0.0],
0u32,
),
Brush::Sweep {
center,
start_color,
end_color,
} => (
1u32,
2u32,
start_color.to_linear(),
end_color.to_linear(),
to_local(*center),
[0.0, 0.0],
0u32,
),
_ => (0u32, 0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2], 0u32),
}
}
fn brush_to_instance_fields(brush: &Brush) -> (u32, [f32; 4], [f32; 4], [f32; 2], [f32; 2]) {
match brush {
Brush::Solid(c) => (
0u32,
c.to_linear(),
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0],
[0.0, 1.0],
),
Brush::Linear {
start,
end,
start_color,
end_color,
} => (
1u32,
start_color.to_linear(),
end_color.to_linear(),
[start.x, start.y],
[end.x, end.y],
),
Brush::LinearNormalized {
start,
end,
start_color,
end_color,
} => (
1u32,
start_color.to_linear(),
end_color.to_linear(),
[start.x, start.y],
[end.x, end.y],
),
Brush::Radial { start_color, .. } => (
0u32,
start_color.to_linear(),
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0],
[0.0, 1.0],
),
Brush::Sweep { start_color, .. } => (
0u32,
start_color.to_linear(),
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0],
[0.0, 1.0],
),
_ => (0u32, [0.0; 4], [0.0; 4], [0.0; 2], [0.0; 2]),
}
}
#[allow(dead_code)]
fn brush_to_solid_color(brush: &Brush) -> [f32; 4] {
match brush {
Brush::Solid(c) => c.to_linear(),
Brush::Linear { start_color, .. } => start_color.to_linear(),
Brush::LinearNormalized { start_color, .. } => start_color.to_linear(),
Brush::Radial { start_color, .. } => start_color.to_linear(),
Brush::Sweep { start_color, .. } => start_color.to_linear(),
_ => [0.0; 4],
}
}
fn init_atlas_mask(device: &wgpu::Device) -> AtlasA8 {
let size = 1024u32;
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("glyph atlas A8"),
size: wgpu::Extent3d {
width: size,
height: size,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("glyph atlas sampler A8"),
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::Linear,
..Default::default()
});
AtlasA8 {
tex,
view,
sampler,
size,
next_x: 1,
next_y: 1,
row_h: 0,
map: HashMap::new(),
used_area: 0,
}
}
fn init_atlas_color(device: &wgpu::Device) -> AtlasRGBA {
let size = 1024u32;
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("glyph atlas RGBA"),
size: wgpu::Extent3d {
width: size,
height: size,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("glyph atlas sampler RGBA"),
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::Linear,
..Default::default()
});
AtlasRGBA {
tex,
view,
sampler,
size,
next_x: 1,
next_y: 1,
row_h: 0,
map: HashMap::new(),
used_area: 0,
}
}
#[cfg(feature = "winit-surface")]
impl WgpuSurfaceBackend {
pub fn take_surface(&mut self) -> Option<wgpu::Surface<'static>> {
self.surface.take()
}
pub fn recreate_surface(&mut self, window: &Arc<winit::window::Window>) -> anyhow::Result<()> {
self.window = Some(window.clone());
let size = window.inner_size();
if size.width == 0 || size.height == 0 {
self.surface = None;
self.pending_reconfigure = false;
self.renderer.resize(0, 0);
anyhow::bail!("window has zero size; surface recreation deferred");
}
let Some(instance) = self.instance.as_ref() else {
anyhow::bail!("no wgpu instance retained; cannot recreate surface")
};
let Some(config) = self.surface_config.as_mut() else {
anyhow::bail!("no surface config retained; cannot recreate surface")
};
let max = self.renderer.device.limits().max_texture_dimension_2d;
let width = size.width.min(max);
let height = size.height.min(max);
config.width = width;
config.height = height;
let surface = instance.create_surface(window.clone())?;
surface.configure(&self.renderer.device, config);
self.surface = Some(surface);
self.pending_reconfigure = false;
self.renderer.resize(width, height);
Ok(())
}
}
#[cfg(feature = "winit-surface")]
impl RenderBackend for WgpuSurfaceBackend {
fn configure_surface(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 {
self.renderer.resize(0, 0);
self.pending_reconfigure = true;
return;
}
let max = self.renderer.device.limits().max_texture_dimension_2d;
let width = width.min(max);
let height = height.min(max);
if self.renderer.output_width == width && self.renderer.output_height == height {
if let Some(ref mut config) = self.surface_config {
config.width = width;
config.height = height;
}
return;
}
if let Some(ref mut config) = self.surface_config {
config.width = width;
config.height = height;
}
self.renderer.resize(width, height);
if let (Some(surface), Some(config)) = (self.surface.as_ref(), self.surface_config.as_ref())
{
surface.configure(&self.renderer.device, config);
}
}
fn frame(&mut self, scene: &Scene, _glyph_cfg: GlyphRasterConfig) -> bool {
if self.pending_reconfigure {
if let (Some(surface), Some(config)) =
(self.surface.as_ref(), self.surface_config.as_ref())
{
surface.configure(&self.renderer.device, config);
}
self.pending_reconfigure = false;
}
if self.renderer.output_width == 0 || self.renderer.output_height == 0 {
request_frame();
return false;
}
if let Err(error) = self.renderer.begin_frame() {
log::warn!("begin renderer frame: {error:#}");
request_frame();
return false;
}
let Some(surface) = self.surface.as_ref() else {
self.renderer.end_frame();
request_frame();
return false;
};
let frame = match surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(f) => f,
wgpu::CurrentSurfaceTexture::Suboptimal(f) => {
self.pending_reconfigure = true;
f
}
other @ (wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost) => {
self.surface = None;
let result = self
.window
.clone()
.ok_or_else(|| anyhow::anyhow!("no window retained for surface recreation"))
.and_then(|window| self.recreate_surface(&window));
if let Err(error) = result {
log::warn!("surface {other:?}; recreation deferred: {error:#}");
}
self.renderer.end_frame();
request_frame();
return false;
}
other => {
match other {
wgpu::CurrentSurfaceTexture::Validation => {
log::warn!("surface {other:?}; reconfiguring next frame");
self.pending_reconfigure = true;
}
wgpu::CurrentSurfaceTexture::Timeout
| wgpu::CurrentSurfaceTexture::Occluded => {
log::debug!("surface {other:?}; retrying next frame");
}
_ => {}
}
self.renderer.end_frame();
request_frame();
return false;
}
};
let (swap_w, swap_h) = (frame.texture.width(), frame.texture.height());
if swap_w != self.renderer.output_width || swap_h != self.renderer.output_height {
log::debug!(
"swapchain {swap_w}x{swap_h} != renderer {}x{}; resyncing",
self.renderer.output_width,
self.renderer.output_height
);
drop(frame);
if let Some(config) = self.surface_config.as_mut() {
config.width = swap_w;
config.height = swap_h;
}
self.renderer.resize(swap_w, swap_h);
self.pending_reconfigure = true;
self.renderer.end_frame();
request_frame();
return false;
}
let swap_view = if let Some(view_format) = self
.surface_config
.as_ref()
.and_then(|c| c.view_formats.iter().find(|f| f.is_srgb()).copied())
{
frame.texture.create_view(&wgpu::TextureViewDescriptor {
format: Some(view_format),
..Default::default()
})
} else {
frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default())
};
let mut encoder =
self.renderer
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frame encoder"),
});
self.renderer.render_scene_to_encoder_with_texture(
scene,
&mut encoder,
&swap_view,
Some(&frame.texture),
None,
);
{
let _reset = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("webgl color_mask reset before present"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &swap_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
self.renderer
.queue
.submit(std::iter::once(encoder.finish()));
if let Err(e) = catch_unwind(AssertUnwindSafe(|| self.renderer.queue.present(frame))) {
log::warn!("queue.present panicked: {:?}", e);
self.renderer.end_frame();
request_frame();
return false;
}
self.renderer.end_frame();
true
}
}
impl WgpuSceneRenderer {
#[allow(clippy::too_many_arguments)]
fn replay_active_clips(
&mut self,
clips: &[ActiveClip],
parent_transform: &Transform,
origin: (f32, f32),
target_size: (f32, f32),
pass: &mut Pass,
encoder: &mut wgpu::CommandEncoder,
) -> Vec<ActiveClip> {
let mut current = repose_core::Rect {
x: 0.0,
y: 0.0,
w: target_size.0,
h: target_size.1,
};
let mut replayed = Vec::with_capacity(clips.len());
for (index, clip) in clips.iter().enumerate() {
let has_inverse = clips[..index].iter().any(ActiveClip::difference);
let mut blocked = clip.blocked();
if has_inverse && !blocked {
log::error!(
"unsupported clip nesting: a Difference clip cannot contain another clip"
);
blocked = true;
}
let difference = clip.difference();
let mut command = None;
match clip {
ActiveClip::Rect {
off: _,
cnt: _,
rect,
radii,
..
} => {
let local_rect = parent_rect_to_layer_rect(*rect, parent_transform, origin)
.unwrap_or(repose_core::Rect {
x: 0.0,
y: 0.0,
w: 0.0,
h: 0.0,
});
let local_radii =
parent_radii_to_layer_radii(*radii, parent_transform).unwrap_or([0.0; 4]);
let next = if difference {
current
} else {
intersect(current, local_rect)
};
let scissor = rect_to_scissor(
next,
target_size.0.max(1.0) as u32,
target_size.1.max(1.0) as u32,
);
let has_area = local_rect.x.is_finite()
&& local_rect.y.is_finite()
&& local_rect.w.is_finite()
&& local_rect.h.is_finite()
&& local_rect.w > 0.0
&& local_rect.h > 0.0;
if blocked || !has_area || (!difference && scissor.2 == 0) {
blocked = blocked || !difference;
} else {
let ndc = rect_to_ndc(local_rect, target_size.0, target_size.1);
let instance = ClipInstance {
xywh: [ndc[0] + ndc[2] * 0.5, ndc[1] + ndc[3] * 0.5, ndc[2], ndc[3]],
radii: local_radii,
fwd_mat: [1.0, 0.0, 0.0, 1.0],
};
let bytes = bytemuck::bytes_of(&instance);
if self
.clip_ring
.grow_to_fit(
&self.device,
encoder,
u64::try_from(bytes.len()).unwrap_or(u64::MAX),
)
.is_ok()
&& let Ok(new_off) = self.clip_ring.alloc_write(&self.queue, bytes)
{
command = Some(Cmd::ClipPush {
off: new_off,
cnt: 1,
scissor,
difference,
applied: true,
});
replayed.push(ActiveClip::Rect {
off: new_off,
cnt: 1,
rect: local_rect,
radii: local_radii,
scissor,
difference,
applied: true,
blocked: false,
});
} else {
blocked = blocked || !difference;
}
}
current = next;
}
ActiveClip::Vector {
mesh,
source,
affine,
..
} => {
let local_affine =
parent_affine_to_layer_affine(*affine, parent_transform, origin)
.unwrap_or([0.0; 6]);
let aabb = mesh_aabb(source, local_affine);
let next = if difference {
current
} else {
intersect(current, aabb)
};
let scissor = rect_to_scissor(
next,
target_size.0.max(1.0) as u32,
target_size.1.max(1.0) as u32,
);
let has_area = aabb.x.is_finite()
&& aabb.y.is_finite()
&& aabb.w.is_finite()
&& aabb.h.is_finite()
&& aabb.w > 0.0
&& aabb.h > 0.0
&& mesh
.as_ref()
.is_some_and(|mesh| mesh.vertex_count > 0 && mesh.index_count > 0);
if blocked || !has_area || (!difference && scissor.2 == 0) {
blocked = blocked || !difference;
} else if let Some(mesh_gpu) = mesh.clone()
&& let Some(uoff) = self.alloc_mesh_uniform(mesh_uniform_from_paint(
local_affine,
&repose_core::PaintDesc::Solid,
))
{
command = Some(Cmd::VectorClipPush {
mesh: mesh_gpu.clone(),
uoff,
scissor,
difference,
applied: true,
});
replayed.push(ActiveClip::Vector {
mesh: Some(mesh_gpu),
source: source.clone(),
uoff,
affine: local_affine,
scissor,
difference,
applied: true,
blocked: false,
});
} else {
blocked = blocked || !difference;
}
current = next;
}
}
if blocked {
current = repose_core::Rect {
x: 0.0,
y: 0.0,
w: 0.0,
h: 0.0,
};
}
if command.is_none() {
replayed.push(match clip {
ActiveClip::Rect {
rect,
radii,
scissor,
difference,
..
} => ActiveClip::Rect {
off: 0,
cnt: 0,
rect: *rect,
radii: *radii,
scissor: *scissor,
difference: *difference,
applied: false,
blocked,
},
ActiveClip::Vector {
mesh,
source,
affine,
scissor,
difference,
..
} => ActiveClip::Vector {
mesh: mesh.clone(),
source: source.clone(),
uoff: 0,
affine: parent_affine_to_layer_affine(*affine, parent_transform, origin)
.unwrap_or([0.0; 6]),
scissor: *scissor,
difference: *difference,
applied: false,
blocked,
},
});
}
if let Some(command) = command {
pass.cmds.push(command);
}
pass.active_scissor = if blocked {
None
} else {
let scissor = rect_to_scissor(
current,
target_size.0.max(1.0) as u32,
target_size.1.max(1.0) as u32,
);
(scissor.2 > 0 && scissor.3 > 0).then_some(scissor)
};
}
replayed
}
#[allow(clippy::too_many_arguments)]
fn push_perspective_layer(
&mut self,
node: Transform,
top: Transform,
transform_stack: &mut Vec<Transform>,
scissor_stack: &mut Vec<repose_core::Rect>,
root_clip_rect: &mut repose_core::Rect,
current_target_size: &mut (f32, f32),
current_pass: &mut Pass,
active_clips: &mut Vec<ActiveClip>,
passes: &mut Vec<Pass>,
target_stack: &mut Vec<PassTarget>,
flatten_stack: &mut Vec<FlattenRecord>,
id_head: &mut u32,
pass_id_head: &mut u64,
ids_used: &mut Vec<u32>,
encoder: &mut wgpu::CommandEncoder,
) {
let map =
Transform::compose_projective(&top.projective_matrix(), &node.projective_matrix());
let scr = scissor_stack.last().copied().unwrap_or(*root_clip_rect);
let max_dimension = self.device.limits().max_texture_dimension_2d as f32;
if !scr.w.is_finite()
|| !scr.h.is_finite()
|| scr.w < 1.0
|| scr.h < 1.0
|| scr.w > max_dimension
|| scr.h > max_dimension
{
return;
}
let w = scr.w.ceil();
let h = scr.h.ceil();
let layer_rect = repose_core::Rect {
x: scr.x,
y: scr.y,
w,
h,
};
if ids_used.len() >= MAX_TRANSIENT_GRAPHICS_LAYERS {
return;
}
let layer_id = *id_head;
*id_head = id_head.wrapping_add(1);
ids_used.push(layer_id);
let stack_len = transform_stack.len();
transform_stack.push(top);
transform_stack.push(Transform::translate(-layer_rect.x, -layer_rect.y));
let saved_scissor_stack = std::mem::replace(
scissor_stack,
vec![repose_core::Rect {
x: 0.0,
y: 0.0,
w,
h,
}],
);
let saved_root = std::mem::replace(
root_clip_rect,
repose_core::Rect {
x: 0.0,
y: 0.0,
w,
h,
},
);
let saved_size = std::mem::replace(current_target_size, (w, h));
let prev_target = current_pass.target;
let saved_active_scissor = current_pass.active_scissor;
let saved_clips = std::mem::take(active_clips);
let layer_pass_id = *pass_id_head;
*pass_id_head += 1;
let mut layer_pass = Pass {
id: layer_pass_id,
target: PassTarget::Layer(layer_id),
initial_scissor: (0, 0, w as u32, h as u32),
active_scissor: Some((0, 0, w as u32, h as u32)),
clear_color: Some([0.0, 0.0, 0.0, 0.0]),
cmds: Vec::new(),
};
let layer_clips = self.replay_active_clips(
&saved_clips,
&top,
(layer_rect.x, layer_rect.y),
(w, h),
&mut layer_pass,
encoder,
);
*active_clips = layer_clips;
let saved = std::mem::replace(current_pass, layer_pass);
passes.push(saved);
target_stack.push(prev_target);
self.get_or_create_layer(layer_id, w as u32, h as u32, layer_rect, true);
*current_target_size = (w, h);
flatten_stack.push(FlattenRecord {
stack_len,
layer_id,
map,
layer_rect,
saved_scissor_stack,
saved_root,
saved_size,
saved_active_scissor,
saved_clips,
});
}
#[allow(clippy::too_many_arguments)]
fn pop_perspective_layer(
&mut self,
rec: FlattenRecord,
scissor_stack: &mut Vec<repose_core::Rect>,
root_clip_rect: &mut repose_core::Rect,
current_target_size: &mut (f32, f32),
current_pass: &mut Pass,
active_clips: &mut Vec<ActiveClip>,
passes: &mut Vec<Pass>,
target_stack: &mut Vec<PassTarget>,
pass_id_head: &mut u64,
encoder: &mut wgpu::CommandEncoder,
) {
*scissor_stack = rec.saved_scissor_stack;
*root_clip_rect = rec.saved_root;
*current_target_size = rec.saved_size;
*active_clips = rec.saved_clips;
let resumed_pass_id = *pass_id_head;
*pass_id_head += 1;
let saved = std::mem::replace(
current_pass,
Pass {
id: resumed_pass_id,
target: target_stack.pop().unwrap_or(PassTarget::Surface),
initial_scissor: rec.saved_active_scissor.unwrap_or((0, 0, 1, 1)),
active_scissor: rec.saved_active_scissor,
clear_color: None,
cmds: Vec::new(),
},
);
passes.push(saved);
let (tw, th) = rec.saved_size;
let r = rec.layer_rect;
let corners = [
(r.x, r.y),
(r.x + r.w, r.y),
(r.x + r.w, r.y + r.h),
(r.x, r.y + r.h),
];
let mut ndc = [[0.0f32; 2]; 4];
let mut ws = [1.0f32; 4];
let mut all_behind = true;
for (i, (x, y)) in corners.iter().enumerate() {
let w_raw = rec.map[6] * x + rec.map[7] * y + rec.map[8];
let w = if w_raw.abs() < 1e-6 {
if w_raw < 0.0 { -1e-6 } else { 1e-6 }
} else {
w_raw
};
if w > 0.0 {
all_behind = false;
}
let px = (rec.map[0] * x + rec.map[1] * y + rec.map[2]) / w;
let py = (rec.map[3] * x + rec.map[4] * y + rec.map[5]) / w;
ndc[i] = [px / tw * 2.0 - 1.0, 1.0 - py / th * 2.0];
ws[i] = w;
}
if all_behind {
return;
}
let Some(layer) = self.layer_pool.get(&rec.layer_id) else {
return;
};
let uv_u1 = layer.rect_px.2 / layer.width.max(1) as f32;
let uv_v1 = layer.rect_px.3 / layer.height.max(1) as f32;
let inst = ProjectiveInstance {
c0: ndc[0],
c1: ndc[1],
c2: ndc[2],
c3: ndc[3],
uv: [0.0, 0.0, uv_u1, uv_v1],
w: ws,
alpha: 1.0,
_pad: [0.0; 3],
};
if self
.projective_ring
.grow_to_fit(
&self.device,
encoder,
std::mem::size_of::<ProjectiveInstance>() as u64,
)
.is_err()
{
return;
}
let bytes = bytemuck::bytes_of(&inst);
let Ok(off) = self.projective_ring.alloc_write(&self.queue, bytes) else {
return;
};
current_pass.cmds.push(Cmd::CompositeProjective {
off,
cnt: 1,
layer_id: rec.layer_id,
});
}
fn mesh_cache_key(mesh: &Arc<repose_core::VectorMeshData>) -> MeshCacheKey {
MeshCacheKey {
source: Arc::as_ptr(mesh) as usize,
index: 0,
kind: 1,
vertex_len: mesh.vertices.len(),
index_len: mesh.indices.len(),
}
}
fn overlay_mesh_cache_key(
meshes: &Arc<[repose_core::VectorMeshData]>,
index: usize,
) -> MeshCacheKey {
let mesh = &meshes[index];
MeshCacheKey {
source: meshes.as_ptr() as usize,
index,
kind: 2,
vertex_len: mesh.vertices.len(),
index_len: mesh.indices.len(),
}
}
fn mesh_owner_matches(owner: &MeshOwner, other: &MeshOwner) -> bool {
match (owner, other) {
(MeshOwner::Object(a), MeshOwner::Object(b)) => Arc::ptr_eq(a, b),
(MeshOwner::Overlay(a), MeshOwner::Overlay(b)) => Arc::ptr_eq(a, b),
_ => false,
}
}
fn evict_mesh_cache(&mut self) -> bool {
let victim = self
.mesh_cache
.iter()
.filter(|(_, entry)| entry.last_used_frame != self.frame_index)
.min_by_key(|(_, entry)| entry.last_touch_frame)
.map(|(key, _)| *key);
let Some(victim) = victim else {
return false;
};
let Some(entry) = self.mesh_cache.remove(&victim) else {
return false;
};
self.mesh_cache_bytes_total = self.mesh_cache_bytes_total.saturating_sub(entry.gpu.bytes);
true
}
fn reserve_mesh_cache(&mut self, bytes: u64) -> bool {
while self.mesh_cache.len() >= MAX_MESH_CACHE_ENTRIES
|| self.mesh_cache_bytes_total.saturating_add(bytes) > MAX_MESH_CACHE_BYTES
{
if !self.evict_mesh_cache() {
return false;
}
}
true
}
fn reserve_mesh_gpu_budget(&mut self, bytes: u64) -> bool {
while self.gpu_bytes_total().saturating_add(bytes) > self.gpu_budget_bytes {
if !self.evict_mesh_cache() {
return false;
}
}
true
}
fn upload_mesh_geometry_with_owner(
&mut self,
mesh: &repose_core::VectorMeshData,
key: MeshCacheKey,
owner: MeshOwner,
) -> Option<MeshGpu> {
let touch = self.next_cache_touch();
let cached = self
.mesh_cache
.get(&key)
.filter(|entry| Self::mesh_owner_matches(&entry.owner, &owner))
.map(|entry| entry.gpu.clone());
if let Some(gpu) = cached {
if let Some(entry) = self.mesh_cache.get_mut(&key) {
entry.last_used_frame = self.frame_index;
entry.last_touch_frame = touch;
}
return Some(gpu);
}
if let Some(entry) = self.mesh_cache.remove(&key) {
self.mesh_cache_bytes_total =
self.mesh_cache_bytes_total.saturating_sub(entry.gpu.bytes);
}
if mesh.vertices.is_empty() || mesh.indices.is_empty() {
log::warn!("vector mesh has no geometry");
return None;
}
if mesh.indices.len() % 3 != 0 {
log::warn!("vector mesh index count is not a triangle list");
return None;
}
let vertex_count = u32::try_from(mesh.vertices.len()).ok()?;
let index_count = u32::try_from(mesh.indices.len()).ok()?;
if mesh.indices.iter().any(|&index| index >= vertex_count) {
log::warn!("vector mesh index is outside its vertex buffer");
return None;
}
if mesh.vertices.iter().any(|vertex| {
vertex.pos.iter().any(|value| !value.is_finite())
|| vertex.color.iter().any(|value| !value.is_finite())
|| vertex.uv.iter().any(|value| !value.is_finite())
}) {
log::warn!("vector mesh contains non-finite vertex data");
return None;
}
let verts: Vec<MeshVertex> = mesh
.vertices
.iter()
.map(|v| MeshVertex {
pos: v.pos,
color: v.color,
uv: v.uv,
})
.collect();
let vbytes = bytemuck::cast_slice(&verts);
let ibytes = bytemuck::cast_slice(&mesh.indices[..]);
let bytes = u64::try_from(vbytes.len())
.ok()?
.checked_add(u64::try_from(ibytes.len()).ok()?)?;
let max_buffer_size = self.device.limits().max_buffer_size;
if vbytes.len() as u64 > max_buffer_size || ibytes.len() as u64 > max_buffer_size {
return None;
}
let cacheable = bytes <= MAX_MESH_CACHE_BYTES
&& self.reserve_mesh_cache(bytes)
&& self.reserve_mesh_gpu_budget(bytes);
let vertices = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("vector mesh vertices"),
size: vbytes.len().max(4) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let indices = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("vector mesh indices"),
size: ibytes.len().max(4) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue.write_buffer(&vertices, 0, vbytes);
self.queue.write_buffer(&indices, 0, ibytes);
let gpu = MeshGpu {
vertices,
indices,
vertex_count,
index_count,
bytes,
};
if cacheable {
let touch = self.next_cache_touch();
self.mesh_cache.insert(
key,
MeshCacheEntry {
owner,
gpu: gpu.clone(),
last_used_frame: self.frame_index,
last_touch_frame: touch,
},
);
self.mesh_cache_bytes_total = self.mesh_cache_bytes_total.saturating_add(bytes);
}
Some(gpu)
}
fn upload_mesh_geometry(&mut self, mesh: &Arc<repose_core::VectorMeshData>) -> Option<MeshGpu> {
self.upload_mesh_geometry_with_owner(
mesh,
Self::mesh_cache_key(mesh),
MeshOwner::Object(mesh.clone()),
)
}
fn upload_overlay_mesh(
&mut self,
meshes: &Arc<[repose_core::VectorMeshData]>,
index: usize,
) -> Option<MeshGpu> {
self.upload_mesh_geometry_with_owner(
&meshes[index],
Self::overlay_mesh_cache_key(meshes, index),
MeshOwner::Overlay(meshes.clone()),
)
}
fn alloc_mesh_uniform(&mut self, u: MeshUniform) -> Option<u64> {
let size = std::mem::size_of::<MeshUniform>() as u64;
let next = self
.mesh_uniform_head
.checked_add(self.mesh_uniform_alignment)?;
if next > self.mesh_uniform_cap || next > u64::from(u32::MAX) {
log::warn!("mesh uniform buffer exhausted; remaining meshes skipped");
return None;
}
let slot = self.mesh_uniform_head;
if slot.checked_add(size)? > self.mesh_uniform_cap {
return None;
}
self.queue
.write_buffer(&self.mesh_uniform_buf, slot, bytemuck::bytes_of(&u));
self.mesh_uniform_head = next;
Some(slot)
}
#[allow(clippy::too_many_arguments)]
fn emit_isolated_blend(
&mut self,
mesh: std::sync::Arc<repose_core::VectorMeshData>,
transform: [f32; 6],
paint: repose_core::PaintDesc,
blend: repose_core::BlendMode,
current_transform: &repose_core::Transform,
current_pass: &mut Pass,
active_clips: &[ActiveClip],
passes: &mut Vec<Pass>,
id_head: &mut u32,
pass_id_head: &mut u64,
ids_used: &mut Vec<u32>,
scissor: (u32, u32, u32, u32),
encoder: &mut wgpu::CommandEncoder,
fb_w: f32,
fb_h: f32,
) {
if scissor.2 == 0 || scissor.3 == 0 || active_clips.iter().any(ActiveClip::blocked) {
return;
}
let parent_target = current_pass.target;
let (parent_w, parent_h) = match parent_target {
PassTarget::Surface => (fb_w, fb_h),
PassTarget::Layer(id) => match self.layer_pool.get(&id) {
Some(layer) => (layer.width as f32, layer.height as f32),
None => return,
},
};
let affine = combine_mesh_affine(current_transform, transform);
let local_rect = intersect(
mesh_aabb(&mesh, affine),
repose_core::Rect {
x: 0.0,
y: 0.0,
w: parent_w,
h: parent_h,
},
);
if !local_rect.x.is_finite()
|| !local_rect.y.is_finite()
|| !local_rect.w.is_finite()
|| !local_rect.h.is_finite()
|| local_rect.w <= 0.0
|| local_rect.h <= 0.0
|| local_rect.w > self.device.limits().max_texture_dimension_2d as f32
|| local_rect.h > self.device.limits().max_texture_dimension_2d as f32
{
return;
}
let layer_rect = repose_core::Rect {
x: local_rect.x.floor(),
y: local_rect.y.floor(),
w: (local_rect.x + local_rect.w).ceil() - local_rect.x.floor(),
h: (local_rect.y + local_rect.h).ceil() - local_rect.y.floor(),
};
let w = layer_rect.w;
let h = layer_rect.h;
let local_rect = layer_rect;
if ids_used.len() >= MAX_TRANSIENT_GRAPHICS_LAYERS {
return;
}
let layer_id = *id_head;
*id_head = id_head.wrapping_add(1);
ids_used.push(layer_id);
let snapshot_format = match parent_target {
PassTarget::Surface if self.working_space => wgpu::TextureFormat::Rgba16Float,
PassTarget::Surface => self.output_format,
PassTarget::Layer(_) => self.layer_target_format(),
};
if !self.alloc_blend_snapshot(layer_id, w as u32, h as u32, snapshot_format) {
log::warn!("blend snapshot budget exhausted; blend skipped");
return;
}
let source_pass_id = *pass_id_head;
*pass_id_head += 1;
let resumed_pass_id = *pass_id_head;
*pass_id_head += 1;
self.blend_copies.push(BlendCopy {
pass_id: resumed_pass_id,
blend_id: layer_id,
target: parent_target,
region: layer_rect,
});
let mut layer_pass = Pass {
id: source_pass_id,
target: PassTarget::Layer(layer_id),
initial_scissor: (0, 0, w as u32, h as u32),
active_scissor: Some((0, 0, w as u32, h as u32)),
clear_color: Some([0.0, 0.0, 0.0, 0.0]),
cmds: Vec::new(),
};
self.replay_active_clips(
active_clips,
current_transform,
(local_rect.x, local_rect.y),
(w, h),
&mut layer_pass,
encoder,
);
self.get_or_create_layer(layer_id, w as u32, h as u32, layer_rect, true);
let shift = repose_core::Transform::translate(-local_rect.x, -local_rect.y);
let local = current_transform.combine(&shift);
self.emit_vector_mesh(
&local,
&mesh,
transform,
&paint,
repose_core::BlendMode::Alpha,
&mut layer_pass.cmds,
);
let saved = std::mem::replace(
current_pass,
Pass {
id: resumed_pass_id,
target: parent_target,
initial_scissor: scissor,
active_scissor: Some(scissor),
clear_color: None,
cmds: Vec::new(),
},
);
passes.push(layer_pass);
passes.push(saved);
let ndc = {
let cx = local_rect.x + local_rect.w * 0.5;
let cy = local_rect.y + local_rect.h * 0.5;
let ndc_cx = (cx / parent_w) * 2.0 - 1.0;
let ndc_cy = 1.0 - (cy / parent_h) * 2.0;
let ndc_w = (local_rect.w / parent_w) * 2.0;
let ndc_h = (local_rect.h / parent_h) * 2.0;
[ndc_cx, ndc_cy, ndc_w, ndc_h]
};
let inst = BlendInstance {
xywh: ndc,
uv: [0.0, 0.0, 1.0, 1.0],
color: [1.0, 1.0, 1.0, 1.0],
fwd_mat: [1.0, 0.0, 0.0, 1.0],
mode: blend.shader_mode(),
_pad: [0.0; 3],
};
if self
.blend_ring
.grow_to_fit(
&self.device,
encoder,
std::mem::size_of::<BlendInstance>() as u64,
)
.is_err()
{
return;
}
let bytes = bytemuck::bytes_of(&inst);
let Ok(off) = self.blend_ring.alloc_write(&self.queue, bytes) else {
return;
};
current_pass.cmds.push(Cmd::BlendLayer {
off,
cnt: 1,
src_layer: layer_id,
dst_layer: Some(layer_id),
parent: parent_target,
scissor,
});
}
fn recycle_blend_snapshot(&mut self, snapshot: BlendSnapshot) {
let key = snapshot.key;
if let Some(old) = self.blend_snapshot_pool.insert(key, snapshot) {
self.blend_snapshot_bytes_total =
self.blend_snapshot_bytes_total.saturating_sub(old.bytes);
}
while self.blend_snapshot_bytes_total > MAX_BLEND_SNAPSHOT_BYTES
|| self.blend_snapshot_pool.len() > MAX_BLEND_SNAPSHOT_POOL_ENTRIES
{
let Some(victim) = self.blend_snapshot_pool.keys().next().copied() else {
break;
};
if let Some(snapshot) = self.blend_snapshot_pool.remove(&victim) {
self.blend_snapshot_bytes_total = self
.blend_snapshot_bytes_total
.saturating_sub(snapshot.bytes);
}
}
}
fn recycle_blend_snapshots(&mut self) {
for (_, snapshot) in std::mem::take(&mut self.blend_snapshots) {
self.recycle_blend_snapshot(snapshot);
}
}
fn alloc_blend_snapshot(
&mut self,
layer_id: u32,
w: u32,
h: u32,
format: wgpu::TextureFormat,
) -> bool {
let key = BlendSnapshotKey {
format,
width: w,
height: h,
};
if self
.blend_snapshots
.get(&layer_id)
.is_some_and(|snapshot| snapshot.key == key)
{
return true;
}
if w == 0 || h == 0 {
return false;
}
if let Some(old) = self.blend_snapshots.remove(&layer_id) {
self.recycle_blend_snapshot(old);
}
if let Some(snapshot) = self.blend_snapshot_pool.remove(&key) {
self.blend_snapshots.insert(layer_id, snapshot);
return true;
}
let Ok(bytes) = texture_storage_bytes(format, w, h) else {
return false;
};
if bytes > MAX_BLEND_SNAPSHOT_BYTES {
return false;
}
while self.blend_snapshot_bytes_total.saturating_add(bytes) > MAX_BLEND_SNAPSHOT_BYTES
|| self.gpu_bytes_total().saturating_add(bytes) > self.gpu_budget_bytes
{
let Some(victim) = self.blend_snapshot_pool.keys().next().copied() else {
return false;
};
let Some(snapshot) = self.blend_snapshot_pool.remove(&victim) else {
return false;
};
self.blend_snapshot_bytes_total = self
.blend_snapshot_bytes_total
.saturating_sub(snapshot.bytes);
}
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("blend backdrop snapshot"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("blend snapshot bind"),
layout: &self.image_bind_layout_rgba,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.layer_sampler),
},
],
});
self.blend_snapshot_bytes_total = self.blend_snapshot_bytes_total.saturating_add(bytes);
self.blend_snapshots.insert(
layer_id,
BlendSnapshot {
key,
texture: tex,
bind,
bytes,
},
);
true
}
fn remove_blend_snapshot(&mut self, layer_id: u32) {
if let Some(snapshot) = self.blend_snapshots.remove(&layer_id) {
self.recycle_blend_snapshot(snapshot);
}
}
fn blend_snapshot_texture(&self, layer_id: u32) -> Option<wgpu::Texture> {
self.blend_snapshots
.get(&layer_id)
.map(|s| s.texture.clone())
}
#[allow(clippy::too_many_arguments)]
fn emit_vector_mesh(
&mut self,
current_transform: &Transform,
mesh: &Arc<repose_core::VectorMeshData>,
transform: [f32; 6],
paint: &repose_core::PaintDesc,
blend: repose_core::BlendMode,
cmds: &mut Vec<Cmd>,
) {
let affine = combine_mesh_affine(current_transform, transform);
let Some(mesh_gpu) = self.upload_mesh_geometry(mesh) else {
return;
};
let Some(uoff) = self.alloc_mesh_uniform(mesh_uniform_from_paint(affine, paint)) else {
return;
};
cmds.push(Cmd::VectorMesh {
mesh: mesh_gpu,
uoff,
blend,
});
}
fn touch_callback_scope(&mut self, scope: CallbackScopeKey) {
self.callback_scope_clock = self.callback_scope_clock.wrapping_add(1);
let tick = self.callback_scope_clock;
self.callback_scope_uses.insert(
scope,
CallbackScopeUse {
tick,
frame: self.frame_index,
},
);
}
fn remove_callback_scope(&mut self, scope: &CallbackScopeKey) {
self.callback_scoped_resources.remove(scope);
self.callback_scope_uses.remove(scope);
self.callback_scope_payloads.remove(scope);
}
fn evict_callback_scope(&mut self, protected: &HashSet<CallbackScopeKey>) -> bool {
let candidate = self
.callback_scoped_resources
.keys()
.filter(|scope| !protected.contains(*scope))
.min_by_key(|scope| {
self.callback_scope_uses
.get(*scope)
.map(|usage| usage.tick)
.unwrap_or(0)
})
.copied();
let Some(scope) = candidate else {
return false;
};
self.remove_callback_scope(&scope);
true
}
fn prune_callback_scopes(&mut self) {
let current_frame = self.frame_index;
let mut dead: Vec<(CallbackScopeKey, u64)> = self
.callback_scope_uses
.iter()
.filter_map(|(scope, usage)| {
let payload_dead = self
.callback_scope_payloads
.get(scope)
.is_none_or(|weak| weak.upgrade().is_none());
(payload_dead && !scope.callback.reusable).then_some((*scope, usage.tick))
})
.collect();
dead.sort_by_key(|(_, tick)| *tick);
for (scope, _) in dead {
self.remove_callback_scope(&scope);
}
if self.callback_scoped_resources.len() <= MAX_CALLBACK_SCOPES {
return;
}
let mut inactive: Vec<(CallbackScopeKey, u64)> = self
.callback_scope_uses
.iter()
.filter_map(|(scope, usage)| {
(usage.frame != current_frame).then_some((*scope, usage.tick))
})
.collect();
inactive.sort_by_key(|(_, tick)| *tick);
for (scope, _) in inactive {
if self.callback_scoped_resources.len() <= MAX_CALLBACK_SCOPES {
break;
}
self.remove_callback_scope(&scope);
}
}
pub fn begin_frame(&mut self) -> anyhow::Result<()> {
if self.frame_active {
self.last_render_error = Some("renderer frame is already active".into());
anyhow::bail!("renderer frame is already active");
}
self.last_render_error = None;
self.frame_active = true;
self.frame_index = self.frame_index.wrapping_add(1);
self.apply_atlas_pending();
self.slug_cache.next_frame();
if let Some(composite) = self.callback_resources.get_mut::<DepthComposite>() {
composite.begin_frame();
}
for resources in self.callback_scoped_resources.values_mut() {
if let Some(composite) = resources.get_mut::<DepthComposite>() {
composite.begin_frame();
}
}
Ok(())
}
pub fn end_frame(&mut self) {
if !self.frame_active {
return;
}
if let Some(composite) = self.callback_resources.get_mut::<DepthComposite>() {
composite.end_frame();
}
for resources in self.callback_scoped_resources.values_mut() {
if let Some(composite) = resources.get_mut::<DepthComposite>() {
composite.end_frame();
}
}
self.prune_callback_scopes();
self.evict_unused_images();
self.frame_active = false;
}
pub fn last_render_error(&self) -> Option<&str> {
self.last_render_error.as_deref()
}
pub fn render_scene_to_encoder(
&mut self,
scene: &Scene,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
clear_color_override: Option<[f64; 4]>,
) {
self.render_scene_to_encoder_with_texture(
scene,
encoder,
target_view,
None,
clear_color_override,
)
}
pub fn render_scene_to_encoder_with_texture(
&mut self,
scene: &Scene,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
target_texture: Option<&wgpu::Texture>,
clear_color_override: Option<[f64; 4]>,
) {
if self.output_width == 0 || self.output_height == 0 {
return;
}
self.sync_text_caches();
fn affine_aabb(transform: &Transform, rect: &repose_core::Rect) -> repose_core::Rect {
let m = transform.linear();
let (tx, ty) = (transform.translate_x, transform.translate_y);
let corners = [
(rect.x, rect.y),
(rect.x + rect.w, rect.y),
(rect.x, rect.y + rect.h),
(rect.x + rect.w, rect.y + rect.h),
];
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for (x, y) in corners {
let wx = m[0] * x + m[1] * y + tx;
let wy = m[2] * x + m[3] * y + ty;
min_x = min_x.min(wx);
min_y = min_y.min(wy);
max_x = max_x.max(wx);
max_y = max_y.max(wy);
}
repose_core::Rect {
x: min_x,
y: min_y,
w: (max_x - min_x).max(0.0),
h: (max_y - min_y).max(0.0),
}
}
fn visible(aabb: repose_core::Rect, clip: repose_core::Rect) -> bool {
if !aabb.x.is_finite()
|| !aabb.y.is_finite()
|| !aabb.w.is_finite()
|| !aabb.h.is_finite()
|| aabb.w <= 0.0
|| aabb.h <= 0.0
{
return false;
}
let aabb = repose_core::Rect {
x: aabb.x - 1.0,
y: aabb.y - 1.0,
w: aabb.w + 2.0,
h: aabb.h + 2.0,
};
let clipped = intersect(aabb, clip);
clipped.x.is_finite()
&& clipped.y.is_finite()
&& clipped.w.is_finite()
&& clipped.h.is_finite()
&& clipped.w > 0.0
&& clipped.h > 0.0
}
fn to_ndc(x: f32, y: f32, w: f32, h: f32, fb_w: f32, fb_h: f32) -> [f32; 4] {
let x0 = (x / fb_w) * 2.0 - 1.0;
let y0 = 1.0 - (y / fb_h) * 2.0;
let x1 = ((x + w) / fb_w) * 2.0 - 1.0;
let y1 = 1.0 - ((y + h) / fb_h) * 2.0;
let min_x = x0.min(x1);
let min_y = y0.min(y1);
let w_ndc = (x1 - x0).abs();
let h_ndc = (y1 - y0).abs();
[min_x, min_y, w_ndc, h_ndc]
}
fn rect_to_instance_ndc(
rect: repose_core::Rect,
transform: &Transform,
fb_w: f32,
fb_h: f32,
) -> ([f32; 4], [f32; 4]) {
let cx = rect.x + rect.w * 0.5;
let cy = rect.y + rect.h * 0.5;
let m = transform.linear();
let tx = m[0] * cx + m[1] * cy + transform.translate_x;
let ty = m[2] * cx + m[3] * cy + transform.translate_y;
let ndc_cx = (tx / fb_w) * 2.0 - 1.0;
let ndc_cy = 1.0 - (ty / fb_h) * 2.0;
let ndc_w = (rect.w * transform.scale_x / fb_w) * 2.0;
let ndc_h = (rect.h * transform.scale_y / fb_h) * 2.0;
([ndc_cx, ndc_cy, ndc_w, ndc_h], forward_rs_mat(transform))
}
fn forward_rs_mat(transform: &Transform) -> [f32; 4] {
let c = transform.rotate.cos();
let s = transform.rotate.sin();
let (hx, hy) = (transform.shear_x, transform.shear_y);
let m = [c - s * hy, c * hx - s, s + c * hy, s * hx + c];
if (m[0] * m[3] - m[1] * m[2]).abs() < 1e-6 {
return [1.0, 0.0, 0.0, 1.0];
}
m
}
fn to_scissor(r: &repose_core::Rect, fb_w: u32, fb_h: u32) -> (u32, u32, u32, u32) {
if fb_w == 0
|| fb_h == 0
|| !r.x.is_finite()
|| !r.y.is_finite()
|| !r.w.is_finite()
|| !r.h.is_finite()
|| r.w <= 0.0
|| r.h <= 0.0
{
return (0, 0, 0, 0);
}
let x0 = r.x.ceil().max(0.0).min(fb_w as f32);
let y0 = r.y.ceil().max(0.0).min(fb_h as f32);
let x1 = (r.x + r.w).floor().max(x0).min(fb_w as f32);
let y1 = (r.y + r.h).floor().max(y0).min(fb_h as f32);
if x1 <= x0 || y1 <= y0 {
return (0, 0, 0, 0);
}
(x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32)
}
let fb_w = self.output_width as f32;
let fb_h = self.output_height as f32;
let mut passes: Vec<Pass> = Vec::with_capacity(1);
let clear_color = clear_color_override
.map(|color| {
let alpha = color[3];
[color[0] * alpha, color[1] * alpha, color[2] * alpha, alpha]
})
.unwrap_or_else(|| {
let color = scene.clear_color.to_linear();
let alpha = color[3] as f64;
[
color[0] as f64 * alpha,
color[1] as f64 * alpha,
color[2] as f64 * alpha,
alpha,
]
});
let mut next_pass_id = 0u64;
let mut current_pass: Pass = Pass {
id: next_pass_id,
target: PassTarget::Surface,
initial_scissor: (0, 0, self.output_width, self.output_height),
active_scissor: Some((0, 0, self.output_width, self.output_height)),
clear_color: Some([
clear_color[0] as f32,
clear_color[1] as f32,
clear_color[2] as f32,
clear_color[3] as f32,
]),
cmds: Vec::with_capacity(scene.nodes.len()),
};
next_pass_id += 1;
let mut target_stack: Vec<PassTarget> = Vec::new();
let mut layer_stack: Vec<LayerState> = Vec::new();
let mut current_target_size: (f32, f32) = (fb_w, fb_h);
struct Batch {
rects: Vec<RectInstance>,
borders: Vec<BorderInstance>,
ellipses: Vec<EllipseInstance>,
e_borders: Vec<EllipseBorderInstance>,
arcs: Vec<ArcInstance>,
masks: Vec<GlyphInstance>,
colors: Vec<GlyphInstance>,
}
impl Batch {
fn new() -> Self {
Self {
rects: vec![],
borders: vec![],
ellipses: vec![],
e_borders: vec![],
arcs: vec![],
masks: vec![],
colors: vec![],
}
}
fn is_empty(&self) -> bool {
self.rects.is_empty()
&& self.borders.is_empty()
&& self.ellipses.is_empty()
&& self.e_borders.is_empty()
&& self.arcs.is_empty()
&& self.masks.is_empty()
&& self.colors.is_empty()
}
fn flush(
&mut self,
pipes: (
&mut InstancedPipe<RectInstance>,
&mut InstancedPipe<BorderInstance>,
&mut InstancedPipe<EllipseInstance>,
&mut InstancedPipe<EllipseBorderInstance>,
&mut InstancedPipe<ArcInstance>,
),
glyph_pipes: (
&mut InstancedPipe<GlyphInstance>,
&mut InstancedPipe<GlyphInstance>,
),
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
cmds: &mut Vec<Cmd>,
) {
let (rects, borders, ellipses, e_borders, arcs) = pipes;
let (masks, colors) = glyph_pipes;
macro_rules! flush_one {
($buf:ident, $pipe:expr, $variant:ident) => {
if !self.$buf.is_empty() {
if let Some((off, cnt)) =
$pipe.upload(device, queue, encoder, &self.$buf)
{
cmds.push(Cmd::$variant { off, cnt });
}
self.$buf.clear();
}
};
}
flush_one!(rects, rects, Rect);
flush_one!(borders, borders, Border);
flush_one!(ellipses, ellipses, Ellipse);
flush_one!(e_borders, e_borders, EllipseBorder);
flush_one!(arcs, arcs, Arc);
flush_one!(masks, masks, GlyphsMask);
flush_one!(colors, colors, GlyphsColor);
}
}
self.rects.reset();
self.borders.reset();
self.ellipses.reset();
self.ellipse_borders.reset();
self.arcs.reset();
self.glyph_mask.reset();
self.glyph_color.reset();
self.clip_ring.reset();
self.blur_ring.reset();
self.nv12.reset();
self.slug_ring.reset();
self.mesh_uniform_head = 0;
self.projective_ring.reset();
self.blend_ring.reset();
for id in self.flatten_layer_ids.drain(..) {
if let Some(layer) = self.layer_pool.remove(&id) {
self.layer_bytes_total = self.layer_bytes_total.saturating_sub(layer.bytes);
if layer.transient {
self.transient_layer_bytes_total =
self.transient_layer_bytes_total.saturating_sub(layer.bytes);
}
}
}
self.producer_layer_ids.clear();
self.recycle_blend_snapshots();
self.blend_copies.clear();
let mut batch = Batch::new();
let mut image_run: Option<(u64, ImageFilter, bool, Vec<GlyphInstance>)> = None;
let mut nv12_run: Option<(u64, ImageFilter, Vec<Nv12Instance>)> = None;
let mut slug_verts_local: Vec<slug::TessVertex> = Vec::new();
let mut transform_stack: Vec<Transform> = vec![Transform::identity()];
let mut flatten_stack: Vec<FlattenRecord> = Vec::new();
let mut flatten_id_head: u32 = FLATTEN_ID_BASE;
let mut flatten_ids_used: Vec<u32> = Vec::new();
let mut scissor_stack: Vec<repose_core::Rect> = Vec::with_capacity(8);
let mut active_clips: Vec<ActiveClip> = Vec::with_capacity(8);
let mut root_clip_rect = repose_core::Rect {
x: 0.0,
y: 0.0,
w: fb_w,
h: fb_h,
};
let mut current_prim: Option<&'static str> = None;
macro_rules! flush_if_prim_changed {
($prim:literal, $pipe:expr) => {
if current_prim != Some($prim) {
flush_batch!();
current_prim = Some($prim);
}
};
}
macro_rules! flush_image_runs {
() => {{
if let Some((handle, filter, tile, instances)) = image_run.take()
&& let Some((off, cnt)) =
self.glyph_color
.upload(&self.device, &self.queue, encoder, &instances)
{
current_pass.cmds.push(Cmd::ImageRgba {
off,
cnt,
handle,
filter,
tile,
});
}
if let Some((handle, filter, instances)) = nv12_run.take()
&& let Some((off, cnt)) =
self.nv12
.upload(&self.device, &self.queue, encoder, &instances)
{
current_pass.cmds.push(Cmd::ImageNv12 {
off,
cnt,
handle,
filter,
});
}
}};
}
macro_rules! flush_primitives {
() => {{
if !batch.is_empty() {
batch.flush(
(
&mut self.rects,
&mut self.borders,
&mut self.ellipses,
&mut self.ellipse_borders,
&mut self.arcs,
),
(&mut self.glyph_mask, &mut self.glyph_color),
&self.device,
&self.queue,
encoder,
&mut current_pass.cmds,
)
}
}};
}
macro_rules! flush_batch {
() => {{
flush_image_runs!();
flush_primitives!();
}};
}
let mut pending_layer_composite: Option<Cmd> = None;
let shadow_layer_ids: HashSet<u32> = scene
.nodes
.iter()
.filter_map(|node| match node {
SceneNode::CompositeShadow { layer_id, .. } => Some(*layer_id),
_ => None,
})
.collect();
macro_rules! flush_pending_layer {
() => {
if let Some(command) = pending_layer_composite.take() {
current_pass.cmds.push(command);
}
};
}
for node in &scene.nodes {
let t_identity = Transform::identity();
let current_transform = transform_stack.last().unwrap_or(&t_identity);
match node {
SceneNode::Rect {
rect,
brush,
radius,
} => {
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
if !visible(affine_aabb(current_transform, rect), clip) {
continue;
}
flush_if_prim_changed!("rect", &self.rects);
let (ndc, fwd_mat) = rect_to_instance_ndc(
*rect,
current_transform,
current_target_size.0,
current_target_size.1,
);
let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
brush_to_shape_fields(brush, rect, current_transform);
batch.rects.push(RectInstance {
xywh: ndc,
radii: radius.map(|r| r.0),
brush_type,
grad_kind,
_pad: [0.0; 2],
color0,
color1,
grad_p0,
grad_p1,
tile_mode,
_pad2: [0.0; 3],
fwd_mat,
});
}
SceneNode::Border {
rect,
brush,
width,
radius,
} => {
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
if !visible(affine_aabb(current_transform, rect), clip) {
continue;
}
flush_if_prim_changed!("border", &self.borders);
let (ndc, fwd_mat) = rect_to_instance_ndc(
*rect,
current_transform,
current_target_size.0,
current_target_size.1,
);
let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
brush_to_shape_fields(brush, rect, current_transform);
batch.borders.push(BorderInstance {
xywh: ndc,
radii: radius.map(|r| r.0),
stroke: width.0,
brush_type,
_pad: [0.0; 2],
grad_kind,
color0,
color1,
grad_p0,
grad_p1,
tile_mode,
_pad2: [0.0; 3],
fwd_mat,
});
}
SceneNode::Ellipse { rect, brush } => {
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
if !visible(affine_aabb(current_transform, rect), clip) {
continue;
}
flush_if_prim_changed!("ellipse", &self.ellipses);
let (ndc, fwd_mat) = rect_to_instance_ndc(
*rect,
current_transform,
current_target_size.0,
current_target_size.1,
);
let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
brush_to_shape_fields(brush, rect, current_transform);
batch.ellipses.push(EllipseInstance {
xywh: ndc,
brush_type,
grad_kind,
_pad: [0.0; 2],
color0,
color1,
grad_p0,
grad_p1,
tile_mode,
_pad2: [0.0; 3],
fwd_mat,
});
}
SceneNode::EllipseBorder { rect, brush, width } => {
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
let cull_pad = width.0.abs() * 0.5 + 2.0;
let cull_rect = repose_core::Rect {
x: rect.x - cull_pad,
y: rect.y - cull_pad,
w: rect.w + cull_pad * 2.0,
h: rect.h + cull_pad * 2.0,
};
if !visible(affine_aabb(current_transform, &cull_rect), clip) {
continue;
}
flush_if_prim_changed!("ellipse_border", &self.ellipse_borders);
let (ndc, fwd_mat) = rect_to_instance_ndc(
*rect,
current_transform,
current_target_size.0,
current_target_size.1,
);
let pad_px = width.0 * 0.5 + 2.0;
let pad = (pad_px / current_target_size.0.max(1.0)) * 2.0;
let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
brush_to_shape_fields(brush, rect, current_transform);
batch.e_borders.push(EllipseBorderInstance {
xywh: ndc,
stroke: width.0,
pad,
brush_type,
grad_kind,
color0,
color1,
grad_p0,
grad_p1,
tile_mode,
_pad2: [0.0; 3],
fwd_mat,
});
}
SceneNode::Arc {
rect,
start_angle,
sweep_angle,
stroke_width,
brush,
cap,
} => {
if !start_angle.is_finite()
|| !sweep_angle.is_finite()
|| !stroke_width.0.is_finite()
|| !rect.x.is_finite()
|| !rect.y.is_finite()
|| !rect.w.is_finite()
|| !rect.h.is_finite()
|| rect.w <= 0.0
|| rect.h <= 0.0
|| stroke_width.0 <= 0.0
|| sweep_angle.abs() <= 1e-6
{
continue;
}
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
let cull_pad = stroke_width.0.abs() * 0.5 + 2.0;
let cull_rect = repose_core::Rect {
x: rect.x - cull_pad,
y: rect.y - cull_pad,
w: rect.w + cull_pad * 2.0,
h: rect.h + cull_pad * 2.0,
};
if !visible(affine_aabb(current_transform, &cull_rect), clip) {
continue;
}
flush_if_prim_changed!("arc", &self.arcs);
let (ndc, fwd_mat) = rect_to_instance_ndc(
*rect,
current_transform,
current_target_size.0.max(1.0),
current_target_size.1.max(1.0),
);
let pad_px = stroke_width.0 * 0.5 + 2.0;
let pad = (pad_px / current_target_size.0.max(1.0)) * 2.0;
let cap_val = match cap {
StrokeCap::Butt => 0.0,
StrokeCap::Round => 1.0,
StrokeCap::Square => 2.0,
};
let (brush_type, grad_kind, color0, color1, grad_p0, grad_p1, tile_mode) =
brush_to_shape_fields(brush, rect, current_transform);
let (start, sweep) = if *sweep_angle < 0.0 {
(*start_angle + *sweep_angle, -*sweep_angle)
} else {
(*start_angle, *sweep_angle)
};
batch.arcs.push(ArcInstance {
xywh: ndc,
start_angle: start,
sweep_angle: sweep,
stroke: stroke_width.0,
pad,
brush_type,
grad_kind,
_pad0: [0.0; 2],
color0,
color1,
grad_p0,
grad_p1,
tile_mode,
cap: cap_val,
_pad1: [0.0; 2],
fwd_mat,
});
}
SceneNode::Text {
rect,
text,
color,
size,
style,
} => {
let TextPaintStyle {
font_family,
font_weight,
font_style,
text_decoration,
letter_spacing,
font_variation_settings,
baseline_shift,
draw_style,
..
} = style;
let px = size.0;
if !px.is_finite()
|| px <= 0.0
|| !rect.x.is_finite()
|| !rect.y.is_finite()
|| !rect.w.is_finite()
|| !rect.h.is_finite()
{
continue;
}
let margin = px * 4.0 + letter_spacing.0.abs() * 2.0;
let text_bounds = repose_core::Rect {
x: rect.x - margin,
y: rect.y - margin,
w: rect.w + margin * 2.0,
h: rect.h + margin * 2.0,
};
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
if !visible(affine_aabb(current_transform, &text_bounds), clip) {
continue;
}
flush_batch!();
let lh_ratio = rect.h / px;
let fw = font_weight.0;
let fs = if *font_style == FontStyle::Italic {
1
} else {
0
};
let shaped = self.shape_text_cached(
text,
px,
lh_ratio,
*font_family,
fw,
fs,
letter_spacing.0,
font_variation_settings.as_ref(),
);
let baseline_y = shaped.glyphs.first().map(|g| rect.y + g.y);
let fwd = forward_rs_mat(current_transform);
let has_linear = fwd != [1.0, 0.0, 0.0, 1.0];
let lin = current_transform.linear();
let tr_x = current_transform.translate_x;
let tr_y = current_transform.translate_y;
let make_glyph_instance =
|gx: f32, gy: f32, gw: f32, gh: f32| -> ([f32; 4], [f32; 4]) {
if has_linear {
let gc_x = gx + gw * 0.5;
let gc_y = gy + gh * 0.5;
let wc_x = lin[0] * gc_x + lin[1] * gc_y + tr_x;
let wc_y = lin[2] * gc_x + lin[3] * gc_y + tr_y;
let ww = gw * current_transform.scale_x;
let wh = gh * current_transform.scale_y;
let ex = fwd[0].abs() * ww * 0.5 + fwd[1].abs() * wh * 0.5;
let ey = fwd[2].abs() * ww * 0.5 + fwd[3].abs() * wh * 0.5;
let ndc_tl = to_ndc(
wc_x - ex,
wc_y - ey,
ex * 2.0,
ey * 2.0,
current_target_size.0,
current_target_size.1,
);
let ndc = [
ndc_tl[0] + ndc_tl[2] * 0.5,
ndc_tl[1] + ndc_tl[3] * 0.5,
ndc_tl[2],
ndc_tl[3],
];
(ndc, fwd)
} else {
let (sx, sy) = if current_transform.scale_x == 1.0
&& current_transform.scale_y == 1.0
{
(gx.round(), gy.round())
} else {
(gx, gy)
};
rect_to_instance_ndc(
repose_core::Rect {
x: sx,
y: sy,
w: gw,
h: gh,
},
current_transform,
current_target_size.0,
current_target_size.1,
)
}
};
let baseline_shift_y: f32 = px * baseline_shift.0;
let (
draws_fill,
is_stroke,
stroke_width,
stroke_cap,
stroke_join,
stroke_miter,
stroke_path_effect,
) = match draw_style {
repose_core::DrawStyle::Stroke {
width,
cap,
join,
miter,
path_effect,
} => (
false,
true,
*width,
*cap,
*join,
*miter,
path_effect.clone(),
),
repose_core::DrawStyle::FillAndStroke {
width,
cap,
join,
miter,
path_effect,
} => (true, true, *width, *cap, *join, *miter, path_effect.clone()),
_ => (
true,
false,
0.0,
repose_core::StrokeCap::Butt,
repose_core::StrokeJoin::Miter,
4.0,
None,
),
};
let stroke_tess_key = if is_stroke {
Some(slug::StrokeTessKey::new(
stroke_width,
stroke_cap,
stroke_join,
stroke_miter,
&stroke_path_effect,
))
} else {
None
};
let mut legacy_shaped = None;
for (glyph_index, sg) in shaped.glyphs.iter().enumerate() {
let mut fill_cached = false;
let mut stroke_cached = false;
if self.slug_enabled {
let ck = sg.cache_key;
let color_linear = color.to_linear();
let fill_key = draws_fill.then(|| {
Self::slug_draw_key(
ck,
true,
None,
current_transform,
(rect.x + sg.x, rect.y + sg.y + baseline_shift_y),
px,
current_target_size,
color_linear,
)
});
let stroke_key = is_stroke.then(|| {
Self::slug_draw_key(
ck,
false,
stroke_tess_key.as_ref(),
current_transform,
(rect.x + sg.x, rect.y + sg.y + baseline_shift_y),
px,
current_target_size,
color_linear,
)
});
let mut all_cached = true;
if let Some(key) = fill_key {
if let Some(vertices) = self.get_cached_slug_vertices(&key) {
slug_verts_local.extend_from_slice(&vertices);
fill_cached = true;
} else {
all_cached = false;
}
}
if let Some(key) = stroke_key {
if let Some(vertices) = self.get_cached_slug_vertices(&key) {
slug_verts_local.extend_from_slice(&vertices);
stroke_cached = true;
} else {
all_cached = false;
}
}
if all_cached {
continue;
}
let outline = self.cached_glyph_outline(ck);
if let Some(commands) = outline.as_ref() {
let need_tessellate = self.slug_cache.get(&ck).is_none_or(|g| {
(draws_fill && g.fill_vertices.is_none())
|| (is_stroke
&& !g
.stroke_variants
.contains_key(stroke_tess_key.as_ref().unwrap()))
});
if need_tessellate {
let font_size_px = f32::from_bits(ck.font_size_bits);
if draws_fill {
self.slug_cache.get_or_insert(
ck,
font_size_px,
commands.as_ref(),
);
}
if is_stroke {
self.slug_cache.get_or_insert_stroke(
ck,
font_size_px,
commands.as_ref(),
stroke_width,
stroke_cap,
stroke_join,
stroke_miter,
&stroke_path_effect,
);
}
} else {
self.slug_cache.touch(&ck);
}
}
if let Some(_commands) = outline.as_ref()
&& let Some(entry) = self.slug_cache.get(&ck)
{
let ox = rect.x + sg.x;
let oy = rect.y + sg.y + baseline_shift_y;
let scx = current_transform.scale_x;
let scy = current_transform.scale_y;
let ttx = current_transform.translate_x;
let tty = current_transform.translate_y;
let tf = |x: f32, y: f32| -> (f32, f32) {
if has_linear {
(
lin[0] * x + lin[1] * y + ttx,
lin[2] * x + lin[3] * y + tty,
)
} else {
(x * scx + ttx, y * scy + tty)
}
};
let tw = current_target_size.0;
let th = current_target_size.1;
let fill_vertices = if draws_fill && !fill_cached {
entry.fill_vertices.clone().unwrap_or_default()
} else {
Vec::new()
};
let stroke_vertices = if is_stroke && !stroke_cached {
stroke_tess_key
.as_ref()
.and_then(|key| entry.stroke_variants.get(key))
.cloned()
.unwrap_or_default()
} else {
Vec::new()
};
let mut emit = |verts: Vec<[f32; 2]>, cache_key: SlugDrawKey| {
let transformed: Vec<slug::TessVertex> = verts
.iter()
.map(|v| {
let (sx, sy) = tf(ox + v[0] * px, oy - v[1] * px);
slug::TessVertex {
ndc_pos: [
sx / tw * 2.0 - 1.0,
-(sy / th) * 2.0 + 1.0,
],
color: color_linear,
}
})
.collect();
let vertices =
self.cached_slug_vertices(cache_key, transformed);
slug_verts_local.extend_from_slice(&vertices);
};
if draws_fill
&& !fill_cached
&& let Some(key) = fill_key
{
emit(fill_vertices, key);
}
if is_stroke
&& !stroke_cached
&& let Some(key) = stroke_key
{
emit(stroke_vertices, key);
}
if !draws_fill {
continue;
}
continue;
}
}
if !draws_fill {
continue;
}
if fill_cached {
continue;
}
if legacy_shaped.is_none() {
legacy_shaped = Some(self.legacy_shape_text_cached(
text,
px,
lh_ratio,
*font_family,
fw,
fs,
letter_spacing.0,
font_variation_settings.as_ref(),
));
}
let Some(legacy_glyph) = legacy_shaped
.as_ref()
.and_then(|glyphs| glyphs.get(glyph_index))
else {
continue;
};
let gx = rect.x + legacy_glyph.x + legacy_glyph.bearing_x;
let gy =
rect.y + legacy_glyph.y - legacy_glyph.bearing_y + baseline_shift_y;
if let Some(info) =
self.upload_glyph_color(legacy_glyph.cache_key, legacy_glyph.px)
{
let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
batch.colors.push(GlyphInstance {
xywh: ndc,
uv: [info.u0, info.v1, info.u1, info.v0],
color: color.to_linear(),
fwd_mat,
});
} else if let Some(info) =
self.upload_glyph_mask(legacy_glyph.cache_key, legacy_glyph.px)
{
let (ndc, fwd_mat) = make_glyph_instance(gx, gy, info.w, info.h);
batch.masks.push(GlyphInstance {
xywh: ndc,
uv: [info.u0, info.v1, info.u1, info.v0],
color: color.to_linear(),
fwd_mat,
});
}
}
if !slug_verts_local.is_empty() {
let bytes = bytemuck::cast_slice(&slug_verts_local);
let byte_len = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
let Ok(count) = u32::try_from(slug_verts_local.len()) else {
slug_verts_local.clear();
continue;
};
if self
.slug_ring
.grow_to_fit(&self.device, encoder, byte_len)
.is_ok()
&& let Ok(off) = self.slug_ring.alloc_write(&self.queue, bytes)
{
current_pass
.cmds
.push(Cmd::GlyphsVector { off, cnt: count });
}
slug_verts_local.clear();
}
if (text_decoration.underline || text_decoration.strikethrough)
&& let Some(baseline_y) = baseline_y
{
flush_batch!();
current_prim = Some("rect");
let deco_color = text_decoration.color.unwrap_or(*color);
let thickness = (px * 0.07).max(1.0);
if text_decoration.underline {
let dy = baseline_y + px * 0.1;
let (ndc, fwd_mat) = rect_to_instance_ndc(
repose_core::Rect {
x: rect.x,
y: dy,
w: rect.w,
h: thickness,
},
current_transform,
current_target_size.0,
current_target_size.1,
);
batch.rects.push(RectInstance {
xywh: ndc,
radii: [0.0; 4],
brush_type: 0,
grad_kind: 0,
_pad: [0.0; 2],
color0: deco_color.to_linear(),
color1: [0.0; 4],
grad_p0: [0.0; 2],
grad_p1: [0.0; 2],
tile_mode: 0,
_pad2: [0.0; 3],
fwd_mat,
});
}
if text_decoration.strikethrough {
let sy = baseline_y - px * 0.3;
let (ndc, fwd_mat) = rect_to_instance_ndc(
repose_core::Rect {
x: rect.x,
y: sy,
w: rect.w,
h: thickness,
},
current_transform,
current_target_size.0,
current_target_size.1,
);
batch.rects.push(RectInstance {
xywh: ndc,
radii: [0.0; 4],
brush_type: 0,
grad_kind: 0,
_pad: [0.0; 2],
color0: deco_color.to_linear(),
color1: [0.0; 4],
grad_p0: [0.0; 2],
grad_p1: [0.0; 2],
tile_mode: 0,
_pad2: [0.0; 3],
fwd_mat,
});
}
}
}
SceneNode::Image {
rect,
handle,
tint,
style,
} => {
let ImagePaintStyle {
fit,
filter,
source_rect,
alignment,
} = style;
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
if !visible(affine_aabb(current_transform, rect), clip) {
continue;
}
flush_batch!();
let (img_w, img_h, is_nv12) = match self.resolve_image_for_draw(*handle) {
Some(wh) => wh,
None => {
log::warn!("Image handle {} not found", handle);
continue;
}
};
let Some(source) = resolve_image_source(*source_rect, img_w, img_h) else {
log::warn!(
"Image handle {} has invalid source rect {:?}",
handle,
source_rect
);
continue;
};
let Some((draw_rect, uv_rect)) =
image_fit_geometry(*rect, *fit, *alignment, &source)
else {
continue;
};
let (ndc_center, fwd_mat) = rect_to_instance_ndc(
draw_rect,
current_transform,
current_target_size.0,
current_target_size.1,
);
if is_nv12 {
if image_run.is_some() {
flush_image_runs!();
}
let (uv_x_offset, uv_y_offset) =
if let Some(ImageTex::Nv12 {
w, h, color_info, ..
}) = self.images.get(handle)
{
let chroma_w = w.div_ceil(2).max(1) as f32;
let chroma_h = h.div_ceil(2).max(1) as f32;
match color_info.chroma_siting {
ChromaSiting::Center => (0.0, 0.0),
ChromaSiting::Left => (-0.5 / chroma_w, 0.0),
ChromaSiting::TopLeft => (-0.5 / chroma_w, -0.5 / chroma_h),
}
} else {
(0.0, 0.0)
};
let inst = Nv12Instance {
xywh: ndc_center,
uv: uv_rect,
color: tint.to_linear(),
uv_x_offset,
uv_y_offset,
fwd_mat,
};
if nv12_run
.as_ref()
.is_some_and(|(run_handle, run_filter, _)| {
*run_handle != *handle || *run_filter != *filter
})
{
flush_image_runs!();
}
match &mut nv12_run {
Some((run_handle, run_filter, instances))
if *run_handle == *handle && *run_filter == *filter =>
{
instances.push(inst)
}
_ => nv12_run = Some((*handle, *filter, vec![inst])),
}
} else {
if nv12_run.is_some() {
flush_image_runs!();
}
let tile = matches!(fit, ImageFit::Tile);
let inst = GlyphInstance {
xywh: ndc_center,
uv: uv_rect,
color: tint.to_linear(),
fwd_mat,
};
if image_run.as_ref().is_some_and(
|(run_handle, run_filter, run_tile, _)| {
*run_handle != *handle
|| *run_filter != *filter
|| *run_tile != tile
},
) {
flush_image_runs!();
}
match &mut image_run {
Some((run_handle, run_filter, run_tile, instances))
if *run_handle == *handle
&& *run_filter == *filter
&& *run_tile == tile =>
{
instances.push(inst)
}
_ => image_run = Some((*handle, *filter, tile, vec![inst])),
}
}
}
SceneNode::Coverage {
rect,
handle,
color,
} => {
flush_batch!();
let Some((tile_w, tile_h)) = self.coverage_dimensions(*handle) else {
log::warn!("Coverage handle {handle} not found");
continue;
};
let draw_rect = repose_core::Rect {
x: rect.x,
y: rect.y,
w: tile_w as f32,
h: tile_h as f32,
};
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
if !visible(affine_aabb(current_transform, &draw_rect), clip) {
continue;
}
let (ndc_center, fwd_mat) = rect_to_instance_ndc(
draw_rect,
current_transform,
current_target_size.0,
current_target_size.1,
);
let inst = GlyphInstance {
xywh: ndc_center,
uv: [0.0, 1.0, 1.0, 0.0],
color: color.to_linear(),
fwd_mat,
};
if let Some((off, _)) =
self.glyph_color
.upload(&self.device, &self.queue, encoder, &[inst])
{
current_pass.cmds.push(Cmd::Coverage {
off,
cnt: 1,
handle: *handle,
});
}
}
SceneNode::PushClip { rect, radius, op } => {
flush_batch!();
let is_diff = matches!(op, repose_core::ClipOp::Difference);
let t_identity = Transform::identity();
let current_transform = transform_stack.last().unwrap_or(&t_identity);
let transformed = affine_aabb(current_transform, rect);
let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
let next_scissor = if is_diff {
top
} else {
intersect(top, transformed)
};
scissor_stack.push(next_scissor);
let scissor = to_scissor(
&next_scissor,
current_target_size.0 as u32,
current_target_size.1 as u32,
);
let has_inverse = active_clips.iter().any(ActiveClip::difference);
let mut blocked =
current_pass.active_scissor.is_none() || (has_inverse && !is_diff);
if has_inverse && is_diff {
log::error!(
"unsupported clip nesting: Difference cannot contain a Difference clip"
);
blocked = true;
}
let has_area = transformed.x.is_finite()
&& transformed.y.is_finite()
&& transformed.w.is_finite()
&& transformed.h.is_finite()
&& transformed.w > 0.0
&& transformed.h > 0.0;
let mut applied = false;
let mut off = 0;
if !blocked && has_area && (is_diff || scissor.2 > 0 && scissor.3 > 0) {
let clip_ndc_tl = to_ndc(
transformed.x,
transformed.y,
transformed.w,
transformed.h,
current_target_size.0,
current_target_size.1,
);
let inst = ClipInstance {
xywh: [
clip_ndc_tl[0] + clip_ndc_tl[2] * 0.5,
clip_ndc_tl[1] + clip_ndc_tl[3] * 0.5,
clip_ndc_tl[2],
clip_ndc_tl[3],
],
radii: radius.map(|r| r.0),
fwd_mat: [1.0, 0.0, 0.0, 1.0],
};
let bytes = bytemuck::bytes_of(&inst);
if self
.clip_ring
.grow_to_fit(
&self.device,
encoder,
u64::try_from(bytes.len()).unwrap_or(u64::MAX),
)
.is_ok()
&& let Ok(new_off) = self.clip_ring.alloc_write(&self.queue, bytes)
{
off = new_off;
applied = true;
current_pass.cmds.push(Cmd::ClipPush {
off,
cnt: 1,
scissor,
difference: is_diff,
applied,
});
} else {
blocked = !is_diff;
}
} else if !is_diff {
blocked = true;
}
current_pass.active_scissor = if blocked {
None
} else {
(scissor.2 > 0 && scissor.3 > 0).then_some(scissor)
};
active_clips.push(ActiveClip::Rect {
off,
cnt: u32::from(applied),
rect: transformed,
radii: radius.map(|r| r.0),
scissor,
difference: is_diff,
applied,
blocked,
});
}
SceneNode::PopClip => {
flush_batch!();
if scissor_stack.is_empty() || active_clips.is_empty() {
log::error!("PopClip does not match an active clip");
} else {
scissor_stack.pop();
}
let clip = active_clips.pop();
let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
let scissor = to_scissor(
&top,
current_target_size.0 as u32,
current_target_size.1 as u32,
);
current_pass.active_scissor =
(scissor.2 > 0 && scissor.3 > 0).then_some(scissor);
match clip {
Some(ActiveClip::Rect {
off,
cnt,
scissor: stencil_scissor,
difference,
applied,
..
}) if applied && cnt > 0 => {
current_pass.cmds.push(Cmd::ClipPop {
off,
cnt,
scissor,
stencil_scissor,
difference,
applied: true,
});
}
Some(ActiveClip::Vector { .. }) => {
log::error!("PopClip matched a vector clip");
}
None => {}
_ => {}
}
}
SceneNode::Shadow {
rect,
radius,
elevation: _,
color,
} => {
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
if !visible(affine_aabb(current_transform, rect), clip) {
continue;
}
flush_if_prim_changed!("rect", &self.rects);
let (ndc, fwd_mat) = rect_to_instance_ndc(
*rect,
current_transform,
current_target_size.0,
current_target_size.1,
);
let (brush_type, color0, _color1, _grad_p0, _grad_p1) =
brush_to_instance_fields(&Brush::Solid(*color));
batch.rects.push(RectInstance {
xywh: ndc,
radii: radius.map(|r| r.0),
brush_type,
grad_kind: 0,
_pad: [0.0; 2],
color0,
color1: [0.0; 4],
grad_p0: [0.0; 2],
grad_p1: [0.0; 2],
tile_mode: 0,
_pad2: [0.0; 3],
fwd_mat,
});
}
SceneNode::PushTransform { transform } => {
flush_batch!(); if transform.has_perspective() {
let top = *transform_stack.last().unwrap_or(&t_identity);
self.push_perspective_layer(
*transform,
top,
&mut transform_stack,
&mut scissor_stack,
&mut root_clip_rect,
&mut current_target_size,
&mut current_pass,
&mut active_clips,
&mut passes,
&mut target_stack,
&mut flatten_stack,
&mut flatten_id_head,
&mut next_pass_id,
&mut flatten_ids_used,
encoder,
);
} else {
let combined = current_transform.combine(transform);
transform_stack.push(combined);
}
}
SceneNode::PopTransform => {
flush_batch!(); if let Some(rec) = flatten_stack.last() {
if transform_stack.len() == rec.stack_len + 2 {
let rec = flatten_stack.pop().expect("checked above");
transform_stack.pop();
transform_stack.pop();
self.pop_perspective_layer(
rec,
&mut scissor_stack,
&mut root_clip_rect,
&mut current_target_size,
&mut current_pass,
&mut active_clips,
&mut passes,
&mut target_stack,
&mut next_pass_id,
encoder,
);
continue;
}
}
transform_stack.pop();
}
SceneNode::BeginLayer {
rect,
layer_id,
alpha,
blur_radius_x,
blur_radius_y,
rectangle_edge,
} => {
flush_batch!();
let width_f = rect.w.round();
let height_f = rect.h.round();
let max_dimension = self.device.limits().max_texture_dimension_2d as f32;
if !width_f.is_finite()
|| !height_f.is_finite()
|| width_f < 1.0
|| height_f < 1.0
|| width_f > max_dimension
|| height_f > max_dimension
{
log::warn!("BeginLayer dimensions are invalid");
continue;
}
let width = width_f as u32;
let height = height_f as u32;
let parent_scissors =
std::mem::replace(&mut scissor_stack, Vec::with_capacity(8));
let parent_root_clip = std::mem::replace(
&mut root_clip_rect,
repose_core::Rect {
x: 0.0,
y: 0.0,
w: width as f32,
h: height as f32,
},
);
scissor_stack.push(root_clip_rect);
let layer_pass_id = next_pass_id;
next_pass_id += 1;
let parent_clips = std::mem::take(&mut active_clips);
let mut layer_pass = Pass {
id: layer_pass_id,
target: PassTarget::Layer(*layer_id),
initial_scissor: (0, 0, width, height),
active_scissor: Some((0, 0, width, height)),
clear_color: Some([0.0, 0.0, 0.0, 0.0]),
cmds: Vec::new(),
};
let parent_transform = *current_transform;
active_clips = self.replay_active_clips(
&parent_clips,
&parent_transform,
(rect.x, rect.y),
(width as f32, height as f32),
&mut layer_pass,
encoder,
);
let saved = std::mem::replace(&mut current_pass, layer_pass);
if let Some(top) = transform_stack.last_mut() {
*top = Transform::identity();
}
layer_stack.push(LayerState {
layer_id: *layer_id,
parent_target: saved.target,
parent_scissors,
parent_root_clip,
parent_size: current_target_size,
parent_transform,
parent_scissor: saved.active_scissor,
parent_clips,
alpha: *alpha,
blur: (blur_radius_x.0, blur_radius_y.0),
rectangle_edge: *rectangle_edge,
});
passes.push(saved);
if self.get_or_create_layer(*layer_id, width, height, *rect, false) {
self.producer_layer_ids.insert(*layer_id);
}
current_target_size = (width as f32, height as f32);
}
SceneNode::EndLayer { layer_id } => {
flush_batch!();
if layer_stack.last().map(|state| state.layer_id) != Some(*layer_id) {
log::warn!("EndLayer {} does not match active layer", layer_id);
continue;
}
let state = layer_stack.pop().expect("checked above");
scissor_stack = state.parent_scissors;
root_clip_rect = state.parent_root_clip;
active_clips = state.parent_clips;
if let Some(top) = transform_stack.last_mut() {
*top = state.parent_transform;
}
current_target_size = state.parent_size;
let resumed_pass_id = next_pass_id;
next_pass_id += 1;
let saved = std::mem::replace(
&mut current_pass,
Pass {
id: resumed_pass_id,
target: state.parent_target,
initial_scissor: state.parent_scissor.unwrap_or((0, 0, 1, 1)),
active_scissor: state.parent_scissor,
clear_color: None,
cmds: Vec::new(),
},
);
passes.push(saved);
let Some(layer) = self.layer_pool.get(layer_id).cloned() else {
continue;
};
let layer_rect = repose_core::Rect {
x: layer.rect_px.0,
y: layer.rect_px.1,
w: layer.rect_px.2,
h: layer.rect_px.3,
};
let local_layer_rect = affine_aabb(&state.parent_transform, &layer_rect);
let (parent_width, parent_height) = state.parent_size;
let parent_clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
let composite_rect = if state.blur.0 > 0.0 || state.blur.1 > 0.0 {
repose_core::Rect {
x: local_layer_rect.x - state.blur.0 * 1.5,
y: local_layer_rect.y - state.blur.1 * 1.5,
w: local_layer_rect.w + state.blur.0 * 3.0,
h: local_layer_rect.h + state.blur.1 * 3.0,
}
} else {
local_layer_rect
};
let shadow_follows = shadow_layer_ids.contains(layer_id);
if !visible(composite_rect, parent_clip) {
continue;
}
if state.blur.0 > 0.0 || state.blur.1 > 0.0 {
let blur_x = state.blur.0 * 1.5;
let blur_y = state.blur.1 * 1.5;
let rect = repose_core::Rect {
x: local_layer_rect.x - blur_x,
y: local_layer_rect.y - blur_y,
w: local_layer_rect.w + blur_x * 2.0,
h: local_layer_rect.h + blur_y * 2.0,
};
let ndc =
to_ndc(rect.x, rect.y, rect.w, rect.h, parent_width, parent_height);
let inst = BlurInstance {
xywh: [ndc[0] + ndc[2] * 0.5, ndc[1] + ndc[3] * 0.5, ndc[2], ndc[3]],
uv: [0.0, 0.0, 1.0, 1.0],
color: [1.0, 1.0, 1.0, state.alpha],
blur_uv: [
(state.blur.0 * 1.5) / layer.width.max(1) as f32,
(state.blur.1 * 1.5) / layer.height.max(1) as f32,
],
fwd_mat: [1.0, 0.0, 0.0, 1.0],
edge_mode: if state.rectangle_edge { 0 } else { 1 },
_pad: [0.0; 3],
};
if self
.blur_ring
.grow_to_fit(
&self.device,
encoder,
std::mem::size_of::<BlurInstance>() as u64,
)
.is_ok()
&& let Ok(off) = self
.blur_ring
.alloc_write(&self.queue, bytemuck::bytes_of(&inst))
{
let command = Cmd::CompositeBlur {
off,
cnt: 1,
layer_id: *layer_id,
};
if shadow_follows {
pending_layer_composite = Some(command);
} else {
current_pass.cmds.push(command);
}
}
} else {
let ndc = to_ndc(
local_layer_rect.x,
local_layer_rect.y,
local_layer_rect.w,
local_layer_rect.h,
parent_width,
parent_height,
);
let inst = GlyphInstance {
xywh: [ndc[0] + ndc[2] * 0.5, ndc[1] + ndc[3] * 0.5, ndc[2], ndc[3]],
uv: [0.0, 1.0, 1.0, 0.0],
color: [1.0, 1.0, 1.0, state.alpha],
fwd_mat: [1.0, 0.0, 0.0, 1.0],
};
if let Some((off, cnt)) =
self.glyph_color
.upload(&self.device, &self.queue, encoder, &[inst])
{
let command = Cmd::CompositeLayer {
off,
cnt,
layer_id: *layer_id,
};
if shadow_follows {
pending_layer_composite = Some(command);
} else {
current_pass.cmds.push(command);
}
}
}
}
SceneNode::CompositeShadow {
layer_id,
blur_px,
offset_px,
color,
} => {
if let Some(layer) = self.layer_pool.get(layer_id).cloned() {
let layer_rect = repose_core::Rect {
x: layer.rect_px.0,
y: layer.rect_px.1,
w: layer.rect_px.2,
h: layer.rect_px.3,
};
let local_layer_rect =
affine_aabb(transform_stack.last().unwrap_or(&t_identity), &layer_rect);
let blur_x = blur_px.0.max(0.0) * 1.5;
let blur_y = blur_px.0.max(0.0) * 1.5;
let sx = local_layer_rect.x + offset_px.0.0 - blur_x;
let sy = local_layer_rect.y + offset_px.1.0 - blur_y;
let sw = local_layer_rect.w + blur_x * 2.0;
let sh = local_layer_rect.h + blur_y * 2.0;
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
if !visible(
repose_core::Rect {
x: sx,
y: sy,
w: sw,
h: sh,
},
clip,
) {
flush_pending_layer!();
continue;
}
flush_batch!();
let bw_uv = blur_x / layer.width.max(1) as f32;
let bh_uv = blur_y / layer.height.max(1) as f32;
let ndc_tl =
to_ndc(sx, sy, sw, sh, current_target_size.0, current_target_size.1);
let inst = BlurInstance {
xywh: [
ndc_tl[0] + ndc_tl[2] * 0.5,
ndc_tl[1] + ndc_tl[3] * 0.5,
ndc_tl[2],
ndc_tl[3],
],
uv: [0.0, 0.0, 1.0, 1.0],
color: color.to_linear(),
blur_uv: [bw_uv, bh_uv],
fwd_mat: [1.0, 0.0, 0.0, 1.0],
edge_mode: 0,
_pad: [0.0; 3],
};
if self
.blur_ring
.grow_to_fit(
&self.device,
encoder,
std::mem::size_of::<BlurInstance>() as u64,
)
.is_ok()
&& let Ok(off) = self
.blur_ring
.alloc_write(&self.queue, bytemuck::bytes_of(&inst))
{
current_pass.cmds.push(Cmd::CompositeShadow {
off,
cnt: 1,
layer_id: *layer_id,
});
}
}
flush_pending_layer!();
}
SceneNode::VectorMesh {
mesh,
transform,
paint,
clip: _,
blend,
} => {
let affine = combine_mesh_affine(current_transform, *transform);
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
if !visible(mesh_aabb(mesh, affine), clip) {
continue;
}
flush_batch!();
let paint_alpha = match paint {
repose_core::PaintDesc::Linear {
start_color,
end_color,
..
}
| repose_core::PaintDesc::Radial {
start_color,
end_color,
..
}
| repose_core::PaintDesc::Sweep {
start_color,
end_color,
..
} => Some(start_color.3.min(end_color.3) as f32 / 255.0),
_ => None,
};
let translucent_fixed_blend =
matches!(
blend,
repose_core::BlendMode::Add
| repose_core::BlendMode::Multiply
| repose_core::BlendMode::Screen
| repose_core::BlendMode::Darken
| repose_core::BlendMode::Lighten
) && (mesh.vertices.iter().any(|vertex| vertex.color[3] < 0.9999)
|| paint_alpha.is_some_and(|alpha| alpha < 0.9999));
if blend.needs_isolation() || translucent_fixed_blend {
let blend_scissor = to_scissor(
&scissor_stack.last().copied().unwrap_or(root_clip_rect),
current_target_size.0 as u32,
current_target_size.1 as u32,
);
self.emit_isolated_blend(
mesh.clone(),
*transform,
*paint,
*blend,
current_transform,
&mut current_pass,
&active_clips,
&mut passes,
&mut flatten_id_head,
&mut next_pass_id,
&mut flatten_ids_used,
blend_scissor,
encoder,
fb_w,
fb_h,
);
} else {
let t_identity = Transform::identity();
let current_transform = transform_stack.last().unwrap_or(&t_identity);
self.emit_vector_mesh(
current_transform,
mesh,
*transform,
paint,
*blend,
&mut current_pass.cmds,
);
}
}
SceneNode::VectorOverlay { meshes } => {
let clip = scissor_stack.last().copied().unwrap_or(root_clip_rect);
let has_visible = meshes
.iter()
.any(|mesh| visible(mesh_aabb(mesh, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]), clip));
if !has_visible {
continue;
}
flush_batch!();
for (index, m) in meshes.iter().enumerate() {
if !visible(mesh_aabb(m, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]), clip) {
continue;
}
let Some(mesh_gpu) = self.upload_overlay_mesh(meshes, index) else {
continue;
};
let Some(uoff) = self.alloc_mesh_uniform(MeshUniform::identity()) else {
continue;
};
current_pass.cmds.push(Cmd::VectorOverlay {
mesh: mesh_gpu,
uoff,
});
}
}
SceneNode::PushVectorClip { mesh, op } => {
flush_batch!();
let difference = matches!(op, repose_core::ClipOp::Difference);
let t_identity = Transform::identity();
let current_transform = transform_stack.last().unwrap_or(&t_identity);
let affine =
combine_mesh_affine(current_transform, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
let aabb = mesh_aabb(mesh, affine);
let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
let next = if difference {
top
} else {
intersect(top, aabb)
};
scissor_stack.push(next);
let scissor = to_scissor(
&next,
current_target_size.0 as u32,
current_target_size.1 as u32,
);
let has_inverse = active_clips.iter().any(ActiveClip::difference);
let mut blocked = current_pass.active_scissor.is_none() || has_inverse;
if has_inverse {
log::error!(
"unsupported clip nesting: a Difference clip cannot contain another vector clip"
);
}
let has_area = aabb.x.is_finite()
&& aabb.y.is_finite()
&& aabb.w.is_finite()
&& aabb.h.is_finite()
&& aabb.w > 0.0
&& aabb.h > 0.0;
let mut applied = false;
let mut geometry = None;
let mut uoff = 0;
if !blocked
&& has_area
&& (if difference {
visible(aabb, top)
} else {
scissor.2 > 0 && scissor.3 > 0
})
{
if let Some(mesh_gpu) = self.upload_mesh_geometry(mesh)
&& let Some(slot) = self.alloc_mesh_uniform(mesh_uniform_from_paint(
affine,
&repose_core::PaintDesc::Solid,
))
{
geometry = Some(mesh_gpu.clone());
uoff = slot;
applied = true;
current_pass.cmds.push(Cmd::VectorClipPush {
mesh: mesh_gpu,
uoff,
scissor,
difference,
applied,
});
} else {
blocked = !difference;
}
} else if !difference {
blocked = true;
}
current_pass.active_scissor = if blocked {
None
} else {
(scissor.2 > 0 && scissor.3 > 0).then_some(scissor)
};
active_clips.push(ActiveClip::Vector {
mesh: geometry,
source: mesh.clone(),
uoff,
affine,
scissor,
difference,
applied,
blocked,
});
}
SceneNode::PopVectorClip => {
flush_batch!();
if scissor_stack.is_empty() || active_clips.is_empty() {
log::error!("PopVectorClip does not match an active clip");
} else {
scissor_stack.pop();
}
let clip = active_clips.pop();
let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
let scissor = to_scissor(
&top,
current_target_size.0 as u32,
current_target_size.1 as u32,
);
current_pass.active_scissor =
(scissor.2 > 0 && scissor.3 > 0).then_some(scissor);
match clip {
Some(ActiveClip::Vector {
mesh: Some(mesh),
uoff,
scissor: stencil_scissor,
difference,
applied,
..
}) if applied && mesh.vertex_count > 0 && mesh.index_count > 0 => {
current_pass.cmds.push(Cmd::VectorClipPop {
mesh,
uoff,
scissor,
stencil_scissor,
difference,
applied: true,
});
}
Some(ActiveClip::Rect { .. }) => {
log::error!("PopVectorClip matched a rounded-rect clip");
}
None => {}
_ => {}
}
}
SceneNode::Callback { rect, payload } => {
let t = transform_stack
.last()
.copied()
.unwrap_or(Transform::identity());
let transformed = affine_aabb(&t, rect);
let top = scissor_stack.last().copied().unwrap_or(root_clip_rect);
let clip_rect = intersect(transformed, top);
let scissor = to_scissor(
&clip_rect,
current_target_size.0 as u32,
current_target_size.1 as u32,
);
let restore_scissor = to_scissor(
&top,
current_target_size.0 as u32,
current_target_size.1 as u32,
);
if current_pass.active_scissor.is_none()
|| !visible(transformed, top)
|| scissor.2 == 0
|| scissor.3 == 0
{
continue;
}
flush_batch!();
current_pass.cmds.push(Cmd::Callback {
rect: transformed,
clip_rect,
scissor,
restore_scissor,
callback_id: Arc::as_ptr(payload) as *const () as usize,
payload: payload.clone(),
});
}
_ => {}
}
}
flush_pending_layer!();
flush_batch!();
passes.push(current_pass);
{
let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
let mut prepare_list: Vec<(usize, Arc<Callback>)> = Vec::new();
for pass in &passes {
for command in &pass.cmds {
if let Cmd::Callback { payload, .. } = command
&& payload.downcast_ref::<Callback>().is_some()
{
let ptr = Arc::as_ptr(payload) as *const () as usize;
if seen.insert(ptr)
&& let Ok(cb_arc) = payload.clone().downcast::<Callback>()
{
prepare_list.push((ptr, cb_arc));
}
}
}
}
if !prepare_list.is_empty() {
let mut callback_targets: HashMap<usize, Vec<(PassTarget, ScreenDescriptor)>> =
HashMap::new();
for pass in &passes {
let (size, target_format, sample_count) = match pass.target {
PassTarget::Surface => (
[self.output_width, self.output_height],
if self.working_space {
wgpu::TextureFormat::Rgba16Float
} else {
self.output_format
},
self.active_surface_msaa_samples(),
),
PassTarget::Layer(layer_id) => {
let layer = self.layer_pool.get(&layer_id);
(
layer.map_or([self.output_width, self.output_height], |layer| {
[layer.width, layer.height]
}),
self.layer_target_format(),
1,
)
}
};
for command in &pass.cmds {
if let Cmd::Callback { payload, .. } = command {
let descriptor = ScreenDescriptor {
size_in_pixels: size,
pixels_per_point: self.pixels_per_point,
target_format,
sample_count,
};
let targets = callback_targets
.entry(Arc::as_ptr(payload) as *const () as usize)
.or_default();
if !targets.iter().any(|(target, target_desc)| {
*target == pass.target
&& target_desc.size_in_pixels == descriptor.size_in_pixels
&& target_desc.pixels_per_point == descriptor.pixels_per_point
&& target_desc.target_format == descriptor.target_format
&& target_desc.sample_count == descriptor.sample_count
}) {
targets.push((pass.target, descriptor));
}
}
}
}
let default_descriptor = ScreenDescriptor {
size_in_pixels: [self.output_width, self.output_height],
pixels_per_point: self.pixels_per_point,
target_format: if self.working_space {
wgpu::TextureFormat::Rgba16Float
} else {
self.output_format
},
sample_count: self.active_surface_msaa_samples(),
};
let default_descriptors = [(PassTarget::Surface, default_descriptor)];
let mut prepare_encoder =
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("callback prepare"),
});
let mut finish_encoder =
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("callback finish prepare"),
});
let mut active_callback_scopes = HashSet::new();
for (key, cb) in &prepare_list {
let descriptors = callback_targets
.get(key)
.map(Vec::as_slice)
.unwrap_or(&default_descriptors);
for &(target, screen_desc) in descriptors {
let scope = callback_scope_key(cb, *key, target, &screen_desc);
active_callback_scopes.insert(scope);
if !scope.callback.reusable
&& self
.callback_scope_payloads
.get(&scope)
.is_some_and(|payload| payload.upgrade().is_none())
{
self.remove_callback_scope(&scope);
}
let payload: Weak<Callback> = Arc::downgrade(cb);
self.callback_scope_payloads.insert(scope, payload);
}
}
let mut prepare_buffers = Vec::new();
let mut finish_buffers = Vec::new();
for (key, cb) in &prepare_list {
let descriptors = callback_targets
.get(key)
.map(Vec::as_slice)
.unwrap_or(&default_descriptors);
for &(target, screen_desc) in descriptors {
let scope = callback_scope_key(cb, *key, target, &screen_desc);
self.touch_callback_scope(scope);
if !self.callback_scoped_resources.contains_key(&scope)
&& self.callback_scoped_resources.len() >= MAX_CALLBACK_SCOPES
{
self.evict_callback_scope(&active_callback_scopes);
}
let resources = self.callback_scoped_resources.entry(scope).or_default();
prepare_buffers.extend(cb.0.prepare(
&self.device,
&self.queue,
&mut prepare_encoder,
&screen_desc,
resources,
));
}
}
for (key, cb) in &prepare_list {
let descriptors = callback_targets
.get(key)
.map(Vec::as_slice)
.unwrap_or(&default_descriptors);
for &(target, screen_desc) in descriptors {
let scope = callback_scope_key(cb, *key, target, &screen_desc);
self.touch_callback_scope(scope);
if let Some(resources) = self.callback_scoped_resources.get_mut(&scope) {
finish_buffers.extend(cb.0.finish_prepare(
&self.device,
&self.queue,
&mut finish_encoder,
&screen_desc,
resources,
));
}
}
}
let mut callback_buffers =
Vec::with_capacity(2 + prepare_buffers.len() + finish_buffers.len());
callback_buffers.push(prepare_encoder.finish());
callback_buffers.extend(prepare_buffers);
callback_buffers.push(finish_encoder.finish());
callback_buffers.extend(finish_buffers);
self.queue.submit(callback_buffers);
}
}
let globals_bytes = std::mem::size_of::<Globals>() as u64;
if !self.ensure_globals_staging(passes.len()) {
return;
}
for (i, pass) in passes.iter().enumerate() {
let (target_w, target_h) = match pass.target {
PassTarget::Surface => (fb_w, fb_h),
PassTarget::Layer(layer_id) => {
let lt = self.layer_pool.get(&layer_id);
(
lt.map_or(fb_w, |l| l.width as f32),
lt.map_or(fb_h, |l| l.height as f32),
)
}
};
self.queue.write_buffer(
&self.globals_staging,
(i as u64) * globals_bytes,
bytemuck::bytes_of(&make_globals(target_w, target_h)),
);
}
let bind_mask = self.atlas_bind_group_mask();
let bind_color = self.atlas_bind_group_color();
let mut clip_depth: u32 = 0;
let mut clip_depth_stack: Vec<u32> = Vec::new();
let snapshot_source = target_texture.cloned();
for (pass_index, pass) in std::mem::take(&mut passes).into_iter().enumerate() {
let needs_snapshot = pass
.cmds
.iter()
.any(|c| matches!(c, Cmd::BlendLayer { .. }));
if needs_snapshot {
let copies = std::mem::take(&mut self.blend_copies);
let has_surface_copy = copies.iter().any(|copy| copy.target == PassTarget::Surface);
let mut resolved_surface = None;
if has_surface_copy
&& !self.working_space
&& let (Some(msaa_view), Some(resolve_view)) =
(self.msaa_view.as_ref(), self.surface_resolve_view.as_ref())
{
let resolve_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("surface blend resolve"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa_view,
resolve_target: Some(resolve_view),
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
drop(resolve_pass);
resolved_surface = self.surface_resolve_tex.clone();
}
let mut remaining = Vec::with_capacity(copies.len());
for copy in copies {
if copy.pass_id != pass.id {
remaining.push(copy);
continue;
}
let BlendCopy {
blend_id,
target,
region,
..
} = copy;
let (src_tex, tw, th) = match target {
PassTarget::Layer(parent_id) => match self.layer_pool.get(&parent_id) {
Some(lt) => (lt.texture.clone(), lt.width, lt.height),
None => continue,
},
PassTarget::Surface => {
let texture = if self.working_space {
self.ws_tex.clone()
} else if self.msaa_view.is_some() {
resolved_surface.clone()
} else {
snapshot_source.clone()
};
match texture {
Some(texture) => (texture, self.output_width, self.output_height),
None => {
self.remove_blend_snapshot(blend_id);
continue;
}
}
}
};
let Some(dst_tex) = self.blend_snapshot_texture(blend_id) else {
continue;
};
if !src_tex.usage().contains(wgpu::TextureUsages::COPY_SRC)
|| !dst_tex.usage().contains(wgpu::TextureUsages::COPY_DST)
{
self.remove_blend_snapshot(blend_id);
continue;
}
let Some((sx, sy, cw, ch)) = checked_copy_region(region, tw, th) else {
self.remove_blend_snapshot(blend_id);
continue;
};
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: &src_tex,
mip_level: 0,
origin: wgpu::Origin3d { x: sx, y: sy, z: 0 },
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: &dst_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d {
width: cw,
height: ch,
depth_or_array_layers: 1,
},
);
}
self.blend_copies = remaining;
}
let (color_view, resolve_target, depth_stencil_view, is_layer, working_target) =
match pass.target {
PassTarget::Surface => {
let swap_view = target_view.clone();
let use_ws = self.working_space && self.ws_view.is_some();
let (color, resolve) = if use_ws {
let ws_view = self.ws_view.as_ref().unwrap();
if let Some(msaa_view) = &self.ws_msaa_view {
(msaa_view.clone(), Some(ws_view.clone()))
} else {
(ws_view.clone(), None)
}
} else if let Some(msaa_view) = &self.msaa_view {
(msaa_view.clone(), Some(swap_view))
} else {
(swap_view, None)
};
(
color,
resolve,
self.depth_stencil_view.clone(),
false,
use_ws,
)
}
PassTarget::Layer(layer_id) => {
if let Some(lt) = self.layer_pool.get(&layer_id) {
(
lt.view.clone(),
None,
lt.depth_stencil_view.clone(),
true,
false,
)
} else {
log::warn!("missing layer target {layer_id}");
continue;
}
}
};
encoder.copy_buffer_to_buffer(
&self.globals_staging,
(pass_index as u64) * globals_bytes,
&self.globals_buf,
0,
globals_bytes,
);
if is_layer {
clip_depth_stack.push(clip_depth);
clip_depth = 0;
}
let (tw, th) = match pass.target {
PassTarget::Surface => (self.output_width, self.output_height),
PassTarget::Layer(layer_id) => self
.layer_pool
.get(&layer_id)
.map(|l| (l.width, l.height))
.unwrap_or((self.output_width, self.output_height)),
};
let initial_scissor = clamp_scissor(
pass.initial_scissor.0,
pass.initial_scissor.1,
pass.initial_scissor.2,
pass.initial_scissor.3,
tw,
th,
);
let mut active_scissor = pass
.active_scissor
.map(|scissor| clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th));
if active_scissor.is_some_and(|scissor| scissor.2 == 0 || scissor.3 == 0) {
active_scissor = None;
}
let pipes: &Pipelines = if is_layer {
if self.working_space {
&self.working_space_layer_pipes
} else {
&self.layer_pipes
}
} else if working_target {
&self.working_space_pipes
} else {
&self.surface_pipes
};
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &color_view,
resolve_target: resolve_target.as_ref(),
ops: wgpu::Operations {
load: match pass.clear_color {
Some(c) => wgpu::LoadOp::Clear(wgpu::Color {
r: c[0] as f64,
g: c[1] as f64,
b: c[2] as f64,
a: c[3] as f64,
}),
None => wgpu::LoadOp::Load,
},
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth_stencil_view,
depth_ops: None,
stencil_ops: Some(wgpu::Operations {
load: if pass.clear_color.is_some() {
wgpu::LoadOp::Clear(STENCIL_BASE)
} else {
wgpu::LoadOp::Load
},
store: wgpu::StoreOp::Store,
}),
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rpass.set_bind_group(0, &self.globals_bind, &[]);
rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
let initial_draw_scissor = if initial_scissor.2 == 0 || initial_scissor.3 == 0 {
(0, 0, 1, 1)
} else {
initial_scissor
};
rpass.set_scissor_rect(
initial_draw_scissor.0,
initial_draw_scissor.1,
initial_draw_scissor.2,
initial_draw_scissor.3,
);
macro_rules! draw_simple {
($pipeline:expr, $ring:expr, $inst:ty, $off:ident, $n:ident) => {{
rpass.set_pipeline($pipeline);
let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
rpass.draw(0..6, 0..$n);
}};
}
macro_rules! draw_with_bind {
($pipeline:expr, $ring:expr, $inst:ty, $bind:expr, $off:ident, $n:ident) => {{
rpass.set_pipeline($pipeline);
rpass.set_bind_group(1, $bind, &[]);
let bytes = ($n as u64) * std::mem::size_of::<$inst>() as u64;
rpass.set_vertex_buffer(0, $ring.buf.slice($off..$off + bytes));
rpass.draw(0..6, 0..$n);
}};
}
macro_rules! draw_indexed_mesh {
($pipeline:expr, $uoff:ident, $mesh:ident) => {{
rpass.set_pipeline($pipeline);
rpass.set_bind_group(1, &self.mesh_bind, &[$uoff as u32]);
let vbytes =
($mesh.vertex_count as u64) * std::mem::size_of::<MeshVertex>() as u64;
rpass.set_vertex_buffer(0, $mesh.vertices.slice(0..vbytes));
let ibytes = ($mesh.index_count as u64) * std::mem::size_of::<u32>() as u64;
rpass.set_index_buffer(
$mesh.indices.slice(0..ibytes),
wgpu::IndexFormat::Uint32,
);
rpass.draw_indexed(0..$mesh.index_count, 0, 0..1);
}};
}
for cmd in pass.cmds {
if active_scissor.is_none()
&& !matches!(
&cmd,
Cmd::ClipPush { .. }
| Cmd::ClipPop { .. }
| Cmd::VectorClipPush { .. }
| Cmd::VectorClipPop { .. }
)
{
continue;
}
match cmd {
Cmd::ClipPush {
off,
cnt: n,
scissor,
difference,
applied,
} => {
let scissor =
clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
let usable = applied && scissor.2 > 0 && scissor.3 > 0;
if usable {
rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
active_scissor = Some(scissor);
rpass.set_pipeline(if difference {
&pipes.clip_dec
} else {
&pipes.clip_bin
});
let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
rpass.draw(0..6, 0..n);
if !difference {
clip_depth = (clip_depth + 1).min(STENCIL_MAX_DEPTH);
}
} else {
active_scissor = None;
}
rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
}
Cmd::ClipPop {
off,
cnt: n,
scissor,
stencil_scissor,
difference,
applied,
} => {
let scissor =
clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
let dec = clamp_scissor(
stencil_scissor.0,
stencil_scissor.1,
stencil_scissor.2,
stencil_scissor.3,
tw,
th,
);
if scissor.2 > 0 && scissor.3 > 0 {
rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
active_scissor = Some(scissor);
} else {
active_scissor = None;
}
if applied && n > 0 && dec.2 > 0 && dec.3 > 0 {
rpass.set_scissor_rect(dec.0, dec.1, dec.2, dec.3);
rpass.set_pipeline(if difference {
&pipes.clip_bin
} else {
&pipes.clip_dec
});
let bytes = (n as u64) * std::mem::size_of::<ClipInstance>() as u64;
rpass.set_vertex_buffer(0, self.clip_ring.buf.slice(off..off + bytes));
rpass.draw(0..6, 0..n);
if scissor.2 > 0 && scissor.3 > 0 {
rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
}
if !difference {
clip_depth = clip_depth.saturating_sub(1);
}
}
rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
}
Cmd::Rect { off, cnt: n } => {
draw_simple!(&pipes.rects, self.rects.ring, RectInstance, off, n);
}
Cmd::Border { off, cnt: n } => {
draw_simple!(&pipes.borders, self.borders.ring, BorderInstance, off, n);
}
Cmd::GlyphsMask { off, cnt: n } => {
draw_with_bind!(
&pipes.text_mask,
self.glyph_mask.ring,
GlyphInstance,
&bind_mask,
off,
n
);
}
Cmd::GlyphsColor { off, cnt: n } => {
draw_with_bind!(
&pipes.text_color,
self.glyph_color.ring,
GlyphInstance,
&bind_color,
off,
n
);
}
Cmd::GlyphsVector { off, cnt: n } => {
if let Some(slug_pipe) = pipes.slug.as_ref() {
rpass.set_pipeline(slug_pipe);
let bytes = (n as u64) * std::mem::size_of::<slug::TessVertex>() as u64;
rpass.set_vertex_buffer(0, self.slug_ring.buf.slice(off..off + bytes));
rpass.draw(0..n, 0..1);
}
}
Cmd::ImageRgba {
off,
cnt: n,
handle,
filter,
tile,
} => {
let bind_opt = match self.images.get(&handle) {
Some(ImageTex::Rgba { binds, .. }) => Some(binds.get(filter, tile)),
Some(ImageTex::User { binds, .. }) => Some(binds.get(filter, tile)),
_ => None,
};
if let Some(bind) = bind_opt {
draw_with_bind!(
&pipes.image_rgba,
self.glyph_color.ring,
GlyphInstance,
bind,
off,
n
);
}
}
Cmd::Coverage {
off,
cnt: n,
handle,
} => {
if let Some(tile) = self.coverages.get(&handle) {
draw_with_bind!(
&pipes.coverage,
self.glyph_color.ring,
GlyphInstance,
&tile.bind,
off,
n
);
}
}
Cmd::ImageNv12 {
off,
cnt: n,
handle,
filter,
} => {
if let Some(ImageTex::Nv12 { binds, .. }) = self.images.get(&handle) {
draw_with_bind!(
&pipes.image_nv12,
self.nv12.ring,
Nv12Instance,
binds.get(filter, false),
off,
n
);
}
}
Cmd::Ellipse { off, cnt: n } => {
draw_simple!(&pipes.ellipses, self.ellipses.ring, EllipseInstance, off, n);
}
Cmd::EllipseBorder { off, cnt: n } => {
draw_simple!(
&pipes.ellipse_borders,
self.ellipse_borders.ring,
EllipseBorderInstance,
off,
n
);
}
Cmd::Arc { off, cnt: n } => {
draw_simple!(&pipes.arcs, self.arcs.ring, ArcInstance, off, n);
}
Cmd::CompositeLayer {
off,
cnt: n,
layer_id,
} => {
if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
draw_with_bind!(
&pipes.image_rgba,
self.glyph_color.ring,
GlyphInstance,
<.bind,
off,
n
);
}
}
Cmd::CompositeShadow {
off,
cnt: n,
layer_id,
} => {
if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
draw_with_bind!(
&pipes.blur,
self.blur_ring,
BlurInstance,
<.bind_linear,
off,
n
);
}
}
Cmd::CompositeBlur {
off,
cnt: n,
layer_id,
} => {
if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
draw_with_bind!(
&pipes.blur_content,
self.blur_ring,
BlurInstance,
<.bind_linear,
off,
n
);
}
}
Cmd::CompositeProjective {
off,
cnt: n,
layer_id,
} => {
if let Some(lt) = self.layer_pool.get(&layer_id).cloned() {
draw_with_bind!(
&pipes.projective_layer,
self.projective_ring,
ProjectiveInstance,
<.bind,
off,
n
);
}
}
Cmd::BlendLayer {
off,
cnt: n,
src_layer,
dst_layer,
parent,
scissor,
} => {
let src = self.layer_pool.get(&src_layer).cloned();
let dst = dst_layer.and_then(|id| self.blend_snapshots.get(&id).cloned());
let in_parent = match parent {
PassTarget::Surface => !is_layer,
PassTarget::Layer(id) => {
matches!(pass.target, PassTarget::Layer(pid) if pid == id)
}
};
if let (Some(src_lt), Some(dst_snap)) = (src, dst)
&& in_parent
{
let bytes = (n as u64) * std::mem::size_of::<BlendInstance>() as u64;
rpass.set_pipeline(&pipes.blend_layer);
let scissor =
clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
if scissor.2 > 0 && scissor.3 > 0 {
rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
rpass.set_bind_group(0, &self.globals_bind, &[]);
rpass.set_bind_group(1, &src_lt.bind, &[]);
rpass.set_bind_group(2, &dst_snap.bind, &[]);
rpass.set_vertex_buffer(
0,
self.blend_ring.buf.slice(off..off + bytes),
);
rpass.draw(0..6, 0..n);
}
}
}
Cmd::VectorMesh { mesh, uoff, blend } => {
let pipe = match blend {
repose_core::BlendMode::Add => &pipes.mesh_add,
repose_core::BlendMode::Multiply => &pipes.mesh_multiply,
repose_core::BlendMode::Screen => &pipes.mesh_screen,
repose_core::BlendMode::Darken => &pipes.mesh_darken,
repose_core::BlendMode::Lighten => &pipes.mesh_lighten,
_ => &pipes.mesh,
};
draw_indexed_mesh!(pipe, uoff, mesh);
}
Cmd::VectorOverlay { mesh, uoff } => {
rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
draw_indexed_mesh!(&pipes.mesh_overlay, uoff, mesh);
}
Cmd::VectorClipPush {
mesh,
uoff,
scissor,
difference,
applied,
} => {
let scissor =
clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
let usable = applied && scissor.2 > 0 && scissor.3 > 0;
if usable {
rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
active_scissor = Some(scissor);
let pipe = if difference {
&pipes.mesh_clip_dec
} else {
&pipes.mesh_clip_inc
};
draw_indexed_mesh!(pipe, uoff, mesh);
if !difference {
clip_depth = (clip_depth + 1).min(STENCIL_MAX_DEPTH);
}
} else {
active_scissor = None;
}
rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
}
Cmd::VectorClipPop {
mesh,
uoff,
scissor,
stencil_scissor,
difference,
applied,
} => {
let scissor =
clamp_scissor(scissor.0, scissor.1, scissor.2, scissor.3, tw, th);
let dec = clamp_scissor(
stencil_scissor.0,
stencil_scissor.1,
stencil_scissor.2,
stencil_scissor.3,
tw,
th,
);
if scissor.2 > 0 && scissor.3 > 0 {
rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
active_scissor = Some(scissor);
} else {
active_scissor = None;
}
if applied && dec.2 > 0 && dec.3 > 0 {
rpass.set_scissor_rect(dec.0, dec.1, dec.2, dec.3);
let pipe = if difference {
&pipes.mesh_clip_inc
} else {
&pipes.mesh_clip_dec
};
draw_indexed_mesh!(pipe, uoff, mesh);
if scissor.2 > 0 && scissor.3 > 0 {
rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
}
if !difference {
clip_depth = clip_depth.saturating_sub(1);
}
}
rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
}
Cmd::Callback {
rect,
clip_rect,
scissor,
restore_scissor,
callback_id,
payload,
} => {
if let Some(cb) = payload.downcast_ref::<Callback>() {
let vp_x = rect.x.ceil().max(0.0);
let vp_y = rect.y.ceil().max(0.0);
let vp_w = ((rect.x + rect.w).floor() - vp_x).max(1.0);
let vp_h = ((rect.y + rect.h).floor() - vp_y).max(1.0);
if vp_w > 0.0 && vp_h > 0.0 && clip_rect.w > 0.0 && clip_rect.h > 0.0 {
rpass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
rpass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0);
let info = repose_core::PaintCallbackInfo {
viewport: rect,
clip_rect,
pixels_per_point: self.pixels_per_point,
screen_size_px: [tw, th],
};
let descriptor = ScreenDescriptor {
size_in_pixels: [tw, th],
pixels_per_point: self.pixels_per_point,
target_format: if is_layer {
self.layer_target_format()
} else if working_target {
wgpu::TextureFormat::Rgba16Float
} else {
self.output_format
},
sample_count: if is_layer {
1
} else {
self.active_surface_msaa_samples()
},
};
let scope =
callback_scope_key(cb, callback_id, pass.target, &descriptor);
let resources = self
.callback_scoped_resources
.get(&scope)
.unwrap_or(&self.callback_resources);
let mut callback_pass = CallbackRenderPass::new(
&mut rpass,
descriptor.target_format,
descriptor.sample_count,
);
cb.0.paint(info, &mut callback_pass, resources);
rpass.set_viewport(0.0, 0.0, tw as f32, th as f32, 0.0, 1.0);
let restore = if restore_scissor.2 > 0 && restore_scissor.3 > 0 {
restore_scissor
} else {
(0, 0, 1, 1)
};
rpass.set_scissor_rect(restore.0, restore.1, restore.2, restore.3);
rpass.set_bind_group(0, &self.globals_bind, &[]);
rpass.set_stencil_reference(STENCIL_BASE + clip_depth);
}
} else {
log::warn!("Unknown paint callback payload");
}
}
}
}
if is_layer {
clip_depth = clip_depth_stack.pop().unwrap_or(0);
}
}
let transient_ids: HashSet<u32> = flatten_ids_used.iter().copied().collect();
let producer_ids = std::mem::take(&mut self.producer_layer_ids);
let stale_layers: Vec<u32> = self
.layer_pool
.iter()
.filter_map(|(id, _)| {
(!producer_ids.contains(id) && !transient_ids.contains(id)).then_some(*id)
})
.collect();
for id in stale_layers {
if let Some(layer) = self.layer_pool.remove(&id) {
self.layer_bytes_total = self.layer_bytes_total.saturating_sub(layer.bytes);
if layer.transient {
self.transient_layer_bytes_total =
self.transient_layer_bytes_total.saturating_sub(layer.bytes);
}
}
}
self.flatten_layer_ids = flatten_ids_used;
if self.working_space
&& let (Some(_ws_view), Some(ws_bind), Some(display_pipeline)) =
(&self.ws_view, &self.ws_bind, &self.display_pipeline)
{
let swap_view = target_view.clone();
let mut display_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("display transform"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &swap_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
display_pass.set_pipeline(display_pipeline);
display_pass.set_bind_group(1, ws_bind, &[]);
display_pass.draw(0..3, 0..1);
}
self.recycle_blend_snapshots();
}
pub fn render_to_view(
&mut self,
scene: &Scene,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
width: u32,
height: u32,
clear_color: Option<[f64; 4]>,
) {
self.resize(width, height);
if let Err(error) = self.begin_frame() {
log::error!("render_to_view: {error:#}");
return;
}
if self.output_width == 0 || self.output_height == 0 {
self.end_frame();
return;
}
self.render_scene_to_encoder(scene, encoder, target_view, clear_color);
self.end_frame();
}
}
#[cfg(target_os = "linux")]
fn validate_dmabuf_plane(
fd: &std::os::unix::io::OwnedFd,
offset: u64,
stride: u32,
width: u32,
height: u32,
bytes_per_pixel: u32,
) -> anyhow::Result<u64> {
if width == 0 || height == 0 || bytes_per_pixel == 0 {
anyhow::bail!("DMA-BUF plane dimensions and bytes per pixel must be non-zero");
}
if offset % 4 != 0 || stride % 4 != 0 {
anyhow::bail!("DMA-BUF offset and stride must be 4-byte aligned");
}
let row_bytes = (width as u64)
.checked_mul(bytes_per_pixel as u64)
.ok_or_else(|| anyhow::anyhow!("DMA-BUF row size overflow"))?;
if (stride as u64) < row_bytes {
anyhow::bail!(
"DMA-BUF stride {} is smaller than row size {row_bytes}",
stride
);
}
let span = (height as u64 - 1)
.checked_mul(stride as u64)
.and_then(|value| value.checked_add(row_bytes))
.ok_or_else(|| anyhow::anyhow!("DMA-BUF plane size overflow"))?;
let file = std::fs::File::from(
fd.try_clone()
.map_err(|error| anyhow::anyhow!("duplicate DMA-BUF fd: {error}"))?,
);
let file_size = file
.metadata()
.map_err(|error| anyhow::anyhow!("stat DMA-BUF fd: {error}"))?
.len();
if offset > file_size || span > file_size - offset {
anyhow::bail!("DMA-BUF plane offset/size {offset}+{span} exceeds file size {file_size}");
}
Ok(file_size)
}
fn is_filterable_color_format(format: wgpu::TextureFormat) -> bool {
matches!(
format,
wgpu::TextureFormat::Rgba8Unorm
| wgpu::TextureFormat::Rgba8UnormSrgb
| wgpu::TextureFormat::Bgra8Unorm
| wgpu::TextureFormat::Bgra8UnormSrgb
| wgpu::TextureFormat::Rgba16Unorm
| wgpu::TextureFormat::Rgba16Float
| wgpu::TextureFormat::Rgba32Float
| wgpu::TextureFormat::Rgb10a2Unorm
)
}
fn native_format_supported(format: wgpu::TextureFormat, features: wgpu::Features) -> bool {
if !is_filterable_color_format(format) {
return false;
}
match format {
wgpu::TextureFormat::Rgba16Unorm => {
features.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
}
wgpu::TextureFormat::Rgba32Float => features.contains(wgpu::Features::FLOAT32_FILTERABLE),
_ => true,
}
}
fn validate_sampler_descriptor(
device: &wgpu::Device,
sampler: &wgpu::SamplerDescriptor<'_>,
) -> anyhow::Result<()> {
if sampler.compare.is_some()
|| !matches!(
sampler.mag_filter,
wgpu::FilterMode::Nearest | wgpu::FilterMode::Linear
)
|| !matches!(
sampler.min_filter,
wgpu::FilterMode::Nearest | wgpu::FilterMode::Linear
)
|| sampler.anisotropy_clamp == 0
|| !sampler.lod_min_clamp.is_finite()
|| !sampler.lod_max_clamp.is_finite()
|| sampler.lod_min_clamp < 0.0
|| sampler.lod_max_clamp < sampler.lod_min_clamp
{
anyhow::bail!("native sampler must be non-comparison linear filtering with finite LODs");
}
if matches!(sampler.address_mode_u, wgpu::AddressMode::ClampToBorder)
|| matches!(sampler.address_mode_v, wgpu::AddressMode::ClampToBorder)
|| matches!(sampler.address_mode_w, wgpu::AddressMode::ClampToBorder)
{
if !device
.features()
.contains(wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER)
{
anyhow::bail!("native sampler uses unsupported clamp-to-border addressing");
}
}
Ok(())
}
fn validate_texture_dimensions(
device: &wgpu::Device,
width: u32,
height: u32,
) -> anyhow::Result<()> {
if width == 0
|| height == 0
|| width > device.limits().max_texture_dimension_2d
|| height > device.limits().max_texture_dimension_2d
{
anyhow::bail!("texture dimensions are outside the device limit");
}
Ok(())
}
fn validate_image_handle(handle: u64) -> anyhow::Result<()> {
if handle == 0 {
anyhow::bail!("image handle 0 is reserved");
}
Ok(())
}
fn checked_image_bytes(w: u32, h: u32, bytes_per_pixel: u32) -> anyhow::Result<u64> {
if w == 0 || h == 0 || bytes_per_pixel == 0 {
anyhow::bail!("image dimensions and bytes per pixel must be non-zero");
}
(w as u64)
.checked_mul(h as u64)
.and_then(|size| size.checked_mul(bytes_per_pixel as u64))
.ok_or_else(|| anyhow::anyhow!("image byte size overflow"))
}
fn texture_storage_bytes(
format: wgpu::TextureFormat,
width: u32,
height: u32,
) -> anyhow::Result<u64> {
if width == 0 || height == 0 {
anyhow::bail!("texture dimensions must be non-zero");
}
let bytes = format.theoretical_memory_footprint(wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
});
if bytes == 0 {
anyhow::bail!("texture format has no storage size");
}
Ok(bytes)
}
fn texture_storage_bytes_with_samples(
format: wgpu::TextureFormat,
width: u32,
height: u32,
sample_count: u32,
) -> anyhow::Result<u64> {
texture_storage_bytes(format, width, height)?
.checked_mul(u64::from(sample_count.max(1)))
.ok_or_else(|| anyhow::anyhow!("multisample texture size overflow"))
}
fn checked_image_bytes_usize(w: u32, h: u32, bytes_per_pixel: u32) -> anyhow::Result<usize> {
usize::try_from(checked_image_bytes(w, h, bytes_per_pixel)?)
.map_err(|_| anyhow::anyhow!("image byte size exceeds addressable memory"))
}
fn clamp_scissor(x: u32, y: u32, w: u32, h: u32, tw: u32, th: u32) -> (u32, u32, u32, u32) {
if w == 0 || h == 0 || tw == 0 || th == 0 {
return (0, 0, 0, 0);
}
let x = x.min(tw - 1);
let y = y.min(th - 1);
let w = w.min(tw - x);
let h = h.min(th - y);
(x, y, w, h)
}
fn rect_to_scissor(r: repose_core::Rect, width: u32, height: u32) -> (u32, u32, u32, u32) {
if width == 0
|| height == 0
|| !r.x.is_finite()
|| !r.y.is_finite()
|| !r.w.is_finite()
|| !r.h.is_finite()
|| r.w <= 0.0
|| r.h <= 0.0
{
return (0, 0, 0, 0);
}
let x0 = r.x.ceil().max(0.0).min(width as f32);
let y0 = r.y.ceil().max(0.0).min(height as f32);
let x1 = (r.x + r.w).floor().max(x0).min(width as f32);
let y1 = (r.y + r.h).floor().max(y0).min(height as f32);
if x1 <= x0 || y1 <= y0 {
return (0, 0, 0, 0);
}
(x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32)
}
fn rect_to_ndc(r: repose_core::Rect, width: f32, height: f32) -> [f32; 4] {
if width <= 0.0 || height <= 0.0 || !r.x.is_finite() || !r.y.is_finite() {
return [0.0, 0.0, 0.0, 0.0];
}
let x0 = r.x / width * 2.0 - 1.0;
let y0 = 1.0 - r.y / height * 2.0;
let x1 = (r.x + r.w) / width * 2.0 - 1.0;
let y1 = 1.0 - (r.y + r.h) / height * 2.0;
[x0.min(x1), y0.min(y1), (x1 - x0).abs(), (y1 - y0).abs()]
}
fn parent_rect_to_layer_rect(
rect: repose_core::Rect,
parent_transform: &Transform,
origin: (f32, f32),
) -> Option<repose_core::Rect> {
let inverse = parent_transform.inverse_linear()?;
let tx = parent_transform.translate_x;
let ty = parent_transform.translate_y;
let corners = [
(rect.x, rect.y),
(rect.x + rect.w, rect.y),
(rect.x, rect.y + rect.h),
(rect.x + rect.w, rect.y + rect.h),
];
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for (x, y) in corners {
let local_x = x - tx;
let local_y = y - ty;
let transformed_x = inverse[0] * local_x + inverse[1] * local_y;
let transformed_y = inverse[2] * local_x + inverse[3] * local_y;
min_x = min_x.min(transformed_x);
min_y = min_y.min(transformed_y);
max_x = max_x.max(transformed_x);
max_y = max_y.max(transformed_y);
}
let result = repose_core::Rect {
x: min_x - origin.0,
y: min_y - origin.1,
w: (max_x - min_x).max(0.0),
h: (max_y - min_y).max(0.0),
};
(result.x.is_finite() && result.y.is_finite() && result.w.is_finite() && result.h.is_finite())
.then_some(result)
}
fn parent_radii_to_layer_radii(radii: [f32; 4], parent_transform: &Transform) -> Option<[f32; 4]> {
let m = parent_transform.linear();
let sx = (m[0] * m[0] + m[2] * m[2]).sqrt();
let sy = (m[1] * m[1] + m[3] * m[3]).sqrt();
(sx.is_finite() && sy.is_finite() && sx > 1e-6 && sy > 1e-6)
.then(|| [radii[0] / sx, radii[1] / sy, radii[2] / sx, radii[3] / sy])
}
fn parent_affine_to_layer_affine(
affine: [f32; 6],
parent_transform: &Transform,
origin: (f32, f32),
) -> Option<[f32; 6]> {
let inverse = parent_transform.inverse_linear()?;
let tx = affine[4] - parent_transform.translate_x;
let ty = affine[5] - parent_transform.translate_y;
Some([
inverse[0] * affine[0] + inverse[1] * affine[2],
inverse[0] * affine[1] + inverse[1] * affine[3],
inverse[2] * affine[0] + inverse[3] * affine[2],
inverse[2] * affine[1] + inverse[3] * affine[3],
inverse[0] * tx + inverse[1] * ty - origin.0,
inverse[2] * tx + inverse[3] * ty - origin.1,
])
}
fn checked_copy_region(
region: repose_core::Rect,
target_width: u32,
target_height: u32,
) -> Option<(u32, u32, u32, u32)> {
if target_width == 0
|| target_height == 0
|| !region.x.is_finite()
|| !region.y.is_finite()
|| !region.w.is_finite()
|| !region.h.is_finite()
|| region.w <= 0.0
|| region.h <= 0.0
{
return None;
}
let x0 = region.x.floor().max(0.0).min(target_width as f32) as u32;
let y0 = region.y.floor().max(0.0).min(target_height as f32) as u32;
let x1 = (region.x + region.w)
.ceil()
.max(x0 as f32)
.min(target_width as f32) as u32;
let y1 = (region.y + region.h)
.ceil()
.max(y0 as f32)
.min(target_height as f32) as u32;
(x1 > x0 && y1 > y0).then_some((x0, y0, x1 - x0, y1 - y0))
}
fn intersect(a: repose_core::Rect, b: repose_core::Rect) -> repose_core::Rect {
let x0 = a.x.max(b.x);
let y0 = a.y.max(b.y);
let x1 = (a.x + a.w).min(b.x + b.w);
let y1 = (a.y + a.h).min(b.y + b.h);
repose_core::Rect {
x: x0,
y: y0,
w: (x1 - x0).max(0.0),
h: (y1 - y0).max(0.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_source_rect_uses_texel_edges() {
let source = resolve_image_source(Some(ImageSourceRect::new(2, 1, 3, 2)), 10, 8)
.expect("source should be valid");
assert_eq!(source.uv_min, [0.2, 0.125]);
assert_eq!(source.uv_max, [0.5, 0.375]);
assert_eq!(source.width, 3.0);
assert_eq!(source.height, 2.0);
assert!(resolve_image_source(Some(ImageSourceRect::new(8, 0, 3, 1)), 10, 8).is_none());
assert!(resolve_image_source(Some(ImageSourceRect::new(0, 0, 0, 1)), 10, 8).is_none());
}
#[test]
fn image_fit_stays_inside_selected_source() {
let source = resolve_image_source(Some(ImageSourceRect::new(2, 1, 4, 2)), 10, 8)
.expect("source should be valid");
let rect = repose_core::Rect {
x: 20.0,
y: 30.0,
w: 8.0,
h: 8.0,
};
let (_, uv) = image_fit_geometry(rect, ImageFit::Cover, ImageAlignment::Center, &source)
.expect("cover geometry should be valid");
assert!(uv[0] >= source.uv_min[0] && uv[2] <= source.uv_max[0]);
assert!(uv[1] >= source.uv_min[1] && uv[3] <= source.uv_max[1]);
}
#[test]
fn ring_alignment_rejects_bad_input() {
assert_eq!(align_up(0, 4).unwrap(), 0);
assert_eq!(align_up(1, 4).unwrap(), 4);
assert!(align_up(1, 3).is_err());
assert!(align_up(u64::MAX, 4).is_err());
}
#[test]
fn atlas_eviction_spares_the_frame_in_flight() {
fn key(glyph_id: u32) -> repose_text::CacheKey {
repose_text::CacheKey {
font_id: 1,
glyph_id,
font_size_bits: 16.0f32.to_bits(),
}
}
let mut map: HashMap<repose_text::CacheKey, GlyphInfo> = HashMap::new();
for (glyph_id, last_touch, last_used_frame) in
[(1u32, 10u64, 9u64), (2, 20, 7), (3, 30, 7), (4, 40, 7)]
{
map.insert(
key(glyph_id),
GlyphInfo {
u0: 0.0,
v0: 0.0,
u1: 0.1,
v1: 0.1,
w: 10.0,
h: 10.0,
last_touch,
last_used_frame,
},
);
}
let mut used_area = 400;
assert!(evict_atlas_lru(&mut map, &mut used_area, 9, 250));
assert_eq!(used_area, 200);
assert!(map.contains_key(&key(1)), "in-flight glyph was evicted");
assert!(!map.contains_key(&key(2)) && !map.contains_key(&key(3)));
assert!(map.contains_key(&key(4)));
}
#[test]
fn atlas_eviction_refuses_to_empty_the_atlas() {
let only = repose_text::CacheKey {
font_id: 1,
glyph_id: 1,
font_size_bits: 16.0f32.to_bits(),
};
let mut map: HashMap<repose_text::CacheKey, GlyphInfo> = HashMap::new();
map.insert(
only,
GlyphInfo {
u0: 0.0,
v0: 0.0,
u1: 0.1,
v1: 0.1,
w: 10.0,
h: 10.0,
last_touch: 1,
last_used_frame: 1,
},
);
let mut used_area = 100;
assert!(!evict_atlas_lru(&mut map, &mut used_area, 2, 0));
assert_eq!(map.len(), 1);
assert_eq!(used_area, 100);
}
#[test]
fn empty_and_negative_rects_are_suppressed() {
assert_eq!(
rect_to_scissor(
repose_core::Rect {
x: 0.0,
y: 0.0,
w: 0.0,
h: 4.0,
},
16,
16,
),
(0, 0, 0, 0)
);
assert_eq!(
rect_to_scissor(
repose_core::Rect {
x: 0.0,
y: 0.0,
w: -1.0,
h: 4.0,
},
16,
16,
),
(0, 0, 0, 0)
);
}
#[test]
fn copy_region_is_checked() {
assert_eq!(
checked_copy_region(
repose_core::Rect {
x: 2.25,
y: 3.5,
w: 4.0,
h: 5.0,
},
16,
16,
),
Some((2, 3, 5, 6))
);
assert_eq!(
checked_copy_region(
repose_core::Rect {
x: 20.0,
y: 0.0,
w: 2.0,
h: 2.0,
},
16,
16,
),
None
);
}
#[test]
fn p010_uses_ten_bit_sample_range() {
let raw = make_yuv_transform_raw(ColorInfo::default(), PixelFormat::P010);
let expected = 65535.0 / (64.0 * 1020.0);
assert!((raw.b[3] - expected).abs() < 1e-6);
assert_eq!(canonical_fourcc(PixelFormat::P010), P010_FOURCC);
assert_eq!(canonical_fourcc(PixelFormat::Nv12), NV12_FOURCC);
assert_eq!(
make_yuv_transform_raw(
ColorInfo {
transfer: repose_core::color::Transfer::Pq,
..ColorInfo::default()
},
PixelFormat::Nv12,
)
.transfer[0],
3.0
);
assert_eq!(
make_yuv_transform_raw(
ColorInfo {
transfer: repose_core::color::Transfer::Hlg,
..ColorInfo::default()
},
PixelFormat::Nv12,
)
.transfer[0],
4.0
);
}
}