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},
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    /// Records and submits a compute pass immediately, in its own command
55    /// encoder — not deferred to any render stage, since compute work
56    /// doesn't need a frame to exist. Read a result back with
57    /// [`Buffer::read`](crate::graphics::pipeline::buffers::Buffer::read)
58    /// once the pass has run.
59    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    // already waiting on a previous request, nothing to do
166    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    // the window is bound to the main thread (winit panics if touched off
180    // it), so gpu init runs synchronously here rather than on a spawned
181    // thread — this only happens once, so blocking is fine
182    #[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        // continue polling for the gpu
206        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
277// checks for finished GPU work (buffer map callbacks, etc.) once per tick —
278// nothing else drives Device::poll, so anything relying on a callback
279// (e.g. Buffer::read's Promise) only resolves because this runs
280pub(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}