1pub use wgpu;
94
95mod backdrop;
96
97use kui_core::atlas::GlyphAtlas;
98use kui_core::{Clip, DisplayList, Quad, QuadKind};
99
100#[repr(C)]
101#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
102struct Instance {
103 pos: [f32; 2],
104 size: [f32; 2],
105 color: [f32; 4],
106 border_color: [f32; 4],
107 params: [f32; 4],
108 uv: [f32; 4],
109 clip: [f32; 4],
110 radii: [f32; 4],
112 clip_radii: [f32; 4],
114}
115
116#[repr(C)]
121#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
122struct Globals {
123 viewport: [f32; 2],
124 atlas_size: [f32; 2],
125 time: f32,
126 scale: f32,
127 _pad: [f32; 2],
128}
129
130#[repr(C)]
136#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
137struct FragmentParams {
138 params: [f32; 16],
139 image: [f32; 4],
142}
143
144fn instance_of(q: &Quad, clips: &[Clip], textures: &[kui_core::display::TextureDraw]) -> Instance {
148 let clip = clips.get(q.clip as usize).copied().unwrap_or(Clip::NONE);
149 let kind = match q.kind {
150 QuadKind::Solid => 0.0,
151 QuadKind::GlyphMask => 1.0,
152 QuadKind::GlyphColor => 2.0,
153 QuadKind::Image => 3.0,
154 QuadKind::GlyphSubpixel => 4.0,
155 QuadKind::Shadow => 5.0,
156 QuadKind::Segment => 6.0,
157 QuadKind::Fragment => 7.0,
158 QuadKind::Texture => 3.0,
161 QuadKind::Backdrop => 0.0,
165 };
166 let uv = if q.kind == QuadKind::Segment {
170 q.segment_ends()
171 } else if q.kind == QuadKind::Texture {
172 let uv = textures.get(q.uv[0] as usize).map_or([0; 4], |t| t.uv);
173 [uv[0] as f32, uv[1] as f32, uv[2] as f32, uv[3] as f32]
174 } else {
175 [
176 q.uv[0] as f32,
177 q.uv[1] as f32,
178 q.uv[2] as f32,
179 q.uv[3] as f32,
180 ]
181 };
182 if q.kind == QuadKind::Backdrop {
183 return Instance {
184 pos: [q.rect.x, q.rect.y],
185 size: [0.0, 0.0],
186 color: [0.0; 4],
187 border_color: [0.0; 4],
188 params: [0.0; 4],
189 uv: [0.0; 4],
190 clip: [0.0; 4],
191 radii: [0.0; 4],
192 clip_radii: [0.0; 4],
193 };
194 }
195 Instance {
196 pos: [q.rect.x, q.rect.y],
197 size: [q.rect.w, q.rect.h],
198 color: [q.color.r, q.color.g, q.color.b, q.color.a],
199 border_color: [
200 q.border_color.r,
201 q.border_color.g,
202 q.border_color.b,
203 q.border_color.a,
204 ],
205 params: [q.blur, q.border_w, kind, 0.0],
206 uv,
207 clip: [clip.rect.x, clip.rect.y, clip.rect.w, clip.rect.h],
208 radii: q.radius,
209 clip_radii: clip.radius,
210 }
211}
212
213#[derive(Clone)]
222pub struct Gpu(std::sync::Arc<GpuInner>);
223
224struct GpuInner {
225 instance: wgpu::Instance,
226 adapter: wgpu::Adapter,
227 device: wgpu::Device,
228 queue: wgpu::Queue,
229 options: GpuOptions,
231 alpha: bool,
235 dual_source: bool,
236 fragment_pipelines: std::sync::Mutex<
243 std::collections::HashMap<(u64, wgpu::TextureFormat), wgpu::RenderPipeline>,
244 >,
245 textures: std::sync::Mutex<std::collections::HashMap<u64, std::sync::Arc<ImageTexture>>>,
250 lost: std::sync::Arc<std::sync::atomic::AtomicBool>,
254}
255
256struct ImageTexture {
260 texture: wgpu::Texture,
261 view: wgpu::TextureView,
262 width: u32,
263 height: u32,
264 rev: std::sync::Mutex<u32>,
267}
268
269#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
272#[non_exhaustive]
273pub struct GpuOptions {
274 pub transparent: bool,
285}
286
287impl GpuOptions {
288 pub fn transparent(transparent: bool) -> Self {
290 Self { transparent }
291 }
292}
293
294pub fn see_through_by_visual() -> bool {
309 static DECIDED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
310 *DECIDED.get_or_init(|| {
311 see_through_by_visual_with(
314 std::env::var("WGPU_BACKEND").ok().as_deref(),
315 std::env::var("WGPU_DX12_PRESENTATION_SYSTEM")
316 .ok()
317 .as_deref(),
318 )
319 })
320}
321
322pub fn see_through_by_visual_with(backend: Option<&str>, presentation: Option<&str>) -> bool {
336 let d3d12 =
337 backend.is_none_or(|b| wgpu::Backends::from_comma_list(b).contains(wgpu::Backends::DX12));
338 let hwnd =
340 presentation.is_some_and(|p| matches!(p.to_lowercase().as_str(), "dxgifromhwnd" | "hwnd"));
341 d3d12 && !hwnd
342}
343
344impl Gpu {
345 pub async fn new(
356 target: impl Into<wgpu::SurfaceTarget<'static>>,
357 ) -> Result<(Self, wgpu::Surface<'static>), Box<dyn std::error::Error>> {
358 Self::new_with(target, GpuOptions::default()).await
359 }
360
361 pub async fn new_with(
364 target: impl Into<wgpu::SurfaceTarget<'static>>,
365 options: GpuOptions,
366 ) -> Result<(Self, wgpu::Surface<'static>), Box<dyn std::error::Error>> {
367 report_faults();
368 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle_from_env();
379 let by_visual = cfg!(windows) && options.transparent && see_through_by_visual();
385 if cfg!(windows) {
386 if std::env::var("WGPU_BACKEND").is_err() {
387 desc.backends = wgpu::Backends::DX12;
388 } else if by_visual {
389 desc.backends &= wgpu::Backends::DX12;
394 }
395 }
396 if by_visual {
397 desc.backend_options.dx12.presentation_system = wgpu::Dx12SwapchainKind::DxgiFromVisual;
398 }
399 let instance = wgpu::Instance::new(desc);
400 let surface = instance.create_surface(target)?;
401 let adapter = instance
402 .request_adapter(&wgpu::RequestAdapterOptions {
403 compatible_surface: Some(&surface),
404 ..Default::default()
405 })
406 .await?;
407 let alpha =
411 !cfg!(windows) || (by_visual && adapter.get_info().backend == wgpu::Backend::Dx12);
412 let dual_source = adapter
414 .features()
415 .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
416 let (device, queue) = adapter
417 .request_device(&wgpu::DeviceDescriptor {
418 required_features: if dual_source {
419 wgpu::Features::DUAL_SOURCE_BLENDING
420 } else {
421 wgpu::Features::empty()
422 },
423 ..Default::default()
424 })
425 .await?;
426 device.on_uncaptured_error(std::sync::Arc::new(|e| eprintln!("kui: wgpu: {e}")));
430 let lost = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
431 device.set_device_lost_callback({
432 let lost = lost.clone();
433 move |reason, message| {
434 if reason == wgpu::DeviceLostReason::Unknown {
435 eprintln!("kui: device lost: {message}");
436 lost.store(true, std::sync::atomic::Ordering::Release);
437 }
438 }
439 });
440 let gpu = Self(std::sync::Arc::new(GpuInner {
441 instance,
442 adapter,
443 device,
444 queue,
445 options,
446 alpha,
447 dual_source,
448 fragment_pipelines: Default::default(),
449 textures: Default::default(),
450 lost,
451 }));
452 Ok((gpu, surface))
453 }
454
455 pub fn lost(&self) -> bool {
461 self.0.lost.load(std::sync::atomic::Ordering::Acquire)
462 }
463
464 pub fn mark_lost(&self) {
471 #[cfg(windows)]
472 {
473 use windows::Win32::Graphics::Direct3D12::ID3D12Device5;
474 use windows::core::Interface;
475 let removed = unsafe {
478 self.0
479 .device
480 .as_hal::<wgpu::hal::api::Dx12>()
481 .and_then(|d| d.raw_device().cast::<ID3D12Device5>().ok())
482 .map(|d| d.RemoveDevice())
483 };
484 if removed.is_some() {
485 return;
488 }
489 }
490 self.0
491 .lost
492 .store(true, std::sync::atomic::Ordering::Release);
493 }
494
495 pub fn create_surface(
498 &self,
499 target: impl Into<wgpu::SurfaceTarget<'static>>,
500 ) -> Result<wgpu::Surface<'static>, wgpu::CreateSurfaceError> {
501 self.0.instance.create_surface(target)
502 }
503
504 pub fn options(&self) -> GpuOptions {
506 self.0.options
507 }
508
509 pub fn instance(&self) -> &wgpu::Instance {
511 &self.0.instance
512 }
513
514 pub fn adapter(&self) -> &wgpu::Adapter {
516 &self.0.adapter
517 }
518
519 pub fn device(&self) -> &wgpu::Device {
521 &self.0.device
522 }
523
524 pub fn queue(&self) -> &wgpu::Queue {
526 &self.0.queue
527 }
528
529 pub fn dual_source(&self) -> bool {
533 self.0.dual_source
534 }
535
536 fn image_texture(
541 &self,
542 id: u64,
543 px: &kui_core::display::TexturePixels,
544 ) -> Option<std::sync::Arc<ImageTexture>> {
545 let max = self.0.device.limits().max_texture_dimension_2d;
551 if px.width == 0 || px.height == 0 || px.width > max || px.height > max {
552 return None;
553 }
554 let mut cache = self.0.textures.lock().unwrap_or_else(|e| e.into_inner());
555 let fresh = match cache.get(&id) {
556 Some(t) if t.width == px.width && t.height == px.height => {
557 let mut rev = t.rev.lock().unwrap_or_else(|e| e.into_inner());
558 if *rev != px.rev {
559 upload_image(&self.0.queue, &t.texture, px);
560 *rev = px.rev;
561 }
562 return Some(t.clone());
563 }
564 _ => {
565 let texture = self.0.device.create_texture(&wgpu::TextureDescriptor {
566 label: Some("kui.image"),
567 size: wgpu::Extent3d {
568 width: px.width,
569 height: px.height,
570 depth_or_array_layers: 1,
571 },
572 mip_level_count: 1,
573 sample_count: 1,
574 dimension: wgpu::TextureDimension::D2,
575 format: wgpu::TextureFormat::Rgba8Unorm,
576 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
577 view_formats: &[],
578 });
579 upload_image(&self.0.queue, &texture, px);
580 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
581 std::sync::Arc::new(ImageTexture {
582 texture,
583 view,
584 width: px.width,
585 height: px.height,
586 rev: std::sync::Mutex::new(px.rev),
587 })
588 }
589 };
590 cache.insert(id, fresh.clone());
591 Some(fresh)
592 }
593
594 fn drop_fragment_pipelines(&self, id: u64) {
598 self.0
599 .fragment_pipelines
600 .lock()
601 .unwrap_or_else(|e| e.into_inner())
602 .retain(|(fid, _), _| *fid != id);
603 }
604
605 fn drop_image_texture(&self, id: u64) {
608 self.0
609 .textures
610 .lock()
611 .unwrap_or_else(|e| e.into_inner())
612 .remove(&id);
613 }
614
615 fn holds_image_texture(&self, id: u64, texture: &std::sync::Arc<ImageTexture>) -> bool {
621 self.0
622 .textures
623 .lock()
624 .unwrap_or_else(|e| e.into_inner())
625 .get(&id)
626 .is_some_and(|t| std::sync::Arc::ptr_eq(t, texture))
627 }
628
629 fn fragment_pipeline(
641 &self,
642 id: u64,
643 source: &str,
644 format: wgpu::TextureFormat,
645 layouts: &FragmentLayouts,
646 ) -> wgpu::RenderPipeline {
647 let mut cache = self
648 .0
649 .fragment_pipelines
650 .lock()
651 .unwrap_or_else(|e| e.into_inner());
652 if let Some(p) = cache.get(&(id, format)) {
653 return p.clone();
654 }
655 let device = &self.0.device;
656 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
657 label: Some("kui.fragment"),
658 source: wgpu::ShaderSource::Wgsl(kui_core::fragment::module_source(source).into()),
659 });
660 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
661 label: Some("kui.fragment"),
662 layout: Some(&layouts.pipeline),
663 vertex: wgpu::VertexState {
664 module: &layouts.vertex,
665 entry_point: Some("vs_main"),
666 compilation_options: Default::default(),
667 buffers: &[Some(instance_buffer_layout(&INSTANCE_ATTRS))],
668 },
669 fragment: Some(wgpu::FragmentState {
670 module: &module,
671 entry_point: Some(kui_core::fragment::ENTRY_POINT),
672 compilation_options: Default::default(),
673 targets: &[Some(wgpu::ColorTargetState {
674 format,
675 blend: Some(wgpu::BlendState {
677 color: wgpu::BlendComponent {
678 src_factor: wgpu::BlendFactor::One,
679 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
680 operation: wgpu::BlendOperation::Add,
681 },
682 alpha: wgpu::BlendComponent {
683 src_factor: wgpu::BlendFactor::One,
684 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
685 operation: wgpu::BlendOperation::Add,
686 },
687 }),
688 write_mask: wgpu::ColorWrites::ALL,
689 })],
690 }),
691 primitive: wgpu::PrimitiveState::default(),
692 depth_stencil: None,
693 multisample: wgpu::MultisampleState::default(),
694 multiview_mask: None,
695 cache: None,
696 });
697 cache.insert((id, format), pipeline.clone());
698 pipeline
699 }
700}
701
702struct FragmentLayouts {
706 vertex: wgpu::ShaderModule,
707 pipeline: wgpu::PipelineLayout,
708}
709
710const INSTANCE_ATTRS: [wgpu::VertexAttribute; 9] = wgpu::vertex_attr_array![
713 0 => Float32x2, 1 => Float32x2, 2 => Float32x4,
714 3 => Float32x4, 4 => Float32x4, 5 => Float32x4,
715 6 => Float32x4, 7 => Float32x4, 8 => Float32x4,
716];
717
718fn instance_buffer_layout(attrs: &[wgpu::VertexAttribute]) -> wgpu::VertexBufferLayout<'_> {
719 wgpu::VertexBufferLayout {
720 array_stride: std::mem::size_of::<Instance>() as u64,
721 step_mode: wgpu::VertexStepMode::Instance,
722 attributes: attrs,
723 }
724}
725
726impl std::fmt::Debug for Gpu {
727 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
728 f.debug_struct("Gpu")
729 .field("adapter", &self.0.adapter.get_info().name)
730 .field("dual_source", &self.0.dual_source)
731 .finish()
732 }
733}
734
735pub struct Renderer {
743 gpu: Gpu,
744 surface: wgpu::Surface<'static>,
745 config: wgpu::SurfaceConfiguration,
746 pipeline: wgpu::RenderPipeline,
747 globals_buf: wgpu::Buffer,
748 bind_group: wgpu::BindGroup,
749 bind_layout: wgpu::BindGroupLayout,
750 samplers: Samplers,
753 texture_binds: std::collections::HashMap<u64, TextureBind>,
758 atlas_tex: wgpu::Texture,
759 atlas_size: u32,
760 atlas_epoch: u64,
761 instance_buf: wgpu::Buffer,
762 instance_cap: usize,
763 instances: Vec<Instance>,
764 fragment_layouts: FragmentLayouts,
768 fragment_params_buf: wgpu::Buffer,
771 fragment_params_cap: usize,
772 fragment_bind: wgpu::BindGroup,
773 fragment_bind_layout: wgpu::BindGroupLayout,
774 uniform_align: u32,
776 fragment_bytes: Vec<u8>,
778 pub clear_color: wgpu::Color,
785 ground: Option<Ground>,
788 backdrop_pipes: Option<backdrop::Pipes>,
791 backdrop: backdrop::Targets,
795 blurs: Vec<backdrop::Blur>,
797}
798
799struct Ground {
802 pipeline: wgpu::RenderPipeline,
803 bind: wgpu::BindGroup,
804 uv: wgpu::Buffer,
805 _texture: wgpu::Texture,
807}
808
809const GROUND_SHADER: &str = r"
812struct G { uv: vec4<f32> }
813@group(0) @binding(0) var<uniform> g: G;
814@group(0) @binding(1) var t: texture_2d<f32>;
815@group(0) @binding(2) var s: sampler;
816struct V { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32> }
817@vertex fn vs(@builtin(vertex_index) i: u32) -> V {
818 let x = f32(i & 1u);
819 let y = f32((i >> 1u) & 1u);
820 var o: V;
821 o.pos = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
822 o.uv = vec2<f32>(mix(g.uv.x, g.uv.z, x), mix(g.uv.y, g.uv.w, y));
823 return o;
824}
825@fragment fn fs(v: V) -> @location(0) vec4<f32> {
826 return vec4<f32>(textureSample(t, s, v.uv).rgb, 1.0);
827}
828";
829
830struct Samplers {
831 linear: wgpu::Sampler,
832 nearest: wgpu::Sampler,
833}
834
835struct TextureBind {
836 texture: std::sync::Arc<ImageTexture>,
837 globals: wgpu::Buffer,
838 bind: wgpu::BindGroup,
839}
840
841fn upload_image(
842 queue: &wgpu::Queue,
843 texture: &wgpu::Texture,
844 px: &kui_core::display::TexturePixels,
845) {
846 queue.write_texture(
847 wgpu::TexelCopyTextureInfo {
848 texture,
849 mip_level: 0,
850 origin: wgpu::Origin3d::ZERO,
851 aspect: wgpu::TextureAspect::All,
852 },
853 &px.rgba,
854 wgpu::TexelCopyBufferLayout {
855 offset: 0,
856 bytes_per_row: Some(px.width * 4),
857 rows_per_image: Some(px.height),
858 },
859 wgpu::Extent3d {
860 width: px.width,
861 height: px.height,
862 depth_or_array_layers: 1,
863 },
864 );
865}
866
867fn opaque_mode(modes: &[wgpu::CompositeAlphaMode]) -> wgpu::CompositeAlphaMode {
873 if modes.contains(&wgpu::CompositeAlphaMode::Opaque) {
874 wgpu::CompositeAlphaMode::Opaque
875 } else {
876 modes
877 .first()
878 .copied()
879 .unwrap_or(wgpu::CompositeAlphaMode::Opaque)
880 }
881}
882
883fn transparent_mode(
893 modes: &[wgpu::CompositeAlphaMode],
894 backend: wgpu::Backend,
895) -> Option<wgpu::CompositeAlphaMode> {
896 use wgpu::CompositeAlphaMode as M;
897 let mut wanted = vec![M::PreMultiplied];
898 if backend == wgpu::Backend::Metal {
899 wanted.push(M::PostMultiplied);
900 }
901 wanted.push(M::Inherit);
902 wanted.into_iter().find(|m| modes.contains(m))
903}
904
905fn preprocess_shader(src: &str, dual: bool) -> String {
907 let (keep, drop) = if dual {
908 ("//DUAL:", "//SINGLE:")
909 } else {
910 ("//SINGLE:", "//DUAL:")
911 };
912 let mut out = String::with_capacity(src.len());
913 for line in src.lines() {
914 if let Some(rest) = line.strip_prefix(keep) {
915 out.push_str(rest);
916 } else if line.starts_with(drop) {
917 continue;
918 } else {
919 out.push_str(line);
920 }
921 out.push('\n');
922 }
923 out
924}
925
926pub const DEFAULT_FRAME_LATENCY: u32 = if cfg!(target_os = "windows") { 1 } else { 2 };
938
939impl Renderer {
940 pub async fn new(
947 target: impl Into<wgpu::SurfaceTarget<'static>>,
948 width: u32,
949 height: u32,
950 ) -> Result<Self, Box<dyn std::error::Error>> {
951 Self::new_with(target, width, height, GpuOptions::default()).await
952 }
953
954 pub async fn new_with(
959 target: impl Into<wgpu::SurfaceTarget<'static>>,
960 width: u32,
961 height: u32,
962 options: GpuOptions,
963 ) -> Result<Self, Box<dyn std::error::Error>> {
964 let (gpu, surface) = Gpu::new_with(target, options).await?;
965 Self::with_surface(gpu, surface, width, height, options.transparent)
966 }
967
968 pub fn transparent(&self) -> bool {
975 !matches!(
976 self.config.alpha_mode,
977 wgpu::CompositeAlphaMode::Opaque | wgpu::CompositeAlphaMode::Auto
978 )
979 }
980
981 pub fn new_in(
985 gpu: &Gpu,
986 target: impl Into<wgpu::SurfaceTarget<'static>>,
987 width: u32,
988 height: u32,
989 ) -> Result<Self, Box<dyn std::error::Error>> {
990 Self::new_in_with(gpu, target, width, height, false)
991 }
992
993 pub fn new_in_with(
1003 gpu: &Gpu,
1004 target: impl Into<wgpu::SurfaceTarget<'static>>,
1005 width: u32,
1006 height: u32,
1007 transparent: bool,
1008 ) -> Result<Self, Box<dyn std::error::Error>> {
1009 let surface = gpu.create_surface(target)?;
1010 Self::with_surface(gpu.clone(), surface, width, height, transparent)
1011 }
1012
1013 pub fn gpu(&self) -> &Gpu {
1015 &self.gpu
1016 }
1017
1018 pub fn set_frame_latency(&mut self, frames: u32) {
1022 let frames = frames.max(1);
1023 if self.config.desired_maximum_frame_latency != frames {
1024 self.config.desired_maximum_frame_latency = frames;
1025 self.surface.configure(self.gpu.device(), &self.config);
1026 }
1027 }
1028
1029 pub fn frame_latency(&self) -> u32 {
1031 self.config.desired_maximum_frame_latency
1032 }
1033
1034 fn with_surface(
1035 gpu: Gpu,
1036 surface: wgpu::Surface<'static>,
1037 width: u32,
1038 height: u32,
1039 transparent: bool,
1040 ) -> Result<Self, Box<dyn std::error::Error>> {
1041 let device = gpu.device();
1042 let dual_source = gpu.dual_source();
1043
1044 let caps = surface.get_capabilities(gpu.adapter());
1045 let format = caps
1046 .formats
1047 .iter()
1048 .copied()
1049 .find(|f| !f.is_srgb())
1050 .unwrap_or(caps.formats[0]);
1051 let backend = gpu.adapter().get_info().backend;
1052 let alpha_mode = (transparent && gpu.0.alpha)
1056 .then(|| transparent_mode(&caps.alpha_modes, backend))
1057 .flatten()
1058 .unwrap_or_else(|| opaque_mode(&caps.alpha_modes));
1059 let config = wgpu::SurfaceConfiguration {
1060 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1061 format,
1062 width: width.clamp(1, device.limits().max_texture_dimension_2d),
1063 height: height.clamp(1, device.limits().max_texture_dimension_2d),
1064 present_mode: wgpu::PresentMode::AutoVsync,
1065 alpha_mode,
1066 color_space: wgpu::SurfaceColorSpace::Auto,
1067 view_formats: vec![],
1068 desired_maximum_frame_latency: DEFAULT_FRAME_LATENCY,
1071 };
1072 let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
1077 surface.configure(device, &config);
1078 if let Some(err) = pollster::block_on(scope.pop()) {
1079 return Err(format!("configuring the surface: {err}").into());
1080 }
1081
1082 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1083 label: Some("kui"),
1084 source: wgpu::ShaderSource::Wgsl(
1085 preprocess_shader(include_str!("shader.wgsl"), dual_source).into(),
1086 ),
1087 });
1088 let blend = if dual_source {
1093 wgpu::BlendState {
1094 color: wgpu::BlendComponent {
1095 src_factor: wgpu::BlendFactor::One,
1096 dst_factor: wgpu::BlendFactor::OneMinusSrc1,
1097 operation: wgpu::BlendOperation::Add,
1098 },
1099 alpha: wgpu::BlendComponent {
1100 src_factor: wgpu::BlendFactor::One,
1101 dst_factor: wgpu::BlendFactor::OneMinusSrc1Alpha,
1102 operation: wgpu::BlendOperation::Add,
1103 },
1104 }
1105 } else {
1106 wgpu::BlendState::ALPHA_BLENDING
1107 };
1108
1109 let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1110 label: Some("kui.globals"),
1111 entries: &[
1112 wgpu::BindGroupLayoutEntry {
1113 binding: 0,
1114 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1115 ty: wgpu::BindingType::Buffer {
1116 ty: wgpu::BufferBindingType::Uniform,
1117 has_dynamic_offset: false,
1118 min_binding_size: None,
1119 },
1120 count: None,
1121 },
1122 wgpu::BindGroupLayoutEntry {
1123 binding: 1,
1124 visibility: wgpu::ShaderStages::FRAGMENT,
1125 ty: wgpu::BindingType::Texture {
1126 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1127 view_dimension: wgpu::TextureViewDimension::D2,
1128 multisampled: false,
1129 },
1130 count: None,
1131 },
1132 wgpu::BindGroupLayoutEntry {
1133 binding: 2,
1134 visibility: wgpu::ShaderStages::FRAGMENT,
1135 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1136 count: None,
1137 },
1138 wgpu::BindGroupLayoutEntry {
1139 binding: 3,
1140 visibility: wgpu::ShaderStages::FRAGMENT,
1141 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1142 count: None,
1143 },
1144 ],
1145 });
1146
1147 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1148 label: Some("kui"),
1149 bind_group_layouts: &[Some(&bind_layout)],
1150 immediate_size: 0,
1151 });
1152
1153 let instance_attrs = INSTANCE_ATTRS;
1154 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1155 label: Some("kui.quads"),
1156 layout: Some(&pipeline_layout),
1157 vertex: wgpu::VertexState {
1158 module: &shader,
1159 entry_point: Some("vs_main"),
1160 compilation_options: Default::default(),
1161 buffers: &[Some(instance_buffer_layout(&instance_attrs))],
1162 },
1163 fragment: Some(wgpu::FragmentState {
1164 module: &shader,
1165 entry_point: Some("fs_main"),
1166 compilation_options: Default::default(),
1167 targets: &[Some(wgpu::ColorTargetState {
1168 format,
1169 blend: Some(blend),
1170 write_mask: wgpu::ColorWrites::ALL,
1171 })],
1172 }),
1173 primitive: wgpu::PrimitiveState::default(),
1174 depth_stencil: None,
1175 multisample: wgpu::MultisampleState::default(),
1176 multiview_mask: None,
1177 cache: None,
1178 });
1179
1180 let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
1181 label: Some("kui.globals"),
1182 size: std::mem::size_of::<Globals>() as u64,
1183 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1184 mapped_at_creation: false,
1185 });
1186
1187 let atlas_size = kui_core::atlas::ATLAS_SIZE;
1188 let atlas_tex = create_atlas_texture(device, atlas_size);
1189 let samplers = Samplers {
1190 linear: device.create_sampler(&wgpu::SamplerDescriptor {
1191 label: Some("kui.linear"),
1192 mag_filter: wgpu::FilterMode::Linear,
1193 min_filter: wgpu::FilterMode::Linear,
1194 ..Default::default()
1195 }),
1196 nearest: device.create_sampler(&wgpu::SamplerDescriptor {
1197 label: Some("kui.nearest"),
1198 mag_filter: wgpu::FilterMode::Nearest,
1199 min_filter: wgpu::FilterMode::Nearest,
1200 ..Default::default()
1201 }),
1202 };
1203 let atlas_view = atlas_tex.create_view(&wgpu::TextureViewDescriptor::default());
1204 let bind_group =
1205 create_bind_group(device, &bind_layout, &globals_buf, &atlas_view, &samplers);
1206
1207 let instance_cap = 4096;
1208 let instance_buf = create_instance_buffer(device, instance_cap);
1209
1210 let fragment_bind_layout =
1216 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1217 label: Some("kui.fragment.params"),
1218 entries: &[wgpu::BindGroupLayoutEntry {
1219 binding: 0,
1220 visibility: wgpu::ShaderStages::FRAGMENT,
1221 ty: wgpu::BindingType::Buffer {
1222 ty: wgpu::BufferBindingType::Uniform,
1223 has_dynamic_offset: true,
1224 min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<
1225 FragmentParams,
1226 >()
1227 as u64),
1228 },
1229 count: None,
1230 }],
1231 });
1232 let fragment_layouts = FragmentLayouts {
1233 vertex: shader,
1234 pipeline: device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1235 label: Some("kui.fragment"),
1236 bind_group_layouts: &[Some(&bind_layout), Some(&fragment_bind_layout)],
1237 immediate_size: 0,
1238 }),
1239 };
1240 let uniform_align = device.limits().min_uniform_buffer_offset_alignment;
1247 let fragment_params_cap = 16;
1248 let fragment_params_buf =
1249 create_fragment_params_buffer(device, fragment_params_cap, uniform_align);
1250 let fragment_bind =
1251 create_fragment_bind_group(device, &fragment_bind_layout, &fragment_params_buf);
1252
1253 Ok(Self {
1254 gpu,
1255 surface,
1256 config,
1257 pipeline,
1258 globals_buf,
1259 bind_group,
1260 bind_layout,
1261 samplers,
1262 texture_binds: Default::default(),
1263 atlas_tex,
1264 atlas_size,
1265 atlas_epoch: u64::MAX,
1266 instance_buf,
1267 instance_cap,
1268 instances: Vec::new(),
1269 fragment_layouts,
1270 fragment_params_buf,
1271 fragment_params_cap,
1272 fragment_bind,
1273 fragment_bind_layout,
1274 uniform_align,
1275 fragment_bytes: Vec::new(),
1276 clear_color: wgpu::Color {
1277 r: 0.06,
1278 g: 0.065,
1279 b: 0.08,
1280 a: 1.0,
1281 },
1282 ground: None,
1283 backdrop_pipes: None,
1284 backdrop: backdrop::Targets::default(),
1285 blurs: Vec::new(),
1286 })
1287 }
1288
1289 pub fn set_ground(&mut self, rgba: &[u8], width: u32, height: u32) {
1300 let max = self.gpu.device().limits().max_texture_dimension_2d;
1301 if width == 0
1302 || height == 0
1303 || width > max
1304 || height > max
1305 || rgba.len() != width as usize * height as usize * 4
1306 {
1307 self.ground = None;
1308 return;
1309 }
1310 let device = self.gpu.device();
1311 let size = wgpu::Extent3d {
1312 width,
1313 height,
1314 depth_or_array_layers: 1,
1315 };
1316 let texture = device.create_texture(&wgpu::TextureDescriptor {
1317 label: Some("kui.ground"),
1318 size,
1319 mip_level_count: 1,
1320 sample_count: 1,
1321 dimension: wgpu::TextureDimension::D2,
1322 format: wgpu::TextureFormat::Rgba8Unorm,
1323 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1324 view_formats: &[],
1325 });
1326 self.gpu.queue().write_texture(
1327 wgpu::TexelCopyTextureInfo {
1328 texture: &texture,
1329 mip_level: 0,
1330 origin: wgpu::Origin3d::ZERO,
1331 aspect: wgpu::TextureAspect::All,
1332 },
1333 rgba,
1334 wgpu::TexelCopyBufferLayout {
1335 offset: 0,
1336 bytes_per_row: Some(width * 4),
1337 rows_per_image: Some(height),
1338 },
1339 size,
1340 );
1341 let uv = device.create_buffer(&wgpu::BufferDescriptor {
1342 label: Some("kui.ground.uv"),
1343 size: 16,
1344 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1345 mapped_at_creation: false,
1346 });
1347 self.gpu
1348 .queue()
1349 .write_buffer(&uv, 0, bytemuck::cast_slice(&[0.0f32, 0.0, 1.0, 1.0]));
1350 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1351 label: Some("kui.ground"),
1352 entries: &[
1353 wgpu::BindGroupLayoutEntry {
1354 binding: 0,
1355 visibility: wgpu::ShaderStages::VERTEX,
1356 ty: wgpu::BindingType::Buffer {
1357 ty: wgpu::BufferBindingType::Uniform,
1358 has_dynamic_offset: false,
1359 min_binding_size: None,
1360 },
1361 count: None,
1362 },
1363 wgpu::BindGroupLayoutEntry {
1364 binding: 1,
1365 visibility: wgpu::ShaderStages::FRAGMENT,
1366 ty: wgpu::BindingType::Texture {
1367 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1368 view_dimension: wgpu::TextureViewDimension::D2,
1369 multisampled: false,
1370 },
1371 count: None,
1372 },
1373 wgpu::BindGroupLayoutEntry {
1374 binding: 2,
1375 visibility: wgpu::ShaderStages::FRAGMENT,
1376 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1377 count: None,
1378 },
1379 ],
1380 });
1381 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1382 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1383 label: Some("kui.ground"),
1384 layout: &layout,
1385 entries: &[
1386 wgpu::BindGroupEntry {
1387 binding: 0,
1388 resource: uv.as_entire_binding(),
1389 },
1390 wgpu::BindGroupEntry {
1391 binding: 1,
1392 resource: wgpu::BindingResource::TextureView(&view),
1393 },
1394 wgpu::BindGroupEntry {
1395 binding: 2,
1396 resource: wgpu::BindingResource::Sampler(&self.samplers.linear),
1397 },
1398 ],
1399 });
1400 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1401 label: Some("kui.ground"),
1402 source: wgpu::ShaderSource::Wgsl(GROUND_SHADER.into()),
1403 });
1404 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1405 label: Some("kui.ground"),
1406 bind_group_layouts: &[Some(&layout)],
1407 immediate_size: 0,
1408 });
1409 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1410 label: Some("kui.ground"),
1411 layout: Some(&pipeline_layout),
1412 vertex: wgpu::VertexState {
1413 module: &module,
1414 entry_point: Some("vs"),
1415 compilation_options: Default::default(),
1416 buffers: &[],
1417 },
1418 fragment: Some(wgpu::FragmentState {
1419 module: &module,
1420 entry_point: Some("fs"),
1421 compilation_options: Default::default(),
1422 targets: &[Some(wgpu::ColorTargetState {
1423 format: self.config.format,
1424 blend: None,
1425 write_mask: wgpu::ColorWrites::ALL,
1426 })],
1427 }),
1428 primitive: wgpu::PrimitiveState {
1429 topology: wgpu::PrimitiveTopology::TriangleStrip,
1430 ..Default::default()
1431 },
1432 depth_stencil: None,
1433 multisample: wgpu::MultisampleState::default(),
1434 multiview_mask: None,
1435 cache: None,
1436 });
1437 self.ground = Some(Ground {
1438 pipeline,
1439 bind,
1440 uv,
1441 _texture: texture,
1442 });
1443 }
1444
1445 pub fn set_ground_uv(&mut self, uv: [f32; 4]) {
1450 if let Some(g) = &self.ground {
1451 self.gpu
1452 .queue()
1453 .write_buffer(&g.uv, 0, bytemuck::cast_slice(&uv));
1454 }
1455 }
1456
1457 pub fn clear_ground(&mut self) {
1459 self.ground = None;
1460 }
1461
1462 pub fn has_ground(&self) -> bool {
1464 self.ground.is_some()
1465 }
1466
1467 pub fn subpixel_text(&self) -> bool {
1474 self.gpu.dual_source()
1475 }
1476
1477 pub fn resize(&mut self, width: u32, height: u32) {
1485 let max = self.gpu.device().limits().max_texture_dimension_2d;
1486 self.config.width = width.clamp(1, max);
1487 self.config.height = height.clamp(1, max);
1488 self.surface.configure(self.gpu.device(), &self.config);
1489 }
1490
1491 fn sync_atlas(&mut self, atlas: &mut GlyphAtlas) {
1492 if atlas.size != self.atlas_size {
1493 self.atlas_size = atlas.size;
1494 self.atlas_tex = create_atlas_texture(self.gpu.device(), atlas.size);
1495 let view = self
1496 .atlas_tex
1497 .create_view(&wgpu::TextureViewDescriptor::default());
1498 self.bind_group = create_bind_group(
1499 self.gpu.device(),
1500 &self.bind_layout,
1501 &self.globals_buf,
1502 &view,
1503 &self.samplers,
1504 );
1505 self.atlas_epoch = u64::MAX;
1506 }
1507 if atlas.dirty || self.atlas_epoch != atlas.epoch {
1508 self.gpu.queue().write_texture(
1509 wgpu::TexelCopyTextureInfo {
1510 texture: &self.atlas_tex,
1511 mip_level: 0,
1512 origin: wgpu::Origin3d::ZERO,
1513 aspect: wgpu::TextureAspect::All,
1514 },
1515 &atlas.pixels,
1516 wgpu::TexelCopyBufferLayout {
1517 offset: 0,
1518 bytes_per_row: Some(atlas.size * 4),
1519 rows_per_image: Some(atlas.size),
1520 },
1521 wgpu::Extent3d {
1522 width: atlas.size,
1523 height: atlas.size,
1524 depth_or_array_layers: 1,
1525 },
1526 );
1527 atlas.dirty = false;
1528 self.atlas_epoch = atlas.epoch;
1529 }
1530 }
1531
1532 fn plan_backdrops(&mut self, dl: &DisplayList, any: bool) {
1542 self.blurs.clear();
1543 let (w, h) = (self.config.width, self.config.height);
1544 if any {
1545 for (i, q) in dl.quads.iter().enumerate() {
1546 if q.kind == QuadKind::Backdrop
1547 && let Some(b) = backdrop::plan(i as u32, q, dl.clip_of(q), w, h)
1548 {
1549 self.blurs.push(b);
1550 }
1551 }
1552 }
1553 if self.blurs.is_empty() {
1554 self.backdrop = backdrop::Targets::default();
1555 return;
1556 }
1557 let device = self.gpu.device();
1558 let align = self.uniform_align;
1559 let format = self.config.format;
1560 let pipes = self
1561 .backdrop_pipes
1562 .get_or_insert_with(|| backdrop::Pipes::new(device, format, align));
1563 if self.blurs.len() > pipes.params_cap {
1564 pipes.params_cap = self.blurs.len().next_power_of_two();
1565 pipes.params = backdrop::params_buffer(device, pipes.params_cap, align);
1566 self.backdrop = backdrop::Targets::default();
1568 }
1569 self.backdrop
1570 .fit(device, pipes, format, (w, h), &self.blurs);
1571 let Some((_, scratch)) = self.backdrop.get() else {
1572 return;
1573 };
1574 let slot = align as usize;
1575 let mut bytes = vec![0u8; self.blurs.len() * slot];
1576 for (i, b) in self.blurs.iter().enumerate() {
1577 bytes[i * slot..i * slot + std::mem::size_of::<backdrop::Params>()]
1578 .copy_from_slice(bytemuck::bytes_of(&scratch.params(b)));
1579 }
1580 self.gpu.queue().write_buffer(&pipes.params, 0, &bytes);
1581 }
1582
1583 fn draw_quads(
1590 &self,
1591 pass: &mut wgpu::RenderPass<'_>,
1592 dl: &DisplayList,
1593 range: std::ops::Range<u32>,
1594 fragment_pipelines: &[wgpu::RenderPipeline],
1595 texture_binds: &[Option<u64>],
1596 ) {
1597 if range.is_empty() {
1598 return;
1599 }
1600 pass.set_vertex_buffer(0, self.instance_buf.slice(..));
1601 if fragment_pipelines.is_empty() && texture_binds.is_empty() {
1602 pass.set_pipeline(&self.pipeline);
1605 pass.set_bind_group(0, &self.bind_group, &[]);
1606 pass.draw(0..6, range);
1607 return;
1608 }
1609 let mut run_start = range.start;
1610 let mut on_quads = false;
1611 for i in range.clone() {
1612 let q = &dl.quads[i as usize];
1613 if q.kind != QuadKind::Fragment && q.kind != QuadKind::Texture {
1614 continue;
1615 }
1616 if i > run_start {
1617 if !on_quads {
1618 pass.set_pipeline(&self.pipeline);
1619 pass.set_bind_group(0, &self.bind_group, &[]);
1620 on_quads = true;
1621 }
1622 pass.draw(0..6, run_start..i);
1623 }
1624 let slot = q.uv[0] as usize;
1627 if q.kind == QuadKind::Texture {
1628 if let Some(Some(id)) = texture_binds.get(slot)
1629 && let Some(b) = self.texture_binds.get(id)
1630 {
1631 pass.set_pipeline(&self.pipeline);
1632 pass.set_bind_group(0, &b.bind, &[]);
1633 on_quads = false;
1634 pass.draw(0..6, i..i + 1);
1635 }
1636 } else if let Some(pipeline) = fragment_pipelines.get(slot) {
1637 let group0 = match draw_image_texture(&dl.fragments[slot]) {
1645 Some(index) => texture_binds
1646 .get(index)
1647 .copied()
1648 .flatten()
1649 .and_then(|id| self.texture_binds.get(&id))
1650 .map_or(&self.bind_group, |b| &b.bind),
1651 None => &self.bind_group,
1652 };
1653 pass.set_pipeline(pipeline);
1654 pass.set_bind_group(0, group0, &[]);
1655 pass.set_bind_group(1, &self.fragment_bind, &[slot as u32 * self.uniform_align]);
1656 on_quads = false;
1657 pass.draw(0..6, i..i + 1);
1658 }
1659 run_start = i + 1;
1660 }
1661 if range.end > run_start {
1662 if !on_quads {
1663 pass.set_pipeline(&self.pipeline);
1664 pass.set_bind_group(0, &self.bind_group, &[]);
1665 }
1666 pass.draw(0..6, run_start..range.end);
1667 }
1668 }
1669
1670 pub fn render(
1683 &mut self,
1684 dl: &DisplayList,
1685 atlas: &mut GlyphAtlas,
1686 ) -> Result<RenderReport, RenderError> {
1687 if self.gpu.lost() {
1690 return Err(RenderError::DeviceLost);
1691 }
1692 self.sync_atlas(atlas);
1693
1694 self.instances.clear();
1695 let mut any_backdrop = false;
1696 self.instances.extend(dl.quads.iter().map(|q| {
1697 any_backdrop |= q.kind == QuadKind::Backdrop;
1698 instance_of(q, &dl.clips, &dl.textures)
1699 }));
1700 self.plan_backdrops(dl, any_backdrop);
1701 if self.instances.len() > self.instance_cap {
1702 self.instance_cap = self.instances.len().next_power_of_two();
1703 self.instance_buf = create_instance_buffer(self.gpu.device(), self.instance_cap);
1704 }
1705 if !self.instances.is_empty() {
1706 self.gpu.queue().write_buffer(
1707 &self.instance_buf,
1708 0,
1709 bytemuck::cast_slice(&self.instances),
1710 );
1711 }
1712 let globals = Globals {
1713 viewport: [dl.viewport.w.max(1.0), dl.viewport.h.max(1.0)],
1714 atlas_size: [self.atlas_size as f32, self.atlas_size as f32],
1715 time: dl.time,
1716 scale: dl.scale,
1717 _pad: [0.0; 2],
1718 };
1719 self.gpu
1720 .queue()
1721 .write_buffer(&self.globals_buf, 0, bytemuck::bytes_of(&globals));
1722
1723 for id in &dl.dropped_textures {
1729 self.texture_binds.remove(&id.to_ffi());
1730 self.gpu.drop_image_texture(id.to_ffi());
1731 }
1732 for id in &dl.dropped_fragments {
1736 self.gpu.drop_fragment_pipelines(id.to_ffi());
1737 }
1738 if !self.texture_binds.is_empty() {
1742 let gpu = &self.gpu;
1743 self.texture_binds
1744 .retain(|id, b| gpu.holds_image_texture(*id, &b.texture));
1745 }
1746 let mut texture_binds: Vec<Option<u64>> = Vec::new();
1747 if !dl.textures.is_empty() {
1748 texture_binds.reserve(dl.textures.len());
1749 for (draw, px) in dl.textures.iter().zip(&dl.texture_pixels) {
1750 let id = draw.id.to_ffi();
1751 let Some(texture) = self.gpu.image_texture(id, px) else {
1752 texture_binds.push(None);
1753 continue;
1754 };
1755 let stale = self
1756 .texture_binds
1757 .get(&id)
1758 .is_none_or(|b| !std::sync::Arc::ptr_eq(&b.texture, &texture));
1759 if stale {
1760 let device = self.gpu.device();
1761 let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
1762 label: Some("kui.image.globals"),
1763 size: std::mem::size_of::<Globals>() as u64,
1764 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1765 mapped_at_creation: false,
1766 });
1767 let bind = create_bind_group(
1768 device,
1769 &self.bind_layout,
1770 &globals_buf,
1771 &texture.view,
1772 &self.samplers,
1773 );
1774 self.texture_binds.insert(
1775 id,
1776 TextureBind {
1777 texture: texture.clone(),
1778 globals: globals_buf,
1779 bind,
1780 },
1781 );
1782 }
1783 let b = &self.texture_binds[&id];
1784 let mine = Globals {
1785 atlas_size: [texture.width as f32, texture.height as f32],
1786 ..globals
1787 };
1788 self.gpu
1789 .queue()
1790 .write_buffer(&b.globals, 0, bytemuck::bytes_of(&mine));
1791 texture_binds.push(Some(id));
1792 }
1793 }
1794
1795 let mut fragment_pipelines: Vec<wgpu::RenderPipeline> = Vec::new();
1799 if !dl.fragments.is_empty() {
1800 let align = self.uniform_align as usize;
1801 if dl.fragments.len() > self.fragment_params_cap {
1802 self.fragment_params_cap = dl.fragments.len().next_power_of_two();
1803 self.fragment_params_buf = create_fragment_params_buffer(
1804 self.gpu.device(),
1805 self.fragment_params_cap,
1806 self.uniform_align,
1807 );
1808 self.fragment_bind = create_fragment_bind_group(
1809 self.gpu.device(),
1810 &self.fragment_bind_layout,
1811 &self.fragment_params_buf,
1812 );
1813 }
1814 self.fragment_bytes.clear();
1815 self.fragment_bytes.resize(dl.fragments.len() * align, 0);
1816 for (i, draw) in dl.fragments.iter().enumerate() {
1817 let uv = draw.image.uv();
1818 let slot = FragmentParams {
1819 params: draw.params,
1820 image: [uv[0] as f32, uv[1] as f32, uv[2] as f32, uv[3] as f32],
1821 };
1822 let at = i * align;
1823 self.fragment_bytes[at..at + std::mem::size_of::<FragmentParams>()]
1824 .copy_from_slice(bytemuck::bytes_of(&slot));
1825 }
1826 self.gpu
1827 .queue()
1828 .write_buffer(&self.fragment_params_buf, 0, &self.fragment_bytes);
1829 fragment_pipelines.reserve(dl.fragments.len());
1830 for (draw, source) in dl.fragments.iter().zip(&dl.fragment_sources) {
1831 fragment_pipelines.push(self.gpu.fragment_pipeline(
1832 draw.id.to_ffi(),
1833 source,
1834 self.config.format,
1835 &self.fragment_layouts,
1836 ));
1837 }
1838 }
1839
1840 let t_wait = std::time::Instant::now();
1843 let frame = match self.surface.get_current_texture() {
1844 wgpu::CurrentSurfaceTexture::Success(f)
1845 | wgpu::CurrentSurfaceTexture::Suboptimal(f) => f,
1846 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
1847 return Err(RenderError::Skip);
1848 }
1849 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
1850 return Err(RenderError::Reconfigure);
1851 }
1852 wgpu::CurrentSurfaceTexture::Validation => {
1855 return Err(if self.gpu.lost() {
1856 RenderError::DeviceLost
1857 } else {
1858 RenderError::Validation
1859 });
1860 }
1861 };
1862 let vsync_wait_ms = t_wait.elapsed().as_secs_f32() * 1e3;
1863 let surface_view = frame
1864 .texture
1865 .create_view(&wgpu::TextureViewDescriptor::default());
1866 let mut encoder = self
1867 .gpu
1868 .device()
1869 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("kui") });
1870 let blurring = !self.blurs.is_empty();
1875 let offscreen = match (&self.backdrop_pipes, self.backdrop.get(), blurring) {
1876 (Some(p), Some((f, s)), true) => Some((p, f, s)),
1877 _ => None,
1878 };
1879 let target = offscreen.map_or(&surface_view, |(_, f, _)| &f.view);
1880 let end = self.instances.len() as u32;
1881 let mut start = 0u32;
1882 let mut next = 0usize;
1883 loop {
1884 let stop = self.blurs.get(next).map_or(end, |b| b.quad);
1885 {
1886 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1887 label: Some("kui"),
1888 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1889 view: target,
1890 depth_slice: None,
1891 resolve_target: None,
1892 ops: wgpu::Operations {
1893 load: if next == 0 {
1894 wgpu::LoadOp::Clear(self.clear_color)
1895 } else {
1896 wgpu::LoadOp::Load
1897 },
1898 store: wgpu::StoreOp::Store,
1899 },
1900 })],
1901 depth_stencil_attachment: None,
1902 timestamp_writes: None,
1903 occlusion_query_set: None,
1904 multiview_mask: None,
1905 });
1906 if next == 0
1909 && let Some(g) = &self.ground
1910 {
1911 pass.set_pipeline(&g.pipeline);
1912 pass.set_bind_group(0, &g.bind, &[]);
1913 pass.draw(0..4, 0..1);
1914 }
1915 if let Some(((pipes, frame, scratch), b)) =
1918 offscreen.zip(next.checked_sub(1).and_then(|i| self.blurs.get(i)))
1919 {
1920 let slot = (next - 1) as u32 * self.uniform_align;
1921 backdrop::composite(&mut pass, pipes, scratch, b, slot);
1922 pass.set_scissor_rect(0, 0, frame.size.0, frame.size.1);
1923 }
1924 self.draw_quads(
1925 &mut pass,
1926 dl,
1927 start..stop,
1928 &fragment_pipelines,
1929 &texture_binds,
1930 );
1931 }
1932 let (Some(b), Some((pipes, frame, scratch))) = (self.blurs.get(next), offscreen) else {
1933 break;
1934 };
1935 let slot = next as u32 * self.uniform_align;
1936 backdrop::record(&mut encoder, pipes, frame, scratch, b, slot);
1937 start = b.quad + 1;
1938 next += 1;
1939 }
1940 if let Some((pipes, frame, _)) = offscreen {
1941 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1942 label: Some("kui.backdrop.blit"),
1943 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1944 view: &surface_view,
1945 depth_slice: None,
1946 resolve_target: None,
1947 ops: wgpu::Operations {
1948 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1949 store: wgpu::StoreOp::Store,
1950 },
1951 })],
1952 depth_stencil_attachment: None,
1953 timestamp_writes: None,
1954 occlusion_query_set: None,
1955 multiview_mask: None,
1956 });
1957 pass.set_pipeline(&pipes.blit);
1958 pass.set_bind_group(0, &frame.blit, &[0]);
1959 pass.draw(0..3, 0..1);
1960 }
1961 self.gpu.queue().submit([encoder.finish()]);
1962 self.gpu.queue().present(frame);
1963 Ok(RenderReport { vsync_wait_ms })
1964 }
1965}
1966
1967fn draw_image_texture(draw: &kui_core::FragmentDraw) -> Option<usize> {
1970 match draw.image {
1971 kui_core::FragmentImage::Texture { index, .. } => Some(index as usize),
1972 _ => None,
1973 }
1974}
1975
1976#[derive(Clone, Copy, Debug, Default)]
1978pub struct RenderReport {
1979 pub vsync_wait_ms: f32,
1982}
1983
1984#[derive(Clone, Copy, Debug)]
1989pub enum RenderError {
1990 Reconfigure,
1993 Skip,
1996 Validation,
2000 DeviceLost,
2003}
2004
2005impl std::fmt::Display for RenderError {
2006 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2007 match self {
2008 Self::Reconfigure => write!(f, "surface outdated or lost; reconfigure"),
2009 Self::Skip => write!(f, "no frame available; skip"),
2010 Self::Validation => write!(f, "surface texture validation error"),
2011 Self::DeviceLost => write!(f, "device lost; reopen"),
2012 }
2013 }
2014}
2015
2016impl std::error::Error for RenderError {}
2017
2018fn create_atlas_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
2019 device.create_texture(&wgpu::TextureDescriptor {
2020 label: Some("kui.atlas"),
2021 size: wgpu::Extent3d {
2022 width: size,
2023 height: size,
2024 depth_or_array_layers: 1,
2025 },
2026 mip_level_count: 1,
2027 sample_count: 1,
2028 dimension: wgpu::TextureDimension::D2,
2029 format: wgpu::TextureFormat::Rgba8Unorm,
2030 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2031 view_formats: &[],
2032 })
2033}
2034
2035fn create_bind_group(
2038 device: &wgpu::Device,
2039 layout: &wgpu::BindGroupLayout,
2040 globals: &wgpu::Buffer,
2041 view: &wgpu::TextureView,
2042 samplers: &Samplers,
2043) -> wgpu::BindGroup {
2044 device.create_bind_group(&wgpu::BindGroupDescriptor {
2045 label: Some("kui"),
2046 layout,
2047 entries: &[
2048 wgpu::BindGroupEntry {
2049 binding: 0,
2050 resource: globals.as_entire_binding(),
2051 },
2052 wgpu::BindGroupEntry {
2053 binding: 1,
2054 resource: wgpu::BindingResource::TextureView(view),
2055 },
2056 wgpu::BindGroupEntry {
2057 binding: 2,
2058 resource: wgpu::BindingResource::Sampler(&samplers.linear),
2059 },
2060 wgpu::BindGroupEntry {
2061 binding: 3,
2062 resource: wgpu::BindingResource::Sampler(&samplers.nearest),
2063 },
2064 ],
2065 })
2066}
2067
2068fn create_instance_buffer(device: &wgpu::Device, cap: usize) -> wgpu::Buffer {
2069 device.create_buffer(&wgpu::BufferDescriptor {
2070 label: Some("kui.instances"),
2071 size: (cap * std::mem::size_of::<Instance>()) as u64,
2072 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2073 mapped_at_creation: false,
2074 })
2075}
2076
2077fn create_fragment_params_buffer(device: &wgpu::Device, cap: usize, align: u32) -> wgpu::Buffer {
2078 device.create_buffer(&wgpu::BufferDescriptor {
2079 label: Some("kui.fragment.params"),
2080 size: (cap.max(1) * align as usize) as u64,
2081 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2082 mapped_at_creation: false,
2083 })
2084}
2085
2086fn create_fragment_bind_group(
2087 device: &wgpu::Device,
2088 layout: &wgpu::BindGroupLayout,
2089 buf: &wgpu::Buffer,
2090) -> wgpu::BindGroup {
2091 device.create_bind_group(&wgpu::BindGroupDescriptor {
2092 label: Some("kui.fragment.params"),
2093 layout,
2094 entries: &[wgpu::BindGroupEntry {
2095 binding: 0,
2096 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2097 buffer: buf,
2098 offset: 0,
2099 size: std::num::NonZeroU64::new(std::mem::size_of::<FragmentParams>() as u64),
2100 }),
2101 }],
2102 })
2103}
2104
2105#[cfg(windows)]
2119pub fn report_faults() {
2120 use std::cell::Cell;
2121 use windows::Win32::Foundation::{
2122 EXCEPTION_ACCESS_VIOLATION, EXCEPTION_ILLEGAL_INSTRUCTION, EXCEPTION_IN_PAGE_ERROR,
2123 EXCEPTION_STACK_OVERFLOW, HMODULE, NTSTATUS, STATUS_FATAL_USER_CALLBACK_EXCEPTION,
2124 };
2125 use windows::Win32::Storage::FileSystem::WriteFile;
2126 use windows::Win32::System::Console::{GetStdHandle, STD_ERROR_HANDLE};
2127 use windows::Win32::System::Diagnostics::Debug::{
2128 AddVectoredExceptionHandler, EXCEPTION_POINTERS, EXCEPTION_RECORD,
2129 LPTOP_LEVEL_EXCEPTION_FILTER, SetUnhandledExceptionFilter,
2130 };
2131 use windows::Win32::System::LibraryLoader::{
2132 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
2133 GetModuleFileNameW, GetModuleHandleExW,
2134 };
2135
2136 const CONTINUE_SEARCH: i32 = 0;
2137 const FAULTS: [NTSTATUS; 4] = [
2140 EXCEPTION_ACCESS_VIOLATION,
2141 EXCEPTION_ILLEGAL_INSTRUCTION,
2142 EXCEPTION_IN_PAGE_ERROR,
2143 EXCEPTION_STACK_OVERFLOW,
2144 ];
2145 static PREVIOUS: std::sync::OnceLock<LPTOP_LEVEL_EXCEPTION_FILTER> = std::sync::OnceLock::new();
2148 thread_local! {
2149 static LAST: Cell<Option<(i32, usize)>> = const { Cell::new(None) };
2154 }
2155
2156 unsafe extern "system" fn remember(info: *mut EXCEPTION_POINTERS) -> i32 {
2158 if let Some(record) =
2160 (unsafe { info.as_ref() }).and_then(|i| unsafe { i.ExceptionRecord.as_ref() })
2161 && FAULTS.contains(&record.ExceptionCode)
2162 {
2163 let seen = (record.ExceptionCode.0, record.ExceptionAddress as usize);
2164 let _ = LAST.try_with(|l| l.set(Some(seen)));
2165 }
2166 CONTINUE_SEARCH
2167 }
2168
2169 fn nested(record: &EXCEPTION_RECORD) -> Option<(i32, usize)> {
2173 let mut at = record.ExceptionRecord;
2174 for _ in 0..4 {
2175 let inner = unsafe { at.as_ref() }?;
2177 if FAULTS.contains(&inner.ExceptionCode) {
2178 return Some((inner.ExceptionCode.0, inner.ExceptionAddress as usize));
2179 }
2180 at = inner.ExceptionRecord;
2181 }
2182 None
2183 }
2184
2185 fn say(code: i32, at: usize, escaped: Option<i32>) {
2190 let mut module = HMODULE::default();
2191 let mut name = [0u16; 260];
2192 let found = unsafe {
2194 GetModuleHandleExW(
2195 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
2196 | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
2197 windows::core::PCWSTR(at as *const u16),
2198 &mut module,
2199 )
2200 }
2201 .is_ok();
2202 let path = found.then(|| {
2203 let n = unsafe { GetModuleFileNameW(Some(module), &mut name) } as usize;
2205 &name[..n.min(name.len())]
2206 });
2207 let line = FaultLine::new(code as u32, at, path, escaped.map(|c| c as u32));
2208 if let Ok(err) = unsafe { GetStdHandle(STD_ERROR_HANDLE) } {
2210 let mut written = 0u32;
2211 let _ = unsafe { WriteFile(err, Some(line.bytes()), Some(&mut written), None) };
2212 }
2213 }
2214
2215 unsafe extern "system" fn filter(info: *const EXCEPTION_POINTERS) -> i32 {
2217 if let Some(record) =
2219 (unsafe { info.as_ref() }).and_then(|i| unsafe { i.ExceptionRecord.as_ref() })
2220 {
2221 let (code, at) = (record.ExceptionCode, record.ExceptionAddress as usize);
2222 let inner = if code == STATUS_FATAL_USER_CALLBACK_EXCEPTION {
2223 nested(record).or_else(|| LAST.try_with(Cell::get).ok().flatten())
2224 } else {
2225 None
2226 };
2227 match inner {
2228 Some((fault, fault_at)) => say(fault, fault_at, Some(code.0)),
2229 None => say(code.0, at, None),
2230 }
2231 }
2232 match PREVIOUS.get().copied().flatten() {
2233 Some(previous) => unsafe { previous(info) },
2236 None => CONTINUE_SEARCH,
2237 }
2238 }
2239
2240 PREVIOUS.get_or_init(|| {
2241 unsafe {
2244 AddVectoredExceptionHandler(0, Some(remember));
2245 SetUnhandledExceptionFilter(Some(filter))
2246 }
2247 });
2248}
2249
2250#[cfg(not(windows))]
2257pub fn report_faults() {}
2258
2259#[cfg_attr(not(windows), allow(dead_code))]
2267struct FaultLine {
2268 buf: [u8; 640],
2269 len: usize,
2270}
2271
2272#[cfg_attr(not(windows), allow(dead_code))]
2273impl FaultLine {
2274 fn new(code: u32, at: usize, module: Option<&[u16]>, escaped: Option<u32>) -> Self {
2278 use std::fmt::Write;
2279 let mut line = Self {
2280 buf: [0; 640],
2281 len: 0,
2282 };
2283 let _ = write!(line, "kui: fault {code:#010x} at {at:#x} in ");
2284 match module {
2285 Some(path) => {
2286 for c in char::decode_utf16(path.iter().copied()) {
2287 let _ = line.write_char(c.unwrap_or(char::REPLACEMENT_CHARACTER));
2288 }
2289 }
2290 None => {
2291 let _ = line.write_str("no module (jit or freed code)");
2292 }
2293 }
2294 if let Some(escaped) = escaped {
2295 let _ = write!(line, ", escaped from a window callback as {escaped:#010x}");
2296 }
2297 line.buf[line.len] = b'\n';
2299 line.len += 1;
2300 line
2301 }
2302
2303 fn bytes(&self) -> &[u8] {
2304 &self.buf[..self.len]
2305 }
2306}
2307
2308impl std::fmt::Write for FaultLine {
2309 fn write_str(&mut self, s: &str) -> std::fmt::Result {
2310 let room = self.buf.len() - 1 - self.len;
2312 let mut n = s.len().min(room);
2313 while !s.is_char_boundary(n) {
2314 n -= 1;
2315 }
2316 self.buf[self.len..self.len + n].copy_from_slice(&s.as_bytes()[..n]);
2317 self.len += n;
2318 Ok(())
2319 }
2320}
2321
2322#[cfg(test)]
2323mod tests {
2324 use super::*;
2325
2326 #[test]
2330 fn an_opaque_window_presents_opaque_wherever_it_can() {
2331 use wgpu::CompositeAlphaMode as M;
2332 assert_eq!(opaque_mode(&[M::Opaque]), M::Opaque);
2333 assert_eq!(opaque_mode(&[M::Opaque, M::PostMultiplied]), M::Opaque);
2334 assert_eq!(
2335 opaque_mode(&[
2336 M::Auto,
2337 M::Inherit,
2338 M::Opaque,
2339 M::PostMultiplied,
2340 M::PreMultiplied
2341 ]),
2342 M::Opaque
2343 );
2344 assert_eq!(opaque_mode(&[M::Inherit]), M::Inherit);
2345 }
2346
2347 #[test]
2352 fn the_window_and_the_swapchain_are_decided_together() {
2353 let by = see_through_by_visual_with;
2354 assert!(by(None, None));
2356 for b in ["dx12", "D3D12", "DX12", "vulkan,dx12", " gl , d3d12 "] {
2359 assert!(by(Some(b), None), "{b}");
2360 }
2361 for b in ["vulkan", "gl", "vk,gl", "", "directx", "dx11"] {
2363 assert!(!by(Some(b), None), "{b}");
2364 }
2365 for p in ["hwnd", "Hwnd", "DxgiFromHwnd", "dxgifromhwnd"] {
2368 assert!(!by(None, Some(p)), "{p}");
2369 assert!(!by(Some("dx12"), Some(p)), "{p}");
2370 }
2371 for p in ["visual", "DxgiFromVisual", "", "nonsense", " hwnd"] {
2372 assert!(by(None, Some(p)), "{p}");
2373 }
2374 assert!(!by(Some("vulkan"), Some("visual")));
2376 }
2377
2378 #[test]
2381 fn a_transparent_window_presents_premultiplied_or_not_at_all() {
2382 use wgpu::{Backend, CompositeAlphaMode as M};
2383 let visual = [
2385 M::Auto,
2386 M::Inherit,
2387 M::Opaque,
2388 M::PostMultiplied,
2389 M::PreMultiplied,
2390 ];
2391 assert_eq!(
2392 transparent_mode(&visual, Backend::Dx12),
2393 Some(M::PreMultiplied)
2394 );
2395 assert_eq!(transparent_mode(&[M::Opaque], Backend::Dx12), None);
2397 let metal = [M::Opaque, M::PostMultiplied];
2399 assert_eq!(
2400 transparent_mode(&metal, Backend::Metal),
2401 Some(M::PostMultiplied)
2402 );
2403 assert_eq!(
2405 transparent_mode(&[M::Opaque, M::PostMultiplied], Backend::Vulkan),
2406 None
2407 );
2408 assert_eq!(
2409 transparent_mode(&[M::Opaque, M::Inherit], Backend::Vulkan),
2410 Some(M::Inherit)
2411 );
2412 }
2413
2414 #[test]
2422 fn globals_layout_matches() {
2423 let fields = ["viewport", "atlas_size", "time", "scale", "_pad"];
2424 let of = |src: &str, name: &str| {
2425 let start = src
2426 .find(name)
2427 .unwrap_or_else(|| panic!("{name} is not declared in\n{src}"));
2428 let body = &src[start..];
2429 let end = body.find('}').expect("a closing brace");
2430 body[..end].to_string()
2431 };
2432 let quads = of(include_str!("shader.wgsl"), "struct Globals {");
2433 let frags = of(kui_core::fragment::PRELUDE, "struct KuiGlobals {");
2434 let read = |body: &str| -> Vec<String> {
2435 body.lines()
2436 .filter_map(|l| l.split_once(':'))
2437 .map(|(name, ty)| format!("{}: {}", name.trim(), ty.trim().trim_end_matches(',')))
2438 .collect()
2439 };
2440 let (a, b) = (read(&quads), read(&frags));
2441 assert_eq!(a, b, "shader.wgsl and the fragment prelude disagree");
2442 assert_eq!(
2443 a.len(),
2444 fields.len(),
2445 "a field was added to the globals without this test being told"
2446 );
2447 for (row, want) in a.iter().zip(fields) {
2448 assert!(row.starts_with(want), "expected {want}, got {row}");
2449 }
2450 assert_eq!(std::mem::size_of::<Globals>(), 32);
2453 }
2454
2455 #[test]
2459 fn fragment_params_layout_matches() {
2460 assert_eq!(std::mem::size_of::<FragmentParams>(), 80);
2461 assert_eq!(std::mem::offset_of!(FragmentParams, image), 64);
2462 let epilogue = kui_core::fragment::EPILOGUE;
2463 assert!(
2464 epilogue
2465 .contains("struct KuiFragmentParams { p: array<vec4<f32>, 4>, image: vec4<f32> };"),
2466 "the epilogue's params struct moved without this test being told"
2467 );
2468 }
2469
2470 #[test]
2474 fn prelude_bindings_match_group_zero() {
2475 let prelude = kui_core::fragment::PRELUDE;
2476 for line in [
2477 "@group(0) @binding(0) var<uniform> kui_globals: KuiGlobals;",
2478 "@group(0) @binding(1) var kui_atlas: texture_2d<f32>;",
2479 "@group(0) @binding(2) var kui_sampler: sampler;",
2480 "@group(0) @binding(3) var kui_sampler_nearest: sampler;",
2481 ] {
2482 assert!(prelude.contains(line), "prelude lacks `{line}`");
2483 }
2484 let quads = include_str!("shader.wgsl");
2485 for line in [
2486 "@group(0) @binding(1) var atlas_tex: texture_2d<f32>;",
2487 "@group(0) @binding(2) var atlas_smp: sampler;",
2488 "@group(0) @binding(3) var nearest_smp: sampler;",
2489 ] {
2490 assert!(quads.contains(line), "shader.wgsl lacks `{line}`");
2491 }
2492 }
2493
2494 #[test]
2497 fn shader_variants_validate() {
2498 use wgpu::naga::valid::{Capabilities, ValidationFlags, Validator};
2499 for dual in [false, true] {
2500 let src = preprocess_shader(include_str!("shader.wgsl"), dual);
2501 let module = wgpu::naga::front::wgsl::parse_str(&src)
2502 .unwrap_or_else(|e| panic!("dual={dual}: {}", e.emit_to_string(&src)));
2503 let caps = if dual {
2504 Capabilities::DUAL_SOURCE_BLENDING
2505 } else {
2506 Capabilities::empty()
2507 };
2508 Validator::new(ValidationFlags::all(), caps)
2509 .validate(&module)
2510 .unwrap_or_else(|e| panic!("dual={dual}: {e:?}"));
2511 }
2512 }
2513
2514 #[test]
2518 fn the_ground_shader_validates() {
2519 use wgpu::naga::valid::{Capabilities, ValidationFlags, Validator};
2520 let module = wgpu::naga::front::wgsl::parse_str(GROUND_SHADER)
2521 .unwrap_or_else(|e| panic!("{}", e.emit_to_string(GROUND_SHADER)));
2522 Validator::new(ValidationFlags::all(), Capabilities::empty())
2523 .validate(&module)
2524 .unwrap_or_else(|e| panic!("{e:?}"));
2525 }
2526
2527 #[test]
2530 fn the_backdrop_shader_validates() {
2531 use wgpu::naga::valid::{Capabilities, ValidationFlags, Validator};
2532 let src = include_str!("backdrop.wgsl");
2533 let module = wgpu::naga::front::wgsl::parse_str(src)
2534 .unwrap_or_else(|e| panic!("{}", e.emit_to_string(src)));
2535 Validator::new(ValidationFlags::all(), Capabilities::empty())
2536 .validate(&module)
2537 .unwrap_or_else(|e| panic!("{e:?}"));
2538 let names: Vec<&str> = module
2539 .entry_points
2540 .iter()
2541 .map(|e| e.name.as_str())
2542 .collect();
2543 for want in [
2544 "vs",
2545 "fs_down",
2546 "fs_blur_h",
2547 "fs_blur_v",
2548 "fs_composite",
2549 "fs_blit",
2550 ] {
2551 assert!(names.contains(&want), "{want} in {names:?}");
2552 }
2553 let params = module
2554 .types
2555 .iter()
2556 .find(|(_, t)| t.name.as_deref() == Some("Params"))
2557 .expect("Params")
2558 .1;
2559 let wgpu::naga::TypeInner::Struct { span, .. } = params.inner else {
2560 panic!("Params is a struct");
2561 };
2562 assert_eq!(span as usize, std::mem::size_of::<backdrop::Params>());
2563 }
2564
2565 #[test]
2569 fn a_fault_line_names_the_code_the_address_and_the_module() {
2570 let path: Vec<u16> = r"C:\Windows\System32\nvoglv64.dll".encode_utf16().collect();
2571 let line = FaultLine::new(0xC000_0005, 0x7ff6_1234, Some(&path), None);
2572 assert_eq!(
2573 std::str::from_utf8(line.bytes()).unwrap(),
2574 "kui: fault 0xc0000005 at 0x7ff61234 in C:\\Windows\\System32\\nvoglv64.dll\n"
2575 );
2576 let line = FaultLine::new(0xC000_0005, 0x10, None, Some(0xC000_041D));
2577 assert_eq!(
2578 std::str::from_utf8(line.bytes()).unwrap(),
2579 "kui: fault 0xc0000005 at 0x10 in no module (jit or freed code), \
2580 escaped from a window callback as 0xc000041d\n"
2581 );
2582 let line = FaultLine::new(0xC000_001D, 0x20, Some(&[0x44, 0xD800, 0x45]), None);
2584 assert_eq!(
2585 std::str::from_utf8(line.bytes()).unwrap(),
2586 "kui: fault 0xc000001d at 0x20 in D\u{FFFD}E\n"
2587 );
2588 }
2589
2590 #[test]
2593 fn a_fault_line_too_long_is_cut_at_a_character() {
2594 let path: Vec<u16> = "é".repeat(1000).encode_utf16().collect();
2595 let line = FaultLine::new(0xC000_00FD, 0x30, Some(&path), Some(0xC000_041D));
2596 let text = std::str::from_utf8(line.bytes()).expect("cut at a character");
2597 assert!(text.ends_with("é\n"), "{text:?}");
2598 assert!(text.len() <= 640 && text.len() >= 638, "{}", text.len());
2599 assert!(text.starts_with("kui: fault 0xc00000fd at 0x30 in é"));
2600 }
2601}