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
//! Items related to the **Frame** - a window's render target for a single update.
//!
//! A [`Frame`] wraps Bevy's [`ViewTarget`] for a particular window, giving access to the
//! intermediary texture that nannou draws into and the window's final surface texture, along with
//! the GPU device and render context used to encode draw commands.
use bevy::ecs::entity::EntityHashMap;
use bevy::prelude::*;
use bevy::render::render_resource::Extent3d;
use bevy::render::renderer::{RenderContext, RenderDevice};
use bevy::render::view::{ExtractedWindows, ViewTarget};
use bevy::render::{Extract, RenderApp};
use nannou_core::geom;
use std::cell::RefCell;
use std::ops::Deref;
pub struct FramePlugin;
impl Plugin for FramePlugin {
fn build(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<ExtractedWindowsScaleFactor>()
.add_systems(ExtractSchedule, extract_scale_factors);
}
}
}
fn extract_scale_factors(
mut window_scale_factors: ResMut<ExtractedWindowsScaleFactor>,
windows_q: Extract<Query<(Entity, &Window)>>,
) {
window_scale_factors.clear();
for (entity, window) in windows_q.iter() {
window_scale_factors.insert(entity, window.scale_factor());
}
}
/// The per-window scale factors extracted into the render world by [`FramePlugin`].
///
/// Take this as a `Res` in a render-world system to construct a [`Frame`] via [`Frame::new`].
#[derive(Resource, Deref, DerefMut, Default)]
pub struct ExtractedWindowsScaleFactor(EntityHashMap<f32>);
pub struct Frame<'a, 'r, 'w, 's> {
window_id: Entity,
view_target: &'r ViewTarget,
extracted_windows: &'r ExtractedWindows,
scale_factors: &'r ExtractedWindowsScaleFactor,
render_device: &'r RenderDevice,
frame_count: u32,
render_context: RefCell<&'a mut RenderContext<'w, 's>>,
}
impl<'a, 'r, 'w, 's> Frame<'a, 'r, 'w, 's> {
pub const TEXTURE_FORMAT: wgpu::TextureFormat =
nannou_wgpu::RenderPipelineBuilder::DEFAULT_COLOR_FORMAT;
/// Construct a `Frame` from render-world resources.
///
/// Use this to do custom wgpu rendering from your own render-world system: add
/// [`FramePlugin`] (bundled in [`NannouPlugin`](crate::NannouPlugin)), then take the
/// [`RenderContext`], a [`ViewTarget`] (e.g. via `ViewQuery`), and `Res`-access to
/// [`ExtractedWindows`], [`RenderDevice`] and [`ExtractedWindowsScaleFactor`]. `frame_count` is
/// the render-world [`FrameCount`](bevy::diagnostic::FrameCount) (`FrameCount.0`). See
/// `nannou::render` and the `wgpu_*` examples for the pattern the classic `app(..).render(..)`
/// builder uses internally.
pub fn new(
render_device: &'r RenderDevice,
scale_factors: &'r ExtractedWindowsScaleFactor,
view_target_id: Entity,
view_target: &'r ViewTarget,
extracted_windows: &'r ExtractedWindows,
frame_count: u32,
render_context: &'a mut RenderContext<'w, 's>,
) -> Self {
Frame {
window_id: view_target_id,
view_target,
render_device,
scale_factors,
frame_count,
render_context: RefCell::new(render_context),
extracted_windows,
}
}
/// Access the command encoder in order to encode commands that will be submitted to the GPU
/// queue at the end of the call to **view**.
pub fn command_encoder(&self) -> std::cell::RefMut<'_, wgpu::CommandEncoder> {
std::cell::RefMut::map(self.render_context.borrow_mut(), |x| x.command_encoder())
}
/// The [`Entity`] of the window whose surface is associated with this frame.
pub fn window_id(&self) -> Entity {
self.window_id
}
/// A **Rect** representing the full surface of the frame.
///
/// The returned **Rect** is equivalent to the result of calling **Window::rect** on the window
/// associated with this **Frame**.
pub fn rect(&self) -> geom::Rect {
let window = self.extracted_windows.windows.get(&self.window_id).unwrap();
let scale_factor = self.scale_factors.get(&self.window_id).unwrap();
let scale_factor = *scale_factor as f32;
let [width, height] = [window.physical_width, window.physical_height];
geom::Rect::from_x_y_w_h(
0.0,
0.0,
width as f32 / scale_factor,
height as f32 / scale_factor,
)
}
/// The `nth` frame for the associated window since the application started.
///
/// E.g. the first frame yielded will return `0`, the second will return `1`, and so on.
pub fn nth(&self) -> u64 {
// `FrameCount` is incremented in `Last`, so it reads `1` on the first rendered frame.
(self.frame_count as u64).saturating_sub(1)
}
/// The window's surface texture that will be the target for presenting this frame.
pub fn swap_chain_texture(&self) -> &wgpu::TextureView {
// Bevy 0.19's `ViewTarget` exposes the output texture as an `Option`.
self.view_target
.out_texture()
.expect("frame has no output texture")
}
/// The texture format of the window's surface texture.
pub fn swap_chain_texture_format(&self) -> wgpu::TextureFormat {
self.view_target
.out_texture_view_format()
.expect("frame has no output texture")
}
/// The GPU device used to submit this frame's encoded commands.
pub fn device(&self) -> &wgpu::Device {
self.render_device.wgpu_device()
}
/// The texture to which all graphics should be drawn this frame.
///
/// This is **not** the window's surface texture, but rather an intermediary linear sRGBA image. This
/// intermediary image is used in order to:
///
/// - Ensure consistent MSAA resolve behaviour across platforms.
/// - Avoid the need for multiple implicit conversions to and from linear sRGBA for each
/// graphics pipeline render pass that is used.
/// - Allow for the user's rendered image to persist between frames.
///
/// The exact format of the texture is equal to `Frame::TEXTURE_FORMAT`.
///
/// If the number of MSAA samples specified is greater than `1` (which it is by default if
/// supported by the platform), this will be a multisampled texture. After the **view**
/// function returns, this texture will be resolved to a non-multisampled linear sRGBA texture.
/// After the texture has been resolved if necessary, it will then be used as a shader input
/// within a graphics pipeline used to draw to the window's surface texture.
pub fn texture(&self) -> &wgpu::Texture {
self.view_target.main_texture()
}
/// A full view into the frame's single-sample texture.
///
/// This is the texture that is presented to the window's surface (after tonemapping).
/// When MSAA is enabled it is the *resolve target* - the multisampled texture returned by
/// [`Frame::resolve_target_view`] is resolved into it. Prefer [`Frame::color_attachment`],
/// which wires this up for you, over targeting these views by hand.
///
/// See `texture` for details.
pub fn texture_view(&self) -> &wgpu::TextureView {
self.view_target.main_texture_view()
}
/// Returns the multisampled texture in the case that MSAA is enabled.
///
/// This is the texture a render pass should draw *into* when MSAA is enabled; it is
/// resolved into the single-sample [`Frame::texture`] before presentation.
pub fn resolve_target(&self) -> Option<&wgpu::Texture> {
self.view_target.sampled_main_texture().map(|x| x.deref())
}
/// Returns the multisampled texture view in the case that MSAA is enabled.
///
/// This is the view a render pass should draw *into* when MSAA is enabled; it is resolved
/// into the single-sample [`Frame::texture_view`] before presentation. Prefer
/// [`Frame::color_attachment`], which wires this up for you.
pub fn resolve_target_view(&self) -> Option<&wgpu::TextureView> {
self.view_target
.sampled_main_texture_view()
.map(|x| x.deref())
}
pub fn resolve_target_msaa_samples(&self) -> u32 {
self.view_target
.sampled_main_texture()
.map(|x| x.sample_count())
.unwrap_or(1)
}
/// The color format of the `Frame`'s intermediary linear sRGBA texture (equal to
/// `Frame::TEXTURE_FORMAT`).
pub fn texture_format(&self) -> wgpu::TextureFormat {
self.view_target.main_texture_format()
}
/// The number of MSAA samples of the `Frame`'s intermediary linear sRGBA texture.
pub fn texture_msaa_samples(&self) -> u32 {
self.view_target.main_texture().sample_count()
}
/// The size of the frame's texture in pixels.
pub fn texture_size(&self) -> [u32; 2] {
let Extent3d { width, height, .. } = self.view_target.main_texture().size();
[width, height]
}
/// A [`wgpu::RenderPassColorAttachment`] that targets this frame, with the MSAA resolve
/// already wired up.
///
/// This is the recommended way to target the frame from a custom render pass. When MSAA is
/// enabled the frame's multisampled texture is used as the attachment and is automatically
/// resolved into the single-sample texture presented to the window's surface; when MSAA is
/// disabled the single-sample texture is targeted directly. Either way the result is read
/// by Bevy's tonemapping pass and drawn to the surface.
///
/// `load` selects how the target is initialised at the start of the pass - e.g.
/// [`wgpu::LoadOp::Clear`] to clear it each frame, or [`wgpu::LoadOp::Load`] to build upon
/// the previous frame's contents.
pub fn color_attachment(
&self,
load: wgpu::LoadOp<wgpu::Color>,
) -> wgpu::RenderPassColorAttachment<'_> {
let (view, resolve_target): (&wgpu::TextureView, Option<&wgpu::TextureView>) =
match self.view_target.sampled_main_texture_view() {
Some(sampled) => (sampled, Some(self.view_target.main_texture_view())),
None => (self.view_target.main_texture_view(), None),
};
wgpu::RenderPassColorAttachment {
view,
resolve_target,
ops: wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
}
}
/// Clear the texture with the given color.
pub fn clear<C>(&mut self, color: C)
where
C: Into<Color>,
{
let linear_color = color.into().to_linear();
let view = self.view_target.main_texture_view();
let mut render_ctx = self.render_context.borrow_mut();
render_ctx.begin_tracked_render_pass(wgpu::RenderPassDescriptor {
label: Some("nannou_frame_clear"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(linear_color.into()),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
}