aura-gui 0.1.0

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 y dibuja por lotes.

use crate::gpu::atlas::TextureAtlas;
use crate::gpu::context::{Error, GpuContext};
use crate::gpu::instance_buffer::InstanceBuffer;
use crate::gpu::pipelines::Pipelines;
use crate::gpu::scene::{PrimitiveKind, Scene};
use crate::gpu::text::GlyphTarget;
use crate::core::Rgba;
use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct Globals {
    viewport: [f32; 2],
    padding: [f32; 2],
}

pub struct GpuRenderer {
    pub context: GpuContext,
    pipelines: Pipelines,
    globals_buffer: wgpu::Buffer,
    globals_bind_group: wgpu::BindGroup,
    monochrome_atlas: TextureAtlas,
    polychrome_atlas: TextureAtlas,
    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_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("aura_globals"),
            contents: bytemuck::bytes_of(&Globals { viewport: [1.0, 1.0], padding: [0.0; 2] }),
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
        });
        let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("aura_globals"),
            layout: &pipelines.globals_layout,
            entries: &[wgpu::BindGroupEntry { binding: 0, resource: globals_buffer.as_entire_binding() }],
        });

        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            ..Default::default()
        });
        let monochrome_atlas = TextureAtlas::monochrome(device);
        let polychrome_atlas = TextureAtlas::polychrome(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(&monochrome_atlas, "aura_mono_bind");
        let polychrome_bind_group = atlas_bind_group(&polychrome_atlas, "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_buffer,
            globals_bind_group,
            monochrome_atlas,
            polychrome_atlas,
            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 {
        let mono = self.monochrome_atlas.take_overflow();
        let color = self.polychrome_atlas.take_overflow();
        mono || color
    }

    pub fn glyph_target(&mut self) -> GlyphTarget<'_> {
        GlyphTarget {
            queue: Some(&self.context.queue),
            monochrome_atlas: &mut self.monochrome_atlas,
            polychrome_atlas: &mut self.polychrome_atlas,
        }
    }

    /// 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") });
        {
            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);
        }
        encoder
    }

    pub(crate) fn upload(&mut self, scene: &Scene, scale: f32) {
        let globals = Globals {
            viewport: [self.context.config.width as f32 / scale, self.context.config.height as f32 / scale],
            padding: [0.0; 2],
        };
        self.context.queue.write_buffer(&self.globals_buffer, 0, bytemuck::bytes_of(&globals));

        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<'_>, scene: &Scene) {
        pass.set_bind_group(0, &self.globals_bind_group, &[]);
        for batch in &scene.batches {
            let (pipeline, instances, atlas) = match batch.kind {
                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.monochrome_atlas.clear();
        self.polychrome_atlas.clear();
    }
}