1use crate::gpu::buffer::{push_fullscreen_quad, push_rect_quad, BlurUniforms, CompUniforms};
20use crate::gpu::buffer::{GradientVertex, Vertex};
21use crate::gpu::device::TARGET_FORMAT;
22use crate::gpu::pipeline::{BlurPipeline, CompositePipeline, SolidPipeline};
23use oxiui_core::paint::{DrawCommand, DrawList};
24use oxiui_core::{Color, UiError};
25use wgpu::util::DeviceExt;
26
27pub const MAX_BLUR_RADIUS: u32 = 64;
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36pub struct ShadowRenderStats {
37 pub render_passes: u32,
39 pub draw_calls: u32,
41}
42
43pub struct ShadowDesc {
47 pub shadow_rect: oxiui_core::geometry::Rect,
49 pub color: Color,
51 pub blur_radius: f32,
53}
54
55pub struct ShadowPipelines<'a> {
59 pub solid: &'a SolidPipeline,
61 pub blur: &'a BlurPipeline,
63 pub composite: &'a CompositePipeline,
65}
66
67pub struct ShadowGpuState<'a> {
71 pub device: &'a wgpu::Device,
73 pub queue: &'a wgpu::Queue,
75 pub target_view: &'a wgpu::TextureView,
78 pub resolve_target: Option<&'a wgpu::TextureView>,
82 pub globals_buffer: &'a wgpu::Buffer,
84 pub globals_bind_group: &'a wgpu::BindGroup,
86 pub viewport_w: u32,
88 pub viewport_h: u32,
90}
91
92struct ShadowContext<'a> {
96 gpu: &'a ShadowGpuState<'a>,
97 pipelines: &'a ShadowPipelines<'a>,
98 linear_sampler: &'a wgpu::Sampler,
99 fs_vb: &'a wgpu::Buffer,
100 fs_count: u32,
101 texel_w: f32,
102 texel_h: f32,
103}
104
105struct PingPong<'a> {
109 ping_tex: &'a wgpu::Texture,
110 ping_view: &'a wgpu::TextureView,
111 pong_tex: &'a wgpu::Texture,
112 pong_view: &'a wgpu::TextureView,
113}
114
115pub fn collect_shadows(list: &DrawList) -> Vec<ShadowDesc> {
120 list.iter()
121 .filter_map(|cmd| match cmd {
122 DrawCommand::BoxShadow {
123 rect,
124 offset,
125 blur_radius,
126 color,
127 } => {
128 let sr = oxiui_core::geometry::Rect::new(
129 rect.left() + offset.x,
130 rect.top() + offset.y,
131 rect.width(),
132 rect.height(),
133 );
134 Some(ShadowDesc {
135 shadow_rect: sr,
136 color: *color,
137 blur_radius: *blur_radius,
138 })
139 }
140 _ => None,
141 })
142 .collect()
143}
144
145pub fn render_shadows(
161 gpu: &ShadowGpuState<'_>,
162 pipelines: &ShadowPipelines<'_>,
163 descs: &[ShadowDesc],
164) -> Result<ShadowRenderStats, UiError> {
165 if descs.is_empty() {
166 return Ok(ShadowRenderStats::default());
167 }
168
169 let vp_w = gpu.viewport_w;
170 let vp_h = gpu.viewport_h;
171
172 let ping_tex = gpu.device.create_texture(&wgpu::TextureDescriptor {
174 label: Some("oxiui-render-wgpu shadow ping"),
175 size: wgpu::Extent3d {
176 width: vp_w,
177 height: vp_h,
178 depth_or_array_layers: 1,
179 },
180 mip_level_count: 1,
181 sample_count: 1,
182 dimension: wgpu::TextureDimension::D2,
183 format: TARGET_FORMAT,
184 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
185 view_formats: &[],
186 });
187 let pong_tex = gpu.device.create_texture(&wgpu::TextureDescriptor {
188 label: Some("oxiui-render-wgpu shadow pong"),
189 size: wgpu::Extent3d {
190 width: vp_w,
191 height: vp_h,
192 depth_or_array_layers: 1,
193 },
194 mip_level_count: 1,
195 sample_count: 1,
196 dimension: wgpu::TextureDimension::D2,
197 format: TARGET_FORMAT,
198 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
199 view_formats: &[],
200 });
201
202 let ping_view = ping_tex.create_view(&wgpu::TextureViewDescriptor::default());
203 let pong_view = pong_tex.create_view(&wgpu::TextureViewDescriptor::default());
204
205 let linear_sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
207 label: Some("oxiui-render-wgpu shadow sampler"),
208 address_mode_u: wgpu::AddressMode::ClampToEdge,
209 address_mode_v: wgpu::AddressMode::ClampToEdge,
210 address_mode_w: wgpu::AddressMode::ClampToEdge,
211 mag_filter: wgpu::FilterMode::Linear,
212 min_filter: wgpu::FilterMode::Linear,
213 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
214 lod_min_clamp: 0.0,
215 lod_max_clamp: 32.0,
216 compare: None,
217 anisotropy_clamp: 1,
218 border_color: None,
219 });
220
221 let mut fs_verts: Vec<GradientVertex> = Vec::new();
223 push_fullscreen_quad(&mut fs_verts, vp_w as f32, vp_h as f32);
224 let fs_vb = gpu
225 .device
226 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
227 label: Some("oxiui-render-wgpu shadow fullscreen quad"),
228 contents: bytemuck::cast_slice(&fs_verts),
229 usage: wgpu::BufferUsages::VERTEX,
230 });
231
232 let ctx = ShadowContext {
233 gpu,
234 pipelines,
235 linear_sampler: &linear_sampler,
236 fs_vb: &fs_vb,
237 fs_count: fs_verts.len() as u32,
238 texel_w: 1.0 / vp_w as f32,
239 texel_h: 1.0 / vp_h as f32,
240 };
241
242 let pp = PingPong {
243 ping_tex: &ping_tex,
244 ping_view: &ping_view,
245 pong_tex: &pong_tex,
246 pong_view: &pong_view,
247 };
248
249 let mut stats = ShadowRenderStats::default();
250 for shadow in descs {
251 let s = render_shadow_desc(&ctx, &pp, shadow)?;
252 stats.render_passes += s.render_passes;
253 stats.draw_calls += s.draw_calls;
254 }
255
256 Ok(stats)
257}
258
259fn render_shadow_desc(
265 ctx: &ShadowContext<'_>,
266 pp: &PingPong<'_>,
267 shadow: &ShadowDesc,
268) -> Result<ShadowRenderStats, UiError> {
269 let sigma = shadow.blur_radius.max(1.0) / 2.0;
270 let radius = ((3.0 * sigma).ceil() as u32).min(MAX_BLUR_RADIUS) as f32;
271
272 let mut stats = ShadowRenderStats::default();
273
274 render_mask(ctx, pp.ping_view, shadow)?;
277 stats.render_passes += 1;
278 stats.draw_calls += 1;
279
280 if shadow.blur_radius > 0.5 {
281 execute_blur_pass(
283 ctx,
284 pp.ping_tex,
285 pp.pong_view,
286 &BlurPassParams {
287 direction: [1.0, 0.0],
288 texel_size: [ctx.texel_w, ctx.texel_h],
289 radius,
290 sigma,
291 },
292 )?;
293 stats.render_passes += 1;
294 stats.draw_calls += 1;
295 execute_blur_pass(
297 ctx,
298 pp.pong_tex,
299 pp.ping_view,
300 &BlurPassParams {
301 direction: [0.0, 1.0],
302 texel_size: [ctx.texel_w, ctx.texel_h],
303 radius,
304 sigma,
305 },
306 )?;
307 stats.render_passes += 1;
308 stats.draw_calls += 1;
309 }
310
311 execute_composite_pass(ctx, pp.ping_tex, shadow.color)?;
313 stats.render_passes += 1;
314 stats.draw_calls += 1;
315
316 Ok(stats)
317}
318
319fn render_mask(
323 ctx: &ShadowContext<'_>,
324 ping_view: &wgpu::TextureView,
325 shadow: &ShadowDesc,
326) -> Result<(), UiError> {
327 let sr = &shadow.shadow_rect;
328 let mut rect_verts: Vec<Vertex> = Vec::new();
329 push_rect_quad(
330 &mut rect_verts,
331 sr.left(),
332 sr.top(),
333 sr.width(),
334 sr.height(),
335 Color(255, 255, 255, 255),
336 );
337
338 let rect_vb = ctx
339 .gpu
340 .device
341 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
342 label: Some("oxiui-render-wgpu shadow mask rect"),
343 contents: bytemuck::cast_slice(&rect_verts),
344 usage: wgpu::BufferUsages::VERTEX,
345 });
346
347 let mut encoder = ctx
348 .gpu
349 .device
350 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
351 label: Some("oxiui-render-wgpu shadow mask encoder"),
352 });
353
354 {
355 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
356 label: Some("oxiui-render-wgpu shadow mask pass"),
357 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
358 view: ping_view,
359 depth_slice: None,
360 resolve_target: None,
361 ops: wgpu::Operations {
362 load: wgpu::LoadOp::Clear(wgpu::Color {
363 r: 0.0,
364 g: 0.0,
365 b: 0.0,
366 a: 0.0,
367 }),
368 store: wgpu::StoreOp::Store,
369 },
370 })],
371 depth_stencil_attachment: None,
372 timestamp_writes: None,
373 occlusion_query_set: None,
374 multiview_mask: None,
375 });
376
377 pass.set_pipeline(&ctx.pipelines.solid.pipeline);
378 pass.set_bind_group(0, ctx.gpu.globals_bind_group, &[]);
379 pass.set_vertex_buffer(0, rect_vb.slice(..));
380 pass.set_scissor_rect(0, 0, ctx.gpu.viewport_w, ctx.gpu.viewport_h);
381 pass.draw(0..rect_verts.len() as u32, 0..1);
382 }
383
384 ctx.gpu.queue.submit(Some(encoder.finish()));
385 Ok(())
386}
387
388struct BlurPassParams {
392 direction: [f32; 2],
393 texel_size: [f32; 2],
394 radius: f32,
395 sigma: f32,
396}
397
398fn execute_blur_pass(
401 ctx: &ShadowContext<'_>,
402 src_tex: &wgpu::Texture,
403 dst_view: &wgpu::TextureView,
404 params: &BlurPassParams,
405) -> Result<(), UiError> {
406 let src_view = src_tex.create_view(&wgpu::TextureViewDescriptor::default());
407
408 let blur_uni = BlurUniforms {
409 direction: params.direction,
410 texel_size: params.texel_size,
411 radius: params.radius,
412 sigma: params.sigma,
413 _pad: [0.0; 2],
414 };
415
416 let blur_ub = ctx
417 .gpu
418 .device
419 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
420 label: Some("oxiui-render-wgpu blur uniform"),
421 contents: bytemuck::bytes_of(&blur_uni),
422 usage: wgpu::BufferUsages::UNIFORM,
423 });
424
425 let globals_bg = ctx
426 .gpu
427 .device
428 .create_bind_group(&wgpu::BindGroupDescriptor {
429 label: Some("oxiui-render-wgpu blur globals bg"),
430 layout: &ctx.pipelines.blur.globals_layout,
431 entries: &[wgpu::BindGroupEntry {
432 binding: 0,
433 resource: ctx.gpu.globals_buffer.as_entire_binding(),
434 }],
435 });
436
437 let src_bg = ctx
438 .gpu
439 .device
440 .create_bind_group(&wgpu::BindGroupDescriptor {
441 label: Some("oxiui-render-wgpu blur source bg"),
442 layout: &ctx.pipelines.blur.source_layout,
443 entries: &[
444 wgpu::BindGroupEntry {
445 binding: 0,
446 resource: wgpu::BindingResource::TextureView(&src_view),
447 },
448 wgpu::BindGroupEntry {
449 binding: 1,
450 resource: wgpu::BindingResource::Sampler(ctx.linear_sampler),
451 },
452 wgpu::BindGroupEntry {
453 binding: 2,
454 resource: blur_ub.as_entire_binding(),
455 },
456 ],
457 });
458
459 let mut encoder = ctx
460 .gpu
461 .device
462 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
463 label: Some("oxiui-render-wgpu blur encoder"),
464 });
465
466 {
467 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
468 label: Some("oxiui-render-wgpu blur pass"),
469 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
470 view: dst_view,
471 depth_slice: None,
472 resolve_target: None,
473 ops: wgpu::Operations {
474 load: wgpu::LoadOp::Clear(wgpu::Color {
475 r: 0.0,
476 g: 0.0,
477 b: 0.0,
478 a: 0.0,
479 }),
480 store: wgpu::StoreOp::Store,
481 },
482 })],
483 depth_stencil_attachment: None,
484 timestamp_writes: None,
485 occlusion_query_set: None,
486 multiview_mask: None,
487 });
488
489 pass.set_pipeline(&ctx.pipelines.blur.pipeline);
490 pass.set_bind_group(0, &globals_bg, &[]);
491 pass.set_bind_group(1, &src_bg, &[]);
492 pass.set_vertex_buffer(0, ctx.fs_vb.slice(..));
493 pass.set_scissor_rect(0, 0, ctx.gpu.viewport_w, ctx.gpu.viewport_h);
494 pass.draw(0..ctx.fs_count, 0..1);
495 }
496
497 ctx.gpu.queue.submit(Some(encoder.finish()));
498 Ok(())
499}
500
501fn execute_composite_pass(
505 ctx: &ShadowContext<'_>,
506 mask_tex: &wgpu::Texture,
507 shadow_color: Color,
508) -> Result<(), UiError> {
509 let mask_view = mask_tex.create_view(&wgpu::TextureViewDescriptor::default());
510
511 let tint = [
512 shadow_color.0 as f32 / 255.0,
513 shadow_color.1 as f32 / 255.0,
514 shadow_color.2 as f32 / 255.0,
515 shadow_color.3 as f32 / 255.0,
516 ];
517
518 let comp_uni = CompUniforms {
519 tint,
520 texel_size: [ctx.texel_w, ctx.texel_h],
521 _pad: [0.0; 2],
522 };
523
524 let comp_ub = ctx
525 .gpu
526 .device
527 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
528 label: Some("oxiui-render-wgpu composite uniform"),
529 contents: bytemuck::bytes_of(&comp_uni),
530 usage: wgpu::BufferUsages::UNIFORM,
531 });
532
533 let globals_bg = ctx
534 .gpu
535 .device
536 .create_bind_group(&wgpu::BindGroupDescriptor {
537 label: Some("oxiui-render-wgpu composite globals bg"),
538 layout: &ctx.pipelines.composite.globals_layout,
539 entries: &[wgpu::BindGroupEntry {
540 binding: 0,
541 resource: ctx.gpu.globals_buffer.as_entire_binding(),
542 }],
543 });
544
545 let src_bg = ctx
546 .gpu
547 .device
548 .create_bind_group(&wgpu::BindGroupDescriptor {
549 label: Some("oxiui-render-wgpu composite source bg"),
550 layout: &ctx.pipelines.composite.source_layout,
551 entries: &[
552 wgpu::BindGroupEntry {
553 binding: 0,
554 resource: wgpu::BindingResource::TextureView(&mask_view),
555 },
556 wgpu::BindGroupEntry {
557 binding: 1,
558 resource: wgpu::BindingResource::Sampler(ctx.linear_sampler),
559 },
560 wgpu::BindGroupEntry {
561 binding: 2,
562 resource: comp_ub.as_entire_binding(),
563 },
564 ],
565 });
566
567 let mut encoder = ctx
568 .gpu
569 .device
570 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
571 label: Some("oxiui-render-wgpu composite encoder"),
572 });
573
574 {
575 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
576 label: Some("oxiui-render-wgpu composite pass"),
577 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
578 view: ctx.gpu.target_view,
579 depth_slice: None,
580 resolve_target: ctx.gpu.resolve_target,
583 ops: wgpu::Operations {
584 load: wgpu::LoadOp::Load,
587 store: wgpu::StoreOp::Store,
588 },
589 })],
590 depth_stencil_attachment: None,
591 timestamp_writes: None,
592 occlusion_query_set: None,
593 multiview_mask: None,
594 });
595
596 pass.set_pipeline(&ctx.pipelines.composite.pipeline);
597 pass.set_bind_group(0, &globals_bg, &[]);
598 pass.set_bind_group(1, &src_bg, &[]);
599 pass.set_vertex_buffer(0, ctx.fs_vb.slice(..));
600 pass.set_scissor_rect(0, 0, ctx.gpu.viewport_w, ctx.gpu.viewport_h);
601 pass.draw(0..ctx.fs_count, 0..1);
602 }
603
604 ctx.gpu.queue.submit(Some(encoder.finish()));
605 Ok(())
606}