diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/graphics.rs | 234 | ||||
| -rw-r--r-- | src/main.rs | 44 | ||||
| -rw-r--r-- | src/shader.wgsl | 10 |
3 files changed, 279 insertions, 9 deletions
diff --git a/src/graphics.rs b/src/graphics.rs new file mode 100644 index 0000000..d933c8b --- /dev/null +++ b/src/graphics.rs @@ -0,0 +1,234 @@ +use std::{borrow::Cow, sync::Arc}; +use wgpu::*; +use winit::{dpi::PhysicalSize, event_loop::OwnedDisplayHandle, window::Window}; + +pub enum RenderResult { + 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<OwnedDisplayHandle>, + window: Arc<Window>, + ) -> Result<Self, String> { + 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); + + 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) -> RenderResult { + match self.surface.get_current_texture() { + CurrentSurfaceTexture::Success(v) => RenderResult::Ok(v), + CurrentSurfaceTexture::Suboptimal(v) => { + // NOTE: 取得したテクスチャが生存した状態でサーフェスを再設定すると + // `PreviousOutputExists`エラーになるので、 + // 破棄した上で再設定し、描画をスキップする。 + drop(v); + self.configure_surface(window.inner_size()); + RenderResult::ShouldSkip + } + CurrentSurfaceTexture::Occluded => RenderResult::ShouldSkip, + CurrentSurfaceTexture::Timeout => RenderResult::ShouldSkip, + CurrentSurfaceTexture::Outdated => { + self.configure_surface(window.inner_size()); + RenderResult::ShouldSkip + } + CurrentSurfaceTexture::Lost => RenderResult::Lost, + 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); + + 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<u32>) { + 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) + } +} diff --git a/src/main.rs b/src/main.rs index 6303919..3c7f20d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,14 +12,14 @@ use winit::{ window::{Window, WindowId}, }; +mod graphics; + +use graphics::{RenderResult, Renderer}; + +#[derive(Default)] struct App { window: Option<Arc<Window>>, -} - -impl App { - fn new() -> Self { - Self { window: None } - } + renderer: Option<Renderer>, } impl ApplicationHandler for App { @@ -29,6 +29,7 @@ impl ApplicationHandler for App { event_loop.set_control_flow(ControlFlow::Poll); let attributes = Window::default_attributes() + .with_title("君は呪われている!") .with_inner_size(LogicalSize::new(1280, 720)) .with_fullscreen(None); @@ -37,7 +38,17 @@ impl ApplicationHandler for App { Err(e) => panic!("error: {e}"), }; + let renderer = pollster::block_on(Renderer::new( + Box::new(event_loop.owned_display_handle()), + window.clone(), + )); + let renderer = match renderer { + Ok(v) => v, + Err(e) => panic!("error: {e}"), + }; + self.window = Some(window); + self.renderer = Some(renderer); } fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { @@ -73,14 +84,29 @@ impl ApplicationHandler for App { fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { let start = Instant::now(); - let Some(_) = &self.window else { + let (Some(window), Some(renderer)) = (&self.window, &self.renderer) else { // NOTE: `window`が`Some`になる`resumed`メソッドで`ControlFlow::Poll`を指定しているため、 // ここで`ControlFlow::WaitUntil`を指定しても問題ない。 event_loop.set_control_flow(ControlFlow::WaitUntil(start + Duration::from_millis(16))); return; }; - sync_60fps(start); + // NOTE: Presentation modeがFIFOの場合はここでスレッドが停止する。 + let current_texture = match renderer.get_current_texture(window) { + RenderResult::Ok(v) => v, + RenderResult::ShouldSkip => { + event_loop + .set_control_flow(ControlFlow::WaitUntil(start + Duration::from_millis(16))); + return; + } + RenderResult::Lost => panic!("fatal: device or surface lost."), + }; + + renderer.render(window, current_texture); + + if !renderer.is_presentation_mode_fifo() { + sync_60fps(start); + } } } @@ -114,7 +140,7 @@ fn main() { Err(e) => panic!("error: {e}"), }; - match event_loop.run_app(&mut App::new()) { + match event_loop.run_app(&mut App::default()) { Ok(()) => {} Err(e) => panic!("error: {e}"), } diff --git a/src/shader.wgsl b/src/shader.wgsl new file mode 100644 index 0000000..9f158f1 --- /dev/null +++ b/src/shader.wgsl @@ -0,0 +1,10 @@ +@vertex +fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> { + let uv = vec2<f32>(f32((in_vertex_index << 1) & 2), f32(in_vertex_index & 2)) / 2.0; + return vec4<f32>(uv.x - 0.5, uv.y - 0.5, 0.0, 1.0); +} + +@fragment +fn fs_main() -> @location(0) vec4<f32> { + return vec4<f32>(1.0, 1.0, 1.0, 1.0); +} |
