1use 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
60pub struct UiRenderer {
64 pipeline: wgpu::RenderPipeline,
65 logo_pipeline: wgpu::RenderPipeline,
66 logo_sheets: std::collections::HashSet<Texture>,
68 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 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 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 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 pub fn clear_sheets(&mut self) {
240 self.textures.retain(|t, _| !matches!(t, Texture::Sheet(_)));
241 self.logo_sheets.clear();
242 }
243
244 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 pub fn set_time(&mut self, seconds: f32) {
285 self.time = seconds;
286 }
287
288 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}