Skip to main content

banished_gfx/
ui.rs

1//! Draws `banished_ui` quads: one alpha-blended pipeline, batches split by texture and scissor.
2
3use std::collections::HashMap;
4
5use banished_formats::texture::decode_rgba;
6use banished_formats::types;
7use banished_ui::{Quad, Texture, Ui};
8
9#[repr(C)]
10#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
11struct Vertex {
12    pos: [f32; 2],
13    uv: [f32; 2],
14    color: [u8; 4],
15}
16
17const SHADER: &str = r#"
18struct Screen { size: vec4<f32> };
19@group(0) @binding(0) var<uniform> screen: Screen;
20@group(1) @binding(0) var tex: texture_2d<f32>;
21@group(1) @binding(1) var samp: sampler;
22
23struct VsOut {
24    @builtin(position) clip: vec4<f32>,
25    @location(0) uv: vec2<f32>,
26    @location(1) color: vec4<f32>,
27};
28
29@vertex
30fn vs_main(@location(0) pos: vec2<f32>, @location(1) uv: vec2<f32>, @location(2) color: vec4<f32>) -> VsOut {
31    var out: VsOut;
32    out.clip = vec4<f32>(pos.x / screen.size.x * 2.0 - 1.0, 1.0 - pos.y / screen.size.y * 2.0, 0.0, 1.0);
33    out.uv = uv;
34    out.color = color;
35    return out;
36}
37
38@fragment
39fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
40    return textureSample(tex, samp, in.uv) * in.color;
41}
42
43// `Material\UI\Logo` (the Shining Rock splash), from the D3D9 pixel shader: red is the logo, green
44// where rays show, blue a ray pattern sampled after rotating by `userPosition.x` (seconds) about
45// texel (800, 421) of the 1600x900 image. Dark logo on white.
46@fragment
47fn fs_logo(in: VsOut) -> @location(0) vec4<f32> {
48    let base = textureSample(tex, samp, in.uv);
49    let a = screen.size.z;
50    let s = sin(a);
51    let c = cos(a);
52    let p = in.uv * vec2<f32>(1600.0, 900.0) + vec2<f32>(-800.0, -421.0);
53    let r = vec2<f32>(c * p.x + s * p.y + 800.0, -s * p.x + c * p.y + 421.0);
54    let rays = textureSample(tex, samp, r * vec2<f32>(0.000624999986, 0.00111111114));
55    let i = base.r + base.g * (1.0 - rays.b);
56    return vec4<f32>(vec3<f32>(1.0) - i * vec3<f32>(0.831372559, 0.792156875, 0.752941191), in.color.a);
57}
58"#;
59
60/// Draws the quads of `banished-ui` with wgpu: one alpha-blended pipeline (and one for the logo
61/// material), in batches split by texture and clip rectangle. It uploads each sprite sheet the
62/// first time a quad uses it.
63pub struct UiRenderer {
64    pipeline: wgpu::RenderPipeline,
65    logo_pipeline: wgpu::RenderPipeline,
66    /// Sheets drawn with the Logo material.
67    logo_sheets: std::collections::HashSet<Texture>,
68    /// `pc_userPosition.x` for material shaders (seconds since the loader loop started).
69    time: f32,
70    screen: wgpu::Buffer,
71    screen_group: wgpu::BindGroup,
72    texture_layout: wgpu::BindGroupLayout,
73    sampler: wgpu::Sampler,
74    textures: HashMap<Texture, wgpu::BindGroup>,
75    vertices: Option<wgpu::Buffer>,
76    /// Sprite sheets are sRGB when the target is, so colours come out unchanged either way.
77    sheet_format: wgpu::TextureFormat,
78}
79
80struct Batch {
81    texture: Texture,
82    clip: Option<[i32; 4]>,
83    range: std::ops::Range<u32>,
84}
85
86impl UiRenderer {
87    /// A renderer for a colour target of `format` and a depth target of `depth`.
88    pub fn new(
89        device: &wgpu::Device,
90        format: wgpu::TextureFormat,
91        depth: wgpu::TextureFormat,
92    ) -> Self {
93        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
94            label: Some("ui"),
95            source: wgpu::ShaderSource::Wgsl(SHADER.into()),
96        });
97        let uniform_entry = wgpu::BindGroupLayoutEntry {
98            binding: 0,
99            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
100            ty: wgpu::BindingType::Buffer {
101                ty: wgpu::BufferBindingType::Uniform,
102                has_dynamic_offset: false,
103                min_binding_size: None,
104            },
105            count: None,
106        };
107        let screen_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
108            label: Some("ui screen"),
109            entries: &[uniform_entry],
110        });
111        let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
112            label: Some("ui texture"),
113            entries: &[
114                wgpu::BindGroupLayoutEntry {
115                    binding: 0,
116                    visibility: wgpu::ShaderStages::FRAGMENT,
117                    ty: wgpu::BindingType::Texture {
118                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
119                        view_dimension: wgpu::TextureViewDimension::D2,
120                        multisampled: false,
121                    },
122                    count: None,
123                },
124                wgpu::BindGroupLayoutEntry {
125                    binding: 1,
126                    visibility: wgpu::ShaderStages::FRAGMENT,
127                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
128                    count: None,
129                },
130            ],
131        });
132        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
133            label: Some("ui"),
134            bind_group_layouts: &[Some(&screen_layout), Some(&texture_layout)],
135            immediate_size: 0,
136        });
137        let make = |entry: &str| {
138            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
139            label: Some("ui"),
140            layout: Some(&layout),
141            vertex: wgpu::VertexState {
142                module: &shader,
143                entry_point: Some("vs_main"),
144                compilation_options: Default::default(),
145                buffers: &[Some(wgpu::VertexBufferLayout {
146                    array_stride: std::mem::size_of::<Vertex>() as u64,
147                    step_mode: wgpu::VertexStepMode::Vertex,
148                    attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Unorm8x4],
149                })],
150            },
151            primitive: Default::default(),
152            depth_stencil: Some(wgpu::DepthStencilState {
153                format: depth,
154                depth_write_enabled: Some(false),
155                depth_compare: Some(wgpu::CompareFunction::Always),
156                stencil: Default::default(),
157                bias: Default::default(),
158            }),
159            multisample: Default::default(),
160            fragment: Some(wgpu::FragmentState {
161                module: &shader,
162                entry_point: Some(entry),
163                compilation_options: Default::default(),
164                targets: &[Some(wgpu::ColorTargetState {
165                    format,
166                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
167                    write_mask: wgpu::ColorWrites::ALL,
168                })],
169            }),
170            multiview_mask: None,
171            cache: None,
172        })
173        };
174        let pipeline = make("fs_main");
175        let logo_pipeline = make("fs_logo");
176        let screen = device.create_buffer(&wgpu::BufferDescriptor {
177            label: Some("ui screen"),
178            size: 16,
179            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
180            mapped_at_creation: false,
181        });
182        let screen_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
183            label: Some("ui screen"),
184            layout: &screen_layout,
185            entries: &[wgpu::BindGroupEntry {
186                binding: 0,
187                resource: screen.as_entire_binding(),
188            }],
189        });
190        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
191            mag_filter: wgpu::FilterMode::Linear,
192            min_filter: wgpu::FilterMode::Linear,
193            ..Default::default()
194        });
195        UiRenderer {
196            pipeline,
197            logo_pipeline,
198            logo_sheets: Default::default(),
199            time: 0.0,
200            screen,
201            screen_group,
202            texture_layout,
203            sampler,
204            textures: HashMap::new(),
205            vertices: None,
206            sheet_format: if format.is_srgb() {
207                wgpu::TextureFormat::Rgba8UnormSrgb
208            } else {
209                wgpu::TextureFormat::Rgba8Unorm
210            },
211        }
212    }
213
214    fn bind(&self, device: &wgpu::Device, view: &wgpu::TextureView) -> wgpu::BindGroup {
215        device.create_bind_group(&wgpu::BindGroupDescriptor {
216            label: Some("ui texture"),
217            layout: &self.texture_layout,
218            entries: &[
219                wgpu::BindGroupEntry {
220                    binding: 0,
221                    resource: wgpu::BindingResource::TextureView(view),
222                },
223                wgpu::BindGroupEntry {
224                    binding: 1,
225                    resource: wgpu::BindingResource::Sampler(&self.sampler),
226                },
227            ],
228        })
229    }
230
231    /// Register an application texture for `Texture::External(id)`.
232    pub fn set_external(&mut self, device: &wgpu::Device, id: u32, view: &wgpu::TextureView) {
233        let group = self.bind(device, view);
234        self.textures.insert(Texture::External(id), group);
235    }
236
237    /// Forget the sprite sheets' textures (the UI's resources were made again, so its sheet
238    /// numbers name other sheets now).
239    pub fn clear_sheets(&mut self) {
240        self.textures.retain(|t, _| !matches!(t, Texture::Sheet(_)));
241        self.logo_sheets.clear();
242    }
243
244    /// Upload any sprite-sheet images the UI references that are not on the GPU yet.
245    fn ensure_sheets(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, ui: &Ui) {
246        for (i, sheet) in ui.res.sheets.iter().enumerate() {
247            if self.textures.contains_key(&Texture::Sheet(i)) {
248                continue;
249            }
250            let made = sheet.pixels.as_deref().map(|(w, h, p)| (*w, *h, p.clone()));
251            let rgba = made.or_else(|| {
252                sheet.image.as_deref().and_then(|r| {
253                    let img = types::image_buffer(&ui.res.archive.read_ref(r).ok()?).ok()?;
254                    let data = decode_rgba(img.format, img.width, img.height, img.levels.first()?)?;
255                    Some((img.width, img.height, data))
256                })
257            });
258            let (w, h, data) = rgba.unwrap_or_else(|| {
259                log::warn!("sprite sheet {} has no readable image", sheet.reference);
260                (1, 1, vec![255; 4])
261            });
262            let view = crate::upload_rgba_as(
263                device,
264                queue,
265                &sheet.reference,
266                w,
267                h,
268                &[data],
269                self.sheet_format,
270            );
271            let group = self.bind(device, &view);
272            self.textures.insert(Texture::Sheet(i), group);
273            if sheet
274                .shader
275                .as_deref()
276                .is_some_and(|m| m.replace('/', "\\").contains("Material\\UI\\Logo"))
277            {
278                self.logo_sheets.insert(Texture::Sheet(i));
279            }
280        }
281    }
282
283    /// Time for animated UI materials (`pc_userPosition.x`).
284    pub fn set_time(&mut self, seconds: f32) {
285        self.time = seconds;
286    }
287
288    /// Draw `quads` (from [`Ui::draw`](banished_ui::Ui::draw)) into `pass`, on a target of `size`
289    /// pixels.
290    pub fn draw<'p>(
291        &'p mut self,
292        device: &wgpu::Device,
293        queue: &wgpu::Queue,
294        pass: &mut wgpu::RenderPass<'p>,
295        ui: &Ui,
296        quads: &[Quad],
297        size: (u32, u32),
298    ) {
299        self.ensure_sheets(device, queue, ui);
300        queue.write_buffer(
301            &self.screen,
302            0,
303            bytemuck::cast_slice(&[size.0 as f32, size.1 as f32, self.time, 0.0]),
304        );
305        let mut vertices = Vec::with_capacity(quads.len() * 6);
306        let mut batches: Vec<Batch> = Vec::new();
307        for q in quads {
308            let start = vertices.len() as u32;
309            let [x0, y0, x1, y1] = q.rect;
310            let [u0, v0, u1, v1] = q.uv;
311            let (sin, cos) = q.rotation.to_radians().sin_cos();
312            let (cx, cy) = ((x0 + x1) * 0.5, (y0 + y1) * 0.5);
313            let v = |x: f32, y: f32, u, v| {
314                let (dx, dy) = (x - cx, y - cy);
315                Vertex {
316                    pos: [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos],
317                    uv: [u, v],
318                    color: q.color,
319                }
320            };
321            vertices.extend_from_slice(&[
322                v(x0, y0, u0, v0),
323                v(x1, y0, u1, v0),
324                v(x1, y1, u1, v1),
325                v(x0, y0, u0, v0),
326                v(x1, y1, u1, v1),
327                v(x0, y1, u0, v1),
328            ]);
329            match batches.last_mut() {
330                Some(b) if b.texture == q.texture && b.clip == q.clip => b.range.end += 6,
331                _ => batches.push(Batch {
332                    texture: q.texture,
333                    clip: q.clip,
334                    range: start..start + 6,
335                }),
336            }
337        }
338        if vertices.is_empty() {
339            return;
340        }
341        let bytes: &[u8] = bytemuck::cast_slice(&vertices);
342        let needs_new = self
343            .vertices
344            .as_ref()
345            .is_none_or(|b| b.size() < bytes.len() as u64);
346        if needs_new {
347            self.vertices = Some(device.create_buffer(&wgpu::BufferDescriptor {
348                label: Some("ui vertices"),
349                size: (bytes.len() as u64).next_power_of_two(),
350                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
351                mapped_at_creation: false,
352            }));
353        }
354        let buffer = self.vertices.as_ref().unwrap();
355        queue.write_buffer(buffer, 0, bytes);
356        pass.set_viewport(0.0, 0.0, size.0 as f32, size.1 as f32, 0.0, 1.0);
357        pass.set_bind_group(0, &self.screen_group, &[]);
358        pass.set_vertex_buffer(0, buffer.slice(..));
359        let mut logo = None;
360        for b in batches {
361            let is_logo = self.logo_sheets.contains(&b.texture);
362            if logo != Some(is_logo) {
363                pass.set_pipeline(if is_logo {
364                    &self.logo_pipeline
365                } else {
366                    &self.pipeline
367                });
368                logo = Some(is_logo);
369            }
370            let Some(group) = self.textures.get(&b.texture) else {
371                continue;
372            };
373            let [x0, y0, x1, y1] = b.clip.unwrap_or([0, 0, size.0 as i32, size.1 as i32]);
374            let (x0, y0) = (
375                x0.clamp(0, size.0 as i32) as u32,
376                y0.clamp(0, size.1 as i32) as u32,
377            );
378            let (x1, y1) = (
379                x1.clamp(0, size.0 as i32) as u32,
380                y1.clamp(0, size.1 as i32) as u32,
381            );
382            if x1 <= x0 || y1 <= y0 {
383                continue;
384            }
385            pass.set_scissor_rect(x0, y0, x1 - x0, y1 - y0);
386            pass.set_bind_group(1, group, &[]);
387            pass.draw(b.range, 0..1);
388        }
389        pass.set_scissor_rect(0, 0, size.0, size.1);
390    }
391}