1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
use std::sync::Arc;
use winit::window::Window;
#[derive(Debug)]
pub enum FrameError {
Skip,
Outdated,
Lost,
Fatal,
}
/// The image a frame was drawn into.
enum Drawn {
/// A swapchain image, which has to be handed back to be shown.
Swapchain(wgpu::SurfaceTexture),
/// An offscreen image, which nobody is waiting to see.
Offscreen(wgpu::Texture),
}
/// An acquired image, ready to be rendered into.
pub struct Frame {
drawn: Drawn,
pub view: wgpu::TextureView,
}
impl Frame {
/// The image being drawn into, for copying it back out.
pub fn texture(&self) -> &wgpu::Texture {
match &self.drawn {
Drawn::Swapchain(frame) => &frame.texture,
Drawn::Offscreen(texture) => texture,
}
}
}
/// Where frames go: a window, or nowhere.
enum Target {
Window {
surface: wgpu::Surface<'static>,
config: wgpu::SurfaceConfiguration,
},
/// No window at all — for running the game where nothing can be seen and
/// nothing takes focus, with screenshots read back off the GPU.
Offscreen {
format: wgpu::TextureFormat,
width: u32,
height: u32,
/// The frame the GPU was last handed, which the next frame waits for
/// before it is let go. See [`GpuContext::end_frame`].
in_flight: Option<wgpu::SubmissionIndex>,
},
}
/// Bare device bootstrap shared by every renderer in this workspace.
pub struct GpuContext {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
target: Target,
/// Where the queue had got to at the last [`submit`](Self::submit).
submitted: Option<wgpu::SubmissionIndex>,
}
impl GpuContext {
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let surface = instance.create_surface(window).unwrap();
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
..Default::default()
}))
.expect("no suitable GPU adapter found");
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
}))
.expect("failed to create device");
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
// COPY_SRC so a frame can be read back for a screenshot.
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
format: surface_format,
width: size.width.max(1),
height: size.height.max(1),
present_mode: surface_caps.present_modes[0],
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
color_space: wgpu::SurfaceColorSpace::Auto,
};
surface.configure(&device, &config);
Self {
device,
queue,
target: Target::Window { surface, config },
submitted: None,
}
}
/// A context with no window: frames are drawn into a plain texture.
pub fn offscreen(width: u32, height: u32) -> Self {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: None,
..Default::default()
}))
.expect("no suitable GPU adapter found");
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
}))
.expect("failed to create device");
Self {
device,
queue,
target: Target::Offscreen {
// An sRGB target, to match what a surface would give.
format: wgpu::TextureFormat::Rgba8UnormSrgb,
width: width.max(1),
height: height.max(1),
in_flight: None,
},
submitted: None,
}
}
pub fn width(&self) -> u32 {
match &self.target {
Target::Window { config, .. } => config.width,
Target::Offscreen { width, .. } => *width,
}
}
pub fn height(&self) -> u32 {
match &self.target {
Target::Window { config, .. } => config.height,
Target::Offscreen { height, .. } => *height,
}
}
pub fn format(&self) -> wgpu::TextureFormat {
match &self.target {
Target::Window { config, .. } => config.format,
Target::Offscreen { format, .. } => *format,
}
}
pub fn resize(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
match &mut self.target {
Target::Window { surface, config } => {
config.width = width;
config.height = height;
surface.configure(&self.device, config);
}
Target::Offscreen {
width: w,
height: h,
..
} => {
*w = width;
*h = height;
}
}
}
pub fn begin_frame(&self) -> Result<Frame, FrameError> {
let Target::Window { surface, .. } = &self.target else {
// Offscreen: a fresh texture each frame, which keeps a screenshot
// from racing the frame after it.
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("offscreen frame"),
size: wgpu::Extent3d {
width: self.width(),
height: self.height(),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.format(),
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
return Ok(Frame {
drawn: Drawn::Offscreen(texture),
view,
});
};
let texture = match surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(texture) => texture,
wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
return Err(FrameError::Skip);
}
wgpu::CurrentSurfaceTexture::Outdated => return Err(FrameError::Outdated),
wgpu::CurrentSurfaceTexture::Lost => return Err(FrameError::Lost),
wgpu::CurrentSurfaceTexture::Validation => return Err(FrameError::Fatal),
};
let view = texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
Ok(Frame {
drawn: Drawn::Swapchain(texture),
view,
})
}
pub fn end_frame(&mut self, frame: Frame) {
match (frame.drawn, &mut self.target) {
(Drawn::Swapchain(texture), _) => self.queue.present(texture),
// Nothing to show it to -- but a swapchain does two things for a
// frame, and both have to be done by hand here.
//
// One is tidying up behind it: presenting is what normally lets
// wgpu let go of what a finished frame used, the texture it was
// drawn into and the staging behind every write_buffer. The
// other is holding the CPU back. A swapchain has only so many
// images, and asking for the next one waits until the GPU has
// given one up, so the CPU can never be more than a couple of
// frames ahead. Offscreen, every frame is a fresh texture that
// nothing ever refuses, and a CPU that is quicker than the GPU
// -- an integrated one, or a discrete one another program is
// using -- runs ahead of it without limit. Every frame it is
// ahead by is a frame's worth of textures, staging and command
// buffers kept alive until the GPU reaches it, and an offscreen
// game grew that way by hundreds of megabytes a second until wgpu
// answered with out of memory. Polling does the first job and not
// the second: it only tidies behind frames that have finished,
// and slows nothing down.
//
// So this waits for the frame before this one, which is what a
// two-image swapchain would have done. The wait is over at once
// when the GPU is keeping up, and when it is not, the loop
// settles at the GPU's pace with one frame in hand, which is
// what a window gets.
(Drawn::Offscreen(_), Target::Offscreen { in_flight, .. }) => {
if let Some(previous) = in_flight.take() {
let _ = self.device.poll(wgpu::PollType::Wait {
submission_index: Some(previous),
timeout: None,
});
}
*in_flight = self.submitted.take();
}
(Drawn::Offscreen(_), Target::Window { .. }) => {
unreachable!("an offscreen frame from a window")
}
}
}
pub fn create_encoder(&self, label: &str) -> wgpu::CommandEncoder {
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) })
}
pub fn submit(&mut self, encoder: wgpu::CommandEncoder) {
self.submitted = Some(self.queue.submit(std::iter::once(encoder.finish())));
}
}