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,
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}
35
36impl Backend {
37    pub fn surface_width(&self) -> u32 {
38        self.surface_configuration.width
39    }
40
41    pub fn surface_height(&self) -> u32 {
42        self.surface_configuration.height
43    }
44
45    pub fn surface_format(&self) -> TextureFormat {
46        self.surface_configuration.format.into()
47    }
48
49    /// Records and submits a compute pass immediately, in its own command
50    /// encoder — not deferred to any render stage, since compute work
51    /// doesn't need a frame to exist. Read a result back with
52    /// [`Buffer::read`](crate::graphics::pipeline::buffers::Buffer::read)
53    /// once the pass has run.
54    pub fn dispatch_compute(&self, record: impl FnOnce(&mut compute_pass::ComputePass)) {
55        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
56        {
57            let raw_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
58            let mut pass = compute_pass::ComputePass::new(raw_pass);
59            record(&mut pass);
60        }
61        self.queue.submit(std::iter::once(encoder.finish()));
62    }
63}
64
65pub(crate) struct BackendPlugin;
66impl Plugin for BackendPlugin {
67    fn build(self, app: crate::app::App) -> crate::app::App {
68        app.insert_resource(CurrentFrame::default())
69            .add_gpu_system(crate::ecs::system::SystemStage::Update, obtain_gpu)
70            .add_gpu_system(crate::ecs::system::SystemStage::Update, poll_gpu)
71            .add_gpu_system(
72                crate::ecs::system::SystemStage::Update,
73                clean_up_gpu_acquisition_resources,
74            )
75            .add_system(crate::ecs::system::SystemStage::PreRender, begin_frame)
76            .add_system(crate::ecs::system::SystemStage::PostRender, end_frame)
77            .add_system(crate::ecs::system::SystemStage::PostRender, maintain_gpu)
78    }
79}
80
81pub struct GPUReceiver {
82    promise: Promise<Backend>,
83}
84
85async fn init_gpu(window: Arc<winit::window::Window>) -> Backend {
86    let instance = wgpu::Instance::default();
87
88    let surface = instance.create_surface(window.clone()).unwrap();
89
90    let adapter = instance
91        .request_adapter(&wgpu::RequestAdapterOptions {
92            power_preference: wgpu::PowerPreference::HighPerformance,
93            compatible_surface: Some(&surface),
94            force_fallback_adapter: false,
95        })
96        .await
97        .unwrap();
98
99    #[cfg(not(target_arch = "wasm32"))]
100    let required_features = wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER;
101    #[cfg(target_arch = "wasm32")]
102    let required_features = wgpu::Features::empty();
103
104    let (device, queue) = adapter
105        .request_device(&wgpu::DeviceDescriptor {
106            required_features,
107            ..Default::default()
108        })
109        .await
110        .unwrap();
111
112    let window_size = window.inner_size();
113
114    let caps = surface.get_capabilities(&adapter);
115    let surface_configuration = wgpu::SurfaceConfiguration {
116        alpha_mode: caps.alpha_modes[0],
117        present_mode: caps.present_modes[0],
118        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
119        format: caps.formats.iter().find(|f| !f.is_srgb()).unwrap().clone(),
120        width: window_size.width,
121        height: window_size.height,
122        view_formats: vec![],
123        desired_maximum_frame_latency: 2,
124    };
125    surface.configure(&device, &surface_configuration);
126
127    let backend = Backend {
128        device,
129        queue,
130        surface,
131        surface_configuration,
132    };
133
134    backend
135}
136
137pub(crate) fn obtain_gpu(
138    mut commands: Commands,
139    window: Read<Window>,
140    receiver: Option<Read<GPUReceiver>>,
141) {
142    // already waiting on a previous request, nothing to do
143    if receiver.is_some() {
144        return;
145    }
146
147    let window = window.raw();
148    let (fulfiller, promise) = Promise::new();
149
150    let fut = async move {
151        let result = init_gpu(window).await;
152        fulfiller.fulfill(result);
153    };
154
155    // the window is bound to the main thread (winit panics if touched off
156    // it), so gpu init runs synchronously here rather than on a spawned
157    // thread — this only happens once, so blocking is fine
158    #[cfg(not(target_arch = "wasm32"))]
159    {
160        pollster::block_on(fut);
161    }
162
163    #[cfg(target_arch = "wasm32")]
164    {
165        wasm_bindgen_futures::spawn_local(fut);
166    }
167
168    commands.insert_resource(GPUReceiver { promise });
169}
170
171pub(crate) fn poll_gpu(
172    mut commands: Commands,
173    mut ready: Write<BackendReady>,
174    receiver: Option<Read<GPUReceiver>>,
175) {
176    let Some(receiver) = receiver else {
177        return;
178    };
179
180    if !ready.0 {
181        // continue polling for the gpu
182        match receiver.promise.poll() {
183            PromiseState::Ready(backend) => {
184                println!("GPU Ready adding as resource");
185                commands.insert_resource(backend);
186                ready.0 = true;
187            }
188            PromiseState::Pending => {}
189            PromiseState::Disconnected => {
190                tracing::error!(
191                    "GPU backend init sender was dropped without ever sending a value — the \
192                         app has no usable backend and will stay idle forever"
193                );
194            }
195        }
196    }
197}
198
199pub(crate) fn clean_up_gpu_acquisition_resources(
200    mut commands: Commands,
201    ready: Read<BackendReady>,
202    receiver: Option<Read<GPUReceiver>>,
203) {
204    if ready.0 && receiver.is_some() {
205        commands.remove_resource::<GPUReceiver>();
206    }
207}
208
209pub(crate) fn begin_frame(
210    mut backend: Write<Backend>,
211    mut current_frame: Write<CurrentFrame>,
212    window: Read<Window>,
213) {
214    let (width, height) = window.inner_size();
215    if width > 0
216        && height > 0
217        && (width != backend.surface_configuration.width
218            || height != backend.surface_configuration.height)
219    {
220        backend.surface_configuration.width = width;
221        backend.surface_configuration.height = height;
222        backend.surface.configure(&backend.device, &backend.surface_configuration);
223    }
224
225    let surface_texture = match backend.surface.get_current_texture() {
226        wgpu::CurrentSurfaceTexture::Success(texture) => texture,
227        wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
228        wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return,
229        wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
230            backend.surface.configure(&backend.device, &backend.surface_configuration);
231            return;
232        }
233        wgpu::CurrentSurfaceTexture::Validation => {
234            tracing::error!("surface validation error acquiring the next frame");
235            return;
236        }
237    };
238
239    let view = surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default());
240    let encoder = backend.device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
241
242    current_frame.set(Frame::new(encoder, view, surface_texture));
243}
244
245// checks for finished GPU work (buffer map callbacks, etc.) once per tick —
246// nothing else drives Device::poll, so anything relying on a callback
247// (e.g. Buffer::read's Promise) only resolves because this runs
248pub(crate) fn maintain_gpu(backend: Read<Backend>) {
249    let _ = backend.device.poll(wgpu::PollType::Poll);
250}
251
252pub(crate) fn end_frame(backend: Read<Backend>, mut current_frame: Write<CurrentFrame>) {
253    let Some(frame) = current_frame.take() else {
254        return;
255    };
256
257    let (encoder, surface_texture) = frame.finish();
258    backend.queue.submit(std::iter::once(encoder.finish()));
259    surface_texture.present();
260}