Skip to main content

ratatui_wgpu/backend/
mod.rs

1pub(crate) mod builder;
2pub(crate) mod wgpu_backend;
3
4use std::num::NonZeroU32;
5
6use wgpu::Adapter;
7use wgpu::BindGroup;
8#[cfg(test)]
9use wgpu::Buffer;
10#[cfg(test)]
11use wgpu::BufferDescriptor;
12#[cfg(test)]
13use wgpu::BufferUsages;
14use wgpu::CommandEncoder;
15use wgpu::Device;
16use wgpu::Extent3d;
17use wgpu::Queue;
18use wgpu::RenderPipeline;
19use wgpu::Surface;
20use wgpu::SurfaceConfiguration;
21use wgpu::SurfaceTexture;
22#[cfg(test)]
23use wgpu::Texture;
24use wgpu::TextureDescriptor;
25use wgpu::TextureDimension;
26use wgpu::TextureFormat;
27use wgpu::TextureUsages;
28use wgpu::TextureView;
29use wgpu::TextureViewDescriptor;
30
31/// A pipeline for post-processing rendered text.
32pub trait PostProcessor {
33    /// Custom user data which will be supplied during creation of the post
34    /// processor. Use this to pass in any external state your processor
35    /// requires.
36    type UserData;
37
38    /// Called during initialization of the backend. This should fully
39    /// initialize the post processor for rendering. Note that you are expected
40    /// to render to the final surface during [`PostProcessor::process`].
41    fn compile(
42        device: &Device,
43        text_view: &TextureView,
44        surface_config: &SurfaceConfiguration,
45        user_data: Self::UserData,
46    ) -> Self;
47
48    /// Called after the drawing dimensions have changed (e.g. the surface was
49    /// resized).
50    fn resize(
51        &mut self,
52        device: &Device,
53        text_view: &TextureView,
54        surface_config: &SurfaceConfiguration,
55    );
56
57    /// Called after text has finished compositing. The provided `text_view` is
58    /// the composited text. The final output of your implementation should
59    /// render to the provided `surface_view`.
60    ///
61    /// <div class="warning">
62    ///
63    /// Retaining a reference to the provided surface view will cause a panic if
64    /// the swapchain is recreated.
65    ///
66    /// </div>
67    fn process(
68        &mut self,
69        encoder: &mut CommandEncoder,
70        queue: &Queue,
71        text_view: &TextureView,
72        surface_config: &SurfaceConfiguration,
73        surface_view: &TextureView,
74    );
75
76    /// Called to see if this post processor wants to update the screen. By
77    /// default, the backend only runs the compositor and post processor when
78    /// the text changes. Returning true from this will override that behavior
79    /// and cause the processor to be invoked after a call to flush, even if no
80    /// text changes occurred.
81    fn needs_update(&self) -> bool {
82        false
83    }
84}
85
86/// The surface dimensions of the backend in pixels.
87pub struct Dimensions {
88    pub width: NonZeroU32,
89    pub height: NonZeroU32,
90}
91
92impl From<(NonZeroU32, NonZeroU32)> for Dimensions {
93    fn from((width, height): (NonZeroU32, NonZeroU32)) -> Self {
94        Self { width, height }
95    }
96}
97
98/// Controls the area the text is rendered to relative to the presentation
99/// surface.
100#[derive(Clone, Copy, Debug, Default)]
101#[non_exhaustive]
102pub enum Viewport {
103    /// Render to the entire surface.
104    #[default]
105    Full,
106    /// Render to a reduced area starting at the top right and rendering up to
107    /// the bottom left - (width, height).
108    Shrink { width: u32, height: u32 },
109}
110
111mod private {
112    use wgpu::Surface;
113
114    #[cfg(test)]
115    use crate::backend::HeadlessSurface;
116    #[cfg(test)]
117    use crate::backend::HeadlessTarget;
118    use crate::backend::RenderTarget;
119
120    pub trait Sealed {}
121
122    pub struct Token;
123
124    impl Sealed for Surface<'_> {}
125    impl Sealed for RenderTarget {}
126
127    #[cfg(test)]
128    impl Sealed for HeadlessTarget {}
129
130    #[cfg(test)]
131    impl Sealed for HeadlessSurface {}
132}
133
134/// A Texture target that can be rendered to.
135pub trait RenderTexture: private::Sealed + Sized {
136    /// Gets a [`wgpu::TextureView`] that can be used for rendering.
137    fn get_view(
138        &self,
139        _token: private::Token,
140    ) -> &TextureView;
141    /// Presents the rendered result if applicable.
142    fn present(
143        self,
144        _token: private::Token,
145    ) {
146    }
147}
148
149impl RenderTexture for RenderTarget {
150    fn get_view(
151        &self,
152        _token: private::Token,
153    ) -> &TextureView {
154        &self.view
155    }
156
157    fn present(
158        self,
159        _token: private::Token,
160    ) {
161        self.texture.present();
162    }
163}
164
165#[cfg(test)]
166impl RenderTexture for HeadlessTarget {
167    fn get_view(
168        &self,
169        _token: private::Token,
170    ) -> &TextureView {
171        &self.view
172    }
173}
174
175/// A surface that can be rendered to.
176pub trait RenderSurface<'s>: private::Sealed {
177    type Target: RenderTexture;
178
179    fn wgpu_surface(
180        &self,
181        _token: private::Token,
182    ) -> Option<&Surface<'s>>;
183
184    fn get_default_config(
185        &self,
186        adapter: &Adapter,
187        width: u32,
188        height: u32,
189        _token: private::Token,
190    ) -> Option<SurfaceConfiguration>;
191
192    fn configure(
193        &mut self,
194        device: &Device,
195        config: &SurfaceConfiguration,
196        _token: private::Token,
197    );
198
199    fn get_current_texture(
200        &self,
201        _token: private::Token,
202    ) -> Option<Self::Target>;
203}
204
205pub struct RenderTarget {
206    texture: SurfaceTexture,
207    view: TextureView,
208}
209
210impl<'s> RenderSurface<'s> for Surface<'s> {
211    type Target = RenderTarget;
212
213    fn wgpu_surface(
214        &self,
215        _token: private::Token,
216    ) -> Option<&Surface<'s>> {
217        Some(self)
218    }
219
220    fn get_default_config(
221        &self,
222        adapter: &Adapter,
223        width: u32,
224        height: u32,
225        _token: private::Token,
226    ) -> Option<SurfaceConfiguration> {
227        self.get_default_config(adapter, width, height)
228    }
229
230    fn configure(
231        &mut self,
232        device: &Device,
233        config: &SurfaceConfiguration,
234        _token: private::Token,
235    ) {
236        Surface::configure(self, device, config);
237    }
238
239    fn get_current_texture(
240        &self,
241        _token: private::Token,
242    ) -> Option<Self::Target> {
243        let output = match self.get_current_texture() {
244            Ok(output) => output,
245            Err(err) => {
246                error!("{err}");
247                return None;
248            }
249        };
250
251        let view = output
252            .texture
253            .create_view(&TextureViewDescriptor::default());
254
255        Some(RenderTarget {
256            texture: output,
257            view,
258        })
259    }
260}
261
262#[cfg(test)]
263pub(crate) struct HeadlessTarget {
264    view: TextureView,
265}
266
267#[cfg(test)]
268pub(crate) struct HeadlessSurface {
269    pub(crate) texture: Option<Texture>,
270    pub(crate) buffer: Option<Buffer>,
271    pub(crate) buffer_width: u32,
272    pub(crate) width: u32,
273    pub(crate) height: u32,
274    pub(crate) format: TextureFormat,
275}
276
277#[cfg(test)]
278impl HeadlessSurface {
279    fn new(format: TextureFormat) -> Self {
280        Self {
281            format,
282            ..Default::default()
283        }
284    }
285}
286
287#[cfg(test)]
288impl Default for HeadlessSurface {
289    fn default() -> Self {
290        Self {
291            texture: Default::default(),
292            buffer: Default::default(),
293            buffer_width: Default::default(),
294            width: Default::default(),
295            height: Default::default(),
296            format: TextureFormat::Rgba8Unorm,
297        }
298    }
299}
300
301#[cfg(test)]
302impl RenderSurface<'static> for HeadlessSurface {
303    type Target = HeadlessTarget;
304
305    fn wgpu_surface(
306        &self,
307        _token: private::Token,
308    ) -> Option<&Surface<'static>> {
309        None
310    }
311
312    fn get_default_config(
313        &self,
314        _adapter: &Adapter,
315        width: u32,
316        height: u32,
317        _token: private::Token,
318    ) -> Option<SurfaceConfiguration> {
319        Some(SurfaceConfiguration {
320            usage: TextureUsages::RENDER_ATTACHMENT,
321            format: self.format,
322            width,
323            height,
324            present_mode: wgpu::PresentMode::Immediate,
325            desired_maximum_frame_latency: 2,
326            alpha_mode: wgpu::CompositeAlphaMode::Auto,
327            view_formats: vec![],
328        })
329    }
330
331    fn configure(
332        &mut self,
333        device: &Device,
334        config: &SurfaceConfiguration,
335        _token: private::Token,
336    ) {
337        self.texture = Some(device.create_texture(&TextureDescriptor {
338            label: None,
339            size: Extent3d {
340                width: config.width,
341                height: config.height,
342                depth_or_array_layers: 1,
343            },
344            mip_level_count: 1,
345            sample_count: 1,
346            dimension: TextureDimension::D2,
347            format: self.format,
348            usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
349            view_formats: &[],
350        }));
351
352        self.buffer_width = config.width * 4;
353        self.buffer = Some(device.create_buffer(&BufferDescriptor {
354            label: None,
355            size: (self.buffer_width * config.height) as u64,
356            usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
357            mapped_at_creation: false,
358        }));
359        self.width = config.width;
360        self.height = config.height;
361    }
362
363    fn get_current_texture(
364        &self,
365        _token: private::Token,
366    ) -> Option<Self::Target> {
367        self.texture.as_ref().map(|t| HeadlessTarget {
368            view: t.create_view(&TextureViewDescriptor::default()),
369        })
370    }
371}
372
373#[repr(C)]
374#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
375struct TextBgVertexMember {
376    vertex: [f32; 2],
377    bg_color: u32,
378}
379
380// Vertex + UVCoord + Color
381#[repr(C)]
382#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
383struct TextVertexMember {
384    vertex: [f32; 2],
385    uv: [f32; 2],
386    fg_color: u32,
387    underline_pos: u32,
388    underline_color: u32,
389    strikeout_pos: u32,
390    strikeout_color: u32,
391}
392
393struct TextCacheBgPipeline {
394    pipeline: RenderPipeline,
395    fs_uniforms: BindGroup,
396}
397
398struct TextCacheFgPipeline {
399    pipeline: RenderPipeline,
400    fs_uniforms: BindGroup,
401    atlas_bindings: BindGroup,
402}
403
404struct WgpuState {
405    text_dest_view: TextureView,
406}
407
408fn build_wgpu_state(
409    device: &Device,
410    drawable_width: u32,
411    drawable_height: u32,
412) -> WgpuState {
413    let text_dest = device.create_texture(&TextureDescriptor {
414        label: Some("Text Compositor Out"),
415        size: Extent3d {
416            width: drawable_width.max(1),
417            height: drawable_height.max(1),
418            depth_or_array_layers: 1,
419        },
420        mip_level_count: 1,
421        sample_count: 1,
422        dimension: TextureDimension::D2,
423        format: TextureFormat::Rgba8Unorm,
424        usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
425        view_formats: &[],
426    });
427
428    let text_dest_view = text_dest.create_view(&TextureViewDescriptor::default());
429
430    WgpuState { text_dest_view }
431}