aura-gui 0.2.2

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.
//! Desenfoque en la GPU (`backdrop-blur`, `blur`). Solo existe mientras la escena lo pide.
//!
//! Con desenfoques, el frame se pinta en una textura propia en lugar de la de la ventana. En cada desenfoque se
//! corta el pase: dos pasadas gaussianas (horizontal y vertical) sobre texturas de media resolución, limitadas al
//! rectángulo afectado, y el resultado se pega con la máscara del rectángulo redondeado. Al final, una copia a la
//! ventana. Un frame sin desenfoques no paga nada; las texturas se sueltan tras un rato sin usarlas.

use super::blur_plan::{IDLE_FRAMES, PARAMS_SIZE, Scissor, plan};
use super::blur_setup::{fullscreen, params_slots};
use crate::gpu::instance_buffer::InstanceBuffer;
use crate::gpu::pipelines::Pipelines;
use crate::gpu::scene::Scene;

struct Targets {
    size: (u32, u32),
    scene: wgpu::TextureView,
    low_a: wgpu::TextureView,
    low_b: wgpu::TextureView,
    scene_bind: wgpu::BindGroup,
    a_bind: wgpu::BindGroup,
    b_bind: wgpu::BindGroup,
}

pub(crate) struct BlurState {
    pass: wgpu::RenderPipeline,
    blit: wgpu::RenderPipeline,
    composite: wgpu::RenderPipeline,
    params_layout: wgpu::BindGroupLayout,
    params: wgpu::Buffer,
    params_bind: wgpu::BindGroup,
    params_capacity: usize,
    params_stride: u64,
    sampler: wgpu::Sampler,
    instances: InstanceBuffer,
    format: wgpu::TextureFormat,
    targets: Option<Targets>,
    scissors: Vec<(Scissor, Scissor)>,
    scratch: Vec<u8>,
    idle: u32,
}

impl BlurState {
    pub(crate) fn new(device: &wgpu::Device, pipelines: &Pipelines, format: wgpu::TextureFormat) -> Self {
        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("aura_blur_shader"),
            source: wgpu::ShaderSource::Wgsl(include_str!("blur.wgsl").into()),
        });
        let params_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("aura_blur_params_layout"),
            entries: &[wgpu::BindGroupLayoutEntry {
                binding: 0,
                visibility: wgpu::ShaderStages::FRAGMENT,
                ty: wgpu::BindingType::Buffer {
                    ty: wgpu::BufferBindingType::Uniform,
                    has_dynamic_offset: true,
                    min_binding_size: wgpu::BufferSize::new(PARAMS_SIZE),
                },
                count: None,
            }],
        });
        let layout = |label, groups: &[Option<&wgpu::BindGroupLayout>]| {
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some(label),
                bind_group_layouts: groups,
                immediate_size: 0,
            })
        };
        let pass_layout = layout("aura_blur_pass_layout", &[Some(&pipelines.atlas_layout), Some(&params_layout)]);
        let blit_layout = layout("aura_blit_layout", &[Some(&pipelines.atlas_layout)]);
        let composite = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("aura_blur_composite"),
            layout: Some(&pipelines.layout),
            vertex: wgpu::VertexState {
                module: &pipelines.shader,
                entry_point: Some("vs_blur"),
                compilation_options: Default::default(),
                buffers: &[],
            },
            fragment: Some(wgpu::FragmentState {
                module: &pipelines.shader,
                entry_point: Some("fs_blur"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format,
                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleStrip, ..Default::default() },
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });
        let params_stride =
            PARAMS_SIZE.next_multiple_of(u64::from(device.limits().min_uniform_buffer_offset_alignment));
        let (params, params_bind) = params_slots(device, &params_layout, params_stride, 2);
        Self {
            pass: fullscreen(device, &module, &pass_layout, format, "fs_blur_pass"),
            blit: fullscreen(device, &module, &blit_layout, format, "fs_blit"),
            composite,
            params_layout,
            params,
            params_bind,
            params_capacity: 2,
            params_stride,
            sampler: device.create_sampler(&wgpu::SamplerDescriptor {
                mag_filter: wgpu::FilterMode::Linear,
                min_filter: wgpu::FilterMode::Linear,
                ..Default::default()
            }),
            instances: InstanceBuffer::new(device, &pipelines.instances_layout, "aura_blurs"),
            format,
            targets: None,
            scissors: Vec::new(),
            scratch: Vec::new(),
            idle: 0,
        }
    }

    fn make_targets(&self, device: &wgpu::Device, pipelines: &Pipelines, size: (u32, u32)) -> Targets {
        let texture = |label, (width, height): (u32, u32)| {
            device
                .create_texture(&wgpu::TextureDescriptor {
                    label: Some(label),
                    size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
                    mip_level_count: 1,
                    sample_count: 1,
                    dimension: wgpu::TextureDimension::D2,
                    format: self.format,
                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
                    view_formats: &[],
                })
                .create_view(&wgpu::TextureViewDescriptor::default())
        };
        let low = (size.0.div_ceil(2).max(1), size.1.div_ceil(2).max(1));
        let (scene, low_a, low_b) =
            (texture("aura_blur_scene", size), texture("aura_blur_a", low), texture("aura_blur_b", low));
        let bind = |view: &wgpu::TextureView| {
            device.create_bind_group(&wgpu::BindGroupDescriptor {
                label: Some("aura_blur_bind"),
                layout: &pipelines.atlas_layout,
                entries: &[
                    wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(view) },
                    wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
                ],
            })
        };
        Targets { size, scene_bind: bind(&scene), a_bind: bind(&low_a), b_bind: bind(&low_b), scene, low_a, low_b }
    }

    /// Prepara el frame: texturas del tamaño de la ventana, instancias y los parámetros de cada pasada.
    pub(crate) fn prepare(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        pipelines: &Pipelines,
        scene: &Scene,
        size: (u32, u32),
        scale: f32,
    ) {
        self.idle = 0;
        if self.targets.as_ref().is_none_or(|targets| targets.size != size) {
            self.targets = Some(self.make_targets(device, pipelines, size));
        }
        self.instances.upload(device, queue, &pipelines.instances_layout, &scene.blurs);

        let slots = scene.blurs.len() * 2;
        if slots > self.params_capacity {
            self.params_capacity = slots.next_power_of_two();
            (self.params, self.params_bind) =
                params_slots(device, &self.params_layout, self.params_stride, self.params_capacity);
        }
        let low = (size.0.div_ceil(2).max(1), size.1.div_ceil(2).max(1));
        self.scissors.clear();
        self.scratch.clear();
        self.scratch.resize(slots * self.params_stride as usize, 0);
        for (index, blur) in scene.blurs.iter().enumerate() {
            let (first, second, params) = plan(blur, scale, low);
            self.scissors.push((first, second));
            for (pass, params) in params.iter().enumerate() {
                let start = (index * 2 + pass) * self.params_stride as usize;
                self.scratch[start..start + PARAMS_SIZE as usize].copy_from_slice(bytemuck::bytes_of(params));
            }
        }
        queue.write_buffer(&self.params, 0, &self.scratch);
    }

    /// Cuenta un frame sin desenfoques; pasado un rato se sueltan las texturas (y con ellas la VRAM).
    pub(crate) fn idle_frame(&mut self) {
        self.idle += 1;
        if self.idle > IDLE_FRAMES {
            self.targets = None;
        }
    }

    pub(crate) fn release(&mut self) {
        self.targets = None;
    }

    pub(crate) fn instances(&self) -> &InstanceBuffer {
        &self.instances
    }

    pub(crate) fn composite(&self) -> &wgpu::RenderPipeline {
        &self.composite
    }

    pub(crate) fn blurred_bind(&self) -> &wgpu::BindGroup {
        &self.targets.as_ref().expect("prepare crea las texturas").b_bind
    }

    pub(crate) fn scene_view(&self) -> &wgpu::TextureView {
        &self.targets.as_ref().expect("prepare crea las texturas").scene
    }

    /// Las dos pasadas gaussianas del desenfoque `index`: la escena → `low_a` (horizontal) → `low_b` (vertical).
    pub(crate) fn blur_passes(&self, encoder: &mut wgpu::CommandEncoder, index: usize) {
        let Some(targets) = &self.targets else { return };
        let (first, second) = self.scissors[index];
        for (pass, (scissor, source, destination)) in
            [(first, &targets.scene_bind, &targets.low_a), (second, &targets.a_bind, &targets.low_b)]
                .into_iter()
                .enumerate()
        {
            let Some([x, y, width, height]) = scissor else { continue };
            let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("aura_blur_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: destination,
                    depth_slice: None,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                ..Default::default()
            });
            render.set_pipeline(&self.pass);
            render.set_bind_group(0, source, &[]);
            render.set_bind_group(1, &self.params_bind, &[((index * 2 + pass) as u64 * self.params_stride) as u32]);
            render.set_scissor_rect(x, y, width, height);
            render.draw(0..3, 0..1);
        }
    }

    /// Copia la textura de la escena a `view` (la ventana).
    pub(crate) fn blit(&self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
        let Some(targets) = &self.targets else { return };
        let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("aura_blit"),
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                view,
                depth_slice: None,
                resolve_target: None,
                ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
            })],
            ..Default::default()
        });
        render.set_pipeline(&self.blit);
        render.set_bind_group(0, &targets.scene_bind, &[]);
        render.draw(0..3, 0..1);
    }
}