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 = config.features.into();
128 let (device, queue) = adapter
129 .request_device(&wgpu::DeviceDescriptor {
130 required_features,
131 ..Default::default()
132 })
133 .await
134 .unwrap();
135
136 let window_size = window.inner_size();
137
138 let caps = surface.get_capabilities(&adapter);
139 let surface_configuration = wgpu::SurfaceConfiguration {
140 alpha_mode: caps.alpha_modes[0],
141 present_mode: caps.present_modes[0],
142 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
143 format: caps.formats.iter().find(|f| !f.is_srgb()).unwrap().clone(),
144 width: window_size.width,
145 height: window_size.height,
146 color_space: wgpu::SurfaceColorSpace::Auto,
147 view_formats: vec![],
148 desired_maximum_frame_latency: 2,
149 };
150 surface.configure(&device, &surface_configuration);
151
152 let backend = Backend {
153 device,
154 queue,
155 surface,
156 surface_configuration,
157 features: config.features,
158 };
159
160 backend
161}
162
163pub(crate) fn obtain_gpu(
164 mut commands: Commands,
165 window: Read<Window>,
166 receiver: Option<Read<GPUReceiver>>,
167 config: Read<BackendConfig>,
168) {
169 if receiver.is_some() {
171 return;
172 }
173
174 let window = window.raw();
175 let (fulfiller, promise) = Promise::new();
176 let features = config.features;
177
178 let fut = async move {
179 let result = init_gpu(window, BackendConfig { features }).await;
180 fulfiller.fulfill(result);
181 };
182
183 #[cfg(not(target_arch = "wasm32"))]
187 {
188 pollster::block_on(fut);
189 }
190
191 #[cfg(target_arch = "wasm32")]
192 {
193 wasm_bindgen_futures::spawn_local(fut);
194 }
195
196 commands.insert_resource(GPUReceiver { promise });
197}
198
199pub(crate) fn poll_gpu(
200 mut commands: Commands,
201 mut ready: Write<BackendReady>,
202 receiver: Option<Read<GPUReceiver>>,
203) {
204 let Some(receiver) = receiver else {
205 return;
206 };
207
208 if !ready.0 {
209 match receiver.promise.poll() {
211 PromiseState::Ready(backend) => {
212 println!("GPU Ready adding as resource");
213 commands.insert_resource(backend);
214 ready.0 = true;
215 }
216 PromiseState::Pending => {}
217 PromiseState::Disconnected => {
218 tracing::error!(
219 "GPU backend init sender was dropped without ever sending a value — the \
220 app has no usable backend and will stay idle forever"
221 );
222 }
223 }
224 }
225}
226
227pub(crate) fn clean_up_gpu_acquisition_resources(
228 mut commands: Commands,
229 ready: Read<BackendReady>,
230 receiver: Option<Read<GPUReceiver>>,
231) {
232 if ready.0 && receiver.is_some() {
233 commands.remove_resource::<GPUReceiver>();
234 }
235}
236
237pub(crate) fn begin_frame(
238 mut backend: Write<Backend>,
239 mut current_frame: Write<CurrentFrame>,
240 window: Read<Window>,
241) {
242 let (width, height) = window.inner_size();
243 if width > 0
244 && height > 0
245 && (width != backend.surface_configuration.width
246 || height != backend.surface_configuration.height)
247 {
248 backend.surface_configuration.width = width;
249 backend.surface_configuration.height = height;
250 backend
251 .surface
252 .configure(&backend.device, &backend.surface_configuration);
253 }
254
255 let surface_texture = match backend.surface.get_current_texture() {
256 wgpu::CurrentSurfaceTexture::Success(texture) => texture,
257 wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
258 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return,
259 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
260 backend
261 .surface
262 .configure(&backend.device, &backend.surface_configuration);
263 return;
264 }
265 wgpu::CurrentSurfaceTexture::Validation => {
266 tracing::error!("surface validation error acquiring the next frame");
267 return;
268 }
269 };
270
271 let view = surface_texture
272 .texture
273 .create_view(&wgpu::TextureViewDescriptor::default());
274 let encoder = backend
275 .device
276 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
277
278 current_frame.set(Frame::new(encoder, view, surface_texture));
279}
280
281pub(crate) fn maintain_gpu(backend: Read<Backend>) {
285 let _ = backend.device.poll(wgpu::PollType::Poll);
286}
287
288pub(crate) fn end_frame(backend: Read<Backend>, mut current_frame: Write<CurrentFrame>) {
289 let Some(frame) = current_frame.take() else {
290 return;
291 };
292
293 let (encoder, surface_texture) = frame.finish();
294 backend.queue.submit(std::iter::once(encoder.finish()));
295 backend.queue.present(surface_texture);
296}