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
//! The first frame on WebGPU, drawn twice: once to learn the render
//! pipelines it needs while the browser compiles them all at once, then for
//! real.
//!
//! Created one after another, the twelve pipelines of the gauntlet's first
//! frame held Chrome's GPU process for 0.85 to 0.96 s on an Apple M5 with
//! no Metal shader cached, and nothing showed meanwhile. Through
//! `createRenderPipelineAsync` the same twelve compiled side by side in
//! 0.21 to 0.25 s, and creating them again afterwards took 3 ms in all.
//!
//! On WebGL the first screen's pipelines an earlier run of the app recorded
//! (see [`FIRST_SCREEN_PIPELINES`]) build as the renderer starts. Chrome's
//! GPU process links each program one at a time, about 40 ms on a Mate 20
//! X, and would otherwise sit idle while the page composes, then link all
//! thirteen of the gauntlet's while its first frame waits: that frame ended
//! at 1.11-1.21 s with them built at start, 1.45-1.57 s without. WebGPU
//! builds none at start: Chrome compiles its discovery side by side on an
//! M3 Pro, and a set recorded at another screen size than the first
//! frame's added compiles there without moving the first content.
use std::{cell::RefCell, rc::Rc, sync::Arc};
use cranpose_render_wgpu::WgpuRenderer;
/// The pipelines the app's first screen drew with in a recording run of an
/// earlier build of the same app (`__cranposeRecordFirstScreen`), which the
/// build embeds from the file `CRANPOSE_FIRST_SCREEN_PIPELINES` names;
/// empty for a build that names none.
const FIRST_SCREEN_PIPELINES: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/first_screen_pipelines.bin"));
/// The global a recording run sets before the page loads, and the global
/// the page then leaves its first screen's pipelines in.
const RECORD_REQUEST: &str = "__cranposeRecordFirstScreen";
const RECORDED: &str = "__cranposeFirstScreenPipelines";
/// What the renderer draws with.
pub(crate) struct GpuTarget {
pub(crate) device: Arc<wgpu::Device>,
pub(crate) queue: Arc<wgpu::Queue>,
pub(crate) surface_format: wgpu::TextureFormat,
pub(crate) backend: wgpu::Backend,
pub(crate) downlevel: wgpu::DownlevelFlags,
}
enum Stage {
/// No frame drew yet.
Cold,
/// The first frame is drawing to learn its pipelines.
Discovering,
/// The browser compiles the pipelines the first frame created.
Compiling,
/// They compiled: the next frame installs a new renderer and draws.
Landed(Option<wgpu::webgpu::DiscoveredPipelines>),
/// The frame that creates them again is drawing; held, they stay cached
/// until it is done.
Redrawing {
_compiled: Option<wgpu::webgpu::DiscoveredPipelines>,
},
Done,
}
/// Runs the first frame's pipeline discovery and installs the renderer.
pub(crate) struct PipelineDiscovery {
target: GpuTarget,
stage: Rc<RefCell<Stage>>,
/// Whether this is a recording run whose first screen is still to be
/// left in [`RECORDED`].
recording: std::cell::Cell<bool>,
}
impl PipelineDiscovery {
pub(crate) fn new(target: GpuTarget) -> Self {
let recording = js_sys::Reflect::get(&js_sys::global(), &RECORD_REQUEST.into())
.is_ok_and(|requested| requested.is_truthy());
Self {
target,
stage: Rc::new(RefCell::new(Stage::Cold)),
recording: std::cell::Cell::new(recording),
}
}
/// Installs the first renderer, and on WebGL builds the recorded first
/// screen's pipelines, which the browser links beside the page's start.
pub(crate) fn start(&self, renderer: &mut WgpuRenderer) {
self.install(renderer);
if self.target.backend == wgpu::Backend::Gl {
renderer.build_first_screen_pipelines(FIRST_SCREEN_PIPELINES);
}
}
/// Installs a new GPU renderer on the target: at start, and after
/// discovery, whose frame left the renderer's caches holding what it did
/// not draw.
pub(crate) fn install(&self, renderer: &mut WgpuRenderer) {
renderer.init_gpu(
Arc::clone(&self.target.device),
Arc::clone(&self.target.queue),
self.target.surface_format,
self.target.backend,
self.target.downlevel,
);
}
/// Whether a frame may draw: not while the discovered pipelines compile.
pub(crate) fn may_draw(&self) -> bool {
!matches!(*self.stage.borrow(), Stage::Compiling)
}
/// Prepares the frame about to draw with `renderer`: the first frame on
/// WebGPU discovers its pipelines, and the frame after they compiled
/// draws with a new renderer. Both also build what a later frame needs.
pub(crate) fn begin_frame(&self, renderer: &mut WgpuRenderer) {
self.leave_recorded(renderer);
let mut stage = self.stage.borrow_mut();
*stage = match std::mem::replace(&mut *stage, Stage::Done) {
Stage::Cold if self.target.device.begin_pipeline_discovery() => Stage::Discovering,
Stage::Landed(compiled) => {
self.install(renderer);
Stage::Redrawing {
_compiled: compiled,
}
}
Stage::Cold | Stage::Redrawing { .. } | Stage::Done => Stage::Done,
current @ (Stage::Discovering | Stage::Compiling) => current,
};
if matches!(*stage, Stage::Discovering | Stage::Redrawing { .. }) {
renderer.begin_discovery_frame();
}
}
/// In a recording run, leaves the first screen's pipelines in
/// [`RECORDED`] once the first screen ended.
fn leave_recorded(&self, renderer: &WgpuRenderer) {
if !self.recording.get() {
return;
}
let Some(records) = renderer.first_screen_pipelines() else {
return;
};
self.recording.set(false);
let records = js_sys::Uint8Array::from(records.as_slice());
if js_sys::Reflect::set(&js_sys::global(), &RECORDED.into(), &records).is_err() {
log::warn!(
"[pipeline-records] the first screen's pipelines could not be left in {RECORDED}"
);
}
}
/// Ends the frame that drew: after discovery, waits for the browser to
/// compile what it learned and calls `redraw` once it has. A recording
/// run draws on until its first screen is left: a screen that holds
/// still would draw no frame after the first screen ended.
pub(crate) fn end_frame(&self, redraw: &Rc<dyn Fn()>) {
if self.recording.get() {
// Asked after this frame: a frame asked during one is dropped.
let redraw = Rc::clone(redraw);
wasm_bindgen_futures::spawn_local(async move { redraw() });
}
let mut stage = self.stage.borrow_mut();
match &*stage {
Stage::Discovering => {
*stage = Stage::Compiling;
let compiling = self.target.device.end_pipeline_discovery();
let landed = Rc::clone(&self.stage);
let redraw = Rc::clone(redraw);
wasm_bindgen_futures::spawn_local(async move {
let compiled = compiling.await;
*landed.borrow_mut() = Stage::Landed(compiled);
redraw();
});
}
Stage::Redrawing { .. } => *stage = Stage::Done,
Stage::Cold | Stage::Compiling | Stage::Landed(_) | Stage::Done => {}
}
}
}