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.
//! Lectura del frame desde la GPU: sirve para pruebas visuales sin capturar la pantalla.

use crate::core::Rgba;
use crate::gpu::context::Error;
use crate::gpu::renderer::GpuRenderer;
use crate::gpu::scene::Scene;
use std::sync::mpsc;

/// Píxeles RGBA8 de un frame capturado.
pub struct CapturedFrame {
    pub width: u32,
    pub height: u32,
    pub rgba: Vec<u8>,
}

impl GpuRenderer {
    /// Renderiza `scene` en una textura fuera de pantalla del tamaño de la ventana.
    pub fn capture(&mut self, scene: &Scene, scale: f32, clear: Rgba) -> Result<CapturedFrame, Error> {
        let (width, height, format) =
            (self.context.config.width, self.context.config.height, self.context.config.format);
        self.upload(scene, scale);

        let device = &self.context.device;
        let extent = wgpu::Extent3d { width, height, depth_or_array_layers: 1 };
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("aura_capture"),
            size: extent,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
            view_formats: &[],
        });
        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        // Las filas copiadas a un buffer deben estar alineadas a 256 bytes.
        let padded_row = (width * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("aura_capture_readback"),
            size: padded_row as u64 * height as u64,
            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
            mapped_at_creation: false,
        });

        let mut encoder = self.encode(&view, scene, clear);
        encoder.copy_texture_to_buffer(
            wgpu::TexelCopyTextureInfo {
                texture: &texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            wgpu::TexelCopyBufferInfo {
                buffer: &buffer,
                layout: wgpu::TexelCopyBufferLayout {
                    offset: 0,
                    bytes_per_row: Some(padded_row),
                    rows_per_image: Some(height),
                },
            },
            extent,
        );
        self.context.queue.submit(Some(encoder.finish()));

        let (sender, receiver) = mpsc::channel();
        buffer.map_async(wgpu::MapMode::Read, .., move |result| {
            // Solo falla si el receptor ya no existe, y entonces nadie espera el resultado.
            sender.send(result).ok();
        });
        self.context
            .device
            .poll(wgpu::PollType::Wait { submission_index: None, timeout: None })
            .map_err(|error| Error(error.to_string()))?;
        receiver.recv().map_err(|error| Error(error.to_string()))?.map_err(|error| Error(error.to_string()))?;

        let swap_red_blue = matches!(format, wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb);
        let mapped = buffer.slice(..).get_mapped_range().map_err(|error| Error(error.to_string()))?;
        let mut rgba = Vec::with_capacity((width * height * 4) as usize);
        for row in mapped.chunks(padded_row as usize) {
            for pixel in row[..(width * 4) as usize].as_chunks::<4>().0 {
                let [first, green, third, alpha] = *pixel;
                let (red, blue) = if swap_red_blue { (third, first) } else { (first, third) };
                rgba.extend_from_slice(&[red, green, blue, alpha]);
            }
        }
        drop(mapped);
        buffer.unmap();
        Ok(CapturedFrame { width, height, rgba })
    }
}