Первый треугольник
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(&mut self, size: PhysicalSize<u32>) {
let width = size.width.max(1);
let height = size.height.max(1);
self.surface_config.width = width;
self.surface_config.height = height;
self.surface.configure(
&self.device,
&self.surface_config,
);
}
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- форматы представлений текстур, которые будут поддерживаться данной поверхностью для рендера. Собственный формат поверхности поддерживается всегда, поэтому нет нужды что-либо туда передавать.