cranpose 0.1.164

Cranpose runtime and UI facade
Documentation
use std::{sync::mpsc, time::Duration};

use crate::{embed::EmbedError, embed_protocol::PixelRect};

pub(crate) const FRAME_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm;

const BYTES_PER_PIXEL: u32 = 4;
const READBACK_TIMEOUT: Duration = Duration::from_secs(5);

pub(crate) struct FrameTarget {
    texture: wgpu::Texture,
    view: wgpu::TextureView,
    readback: wgpu::Buffer,
    width: u32,
    height: u32,
    padded_row_bytes: u32,
}

impl FrameTarget {
    pub(crate) fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Embed Frame Texture"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: FRAME_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
            view_formats: &[],
        });
        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        let padded_row_bytes = padded_row_bytes(width);
        let readback = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Embed Frame Readback"),
            size: u64::from(padded_row_bytes) * u64::from(height),
            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
            mapped_at_creation: false,
        });
        Self {
            texture,
            view,
            readback,
            width,
            height,
            padded_row_bytes,
        }
    }

    pub(crate) fn size(&self) -> (u32, u32) {
        (self.width, self.height)
    }

    pub(crate) fn texture(&self) -> &wgpu::Texture {
        &self.texture
    }

    pub(crate) fn view(&self) -> &wgpu::TextureView {
        &self.view
    }

    pub(crate) fn read_into(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        pixels: &mut Vec<u8>,
    ) -> Result<(), EmbedError> {
        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Embed Frame Readback Encoder"),
        });
        encoder.copy_texture_to_buffer(
            wgpu::TexelCopyTextureInfo {
                texture: &self.texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            wgpu::TexelCopyBufferInfo {
                buffer: &self.readback,
                layout: wgpu::TexelCopyBufferLayout {
                    offset: 0,
                    bytes_per_row: Some(self.padded_row_bytes),
                    rows_per_image: Some(self.height),
                },
            },
            wgpu::Extent3d {
                width: self.width,
                height: self.height,
                depth_or_array_layers: 1,
            },
        );
        let submission = queue.submit(std::iter::once(encoder.finish()));

        let slice = self.readback.slice(..);
        let (sender, receiver) = mpsc::channel();
        slice.map_async(wgpu::MapMode::Read, move |result| {
            let _ = sender.send(result);
        });
        device
            .poll(wgpu::PollType::Wait {
                submission_index: Some(submission),
                timeout: Some(READBACK_TIMEOUT),
            })
            .map_err(|error| EmbedError::Readback(error.to_string()))?;
        receiver
            .recv_timeout(READBACK_TIMEOUT)
            .map_err(|error| EmbedError::Readback(error.to_string()))?
            .map_err(|error| EmbedError::Readback(error.to_string()))?;

        let mapped = slice
            .get_mapped_range()
            .map_err(|error| EmbedError::Readback(error.to_string()))?;
        copy_unpadded_rows(
            &mapped,
            self.padded_row_bytes as usize,
            (self.width * BYTES_PER_PIXEL) as usize,
            pixels,
        );
        drop(mapped);
        self.readback.unmap();
        Ok(())
    }
}

pub(crate) fn padded_row_bytes(width: u32) -> u32 {
    (width * BYTES_PER_PIXEL).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
}

pub(crate) fn copy_unpadded_rows(
    padded: &[u8],
    padded_row_bytes: usize,
    row_bytes: usize,
    pixels: &mut Vec<u8>,
) {
    pixels.clear();
    pixels.extend(
        padded
            .chunks(padded_row_bytes)
            .flat_map(|row| &row[..row_bytes.min(row.len())])
            .copied(),
    );
}

pub(crate) fn changed_rect(
    previous: &[u8],
    next: &[u8],
    width: u32,
    height: u32,
) -> Option<PixelRect> {
    let row_bytes = (width * BYTES_PER_PIXEL) as usize;
    if row_bytes == 0 || previous.len() != next.len() {
        return Some(PixelRect::full(width, height));
    }
    let mut bounds: Option<(u32, u32, u32, u32)> = None;
    let rows = previous
        .chunks_exact(row_bytes)
        .zip(next.chunks_exact(row_bytes));
    for (row, (before, after)) in rows.enumerate() {
        if before == after {
            continue;
        }
        let Some((first, last)) = changed_columns(before, after) else {
            continue;
        };
        let row = row as u32;
        bounds = Some(match bounds {
            None => (first, last, row, row),
            Some((first_column, last_column, first_row, _)) => (
                first_column.min(first),
                last_column.max(last),
                first_row,
                row,
            ),
        });
    }
    bounds.map(
        |(first_column, last_column, first_row, last_row)| PixelRect {
            x: first_column,
            y: first_row,
            width: last_column - first_column + 1,
            height: last_row - first_row + 1,
        },
    )
}

fn changed_columns(before: &[u8], after: &[u8]) -> Option<(u32, u32)> {
    let pixel_size = BYTES_PER_PIXEL as usize;
    let pixels = || {
        before
            .chunks_exact(pixel_size)
            .zip(after.chunks_exact(pixel_size))
    };
    let first = pixels().position(|(a, b)| a != b)?;
    let last = pixels().rposition(|(a, b)| a != b)?;
    Some((first as u32, last as u32))
}

#[cfg(test)]
#[path = "tests/embed_frame_tests.rs"]
mod tests;