lumen-engine 0.2.1

Core composition model and rendering orchestration for Lumen.
Documentation
use bytemuck::{Pod, Zeroable};

use crate::{error::RenderError, node::NodeId};

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct ColorParams {
    pub(crate) color: [f32; 4],
}

impl ColorParams {
    pub(crate) fn from_rgba8(color: [u8; 4]) -> Self {
        Self {
            color: [
                f32::from(color[0]) / 255.0,
                f32::from(color[1]) / 255.0,
                f32::from(color[2]) / 255.0,
                f32::from(color[3]) / 255.0,
            ],
        }
    }
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct AlphaPremultiplyParams {
    pub(crate) values: [f32; 4],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct ChannelShuffleParams {
    pub(crate) selector_indices: [f32; 4],
    pub(crate) selector_values: [f32; 4],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct ColorGradeParams {
    pub(crate) strength: f32,
    pub(crate) interpolation: u32,
    pub(crate) _pad: [u32; 2],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct ColorGradeLut {
    stops: [[f32; 4]; LUT_TABLE_SIZE],
}

impl ColorGradeLut {
    pub(crate) fn parse(node_id: NodeId, frame: u32, source: &str) -> crate::Result<Self> {
        let stops = parse_lut_stops(node_id, frame, source)?;
        let mut table = [[0.0; 4]; LUT_TABLE_SIZE];
        for (index, entry) in table.iter_mut().enumerate() {
            let value = index as f32 / (LUT_TABLE_SIZE - 1) as f32;
            let scaled = value * (stops.len() - 1) as f32;
            let low = scaled.floor() as usize;
            let high = (low + 1).min(stops.len() - 1);
            let t = scaled - low as f32;
            *entry = [
                stops[low][0] + (stops[high][0] - stops[low][0]) * t,
                stops[low][1] + (stops[high][1] - stops[low][1]) * t,
                stops[low][2] + (stops[high][2] - stops[low][2]) * t,
                1.0,
            ];
        }
        Ok(Self { stops: table })
    }
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct ExposureParams {
    pub(crate) exposure: f32,
    pub(crate) contrast: f32,
    pub(crate) offset: f32,
    pub(crate) _pad: f32,
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct HueSaturationParams {
    pub(crate) hue_offset: f32,
    pub(crate) saturation: f32,
    pub(crate) lightness: f32,
    pub(crate) _pad: f32,
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct LevelsParams {
    pub(crate) black_point: f32,
    pub(crate) white_point: f32,
    pub(crate) gamma: f32,
    pub(crate) output_black: f32,
    pub(crate) output_white: f32,
    pub(crate) _pad: [f32; 3],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct BlurParams {
    pub(crate) values: [u32; 4],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct CurvesParams {
    pub(crate) values: [f32; 4],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct CurvesTable {
    entries: [[f32; 4]; LUT_TABLE_SIZE],
}

impl CurvesTable {
    pub(crate) fn parse(node_id: NodeId, frame: u32, source: &str) -> crate::Result<Self> {
        let stops = parse_lut_stops(node_id, frame, source)?;
        let mut entries = [[0.0; 4]; LUT_TABLE_SIZE];
        for (index, entry) in entries.iter_mut().enumerate() {
            let value = index as f32 / (LUT_TABLE_SIZE - 1) as f32;
            let scaled = value * (stops.len() - 1) as f32;
            let low = scaled.floor() as usize;
            let high = (low + 1).min(stops.len() - 1);
            let t = scaled - low as f32;
            *entry = [
                stops[low][0] + (stops[high][0] - stops[low][0]) * t,
                stops[low][1] + (stops[high][1] - stops[low][1]) * t,
                stops[low][2] + (stops[high][2] - stops[low][2]) * t,
                1.0,
            ];
        }
        Ok(Self { entries })
    }
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct ShadowParams {
    pub(crate) color: [f32; 4],
    pub(crate) values: [f32; 4],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct WgslShaderParams {
    pub(crate) values: [f32; 4],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct MergeParams {
    pub(crate) opacity: f32,
    pub(crate) blend_mode: u32,
    pub(crate) has_mask: u32,
    pub(crate) _pad: u32,
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct BooleanParams {
    pub(crate) values: [f32; 4],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct RasterMultiMergeParams {
    pub(crate) values: [f32; 4],
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct ChannelSelector {
    pub(crate) index: f32,
    pub(crate) value: f32,
}

const LUT_TABLE_SIZE: usize = 256;

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct TransformParams {
    pub(crate) scale: [f32; 2],
    pub(crate) translate: [f32; 2],
    pub(crate) pivot: [f32; 2],
    pub(crate) rotate_radians: f32,
    pub(crate) sampling: u32,
    pub(crate) _pad: [u32; 4],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct CropParams {
    pub(crate) origin: [i32; 2],
    pub(crate) size: [u32; 2],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub(crate) struct ResizeParams {
    pub(crate) size: [u32; 2],
    pub(crate) mode: u32,
    pub(crate) sampling: u32,
}

pub(crate) fn dispatch_for(size: lumen_gpu::Size) -> lumen_gpu::Dispatch {
    lumen_gpu::Dispatch {
        x: size.width.div_ceil(8),
        y: size.height.div_ceil(8),
        z: 1,
    }
}

pub(crate) fn spatial_bindings(
    input: lumen_gpu::TextureId,
    params: lumen_gpu::BufferId,
    output: lumen_gpu::TextureId,
) -> Vec<lumen_gpu::Binding> {
    vec![
        lumen_gpu::Binding::sampled_texture(0, 0, input),
        lumen_gpu::Binding::uniform(0, 1, params),
        lumen_gpu::Binding::storage_texture(0, 2, output),
    ]
}

pub(crate) fn alpha_operation(node_id: NodeId, mode: &str) -> crate::Result<f32> {
    match mode.trim().to_ascii_lowercase().as_str() {
        "premultiply" | "premul" | "multiply" => Ok(0.0),
        "unpremultiply" | "unpremul" | "straight" | "unmultiply" => Ok(1.0),
        _ => Err(crate::error::PropertyError::InvalidType {
            node_id,
            property_path: "mode".to_string(),
            expected: "`premultiply` or `unpremultiply`",
            actual: "String",
        }
        .into()),
    }
}

pub(crate) fn channel_selector(
    node_id: NodeId,
    property_path: &str,
    spec: &str,
) -> crate::Result<ChannelSelector> {
    let normalized = spec.trim().to_ascii_lowercase();
    match normalized.as_str() {
        "r" | "red" => Ok(ChannelSelector {
            index: 0.0,
            value: 0.0,
        }),
        "g" | "green" => Ok(ChannelSelector {
            index: 1.0,
            value: 0.0,
        }),
        "b" | "blue" => Ok(ChannelSelector {
            index: 2.0,
            value: 0.0,
        }),
        "a" | "alpha" => Ok(ChannelSelector {
            index: 3.0,
            value: 0.0,
        }),
        "zero" => Ok(ChannelSelector {
            index: 4.0,
            value: 0.0,
        }),
        "one" => Ok(ChannelSelector {
            index: 4.0,
            value: 1.0,
        }),
        _ => {
            let value = normalized.parse::<f32>().map_err(|_| {
                crate::error::PropertyError::InvalidType {
                    node_id,
                    property_path: property_path.to_string(),
                    expected: "channel name or numeric constant",
                    actual: "String",
                }
            })?;
            Ok(ChannelSelector {
                index: 4.0,
                value: if value <= 1.0 {
                    value.clamp(0.0, 1.0)
                } else {
                    (value / 255.0).clamp(0.0, 1.0)
                },
            })
        }
    }
}

fn parse_lut_stops(node_id: NodeId, frame: u32, source: &str) -> crate::Result<Vec<[f32; 3]>> {
    let source = source.trim();
    if source.is_empty()
        || source.eq_ignore_ascii_case(crate::node::processing::color_grade::IDENTITY_LUT)
    {
        return Ok(vec![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]);
    }

    let source = source
        .strip_prefix("rgb1d")
        .and_then(|rest| rest.strip_prefix(':'))
        .unwrap_or(source);
    let mut stops = Vec::new();
    for triplet in source.split(';') {
        let triplet = triplet.trim();
        if triplet.is_empty() {
            continue;
        }
        let components = triplet
            .split([',', ' ', '\t'])
            .filter(|part| !part.is_empty())
            .map(str::parse::<f32>)
            .collect::<Result<Vec<_>, _>>()
            .map_err(|_| lut_error(node_id, frame, "LUT contains a non-numeric component"))?;
        if components.len() != 3 {
            return Err(lut_error(
                node_id,
                frame,
                format!("LUT triplet `{triplet}` must contain exactly three RGB components"),
            ));
        }
        stops.push([
            normalize_lut_component(components[0]),
            normalize_lut_component(components[1]),
            normalize_lut_component(components[2]),
        ]);
    }
    if stops.len() < 2 {
        return Err(lut_error(
            node_id,
            frame,
            "LUT must contain at least two RGB triplets",
        ));
    }
    Ok(stops)
}

fn normalize_lut_component(value: f32) -> f32 {
    if value > 1.0 {
        (value / 255.0).clamp(0.0, 1.0)
    } else {
        value.clamp(0.0, 1.0)
    }
}

fn lut_error(node_id: NodeId, frame: u32, details: impl Into<String>) -> crate::error::LumenError {
    RenderError::NodeEvaluation {
        frame,
        node_id,
        node_kind: "ColorGrade",
        details: details.into(),
    }
    .into()
}

pub(crate) fn copyable_texture_desc(size: lumen_gpu::Size) -> lumen_gpu::TextureDesc {
    lumen_gpu::TextureDesc {
        domain: lumen_gpu::TextureDomain::full_frame(size),
        format: lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
        usage: lumen_gpu::wgpu::TextureUsages::COPY_DST
            | lumen_gpu::wgpu::TextureUsages::COPY_SRC
            | lumen_gpu::wgpu::TextureUsages::TEXTURE_BINDING
            | lumen_gpu::wgpu::TextureUsages::STORAGE_BINDING
            | lumen_gpu::wgpu::TextureUsages::RENDER_ATTACHMENT,
    }
}