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},
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 dispatch_compute(&self, record: impl FnOnce(&mut compute_pass::ComputePass)) {
60 let mut encoder = self
61 .device
62 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
63 {
64 let raw_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
65 let mut pass = compute_pass::ComputePass::new(raw_pass);
66 record(&mut pass);
67 }
68 self.queue.submit(std::iter::once(encoder.finish()));
69 }
70}
71
72pub(crate) struct BackendPlugin {
73 features: DeviceFeatures,
74}
75
76pub(crate) struct BackendConfig {
77 features: DeviceFeatures,
78}
79
80impl BackendPlugin {
81 pub fn with_features(features: DeviceFeatures) -> Self {
82 Self { features }
83 }
84}
85
86impl Plugin for BackendPlugin {
87 fn build(self, app: crate::app::App) -> crate::app::App {
88 app.insert_resource(BackendConfig {
89 features: self.features,
90 })
91 .insert_resource(CurrentFrame::default())
92 .add_gpu_system(crate::ecs::system::SystemStage::Update, obtain_gpu)
93 .add_gpu_system(crate::ecs::system::SystemStage::Update, poll_gpu)
94 .add_gpu_system(
95 crate::ecs::system::SystemStage::Update,
96 clean_up_gpu_acquisition_resources,
97 )
98 .add_system(crate::ecs::system::SystemStage::PreRender, begin_frame)
99 .add_system(crate::ecs::system::SystemStage::PostRender, end_frame)
100 .add_system(crate::ecs::system::SystemStage::PostRender, maintain_gpu)
101 }
102}
103
104pub struct GPUReceiver {
105 promise: Promise<Backend>,
106}
107
108async fn init_gpu(window: Arc<winit::window::Window>, config: BackendConfig) -> Backend {
109 let instance = wgpu::Instance::default();
110
111 let surface = instance.create_surface(window.clone()).unwrap();
112
113 let adapter = instance
114 .request_adapter(&wgpu::RequestAdapterOptions {
115 power_preference: wgpu::PowerPreference::HighPerformance,
116 compatible_surface: Some(&surface),
117 force_fallback_adapter: false,
118 apply_limit_buckets: true,
119 })
120 .await
121 .unwrap();
122
123 let required_features = config.features.into();
124 let (device, queue) = adapter
125 .request_device(&wgpu::DeviceDescriptor {
126 required_features,
127 ..Default::default()
128 })
129 .await
130 .unwrap();
131
132 let window_size = window.inner_size();
133
134 let caps = surface.get_capabilities(&adapter);
135 let surface_configuration = wgpu::SurfaceConfiguration {
136 alpha_mode: caps.alpha_modes[0],
137 present_mode: caps.present_modes[0],
138 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
139 format: caps.formats.iter().find(|f| !f.is_srgb()).unwrap().clone(),
140 width: window_size.width,
141 height: window_size.height,
142 color_space: wgpu::SurfaceColorSpace::Auto,
143 view_formats: vec![],
144 desired_maximum_frame_latency: 2,
145 };
146 surface.configure(&device, &surface_configuration);
147
148 let backend = Backend {
149 device,
150 queue,
151 surface,
152 surface_configuration,
153 features: config.features,
154 };
155
156 backend
157}
158
159pub(crate) fn obtain_gpu(
160 mut commands: Commands,
161 window: Read<Window>,
162 receiver: Option<Read<GPUReceiver>>,
163 config: Read<BackendConfig>,
164) {
165 if receiver.is_some() {
167 return;
168 }
169
170 let window = window.raw();
171 let (fulfiller, promise) = Promise::new();
172 let features = config.features;
173
174 let fut = async move {
175 let result = init_gpu(window, BackendConfig { features }).await;
176 fulfiller.fulfill(result);
177 };
178
179 #[cfg(not(target_arch = "wasm32"))]
183 {
184 pollster::block_on(fut);
185 }
186
187 #[cfg(target_arch = "wasm32")]
188 {
189 wasm_bindgen_futures::spawn_local(fut);
190 }
191
192 commands.insert_resource(GPUReceiver { promise });
193}
194
195pub(crate) fn poll_gpu(
196 mut commands: Commands,
197 mut ready: Write<BackendReady>,
198 receiver: Option<Read<GPUReceiver>>,
199) {
200 let Some(receiver) = receiver else {
201 return;
202 };
203
204 if !ready.0 {
205 match receiver.promise.poll() {
207 PromiseState::Ready(backend) => {
208 println!("GPU Ready adding as resource");
209 commands.insert_resource(backend);
210 ready.0 = true;
211 }
212 PromiseState::Pending => {}
213 PromiseState::Disconnected => {
214 tracing::error!(
215 "GPU backend init sender was dropped without ever sending a value — the \
216 app has no usable backend and will stay idle forever"
217 );
218 }
219 }
220 }
221}
222
223pub(crate) fn clean_up_gpu_acquisition_resources(
224 mut commands: Commands,
225 ready: Read<BackendReady>,
226 receiver: Option<Read<GPUReceiver>>,
227) {
228 if ready.0 && receiver.is_some() {
229 commands.remove_resource::<GPUReceiver>();
230 }
231}
232
233pub(crate) fn begin_frame(
234 mut backend: Write<Backend>,
235 mut current_frame: Write<CurrentFrame>,
236 window: Read<Window>,
237) {
238 let (width, height) = window.inner_size();
239 if width > 0
240 && height > 0
241 && (width != backend.surface_configuration.width
242 || height != backend.surface_configuration.height)
243 {
244 backend.surface_configuration.width = width;
245 backend.surface_configuration.height = height;
246 backend
247 .surface
248 .configure(&backend.device, &backend.surface_configuration);
249 }
250
251 let surface_texture = match backend.surface.get_current_texture() {
252 wgpu::CurrentSurfaceTexture::Success(texture) => texture,
253 wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
254 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return,
255 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
256 backend
257 .surface
258 .configure(&backend.device, &backend.surface_configuration);
259 return;
260 }
261 wgpu::CurrentSurfaceTexture::Validation => {
262 tracing::error!("surface validation error acquiring the next frame");
263 return;
264 }
265 };
266
267 let view = surface_texture
268 .texture
269 .create_view(&wgpu::TextureViewDescriptor::default());
270 let encoder = backend
271 .device
272 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
273
274 current_frame.set(Frame::new(encoder, view, surface_texture));
275}
276
277pub(crate) fn maintain_gpu(backend: Read<Backend>) {
281 let _ = backend.device.poll(wgpu::PollType::Poll);
282}
283
284pub(crate) fn end_frame(backend: Read<Backend>, mut current_frame: Write<CurrentFrame>) {
285 let Some(frame) = current_frame.take() else {
286 return;
287 };
288
289 let (encoder, surface_texture) = frame.finish();
290 backend.queue.submit(std::iter::once(encoder.finish()));
291 backend.queue.present(surface_texture);
292}