Skip to main content

pebble/graphics/
render.rs

1use 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
24/// The GPU device/queue/surface — inserted as a resource once acquisition
25/// finishes (see [`Read<Backend>`](crate::ecs::resources::Read), safe to
26/// use unwrapped anywhere from [`SystemStage::Ready`](crate::ecs::system::SystemStage::Ready)
27/// onward). No raw `wgpu` type is exposed on it besides through the
28/// dedicated escape hatches on the wrapper types built on top of it.
29pub 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    /// Records and submits a compute pass immediately, in its own command
59    /// encoder — not deferred to any render stage, since compute work
60    /// doesn't need a frame to exist. Read a result back with
61    /// [`Buffer::read`](crate::graphics::pipeline::buffers::Buffer::read)
62    /// once the pass has run.
63    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    // already waiting on a previous request, nothing to do
170    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    // the window is bound to the main thread (winit panics if touched off
184    // it), so gpu init runs synchronously here rather than on a spawned
185    // thread — this only happens once, so blocking is fine
186    #[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        // continue polling for the gpu
210        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
281// checks for finished GPU work (buffer map callbacks, etc.) once per tick —
282// nothing else drives Device::poll, so anything relying on a callback
283// (e.g. Buffer::read's Promise) only resolves because this runs
284pub(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}