Skip to main content

all_is_cubes_gpu/
headless.rs

1//! Implementation of [`HeadlessRenderer`] using [`wgpu`].
2
3use alloc::boxed::Box;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6
7use futures_core::future::BoxFuture;
8
9use all_is_cubes::character::Cursor;
10use all_is_cubes::listen;
11use all_is_cubes::universe::ReadTicket;
12use all_is_cubes::util::Executor;
13use all_is_cubes_render::camera::{ImageSize, Layers, StandardCameras, Viewport};
14use all_is_cubes_render::{Flaws, HeadlessRenderer, RenderError, Rendering};
15
16use crate::common::FrameBudget;
17use crate::glue::size2d_to_extent;
18use crate::init;
19
20/// Builder for configuring a [headless](HeadlessRenderer) [`Renderer`].
21///
22/// The builder owns a `wgpu::Device`; all created renderers will share this device.
23/// If the device is lost, a new `Builder` must be created.
24#[derive(Clone, Debug)]
25pub struct Builder {
26    executor: Arc<dyn Executor>,
27    pub(crate) adapter: wgpu::Adapter,
28    device: wgpu::Device,
29    queue: wgpu::Queue,
30}
31
32impl Builder {
33    /// Create a [`Builder`] by obtaining a new [`wgpu::Device`] from the given adapter.
34    pub async fn from_adapter(
35        label: &str,
36        adapter: wgpu::Adapter,
37    ) -> Result<Self, wgpu::RequestDeviceError> {
38        let (device, queue) = adapter
39            .request_device(&crate::EverythingRenderer::device_descriptor(
40                label,
41                adapter.limits(),
42            ))
43            .await?;
44        Ok(Self {
45            device,
46            queue,
47            adapter,
48            executor: Arc::new(()),
49        })
50    }
51
52    /// Set the executor for parallel calculations.
53    #[must_use]
54    pub fn executor(mut self, executor: Arc<dyn Executor>) -> Self {
55        self.executor = executor;
56        self
57    }
58
59    /// Create a [`Renderer`] from the GPU connection in this builder and the given cameras.
60    pub fn build(&self, cameras: StandardCameras) -> Renderer {
61        let viewport_source = cameras.viewport_source();
62        let everything = crate::EverythingRenderer::new(
63            self.executor.clone(),
64            self.device.clone(),
65            &self.queue,
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            adapter_info: self.adapter.get_info(),
77            device: self.device.clone(),
78            queue: self.queue.clone(),
79            color_texture,
80            everything,
81            viewport_source,
82            viewport_dirty,
83            flaws: Flaws::UNFINISHED, // unfinished because no update() yet
84        })
85    }
86}
87
88/// Implementation of [`HeadlessRenderer`] using [`wgpu`].
89///
90/// This is constructed from a [`wgpu::Device`] and a [`StandardCameras`] using [`Builder`],
91/// and may then be used once or repeatedly to produce images of what those cameras see.
92#[derive(Debug)]
93pub struct Renderer {
94    /// `wgpu` is currently entirely `!Send` on web-wasm, but also there are no additional threads,
95    /// that might use this renderer, so we can use `send_wrapper` to achieve `Send`.
96    #[cfg(target_family = "wasm")]
97    inner: send_wrapper::SendWrapper<RendererImpl>,
98    #[cfg(not(target_family = "wasm"))]
99    inner: RendererImpl,
100}
101
102/// Internals of [`Renderer`] to actually do the rendering.
103#[derive(Debug)]
104struct RendererImpl {
105    adapter_info: wgpu::AdapterInfo,
106    device: wgpu::Device,
107    queue: wgpu::Queue,
108
109    color_texture: wgpu::Texture,
110    everything: super::EverythingRenderer,
111
112    viewport_source: listen::DynSource<Viewport>,
113    viewport_dirty: listen::Flag,
114
115    /// Flaws from the last [`Self::update()`] call.
116    flaws: Flaws,
117}
118
119impl Renderer {
120    fn wrap(inner: RendererImpl) -> Renderer {
121        Self {
122            #[cfg(target_family = "wasm")]
123            inner: send_wrapper::SendWrapper::new(inner),
124            #[cfg(not(target_family = "wasm"))]
125            inner,
126        }
127    }
128}
129
130impl HeadlessRenderer for Renderer {
131    fn update(
132        &mut self,
133        read_tickets: Layers<ReadTicket<'_>>,
134        cursor: Option<&Cursor>,
135    ) -> Result<(), RenderError> {
136        // Note: this delegation is the simplest ways to
137        self.inner.update(read_tickets, cursor)
138    }
139
140    fn draw<'a>(&'a mut self, info_text: &'a str) -> BoxFuture<'a, Result<Rendering, RenderError>> {
141        let future = async move { self.inner.draw(info_text).await };
142        #[cfg(target_family = "wasm")]
143        let future = send_wrapper::SendWrapper::new(future);
144        Box::pin(future)
145    }
146}
147
148impl RendererImpl {
149    fn update(
150        &mut self,
151        read_tickets: Layers<ReadTicket<'_>>,
152        cursor: Option<&Cursor>,
153    ) -> Result<(), RenderError> {
154        let info = self.everything.update(
155            read_tickets,
156            &self.queue,
157            cursor,
158            &FrameBudget::PRACTICALLY_INFINITE,
159        )?;
160        self.flaws = info.flaws();
161        Ok(())
162    }
163
164    async fn draw(&mut self, info_text: &str) -> Result<Rendering, RenderError> {
165        // TODO: refactor so that this viewport read is done synchronously, outside the RendererImpl
166        let viewport = self.viewport_source.get();
167
168        // If we are using the noop testing backend, then it is expected that the image will be
169        // missing.
170        let adapter_flaws = if self.adapter_info.backend == wgpu::Backend::Noop {
171            Flaws::OTHER
172        } else {
173            Flaws::empty()
174        };
175
176        if viewport.is_empty() {
177            // GPU doesn't accept zero size, so we have to short-circuit it at this layer or we will
178            // get a placeholder at-least-1-pixel size that EverythingRenderer uses internally.
179            return Ok(Rendering {
180                size: viewport.framebuffer_size,
181                data: Vec::new(),
182                flaws: Flaws::empty(),
183            });
184        }
185
186        if self.viewport_dirty.get_and_clear() {
187            self.color_texture = create_color_texture(&self.device, viewport);
188        }
189
190        let draw_info = self.everything.draw_frame_linear(&self.queue);
191        let (post_cmd, post_flaws) = self.everything.add_info_text_and_postprocess(
192            &self.queue,
193            &self.color_texture.create_view(&wgpu::TextureViewDescriptor::default()),
194            info_text,
195        );
196        self.queue.submit([post_cmd]);
197        let image = init::get_image_from_gpu(
198            &self.device,
199            &self.queue,
200            &self.color_texture,
201            self.flaws | draw_info.flaws() | post_flaws | adapter_flaws,
202        )
203        .await;
204        debug_assert_eq!(viewport.framebuffer_size, image.size);
205        Ok(image)
206    }
207}
208
209fn create_color_texture(device: &wgpu::Device, viewport: Viewport) -> wgpu::Texture {
210    device.create_texture(&wgpu::TextureDescriptor {
211        label: Some("headless::Renderer::color_texture"),
212        size: size2d_to_extent(viewport.framebuffer_size.max(ImageSize::splat(1))),
213        mip_level_count: 1,
214        sample_count: 1,
215        dimension: wgpu::TextureDimension::D2,
216        format: wgpu::TextureFormat::Rgba8UnormSrgb,
217        view_formats: &[],
218        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
219    })
220}