unlit3d 0.1.0-dev.1

The high-level WebGPU rendering API over unlit_wgpu and unlit_ecs: ECS components, frame sources, input, UI and winit presentation.
Documentation
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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
//! Presenting a [`Renderer`] into a [winit](https://docs.rs/winit) window.
//!
//! A window presents through a swap chain, which the frame's own attachment
//! helpers do not cover: every frame hands out a new color image, and the
//! depth and multisample attachments have to match the window's size.
//! [`WindowSurface`] owns the surface, its configuration and those
//! attachments, keeps them registered in the frame's resource graph, and binds
//! each frame's image as the renderer's render target — so a frame loop is
//! acquire, render, present.
//!
//! The caller creates the `wgpu::Surface` itself, because the adapter has to
//! be requested with it as the compatible surface. That request is async —
//! on the web it resolves on the browser's task queue, so a frame loop must not
//! block on it — and so is the device's, which leaves both to the caller:
//!
//! ```no_run
//! use std::sync::Arc;
//!
//! use unlit3d::prelude::*;
//! use unlit3d::winit::WindowSurface;
//! use winit::window::Window;
//!
//! # fn setup(
//! #     world: &LocalWorld,
//! #     renderer: Entity,
//! #     instance: &wgpu::Instance,
//! #     adapter: &wgpu::Adapter,
//! #     window: Arc<Window>,
//! #     surface: wgpu::Surface<'static>,
//! # ) {
//! // The renderer the surface draws through, spawned once as a resource.
//! world
//!     .with_mut::<Renderer, _>(renderer, |r| {
//!         WindowSurface::new(world, r, instance, adapter, window, surface, 4)
//!     })
//!     .unwrap();
//! # }
//! ```
//!
//! and the frame loop then only has to render and present:
//!
//! ```no_run
//! # use unlit3d::prelude::*;
//! # use unlit3d::winit::WindowSurface;
//! # fn frame(
//! #     world: &LocalWorld,
//! #     queue: &wgpu::Queue,
//! #     renderer_entity: Entity,
//! #     window_surface: &mut WindowSurface,
//! # ) {
//! let Some(frame) = world
//!     .with_mut::<Renderer, _>(renderer_entity, |r| window_surface.acquire(world, r))
//!     .unwrap()
//! else {
//!     return;
//! };
//! world
//!     .with_mut::<Renderer, _>(renderer_entity, |r| r.render(world))
//!     .unwrap();
//! frame.present(queue);
//! # }
//! ```

use std::sync::Arc;

use unlit_ecs::LocalWorld;
use unlit_wgpu::render_attachments::default_depth_stencil_format;
use unlit_wgpu::resources::{Resource, ResourceId};

use crate::renderer::Renderer;

/// The device the frame's context was created with, cloned out of `world`.
///
/// # Panics
///
/// If the context's device resource is gone.
fn context_device(world: &LocalWorld, renderer: &Renderer) -> wgpu::Device {
    world
        .get::<wgpu::Device>(renderer.context().device)
        .expect("the context's device resource exists")
        .clone()
}

/// The frame's resource graph.
///
/// # Panics
///
/// If the context's graph resource is gone.
fn context_graph<'w>(
    world: &'w LocalWorld,
    renderer: &Renderer,
) -> impl core::ops::DerefMut<Target = unlit_wgpu::resources::ResourceGraph> + 'w {
    world
        .get_mut::<unlit_wgpu::resources::ResourceGraph>(renderer.context().graph)
        .expect("the context's resource graph exists")
}

/// A winit window's swap chain, presented through a [`Renderer`].
///
/// The surface is configured for the window's current size and the
/// depth-stencil — and, when multisampling, the multisample — attachments are
/// registered in the frame's resource graph. [`Self::acquire`] binds the
/// frame's color image as the renderer's render target; [`Self::resize`] keeps
/// everything in step with the window.
///
/// A platform can invalidate the render surface while the app is not in the
/// foreground, and Android does for as long as it is suspended. [`Self::release`]
/// is for that: it drops the swap chain and its attachments without touching the
/// device, the pipelines or anything else the frame draws from, so a scene keeps
/// its state and presents again through a surface built from the same window.
///
/// The sample count is fixed when the surface is created. The renderer
/// specializes every pipeline on the sample count the bound attachments
/// report, so the options a key starts from do not have to agree with it — but
/// the sample count does have to be one the device supports.
pub struct WindowSurface {
    instance: wgpu::Instance,
    window: Arc<::winit::window::Window>,
    surface: wgpu::Surface<'static>,
    config: wgpu::SurfaceConfiguration,
    sample_count: u32,
    /// The format the frame's color attachment is viewed as.
    ///
    /// Usually the swap chain's own format; an sRGB view of it when the
    /// surface offers no sRGB format of its own.
    color_format: wgpu::TextureFormat,
    /// The depth-stencil view, in the frame's graph.
    depth_view: ResourceId,
    /// The multisample view, in the frame's graph; `None` when the frames
    /// are not multisampled.
    msaa_view: Option<ResourceId>,
    /// The color view of the most recently acquired frame, in the frame's
    /// graph. One id is kept for the surface's whole life; `None` until the
    /// first frame is acquired.
    color_view: Option<ResourceId>,
}

impl WindowSurface {
    /// Configure `surface` for `window` and allocate the attachments the
    /// renderer draws with.
    ///
    /// `surface` must have been created from `window`, and `adapter` must be
    /// able to present to it — the adapter the renderer's device was requested
    /// from. `sample_count` is the number of samples every frame is rendered
    /// with; `1` disables multisampling.
    ///
    /// The surface format is the first sRGB format the surface supports. A
    /// surface that supports none — the web's canvas does not — is configured
    /// with a non-sRGB format instead and its frames are viewed as sRGB, which
    /// encodes them on write in exactly the same way. An sRGB attachment is
    /// what lets the built-in unlit shader's colors reach the display
    /// unchanged.
    ///
    /// # Panics
    ///
    /// If `adapter` cannot present to `surface`.
    pub fn new(
        world: &LocalWorld,
        renderer: &mut Renderer,
        instance: &wgpu::Instance,
        adapter: &wgpu::Adapter,
        window: Arc<::winit::window::Window>,
        surface: wgpu::Surface<'static>,
        sample_count: u32,
    ) -> Self {
        let size = window.inner_size();
        let config = surface_config(&surface, adapter, size.width, size.height);
        let color_format = color_format(&config);
        let device = context_device(world, renderer);
        surface.configure(&device, &config);

        let (depth, msaa) = create_attachments(
            &device,
            color_format,
            config.width,
            config.height,
            sample_count,
        );
        let mut graph = context_graph(world, renderer);
        let depth_view = graph
            .insert_strong(view_resource(&depth), &[])
            .expect("a texture view has no dependencies");
        let msaa_view = msaa.map(|texture| {
            graph
                .insert_strong(view_resource(&texture), &[])
                .expect("a texture view has no dependencies")
        });
        drop(graph);

        Self {
            instance: instance.clone(),
            window,
            surface,
            config,
            sample_count,
            color_format,
            depth_view,
            msaa_view,
            color_view: None,
        }
    }

    /// The size the surface is configured for, in physical pixels.
    pub fn size(&self) -> (u32, u32) {
        (self.config.width, self.config.height)
    }

    /// The format the swap chain presents in.
    ///
    /// A surface that offers no sRGB format of its own still presents as one:
    /// the frames are viewed as [`Self::color_format`], which differs from
    /// this.
    pub fn format(&self) -> wgpu::TextureFormat {
        self.config.format
    }

    /// The format the frame's color attachment is viewed as — the format the
    /// render target's pipelines are specialized for.
    pub fn color_format(&self) -> wgpu::TextureFormat {
        self.color_format
    }

    /// The window the surface presents into.
    ///
    /// The surface owns a handle to it, so a caller that needs the window —
    /// to request a redraw, or to read its size — can borrow it back here.
    pub fn window(&self) -> &Arc<::winit::window::Window> {
        &self.window
    }

    /// Reconfigure the surface and rebuild the attachments for a new size.
    ///
    /// A zero width or height — a minimized window — is ignored: a surface
    /// cannot be configured with a zero dimension, and there is nothing to
    /// draw.
    pub fn resize(&mut self, world: &LocalWorld, renderer: &mut Renderer, width: u32, height: u32) {
        if width == 0 || height == 0 {
            return;
        }
        if (width, height) == (self.config.width, self.config.height) {
            return;
        }
        self.config.width = width;
        self.config.height = height;
        let device = context_device(world, renderer);
        self.surface.configure(&device, &self.config);

        let (depth, msaa) =
            create_attachments(&device, self.color_format, width, height, self.sample_count);
        let mut graph = context_graph(world, renderer);
        graph
            .replace(self.depth_view, view_resource(&depth))
            .expect("the depth view is in the graph");
        if let (Some(id), Some(texture)) = (self.msaa_view, msaa) {
            graph
                .replace(id, view_resource(&texture))
                .expect("the multisample view is in the graph");
        }
    }

    /// Release the swap chain.
    ///
    /// Drops the surface and the attachments it registered in the frame's
    /// graph, and unsets the renderer's render target, so nothing is left
    /// naming a swap chain that is gone. The device, the pipelines and
    /// everything else the scene draws from are untouched: the caller's own
    /// window presents the same scene again through a surface built from it.
    ///
    /// This is what a suspension needs. Android invalidates the native surface
    /// for as long as the app is not in the foreground, which outlives the swap
    /// chain but not the window, the scene or the GPU context.
    pub fn release(self, world: &LocalWorld, renderer: &mut Renderer) {
        // The swap chain's image is the bound render target, so it is unset
        // before its view goes: an id left naming a removed view is not merely
        // dangling, because the graph recycles its slot — the renderer would
        // then either panic on a missing view or, worse, draw into whatever
        // unrelated texture took the slot.
        renderer.unset_render_target(world);

        let mut graph = context_graph(world, renderer);
        if let Some(id) = self.color_view {
            graph.remove_drop(id);
        }
        graph.remove_drop(self.depth_view);
        if let Some(id) = self.msaa_view {
            graph.remove_drop(id);
        }
    }

    /// Acquire the next frame and bind it as the renderer's render target.
    ///
    /// Returns `None` when the frame should be skipped — the window is
    /// occluded or minimized, or the swap chain had to be reconfigured. The
    /// renderer must not be asked to render in that case.
    ///
    /// The frame's image replaces the previous one in the resource graph, so
    /// at most the frame just presented is still referenced — never a longer
    /// history of swap-chain images.
    pub fn acquire(&mut self, world: &LocalWorld, renderer: &mut Renderer) -> Option<Frame> {
        let device = context_device(world, renderer);
        let surface_texture = match self.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(texture)
            | wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
            wgpu::CurrentSurfaceTexture::Outdated => {
                self.surface.configure(&device, &self.config);
                return None;
            }
            wgpu::CurrentSurfaceTexture::Lost => {
                self.recreate_surface(&device);
                return None;
            }
            wgpu::CurrentSurfaceTexture::Timeout
            | wgpu::CurrentSurfaceTexture::Occluded
            | wgpu::CurrentSurfaceTexture::Validation => return None,
        };

        // The color view keeps one id for the surface's whole life: replacing
        // the resource swaps in the new frame's view and drops the previous
        // one, and a skipped frame leaves the id — and the target bound to it —
        // untouched.
        let view = color_view_resource(&surface_texture.texture, self.color_format);
        let color = match self.color_view {
            Some(id) => {
                context_graph(world, renderer)
                    .replace(id, view)
                    .expect("the color view is in the graph");
                id
            }
            None => context_graph(world, renderer)
                .insert_strong(view, &[])
                .expect("a texture view has no dependencies"),
        };
        self.color_view = Some(color);
        renderer.set_render_target(world, Some(color), Some(self.depth_view), self.msaa_view);
        Some(Frame { surface_texture })
    }

    /// Rebuild the surface from the window after the swap chain was lost.
    ///
    /// A surface that cannot be rebuilt — the window is gone — leaves the
    /// current one in place, and the next acquire retries.
    fn recreate_surface(&mut self, device: &wgpu::Device) {
        if let Ok(surface) = self.instance.create_surface(self.window.clone()) {
            self.surface = surface;
            self.surface.configure(device, &self.config);
        }
    }
}

/// A swap-chain image acquired for one frame.
pub struct Frame {
    surface_texture: wgpu::SurfaceTexture,
}

impl Frame {
    /// The image this frame draws into.
    pub fn texture(&self) -> &wgpu::Texture {
        &self.surface_texture.texture
    }

    /// Present the frame.
    pub fn present(self, queue: &wgpu::Queue) {
        queue.present(self.surface_texture);
    }
}

/// The configuration to present `surface` with on `adapter`.
///
/// The format is the surface's first sRGB one; when it supports none, the
/// configuration advertises an sRGB view of a non-sRGB format instead (see
/// [`color_format`]).
///
/// # Panics
///
/// If `adapter` cannot present to `surface`.
fn surface_config(
    surface: &wgpu::Surface<'_>,
    adapter: &wgpu::Adapter,
    width: u32,
    height: u32,
) -> wgpu::SurfaceConfiguration {
    let caps = surface.get_capabilities(adapter);
    let format = srgb_format(&caps.formats).or_else(|| {
        // A format that has an sRGB counterpart, so the frames can still be
        // encoded by hardware; the surface's own preference comes first.
        caps.formats
            .iter()
            .copied()
            .find(|format| format.add_srgb_suffix() != *format)
    });
    let format = format.unwrap_or_else(|| {
        // Neither sRGB nor viewable as one, as an fp16 surface might be. The
        // frames are then rendered in this format itself.
        *caps
            .formats
            .first()
            .expect("the adapter can present to the surface")
    });
    let srgb = format.add_srgb_suffix();
    wgpu::SurfaceConfiguration {
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
        format,
        color_space: wgpu::SurfaceColorSpace::Auto,
        // A surface cannot be configured with a zero dimension.
        width: width.max(1),
        height: height.max(1),
        present_mode: caps
            .present_modes
            .first()
            .copied()
            .unwrap_or(wgpu::PresentMode::Fifo),
        alpha_mode: caps
            .alpha_modes
            .first()
            .copied()
            .unwrap_or(wgpu::CompositeAlphaMode::Auto),
        // Only the sRGB counterpart of the configured format is ever viewed
        // through; anything else would need a view descriptor of its own.
        view_formats: (srgb != format).then_some(srgb).into_iter().collect(),
        desired_maximum_frame_latency: 2,
    }
}

/// The surface's first sRGB format.
fn srgb_format(formats: &[wgpu::TextureFormat]) -> Option<wgpu::TextureFormat> {
    formats.iter().copied().find(wgpu::TextureFormat::is_srgb)
}

/// The format a frame's color attachment is viewed as, for a surface
/// configured with `config`.
///
/// The swap chain's own format when it encodes sRGB. When it does not — the
/// web's canvas offers only non-sRGB formats, and writes to those are taken as
/// already encoded — the swap chain is configured with an sRGB view of the
/// format, so the hardware encodes the shader's linear output on write and the
/// display sees exactly what an sRGB swap chain would have shown it.
///
/// # Panics
///
/// If `config.format` is neither sRGB nor listed among its own
/// `view_formats`.
fn color_format(config: &wgpu::SurfaceConfiguration) -> wgpu::TextureFormat {
    if config.format.is_srgb() {
        return config.format;
    }
    config
        .view_formats
        .iter()
        .copied()
        .find(|format| *format != config.format)
        .unwrap_or(config.format)
}

/// The depth-stencil and, when `sample_count > 1`, multisample textures a
/// frame draws with.
///
/// Both are sized `width` x `height` and transient: they are cleared and
/// discarded inside the frame's single pass, so nothing is ever read back from
/// them.
///
/// `color_format` is the format the frame's color attachment is viewed as (see
/// [`WindowSurface::color_format`]), which the multisample texture carries too:
/// its pixels are resolved into the color view, and wgpu requires the two to be
/// in the same format.
fn create_attachments(
    device: &wgpu::Device,
    color_format: wgpu::TextureFormat,
    width: u32,
    height: u32,
    sample_count: u32,
) -> (wgpu::Texture, Option<wgpu::Texture>) {
    let extent = wgpu::Extent3d {
        width: width.max(1),
        height: height.max(1),
        depth_or_array_layers: 1,
    };
    let depth = device.create_texture(&wgpu::TextureDescriptor {
        label: Some("unlit3d::winit::depth"),
        size: extent,
        mip_level_count: 1,
        sample_count,
        dimension: wgpu::TextureDimension::D2,
        format: default_depth_stencil_format(device),
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TRANSIENT_ATTACHMENT,
        view_formats: &[],
    });
    let msaa = (sample_count > 1).then(|| {
        device.create_texture(&wgpu::TextureDescriptor {
            label: Some("unlit3d::winit::msaa"),
            size: extent,
            mip_level_count: 1,
            sample_count,
            dimension: wgpu::TextureDimension::D2,
            format: color_format,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
                | wgpu::TextureUsages::TRANSIENT_ATTACHMENT,
            view_formats: &[],
        })
    });
    (depth, msaa)
}

/// A graph resource holding a default view of `texture`.
///
/// Used for the depth and multisample attachments, whose views are always in
/// their texture's own format.
fn view_resource(texture: &wgpu::Texture) -> Resource {
    Resource::from(texture.create_view(&wgpu::TextureViewDescriptor::default()))
}

/// A graph resource holding a view of `texture` reinterpreted as `format`.
///
/// Used for the swap chain's color image, which the surface configures with a
/// non-sRGB format on the web: the view is what makes the frame sRGB-encoded.
fn color_view_resource(texture: &wgpu::Texture, format: wgpu::TextureFormat) -> Resource {
    Resource::TextureView {
        view: texture.create_view(&wgpu::TextureViewDescriptor {
            format: Some(format),
            ..Default::default()
        }),
        format,
    }
}