all_is_cubes_gpu/in_wgpu/
headless.rs

1//! Implementation of [`HeadlessRenderer`] using [`wgpu`].
2
3use std::sync::Arc;
4
5use futures_channel::oneshot;
6use futures_core::future::BoxFuture;
7
8use all_is_cubes::character::Cursor;
9use all_is_cubes::listen;
10use all_is_cubes::util::Executor;
11use all_is_cubes_render::camera::{StandardCameras, Viewport};
12use all_is_cubes_render::{Flaws, HeadlessRenderer, RenderError, Rendering};
13
14use crate::common::{AdaptedInstant, FrameBudget};
15use crate::in_wgpu::{self, init};
16
17/// Builder for configuring a [headless](HeadlessRenderer) [`Renderer`].
18///
19/// The builder owns a `wgpu::Device`; all created renderers will share this device.
20/// If the device is lost, a new `Builder` must be created.
21#[derive(Clone, Debug)]
22pub struct Builder {
23    executor: Arc<dyn Executor>,
24    adapter: Arc<wgpu::Adapter>,
25    device: Arc<wgpu::Device>,
26    queue: Arc<wgpu::Queue>,
27}
28
29impl Builder {
30    /// Create a [`Builder`] by obtaining a new [`wgpu::Device`] from the given adapter.
31    #[cfg_attr(target_family = "wasm", expect(clippy::arc_with_non_send_sync))]
32    pub async fn from_adapter(
33        label: &str,
34        adapter: wgpu::Adapter,
35    ) -> Result<Self, wgpu::RequestDeviceError> {
36        let (device, queue) = adapter
37            .request_device(
38                &in_wgpu::EverythingRenderer::<AdaptedInstant>::device_descriptor(
39                    label,
40                    adapter.limits(),
41                ),
42                None,
43            )
44            .await?;
45        Ok(Self {
46            device: Arc::new(device),
47            queue: Arc::new(queue),
48            adapter: Arc::new(adapter),
49            executor: Arc::new(()),
50        })
51    }
52
53    /// Set the executor for parallel calculations.
54    #[must_use]
55    pub fn executor(mut self, executor: Arc<dyn Executor>) -> Self {
56        self.executor = executor;
57        self
58    }
59
60    /// Create a [`Renderer`] from the GPU connection in this builder and the given cameras.
61    pub fn build(&self, cameras: StandardCameras) -> Renderer {
62        let viewport_source = cameras.viewport_source();
63        let everything = in_wgpu::EverythingRenderer::new(
64            self.executor.clone(),
65            self.device.clone(),
66            cameras,
67            wgpu::TextureFormat::Rgba8UnormSrgb,
68            &self.adapter,
69        );
70
71        let viewport_dirty = listen::Flag::listening(false, &viewport_source);
72        let viewport = viewport_source.get();
73        let color_texture = create_color_texture(&self.device, viewport);
74
75        Renderer::wrap(RendererImpl {
76            device: self.device.clone(),
77            queue: self.queue.clone(),
78            color_texture,
79            everything,
80            viewport_source,
81            viewport_dirty,
82            flaws: Flaws::UNFINISHED, // unfinished because no update() yet
83        })
84    }
85}
86
87/// Implementation of [`HeadlessRenderer`] using [`wgpu`].
88///
89/// This is constructed from a [`wgpu::Device`] and a [`StandardCameras`] using [`Builder`],
90/// and may then be used once or repeatedly to produce images of what those cameras see.
91#[derive(Debug)]
92pub struct Renderer {
93    /// `wgpu` is currently entirely `!Send` on Wasm; use a channel and actor to handle that.
94    #[cfg(target_family = "wasm")]
95    inner: futures_channel::mpsc::Sender<RenderMsg>,
96    #[cfg(not(target_family = "wasm"))]
97    inner: RendererImpl,
98}
99
100/// Internals of [`Renderer`] to actually do the rendering.
101#[derive(Debug)]
102struct RendererImpl {
103    device: Arc<wgpu::Device>,
104    queue: Arc<wgpu::Queue>,
105    color_texture: wgpu::Texture,
106    everything: super::EverythingRenderer<AdaptedInstant>,
107    viewport_source: listen::DynSource<Viewport>,
108    viewport_dirty: listen::Flag,
109    flaws: Flaws,
110}
111
112/// Messages from [`Renderer`] to [`RendererImpl`].
113pub(super) enum RenderMsg {
114    Update(Option<Cursor>, oneshot::Sender<Result<(), RenderError>>),
115    Render(String, oneshot::Sender<Result<Rendering, RenderError>>),
116}
117
118impl Renderer {
119    fn wrap(inner: RendererImpl) -> Renderer {
120        Self {
121            #[cfg(target_family = "wasm")]
122            inner: {
123                // On Wasm, wgpu objects are not Send. Therefore, spawn an actor which
124                // explicitly runs on the main thread to own all of them.
125
126                let (tx, mut rx) = futures_channel::mpsc::channel(1);
127                wasm_bindgen_futures::spawn_local(async move {
128                    use futures_util::stream::StreamExt as _;
129                    let mut inner = inner;
130                    while let Some(msg) = rx.next().await {
131                        inner.handle(msg).await;
132                    }
133                });
134
135                tx
136            },
137            #[cfg(not(target_family = "wasm"))]
138            inner,
139        }
140    }
141
142    async fn send_maybe_wait(&mut self, msg: RenderMsg) {
143        #[cfg(target_family = "wasm")]
144        {
145            use futures_util::sink::SinkExt as _;
146            self.inner
147                .send(msg)
148                .await
149                .expect("Renderer actor unexpectedly disconnected");
150        }
151        #[cfg(not(target_family = "wasm"))]
152        {
153            self.inner.handle(msg).await;
154        }
155    }
156}
157
158impl HeadlessRenderer for Renderer {
159    fn update<'a>(
160        &'a mut self,
161        cursor: Option<&'a Cursor>,
162    ) -> BoxFuture<'a, Result<(), RenderError>> {
163        let (tx, rx) = oneshot::channel();
164        Box::pin(async move {
165            self.send_maybe_wait(RenderMsg::Update(cursor.cloned(), tx))
166                .await;
167            rx.await.unwrap()
168        })
169    }
170
171    fn draw<'a>(&'a mut self, info_text: &'a str) -> BoxFuture<'a, Result<Rendering, RenderError>> {
172        let (tx, rx) = oneshot::channel();
173        Box::pin(async move {
174            self.send_maybe_wait(RenderMsg::Render(info_text.to_owned(), tx))
175                .await;
176            rx.await.unwrap()
177        })
178    }
179}
180
181impl RendererImpl {
182    async fn handle(&mut self, msg: RenderMsg) {
183        match msg {
184            RenderMsg::Update(cursor, reply) => {
185                _ = reply.send(self.update(cursor.as_ref()));
186            }
187            RenderMsg::Render(info_text, reply) => {
188                _ = reply.send(self.draw(&info_text).await);
189            }
190        }
191    }
192
193    fn update(&mut self, cursor: Option<&Cursor>) -> Result<(), RenderError> {
194        let info =
195            self.everything
196                .update(&self.queue, cursor, &FrameBudget::PRACTICALLY_INFINITE)?;
197        self.flaws = info.flaws();
198        Ok(())
199    }
200
201    async fn draw(&mut self, info_text: &str) -> Result<Rendering, RenderError> {
202        // TODO: refactor so that this viewport read is done synchronously, outside the RendererImpl
203        let viewport = self.viewport_source.get();
204
205        if viewport.is_empty() {
206            // GPU doesn't accept zero size, so we have to short-circuit it at this layer or we will
207            // get a placeholder at-least-1-pixel size that EverythingRenderer uses internally.
208            return Ok(Rendering {
209                size: viewport.framebuffer_size,
210                data: Vec::new(),
211                flaws: Flaws::empty(),
212            });
213        }
214
215        if self.viewport_dirty.get_and_clear() {
216            self.color_texture = create_color_texture(&self.device, viewport);
217        }
218
219        let draw_info = self.everything.draw_frame_linear(&self.queue);
220        let post_flaws = self.everything.add_info_text_and_postprocess(
221            &self.queue,
222            &self
223                .color_texture
224                .create_view(&wgpu::TextureViewDescriptor::default()),
225            info_text,
226        );
227        let image = init::get_image_from_gpu(
228            &self.device,
229            &self.queue,
230            &self.color_texture,
231            self.flaws | draw_info.flaws() | post_flaws,
232        )
233        .await;
234        debug_assert_eq!(viewport.framebuffer_size, image.size);
235        Ok(image)
236    }
237}
238
239fn create_color_texture(device: &wgpu::Device, viewport: Viewport) -> wgpu::Texture {
240    device.create_texture(&wgpu::TextureDescriptor {
241        label: Some("headless::Renderer::color_texture"),
242        size: wgpu::Extent3d {
243            width: viewport.framebuffer_size.width.max(1),
244            height: viewport.framebuffer_size.height.max(1),
245            depth_or_array_layers: 1,
246        },
247        mip_level_count: 1,
248        sample_count: 1,
249        dimension: wgpu::TextureDimension::D2,
250        format: wgpu::TextureFormat::Rgba8UnormSrgb,
251        view_formats: &[],
252        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
253    })
254}