1use bytemuck::{Pod, Zeroable};
22use wgpu::util::DeviceExt;
23
24use crate::config::BorderParams;
25
26#[repr(C)]
32#[derive(Debug, Clone, Copy, Pod, Zeroable, PartialEq)]
33pub struct BorderUniform {
34 pub extents: [f32; 4],
37 pub color: [f32; 4],
40}
41
42impl Default for BorderUniform {
43 fn default() -> Self {
44 Self {
45 extents: [1.0, 1.0, 1.0, 1.0],
46 color: [0.0, 0.0, 0.0, 0.0],
47 }
48 }
49}
50
51pub struct BorderRenderer {
58 pipeline: wgpu::RenderPipeline,
59 bind_group_layout: wgpu::BindGroupLayout,
60 outer_uniform: wgpu::Buffer,
61 inner_uniform: wgpu::Buffer,
62 outer_bind_group: wgpu::BindGroup,
63 inner_bind_group: wgpu::BindGroup,
64 outer_visible: bool,
65 inner_visible: bool,
66}
67
68pub const BORDER_VERTICES_PER_RING: u32 = 24;
71
72impl BorderRenderer {
73 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
74 let shader = crate::pipeline_helpers::load_wgsl(
75 device,
76 "Border Shader",
77 include_str!("../shaders/border.wgsl"),
78 );
79
80 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
81 label: Some("Border Bind Group Layout"),
82 entries: &[wgpu::BindGroupLayoutEntry {
83 binding: 0,
84 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
85 ty: wgpu::BindingType::Buffer {
86 ty: wgpu::BufferBindingType::Uniform,
87 has_dynamic_offset: false,
88 min_binding_size: None,
89 },
90 count: None,
91 }],
92 });
93
94 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
95 label: Some("Border Pipeline Layout"),
96 bind_group_layouts: &[Some(&bind_group_layout)],
97 immediate_size: 0,
98 });
99
100 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
101 label: Some("Border Pipeline"),
102 layout: Some(&pipeline_layout),
103 vertex: wgpu::VertexState {
104 module: &shader,
105 entry_point: Some("vs_main"),
106 buffers: &[],
107 compilation_options: Default::default(),
108 },
109 fragment: Some(wgpu::FragmentState {
110 module: &shader,
111 entry_point: Some("fs_main"),
112 targets: &[Some(wgpu::ColorTargetState {
113 format,
114 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
115 write_mask: wgpu::ColorWrites::ALL,
116 })],
117 compilation_options: Default::default(),
118 }),
119 primitive: wgpu::PrimitiveState {
120 topology: wgpu::PrimitiveTopology::TriangleList,
121 ..Default::default()
122 },
123 depth_stencil: None,
124 multisample: wgpu::MultisampleState::default(),
125 multiview_mask: None,
126 cache: None,
127 });
128
129 let initial = BorderUniform::default();
130 let make_uniform = |label: &str| {
131 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
132 label: Some(label),
133 contents: bytemuck::bytes_of(&initial),
134 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
135 })
136 };
137 let outer_uniform = make_uniform("Border Outer Uniform");
138 let inner_uniform = make_uniform("Border Inner Uniform");
139
140 let make_bind = |label: &str, buffer: &wgpu::Buffer| {
141 device.create_bind_group(&wgpu::BindGroupDescriptor {
142 label: Some(label),
143 layout: &bind_group_layout,
144 entries: &[wgpu::BindGroupEntry {
145 binding: 0,
146 resource: buffer.as_entire_binding(),
147 }],
148 })
149 };
150 let outer_bind_group = make_bind("Border Outer Bind Group", &outer_uniform);
151 let inner_bind_group = make_bind("Border Inner Bind Group", &inner_uniform);
152
153 Self {
154 pipeline,
155 bind_group_layout,
156 outer_uniform,
157 inner_uniform,
158 outer_bind_group,
159 inner_bind_group,
160 outer_visible: false,
161 inner_visible: false,
162 }
163 }
164
165 pub fn update(&mut self, queue: &wgpu::Queue, params: BorderParams, aspect_ratio: f32) {
169 let (outer, outer_visible) = ring_uniform(
170 1.0,
171 1.0,
172 params.outer_size,
173 params.outer_color,
174 aspect_ratio,
175 );
176 let inner_outer_x = outer.extents[2];
179 let inner_outer_y = outer.extents[3];
180 let (inner, inner_visible) = ring_uniform(
181 inner_outer_x,
182 inner_outer_y,
183 params.inner_size,
184 params.inner_color,
185 aspect_ratio,
186 );
187
188 queue.write_buffer(&self.outer_uniform, 0, bytemuck::bytes_of(&outer));
189 queue.write_buffer(&self.inner_uniform, 0, bytemuck::bytes_of(&inner));
190 self.outer_visible = outer_visible;
191 self.inner_visible = inner_visible;
192 }
193
194 pub fn render(&self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
198 if !self.outer_visible && !self.inner_visible {
199 return;
200 }
201 let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
202 label: Some("Border Pass"),
203 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
204 view,
205 depth_slice: None,
206 resolve_target: None,
207 ops: wgpu::Operations {
208 load: wgpu::LoadOp::Load,
209 store: wgpu::StoreOp::Store,
210 },
211 })],
212 depth_stencil_attachment: None,
213 timestamp_writes: None,
214 occlusion_query_set: None,
215 multiview_mask: None,
216 });
217 rp.set_pipeline(&self.pipeline);
218 if self.outer_visible {
219 rp.set_bind_group(0, &self.outer_bind_group, &[]);
220 rp.draw(0..BORDER_VERTICES_PER_RING, 0..1);
221 }
222 if self.inner_visible {
223 rp.set_bind_group(0, &self.inner_bind_group, &[]);
224 rp.draw(0..BORDER_VERTICES_PER_RING, 0..1);
225 }
226 }
227
228 pub fn batch_count(&self) -> usize {
232 usize::from(self.outer_visible) + usize::from(self.inner_visible)
233 }
234
235 pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
238 &self.bind_group_layout
239 }
240}
241
242fn ring_uniform(
251 out_x: f32,
252 out_y: f32,
253 size: f32,
254 color: [f32; 4],
255 aspect_ratio: f32,
256) -> (BorderUniform, bool) {
257 let alpha = color[3];
258 let s = size.clamp(0.0, 0.5);
259 if alpha <= 0.0 || s <= 0.0 {
260 return (
261 BorderUniform {
262 extents: [out_x, out_y, out_x, out_y],
263 color,
264 },
265 false,
266 );
267 }
268
269 let aspect_safe = aspect_ratio.max(1e-3);
276 let t_y = 2.0 * s;
277 let t_x = if aspect_safe >= 1.0 {
278 t_y / aspect_safe
279 } else {
280 t_y
281 };
282 let t_y = if aspect_safe < 1.0 {
283 t_y * aspect_safe
284 } else {
285 t_y
286 };
287 let in_x = (out_x - t_x).max(0.0);
288 let in_y = (out_y - t_y).max(0.0);
289
290 (
291 BorderUniform {
292 extents: [out_x, out_y, in_x, in_y],
293 color,
294 },
295 true,
296 )
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn zero_alpha_is_invisible() {
305 let (_, visible) = ring_uniform(1.0, 1.0, 0.05, [1.0, 0.0, 0.0, 0.0], 1.0);
306 assert!(!visible);
307 }
308
309 #[test]
310 fn zero_size_is_invisible() {
311 let (_, visible) = ring_uniform(1.0, 1.0, 0.0, [1.0, 0.0, 0.0, 1.0], 1.0);
312 assert!(!visible);
313 }
314
315 #[test]
316 fn aspect_correction_keeps_ring_symmetric() {
317 let (sq, vis_sq) = ring_uniform(1.0, 1.0, 0.05, [1.0; 4], 1.0);
319 assert!(vis_sq);
320 let dx_sq = 1.0 - sq.extents[2];
321 let dy_sq = 1.0 - sq.extents[3];
322 assert!((dx_sq - dy_sq).abs() < 1e-5);
323
324 let aspect = 16.0_f32 / 9.0;
326 let (wide, vis_w) = ring_uniform(1.0, 1.0, 0.05, [1.0; 4], aspect);
327 assert!(vis_w);
328 let dx = 1.0 - wide.extents[2];
329 let dy = 1.0 - wide.extents[3];
330 assert!((dy - dy_sq).abs() < 1e-5);
333 assert!((dx - dy / aspect).abs() < 1e-5);
334 }
335
336 #[test]
337 fn portrait_aspect_swaps_axis() {
338 let aspect = 9.0_f32 / 16.0;
340 let (port, vis) = ring_uniform(1.0, 1.0, 0.05, [1.0; 4], aspect);
341 assert!(vis);
342 let dx = 1.0 - port.extents[2];
343 let dy = 1.0 - port.extents[3];
344 assert!((dx - 2.0 * 0.05).abs() < 1e-5);
346 assert!((dy - dx * aspect).abs() < 1e-5);
347 }
348
349 #[test]
350 fn inner_extent_clamps_at_zero() {
351 let (uni, _vis) = ring_uniform(1.0, 1.0, 0.5, [1.0; 4], 1.0);
354 assert!(uni.extents[2] >= 0.0);
355 assert!(uni.extents[3] >= 0.0);
356 }
357}