1pub use wgpu;
94
95use kui_core::atlas::GlyphAtlas;
96use kui_core::{Clip, DisplayList, Quad, QuadKind};
97
98#[repr(C)]
99#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
100struct Instance {
101 pos: [f32; 2],
102 size: [f32; 2],
103 color: [f32; 4],
104 border_color: [f32; 4],
105 params: [f32; 4],
106 uv: [f32; 4],
107 clip: [f32; 4],
108 radii: [f32; 4],
110 clip_radii: [f32; 4],
112}
113
114#[repr(C)]
119#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
120struct Globals {
121 viewport: [f32; 2],
122 atlas_size: [f32; 2],
123 time: f32,
124 scale: f32,
125 _pad: [f32; 2],
126}
127
128#[repr(C)]
134#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
135struct FragmentParams {
136 params: [f32; 16],
137 image: [f32; 4],
140}
141
142fn instance_of(q: &Quad, clips: &[Clip], textures: &[kui_core::display::TextureDraw]) -> Instance {
146 let clip = clips.get(q.clip as usize).copied().unwrap_or(Clip::NONE);
147 let kind = match q.kind {
148 QuadKind::Solid => 0.0,
149 QuadKind::GlyphMask => 1.0,
150 QuadKind::GlyphColor => 2.0,
151 QuadKind::Image => 3.0,
152 QuadKind::GlyphSubpixel => 4.0,
153 QuadKind::Shadow => 5.0,
154 QuadKind::Segment => 6.0,
155 QuadKind::Fragment => 7.0,
156 QuadKind::Texture => 3.0,
159 };
160 let uv = if q.kind == QuadKind::Segment {
164 q.segment_ends()
165 } else if q.kind == QuadKind::Texture {
166 let uv = textures.get(q.uv[0] as usize).map_or([0; 4], |t| t.uv);
167 [uv[0] as f32, uv[1] as f32, uv[2] as f32, uv[3] as f32]
168 } else {
169 [
170 q.uv[0] as f32,
171 q.uv[1] as f32,
172 q.uv[2] as f32,
173 q.uv[3] as f32,
174 ]
175 };
176 Instance {
177 pos: [q.rect.x, q.rect.y],
178 size: [q.rect.w, q.rect.h],
179 color: [q.color.r, q.color.g, q.color.b, q.color.a],
180 border_color: [
181 q.border_color.r,
182 q.border_color.g,
183 q.border_color.b,
184 q.border_color.a,
185 ],
186 params: [q.blur, q.border_w, kind, 0.0],
187 uv,
188 clip: [clip.rect.x, clip.rect.y, clip.rect.w, clip.rect.h],
189 radii: q.radius,
190 clip_radii: clip.radius,
191 }
192}
193
194#[derive(Clone)]
203pub struct Gpu(std::sync::Arc<GpuInner>);
204
205struct GpuInner {
206 instance: wgpu::Instance,
207 adapter: wgpu::Adapter,
208 device: wgpu::Device,
209 queue: wgpu::Queue,
210 dual_source: bool,
211 fragment_pipelines: std::sync::Mutex<
218 std::collections::HashMap<(u64, wgpu::TextureFormat), wgpu::RenderPipeline>,
219 >,
220 textures: std::sync::Mutex<std::collections::HashMap<u64, std::sync::Arc<ImageTexture>>>,
225 lost: std::sync::Arc<std::sync::atomic::AtomicBool>,
229}
230
231struct ImageTexture {
235 texture: wgpu::Texture,
236 view: wgpu::TextureView,
237 width: u32,
238 height: u32,
239 rev: std::sync::Mutex<u32>,
242}
243
244impl Gpu {
245 pub async fn new(
254 target: impl Into<wgpu::SurfaceTarget<'static>>,
255 ) -> Result<(Self, wgpu::Surface<'static>), Box<dyn std::error::Error>> {
256 report_faults();
257 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle_from_env();
268 if cfg!(windows) && std::env::var_os("WGPU_BACKEND").is_none() {
269 desc.backends = wgpu::Backends::DX12;
270 }
271 let instance = wgpu::Instance::new(desc);
272 let surface = instance.create_surface(target)?;
273 let adapter = instance
274 .request_adapter(&wgpu::RequestAdapterOptions {
275 compatible_surface: Some(&surface),
276 ..Default::default()
277 })
278 .await?;
279 let dual_source = adapter
281 .features()
282 .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
283 let (device, queue) = adapter
284 .request_device(&wgpu::DeviceDescriptor {
285 required_features: if dual_source {
286 wgpu::Features::DUAL_SOURCE_BLENDING
287 } else {
288 wgpu::Features::empty()
289 },
290 ..Default::default()
291 })
292 .await?;
293 device.on_uncaptured_error(std::sync::Arc::new(|e| eprintln!("kui: wgpu: {e}")));
297 let lost = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
298 device.set_device_lost_callback({
299 let lost = lost.clone();
300 move |reason, message| {
301 if reason == wgpu::DeviceLostReason::Unknown {
302 eprintln!("kui: device lost: {message}");
303 lost.store(true, std::sync::atomic::Ordering::Release);
304 }
305 }
306 });
307 let gpu = Self(std::sync::Arc::new(GpuInner {
308 instance,
309 adapter,
310 device,
311 queue,
312 dual_source,
313 fragment_pipelines: Default::default(),
314 textures: Default::default(),
315 lost,
316 }));
317 Ok((gpu, surface))
318 }
319
320 pub fn lost(&self) -> bool {
326 self.0.lost.load(std::sync::atomic::Ordering::Acquire)
327 }
328
329 pub fn mark_lost(&self) {
336 #[cfg(windows)]
337 {
338 use windows::Win32::Graphics::Direct3D12::ID3D12Device5;
339 use windows::core::Interface;
340 let removed = unsafe {
343 self.0
344 .device
345 .as_hal::<wgpu::hal::api::Dx12>()
346 .and_then(|d| d.raw_device().cast::<ID3D12Device5>().ok())
347 .map(|d| d.RemoveDevice())
348 };
349 if removed.is_some() {
350 return;
353 }
354 }
355 self.0
356 .lost
357 .store(true, std::sync::atomic::Ordering::Release);
358 }
359
360 pub fn create_surface(
363 &self,
364 target: impl Into<wgpu::SurfaceTarget<'static>>,
365 ) -> Result<wgpu::Surface<'static>, wgpu::CreateSurfaceError> {
366 self.0.instance.create_surface(target)
367 }
368
369 pub fn instance(&self) -> &wgpu::Instance {
371 &self.0.instance
372 }
373
374 pub fn adapter(&self) -> &wgpu::Adapter {
376 &self.0.adapter
377 }
378
379 pub fn device(&self) -> &wgpu::Device {
381 &self.0.device
382 }
383
384 pub fn queue(&self) -> &wgpu::Queue {
386 &self.0.queue
387 }
388
389 pub fn dual_source(&self) -> bool {
393 self.0.dual_source
394 }
395
396 fn image_texture(
401 &self,
402 id: u64,
403 px: &kui_core::display::TexturePixels,
404 ) -> Option<std::sync::Arc<ImageTexture>> {
405 let max = self.0.device.limits().max_texture_dimension_2d;
411 if px.width == 0 || px.height == 0 || px.width > max || px.height > max {
412 return None;
413 }
414 let mut cache = self.0.textures.lock().unwrap_or_else(|e| e.into_inner());
415 let fresh = match cache.get(&id) {
416 Some(t) if t.width == px.width && t.height == px.height => {
417 let mut rev = t.rev.lock().unwrap_or_else(|e| e.into_inner());
418 if *rev != px.rev {
419 upload_image(&self.0.queue, &t.texture, px);
420 *rev = px.rev;
421 }
422 return Some(t.clone());
423 }
424 _ => {
425 let texture = self.0.device.create_texture(&wgpu::TextureDescriptor {
426 label: Some("kui.image"),
427 size: wgpu::Extent3d {
428 width: px.width,
429 height: px.height,
430 depth_or_array_layers: 1,
431 },
432 mip_level_count: 1,
433 sample_count: 1,
434 dimension: wgpu::TextureDimension::D2,
435 format: wgpu::TextureFormat::Rgba8Unorm,
436 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
437 view_formats: &[],
438 });
439 upload_image(&self.0.queue, &texture, px);
440 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
441 std::sync::Arc::new(ImageTexture {
442 texture,
443 view,
444 width: px.width,
445 height: px.height,
446 rev: std::sync::Mutex::new(px.rev),
447 })
448 }
449 };
450 cache.insert(id, fresh.clone());
451 Some(fresh)
452 }
453
454 fn drop_fragment_pipelines(&self, id: u64) {
458 self.0
459 .fragment_pipelines
460 .lock()
461 .unwrap_or_else(|e| e.into_inner())
462 .retain(|(fid, _), _| *fid != id);
463 }
464
465 fn drop_image_texture(&self, id: u64) {
468 self.0
469 .textures
470 .lock()
471 .unwrap_or_else(|e| e.into_inner())
472 .remove(&id);
473 }
474
475 fn holds_image_texture(&self, id: u64, texture: &std::sync::Arc<ImageTexture>) -> bool {
481 self.0
482 .textures
483 .lock()
484 .unwrap_or_else(|e| e.into_inner())
485 .get(&id)
486 .is_some_and(|t| std::sync::Arc::ptr_eq(t, texture))
487 }
488
489 fn fragment_pipeline(
501 &self,
502 id: u64,
503 source: &str,
504 format: wgpu::TextureFormat,
505 layouts: &FragmentLayouts,
506 ) -> wgpu::RenderPipeline {
507 let mut cache = self
508 .0
509 .fragment_pipelines
510 .lock()
511 .unwrap_or_else(|e| e.into_inner());
512 if let Some(p) = cache.get(&(id, format)) {
513 return p.clone();
514 }
515 let device = &self.0.device;
516 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
517 label: Some("kui.fragment"),
518 source: wgpu::ShaderSource::Wgsl(kui_core::fragment::module_source(source).into()),
519 });
520 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
521 label: Some("kui.fragment"),
522 layout: Some(&layouts.pipeline),
523 vertex: wgpu::VertexState {
524 module: &layouts.vertex,
525 entry_point: Some("vs_main"),
526 compilation_options: Default::default(),
527 buffers: &[Some(instance_buffer_layout(&INSTANCE_ATTRS))],
528 },
529 fragment: Some(wgpu::FragmentState {
530 module: &module,
531 entry_point: Some(kui_core::fragment::ENTRY_POINT),
532 compilation_options: Default::default(),
533 targets: &[Some(wgpu::ColorTargetState {
534 format,
535 blend: Some(wgpu::BlendState {
537 color: wgpu::BlendComponent {
538 src_factor: wgpu::BlendFactor::One,
539 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
540 operation: wgpu::BlendOperation::Add,
541 },
542 alpha: wgpu::BlendComponent {
543 src_factor: wgpu::BlendFactor::One,
544 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
545 operation: wgpu::BlendOperation::Add,
546 },
547 }),
548 write_mask: wgpu::ColorWrites::ALL,
549 })],
550 }),
551 primitive: wgpu::PrimitiveState::default(),
552 depth_stencil: None,
553 multisample: wgpu::MultisampleState::default(),
554 multiview_mask: None,
555 cache: None,
556 });
557 cache.insert((id, format), pipeline.clone());
558 pipeline
559 }
560}
561
562struct FragmentLayouts {
566 vertex: wgpu::ShaderModule,
567 pipeline: wgpu::PipelineLayout,
568}
569
570const INSTANCE_ATTRS: [wgpu::VertexAttribute; 9] = wgpu::vertex_attr_array![
573 0 => Float32x2, 1 => Float32x2, 2 => Float32x4,
574 3 => Float32x4, 4 => Float32x4, 5 => Float32x4,
575 6 => Float32x4, 7 => Float32x4, 8 => Float32x4,
576];
577
578fn instance_buffer_layout(attrs: &[wgpu::VertexAttribute]) -> wgpu::VertexBufferLayout<'_> {
579 wgpu::VertexBufferLayout {
580 array_stride: std::mem::size_of::<Instance>() as u64,
581 step_mode: wgpu::VertexStepMode::Instance,
582 attributes: attrs,
583 }
584}
585
586impl std::fmt::Debug for Gpu {
587 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
588 f.debug_struct("Gpu")
589 .field("adapter", &self.0.adapter.get_info().name)
590 .field("dual_source", &self.0.dual_source)
591 .finish()
592 }
593}
594
595pub struct Renderer {
603 gpu: Gpu,
604 surface: wgpu::Surface<'static>,
605 config: wgpu::SurfaceConfiguration,
606 pipeline: wgpu::RenderPipeline,
607 globals_buf: wgpu::Buffer,
608 bind_group: wgpu::BindGroup,
609 bind_layout: wgpu::BindGroupLayout,
610 samplers: Samplers,
613 texture_binds: std::collections::HashMap<u64, TextureBind>,
618 atlas_tex: wgpu::Texture,
619 atlas_size: u32,
620 atlas_epoch: u64,
621 instance_buf: wgpu::Buffer,
622 instance_cap: usize,
623 instances: Vec<Instance>,
624 fragment_layouts: FragmentLayouts,
628 fragment_params_buf: wgpu::Buffer,
631 fragment_params_cap: usize,
632 fragment_bind: wgpu::BindGroup,
633 fragment_bind_layout: wgpu::BindGroupLayout,
634 uniform_align: u32,
636 fragment_bytes: Vec<u8>,
638 pub clear_color: wgpu::Color,
645}
646
647struct Samplers {
648 linear: wgpu::Sampler,
649 nearest: wgpu::Sampler,
650}
651
652struct TextureBind {
653 texture: std::sync::Arc<ImageTexture>,
654 globals: wgpu::Buffer,
655 bind: wgpu::BindGroup,
656}
657
658fn upload_image(
659 queue: &wgpu::Queue,
660 texture: &wgpu::Texture,
661 px: &kui_core::display::TexturePixels,
662) {
663 queue.write_texture(
664 wgpu::TexelCopyTextureInfo {
665 texture,
666 mip_level: 0,
667 origin: wgpu::Origin3d::ZERO,
668 aspect: wgpu::TextureAspect::All,
669 },
670 &px.rgba,
671 wgpu::TexelCopyBufferLayout {
672 offset: 0,
673 bytes_per_row: Some(px.width * 4),
674 rows_per_image: Some(px.height),
675 },
676 wgpu::Extent3d {
677 width: px.width,
678 height: px.height,
679 depth_or_array_layers: 1,
680 },
681 );
682}
683
684fn preprocess_shader(src: &str, dual: bool) -> String {
686 let (keep, drop) = if dual {
687 ("//DUAL:", "//SINGLE:")
688 } else {
689 ("//SINGLE:", "//DUAL:")
690 };
691 let mut out = String::with_capacity(src.len());
692 for line in src.lines() {
693 if let Some(rest) = line.strip_prefix(keep) {
694 out.push_str(rest);
695 } else if line.starts_with(drop) {
696 continue;
697 } else {
698 out.push_str(line);
699 }
700 out.push('\n');
701 }
702 out
703}
704
705pub const DEFAULT_FRAME_LATENCY: u32 = if cfg!(target_os = "windows") { 1 } else { 2 };
717
718impl Renderer {
719 pub async fn new(
726 target: impl Into<wgpu::SurfaceTarget<'static>>,
727 width: u32,
728 height: u32,
729 ) -> Result<Self, Box<dyn std::error::Error>> {
730 let (gpu, surface) = Gpu::new(target).await?;
731 Self::with_surface(gpu, surface, width, height)
732 }
733
734 pub fn new_in(
738 gpu: &Gpu,
739 target: impl Into<wgpu::SurfaceTarget<'static>>,
740 width: u32,
741 height: u32,
742 ) -> Result<Self, Box<dyn std::error::Error>> {
743 let surface = gpu.create_surface(target)?;
744 Self::with_surface(gpu.clone(), surface, width, height)
745 }
746
747 pub fn gpu(&self) -> &Gpu {
749 &self.gpu
750 }
751
752 pub fn set_frame_latency(&mut self, frames: u32) {
756 let frames = frames.max(1);
757 if self.config.desired_maximum_frame_latency != frames {
758 self.config.desired_maximum_frame_latency = frames;
759 self.surface.configure(self.gpu.device(), &self.config);
760 }
761 }
762
763 pub fn frame_latency(&self) -> u32 {
765 self.config.desired_maximum_frame_latency
766 }
767
768 fn with_surface(
769 gpu: Gpu,
770 surface: wgpu::Surface<'static>,
771 width: u32,
772 height: u32,
773 ) -> Result<Self, Box<dyn std::error::Error>> {
774 let device = gpu.device();
775 let dual_source = gpu.dual_source();
776
777 let caps = surface.get_capabilities(gpu.adapter());
778 let format = caps
779 .formats
780 .iter()
781 .copied()
782 .find(|f| !f.is_srgb())
783 .unwrap_or(caps.formats[0]);
784 let config = wgpu::SurfaceConfiguration {
785 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
786 format,
787 width: width.clamp(1, device.limits().max_texture_dimension_2d),
788 height: height.clamp(1, device.limits().max_texture_dimension_2d),
789 present_mode: wgpu::PresentMode::AutoVsync,
790 alpha_mode: caps.alpha_modes[0],
791 color_space: wgpu::SurfaceColorSpace::Auto,
792 view_formats: vec![],
793 desired_maximum_frame_latency: DEFAULT_FRAME_LATENCY,
796 };
797 let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
802 surface.configure(device, &config);
803 if let Some(err) = pollster::block_on(scope.pop()) {
804 return Err(format!("configuring the surface: {err}").into());
805 }
806
807 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
808 label: Some("kui"),
809 source: wgpu::ShaderSource::Wgsl(
810 preprocess_shader(include_str!("shader.wgsl"), dual_source).into(),
811 ),
812 });
813 let blend = if dual_source {
818 wgpu::BlendState {
819 color: wgpu::BlendComponent {
820 src_factor: wgpu::BlendFactor::One,
821 dst_factor: wgpu::BlendFactor::OneMinusSrc1,
822 operation: wgpu::BlendOperation::Add,
823 },
824 alpha: wgpu::BlendComponent {
825 src_factor: wgpu::BlendFactor::One,
826 dst_factor: wgpu::BlendFactor::OneMinusSrc1Alpha,
827 operation: wgpu::BlendOperation::Add,
828 },
829 }
830 } else {
831 wgpu::BlendState::ALPHA_BLENDING
832 };
833
834 let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
835 label: Some("kui.globals"),
836 entries: &[
837 wgpu::BindGroupLayoutEntry {
838 binding: 0,
839 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
840 ty: wgpu::BindingType::Buffer {
841 ty: wgpu::BufferBindingType::Uniform,
842 has_dynamic_offset: false,
843 min_binding_size: None,
844 },
845 count: None,
846 },
847 wgpu::BindGroupLayoutEntry {
848 binding: 1,
849 visibility: wgpu::ShaderStages::FRAGMENT,
850 ty: wgpu::BindingType::Texture {
851 sample_type: wgpu::TextureSampleType::Float { filterable: true },
852 view_dimension: wgpu::TextureViewDimension::D2,
853 multisampled: false,
854 },
855 count: None,
856 },
857 wgpu::BindGroupLayoutEntry {
858 binding: 2,
859 visibility: wgpu::ShaderStages::FRAGMENT,
860 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
861 count: None,
862 },
863 wgpu::BindGroupLayoutEntry {
864 binding: 3,
865 visibility: wgpu::ShaderStages::FRAGMENT,
866 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
867 count: None,
868 },
869 ],
870 });
871
872 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
873 label: Some("kui"),
874 bind_group_layouts: &[Some(&bind_layout)],
875 immediate_size: 0,
876 });
877
878 let instance_attrs = INSTANCE_ATTRS;
879 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
880 label: Some("kui.quads"),
881 layout: Some(&pipeline_layout),
882 vertex: wgpu::VertexState {
883 module: &shader,
884 entry_point: Some("vs_main"),
885 compilation_options: Default::default(),
886 buffers: &[Some(instance_buffer_layout(&instance_attrs))],
887 },
888 fragment: Some(wgpu::FragmentState {
889 module: &shader,
890 entry_point: Some("fs_main"),
891 compilation_options: Default::default(),
892 targets: &[Some(wgpu::ColorTargetState {
893 format,
894 blend: Some(blend),
895 write_mask: wgpu::ColorWrites::ALL,
896 })],
897 }),
898 primitive: wgpu::PrimitiveState::default(),
899 depth_stencil: None,
900 multisample: wgpu::MultisampleState::default(),
901 multiview_mask: None,
902 cache: None,
903 });
904
905 let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
906 label: Some("kui.globals"),
907 size: std::mem::size_of::<Globals>() as u64,
908 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
909 mapped_at_creation: false,
910 });
911
912 let atlas_size = kui_core::atlas::ATLAS_SIZE;
913 let atlas_tex = create_atlas_texture(device, atlas_size);
914 let samplers = Samplers {
915 linear: device.create_sampler(&wgpu::SamplerDescriptor {
916 label: Some("kui.linear"),
917 mag_filter: wgpu::FilterMode::Linear,
918 min_filter: wgpu::FilterMode::Linear,
919 ..Default::default()
920 }),
921 nearest: device.create_sampler(&wgpu::SamplerDescriptor {
922 label: Some("kui.nearest"),
923 mag_filter: wgpu::FilterMode::Nearest,
924 min_filter: wgpu::FilterMode::Nearest,
925 ..Default::default()
926 }),
927 };
928 let atlas_view = atlas_tex.create_view(&wgpu::TextureViewDescriptor::default());
929 let bind_group =
930 create_bind_group(device, &bind_layout, &globals_buf, &atlas_view, &samplers);
931
932 let instance_cap = 4096;
933 let instance_buf = create_instance_buffer(device, instance_cap);
934
935 let fragment_bind_layout =
941 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
942 label: Some("kui.fragment.params"),
943 entries: &[wgpu::BindGroupLayoutEntry {
944 binding: 0,
945 visibility: wgpu::ShaderStages::FRAGMENT,
946 ty: wgpu::BindingType::Buffer {
947 ty: wgpu::BufferBindingType::Uniform,
948 has_dynamic_offset: true,
949 min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<
950 FragmentParams,
951 >()
952 as u64),
953 },
954 count: None,
955 }],
956 });
957 let fragment_layouts = FragmentLayouts {
958 vertex: shader,
959 pipeline: device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
960 label: Some("kui.fragment"),
961 bind_group_layouts: &[Some(&bind_layout), Some(&fragment_bind_layout)],
962 immediate_size: 0,
963 }),
964 };
965 let uniform_align = device.limits().min_uniform_buffer_offset_alignment;
972 let fragment_params_cap = 16;
973 let fragment_params_buf =
974 create_fragment_params_buffer(device, fragment_params_cap, uniform_align);
975 let fragment_bind =
976 create_fragment_bind_group(device, &fragment_bind_layout, &fragment_params_buf);
977
978 Ok(Self {
979 gpu,
980 surface,
981 config,
982 pipeline,
983 globals_buf,
984 bind_group,
985 bind_layout,
986 samplers,
987 texture_binds: Default::default(),
988 atlas_tex,
989 atlas_size,
990 atlas_epoch: u64::MAX,
991 instance_buf,
992 instance_cap,
993 instances: Vec::new(),
994 fragment_layouts,
995 fragment_params_buf,
996 fragment_params_cap,
997 fragment_bind,
998 fragment_bind_layout,
999 uniform_align,
1000 fragment_bytes: Vec::new(),
1001 clear_color: wgpu::Color {
1002 r: 0.06,
1003 g: 0.065,
1004 b: 0.08,
1005 a: 1.0,
1006 },
1007 })
1008 }
1009
1010 pub fn subpixel_text(&self) -> bool {
1017 self.gpu.dual_source()
1018 }
1019
1020 pub fn resize(&mut self, width: u32, height: u32) {
1028 let max = self.gpu.device().limits().max_texture_dimension_2d;
1029 self.config.width = width.clamp(1, max);
1030 self.config.height = height.clamp(1, max);
1031 self.surface.configure(self.gpu.device(), &self.config);
1032 }
1033
1034 fn sync_atlas(&mut self, atlas: &mut GlyphAtlas) {
1035 if atlas.size != self.atlas_size {
1036 self.atlas_size = atlas.size;
1037 self.atlas_tex = create_atlas_texture(self.gpu.device(), atlas.size);
1038 let view = self
1039 .atlas_tex
1040 .create_view(&wgpu::TextureViewDescriptor::default());
1041 self.bind_group = create_bind_group(
1042 self.gpu.device(),
1043 &self.bind_layout,
1044 &self.globals_buf,
1045 &view,
1046 &self.samplers,
1047 );
1048 self.atlas_epoch = u64::MAX;
1049 }
1050 if atlas.dirty || self.atlas_epoch != atlas.epoch {
1051 self.gpu.queue().write_texture(
1052 wgpu::TexelCopyTextureInfo {
1053 texture: &self.atlas_tex,
1054 mip_level: 0,
1055 origin: wgpu::Origin3d::ZERO,
1056 aspect: wgpu::TextureAspect::All,
1057 },
1058 &atlas.pixels,
1059 wgpu::TexelCopyBufferLayout {
1060 offset: 0,
1061 bytes_per_row: Some(atlas.size * 4),
1062 rows_per_image: Some(atlas.size),
1063 },
1064 wgpu::Extent3d {
1065 width: atlas.size,
1066 height: atlas.size,
1067 depth_or_array_layers: 1,
1068 },
1069 );
1070 atlas.dirty = false;
1071 self.atlas_epoch = atlas.epoch;
1072 }
1073 }
1074
1075 pub fn render(
1088 &mut self,
1089 dl: &DisplayList,
1090 atlas: &mut GlyphAtlas,
1091 ) -> Result<RenderReport, RenderError> {
1092 if self.gpu.lost() {
1095 return Err(RenderError::DeviceLost);
1096 }
1097 self.sync_atlas(atlas);
1098
1099 self.instances.clear();
1100 self.instances.extend(
1101 dl.quads
1102 .iter()
1103 .map(|q| instance_of(q, &dl.clips, &dl.textures)),
1104 );
1105 if self.instances.len() > self.instance_cap {
1106 self.instance_cap = self.instances.len().next_power_of_two();
1107 self.instance_buf = create_instance_buffer(self.gpu.device(), self.instance_cap);
1108 }
1109 if !self.instances.is_empty() {
1110 self.gpu.queue().write_buffer(
1111 &self.instance_buf,
1112 0,
1113 bytemuck::cast_slice(&self.instances),
1114 );
1115 }
1116 let globals = Globals {
1117 viewport: [dl.viewport.w.max(1.0), dl.viewport.h.max(1.0)],
1118 atlas_size: [self.atlas_size as f32, self.atlas_size as f32],
1119 time: dl.time,
1120 scale: dl.scale,
1121 _pad: [0.0; 2],
1122 };
1123 self.gpu
1124 .queue()
1125 .write_buffer(&self.globals_buf, 0, bytemuck::bytes_of(&globals));
1126
1127 for id in &dl.dropped_textures {
1133 self.texture_binds.remove(&id.to_ffi());
1134 self.gpu.drop_image_texture(id.to_ffi());
1135 }
1136 for id in &dl.dropped_fragments {
1140 self.gpu.drop_fragment_pipelines(id.to_ffi());
1141 }
1142 if !self.texture_binds.is_empty() {
1146 let gpu = &self.gpu;
1147 self.texture_binds
1148 .retain(|id, b| gpu.holds_image_texture(*id, &b.texture));
1149 }
1150 let mut texture_binds: Vec<Option<u64>> = Vec::new();
1151 if !dl.textures.is_empty() {
1152 texture_binds.reserve(dl.textures.len());
1153 for (draw, px) in dl.textures.iter().zip(&dl.texture_pixels) {
1154 let id = draw.id.to_ffi();
1155 let Some(texture) = self.gpu.image_texture(id, px) else {
1156 texture_binds.push(None);
1157 continue;
1158 };
1159 let stale = self
1160 .texture_binds
1161 .get(&id)
1162 .is_none_or(|b| !std::sync::Arc::ptr_eq(&b.texture, &texture));
1163 if stale {
1164 let device = self.gpu.device();
1165 let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
1166 label: Some("kui.image.globals"),
1167 size: std::mem::size_of::<Globals>() as u64,
1168 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1169 mapped_at_creation: false,
1170 });
1171 let bind = create_bind_group(
1172 device,
1173 &self.bind_layout,
1174 &globals_buf,
1175 &texture.view,
1176 &self.samplers,
1177 );
1178 self.texture_binds.insert(
1179 id,
1180 TextureBind {
1181 texture: texture.clone(),
1182 globals: globals_buf,
1183 bind,
1184 },
1185 );
1186 }
1187 let b = &self.texture_binds[&id];
1188 let mine = Globals {
1189 atlas_size: [texture.width as f32, texture.height as f32],
1190 ..globals
1191 };
1192 self.gpu
1193 .queue()
1194 .write_buffer(&b.globals, 0, bytemuck::bytes_of(&mine));
1195 texture_binds.push(Some(id));
1196 }
1197 }
1198
1199 let mut fragment_pipelines: Vec<wgpu::RenderPipeline> = Vec::new();
1203 if !dl.fragments.is_empty() {
1204 let align = self.uniform_align as usize;
1205 if dl.fragments.len() > self.fragment_params_cap {
1206 self.fragment_params_cap = dl.fragments.len().next_power_of_two();
1207 self.fragment_params_buf = create_fragment_params_buffer(
1208 self.gpu.device(),
1209 self.fragment_params_cap,
1210 self.uniform_align,
1211 );
1212 self.fragment_bind = create_fragment_bind_group(
1213 self.gpu.device(),
1214 &self.fragment_bind_layout,
1215 &self.fragment_params_buf,
1216 );
1217 }
1218 self.fragment_bytes.clear();
1219 self.fragment_bytes.resize(dl.fragments.len() * align, 0);
1220 for (i, draw) in dl.fragments.iter().enumerate() {
1221 let uv = draw.image.uv();
1222 let slot = FragmentParams {
1223 params: draw.params,
1224 image: [uv[0] as f32, uv[1] as f32, uv[2] as f32, uv[3] as f32],
1225 };
1226 let at = i * align;
1227 self.fragment_bytes[at..at + std::mem::size_of::<FragmentParams>()]
1228 .copy_from_slice(bytemuck::bytes_of(&slot));
1229 }
1230 self.gpu
1231 .queue()
1232 .write_buffer(&self.fragment_params_buf, 0, &self.fragment_bytes);
1233 fragment_pipelines.reserve(dl.fragments.len());
1234 for (draw, source) in dl.fragments.iter().zip(&dl.fragment_sources) {
1235 fragment_pipelines.push(self.gpu.fragment_pipeline(
1236 draw.id.to_ffi(),
1237 source,
1238 self.config.format,
1239 &self.fragment_layouts,
1240 ));
1241 }
1242 }
1243
1244 let t_wait = std::time::Instant::now();
1247 let frame = match self.surface.get_current_texture() {
1248 wgpu::CurrentSurfaceTexture::Success(f)
1249 | wgpu::CurrentSurfaceTexture::Suboptimal(f) => f,
1250 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
1251 return Err(RenderError::Skip);
1252 }
1253 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
1254 return Err(RenderError::Reconfigure);
1255 }
1256 wgpu::CurrentSurfaceTexture::Validation => {
1259 return Err(if self.gpu.lost() {
1260 RenderError::DeviceLost
1261 } else {
1262 RenderError::Validation
1263 });
1264 }
1265 };
1266 let vsync_wait_ms = t_wait.elapsed().as_secs_f32() * 1e3;
1267 let view = frame
1268 .texture
1269 .create_view(&wgpu::TextureViewDescriptor::default());
1270 let mut encoder = self
1271 .gpu
1272 .device()
1273 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("kui") });
1274 {
1275 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1276 label: Some("kui"),
1277 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1278 view: &view,
1279 depth_slice: None,
1280 resolve_target: None,
1281 ops: wgpu::Operations {
1282 load: wgpu::LoadOp::Clear(self.clear_color),
1283 store: wgpu::StoreOp::Store,
1284 },
1285 })],
1286 depth_stencil_attachment: None,
1287 timestamp_writes: None,
1288 occlusion_query_set: None,
1289 multiview_mask: None,
1290 });
1291 if !self.instances.is_empty() {
1292 pass.set_vertex_buffer(0, self.instance_buf.slice(..));
1293 if fragment_pipelines.is_empty() && texture_binds.is_empty() {
1294 pass.set_pipeline(&self.pipeline);
1297 pass.set_bind_group(0, &self.bind_group, &[]);
1298 pass.draw(0..6, 0..self.instances.len() as u32);
1299 } else {
1300 let mut run_start = 0u32;
1308 let mut on_quads = false;
1309 for (i, q) in dl.quads.iter().enumerate() {
1310 if q.kind != QuadKind::Fragment && q.kind != QuadKind::Texture {
1311 continue;
1312 }
1313 let i = i as u32;
1314 if i > run_start {
1315 if !on_quads {
1316 pass.set_pipeline(&self.pipeline);
1317 pass.set_bind_group(0, &self.bind_group, &[]);
1318 on_quads = true;
1319 }
1320 pass.draw(0..6, run_start..i);
1321 }
1322 let slot = q.uv[0] as usize;
1326 if q.kind == QuadKind::Texture {
1327 if let Some(Some(id)) = texture_binds.get(slot)
1328 && let Some(b) = self.texture_binds.get(id)
1329 {
1330 pass.set_pipeline(&self.pipeline);
1331 pass.set_bind_group(0, &b.bind, &[]);
1332 on_quads = false;
1333 pass.draw(0..6, i..i + 1);
1334 }
1335 } else if let Some(pipeline) = fragment_pipelines.get(slot) {
1336 let group0 = match draw_image_texture(&dl.fragments[slot]) {
1346 Some(index) => texture_binds
1347 .get(index)
1348 .copied()
1349 .flatten()
1350 .and_then(|id| self.texture_binds.get(&id))
1351 .map_or(&self.bind_group, |b| &b.bind),
1352 None => &self.bind_group,
1353 };
1354 pass.set_pipeline(pipeline);
1355 pass.set_bind_group(0, group0, &[]);
1356 pass.set_bind_group(
1357 1,
1358 &self.fragment_bind,
1359 &[slot as u32 * self.uniform_align],
1360 );
1361 on_quads = false;
1362 pass.draw(0..6, i..i + 1);
1363 }
1364 run_start = i + 1;
1365 }
1366 let end = self.instances.len() as u32;
1367 if end > run_start {
1368 if !on_quads {
1369 pass.set_pipeline(&self.pipeline);
1370 pass.set_bind_group(0, &self.bind_group, &[]);
1371 }
1372 pass.draw(0..6, run_start..end);
1373 }
1374 }
1375 }
1376 }
1377 self.gpu.queue().submit([encoder.finish()]);
1378 self.gpu.queue().present(frame);
1379 Ok(RenderReport { vsync_wait_ms })
1380 }
1381}
1382
1383fn draw_image_texture(draw: &kui_core::FragmentDraw) -> Option<usize> {
1386 match draw.image {
1387 kui_core::FragmentImage::Texture { index, .. } => Some(index as usize),
1388 _ => None,
1389 }
1390}
1391
1392#[derive(Clone, Copy, Debug, Default)]
1394pub struct RenderReport {
1395 pub vsync_wait_ms: f32,
1398}
1399
1400#[derive(Clone, Copy, Debug)]
1405pub enum RenderError {
1406 Reconfigure,
1409 Skip,
1412 Validation,
1416 DeviceLost,
1419}
1420
1421impl std::fmt::Display for RenderError {
1422 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1423 match self {
1424 Self::Reconfigure => write!(f, "surface outdated or lost; reconfigure"),
1425 Self::Skip => write!(f, "no frame available; skip"),
1426 Self::Validation => write!(f, "surface texture validation error"),
1427 Self::DeviceLost => write!(f, "device lost; reopen"),
1428 }
1429 }
1430}
1431
1432impl std::error::Error for RenderError {}
1433
1434fn create_atlas_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
1435 device.create_texture(&wgpu::TextureDescriptor {
1436 label: Some("kui.atlas"),
1437 size: wgpu::Extent3d {
1438 width: size,
1439 height: size,
1440 depth_or_array_layers: 1,
1441 },
1442 mip_level_count: 1,
1443 sample_count: 1,
1444 dimension: wgpu::TextureDimension::D2,
1445 format: wgpu::TextureFormat::Rgba8Unorm,
1446 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1447 view_formats: &[],
1448 })
1449}
1450
1451fn create_bind_group(
1454 device: &wgpu::Device,
1455 layout: &wgpu::BindGroupLayout,
1456 globals: &wgpu::Buffer,
1457 view: &wgpu::TextureView,
1458 samplers: &Samplers,
1459) -> wgpu::BindGroup {
1460 device.create_bind_group(&wgpu::BindGroupDescriptor {
1461 label: Some("kui"),
1462 layout,
1463 entries: &[
1464 wgpu::BindGroupEntry {
1465 binding: 0,
1466 resource: globals.as_entire_binding(),
1467 },
1468 wgpu::BindGroupEntry {
1469 binding: 1,
1470 resource: wgpu::BindingResource::TextureView(view),
1471 },
1472 wgpu::BindGroupEntry {
1473 binding: 2,
1474 resource: wgpu::BindingResource::Sampler(&samplers.linear),
1475 },
1476 wgpu::BindGroupEntry {
1477 binding: 3,
1478 resource: wgpu::BindingResource::Sampler(&samplers.nearest),
1479 },
1480 ],
1481 })
1482}
1483
1484fn create_instance_buffer(device: &wgpu::Device, cap: usize) -> wgpu::Buffer {
1485 device.create_buffer(&wgpu::BufferDescriptor {
1486 label: Some("kui.instances"),
1487 size: (cap * std::mem::size_of::<Instance>()) as u64,
1488 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1489 mapped_at_creation: false,
1490 })
1491}
1492
1493fn create_fragment_params_buffer(device: &wgpu::Device, cap: usize, align: u32) -> wgpu::Buffer {
1494 device.create_buffer(&wgpu::BufferDescriptor {
1495 label: Some("kui.fragment.params"),
1496 size: (cap.max(1) * align as usize) as u64,
1497 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1498 mapped_at_creation: false,
1499 })
1500}
1501
1502fn create_fragment_bind_group(
1503 device: &wgpu::Device,
1504 layout: &wgpu::BindGroupLayout,
1505 buf: &wgpu::Buffer,
1506) -> wgpu::BindGroup {
1507 device.create_bind_group(&wgpu::BindGroupDescriptor {
1508 label: Some("kui.fragment.params"),
1509 layout,
1510 entries: &[wgpu::BindGroupEntry {
1511 binding: 0,
1512 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1513 buffer: buf,
1514 offset: 0,
1515 size: std::num::NonZeroU64::new(std::mem::size_of::<FragmentParams>() as u64),
1516 }),
1517 }],
1518 })
1519}
1520
1521#[cfg(windows)]
1535pub fn report_faults() {
1536 use std::cell::Cell;
1537 use windows::Win32::Foundation::{
1538 EXCEPTION_ACCESS_VIOLATION, EXCEPTION_ILLEGAL_INSTRUCTION, EXCEPTION_IN_PAGE_ERROR,
1539 EXCEPTION_STACK_OVERFLOW, HMODULE, NTSTATUS, STATUS_FATAL_USER_CALLBACK_EXCEPTION,
1540 };
1541 use windows::Win32::Storage::FileSystem::WriteFile;
1542 use windows::Win32::System::Console::{GetStdHandle, STD_ERROR_HANDLE};
1543 use windows::Win32::System::Diagnostics::Debug::{
1544 AddVectoredExceptionHandler, EXCEPTION_POINTERS, EXCEPTION_RECORD,
1545 LPTOP_LEVEL_EXCEPTION_FILTER, SetUnhandledExceptionFilter,
1546 };
1547 use windows::Win32::System::LibraryLoader::{
1548 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
1549 GetModuleFileNameW, GetModuleHandleExW,
1550 };
1551
1552 const CONTINUE_SEARCH: i32 = 0;
1553 const FAULTS: [NTSTATUS; 4] = [
1556 EXCEPTION_ACCESS_VIOLATION,
1557 EXCEPTION_ILLEGAL_INSTRUCTION,
1558 EXCEPTION_IN_PAGE_ERROR,
1559 EXCEPTION_STACK_OVERFLOW,
1560 ];
1561 static PREVIOUS: std::sync::OnceLock<LPTOP_LEVEL_EXCEPTION_FILTER> = std::sync::OnceLock::new();
1564 thread_local! {
1565 static LAST: Cell<Option<(i32, usize)>> = const { Cell::new(None) };
1570 }
1571
1572 unsafe extern "system" fn remember(info: *mut EXCEPTION_POINTERS) -> i32 {
1574 if let Some(record) =
1576 (unsafe { info.as_ref() }).and_then(|i| unsafe { i.ExceptionRecord.as_ref() })
1577 && FAULTS.contains(&record.ExceptionCode)
1578 {
1579 let seen = (record.ExceptionCode.0, record.ExceptionAddress as usize);
1580 let _ = LAST.try_with(|l| l.set(Some(seen)));
1581 }
1582 CONTINUE_SEARCH
1583 }
1584
1585 fn nested(record: &EXCEPTION_RECORD) -> Option<(i32, usize)> {
1589 let mut at = record.ExceptionRecord;
1590 for _ in 0..4 {
1591 let inner = unsafe { at.as_ref() }?;
1593 if FAULTS.contains(&inner.ExceptionCode) {
1594 return Some((inner.ExceptionCode.0, inner.ExceptionAddress as usize));
1595 }
1596 at = inner.ExceptionRecord;
1597 }
1598 None
1599 }
1600
1601 fn say(code: i32, at: usize, escaped: Option<i32>) {
1606 let mut module = HMODULE::default();
1607 let mut name = [0u16; 260];
1608 let found = unsafe {
1610 GetModuleHandleExW(
1611 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
1612 | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
1613 windows::core::PCWSTR(at as *const u16),
1614 &mut module,
1615 )
1616 }
1617 .is_ok();
1618 let path = found.then(|| {
1619 let n = unsafe { GetModuleFileNameW(Some(module), &mut name) } as usize;
1621 &name[..n.min(name.len())]
1622 });
1623 let line = FaultLine::new(code as u32, at, path, escaped.map(|c| c as u32));
1624 if let Ok(err) = unsafe { GetStdHandle(STD_ERROR_HANDLE) } {
1626 let mut written = 0u32;
1627 let _ = unsafe { WriteFile(err, Some(line.bytes()), Some(&mut written), None) };
1628 }
1629 }
1630
1631 unsafe extern "system" fn filter(info: *const EXCEPTION_POINTERS) -> i32 {
1633 if let Some(record) =
1635 (unsafe { info.as_ref() }).and_then(|i| unsafe { i.ExceptionRecord.as_ref() })
1636 {
1637 let (code, at) = (record.ExceptionCode, record.ExceptionAddress as usize);
1638 let inner = if code == STATUS_FATAL_USER_CALLBACK_EXCEPTION {
1639 nested(record).or_else(|| LAST.try_with(Cell::get).ok().flatten())
1640 } else {
1641 None
1642 };
1643 match inner {
1644 Some((fault, fault_at)) => say(fault, fault_at, Some(code.0)),
1645 None => say(code.0, at, None),
1646 }
1647 }
1648 match PREVIOUS.get().copied().flatten() {
1649 Some(previous) => unsafe { previous(info) },
1652 None => CONTINUE_SEARCH,
1653 }
1654 }
1655
1656 PREVIOUS.get_or_init(|| {
1657 unsafe {
1660 AddVectoredExceptionHandler(0, Some(remember));
1661 SetUnhandledExceptionFilter(Some(filter))
1662 }
1663 });
1664}
1665
1666#[cfg(not(windows))]
1673pub fn report_faults() {}
1674
1675#[cfg_attr(not(windows), allow(dead_code))]
1683struct FaultLine {
1684 buf: [u8; 640],
1685 len: usize,
1686}
1687
1688#[cfg_attr(not(windows), allow(dead_code))]
1689impl FaultLine {
1690 fn new(code: u32, at: usize, module: Option<&[u16]>, escaped: Option<u32>) -> Self {
1694 use std::fmt::Write;
1695 let mut line = Self {
1696 buf: [0; 640],
1697 len: 0,
1698 };
1699 let _ = write!(line, "kui: fault {code:#010x} at {at:#x} in ");
1700 match module {
1701 Some(path) => {
1702 for c in char::decode_utf16(path.iter().copied()) {
1703 let _ = line.write_char(c.unwrap_or(char::REPLACEMENT_CHARACTER));
1704 }
1705 }
1706 None => {
1707 let _ = line.write_str("no module (jit or freed code)");
1708 }
1709 }
1710 if let Some(escaped) = escaped {
1711 let _ = write!(line, ", escaped from a window callback as {escaped:#010x}");
1712 }
1713 line.buf[line.len] = b'\n';
1715 line.len += 1;
1716 line
1717 }
1718
1719 fn bytes(&self) -> &[u8] {
1720 &self.buf[..self.len]
1721 }
1722}
1723
1724impl std::fmt::Write for FaultLine {
1725 fn write_str(&mut self, s: &str) -> std::fmt::Result {
1726 let room = self.buf.len() - 1 - self.len;
1728 let mut n = s.len().min(room);
1729 while !s.is_char_boundary(n) {
1730 n -= 1;
1731 }
1732 self.buf[self.len..self.len + n].copy_from_slice(&s.as_bytes()[..n]);
1733 self.len += n;
1734 Ok(())
1735 }
1736}
1737
1738#[cfg(test)]
1739mod tests {
1740 use super::*;
1741
1742 #[test]
1750 fn globals_layout_matches() {
1751 let fields = ["viewport", "atlas_size", "time", "scale", "_pad"];
1752 let of = |src: &str, name: &str| {
1753 let start = src
1754 .find(name)
1755 .unwrap_or_else(|| panic!("{name} is not declared in\n{src}"));
1756 let body = &src[start..];
1757 let end = body.find('}').expect("a closing brace");
1758 body[..end].to_string()
1759 };
1760 let quads = of(include_str!("shader.wgsl"), "struct Globals {");
1761 let frags = of(kui_core::fragment::PRELUDE, "struct KuiGlobals {");
1762 let read = |body: &str| -> Vec<String> {
1763 body.lines()
1764 .filter_map(|l| l.split_once(':'))
1765 .map(|(name, ty)| format!("{}: {}", name.trim(), ty.trim().trim_end_matches(',')))
1766 .collect()
1767 };
1768 let (a, b) = (read(&quads), read(&frags));
1769 assert_eq!(a, b, "shader.wgsl and the fragment prelude disagree");
1770 assert_eq!(
1771 a.len(),
1772 fields.len(),
1773 "a field was added to the globals without this test being told"
1774 );
1775 for (row, want) in a.iter().zip(fields) {
1776 assert!(row.starts_with(want), "expected {want}, got {row}");
1777 }
1778 assert_eq!(std::mem::size_of::<Globals>(), 32);
1781 }
1782
1783 #[test]
1787 fn fragment_params_layout_matches() {
1788 assert_eq!(std::mem::size_of::<FragmentParams>(), 80);
1789 assert_eq!(std::mem::offset_of!(FragmentParams, image), 64);
1790 let epilogue = kui_core::fragment::EPILOGUE;
1791 assert!(
1792 epilogue
1793 .contains("struct KuiFragmentParams { p: array<vec4<f32>, 4>, image: vec4<f32> };"),
1794 "the epilogue's params struct moved without this test being told"
1795 );
1796 }
1797
1798 #[test]
1802 fn prelude_bindings_match_group_zero() {
1803 let prelude = kui_core::fragment::PRELUDE;
1804 for line in [
1805 "@group(0) @binding(0) var<uniform> kui_globals: KuiGlobals;",
1806 "@group(0) @binding(1) var kui_atlas: texture_2d<f32>;",
1807 "@group(0) @binding(2) var kui_sampler: sampler;",
1808 "@group(0) @binding(3) var kui_sampler_nearest: sampler;",
1809 ] {
1810 assert!(prelude.contains(line), "prelude lacks `{line}`");
1811 }
1812 let quads = include_str!("shader.wgsl");
1813 for line in [
1814 "@group(0) @binding(1) var atlas_tex: texture_2d<f32>;",
1815 "@group(0) @binding(2) var atlas_smp: sampler;",
1816 "@group(0) @binding(3) var nearest_smp: sampler;",
1817 ] {
1818 assert!(quads.contains(line), "shader.wgsl lacks `{line}`");
1819 }
1820 }
1821
1822 #[test]
1825 fn shader_variants_validate() {
1826 use wgpu::naga::valid::{Capabilities, ValidationFlags, Validator};
1827 for dual in [false, true] {
1828 let src = preprocess_shader(include_str!("shader.wgsl"), dual);
1829 let module = wgpu::naga::front::wgsl::parse_str(&src)
1830 .unwrap_or_else(|e| panic!("dual={dual}: {}", e.emit_to_string(&src)));
1831 let caps = if dual {
1832 Capabilities::DUAL_SOURCE_BLENDING
1833 } else {
1834 Capabilities::empty()
1835 };
1836 Validator::new(ValidationFlags::all(), caps)
1837 .validate(&module)
1838 .unwrap_or_else(|e| panic!("dual={dual}: {e:?}"));
1839 }
1840 }
1841
1842 #[test]
1846 fn a_fault_line_names_the_code_the_address_and_the_module() {
1847 let path: Vec<u16> = r"C:\Windows\System32\nvoglv64.dll".encode_utf16().collect();
1848 let line = FaultLine::new(0xC000_0005, 0x7ff6_1234, Some(&path), None);
1849 assert_eq!(
1850 std::str::from_utf8(line.bytes()).unwrap(),
1851 "kui: fault 0xc0000005 at 0x7ff61234 in C:\\Windows\\System32\\nvoglv64.dll\n"
1852 );
1853 let line = FaultLine::new(0xC000_0005, 0x10, None, Some(0xC000_041D));
1854 assert_eq!(
1855 std::str::from_utf8(line.bytes()).unwrap(),
1856 "kui: fault 0xc0000005 at 0x10 in no module (jit or freed code), \
1857 escaped from a window callback as 0xc000041d\n"
1858 );
1859 let line = FaultLine::new(0xC000_001D, 0x20, Some(&[0x44, 0xD800, 0x45]), None);
1861 assert_eq!(
1862 std::str::from_utf8(line.bytes()).unwrap(),
1863 "kui: fault 0xc000001d at 0x20 in D\u{FFFD}E\n"
1864 );
1865 }
1866
1867 #[test]
1870 fn a_fault_line_too_long_is_cut_at_a_character() {
1871 let path: Vec<u16> = "é".repeat(1000).encode_utf16().collect();
1872 let line = FaultLine::new(0xC000_00FD, 0x30, Some(&path), Some(0xC000_041D));
1873 let text = std::str::from_utf8(line.bytes()).expect("cut at a character");
1874 assert!(text.ends_with("é\n"), "{text:?}");
1875 assert!(text.len() <= 640 && text.len() >= 638, "{}", text.len());
1876 assert!(text.starts_with("kui: fault 0xc00000fd at 0x30 in é"));
1877 }
1878}