pub use wgpu;
mod backdrop;
use kui_core::atlas::GlyphAtlas;
use kui_core::{Clip, DisplayList, Quad, QuadKind};
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Instance {
pos: [f32; 2],
size: [f32; 2],
color: [f32; 4],
border_color: [f32; 4],
params: [f32; 4],
uv: [f32; 4],
clip: [f32; 4],
radii: [f32; 4],
clip_radii: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Globals {
viewport: [f32; 2],
atlas_size: [f32; 2],
time: f32,
scale: f32,
_pad: [f32; 2],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct FragmentParams {
params: [f32; 16],
image: [f32; 4],
}
fn instance_of(q: &Quad, clips: &[Clip], textures: &[kui_core::display::TextureDraw]) -> Instance {
let clip = clips.get(q.clip as usize).copied().unwrap_or(Clip::NONE);
let kind = match q.kind {
QuadKind::Solid => 0.0,
QuadKind::GlyphMask => 1.0,
QuadKind::GlyphColor => 2.0,
QuadKind::Image => 3.0,
QuadKind::GlyphSubpixel => 4.0,
QuadKind::Shadow => 5.0,
QuadKind::Segment => 6.0,
QuadKind::Fragment => 7.0,
QuadKind::Texture => 3.0,
QuadKind::Backdrop => 0.0,
};
let uv = if q.kind == QuadKind::Segment {
q.segment_ends()
} else if q.kind == QuadKind::Texture {
let uv = textures.get(q.uv[0] as usize).map_or([0; 4], |t| t.uv);
[uv[0] as f32, uv[1] as f32, uv[2] as f32, uv[3] as f32]
} else {
[
q.uv[0] as f32,
q.uv[1] as f32,
q.uv[2] as f32,
q.uv[3] as f32,
]
};
if q.kind == QuadKind::Backdrop {
return Instance {
pos: [q.rect.x, q.rect.y],
size: [0.0, 0.0],
color: [0.0; 4],
border_color: [0.0; 4],
params: [0.0; 4],
uv: [0.0; 4],
clip: [0.0; 4],
radii: [0.0; 4],
clip_radii: [0.0; 4],
};
}
Instance {
pos: [q.rect.x, q.rect.y],
size: [q.rect.w, q.rect.h],
color: [q.color.r, q.color.g, q.color.b, q.color.a],
border_color: [
q.border_color.r,
q.border_color.g,
q.border_color.b,
q.border_color.a,
],
params: [q.blur, q.border_w, kind, 0.0],
uv,
clip: [clip.rect.x, clip.rect.y, clip.rect.w, clip.rect.h],
radii: q.radius,
clip_radii: clip.radius,
}
}
#[derive(Clone)]
pub struct Gpu(std::sync::Arc<GpuInner>);
struct GpuInner {
instance: wgpu::Instance,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
options: GpuOptions,
dual_source: bool,
fragment_pipelines: std::sync::Mutex<
std::collections::HashMap<(u64, wgpu::TextureFormat), wgpu::RenderPipeline>,
>,
textures: std::sync::Mutex<std::collections::HashMap<u64, std::sync::Arc<ImageTexture>>>,
lost: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
struct ImageTexture {
texture: wgpu::Texture,
view: wgpu::TextureView,
width: u32,
height: u32,
rev: std::sync::Mutex<u32>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct GpuOptions {
pub transparent: bool,
}
impl GpuOptions {
pub fn transparent(transparent: bool) -> Self {
Self { transparent }
}
}
impl Gpu {
pub async fn new(
target: impl Into<wgpu::SurfaceTarget<'static>>,
) -> Result<(Self, wgpu::Surface<'static>), Box<dyn std::error::Error>> {
Self::new_with(target, GpuOptions::default()).await
}
pub async fn new_with(
target: impl Into<wgpu::SurfaceTarget<'static>>,
options: GpuOptions,
) -> Result<(Self, wgpu::Surface<'static>), Box<dyn std::error::Error>> {
report_faults();
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle_from_env();
if cfg!(windows) && std::env::var_os("WGPU_BACKEND").is_none() {
desc.backends = wgpu::Backends::DX12;
}
if options.transparent && std::env::var_os("WGPU_DX12_PRESENTATION_SYSTEM").is_none() {
desc.backend_options.dx12.presentation_system = wgpu::Dx12SwapchainKind::DxgiFromVisual;
}
let instance = wgpu::Instance::new(desc);
let surface = instance.create_surface(target)?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
..Default::default()
})
.await?;
let dual_source = adapter
.features()
.contains(wgpu::Features::DUAL_SOURCE_BLENDING);
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_features: if dual_source {
wgpu::Features::DUAL_SOURCE_BLENDING
} else {
wgpu::Features::empty()
},
..Default::default()
})
.await?;
device.on_uncaptured_error(std::sync::Arc::new(|e| eprintln!("kui: wgpu: {e}")));
let lost = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
device.set_device_lost_callback({
let lost = lost.clone();
move |reason, message| {
if reason == wgpu::DeviceLostReason::Unknown {
eprintln!("kui: device lost: {message}");
lost.store(true, std::sync::atomic::Ordering::Release);
}
}
});
let gpu = Self(std::sync::Arc::new(GpuInner {
instance,
adapter,
device,
queue,
options,
dual_source,
fragment_pipelines: Default::default(),
textures: Default::default(),
lost,
}));
Ok((gpu, surface))
}
pub fn lost(&self) -> bool {
self.0.lost.load(std::sync::atomic::Ordering::Acquire)
}
pub fn mark_lost(&self) {
#[cfg(windows)]
{
use windows::Win32::Graphics::Direct3D12::ID3D12Device5;
use windows::core::Interface;
let removed = unsafe {
self.0
.device
.as_hal::<wgpu::hal::api::Dx12>()
.and_then(|d| d.raw_device().cast::<ID3D12Device5>().ok())
.map(|d| d.RemoveDevice())
};
if removed.is_some() {
return;
}
}
self.0
.lost
.store(true, std::sync::atomic::Ordering::Release);
}
pub fn create_surface(
&self,
target: impl Into<wgpu::SurfaceTarget<'static>>,
) -> Result<wgpu::Surface<'static>, wgpu::CreateSurfaceError> {
self.0.instance.create_surface(target)
}
pub fn options(&self) -> GpuOptions {
self.0.options
}
pub fn instance(&self) -> &wgpu::Instance {
&self.0.instance
}
pub fn adapter(&self) -> &wgpu::Adapter {
&self.0.adapter
}
pub fn device(&self) -> &wgpu::Device {
&self.0.device
}
pub fn queue(&self) -> &wgpu::Queue {
&self.0.queue
}
pub fn dual_source(&self) -> bool {
self.0.dual_source
}
fn image_texture(
&self,
id: u64,
px: &kui_core::display::TexturePixels,
) -> Option<std::sync::Arc<ImageTexture>> {
let max = self.0.device.limits().max_texture_dimension_2d;
if px.width == 0 || px.height == 0 || px.width > max || px.height > max {
return None;
}
let mut cache = self.0.textures.lock().unwrap_or_else(|e| e.into_inner());
let fresh = match cache.get(&id) {
Some(t) if t.width == px.width && t.height == px.height => {
let mut rev = t.rev.lock().unwrap_or_else(|e| e.into_inner());
if *rev != px.rev {
upload_image(&self.0.queue, &t.texture, px);
*rev = px.rev;
}
return Some(t.clone());
}
_ => {
let texture = self.0.device.create_texture(&wgpu::TextureDescriptor {
label: Some("kui.image"),
size: wgpu::Extent3d {
width: px.width,
height: px.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
upload_image(&self.0.queue, &texture, px);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
std::sync::Arc::new(ImageTexture {
texture,
view,
width: px.width,
height: px.height,
rev: std::sync::Mutex::new(px.rev),
})
}
};
cache.insert(id, fresh.clone());
Some(fresh)
}
fn drop_fragment_pipelines(&self, id: u64) {
self.0
.fragment_pipelines
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|(fid, _), _| *fid != id);
}
fn drop_image_texture(&self, id: u64) {
self.0
.textures
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&id);
}
fn holds_image_texture(&self, id: u64, texture: &std::sync::Arc<ImageTexture>) -> bool {
self.0
.textures
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&id)
.is_some_and(|t| std::sync::Arc::ptr_eq(t, texture))
}
fn fragment_pipeline(
&self,
id: u64,
source: &str,
format: wgpu::TextureFormat,
layouts: &FragmentLayouts,
) -> wgpu::RenderPipeline {
let mut cache = self
.0
.fragment_pipelines
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(p) = cache.get(&(id, format)) {
return p.clone();
}
let device = &self.0.device;
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("kui.fragment"),
source: wgpu::ShaderSource::Wgsl(kui_core::fragment::module_source(source).into()),
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("kui.fragment"),
layout: Some(&layouts.pipeline),
vertex: wgpu::VertexState {
module: &layouts.vertex,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[Some(instance_buffer_layout(&INSTANCE_ATTRS))],
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some(kui_core::fragment::ENTRY_POINT),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
cache.insert((id, format), pipeline.clone());
pipeline
}
}
struct FragmentLayouts {
vertex: wgpu::ShaderModule,
pipeline: wgpu::PipelineLayout,
}
const INSTANCE_ATTRS: [wgpu::VertexAttribute; 9] = wgpu::vertex_attr_array![
0 => Float32x2, 1 => Float32x2, 2 => Float32x4,
3 => Float32x4, 4 => Float32x4, 5 => Float32x4,
6 => Float32x4, 7 => Float32x4, 8 => Float32x4,
];
fn instance_buffer_layout(attrs: &[wgpu::VertexAttribute]) -> wgpu::VertexBufferLayout<'_> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Instance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: attrs,
}
}
impl std::fmt::Debug for Gpu {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Gpu")
.field("adapter", &self.0.adapter.get_info().name)
.field("dual_source", &self.0.dual_source)
.finish()
}
}
pub struct Renderer {
gpu: Gpu,
surface: wgpu::Surface<'static>,
config: wgpu::SurfaceConfiguration,
pipeline: wgpu::RenderPipeline,
globals_buf: wgpu::Buffer,
bind_group: wgpu::BindGroup,
bind_layout: wgpu::BindGroupLayout,
samplers: Samplers,
texture_binds: std::collections::HashMap<u64, TextureBind>,
atlas_tex: wgpu::Texture,
atlas_size: u32,
atlas_epoch: u64,
instance_buf: wgpu::Buffer,
instance_cap: usize,
instances: Vec<Instance>,
fragment_layouts: FragmentLayouts,
fragment_params_buf: wgpu::Buffer,
fragment_params_cap: usize,
fragment_bind: wgpu::BindGroup,
fragment_bind_layout: wgpu::BindGroupLayout,
uniform_align: u32,
fragment_bytes: Vec<u8>,
pub clear_color: wgpu::Color,
ground: Option<Ground>,
backdrop_pipes: Option<backdrop::Pipes>,
backdrop_targets: Option<backdrop::Targets>,
backdrop_idle: u32,
blurs: Vec<backdrop::Blur>,
}
struct Ground {
pipeline: wgpu::RenderPipeline,
bind: wgpu::BindGroup,
uv: wgpu::Buffer,
_texture: wgpu::Texture,
}
const GROUND_SHADER: &str = r"
struct G { uv: vec4<f32> }
@group(0) @binding(0) var<uniform> g: G;
@group(0) @binding(1) var t: texture_2d<f32>;
@group(0) @binding(2) var s: sampler;
struct V { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32> }
@vertex fn vs(@builtin(vertex_index) i: u32) -> V {
let x = f32(i & 1u);
let y = f32((i >> 1u) & 1u);
var o: V;
o.pos = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
o.uv = vec2<f32>(mix(g.uv.x, g.uv.z, x), mix(g.uv.y, g.uv.w, y));
return o;
}
@fragment fn fs(v: V) -> @location(0) vec4<f32> {
return vec4<f32>(textureSample(t, s, v.uv).rgb, 1.0);
}
";
struct Samplers {
linear: wgpu::Sampler,
nearest: wgpu::Sampler,
}
struct TextureBind {
texture: std::sync::Arc<ImageTexture>,
globals: wgpu::Buffer,
bind: wgpu::BindGroup,
}
fn upload_image(
queue: &wgpu::Queue,
texture: &wgpu::Texture,
px: &kui_core::display::TexturePixels,
) {
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&px.rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(px.width * 4),
rows_per_image: Some(px.height),
},
wgpu::Extent3d {
width: px.width,
height: px.height,
depth_or_array_layers: 1,
},
);
}
fn opaque_mode(modes: &[wgpu::CompositeAlphaMode]) -> wgpu::CompositeAlphaMode {
if modes.contains(&wgpu::CompositeAlphaMode::Opaque) {
wgpu::CompositeAlphaMode::Opaque
} else {
modes
.first()
.copied()
.unwrap_or(wgpu::CompositeAlphaMode::Opaque)
}
}
fn transparent_mode(
modes: &[wgpu::CompositeAlphaMode],
backend: wgpu::Backend,
) -> Option<wgpu::CompositeAlphaMode> {
use wgpu::CompositeAlphaMode as M;
let mut wanted = vec![M::PreMultiplied];
if backend == wgpu::Backend::Metal {
wanted.push(M::PostMultiplied);
}
wanted.push(M::Inherit);
wanted.into_iter().find(|m| modes.contains(m))
}
fn preprocess_shader(src: &str, dual: bool) -> String {
let (keep, drop) = if dual {
("//DUAL:", "//SINGLE:")
} else {
("//SINGLE:", "//DUAL:")
};
let mut out = String::with_capacity(src.len());
for line in src.lines() {
if let Some(rest) = line.strip_prefix(keep) {
out.push_str(rest);
} else if line.starts_with(drop) {
continue;
} else {
out.push_str(line);
}
out.push('\n');
}
out
}
pub const DEFAULT_FRAME_LATENCY: u32 = if cfg!(target_os = "windows") { 1 } else { 2 };
impl Renderer {
pub async fn new(
target: impl Into<wgpu::SurfaceTarget<'static>>,
width: u32,
height: u32,
) -> Result<Self, Box<dyn std::error::Error>> {
Self::new_with(target, width, height, GpuOptions::default()).await
}
pub async fn new_with(
target: impl Into<wgpu::SurfaceTarget<'static>>,
width: u32,
height: u32,
options: GpuOptions,
) -> Result<Self, Box<dyn std::error::Error>> {
let (gpu, surface) = Gpu::new_with(target, options).await?;
Self::with_surface(gpu, surface, width, height, options.transparent)
}
pub fn transparent(&self) -> bool {
!matches!(
self.config.alpha_mode,
wgpu::CompositeAlphaMode::Opaque | wgpu::CompositeAlphaMode::Auto
)
}
pub fn new_in(
gpu: &Gpu,
target: impl Into<wgpu::SurfaceTarget<'static>>,
width: u32,
height: u32,
) -> Result<Self, Box<dyn std::error::Error>> {
Self::new_in_with(gpu, target, width, height, false)
}
pub fn new_in_with(
gpu: &Gpu,
target: impl Into<wgpu::SurfaceTarget<'static>>,
width: u32,
height: u32,
transparent: bool,
) -> Result<Self, Box<dyn std::error::Error>> {
let surface = gpu.create_surface(target)?;
Self::with_surface(gpu.clone(), surface, width, height, transparent)
}
pub fn gpu(&self) -> &Gpu {
&self.gpu
}
pub fn set_frame_latency(&mut self, frames: u32) {
let frames = frames.max(1);
if self.config.desired_maximum_frame_latency != frames {
self.config.desired_maximum_frame_latency = frames;
self.surface.configure(self.gpu.device(), &self.config);
}
}
pub fn frame_latency(&self) -> u32 {
self.config.desired_maximum_frame_latency
}
fn with_surface(
gpu: Gpu,
surface: wgpu::Surface<'static>,
width: u32,
height: u32,
transparent: bool,
) -> Result<Self, Box<dyn std::error::Error>> {
let device = gpu.device();
let dual_source = gpu.dual_source();
let caps = surface.get_capabilities(gpu.adapter());
let format = caps
.formats
.iter()
.copied()
.find(|f| !f.is_srgb())
.unwrap_or(caps.formats[0]);
let backend = gpu.adapter().get_info().backend;
let alpha_mode = transparent
.then(|| transparent_mode(&caps.alpha_modes, backend))
.flatten()
.unwrap_or_else(|| opaque_mode(&caps.alpha_modes));
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: width.clamp(1, device.limits().max_texture_dimension_2d),
height: height.clamp(1, device.limits().max_texture_dimension_2d),
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode,
color_space: wgpu::SurfaceColorSpace::Auto,
view_formats: vec![],
desired_maximum_frame_latency: DEFAULT_FRAME_LATENCY,
};
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
surface.configure(device, &config);
if let Some(err) = pollster::block_on(scope.pop()) {
return Err(format!("configuring the surface: {err}").into());
}
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("kui"),
source: wgpu::ShaderSource::Wgsl(
preprocess_shader(include_str!("shader.wgsl"), dual_source).into(),
),
});
let blend = if dual_source {
wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrc1,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrc1Alpha,
operation: wgpu::BlendOperation::Add,
},
}
} else {
wgpu::BlendState::ALPHA_BLENDING
};
let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("kui.globals"),
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,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
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: 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::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("kui"),
bind_group_layouts: &[Some(&bind_layout)],
immediate_size: 0,
});
let instance_attrs = INSTANCE_ATTRS;
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("kui.quads"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[Some(instance_buffer_layout(&instance_attrs))],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(blend),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("kui.globals"),
size: std::mem::size_of::<Globals>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let atlas_size = kui_core::atlas::ATLAS_SIZE;
let atlas_tex = create_atlas_texture(device, atlas_size);
let samplers = Samplers {
linear: device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("kui.linear"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
}),
nearest: device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("kui.nearest"),
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
}),
};
let atlas_view = atlas_tex.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group =
create_bind_group(device, &bind_layout, &globals_buf, &atlas_view, &samplers);
let instance_cap = 4096;
let instance_buf = create_instance_buffer(device, instance_cap);
let fragment_bind_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("kui.fragment.params"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<
FragmentParams,
>()
as u64),
},
count: None,
}],
});
let fragment_layouts = FragmentLayouts {
vertex: shader,
pipeline: device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("kui.fragment"),
bind_group_layouts: &[Some(&bind_layout), Some(&fragment_bind_layout)],
immediate_size: 0,
}),
};
let uniform_align = device.limits().min_uniform_buffer_offset_alignment;
let fragment_params_cap = 16;
let fragment_params_buf =
create_fragment_params_buffer(device, fragment_params_cap, uniform_align);
let fragment_bind =
create_fragment_bind_group(device, &fragment_bind_layout, &fragment_params_buf);
Ok(Self {
gpu,
surface,
config,
pipeline,
globals_buf,
bind_group,
bind_layout,
samplers,
texture_binds: Default::default(),
atlas_tex,
atlas_size,
atlas_epoch: u64::MAX,
instance_buf,
instance_cap,
instances: Vec::new(),
fragment_layouts,
fragment_params_buf,
fragment_params_cap,
fragment_bind,
fragment_bind_layout,
uniform_align,
fragment_bytes: Vec::new(),
clear_color: wgpu::Color {
r: 0.06,
g: 0.065,
b: 0.08,
a: 1.0,
},
ground: None,
backdrop_pipes: None,
backdrop_targets: None,
backdrop_idle: 0,
blurs: Vec::new(),
})
}
pub fn set_ground(&mut self, rgba: &[u8], width: u32, height: u32) {
let max = self.gpu.device().limits().max_texture_dimension_2d;
if width == 0
|| height == 0
|| width > max
|| height > max
|| rgba.len() != width as usize * height as usize * 4
{
self.ground = None;
return;
}
let device = self.gpu.device();
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("kui.ground"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
self.gpu.queue().write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
size,
);
let uv = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("kui.ground.uv"),
size: 16,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.gpu
.queue()
.write_buffer(&uv, 0, bytemuck::cast_slice(&[0.0f32, 0.0, 1.0, 1.0]));
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("kui.ground"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
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: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("kui.ground"),
layout: &layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uv.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.samplers.linear),
},
],
});
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("kui.ground"),
source: wgpu::ShaderSource::Wgsl(GROUND_SHADER.into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("kui.ground"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("kui.ground"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: self.config.format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
self.ground = Some(Ground {
pipeline,
bind,
uv,
_texture: texture,
});
}
pub fn set_ground_uv(&mut self, uv: [f32; 4]) {
if let Some(g) = &self.ground {
self.gpu
.queue()
.write_buffer(&g.uv, 0, bytemuck::cast_slice(&uv));
}
}
pub fn clear_ground(&mut self) {
self.ground = None;
}
pub fn has_ground(&self) -> bool {
self.ground.is_some()
}
pub fn subpixel_text(&self) -> bool {
self.gpu.dual_source()
}
pub fn resize(&mut self, width: u32, height: u32) {
let max = self.gpu.device().limits().max_texture_dimension_2d;
self.config.width = width.clamp(1, max);
self.config.height = height.clamp(1, max);
self.surface.configure(self.gpu.device(), &self.config);
}
fn sync_atlas(&mut self, atlas: &mut GlyphAtlas) {
if atlas.size != self.atlas_size {
self.atlas_size = atlas.size;
self.atlas_tex = create_atlas_texture(self.gpu.device(), atlas.size);
let view = self
.atlas_tex
.create_view(&wgpu::TextureViewDescriptor::default());
self.bind_group = create_bind_group(
self.gpu.device(),
&self.bind_layout,
&self.globals_buf,
&view,
&self.samplers,
);
self.atlas_epoch = u64::MAX;
}
if atlas.dirty || self.atlas_epoch != atlas.epoch {
self.gpu.queue().write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.atlas_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&atlas.pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(atlas.size * 4),
rows_per_image: Some(atlas.size),
},
wgpu::Extent3d {
width: atlas.size,
height: atlas.size,
depth_or_array_layers: 1,
},
);
atlas.dirty = false;
self.atlas_epoch = atlas.epoch;
}
}
fn plan_backdrops(&mut self, dl: &DisplayList, any: bool) {
self.blurs.clear();
let (w, h) = (self.config.width, self.config.height);
if any {
for (i, q) in dl.quads.iter().enumerate() {
if q.kind == QuadKind::Backdrop
&& let Some(b) = backdrop::plan(i as u32, q, dl.clip_of(q), w, h)
{
self.blurs.push(b);
}
}
}
if self.blurs.is_empty() {
self.backdrop_idle = self.backdrop_idle.saturating_add(1);
if self.backdrop_idle > backdrop::IDLE_FRAMES {
self.backdrop_targets = None;
}
return;
}
self.backdrop_idle = 0;
let device = self.gpu.device();
let align = self.uniform_align;
let format = self.config.format;
let pipes = self
.backdrop_pipes
.get_or_insert_with(|| backdrop::Pipes::new(device, format, align));
let mut rebind = false;
if self.blurs.len() > pipes.params_cap {
pipes.params_cap = self.blurs.len().next_power_of_two();
pipes.params = backdrop::params_buffer(device, pipes.params_cap, align);
rebind = true;
}
if rebind
|| self
.backdrop_targets
.as_ref()
.is_none_or(|t| t.size != (w, h))
{
self.backdrop_targets = Some(backdrop::Targets::new(device, pipes, format, w, h));
}
let slot = align as usize;
let mut bytes = vec![0u8; self.blurs.len() * slot];
for (i, b) in self.blurs.iter().enumerate() {
let mut p = b.params;
p.sizes[2] = w as f32;
p.sizes[3] = h as f32;
bytes[i * slot..i * slot + std::mem::size_of::<backdrop::Params>()]
.copy_from_slice(bytemuck::bytes_of(&p));
}
self.gpu.queue().write_buffer(&pipes.params, 0, &bytes);
}
fn draw_quads(
&self,
pass: &mut wgpu::RenderPass<'_>,
dl: &DisplayList,
range: std::ops::Range<u32>,
fragment_pipelines: &[wgpu::RenderPipeline],
texture_binds: &[Option<u64>],
) {
if range.is_empty() {
return;
}
pass.set_vertex_buffer(0, self.instance_buf.slice(..));
if fragment_pipelines.is_empty() && texture_binds.is_empty() {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.draw(0..6, range);
return;
}
let mut run_start = range.start;
let mut on_quads = false;
for i in range.clone() {
let q = &dl.quads[i as usize];
if q.kind != QuadKind::Fragment && q.kind != QuadKind::Texture {
continue;
}
if i > run_start {
if !on_quads {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
on_quads = true;
}
pass.draw(0..6, run_start..i);
}
let slot = q.uv[0] as usize;
if q.kind == QuadKind::Texture {
if let Some(Some(id)) = texture_binds.get(slot)
&& let Some(b) = self.texture_binds.get(id)
{
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &b.bind, &[]);
on_quads = false;
pass.draw(0..6, i..i + 1);
}
} else if let Some(pipeline) = fragment_pipelines.get(slot) {
let group0 = match draw_image_texture(&dl.fragments[slot]) {
Some(index) => texture_binds
.get(index)
.copied()
.flatten()
.and_then(|id| self.texture_binds.get(&id))
.map_or(&self.bind_group, |b| &b.bind),
None => &self.bind_group,
};
pass.set_pipeline(pipeline);
pass.set_bind_group(0, group0, &[]);
pass.set_bind_group(1, &self.fragment_bind, &[slot as u32 * self.uniform_align]);
on_quads = false;
pass.draw(0..6, i..i + 1);
}
run_start = i + 1;
}
if range.end > run_start {
if !on_quads {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
}
pass.draw(0..6, run_start..range.end);
}
}
pub fn render(
&mut self,
dl: &DisplayList,
atlas: &mut GlyphAtlas,
) -> Result<RenderReport, RenderError> {
if self.gpu.lost() {
return Err(RenderError::DeviceLost);
}
self.sync_atlas(atlas);
self.instances.clear();
let mut any_backdrop = false;
self.instances.extend(dl.quads.iter().map(|q| {
any_backdrop |= q.kind == QuadKind::Backdrop;
instance_of(q, &dl.clips, &dl.textures)
}));
self.plan_backdrops(dl, any_backdrop);
if self.instances.len() > self.instance_cap {
self.instance_cap = self.instances.len().next_power_of_two();
self.instance_buf = create_instance_buffer(self.gpu.device(), self.instance_cap);
}
if !self.instances.is_empty() {
self.gpu.queue().write_buffer(
&self.instance_buf,
0,
bytemuck::cast_slice(&self.instances),
);
}
let globals = Globals {
viewport: [dl.viewport.w.max(1.0), dl.viewport.h.max(1.0)],
atlas_size: [self.atlas_size as f32, self.atlas_size as f32],
time: dl.time,
scale: dl.scale,
_pad: [0.0; 2],
};
self.gpu
.queue()
.write_buffer(&self.globals_buf, 0, bytemuck::bytes_of(&globals));
for id in &dl.dropped_textures {
self.texture_binds.remove(&id.to_ffi());
self.gpu.drop_image_texture(id.to_ffi());
}
for id in &dl.dropped_fragments {
self.gpu.drop_fragment_pipelines(id.to_ffi());
}
if !self.texture_binds.is_empty() {
let gpu = &self.gpu;
self.texture_binds
.retain(|id, b| gpu.holds_image_texture(*id, &b.texture));
}
let mut texture_binds: Vec<Option<u64>> = Vec::new();
if !dl.textures.is_empty() {
texture_binds.reserve(dl.textures.len());
for (draw, px) in dl.textures.iter().zip(&dl.texture_pixels) {
let id = draw.id.to_ffi();
let Some(texture) = self.gpu.image_texture(id, px) else {
texture_binds.push(None);
continue;
};
let stale = self
.texture_binds
.get(&id)
.is_none_or(|b| !std::sync::Arc::ptr_eq(&b.texture, &texture));
if stale {
let device = self.gpu.device();
let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("kui.image.globals"),
size: std::mem::size_of::<Globals>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind = create_bind_group(
device,
&self.bind_layout,
&globals_buf,
&texture.view,
&self.samplers,
);
self.texture_binds.insert(
id,
TextureBind {
texture: texture.clone(),
globals: globals_buf,
bind,
},
);
}
let b = &self.texture_binds[&id];
let mine = Globals {
atlas_size: [texture.width as f32, texture.height as f32],
..globals
};
self.gpu
.queue()
.write_buffer(&b.globals, 0, bytemuck::bytes_of(&mine));
texture_binds.push(Some(id));
}
}
let mut fragment_pipelines: Vec<wgpu::RenderPipeline> = Vec::new();
if !dl.fragments.is_empty() {
let align = self.uniform_align as usize;
if dl.fragments.len() > self.fragment_params_cap {
self.fragment_params_cap = dl.fragments.len().next_power_of_two();
self.fragment_params_buf = create_fragment_params_buffer(
self.gpu.device(),
self.fragment_params_cap,
self.uniform_align,
);
self.fragment_bind = create_fragment_bind_group(
self.gpu.device(),
&self.fragment_bind_layout,
&self.fragment_params_buf,
);
}
self.fragment_bytes.clear();
self.fragment_bytes.resize(dl.fragments.len() * align, 0);
for (i, draw) in dl.fragments.iter().enumerate() {
let uv = draw.image.uv();
let slot = FragmentParams {
params: draw.params,
image: [uv[0] as f32, uv[1] as f32, uv[2] as f32, uv[3] as f32],
};
let at = i * align;
self.fragment_bytes[at..at + std::mem::size_of::<FragmentParams>()]
.copy_from_slice(bytemuck::bytes_of(&slot));
}
self.gpu
.queue()
.write_buffer(&self.fragment_params_buf, 0, &self.fragment_bytes);
fragment_pipelines.reserve(dl.fragments.len());
for (draw, source) in dl.fragments.iter().zip(&dl.fragment_sources) {
fragment_pipelines.push(self.gpu.fragment_pipeline(
draw.id.to_ffi(),
source,
self.config.format,
&self.fragment_layouts,
));
}
}
let t_wait = std::time::Instant::now();
let frame = match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(f)
| wgpu::CurrentSurfaceTexture::Suboptimal(f) => f,
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
return Err(RenderError::Skip);
}
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
return Err(RenderError::Reconfigure);
}
wgpu::CurrentSurfaceTexture::Validation => {
return Err(if self.gpu.lost() {
RenderError::DeviceLost
} else {
RenderError::Validation
});
}
};
let vsync_wait_ms = t_wait.elapsed().as_secs_f32() * 1e3;
let surface_view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.gpu
.device()
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("kui") });
let blurring = !self.blurs.is_empty();
let offscreen = match (&self.backdrop_targets, blurring) {
(Some(t), true) => Some(t),
_ => None,
};
let target = offscreen.map_or(&surface_view, |t| &t.frame);
let end = self.instances.len() as u32;
let mut start = 0u32;
let mut next = 0usize;
loop {
let stop = self.blurs.get(next).map_or(end, |b| b.quad);
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("kui"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: if next == 0 {
wgpu::LoadOp::Clear(self.clear_color)
} else {
wgpu::LoadOp::Load
},
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if next == 0
&& let Some(g) = &self.ground
{
pass.set_pipeline(&g.pipeline);
pass.set_bind_group(0, &g.bind, &[]);
pass.draw(0..4, 0..1);
}
self.draw_quads(
&mut pass,
dl,
start..stop,
&fragment_pipelines,
&texture_binds,
);
}
let (Some(b), Some(pipes), Some(t)) =
(self.blurs.get(next), &self.backdrop_pipes, offscreen)
else {
break;
};
backdrop::record(&mut encoder, pipes, t, b, next as u32 * self.uniform_align);
start = b.quad + 1;
next += 1;
}
if let (Some(pipes), Some(t)) = (&self.backdrop_pipes, offscreen) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("kui.backdrop.blit"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &surface_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&pipes.blit);
pass.set_bind_group(0, &t.blit, &[0]);
pass.draw(0..3, 0..1);
}
self.gpu.queue().submit([encoder.finish()]);
self.gpu.queue().present(frame);
Ok(RenderReport { vsync_wait_ms })
}
}
fn draw_image_texture(draw: &kui_core::FragmentDraw) -> Option<usize> {
match draw.image {
kui_core::FragmentImage::Texture { index, .. } => Some(index as usize),
_ => None,
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct RenderReport {
pub vsync_wait_ms: f32,
}
#[derive(Clone, Copy, Debug)]
pub enum RenderError {
Reconfigure,
Skip,
Validation,
DeviceLost,
}
impl std::fmt::Display for RenderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Reconfigure => write!(f, "surface outdated or lost; reconfigure"),
Self::Skip => write!(f, "no frame available; skip"),
Self::Validation => write!(f, "surface texture validation error"),
Self::DeviceLost => write!(f, "device lost; reopen"),
}
}
}
impl std::error::Error for RenderError {}
fn create_atlas_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("kui.atlas"),
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::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
})
}
fn create_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
globals: &wgpu::Buffer,
view: &wgpu::TextureView,
samplers: &Samplers,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("kui"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: globals.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&samplers.linear),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(&samplers.nearest),
},
],
})
}
fn create_instance_buffer(device: &wgpu::Device, cap: usize) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("kui.instances"),
size: (cap * std::mem::size_of::<Instance>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn create_fragment_params_buffer(device: &wgpu::Device, cap: usize, align: u32) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("kui.fragment.params"),
size: (cap.max(1) * align as usize) as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn create_fragment_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
buf: &wgpu::Buffer,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("kui.fragment.params"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: buf,
offset: 0,
size: std::num::NonZeroU64::new(std::mem::size_of::<FragmentParams>() as u64),
}),
}],
})
}
#[cfg(windows)]
pub fn report_faults() {
use std::cell::Cell;
use windows::Win32::Foundation::{
EXCEPTION_ACCESS_VIOLATION, EXCEPTION_ILLEGAL_INSTRUCTION, EXCEPTION_IN_PAGE_ERROR,
EXCEPTION_STACK_OVERFLOW, HMODULE, NTSTATUS, STATUS_FATAL_USER_CALLBACK_EXCEPTION,
};
use windows::Win32::Storage::FileSystem::WriteFile;
use windows::Win32::System::Console::{GetStdHandle, STD_ERROR_HANDLE};
use windows::Win32::System::Diagnostics::Debug::{
AddVectoredExceptionHandler, EXCEPTION_POINTERS, EXCEPTION_RECORD,
LPTOP_LEVEL_EXCEPTION_FILTER, SetUnhandledExceptionFilter,
};
use windows::Win32::System::LibraryLoader::{
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
GetModuleFileNameW, GetModuleHandleExW,
};
const CONTINUE_SEARCH: i32 = 0;
const FAULTS: [NTSTATUS; 4] = [
EXCEPTION_ACCESS_VIOLATION,
EXCEPTION_ILLEGAL_INSTRUCTION,
EXCEPTION_IN_PAGE_ERROR,
EXCEPTION_STACK_OVERFLOW,
];
static PREVIOUS: std::sync::OnceLock<LPTOP_LEVEL_EXCEPTION_FILTER> = std::sync::OnceLock::new();
thread_local! {
static LAST: Cell<Option<(i32, usize)>> = const { Cell::new(None) };
}
unsafe extern "system" fn remember(info: *mut EXCEPTION_POINTERS) -> i32 {
if let Some(record) =
(unsafe { info.as_ref() }).and_then(|i| unsafe { i.ExceptionRecord.as_ref() })
&& FAULTS.contains(&record.ExceptionCode)
{
let seen = (record.ExceptionCode.0, record.ExceptionAddress as usize);
let _ = LAST.try_with(|l| l.set(Some(seen)));
}
CONTINUE_SEARCH
}
fn nested(record: &EXCEPTION_RECORD) -> Option<(i32, usize)> {
let mut at = record.ExceptionRecord;
for _ in 0..4 {
let inner = unsafe { at.as_ref() }?;
if FAULTS.contains(&inner.ExceptionCode) {
return Some((inner.ExceptionCode.0, inner.ExceptionAddress as usize));
}
at = inner.ExceptionRecord;
}
None
}
fn say(code: i32, at: usize, escaped: Option<i32>) {
let mut module = HMODULE::default();
let mut name = [0u16; 260];
let found = unsafe {
GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
| GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
windows::core::PCWSTR(at as *const u16),
&mut module,
)
}
.is_ok();
let path = found.then(|| {
let n = unsafe { GetModuleFileNameW(Some(module), &mut name) } as usize;
&name[..n.min(name.len())]
});
let line = FaultLine::new(code as u32, at, path, escaped.map(|c| c as u32));
if let Ok(err) = unsafe { GetStdHandle(STD_ERROR_HANDLE) } {
let mut written = 0u32;
let _ = unsafe { WriteFile(err, Some(line.bytes()), Some(&mut written), None) };
}
}
unsafe extern "system" fn filter(info: *const EXCEPTION_POINTERS) -> i32 {
if let Some(record) =
(unsafe { info.as_ref() }).and_then(|i| unsafe { i.ExceptionRecord.as_ref() })
{
let (code, at) = (record.ExceptionCode, record.ExceptionAddress as usize);
let inner = if code == STATUS_FATAL_USER_CALLBACK_EXCEPTION {
nested(record).or_else(|| LAST.try_with(Cell::get).ok().flatten())
} else {
None
};
match inner {
Some((fault, fault_at)) => say(fault, fault_at, Some(code.0)),
None => say(code.0, at, None),
}
}
match PREVIOUS.get().copied().flatten() {
Some(previous) => unsafe { previous(info) },
None => CONTINUE_SEARCH,
}
}
PREVIOUS.get_or_init(|| {
unsafe {
AddVectoredExceptionHandler(0, Some(remember));
SetUnhandledExceptionFilter(Some(filter))
}
});
}
#[cfg(not(windows))]
pub fn report_faults() {}
#[cfg_attr(not(windows), allow(dead_code))]
struct FaultLine {
buf: [u8; 640],
len: usize,
}
#[cfg_attr(not(windows), allow(dead_code))]
impl FaultLine {
fn new(code: u32, at: usize, module: Option<&[u16]>, escaped: Option<u32>) -> Self {
use std::fmt::Write;
let mut line = Self {
buf: [0; 640],
len: 0,
};
let _ = write!(line, "kui: fault {code:#010x} at {at:#x} in ");
match module {
Some(path) => {
for c in char::decode_utf16(path.iter().copied()) {
let _ = line.write_char(c.unwrap_or(char::REPLACEMENT_CHARACTER));
}
}
None => {
let _ = line.write_str("no module (jit or freed code)");
}
}
if let Some(escaped) = escaped {
let _ = write!(line, ", escaped from a window callback as {escaped:#010x}");
}
line.buf[line.len] = b'\n';
line.len += 1;
line
}
fn bytes(&self) -> &[u8] {
&self.buf[..self.len]
}
}
impl std::fmt::Write for FaultLine {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
let room = self.buf.len() - 1 - self.len;
let mut n = s.len().min(room);
while !s.is_char_boundary(n) {
n -= 1;
}
self.buf[self.len..self.len + n].copy_from_slice(&s.as_bytes()[..n]);
self.len += n;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_opaque_window_presents_opaque_wherever_it_can() {
use wgpu::CompositeAlphaMode as M;
assert_eq!(opaque_mode(&[M::Opaque]), M::Opaque);
assert_eq!(opaque_mode(&[M::Opaque, M::PostMultiplied]), M::Opaque);
assert_eq!(
opaque_mode(&[
M::Auto,
M::Inherit,
M::Opaque,
M::PostMultiplied,
M::PreMultiplied
]),
M::Opaque
);
assert_eq!(opaque_mode(&[M::Inherit]), M::Inherit);
}
#[test]
fn a_transparent_window_presents_premultiplied_or_not_at_all() {
use wgpu::{Backend, CompositeAlphaMode as M};
let visual = [
M::Auto,
M::Inherit,
M::Opaque,
M::PostMultiplied,
M::PreMultiplied,
];
assert_eq!(
transparent_mode(&visual, Backend::Dx12),
Some(M::PreMultiplied)
);
assert_eq!(transparent_mode(&[M::Opaque], Backend::Dx12), None);
let metal = [M::Opaque, M::PostMultiplied];
assert_eq!(
transparent_mode(&metal, Backend::Metal),
Some(M::PostMultiplied)
);
assert_eq!(
transparent_mode(&[M::Opaque, M::PostMultiplied], Backend::Vulkan),
None
);
assert_eq!(
transparent_mode(&[M::Opaque, M::Inherit], Backend::Vulkan),
Some(M::Inherit)
);
}
#[test]
fn globals_layout_matches() {
let fields = ["viewport", "atlas_size", "time", "scale", "_pad"];
let of = |src: &str, name: &str| {
let start = src
.find(name)
.unwrap_or_else(|| panic!("{name} is not declared in\n{src}"));
let body = &src[start..];
let end = body.find('}').expect("a closing brace");
body[..end].to_string()
};
let quads = of(include_str!("shader.wgsl"), "struct Globals {");
let frags = of(kui_core::fragment::PRELUDE, "struct KuiGlobals {");
let read = |body: &str| -> Vec<String> {
body.lines()
.filter_map(|l| l.split_once(':'))
.map(|(name, ty)| format!("{}: {}", name.trim(), ty.trim().trim_end_matches(',')))
.collect()
};
let (a, b) = (read(&quads), read(&frags));
assert_eq!(a, b, "shader.wgsl and the fragment prelude disagree");
assert_eq!(
a.len(),
fields.len(),
"a field was added to the globals without this test being told"
);
for (row, want) in a.iter().zip(fields) {
assert!(row.starts_with(want), "expected {want}, got {row}");
}
assert_eq!(std::mem::size_of::<Globals>(), 32);
}
#[test]
fn fragment_params_layout_matches() {
assert_eq!(std::mem::size_of::<FragmentParams>(), 80);
assert_eq!(std::mem::offset_of!(FragmentParams, image), 64);
let epilogue = kui_core::fragment::EPILOGUE;
assert!(
epilogue
.contains("struct KuiFragmentParams { p: array<vec4<f32>, 4>, image: vec4<f32> };"),
"the epilogue's params struct moved without this test being told"
);
}
#[test]
fn prelude_bindings_match_group_zero() {
let prelude = kui_core::fragment::PRELUDE;
for line in [
"@group(0) @binding(0) var<uniform> kui_globals: KuiGlobals;",
"@group(0) @binding(1) var kui_atlas: texture_2d<f32>;",
"@group(0) @binding(2) var kui_sampler: sampler;",
"@group(0) @binding(3) var kui_sampler_nearest: sampler;",
] {
assert!(prelude.contains(line), "prelude lacks `{line}`");
}
let quads = include_str!("shader.wgsl");
for line in [
"@group(0) @binding(1) var atlas_tex: texture_2d<f32>;",
"@group(0) @binding(2) var atlas_smp: sampler;",
"@group(0) @binding(3) var nearest_smp: sampler;",
] {
assert!(quads.contains(line), "shader.wgsl lacks `{line}`");
}
}
#[test]
fn shader_variants_validate() {
use wgpu::naga::valid::{Capabilities, ValidationFlags, Validator};
for dual in [false, true] {
let src = preprocess_shader(include_str!("shader.wgsl"), dual);
let module = wgpu::naga::front::wgsl::parse_str(&src)
.unwrap_or_else(|e| panic!("dual={dual}: {}", e.emit_to_string(&src)));
let caps = if dual {
Capabilities::DUAL_SOURCE_BLENDING
} else {
Capabilities::empty()
};
Validator::new(ValidationFlags::all(), caps)
.validate(&module)
.unwrap_or_else(|e| panic!("dual={dual}: {e:?}"));
}
}
#[test]
fn the_ground_shader_validates() {
use wgpu::naga::valid::{Capabilities, ValidationFlags, Validator};
let module = wgpu::naga::front::wgsl::parse_str(GROUND_SHADER)
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(GROUND_SHADER)));
Validator::new(ValidationFlags::all(), Capabilities::empty())
.validate(&module)
.unwrap_or_else(|e| panic!("{e:?}"));
}
#[test]
fn the_backdrop_shader_validates() {
use wgpu::naga::valid::{Capabilities, ValidationFlags, Validator};
let src = include_str!("backdrop.wgsl");
let module = wgpu::naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("{}", e.emit_to_string(src)));
Validator::new(ValidationFlags::all(), Capabilities::empty())
.validate(&module)
.unwrap_or_else(|e| panic!("{e:?}"));
let names: Vec<&str> = module
.entry_points
.iter()
.map(|e| e.name.as_str())
.collect();
for want in [
"vs",
"fs_down",
"fs_blur_h",
"fs_blur_v",
"fs_composite",
"fs_blit",
] {
assert!(names.contains(&want), "{want} in {names:?}");
}
let params = module
.types
.iter()
.find(|(_, t)| t.name.as_deref() == Some("Params"))
.expect("Params")
.1;
let wgpu::naga::TypeInner::Struct { span, .. } = params.inner else {
panic!("Params is a struct");
};
assert_eq!(span as usize, std::mem::size_of::<backdrop::Params>());
}
#[test]
fn a_fault_line_names_the_code_the_address_and_the_module() {
let path: Vec<u16> = r"C:\Windows\System32\nvoglv64.dll".encode_utf16().collect();
let line = FaultLine::new(0xC000_0005, 0x7ff6_1234, Some(&path), None);
assert_eq!(
std::str::from_utf8(line.bytes()).unwrap(),
"kui: fault 0xc0000005 at 0x7ff61234 in C:\\Windows\\System32\\nvoglv64.dll\n"
);
let line = FaultLine::new(0xC000_0005, 0x10, None, Some(0xC000_041D));
assert_eq!(
std::str::from_utf8(line.bytes()).unwrap(),
"kui: fault 0xc0000005 at 0x10 in no module (jit or freed code), \
escaped from a window callback as 0xc000041d\n"
);
let line = FaultLine::new(0xC000_001D, 0x20, Some(&[0x44, 0xD800, 0x45]), None);
assert_eq!(
std::str::from_utf8(line.bytes()).unwrap(),
"kui: fault 0xc000001d at 0x20 in D\u{FFFD}E\n"
);
}
#[test]
fn a_fault_line_too_long_is_cut_at_a_character() {
let path: Vec<u16> = "é".repeat(1000).encode_utf16().collect();
let line = FaultLine::new(0xC000_00FD, 0x30, Some(&path), Some(0xC000_041D));
let text = std::str::from_utf8(line.bytes()).expect("cut at a character");
assert!(text.ends_with("é\n"), "{text:?}");
assert!(text.len() <= 640 && text.len() >= 638, "{}", text.len());
assert!(text.starts_with("kui: fault 0xc00000fd at 0x30 in é"));
}
}