pebble/graphics/
render.rs1use std::sync::Arc;
2
3use crate::{
4 app::BackendReady,
5 ecs::{
6 commands::Commands,
7 plugin::Plugin,
8 promise::{Promise, PromiseState},
9 resources::{Read, Write},
10 },
11 graphics::{
12 render::frame::{CurrentFrame, Frame},
13 types::{TextureFormat, flags::DeviceFeatures, limits::DeviceLimits},
14 window::Window,
15 },
16};
17
18pub mod compute_pass;
19pub mod frame;
20pub(crate) mod gpu_context;
21pub mod render_pass;
22pub mod targets;
23
24pub struct Backend {
30 pub(crate) device: wgpu::Device,
31 pub(crate) queue: wgpu::Queue,
32 surface: wgpu::Surface<'static>,
33 surface_configuration: wgpu::SurfaceConfiguration,
34 features: DeviceFeatures,
35}
36
37impl Backend {
38 pub fn surface_width(&self) -> u32 {
39 self.surface_configuration.width
40 }
41
42 pub fn surface_height(&self) -> u32 {
43 self.surface_configuration.height
44 }
45
46 pub fn surface_format(&self) -> TextureFormat {
47 self.surface_configuration.format.into()
48 }
49
50 pub fn features(&self) -> DeviceFeatures {
51 self.features.into()
52 }
53
54 pub fn limits(&self) -> DeviceLimits {
55 self.device.limits().into()
56 }
57
58 pub fn dispatch_compute(&self, record: impl FnOnce(&mut compute_pass::ComputePass)) {
64 let mut encoder = self
65 .device
66 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
67 {
68 let raw_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
69 let mut pass = compute_pass::ComputePass::new(raw_pass);
70 record(&mut pass);
71 }
72 self.queue.submit(std::iter::once(encoder.finish()));
73 }
74}
75
76pub(crate) struct BackendPlugin {
77 features: DeviceFeatures,
78}
79
80pub(crate) struct BackendConfig {
81 features: DeviceFeatures,
82}
83
84impl BackendPlugin {
85 pub fn with_features(features: DeviceFeatures) -> Self {
86 Self { features }
87 }
88}
89
90impl Plugin for BackendPlugin {
91 fn build(self, app: crate::app::App) -> crate::app::App {
92 app.insert_resource(BackendConfig {
93 features: self.features,
94 })
95 .insert_resource(CurrentFrame::default())
96 .add_gpu_system(crate::ecs::system::SystemStage::Update, obtain_gpu)
97 .add_gpu_system(crate::ecs::system::SystemStage::Update, poll_gpu)
98 .add_gpu_system(
99 crate::ecs::system::SystemStage::Update,
100 clean_up_gpu_acquisition_resources,
101 )
102 .add_system(crate::ecs::system::SystemStage::PreRender, begin_frame)
103 .add_system(crate::ecs::system::SystemStage::PostRender, end_frame)
104 .add_system(crate::ecs::system::SystemStage::PostRender, maintain_gpu)
105 }
106}
107
108pub struct GPUReceiver {
109 promise: Promise<Backend>,
110}
111
112async fn init_gpu(window: Arc<winit::window::Window>, config: BackendConfig) -> Backend {
113 let instance = wgpu::Instance::default();
114
115 let surface = instance.create_surface(window.clone()).unwrap();
116
117 let adapter = instance
118 .request_adapter(&wgpu::RequestAdapterOptions {
119 power_preference: wgpu::PowerPreference::HighPerformance,
120 compatible_surface: Some(&surface),
121 force_fallback_adapter: false,
122 apply_limit_buckets: true,
123 })
124 .await
125 .unwrap();
126
127 let required_features =
131 Into::<wgpu::Features>::into(config.features) & adapter.features();
132 let (device, queue) = adapter
133 .request_device(&wgpu::DeviceDescriptor {
134 required_features,
135 ..Default::default()
136 })
137 .await
138 .unwrap();
139
140 let window_size = window.inner_size();
141
142 let caps = surface.get_capabilities(&adapter);
143 let surface_configuration = wgpu::SurfaceConfiguration {
144 alpha_mode: caps.alpha_modes[0],
145 present_mode: caps.present_modes[0],
146 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
147 format: caps.formats.iter().find(|f| !f.is_srgb()).unwrap().clone(),
148 width: window_size.width,
149 height: window_size.height,
150 color_space: wgpu::SurfaceColorSpace::Auto,
151 view_formats: vec![],
152 desired_maximum_frame_latency: 2,
153 };
154 surface.configure(&device, &surface_configuration);
155
156 let backend = Backend {
157 device,
158 queue,
159 surface,
160 surface_configuration,
161 features: required_features.into(),
162 };
163
164 backend
165}
166
167pub(crate) fn obtain_gpu(
168 mut commands: Commands,
169 window: Read<Window>,
170 receiver: Option<Read<GPUReceiver>>,
171 config: Read<BackendConfig>,
172) {
173 if receiver.is_some() {
175 return;
176 }
177
178 let window = window.raw();
179 let (fulfiller, promise) = Promise::new();
180 let features = config.features;
181
182 let fut = async move {
183 let result = init_gpu(window, BackendConfig { features }).await;
184 fulfiller.fulfill(result);
185 };
186
187 #[cfg(not(target_arch = "wasm32"))]
191 {
192 pollster::block_on(fut);
193 }
194
195 #[cfg(target_arch = "wasm32")]
196 {
197 wasm_bindgen_futures::spawn_local(fut);
198 }
199
200 commands.insert_resource(GPUReceiver { promise });
201}
202
203pub(crate) fn poll_gpu(
204 mut commands: Commands,
205 mut ready: Write<BackendReady>,
206 receiver: Option<Read<GPUReceiver>>,
207) {
208 let Some(receiver) = receiver else {
209 return;
210 };
211
212 if !ready.0 {
213 match receiver.promise.poll() {
215 PromiseState::Ready(backend) => {
216 tracing::info!("GPU backend ready, adding as resource");
217 commands.insert_resource(backend);
218 ready.0 = true;
219 }
220 PromiseState::Pending => {}
221 PromiseState::Disconnected => {
222 tracing::error!(
223 "GPU backend init sender was dropped without ever sending a value — the \
224 app has no usable backend and will stay idle forever"
225 );
226 }
227 }
228 }
229}
230
231pub(crate) fn clean_up_gpu_acquisition_resources(
232 mut commands: Commands,
233 ready: Read<BackendReady>,
234 receiver: Option<Read<GPUReceiver>>,
235) {
236 if ready.0 && receiver.is_some() {
237 commands.remove_resource::<GPUReceiver>();
238 }
239}
240
241pub(crate) fn begin_frame(
242 mut backend: Write<Backend>,
243 mut current_frame: Write<CurrentFrame>,
244 window: Read<Window>,
245) {
246 let (width, height) = window.inner_size();
247 if width > 0
248 && height > 0
249 && (width != backend.surface_configuration.width
250 || height != backend.surface_configuration.height)
251 {
252 backend.surface_configuration.width = width;
253 backend.surface_configuration.height = height;
254 backend
255 .surface
256 .configure(&backend.device, &backend.surface_configuration);
257 }
258
259 let surface_texture = match backend.surface.get_current_texture() {
260 wgpu::CurrentSurfaceTexture::Success(texture) => texture,
261 wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
262 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return,
263 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
264 backend
265 .surface
266 .configure(&backend.device, &backend.surface_configuration);
267 return;
268 }
269 wgpu::CurrentSurfaceTexture::Validation => {
270 tracing::error!("surface validation error acquiring the next frame");
271 return;
272 }
273 };
274
275 let view = surface_texture
276 .texture
277 .create_view(&wgpu::TextureViewDescriptor::default());
278 let encoder = backend
279 .device
280 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
281
282 current_frame.set(Frame::new(encoder, view, surface_texture));
283}
284
285pub(crate) fn maintain_gpu(backend: Read<Backend>) {
289 let _ = backend.device.poll(wgpu::PollType::Poll);
290}
291
292pub(crate) fn end_frame(backend: Read<Backend>, mut current_frame: Write<CurrentFrame>) {
293 let Some(frame) = current_frame.take() else {
294 return;
295 };
296
297 let (encoder, surface_texture) = frame.finish();
298 backend.queue.submit(std::iter::once(encoder.finish()));
299 backend.queue.present(surface_texture);
300}