devela/ui/back/miniquad/
pixels.rs

1// devela::ui::back::miniquad::pixels
2//
3//! Defines [`MiniquadPixels`].
4//
5// TOC
6// - MiniquadPixels
7// - mod shader
8//
9// TODO
10// - to be able to pass dynamic VERTEX and FRAMENT?
11// - use devela abstractions for ratios.
12//
13// IDEA MAYBE
14// - allow to run extra shaders? what's the bridge to dynamic shadertoying?
15//   build one of this with the given glsl code……
16//   - https://docs.rs/miniquad/latest/miniquad/graphics/trait.RenderingBackend.html#tymethod.new_shader
17//   - https://docs.rs/miniquad/latest/miniquad/graphics/enum.ShaderSource.html
18//   - https://docs.rs/miniquad/latest/miniquad/graphics/struct.ShaderMeta.html
19//
20// TODO: new pipeline
21// > https://chatgpt.com/c/67abb34f-d874-8007-9a6e-9a942aad03d0
22// - compile both shaders and create two pipelines
23// - Add a flag (e.g. use_new_shader: bool) in your struct to indicate which pipeline
24// - draw() method, check the flag and call ctx.apply_pipeline(...)
25//
26// TODO: 1. Multiple Passes (Chained Effects)
27// TODO: 2. Combined Shader Logic (Single Pass) (with const concat)
28
29use crate::{
30    format_buf, g_vec2, g_vertex2, iif, miniquad, vec_ as vec, Box, MiniquadEventHandlerExt,
31    MiniquadWindow, Vec,
32};
33use ::miniquad::{
34    Bindings, BufferLayout, EventHandler, FilterMode, MipmapFilterMode, Pipeline, PipelineParams,
35    RenderingBackend, TextureFormat, TextureId, TextureParams, VertexAttribute, VertexFormat,
36};
37
38/// Draws a single fullscreen quad textured by a pixel buffer.
39pub struct MiniquadPixels {
40    ctx: Option<Box<dyn RenderingBackend>>,
41    pipeline: Option<Pipeline>,
42    bindings: Option<Bindings>,
43    texture: Option<TextureId>,
44
45    /* pixel buffer options */
46    ///
47    pub pixels: Vec<u8>,
48    width: u32,
49    height: u32,
50    viewport: (f32, f32, f32, f32), // x, y, w, h IMPROVE: use Extent2d
51    //
52    interpolation: bool,
53    maintain_aspect_ratio: bool,
54}
55
56impl core::fmt::Debug for MiniquadPixels {
57    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
58        let mut buf = [0u8; 20];
59
60        f.debug_struct("MiniquadPixels")
61            // .field("ctx", "…")
62            // .field("pipeline", "…")
63            // .field("bindings", "…")
64            // .field("texture", "…")
65            .field("pixels", &format_buf![&mut buf, "{}B", self.pixels.len()].unwrap())
66            .field("width", &self.width)
67            .field("height", &self.height)
68            .field("interpolation", &self.interpolation)
69            .field("maintain_aspect_ratio", &self.maintain_aspect_ratio)
70            .finish()
71    }
72}
73
74impl MiniquadPixels {
75    /// Returns an uninitialized pixel-? stage with the given buffer size.
76    ///
77    /// By default it maintains the aspect ratio, and doesn't interpolate.
78    pub fn new(width: u32, height: u32) -> Self {
79        Self {
80            ctx: None,
81            pipeline: None,
82            bindings: None,
83            texture: None,
84            /* pixel buffer */ // IMPROVE use a specialized struct
85            pixels: vec![0; width as usize * height as usize * 4],
86            width,
87            height,
88            viewport: (0.0, 0.0, width as f32, height as f32),
89            interpolation: false,
90            maintain_aspect_ratio: true,
91        }
92    }
93
94    /// Returns the current viewport size
95    // IMPROVE: Use Extent2d
96    pub fn viewport(&self) -> (f32, f32, f32, f32) {
97        self.viewport
98    }
99
100    /// Initialize the pixel buffer stage.
101    ///
102    /// This method performs the following steps:
103    /// - Creates a new rendering backend context.
104    /// - Defines a fullscreen quad in normalized device coordinates.
105    ///   - The vertex shader will flip the y-axis to match the pixel buffer’s top-left origin.
106    ///   - If `maintain_aspect_ratio == true` it will be letterboxed to maintain proportion,
107    ///     otherwise it will be rendered covering the entire screen from (-1, -1) to (1, 1).
108    /// - Creates a texture from the pixel data with the specified size, and filtering mode.
109    /// - Compiles the vertex and fragment shaders.
110    /// - Sets up the rendering pipeline.
111    /// - Returns the initialized `MiniquadPixels` instance.
112    pub fn init(mut self) -> Self {
113        let mut ctx: Box<dyn RenderingBackend> = MiniquadWindow::new_rendering_backend();
114
115        #[rustfmt::skip]
116        let vertices: [g_vertex2; 4] = [
117            g_vertex2 { pos : g_vec2 { x: -1.0, y: -1.0 }, uv: g_vec2 { x: 0., y: 0. } },
118            g_vertex2 { pos : g_vec2 { x:  1.0, y: -1.0 }, uv: g_vec2 { x: 1., y: 0. } },
119            g_vertex2 { pos : g_vec2 { x:  1.0, y:  1.0 }, uv: g_vec2 { x: 1., y: 1. } },
120            g_vertex2 { pos : g_vec2 { x: -1.0, y:  1.0 }, uv: g_vec2 { x: 0., y: 1. } },
121        ];
122        let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
123        let (vbuf, ibuf) = miniquad![new_vertices_indices(ctx, Immutable, &vertices, &indices)];
124
125        let interp = iif![self.interpolation; FilterMode::Linear; FilterMode::Nearest];
126
127        // Create a new texture from the pixel data.
128        // TODO: new_render_texture
129        let texture = ctx.new_render_texture(TextureParams {
130            width: self.width,
131            height: self.height,
132            format: TextureFormat::RGBA8,
133            mag_filter: interp,
134            min_filter: interp,
135            ..Default::default()
136        });
137
138        let bindings = miniquad![bindings(vec![vbuf], ibuf, vec![texture])];
139        let shader = miniquad![new_shader(ctx, VERTEX, FRAGMENT, METAL, shader_meta())].unwrap();
140        // TODO: test in metal
141        // miniquad![new_shader_glsl(ctx, VERTEX, FRAGMENT, shader_meta())].unwrap();
142
143        let pipeline = ctx.new_pipeline(
144            &[BufferLayout::default()],
145            &[
146                VertexAttribute::new("in_pos", VertexFormat::Float2),
147                VertexAttribute::new("in_uv", VertexFormat::Float2),
148            ],
149            shader,
150            PipelineParams::default(),
151        );
152
153        self.ctx = Some(ctx);
154        self.pipeline = Some(pipeline);
155        self.bindings = Some(bindings);
156        self.texture = Some(texture);
157        self
158    }
159}
160
161#[rustfmt::skip]
162impl MiniquadEventHandlerExt for MiniquadPixels {
163    fn init(self) -> Self { self.init() }
164    fn interpolation(&self) -> bool { self.interpolation }
165    fn set_interpolation(&mut self, set: bool) {
166        self.interpolation = set;
167        if let Some(ctx) = &mut self.ctx {
168            if let Some(texture) = self.texture {
169                let f = iif![set; FilterMode::Linear; FilterMode::Nearest];
170                ctx.texture_set_filter(texture, f,  MipmapFilterMode::None);
171            }
172        }
173    }
174    fn maintain_aspect_ratio(&self) -> bool { self.maintain_aspect_ratio }
175    fn set_maintain_aspect_ratio(&mut self, set: bool) { self.maintain_aspect_ratio = set; }
176}
177impl EventHandler for MiniquadPixels {
178    fn update(&mut self) {}
179    fn draw(&mut self) {
180        if self.maintain_aspect_ratio {
181            let ctx = self.ctx.as_mut().unwrap();
182
183            let (sw, sh) = MiniquadWindow::get_size();
184            let (fw, fh) = (self.width as f32, self.height as f32);
185
186            let desired_ratio = fw / fh;
187            let current_ratio = sw / sh;
188            let scale = if current_ratio > desired_ratio {
189                sh / fh // letterbox horizontally
190            } else {
191                sw / fw // letterbox vertically
192            };
193
194            let vp_w = (scale * fw).round();
195            let vp_h = (scale * fh).round();
196            // center within the window
197            let vp_x = ((sw - vp_w) / 2.0).round();
198            let vp_y = ((sh - vp_h) / 2.0).round();
199
200            self.viewport = (vp_x, vp_y, vp_w, vp_h);
201
202            // Update the texture with the pixel data
203            ctx.texture_update(self.texture.unwrap(), &self.pixels);
204            // Begin rendering the default pass.
205            ctx.begin_default_pass(Default::default());
206
207            // Apply our viewport that maintains the aspect ratio.
208            ctx.apply_viewport(vp_x as i32, vp_y as i32, vp_w as i32, vp_h as i32);
209
210            // Apply the pipeline and bindings.
211            ctx.apply_pipeline(&self.pipeline.unwrap());
212            ctx.apply_bindings(self.bindings.as_ref().unwrap());
213            // Draw the vertices.
214            ctx.draw(0, 6, 1);
215
216            // restore the viewport
217            ctx.apply_viewport(0, 0, sw as i32, sh as i32);
218
219            ctx.end_render_pass();
220            ctx.commit_frame();
221        } else {
222            let ctx = self.ctx.as_mut().unwrap();
223
224            let (sw, sh) = MiniquadWindow::get_size();
225            // let (fw, fh) = (self.width as f32, self.height as f32); // MAYBE, CHECK
226            self.viewport = (0.0, 0.0, sw, sh);
227
228            // Update the texture with the pixel data
229            ctx.texture_update(self.texture.unwrap(), &self.pixels);
230            // Begin rendering the default pass
231            ctx.begin_default_pass(Default::default());
232
233            // Apply the pipeline and bindings
234            ctx.apply_pipeline(&self.pipeline.unwrap());
235            ctx.apply_bindings(self.bindings.as_ref().unwrap());
236            // Draw the vertices
237            ctx.draw(0, 6, 1);
238
239            ctx.end_render_pass();
240            ctx.commit_frame();
241        }
242    }
243}
244
245use shader::{shader_meta, FRAGMENT, METAL, VERTEX};
246mod shader {
247    use crate::{vec_ as vec, ToString};
248    use ::miniquad::{ShaderMeta, UniformBlockLayout};
249
250    // Returns the shader metadata, such as the names of the images and uniforms it uses.
251    pub fn shader_meta() -> ShaderMeta {
252        ShaderMeta {
253            images: vec!["tex".to_string()],
254            uniforms: UniformBlockLayout { uniforms: vec![] },
255        }
256    }
257
258    // Define the vertex shader, for transforming the vertices into screen coordinates.
259    pub const VERTEX: &str = r#"#version 100
260    attribute vec2 in_pos;
261    attribute vec2 in_uv;
262    uniform vec2 offset;
263    varying lowp vec2 texcoord;
264    void main() {
265        gl_Position = vec4(in_pos + offset, 0, 1);
266        // Flip y axis: convert texture coordinates from bottom-left to top-left origin
267        texcoord = vec2(in_uv.x, 1.0 - in_uv.y);
268        // texcoord = in_uv; // no flipping
269    }"#;
270
271    // Define the fragment shader, for computing the color of each pixel.
272    pub const FRAGMENT: &str = r#"#version 100
273    varying lowp vec2 texcoord;
274    uniform sampler2D tex;
275    void main() {
276        gl_FragColor = texture2D(tex, texcoord);
277    }"#;
278
279    // Define the Metal shader, translated from VERTEX and FRAGMENT.
280    pub const METAL: &str = r#"#include <metal_stdlib>
281    using namespace metal;
282
283    struct VertexIn {
284        float2 in_pos [[attribute(0)]];
285        float2 in_uv  [[attribute(1)]];
286    };
287    struct Uniforms {
288        float2 offset;
289    };
290    struct VertexOut {
291        float4 position [[position]];
292        float2 texcoord;
293    };
294
295    vertex VertexOut vertex_main(VertexIn in [[stage_in]],
296                                 constant Uniforms &uniforms [[buffer(1)]])
297    {
298        VertexOut out;
299        float2 pos = in.in_pos + uniforms.offset;
300        out.position = float4(pos, 0.0, 1.0);
301        out.texcoord = float2(in.in_uv.x, 1.0 - in.in_uv.y);
302        return out;
303    }
304
305    fragment float4 fragment_main(VertexOut in [[stage_in]],
306                                  texture2d<float> tex [[texture(0)]],
307                                  sampler samp [[sampler(0)]])
308    {
309        return tex.sample(samp, in.texcoord);
310    }
311    "#;
312}