Skip to main content

flow_ngin/
context.rs

1use std::sync::Arc;
2
3use cgmath::num_traits::ToPrimitive;
4use wgpu::{ExperimentalFeatures, util::DeviceExt};
5use winit::{dpi::PhysicalPosition, window::Window};
6
7use crate::{
8    camera::{self, CameraResources, CameraUniform, Projection}, data_structures::texture, pick::PickId, pipelines::{
9        basic::mk_basic_pipeline,
10        gui::{mk_gui_pipeline, mk_screen_size_bind_group, mk_screen_size_bind_group_layout},
11        light::{LightResources, LightUniform, mk_light_pipeline},
12        pick::mk_pick_pipeline,
13        pick_gui::mk_gui_pick_pipeline,
14        terrain::mk_terrain_pipeline,
15        transparent::mk_transparent_pipeline,
16    }, render::Render
17};
18
19pub trait GPUResource<'a, 'pass> {
20    fn write_to_buffer(&mut self, queue: &wgpu::Queue, device: &wgpu::Device);
21    fn get_render(&'a self) -> Render<'a, 'pass>;
22}
23#[cfg(feature = "integration-tests")]
24impl<'a, 'pass> GPUResource<'a, 'pass> for Box<dyn GPUResource<'a, 'pass> + Send> {
25    fn write_to_buffer(&mut self, queue: &wgpu::Queue, device: &wgpu::Device) {
26        (**self).write_to_buffer(queue, device);
27    }
28
29    fn get_render(&'a self) -> Render<'a, 'pass> {
30        (**self).get_render()
31    }
32}
33#[cfg(feature = "integration-tests")]
34impl<'a, 'pass> From<&'a Box<dyn GPUResource<'a, 'pass>>> for Render<'a, 'pass> {
35    fn from(val: &'a Box<dyn GPUResource<'a, 'pass>>) -> Self {
36        val.get_render()
37    }
38}
39#[cfg(feature = "integration-tests")]
40impl<'a, 'pass> GPUResource<'a, 'pass> for Box<dyn GPUResource<'a, 'pass>> {
41    fn write_to_buffer(&mut self, queue: &wgpu::Queue, device: &wgpu::Device) {
42        (**self).write_to_buffer(queue, device);
43    }
44
45    fn get_render(&'a self) -> Render<'a, 'pass> {
46        (**self).get_render()
47    }
48}
49
50/// Anti-aliasing mode for the rendering pipeline.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum AntiAliasing {
53    None,
54    MSAA4x,
55}
56
57impl AntiAliasing {
58    pub fn sample_count(self) -> u32 {
59        match self {
60            AntiAliasing::None => 1,
61            AntiAliasing::MSAA4x => 4,
62        }
63    }
64}
65
66#[derive(Debug)]
67pub enum MouseButtonState {
68    Right,
69    Left,
70    None,
71}
72
73#[derive(Debug)]
74pub struct MouseState {
75    pub coords: PhysicalPosition<f64>,
76    pub pressed: MouseButtonState,
77    pub selection: Option<PickId>,
78}
79impl MouseState {
80    pub(crate) fn toggle(&mut self, pick_id: PickId) {
81        self.selection = self
82            .selection
83            .is_none_or(|id| id != pick_id)
84            .then_some(pick_id);
85    }
86}
87
88#[derive(Debug)]
89pub struct Pipelines {
90    pub light: wgpu::RenderPipeline,
91    pub basic: wgpu::RenderPipeline,
92    pub basic_cw: wgpu::RenderPipeline,
93    pub pick: wgpu::RenderPipeline,
94    pub gui: wgpu::RenderPipeline,
95    pub transparent: wgpu::RenderPipeline,
96    pub terrain: wgpu::RenderPipeline,
97    pub flat_pick: wgpu::RenderPipeline,
98}
99
100#[derive(Debug)]
101pub struct ScreenSizeResources {
102    pub buffer: wgpu::Buffer,
103    pub bind_group: wgpu::BindGroup,
104    pub bind_group_layout: wgpu::BindGroupLayout,
105}
106
107#[derive(Debug)]
108pub struct Context {
109    pub window: Arc<Window>,
110    pub(crate) depth_texture: texture::Texture,
111    pub(crate) msaa_view: Option<wgpu::TextureView>,
112    pub anti_aliasing: AntiAliasing,
113    pub tick_duration_millis: u64,
114    pub clear_colour: wgpu::Color,
115    pub surface: wgpu::Surface<'static>,
116    pub device: wgpu::Device,
117    pub queue: wgpu::Queue,
118    pub mouse: MouseState,
119    pub config: wgpu::SurfaceConfiguration,
120    pub camera: CameraResources,
121    pub projection: Projection,
122    pub light: LightResources,
123    pub pipelines: Pipelines,
124    pub screen_size: ScreenSizeResources,
125}
126impl Context {
127    pub(crate) async fn new(window: Arc<Window>) -> Result<Self, anyhow::Error> {
128        let size = window.inner_size();
129
130        // The instance is a handle to our GPU
131        // BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU
132        log::warn!("WGPU setup");
133        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
134            #[cfg(not(target_arch = "wasm32"))]
135            backends: wgpu::Backends::PRIMARY,
136            #[cfg(target_arch = "wasm32")]
137            backends: wgpu::Backends::GL,
138            ..Default::default()
139        });
140
141        let surface = instance.create_surface(window.clone())?;
142
143        let adapter = instance
144            .request_adapter(&wgpu::RequestAdapterOptions {
145                power_preference: wgpu::PowerPreference::default(),
146                compatible_surface: Some(&surface),
147                force_fallback_adapter: false,
148            })
149            .await?;
150        log::warn!("device and queue");
151        let (device, queue) = adapter
152            .request_device(&wgpu::DeviceDescriptor {
153                label: None,
154                required_features: wgpu::Features::empty(),
155                // WebGL doesn't support all of wgpu's features, so if
156                // we're building for the web we'll have to disable some.
157                required_limits: if cfg!(target_arch = "wasm32") {
158                    wgpu::Limits::downlevel_webgl2_defaults()
159                } else {
160                    wgpu::Limits::default()
161                },
162                memory_hints: Default::default(),
163                trace: wgpu::Trace::Off,
164                experimental_features: ExperimentalFeatures::disabled(),
165            })
166            .await?;
167
168        log::warn!("Surface");
169        let surface_caps = surface.get_capabilities(&adapter);
170        // Shader code in this tutorial assumes an Srgb surface texture. Using a different
171        // one will result all the colors comming out darker. If you want to support non
172        // Srgb surfaces, you'll need to account for that when drawing to the frame.
173        let surface_format = surface_caps
174            .formats
175            .iter()
176            .copied()
177            // Preferrably choose Rgba over Bgra because the image library can only handle Rgba natively (conversion is somewhat expensive in integration tests)
178            .find(|f| f.is_srgb() && format!("{:?}", f).starts_with('R'))
179            .or(surface_caps.formats.iter().copied().find(|f| f.is_srgb()))
180            .unwrap_or(surface_caps.formats[0]);
181        let config = wgpu::SurfaceConfiguration {
182            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
183            format: surface_format,
184            width: size.width,
185            height: size.height,
186            present_mode: surface_caps.present_modes[0],
187            alpha_mode: surface_caps.alpha_modes[0],
188            view_formats: vec![],
189            desired_maximum_frame_latency: 2,
190        };
191
192        // right/left, height, forward/backward - y axis rotation (turn head left/right) - x axis rotation (head up/down)
193        let camera = camera::Camera::new((0.0, 30.0, 20.0), cgmath::Deg(-90.0), cgmath::Deg(-60.0));
194        let projection =
195            camera::Projection::new(config.width, config.height, cgmath::Deg(45.0), 0.1, 500.0)?;
196        let camera_controller = camera::CameraController::new(10.0, 0.4);
197
198        let mut camera_uniform = CameraUniform::new();
199
200        camera_uniform.update_view_proj(&camera, &projection);
201
202        let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
203            label: Some("Camera Buffer"),
204            contents: bytemuck::cast_slice(&[camera_uniform]),
205            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
206        });
207
208        let camera_bind_group_layout =
209            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
210                entries: &[wgpu::BindGroupLayoutEntry {
211                    binding: 0,
212                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
213                    ty: wgpu::BindingType::Buffer {
214                        ty: wgpu::BufferBindingType::Uniform,
215                        has_dynamic_offset: false,
216                        min_binding_size: None,
217                    },
218                    count: None,
219                }],
220                label: Some("camera_bind_group_layout"),
221            });
222
223        let bind_group_layout = camera_bind_group_layout.clone();
224
225        let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
226            layout: &camera_bind_group_layout,
227            entries: &[wgpu::BindGroupEntry {
228                binding: 0,
229                resource: camera_buffer.as_entire_binding(),
230            }],
231            label: Some("camera_bind_group"),
232        });
233
234        let camera = CameraResources {
235            camera,
236            controller: camera_controller,
237            uniform: camera_uniform,
238            buffer: camera_buffer,
239            bind_group: camera_bind_group,
240            bind_group_layout,
241        };
242
243        let anti_aliasing = AntiAliasing::None;
244        let sample_count = anti_aliasing.sample_count();
245
246        let depth_texture = texture::Texture::create_depth_texture(
247            &device,
248            [config.width, config.height],
249            "depth_texture",
250            sample_count,
251        );
252
253        let msaa_view = if sample_count > 1 {
254            Some(texture::Texture::create_msaa_texture(&device, &config, sample_count))
255        } else {
256            None
257        };
258
259        let light_uniform = LightUniform {
260            position: [8.0, 80.0, 50.0],
261            _padding: 0,
262            // change when it's evening
263            color: [1.0, 1.0, 1.0],
264            _padding2: 0,
265        };
266
267        let light = LightResources::new(light_uniform, None, &device);
268
269        let clear_colour = wgpu::Color {
270            r: 0.1,
271            g: 0.2,
272            b: 0.2,
273            a: 1.0,
274        };
275
276        // Screen size uniform is shared by GUI and GUI pick pipelines
277        let screen_size_data = [config.width as f32, config.height as f32];
278        let screen_size_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
279            label: Some("Screen Size Uniform Buffer"),
280            contents: bytemuck::cast_slice(&screen_size_data),
281            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
282        });
283        let screen_size_bind_group_layout = mk_screen_size_bind_group_layout(&device);
284        let screen_size_bind_group = mk_screen_size_bind_group(&device, &screen_size_buffer, &screen_size_bind_group_layout);
285        let screen_size = ScreenSizeResources {
286            buffer: screen_size_buffer,
287            bind_group: screen_size_bind_group,
288            bind_group_layout: screen_size_bind_group_layout,
289        };
290
291        // Generate pipelines once so they can be reused without being initialized every frame
292        let light_pipeline = mk_light_pipeline(
293            &device,
294            &config,
295            &light.bind_group_layout,
296            &camera.bind_group_layout,
297            sample_count,
298        );
299        let basic_pipeline = mk_basic_pipeline(
300            &device,
301            &config,
302            wgpu::FrontFace::Ccw,
303            &light.bind_group_layout,
304            &camera.bind_group_layout,
305            sample_count,
306        );
307        let basic_cw_pipeline = mk_basic_pipeline(
308            &device,
309            &config,
310            wgpu::FrontFace::Cw,
311            &light.bind_group_layout,
312            &camera.bind_group_layout,
313            sample_count,
314        );
315        let pick_pipeline = mk_pick_pipeline(&device, &camera.bind_group_layout);
316        let gui_pipeline = mk_gui_pipeline(&device, &config, &screen_size.bind_group_layout, sample_count);
317        let gui_pick_pipeline = mk_gui_pick_pipeline(&device, &screen_size.bind_group_layout);
318        let transparent_pipeline = mk_transparent_pipeline(
319            &device,
320            &config,
321            &light.bind_group_layout,
322            &camera.bind_group_layout,
323            sample_count,
324        );
325        let terrain_pipeline = mk_terrain_pipeline(
326            &device,
327            &config,
328            &camera.bind_group_layout,
329            &light.bind_group_layout,
330            sample_count,
331        );
332        let pipelines = Pipelines {
333            basic: basic_pipeline,
334            basic_cw: basic_cw_pipeline,
335            gui: gui_pipeline,
336            flat_pick: gui_pick_pipeline,
337            light: light_pipeline,
338            pick: pick_pipeline,
339            transparent: transparent_pipeline,
340            terrain: terrain_pipeline,
341        };
342        let mouse = MouseState {
343            coords: (0.0, 0.0).into(),
344            pressed: MouseButtonState::None,
345            selection: None,
346        };
347        let tick_duration_millis = 500;
348
349        Ok(Self {
350            anti_aliasing,
351            camera,
352            clear_colour,
353            config,
354            depth_texture,
355            device,
356            light,
357            mouse,
358            msaa_view,
359            pipelines,
360            projection,
361            queue,
362            screen_size,
363            surface,
364            tick_duration_millis,
365            window,
366        })
367    }
368
369    /// Switch anti-aliasing mode at runtime, rebuilding all affected GPU state.
370    pub fn configure_anti_aliasing(&mut self, aa: AntiAliasing) {
371        self.anti_aliasing = aa;
372        let sample_count = aa.sample_count();
373
374        self.depth_texture = texture::Texture::create_depth_texture(
375            &self.device,
376            [self.config.width, self.config.height],
377            "depth_texture",
378            sample_count,
379        );
380
381        self.msaa_view = if sample_count > 1 {
382            Some(texture::Texture::create_msaa_texture(
383                &self.device,
384                &self.config,
385                sample_count,
386            ))
387        } else {
388            None
389        };
390
391        self.pipelines = Pipelines {
392            light: mk_light_pipeline(
393                &self.device,
394                &self.config,
395                &self.light.bind_group_layout,
396                &self.camera.bind_group_layout,
397                sample_count,
398            ),
399            basic: mk_basic_pipeline(
400                &self.device,
401                &self.config,
402                wgpu::FrontFace::Ccw,
403                &self.light.bind_group_layout,
404                &self.camera.bind_group_layout,
405                sample_count,
406            ),
407            basic_cw: mk_basic_pipeline(
408                &self.device,
409                &self.config,
410                wgpu::FrontFace::Cw,
411                &self.light.bind_group_layout,
412                &self.camera.bind_group_layout,
413                sample_count,
414            ),
415            pick: mk_pick_pipeline(&self.device, &self.camera.bind_group_layout),
416            gui: mk_gui_pipeline(
417                &self.device,
418                &self.config,
419                &self.screen_size.bind_group_layout,
420                sample_count,
421            ),
422            transparent: mk_transparent_pipeline(
423                &self.device,
424                &self.config,
425                &self.light.bind_group_layout,
426                &self.camera.bind_group_layout,
427                sample_count,
428            ),
429            terrain: mk_terrain_pipeline(
430                &self.device,
431                &self.config,
432                &self.camera.bind_group_layout,
433                &self.light.bind_group_layout,
434                sample_count,
435            ),
436            flat_pick: mk_gui_pick_pipeline(&self.device, &self.screen_size.bind_group_layout),
437        };
438    }
439
440    pub fn ray_to_floor(&self) -> Option<cgmath::Point2<f32>> {
441        self.camera.camera.cast_ray_from_mouse(
442            self.mouse.coords,
443            self.config.width.to_f32()?,
444            self.config.height.to_f32()?,
445            &self.projection,
446        ).intersect_with_floor()
447    }
448}
449
450#[derive(Clone)]
451pub struct InitContext {
452    pub queue: wgpu::Queue,
453    pub device: wgpu::Device,
454}
455impl From<&Context> for InitContext {
456    fn from(ctx: &Context) -> Self {
457        Self {
458            // Queue and Device can be cloned as they're internally handled as Arc
459            queue: ctx.queue.clone(),
460            device: ctx.device.clone(),
461        }
462    }
463}