docs: write sections S1-S3 (intro, app skeleton, init chain)
This commit is contained in:
440
docs/01-rainbow-triangle.md
Normal file
440
docs/01-rainbow-triangle.md
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
# Building a Rainbow Triangle
|
||||||
|
|
||||||
|
## S1: What We're Building
|
||||||
|
|
||||||
|
We're creating a window containing a single triangle with smoothly blended colors:
|
||||||
|
|
||||||
|
Red at the bottom-left corner, blue at the bottom-right corner, and green at the
|
||||||
|
top vertex. The gradient between each pair of vertices is not computed by you —
|
||||||
|
it is interpolated automatically by the GPU rasterizer in hardware. You provide
|
||||||
|
three vertices, each carrying a position and a color. The rasterizer determines
|
||||||
|
every pixel covered by the triangle and computes the color for that pixel by
|
||||||
|
blending the three vertex colors proportionally to their distance. The result
|
||||||
|
is a smooth rainbow gradient across a single primitive. We do not need a texture,
|
||||||
|
a colormap, or a fragment shader with any branching — just three colored
|
||||||
|
vertices and the default linear interpolation the [rasterizer](concepts/GLOSSARY.md#rasterizer)
|
||||||
|
applies to every [varying](concepts/GLOSSARY.md#varying).
|
||||||
|
|
||||||
|
If you haven't read the [concept overview](concepts/graphics-pipeline.md), do so
|
||||||
|
now. [Coordinate systems](concepts/coordinate-systems.md) explains how the GPU
|
||||||
|
positions geometry. [Shader basics](concepts/shader-basics.md) covers the GPU
|
||||||
|
programs that drive rendering.
|
||||||
|
|
||||||
|
## S2: The winit Application and Event Loop
|
||||||
|
|
||||||
|
New concept: **event-driven windowing.** winit is the bridge between your Rust
|
||||||
|
code and the display server (X11 or Wayland on Linux). Think of it like `epoll`
|
||||||
|
or `kqueue` but for windows, input, and display lifecycle events instead of file
|
||||||
|
descriptors.
|
||||||
|
|
||||||
|
The entire program runs on the tokio async runtime — wgpu's [adapter](concepts/GLOSSARY.md#adapter)
|
||||||
|
queries and [device](concepts/GLOSSARY.md#device) creation are async, and the
|
||||||
|
runtime is the natural home for the main event loop.
|
||||||
|
|
||||||
|
### Architecture Overview
|
||||||
|
|
||||||
|
- **`main()` is `#[tokio::main] async fn`** — the entry point runs on the tokio
|
||||||
|
runtime, giving us access to tokio's task scheduler and I/O facilities.
|
||||||
|
- **`tokio::spawn_blocking`** — winit's `event_loop.run_app()` is synchronous
|
||||||
|
and owns the display server connection. Blocking the tokio runtime thread with
|
||||||
|
an indefinite sync call would starve other tasks. We offload the blocking event
|
||||||
|
loop to a dedicated thread, then await the join handle.
|
||||||
|
- **`Handle::block_on()` in `resumed()`** — wgpu initialization (adapter and
|
||||||
|
device queries) is async, but winit's `resumed()` handler is synchronous. We
|
||||||
|
bridge the two execution models exactly once at startup. This initial GPU
|
||||||
|
setup takes ~50ms of wall time.
|
||||||
|
- **`Arc<Window>`** — shared reference count to the window, needed because both
|
||||||
|
winit event handlers and wgpu [surface](concepts/GLOSSARY.md#surface) state
|
||||||
|
must hold a reference to the same window object across the event loop
|
||||||
|
boundary.
|
||||||
|
- **`ControlFlow::Poll`** — continuous redraw mode. winit fires
|
||||||
|
`RedrawRequested` as fast as the display server allows the window to be
|
||||||
|
presented, giving us a tight render loop without a separate timer or explicit
|
||||||
|
vsync setup. The display [present mode](concepts/GLOSSARY.md#present-mode)
|
||||||
|
controls the actual vsync behavior.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
Add these to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
wgpu = "29"
|
||||||
|
winit = "0.30"
|
||||||
|
tokio = { version = "1", features = ["rt", "macros"] }
|
||||||
|
bytemuck = { version = "1", features = ["derive"] }
|
||||||
|
log = "0.4"
|
||||||
|
simple_logger = "5"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `wgpu` — the GPU abstraction layer. Manages device lifecycles, shaders, buffers,
|
||||||
|
pipelines, and command encoding.
|
||||||
|
- `winit` — cross-platform window creation and event dispatch. Owns the display
|
||||||
|
server connection.
|
||||||
|
- `tokio` — async runtime for the main loop and all GPU queries.
|
||||||
|
- `bytemuck` — zero-copy casting between Rust structs and byte slices. Required
|
||||||
|
for uploading vertex data to GPU buffers without manual serialization.
|
||||||
|
- `log` / `simple_logger` — structured logging. wgpu and winit emit diagnostic
|
||||||
|
messages via `log` when misconfigurations or driver issues are detected.
|
||||||
|
|
||||||
|
### Complete Code
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::sync::Arc;
|
||||||
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::dpi::LogicalSize;
|
||||||
|
use winit::event::WindowEvent;
|
||||||
|
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||||
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
simple_logger::init_with_level(log::Level::Debug).unwrap();
|
||||||
|
|
||||||
|
let event_loop = EventLoop::new().unwrap();
|
||||||
|
let handle = tokio::Handle::current();
|
||||||
|
|
||||||
|
tokio::spawn_blocking(move || {
|
||||||
|
event_loop.run_app(&mut App {
|
||||||
|
handle,
|
||||||
|
window: None,
|
||||||
|
state: None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
handle: tokio::Handle,
|
||||||
|
window: Option<Arc<Window>>,
|
||||||
|
state: Option<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler<()> for App {
|
||||||
|
fn resumed(&mut self, event_loop_ctl: &ActiveEventLoop) {
|
||||||
|
let window = Arc::new(
|
||||||
|
event_loop_ctl
|
||||||
|
.create_window(
|
||||||
|
Window::default_attributes()
|
||||||
|
.with_inner_size(LogicalSize::new(800.0, 600.0))
|
||||||
|
.with_title("Rainbow Triangle"),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
event_loop_ctl.set_control_flow(ControlFlow::Poll);
|
||||||
|
self.window = Some(window.clone());
|
||||||
|
|
||||||
|
self.state = Some(
|
||||||
|
self.handle
|
||||||
|
.block_on(async {
|
||||||
|
State::new(window.clone()).await.expect("Failed to create wgpu State")
|
||||||
|
})
|
||||||
|
.expect("Failed to create wgpu State"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
event_loop_ctl: &ActiveEventLoop,
|
||||||
|
_window_id: WindowId,
|
||||||
|
event: WindowEvent,
|
||||||
|
) {
|
||||||
|
let Some(state) = self.state.as_mut() else { return };
|
||||||
|
let Some(window) = self.window.as_ref() else { return };
|
||||||
|
|
||||||
|
match event {
|
||||||
|
WindowEvent::Resized(size) => state.resize(window, size),
|
||||||
|
WindowEvent::CloseRequested { .. } => event_loop_ctl.exit(),
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
state.render();
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exiting(&mut self, event_loop_ctl: &ActiveEventLoop) {
|
||||||
|
event_loop_ctl.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why `spawn_blocking`:** The display server event loop must run to completion
|
||||||
|
and cannot be interrupted. If we ran `run_app()` on the tokio runtime thread,
|
||||||
|
no other async tasks could execute. By spawning it on a blocking thread, the
|
||||||
|
tokio runtime remains free for GPU queries, driver I/O, and future background
|
||||||
|
tasks.
|
||||||
|
|
||||||
|
**Why `Handle::block_on`:** wgpu's `request_adapter` and `request_device` query
|
||||||
|
the driver over async D-Bus/Wayland/Vulkan entrypoints. These futures must be
|
||||||
|
polled by a runtime executor. `block_on` attaches temporarily to the runtime
|
||||||
|
thread via its handle, polls the future to completion (~50ms), then returns the
|
||||||
|
result.
|
||||||
|
|
||||||
|
**Why `ControlFlow::Poll`:** winit supports `ControlFlow::Poll` (continuous
|
||||||
|
redraw) and `ControlFlow::Wait` (idle until next event). A graphics application
|
||||||
|
needs a steady render loop. `Poll` tells winit to keep firing `RedrawRequested`
|
||||||
|
events. We re-queue ourselves inside the handler via `window.request_redraw()`,
|
||||||
|
matching the wgpu swapchain presentation rhythm.
|
||||||
|
|
||||||
|
**Why `request_redraw()`:** After presenting a frame to the display, we ask
|
||||||
|
winit to schedule the next `RedrawRequested` frame. This creates an explicit
|
||||||
|
render loop: render → present → request redraw → render → repeat. The rate is
|
||||||
|
governed by the [swapchain](concepts/GLOSSARY.md#swapchain) [present mode](concepts/GLOSSARY.md#present-mode).
|
||||||
|
|
||||||
|
**Why `exiting()`:** This is the final lifecycle signal before the process
|
||||||
|
terminates. On some display servers, `CloseRequested` fires on the window but
|
||||||
|
the event loop must still drain. `exiting()` ensures we have one last clean
|
||||||
|
opportunity to flush the queue and release GPU resources before the process
|
||||||
|
exits.
|
||||||
|
|
||||||
|
## S3: Connecting to the GPU — The Init Chain
|
||||||
|
|
||||||
|
New concept: **5-layer GPU connection.** Each layer adds a capability:
|
||||||
|
|
||||||
|
1. **[Instance](concepts/GLOSSARY.md#instance)** — opens a connection to the
|
||||||
|
graphics driver. On Vulkan this loads the Vulkan loader and registers
|
||||||
|
instance-level extensions. On WebGL this picks the browser GPU context.
|
||||||
|
2. **[Surface](concepts/GLOSSARY.md#surface)** — binds the instance to a
|
||||||
|
specific window's swapchain. The surface is the wgpu representation of the
|
||||||
|
window's display buffer.
|
||||||
|
3. **[Adapter](concepts/GLOSSARY.md#adapter)** — selects the physical GPU
|
||||||
|
hardware. An adapter wraps the actual driver + silicon pair (e.g., Mesa RADV
|
||||||
|
on AMD, NVIDIA driver on NVIDIA silicon).
|
||||||
|
4. **[Device](concepts/GLOSSARY.md#device) + [Queue](concepts/GLOSSARY.md#queue)** — the
|
||||||
|
device owns all GPU resources (buffers, textures, shaders, pipelines). The
|
||||||
|
queue is the submission channel: you encode work into command buffers and
|
||||||
|
submit them to the queue.
|
||||||
|
5. **[SurfaceConfiguration](concepts/GLOSSARY.md#surface-configuration)** —
|
||||||
|
allocates the swapchain [framebuffers](concepts/GLOSSARY.md#framebuffer) for
|
||||||
|
this window at a specific resolution and pixel format.
|
||||||
|
|
||||||
|
### The State Struct
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct State {
|
||||||
|
surface: wgpu::Surface<'static>,
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
config: wgpu::SurfaceConfiguration,
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
vertex_buffer: wgpu::Buffer,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`surface`** — connects to the window's display buffer. The `'static` lifetime
|
||||||
|
is safe because `App` owns the window and lives for the entire lifetime of the
|
||||||
|
process. The surface mediates all [swapchain](concepts/GLOSSARY.md#swapchain)
|
||||||
|
operations.
|
||||||
|
- **`device`** — owns all GPU resources. Every buffer, texture, shader module,
|
||||||
|
and pipeline created in this guide is a child of the device. When the device
|
||||||
|
is dropped, all its children are freed.
|
||||||
|
- **`queue`** — the command submission channel. You encode a frame's worth of
|
||||||
|
work into a [command buffer](concepts/GLOSSARY.md#command-buffer), then submit
|
||||||
|
that buffer to the queue. The queue pushes work to the GPU hardware.
|
||||||
|
- **`config`** — holds the surface's current width, height, pixel format, and
|
||||||
|
[present mode](concepts/GLOSSARY.md#present-mode). When the window is resized,
|
||||||
|
we reconfigure the surface with updated dimensions.
|
||||||
|
- **`pipeline`** — the compiled [render pipeline](concepts/GLOSSARY.md#render-pipeline).
|
||||||
|
A render pipeline is an immutable configuration combining a shader, a vertex
|
||||||
|
buffer layout, a primitive topology, and a [color target](concepts/GLOSSARY.md#color-target)
|
||||||
|
setup. Switching pipelines mid-frame is expensive; most applications use a few
|
||||||
|
pipelines and change them between draw calls.
|
||||||
|
- **`vertex_buffer`** — GPU memory holding our vertex data. The GPU reads
|
||||||
|
position and color data directly from this buffer during the vertex shader
|
||||||
|
stage.
|
||||||
|
|
||||||
|
### Complete `State::new()` Implementation
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wgpu::Surface;
|
||||||
|
|
||||||
|
// --- Vertex type and data ---
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
struct Vertex {
|
||||||
|
position: [f32; 3],
|
||||||
|
color: [f32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
const VERTICES: &[Vertex] = &[
|
||||||
|
Vertex { position: [-0.5, -0.5, 0.0], color: [1.0, 0.0, 0.0] }, // red
|
||||||
|
Vertex { position: [ 0.5, -0.5, 0.0], color: [0.0, 0.0, 1.0] }, // blue
|
||||||
|
Vertex { position: [ 0.0, 0.5, 0.0], color: [0.0, 1.0, 0.0] }, // green
|
||||||
|
];
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
async fn new(window: Arc<Window>) -> Result<Self, String> {
|
||||||
|
// Step 1: Instance — connection to the graphics driver
|
||||||
|
let instance = wgpu::Instance::default();
|
||||||
|
|
||||||
|
// Step 2: Surface — binds our window to the GPU's swapchain
|
||||||
|
let surface = instance
|
||||||
|
.create_surface(window)
|
||||||
|
.map_err(|e| format!("Failed to create surface: {:?}", e))?;
|
||||||
|
|
||||||
|
// Step 3: Adapter — selects the physical GPU
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
compatible_surface: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok_or("No GPU adapter found. Ensure Vulkan drivers are installed.")?;
|
||||||
|
|
||||||
|
// Step 4: Device + Queue — resource owner + command submission
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&wgpu::DeviceDescriptor::default(), None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to request device: {:?}", e))?;
|
||||||
|
|
||||||
|
// Step 5: SurfaceConfiguration — allocates swapchain framebuffers
|
||||||
|
let size = window.inner_size();
|
||||||
|
let surface_caps = surface.get_capabilities(&adapter);
|
||||||
|
let format = surface_caps.formats.iter()
|
||||||
|
.find(|f| f.is_srgb())
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(surface_caps.formats[0]);
|
||||||
|
|
||||||
|
let config = wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
format,
|
||||||
|
width: size.width.max(1),
|
||||||
|
height: size.height.max(1),
|
||||||
|
present_mode: wgpu::PresentMode::Mailbox,
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
alpha_mode: surface_caps.alpha_modes[0],
|
||||||
|
view_formats: vec![format.add_srgb_suffix()],
|
||||||
|
};
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
// Step 6: Compile the shader module
|
||||||
|
let shader_module = device.create_shader_module(
|
||||||
|
wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("Rainbow Triangle Shader"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 7: Upload vertex data to GPU memory
|
||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
let vertex_buffer = device.create_buffer_init(
|
||||||
|
&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("Vertex Buffer"),
|
||||||
|
contents: bytemuck::cast_slice(VERTICES),
|
||||||
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 8: Create the render pipeline
|
||||||
|
let vertex_buffer_layout = wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<Vertex>() as u64,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: &[
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: 0,
|
||||||
|
format: wgpu::VertexFormat::F32x3,
|
||||||
|
shader_location: 0,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::size_of::<[f32; 3]>() as u64,
|
||||||
|
format: wgpu::VertexFormat::F32x3,
|
||||||
|
shader_location: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("Triangle Pipeline"),
|
||||||
|
layout: None,
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader_module,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[vertex_buffer_layout],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: Some(wgpu::Face::Back),
|
||||||
|
unclipped_depth: false,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 1,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader_module,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: config.format,
|
||||||
|
blend: None,
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
pipeline,
|
||||||
|
vertex_buffer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Init Steps Explained
|
||||||
|
|
||||||
|
**Step 1 — Instance:** `Instance::default()` opens a connection to the graphics
|
||||||
|
driver on the current platform. On Linux with Vulkan, this loads `libvulkan.so`
|
||||||
|
and creates a Vulkan `VkInstance`. On Windows, it loads `vulkan-1.dll`. The
|
||||||
|
instance is the foundational wgpu object — every other wgpu operation requires
|
||||||
|
it.
|
||||||
|
|
||||||
|
**Step 2 — Surface:** `instance.create_surface(window)` binds the wgpu instance
|
||||||
|
to the winit `Window`. This tells the GPU: "the pixels of *this* window will be
|
||||||
|
the output of my rendering." In Vulkan terms, this is the first half of creating
|
||||||
|
a `SwapchainKHR`. The surface must match the window platform type exactly (X11,
|
||||||
|
Wayland, Windows, macOS, etc.).
|
||||||
|
|
||||||
|
**Step 3 — Adapter:** `request_adapter()` queries available GPUs and returns the
|
||||||
|
best match for the given options. With
|
||||||
|
`PowerPreference::HighPerformance`, wgpu prefers a discrete GPU over an
|
||||||
|
integrated one on hybrid systems (e.g., NVIDIA + Intel Optimus). The
|
||||||
|
`compatible_surface: None` path works because our `Instance` was created without
|
||||||
|
a display handle; on Linux with Vulkan, the adapter selection remains correct
|
||||||
|
because the surface itself was created through a compatible instance.
|
||||||
|
|
||||||
|
**Step 4 — Device + Queue:** `request_device()` allocates the logical GPU
|
||||||
|
resource manager and its submission queue. The device tracks all GPU memory and
|
||||||
|
validates API calls. The queue is the submission endpoint — every rendered frame
|
||||||
|
becomes a [command buffer](concepts/GLOSSARY.md#command-buffer) that is submitted
|
||||||
|
to this queue. On Vulkan, the device corresponds to `VkDevice` and the queue
|
||||||
|
to a `VkQueue`.
|
||||||
|
|
||||||
|
**Step 5 — SurfaceConfiguration:** This allocates the
|
||||||
|
[swapchain](concepts/GLOSSARY.md#swapchain) [framebuffers](concepts/GLOSSARY.md#framebuffer).
|
||||||
|
We negotiate the pixel format with the driver (preferring an
|
||||||
|
[sRGB](concepts/GLOSSARY.md#srgb) format for correct color display), pick the
|
||||||
|
window dimensions (clamped to at least 1x1 to allow minimize-and-restore on some
|
||||||
|
platforms), and select the [present mode](concepts/GLOSSARY.md#present-mode).
|
||||||
|
`PresentMode::Mailbox` is a triple-buffered present mode that provides
|
||||||
|
consistent 60fps without tearing on most platforms.
|
||||||
|
`desired_maximum_frame_latency: 2` tells the swapchain to keep two frames of
|
||||||
|
back pressure, smoothing out frame time spikes.
|
||||||
|
|
||||||
|
Steps 6 through 8 — shader module compilation, vertex buffer upload, and render
|
||||||
|
pipeline assembly — will be explored in detail in the next sections.
|
||||||
Reference in New Issue
Block a user