mod icon;
mod image;
mod instance;
mod msaa;
mod pipeline;
mod text;
pub use crate::msaa::MsaaTarget;
use std::collections::{HashMap, HashSet};
use web_time::Instant;
use wgpu::util::DeviceExt;
use aetna_core::event::{KeyChord, KeyModifiers, PointerButton, UiEvent, UiKey};
use aetna_core::ir::TextAnchor;
use aetna_core::paint::{IconRunKind, PhysicalScissor, QuadInstance};
use aetna_core::runtime::{RecordedPaint, RunnerCore, TextRecorder};
use aetna_core::shader::{ShaderHandle, StockShader, stock_wgsl};
use aetna_core::state::{AnimationMode, UiState};
use aetna_core::text::atlas::RunStyle;
use aetna_core::theme::Theme;
use aetna_core::tree::{Color, El, Rect, TextWrap};
use aetna_core::vector::IconMaterial;
pub use aetna_core::paint::PaintItem;
pub use aetna_core::runtime::{PointerMove, PrepareResult, PrepareTimings};
use crate::icon::IconPaint;
use crate::image::ImagePaint;
use crate::instance::set_scissor;
use crate::pipeline::{FrameUniforms, build_quad_pipeline};
use crate::text::TextPaint;
const INITIAL_INSTANCE_CAPACITY: usize = 256;
pub struct Runner {
target_format: wgpu::TextureFormat,
sample_count: u32,
pipeline_layout: wgpu::PipelineLayout,
backdrop_pipeline_layout: wgpu::PipelineLayout,
quad_bind_group: wgpu::BindGroup,
backdrop_bind_layout: wgpu::BindGroupLayout,
backdrop_sampler: wgpu::Sampler,
frame_buf: wgpu::Buffer,
quad_vbo: wgpu::Buffer,
instance_buf: wgpu::Buffer,
instance_capacity: usize,
pipelines: HashMap<ShaderHandle, wgpu::RenderPipeline>,
backdrop_shaders: HashSet<&'static str>,
text_paint: TextPaint,
icon_paint: IconPaint,
image_paint: ImagePaint,
snapshot: Option<SnapshotTexture>,
backdrop_bind_group: Option<wgpu::BindGroup>,
start_time: Instant,
core: RunnerCore,
}
struct SnapshotTexture {
texture: wgpu::Texture,
extent: (u32, u32),
}
struct PaintRecorder<'a> {
text: &'a mut TextPaint,
icons: &'a mut IconPaint,
images: &'a mut ImagePaint,
device: &'a wgpu::Device,
queue: &'a wgpu::Queue,
}
impl TextRecorder for PaintRecorder<'_> {
fn record(
&mut self,
rect: Rect,
scissor: Option<PhysicalScissor>,
style: &aetna_core::text::atlas::RunStyle,
text: &str,
size: f32,
line_height: f32,
wrap: TextWrap,
anchor: TextAnchor,
scale_factor: f32,
) -> std::ops::Range<usize> {
self.text.record(
rect,
scissor,
style,
text,
size,
line_height,
wrap,
anchor,
scale_factor,
)
}
fn record_runs(
&mut self,
rect: Rect,
scissor: Option<PhysicalScissor>,
runs: &[(String, RunStyle)],
size: f32,
line_height: f32,
wrap: TextWrap,
anchor: TextAnchor,
scale_factor: f32,
) -> std::ops::Range<usize> {
self.text.record_runs(
rect,
scissor,
runs,
size,
line_height,
wrap,
anchor,
scale_factor,
)
}
fn record_icon(
&mut self,
rect: Rect,
scissor: Option<PhysicalScissor>,
source: &aetna_core::svg_icon::IconSource,
color: Color,
_size: f32,
stroke_width: f32,
_scale_factor: f32,
) -> RecordedPaint {
RecordedPaint::Icon(
self.icons
.record(rect, scissor, source, color, stroke_width),
)
}
fn record_image(
&mut self,
rect: Rect,
scissor: Option<PhysicalScissor>,
image: &aetna_core::image::Image,
tint: Option<Color>,
radius: f32,
_fit: aetna_core::image::ImageFit,
_scale_factor: f32,
) -> std::ops::Range<usize> {
self.images
.record(self.device, self.queue, rect, scissor, image, tint, radius)
}
}
impl Runner {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
target_format: wgpu::TextureFormat,
) -> Self {
Self::with_sample_count(device, queue, target_format, 1)
}
pub fn with_sample_count(
device: &wgpu::Device,
_queue: &wgpu::Queue,
target_format: wgpu::TextureFormat,
sample_count: u32,
) -> Self {
let frame_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("aetna_wgpu::frame_uniforms"),
size: std::mem::size_of::<FrameUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let frame_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("aetna_wgpu::frame_bind_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 quad_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("aetna_wgpu::frame_bind_group"),
layout: &frame_bind_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: frame_buf.as_entire_binding(),
}],
});
let quad_vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("aetna_wgpu::quad_vbo"),
contents: bytemuck::cast_slice::<f32, u8>(&[0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]),
usage: wgpu::BufferUsages::VERTEX,
});
let instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("aetna_wgpu::instance_buf"),
size: (INITIAL_INSTANCE_CAPACITY * std::mem::size_of::<QuadInstance>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("aetna_wgpu::pipeline_layout"),
bind_group_layouts: &[Some(&frame_bind_layout)],
immediate_size: 0,
});
let backdrop_bind_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("aetna_wgpu::backdrop_bind_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let backdrop_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("aetna_wgpu::backdrop_pipeline_layout"),
bind_group_layouts: &[Some(&frame_bind_layout), Some(&backdrop_bind_layout)],
immediate_size: 0,
});
let backdrop_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("aetna_wgpu::backdrop_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let mut pipelines = HashMap::new();
let rr_pipeline = build_quad_pipeline(
device,
&pipeline_layout,
target_format,
sample_count,
"stock::rounded_rect",
stock_wgsl::ROUNDED_RECT,
);
pipelines.insert(ShaderHandle::Stock(StockShader::RoundedRect), rr_pipeline);
let text_paint = TextPaint::new(device, target_format, sample_count, &frame_bind_layout);
let icon_paint = IconPaint::new(device, target_format, sample_count, &frame_bind_layout);
let image_paint = ImagePaint::new(device, target_format, sample_count, &frame_bind_layout);
let mut core = RunnerCore::new();
core.quad_scratch = Vec::with_capacity(INITIAL_INSTANCE_CAPACITY);
Self {
target_format,
sample_count,
pipeline_layout,
backdrop_pipeline_layout,
quad_bind_group,
backdrop_bind_layout,
backdrop_sampler,
frame_buf,
quad_vbo,
instance_buf,
instance_capacity: INITIAL_INSTANCE_CAPACITY,
pipelines,
backdrop_shaders: HashSet::new(),
text_paint,
icon_paint,
image_paint,
snapshot: None,
backdrop_bind_group: None,
start_time: Instant::now(),
core,
}
}
pub fn set_surface_size(&mut self, width: u32, height: u32) {
self.core.set_surface_size(width, height);
}
pub fn set_theme(&mut self, theme: Theme) {
self.icon_paint.set_material(theme.icon_material());
self.core.set_theme(theme);
}
pub fn theme(&self) -> &Theme {
self.core.theme()
}
pub fn set_icon_material(&mut self, material: IconMaterial) {
self.icon_paint.set_material(material);
}
pub fn icon_material(&self) -> IconMaterial {
self.icon_paint.material()
}
pub fn register_shader(&mut self, device: &wgpu::Device, name: &'static str, wgsl: &str) {
self.register_shader_with(device, name, wgsl, false);
}
pub fn register_shader_with(
&mut self,
device: &wgpu::Device,
name: &'static str,
wgsl: &str,
samples_backdrop: bool,
) {
let label = format!("custom::{name}");
let layout = if samples_backdrop {
&self.backdrop_pipeline_layout
} else {
&self.pipeline_layout
};
let pipeline = build_quad_pipeline(
device,
layout,
self.target_format,
self.sample_count,
&label,
wgsl,
);
self.pipelines.insert(ShaderHandle::Custom(name), pipeline);
if samples_backdrop {
self.backdrop_shaders.insert(name);
} else {
self.backdrop_shaders.remove(name);
}
}
pub fn ui_state(&self) -> &UiState {
self.core.ui_state()
}
pub fn debug_summary(&self) -> String {
self.core.debug_summary()
}
pub fn rect_of_key(&self, key: &str) -> Option<Rect> {
self.core.rect_of_key(key)
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
root: &mut El,
viewport: Rect,
scale_factor: f32,
) -> PrepareResult {
let mut timings = PrepareTimings::default();
let (ops, needs_redraw) =
self.core
.prepare_layout(root, viewport, scale_factor, &mut timings);
self.text_paint.frame_begin();
self.icon_paint.frame_begin();
self.image_paint.frame_begin();
let pipelines = &self.pipelines;
let backdrop_shaders = &self.backdrop_shaders;
let mut recorder = PaintRecorder {
text: &mut self.text_paint,
icons: &mut self.icon_paint,
images: &mut self.image_paint,
device,
queue,
};
self.core.prepare_paint(
&ops,
|shader| pipelines.contains_key(shader),
|shader| match shader {
ShaderHandle::Custom(name) => backdrop_shaders.contains(name),
ShaderHandle::Stock(_) => false,
},
&mut recorder,
scale_factor,
&mut timings,
);
let t_paint_end = Instant::now();
if self.core.quad_scratch.len() > self.instance_capacity {
let new_cap = self.core.quad_scratch.len().next_power_of_two();
self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("aetna_wgpu::instance_buf (resized)"),
size: (new_cap * std::mem::size_of::<QuadInstance>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.instance_capacity = new_cap;
}
if !self.core.quad_scratch.is_empty() {
queue.write_buffer(
&self.instance_buf,
0,
bytemuck::cast_slice(&self.core.quad_scratch),
);
}
self.text_paint.flush(device, queue);
self.icon_paint.flush(device, queue);
self.image_paint.flush(device, queue);
let time = (Instant::now() - self.start_time).as_secs_f32();
let frame = FrameUniforms {
viewport: [viewport.w, viewport.h],
time,
scale_factor,
};
queue.write_buffer(&self.frame_buf, 0, bytemuck::bytes_of(&frame));
timings.gpu_upload = Instant::now() - t_paint_end;
self.core.snapshot(root, &mut timings);
PrepareResult {
needs_redraw,
timings,
}
}
pub fn pointer_moved(&mut self, x: f32, y: f32) -> PointerMove {
self.core.pointer_moved(x, y)
}
pub fn pointer_left(&mut self) {
self.core.pointer_left();
}
pub fn pointer_down(&mut self, x: f32, y: f32, button: PointerButton) -> Vec<UiEvent> {
self.core.pointer_down(x, y, button)
}
pub fn set_modifiers(&mut self, modifiers: KeyModifiers) {
self.core.ui_state.set_modifiers(modifiers);
}
pub fn pointer_up(&mut self, x: f32, y: f32, button: PointerButton) -> Vec<UiEvent> {
self.core.pointer_up(x, y, button)
}
pub fn key_down(&mut self, key: UiKey, modifiers: KeyModifiers, repeat: bool) -> Vec<UiEvent> {
self.core.key_down(key, modifiers, repeat)
}
pub fn text_input(&mut self, text: String) -> Option<UiEvent> {
self.core.text_input(text)
}
pub fn set_hotkeys(&mut self, hotkeys: Vec<(KeyChord, String)>) {
self.core.set_hotkeys(hotkeys);
}
pub fn set_selection(&mut self, selection: aetna_core::selection::Selection) {
self.core.set_selection(selection);
}
pub fn push_toasts(&mut self, specs: Vec<aetna_core::toast::ToastSpec>) {
self.core.push_toasts(specs);
}
pub fn dismiss_toast(&mut self, id: u64) {
self.core.dismiss_toast(id);
}
pub fn set_animation_mode(&mut self, mode: AnimationMode) {
self.core.set_animation_mode(mode);
}
pub fn pointer_wheel(&mut self, x: f32, y: f32, dy: f32) -> bool {
self.core.pointer_wheel(x, y, dy)
}
pub fn draw<'pass>(&'pass self, pass: &mut wgpu::RenderPass<'pass>) {
self.draw_items(pass, &self.core.paint_items);
}
pub fn render(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
target_tex: &wgpu::Texture,
target_view: &wgpu::TextureView,
msaa_view: Option<&wgpu::TextureView>,
load_op: wgpu::LoadOp<wgpu::Color>,
) {
let attachment_view = msaa_view.unwrap_or(target_view);
let resolve_target = msaa_view.map(|_| target_view);
let split_at = self
.core
.paint_items
.iter()
.position(|p| matches!(p, PaintItem::BackdropSnapshot));
if let Some(idx) = split_at {
self.ensure_snapshot(device, target_tex);
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("aetna_wgpu::pass_a"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: attachment_view,
resolve_target,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.draw_items(&mut pass, &self.core.paint_items[..idx]);
}
let snapshot = self.snapshot.as_ref().expect("snapshot ensured");
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: target_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: &snapshot.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d {
width: snapshot.extent.0,
height: snapshot.extent.1,
depth_or_array_layers: 1,
},
);
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("aetna_wgpu::pass_b"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: attachment_view,
resolve_target,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.draw_items(&mut pass, &self.core.paint_items[idx + 1..]);
}
} else {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("aetna_wgpu::pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: attachment_view,
resolve_target,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.draw_items(&mut pass, &self.core.paint_items);
}
}
fn ensure_snapshot(&mut self, device: &wgpu::Device, target_tex: &wgpu::Texture) {
let extent = target_tex.size();
let want = (extent.width, extent.height);
if let Some(s) = &self.snapshot
&& s.extent == want
{
return;
}
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("aetna_wgpu::backdrop_snapshot"),
size: wgpu::Extent3d {
width: want.0,
height: want.1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.target_format,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("aetna_wgpu::backdrop_bind_group"),
layout: &self.backdrop_bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.backdrop_sampler),
},
],
});
self.snapshot = Some(SnapshotTexture {
texture,
extent: want,
});
self.backdrop_bind_group = Some(bind_group);
}
fn draw_items<'pass>(
&'pass self,
pass: &mut wgpu::RenderPass<'pass>,
items: &'pass [PaintItem],
) {
let full = PhysicalScissor {
x: 0,
y: 0,
w: self.core.viewport_px.0,
h: self.core.viewport_px.1,
};
for item in items {
match *item {
PaintItem::QuadRun(index) => {
let run = &self.core.runs[index];
set_scissor(pass, run.scissor, full);
pass.set_bind_group(0, &self.quad_bind_group, &[]);
let is_backdrop_shader = matches!(
run.handle,
ShaderHandle::Custom(name) if self.backdrop_shaders.contains(name)
);
if is_backdrop_shader && let Some(bg) = &self.backdrop_bind_group {
pass.set_bind_group(1, bg, &[]);
}
pass.set_vertex_buffer(0, self.quad_vbo.slice(..));
pass.set_vertex_buffer(1, self.instance_buf.slice(..));
let pipeline = self
.pipelines
.get(&run.handle)
.expect("run handle has no pipeline (bug in prepare)");
pass.set_pipeline(pipeline);
pass.draw(0..4, run.first..run.first + run.count);
}
PaintItem::Text(index) => {
let run = self.text_paint.run(index);
set_scissor(pass, run.scissor, full);
pass.set_pipeline(self.text_paint.pipeline_for(run.kind));
pass.set_bind_group(0, &self.quad_bind_group, &[]);
if !matches!(run.kind, crate::text::TextRunKind::Highlight) {
pass.set_bind_group(
1,
self.text_paint.page_bind_group(run.kind, run.page),
&[],
);
}
pass.set_vertex_buffer(0, self.quad_vbo.slice(..));
pass.set_vertex_buffer(1, self.text_paint.instance_buf_for(run.kind).slice(..));
pass.draw(0..4, run.first..run.first + run.count);
}
PaintItem::IconRun(index) => {
let run = self.icon_paint.run(index);
set_scissor(pass, run.scissor, full);
match run.kind {
IconRunKind::Tess => {
pass.set_pipeline(self.icon_paint.tess_pipeline(run.material));
pass.set_bind_group(0, &self.quad_bind_group, &[]);
pass.set_vertex_buffer(0, self.icon_paint.tess_vertex_buf().slice(..));
pass.draw(run.first..run.first + run.count, 0..1);
}
IconRunKind::Msdf => {
pass.set_pipeline(self.icon_paint.msdf_pipeline());
pass.set_bind_group(0, &self.quad_bind_group, &[]);
pass.set_bind_group(
1,
self.icon_paint.msdf_page_bind_group(run.page),
&[],
);
pass.set_vertex_buffer(0, self.quad_vbo.slice(..));
pass.set_vertex_buffer(
1,
self.icon_paint.msdf_instance_buf().slice(..),
);
pass.draw(0..4, run.first..run.first + run.count);
}
}
}
PaintItem::Image(index) => {
let run = self.image_paint.run(index);
set_scissor(pass, run.scissor, full);
pass.set_pipeline(self.image_paint.pipeline());
pass.set_bind_group(0, &self.quad_bind_group, &[]);
pass.set_bind_group(1, self.image_paint.bind_group_for_run(run), &[]);
pass.set_vertex_buffer(0, self.quad_vbo.slice(..));
pass.set_vertex_buffer(1, self.image_paint.instance_buf().slice(..));
pass.draw(0..4, run.first..run.first + run.count);
}
PaintItem::BackdropSnapshot => {
}
}
}
}
}