Skip to content

Первый треугольник

rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::sync::Arc;

use tokio::runtime;
use tokio::runtime::Runtime;
use wgpu::{
    Backends, BlendComponent, BlendState, Color, ColorTargetState, ColorWrites,
    CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, ExperimentalFeatures,
    Features, FragmentState, FrontFace, Instance, InstanceDescriptor, Limits, LoadOp, MemoryHints,
    MultisampleState, Operations, PipelineCompilationOptions, PolygonMode, PowerPreference,
    PresentMode, PrimitiveState, PrimitiveTopology, Queue, RenderPassColorAttachment,
    RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, RequestAdapterOptions, StoreOp,
    Surface, SurfaceConfiguration, SurfaceError, TextureFormat, TextureUsages,
    TextureViewDescriptor, VertexState, include_wgsl,
};
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event::{ElementState, KeyEvent, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes, WindowId};

enum App {
    Loading,
    Ready {
        window: Arc<Window>,
        renderer: Box<Renderer>,
        need_to_resize_surface: bool,
    },
}

struct Renderer {
    device: Device,
    queue: Queue,
    surface: Surface<'static>,
    surface_config: SurfaceConfiguration,
    surface_format: TextureFormat,
    pipeline: Option<RenderPipeline>,
}

impl Renderer {
    fn new(window: Arc<Window>, runtime: Arc<Runtime>) -> Self {
        let mut physical_size = window.inner_size();
        physical_size.width = physical_size.width.max(1);
        physical_size.height = physical_size.height.max(1);

        let instance = Instance::new(&InstanceDescriptor {
            backends: Backends::PRIMARY,
            ..Default::default()
        });

        let surface = instance
            .create_surface(window)
            .expect("Failed to create surface");
        let adapter = runtime.block_on(async {
            instance
                .request_adapter(&RequestAdapterOptions {
                    power_preference: PowerPreference::default(),
                    force_fallback_adapter: false,
                    compatible_surface: Some(&surface),
                })
                .await
                .expect("Failed to request adapter")
        });

        let (device, queue) = runtime.block_on(async {
            adapter
                .request_device(&DeviceDescriptor {
                    label: Some("Main device"),
                    required_features: adapter.features() & Features::default(),
                    required_limits: Limits::default().using_resolution(adapter.limits()),
                    memory_hints: MemoryHints::Performance,
                    trace: Default::default(),
                    experimental_features: ExperimentalFeatures::disabled(),
                })
                .await
                .expect("Failed to request device")
        });

        let surface_capabilities = surface.get_capabilities(&adapter);

        let surface_format = surface_capabilities
            .formats
            .iter()
            .copied()
            .find(TextureFormat::is_srgb)
            .or_else(|| surface_capabilities.formats.first().copied())
            .expect("Failed to get surface format");

        let surface_config = SurfaceConfiguration {
            usage: TextureUsages::RENDER_ATTACHMENT,
            format: surface_format,
            width: physical_size.width,
            height: physical_size.height,
            present_mode: PresentMode::AutoNoVsync,
            desired_maximum_frame_latency: 2,
            alpha_mode: CompositeAlphaMode::Auto,
            view_formats: vec![],
        };

        let mut renderer = Self {
            device,
            queue,
            surface,
            surface_config,
            surface_format,
            pipeline: None,
        };

        renderer.resize_surface(physical_size);

        let shader_module = renderer
            .device
            .create_shader_module(include_wgsl!("shader.wgsl"));

        renderer.pipeline = Some(renderer.device.create_render_pipeline(
            &RenderPipelineDescriptor {
                label: Some("Main Render Pipeline"),
                layout: None,
                vertex: VertexState {
                    module: &shader_module,
                    entry_point: Some("vs_main"),
                    buffers: &[],
                    compilation_options: PipelineCompilationOptions::default(),
                },
                fragment: Some(FragmentState {
                    module: &shader_module,
                    entry_point: Some("fs_main"),
                    targets: &[Some(ColorTargetState {
                        format: renderer.surface_format,
                        blend: Some(BlendState {
                            color: BlendComponent::REPLACE,
                            alpha: BlendComponent::REPLACE,
                        }),
                        write_mask: ColorWrites::ALL,
                    })],
                    compilation_options: PipelineCompilationOptions::default(),
                }),
                primitive: PrimitiveState {
                    topology: PrimitiveTopology::TriangleList,
                    front_face: FrontFace::Ccw,
                    polygon_mode: PolygonMode::Fill,
                    ..Default::default()
                },
                depth_stencil: None,
                multisample: MultisampleState {
                    count: 1,
                    mask: !0,
                    alpha_to_coverage_enabled: false,
                },
                multiview: None,
                cache: None,
            },
        ));

        renderer
    }

    fn resize_surface(&self, size: PhysicalSize<u32>) {
        let width = size.width.max(1);
        let height = size.height.max(1);

        self.surface.configure(
            &self.device,
            &SurfaceConfiguration {
                width,
                height,
                ..self.surface_config.clone()
            },
        );
    }

    fn render(&mut self, window: Arc<Window>) {
        match self.surface.get_current_texture() {
            Ok(frame) => {
                let mut encoder = self
                    .device
                    .create_command_encoder(&CommandEncoderDescriptor { label: Some("Main command encoder") });

                let view = frame.texture.create_view(&TextureViewDescriptor::default());

                {
                    let mut rpass = encoder.begin_render_pass(&RenderPassDescriptor {
                        label: Some("Main Render Pass"),
                        color_attachments: &[Some(RenderPassColorAttachment {
                            view: &view,
                            resolve_target: None,
                            ops: Operations {
                                load: LoadOp::Clear(Color::GREEN),
                                store: StoreOp::Store,
                            },
                            depth_slice: None,
                        })],
                        depth_stencil_attachment: None,
                        timestamp_writes: None,
                        occlusion_query_set: None,
                    });

                    rpass.set_pipeline(self.pipeline.as_ref().expect("Failed to get pipeline"));
                    rpass.draw(0..3, 0..1);
                }

                self.queue.submit([encoder.finish()]);
                window.pre_present_notify();
                frame.present();
            }
            Err(error) => match error {
                SurfaceError::OutOfMemory => {
                    panic!("Surface error: {error}")
                }
                _ => {
                    window.request_redraw();
                }
            },
        };
    }
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if let Self::Loading = self {
            let runtime = Arc::new(
                runtime::Builder::new_current_thread()
                    .build()
                    .expect("Failed to create tokio runtime"),
            );

            let window_attributes = WindowAttributes::default()
                .with_title("WGPU Tutorial")
                .with_visible(false);

            let window = Arc::new(
                event_loop
                    .create_window(window_attributes)
                    .expect("Failed to create window"),
            );

            center_window(window.clone());

            event_loop.set_control_flow(ControlFlow::Wait);

            let renderer = Renderer::new(window.clone(), runtime.clone());

            *self = Self::Ready {
                window,
                renderer: Box::new(renderer),
                need_to_resize_surface: false,
            }
        }

        let Self::Ready {
            window, renderer, ..
        } = self
        else {
            return;
        };

        renderer.render(window.clone());

        window.set_visible(true);
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window_id: WindowId,
        event: WindowEvent,
    ) {
        let Self::Ready {
            window,
            renderer,
            need_to_resize_surface,
            ..
        } = self
        else {
            return;
        };

        match event {
            WindowEvent::RedrawRequested => {
                if *need_to_resize_surface {
                    let size = window.inner_size();

                    renderer.resize_surface(size);

                    *need_to_resize_surface = false;
                }

                renderer.render(window.clone());

                window.request_redraw();
            }
            WindowEvent::Resized(_) => {
                *need_to_resize_surface = true;
                window.request_redraw();
            }
            WindowEvent::CloseRequested => {
                event_loop.exit();
            }
            WindowEvent::KeyboardInput { event, .. } => handle_keyboard_input(event_loop, event),
            _ => {}
        }
    }
}

fn main() {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .init();

    let event_loop = EventLoop::new().expect("Failed to create event loop");

    let mut app = App::Loading;

    event_loop
        .run_app(&mut app)
        .expect("Failed to run event loop");
}

fn handle_keyboard_input(event_loop: &ActiveEventLoop, event: KeyEvent) {
    match (event.physical_key, event.state) {
        (PhysicalKey::Code(KeyCode::Escape), ElementState::Pressed) => {
            event_loop.exit();
        }
        _ => {}
    }
}

fn center_window(window: Arc<Window>) {
    if let Some(monitor) = window.current_monitor() {
        let screen_size = monitor.size();
        let window_size = window.outer_size();

        window.set_outer_position(winit::dpi::PhysicalPosition {
            x: screen_size.width.saturating_sub(window_size.width) as f64 / 2.0
                + monitor.position().x as f64,
            y: screen_size.height.saturating_sub(window_size.height) as f64 / 2.0
                + monitor.position().y as f64,
        });
    }
}

Получаем доступные возможности нашей поверхности, которые исходят из возможностей переданного адаптера

Далее нам нужно определиться с форматом кадров, которые мы будем выводить на поверхность, то есть в окно. На данный момент мы просто можем взять поддерживаемый список форматов поверхности, попытаться выбрать формат с поддержкой SRGB, и взять первый попавшийся, если такого не нашлось.

В реальном приложении вы, скорее всего, захотите организовать здесь более сложную логику выбора формата.

Теперь, получив подходящий формат поверхности, мы можем создать объект ее конфигурации SurfaceConfiguration:

  • usage - как мы будем использовать эту поверхность. На самом деле это параметр текстуры, поскольку отрисовку мы производим именно в нее. Но эта текстура находится в поверхности, поэтому данный параметр задается здесь. Выбираем RENDER_ATTACHMENT, то есть указываем, что данная текстура в поверхности будет использоваться как цель рендера, в нее будет выводиться полученная картинка.э
  • format - полученный нами формат поверхности
  • width - ширина поверхности. Передаем вычисленное ранее значение
  • height - высота поверхности. Передаем вычисленное ранее значение
  • present_mode - режим отображения кадров на поверхности. Режимов достаточно много, и разные устройства поддерживают разные режимы. Но wgpu сильно облегчает нам здесь жизнь, предоставляя опции AutoVsync и AutoNoVsync. Эти опции будут перебирать разные режимы отображения, пока не наткнутся на первый поддерживаемый нашим устройством. Различие между ними, соответственно, в наличии или отсутствии вертикальной синхронизации, то есть ограничения частоты вывода кадров частотой монитора.
  • desired_maximum_frame_latency - число кадров, которое может одновременно ожидать вывода на экран. Обычно указывается от 1 до 3, но может быть и больше. Не может быть меньше 1. Слишком низкое значение может привести к простою GPU, которой просто некуда будет отдавать готовый кадр, если поверхность не успевает их отрисовывать. По-умолчанию принято ставить 2, как оптимальное значение для большинства случаев.
  • alpha_mode - режим альфа-канала, отвечающего за прозрачность цветов. В данном случае выбираем автоматическое определение на основании поддерживаемого
  • view_formats - форматы представлений текстур, которые будут поддерживаться данной поверхностью для рендера. Собственный формат поверхности поддерживается всегда, поэтому нет нужды что-либо туда передавать.

Опубликовано под лицензией CC-BY-4.0