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.
//! Uniform buffer dinámico de transformaciones globales (`GlobalsSlots`) por escena.
//!
//! Un `Globals` por transformación de la escena, separados al alineamiento que exige la GPU.
//! Guarda un hash de 64 bits de los bytes subidos para omitir `queue.write_buffer` cuando
//! el tamaño de la ventana y las transformaciones no cambiaron respecto al frame anterior.

use crate::gpu::pipelines::GLOBALS_SIZE;
use crate::gpu::scene::Scene;
use bytemuck::{Pod, Zeroable};
use rustc_hash::FxHasher;
use std::hash::{Hash, Hasher};

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct Globals {
    viewport: [f32; 2],
    padding: [f32; 2],
    /// `a, b, c, d` de la transformación del lote.
    matrix: [f32; 4],
    /// `tx, ty`, factor de escala y relleno.
    offset: [f32; 4],
}

pub(crate) struct GlobalsSlots {
    buffer: wgpu::Buffer,
    bind_group: wgpu::BindGroup,
    capacity: usize,
    stride: u64,
    scratch: Vec<u8>,
    last_hash: Option<u64>,
}

fn hash_bytes(bytes: &[u8]) -> u64 {
    let mut hasher = FxHasher::default();
    bytes.hash(&mut hasher);
    hasher.finish()
}

impl GlobalsSlots {
    pub(crate) fn new(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, capacity: usize) -> Self {
        let stride = GLOBALS_SIZE.next_multiple_of(u64::from(device.limits().min_uniform_buffer_offset_alignment));
        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("aura_globals"),
            size: stride * capacity as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("aura_globals"),
            layout,
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
                    buffer: &buffer,
                    offset: 0,
                    size: wgpu::BufferSize::new(GLOBALS_SIZE),
                }),
            }],
        });
        Self { buffer, bind_group, capacity, stride, scratch: Vec::new(), last_hash: None }
    }

    pub(crate) fn bind_group(&self) -> &wgpu::BindGroup {
        &self.bind_group
    }

    pub(crate) fn stride(&self) -> u64 {
        self.stride
    }

    /// Escribe una ranura por transformación (la 0, la identidad, siempre existe) solo si cambió.
    pub(crate) fn upload(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        layout: &wgpu::BindGroupLayout,
        scene: &Scene,
        viewport: [f32; 2],
    ) {
        let slots = scene.transforms.len().max(1);
        if slots > self.capacity {
            *self = Self::new(device, layout, slots.next_power_of_two());
        }
        self.scratch.clear();
        self.scratch.resize(slots * self.stride as usize, 0);
        for slot in 0..slots {
            let transform = scene.transforms.get(slot).copied().unwrap_or_default();
            let globals = Globals {
                viewport,
                padding: [0.0; 2],
                matrix: [transform.a, transform.b, transform.c, transform.d],
                offset: [transform.tx, transform.ty, transform.scale_factor(), 0.0],
            };
            let start = slot * self.stride as usize;
            self.scratch[start..start + GLOBALS_SIZE as usize].copy_from_slice(bytemuck::bytes_of(&globals));
        }
        let hash = hash_bytes(&self.scratch);
        if self.last_hash == Some(hash) {
            return;
        }
        self.last_hash = Some(hash);
        queue.write_buffer(&self.buffer, 0, &self.scratch);
    }
}