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    // Best-effort: an optional feature the adapter doesn't support is
128    // dropped rather than failing device creation. `Backend::features`
129    // reports what was actually granted.
130    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    // already waiting on a previous request, nothing to do
174    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    // the window is bound to the main thread (winit panics if touched off
188    // it), so gpu init runs synchronously here rather than on a spawned
189    // thread — this only happens once, so blocking is fine
190    #[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        // continue polling for the gpu
214        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
285// checks for finished GPU work (buffer map callbacks, etc.) once per tick —
286// nothing else drives Device::poll, so anything relying on a callback
287// (e.g. Buffer::read's Promise) only resolves because this runs
288pub(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}