aura-gui 0.3.5

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Ejecuta una [`Scene`] sobre la swapchain: sube instancias (solo las que cambiaron) y dibuja por lotes.

use crate::core::Rgba;
use crate::gpu::atlas::{AtlasPair, TextureAtlas};
use crate::gpu::blur::BlurState;
use crate::gpu::context::{Error, GpuContext};
use crate::gpu::globals::GlobalsSlots;
use crate::gpu::instance_buffer::InstanceBuffer;
use crate::gpu::pipelines::Pipelines;
use crate::gpu::scene::{Batch, PrimitiveKind, Scene};
use crate::gpu::text::GlyphTarget;

#[path = "renderer_blur.rs"]
mod blur_frame;

pub struct GpuRenderer {
    pub context: GpuContext,
    pipelines: Pipelines,
    globals: GlobalsSlots,
    /// Solo existe si alguna escena pidió desenfoque; se suelta al rato de no usarse.
    blur: Option<BlurState>,
    pub(crate) atlases: AtlasPair,
    monochrome_bind_group: wgpu::BindGroup,
    polychrome_bind_group: wgpu::BindGroup,
    shadows: InstanceBuffer,
    quads: InstanceBuffer,
    underlines: InstanceBuffer,
    monochrome_sprites: InstanceBuffer,
    polychrome_sprites: InstanceBuffer,
}

impl GpuRenderer {
    pub fn new(context: GpuContext) -> Result<Self, Error> {
        let device = &context.device;
        let pipelines = Pipelines::new(device, context.config.format)?;

        let globals = GlobalsSlots::new(device, &pipelines.globals_layout, 1);

        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            ..Default::default()
        });
        let atlases = AtlasPair::gpu(device);
        let atlas_bind_group = |atlas: &TextureAtlas, label: &str| {
            device.create_bind_group(&wgpu::BindGroupDescriptor {
                label: Some(label),
                layout: &pipelines.atlas_layout,
                entries: &[
                    wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(atlas.view()) },
                    wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
                ],
            })
        };
        let monochrome_bind_group = atlas_bind_group(&atlases.monochrome, "aura_mono_bind");
        let polychrome_bind_group = atlas_bind_group(&atlases.polychrome, "aura_color_bind");

        let layout = &pipelines.instances_layout;
        Ok(Self {
            shadows: InstanceBuffer::new(device, layout, "aura_shadows"),
            quads: InstanceBuffer::new(device, layout, "aura_quads"),
            underlines: InstanceBuffer::new(device, layout, "aura_underlines"),
            monochrome_sprites: InstanceBuffer::new(device, layout, "aura_mono_sprites"),
            polychrome_sprites: InstanceBuffer::new(device, layout, "aura_color_sprites"),
            pipelines,
            globals,
            blur: None,
            atlases,
            monochrome_bind_group,
            polychrome_bind_group,
            context,
        })
    }

    pub fn resize(&mut self, width: u32, height: u32) {
        self.context.resize(width, height);
    }

    /// `true` (una sola vez) si algún atlas se llenó: el llamador debe vaciar sus cachés y repintar.
    pub fn take_atlas_overflow(&mut self) -> bool {
        self.atlases.take_overflow()
    }

    pub fn glyph_target(&mut self) -> GlyphTarget<'_> {
        let queue = &self.context.queue;
        self.atlases.target(Some(queue))
    }

    /// Devuelve `Ok(false)` cuando el frame se omitió (ventana oculta o superficie obsoleta).
    pub fn render(&mut self, scene: &Scene, scale: f32, clear: Rgba) -> Result<bool, Error> {
        let frame = match self.context.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(frame) => frame,
            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
                self.context.resize(self.context.config.width, self.context.config.height);
                frame
            }
            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return Ok(false),
            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
                self.context.resize(self.context.config.width, self.context.config.height);
                return Ok(false);
            }
            other => return Err(Error(format!("no se pudo adquirir el frame: {other:?}"))),
        };

        // No subir instancias ni uniformes si la superficie no se puede presentar.
        self.upload(scene, scale);
        let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
        let encoder = self.encode(&view, scene, clear);
        self.context.queue.submit(Some(encoder.finish()));
        self.context.queue.present(frame);
        Ok(true)
    }

    /// Graba el pase de render de la escena sobre `view`.
    pub(crate) fn encode(&self, view: &wgpu::TextureView, scene: &Scene, clear: Rgba) -> wgpu::CommandEncoder {
        let mut encoder =
            self.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("aura_frame") });
        if let Some(blur) = self.blur.as_ref().filter(|_| scene.has_blur()) {
            self.encode_blurred(&mut encoder, blur, view, scene, clear);
            return encoder;
        }
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("aura_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view,
                    depth_slice: None,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: clear.r as f64,
                            g: clear.g as f64,
                            b: clear.b as f64,
                            a: clear.a as f64,
                        }),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                ..Default::default()
            });
            self.draw_batches(&mut pass, &scene.batches);
        }
        encoder
    }

    pub(crate) fn upload(&mut self, scene: &Scene, scale: f32) {
        let viewport = [self.context.config.width as f32 / scale, self.context.config.height as f32 / scale];
        let (device, queue) = (&self.context.device, &self.context.queue);
        self.globals.upload(device, queue, &self.pipelines.globals_layout, scene, viewport);
        self.prepare_blur(scene, scale);
        let (device, queue) = (&self.context.device, &self.context.queue);
        let layout = &self.pipelines.instances_layout;
        self.shadows.upload(device, queue, layout, &scene.shadows);
        self.quads.upload(device, queue, layout, &scene.quads);
        self.underlines.upload(device, queue, layout, &scene.underlines);
        self.monochrome_sprites.upload(device, queue, layout, &scene.monochrome_sprites);
        self.polychrome_sprites.upload(device, queue, layout, &scene.polychrome_sprites);
    }

    fn draw_batches(&self, pass: &mut wgpu::RenderPass<'_>, batches: &[Batch]) {
        let mut bound = None;
        for batch in batches {
            // Solo se cambia de ranura al cambiar de transformación: sin ellas, un único `set_bind_group`.
            if bound != Some(batch.transform) {
                bound = Some(batch.transform);
                pass.set_bind_group(
                    0,
                    self.globals.bind_group(),
                    &[(u64::from(batch.transform) * self.globals.stride()) as u32],
                );
            }
            let (pipeline, instances, atlas) = match batch.kind {
                PrimitiveKind::Blur => {
                    let Some(blur) = &self.blur else { continue };
                    (blur.composite(), blur.instances(), blur.blurred_bind())
                }
                PrimitiveKind::Shadow => (&self.pipelines.shadow, &self.shadows, &self.monochrome_bind_group),
                PrimitiveKind::Quad => (&self.pipelines.quad, &self.quads, &self.monochrome_bind_group),
                PrimitiveKind::Underline => (&self.pipelines.underline, &self.underlines, &self.monochrome_bind_group),
                PrimitiveKind::Monochrome => {
                    (&self.pipelines.monochrome, &self.monochrome_sprites, &self.monochrome_bind_group)
                }
                PrimitiveKind::Polychrome => {
                    (&self.pipelines.polychrome, &self.polychrome_sprites, &self.polychrome_bind_group)
                }
            };
            pass.set_pipeline(pipeline);
            pass.set_bind_group(1, instances.bind_group(), &[]);
            pass.set_bind_group(2, atlas, &[]);
            pass.draw(0..4, batch.range.clone());
        }
    }

    /// Libera VRAM de buffers y atlas; el llamador debe vaciar también la caché de glifos.
    pub fn shrink_caches(&mut self) {
        let (device, layout) = (&self.context.device, &self.pipelines.instances_layout);
        self.shadows.shrink(device, layout);
        self.quads.shrink(device, layout);
        self.underlines.shrink(device, layout);
        self.monochrome_sprites.shrink(device, layout);
        self.polychrome_sprites.shrink(device, layout);
        self.atlases.clear();
        if let Some(blur) = &mut self.blur {
            blur.release();
        }
    }
}