use std::{borrow::Cow, sync::Arc}; use wgpu::*; use winit::{dpi::PhysicalSize, event_loop::OwnedDisplayHandle, window::Window}; pub enum AcquireSurfaceTextureResult { Ok(SurfaceTexture), ShouldSkip, Lost, } pub struct Renderer { device: Device, queue: Queue, surface: Surface<'static>, surface_format: TextureFormat, view_format: TextureFormat, present_mode: PresentMode, pipeline: RenderPipeline, } impl Renderer { pub async fn new( display: Box, window: Arc, ) -> Result { let size = window.inner_size(); let instance = Instance::new(InstanceDescriptor::new_with_display_handle(display)); let surface = instance.create_surface(window).map_err(|e| e.to_string())?; let adapter = instance .request_adapter(&RequestAdapterOptions { compatible_surface: Some(&surface), ..Default::default() }) .await .map_err(|e| e.to_string())?; let (device, queue) = adapter .request_device(&DeviceDescriptor::default()) .await .map_err(|e| e.to_string())?; let surface_cap = surface.get_capabilities(&adapter); if surface_cap.formats.is_empty() { return Err("no surface format found".to_string()); } let (surface_format, view_format) = decide_surface_and_view_format(&adapter, &surface_cap); // REVIEW: 高負荷なので素直にFIFOにして垂直同期で回す方が良いかもしれない。 let present_mode = if surface_cap.present_modes.contains(&PresentMode::Mailbox) { // 垂直同期を待つフレーム上書きモード PresentMode::Mailbox } else if surface_cap.present_modes.contains(&PresentMode::Immediate) { // 垂直同期を待たないフレーム上書きモード PresentMode::Immediate } else { // 垂直同期を待つフレーム保守モード // NOTE: 殆どすべての環境でサポートされていることを期待して良い。 // https://wgpu.rs/doc/wgpu/enum.PresentMode.html PresentMode::Fifo }; let shader = device.create_shader_module(ShaderModuleDescriptor { label: None, source: ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))), }); let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { label: None, bind_group_layouts: &[], immediate_size: 0, }); let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { label: None, layout: Some(&pipeline_layout), vertex: VertexState { module: &shader, entry_point: Some("vs_main"), buffers: &[], compilation_options: Default::default(), }, fragment: Some(FragmentState { module: &shader, entry_point: Some("fs_main"), compilation_options: Default::default(), targets: &[Some(view_format.into())], }), primitive: PrimitiveState { topology: PrimitiveTopology::TriangleStrip, cull_mode: None, ..Default::default() }, depth_stencil: None, multisample: MultisampleState::default(), multiview_mask: None, cache: None, }); let v = Self { device, queue, surface, surface_format, view_format, present_mode, pipeline, }; v.configure_surface(size); Ok(v) } pub fn get_current_texture(&self, window: &Window) -> AcquireSurfaceTextureResult { match self.surface.get_current_texture() { CurrentSurfaceTexture::Success(v) => AcquireSurfaceTextureResult::Ok(v), CurrentSurfaceTexture::Suboptimal(v) => { // NOTE: 取得したテクスチャが生存した状態でサーフェスを再設定すると // `PreviousOutputExists`エラーになるので、 // 破棄した上で再設定し、描画をスキップする。 drop(v); self.configure_surface(window.inner_size()); AcquireSurfaceTextureResult::ShouldSkip } CurrentSurfaceTexture::Occluded => AcquireSurfaceTextureResult::ShouldSkip, CurrentSurfaceTexture::Timeout => AcquireSurfaceTextureResult::ShouldSkip, CurrentSurfaceTexture::Outdated => { self.configure_surface(window.inner_size()); AcquireSurfaceTextureResult::ShouldSkip } CurrentSurfaceTexture::Lost => AcquireSurfaceTextureResult::Lost, // TODO: 一応対応する。 CurrentSurfaceTexture::Validation => unreachable!(), } } pub fn render(&self, window: &Window, current_texture: SurfaceTexture) { let surface_texture_view = current_texture.texture.create_view(&TextureViewDescriptor { format: Some(self.view_format), ..Default::default() }); let mut encoder = self.device.create_command_encoder(&Default::default()); let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor { label: None, color_attachments: &[Some(RenderPassColorAttachment { view: &surface_texture_view, depth_slice: None, resolve_target: None, ops: Operations { load: LoadOp::Clear(Color::BLACK), store: StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }); render_pass.set_pipeline(&self.pipeline); // TODO: 描画 drop(render_pass); self.queue.submit([encoder.finish()]); window.pre_present_notify(); self.queue.present(current_texture); } /// サーフェスを設定するメソッド /// /// * size - 現在のクライアントサイズ /// /// クライアントサイズのリサイズがあったらこのメソッドを呼び出す。 /// 幅・高さいずれかが`0`である場合は何もしない。 pub fn configure_surface(&self, size: PhysicalSize) { if size.width == 0 || size.height == 0 { return; } let surface_config = SurfaceConfiguration { usage: TextureUsages::RENDER_ATTACHMENT, format: self.surface_format, color_space: SurfaceColorSpace::Auto, view_formats: if self.surface_format != self.view_format { vec![self.view_format] } else { vec![] }, alpha_mode: CompositeAlphaMode::Auto, width: size.width, height: size.height, desired_maximum_frame_latency: 2, present_mode: self.present_mode, }; self.surface.configure(&self.device, &surface_config); } /// Presentation modeがFIFOか確認するメソッド /// /// FIFOである場合はsleep+spinではなく垂直同期でスレッドを待機した方が良い。 pub fn is_presentation_mode_fifo(&self) -> bool { self.present_mode == PresentMode::Fifo } } fn decide_surface_and_view_format( adapter: &Adapter, cap: &SurfaceCapabilities, ) -> (TextureFormat, TextureFormat) { // sRGBがサポートされているならsRGB if let Some(&v) = cap.formats.iter().find(|v| v.is_srgb()) { return (v, v); } let can_srgb_view = adapter .get_downlevel_capabilities() .flags .contains(DownlevelFlags::SURFACE_VIEW_FORMATS); let format = cap.formats[0]; // sRGBがサポートされていない・かつテクスチャとビューのフォーマットが一致しなくて良いなら // テクスチャを`formats[0]`・ビューをsRGB // NOTE: ビューのフォーマットに従って色変換が行われるので、 // テクスチャの実際のフォーマットが違うだけでsRGBと同様の結果になる。 if can_srgb_view { (format, format.add_srgb_suffix()) } // sRGBがサポートされていない・かつテクスチャとビューのフォーマットが一致する必要があるなら // テクスチャもビューも`formats[0]` // NOTE: 色変換が行われないので中間色が暗くなる。 // シェーダで自力補正してやれば解決するが、まあ余力があったらね。 else { (format, format) } }