alpha2-render 0.5.0

Render graph and effect registry for alpha2.
Documentation
//! Native ports of the built-in passes: one kernel per built-in effect, transition and internal
//! pass, keyed by the identity and version of the [`EffectDef`] it ports.
//!
//! Kernels shade rows. [`KernelContext`] provides what the WGSL prelude (`registry.rs`
//! `build_shader`) gives a shader: bilinear clamp-to-edge inputs, parameters with defaults, and
//! runtime values.

use crate::cpu::image::{CpuImage, PixelBox};
use crate::{EffectDef, EffectParams, EffectRuntime, ParamValue, RenderSize};

mod effects;
mod generator;
mod internal;
mod prelude;
mod transitions;

#[allow(unused_imports)]
pub(crate) use prelude::*;

/// A 3D LUT stored as the GPU texture stores it (half floats rounded to nearest).
#[derive(Clone, Debug)]
pub(crate) struct CpuLut {
    pub(crate) edge: u32,
    pub(crate) domain_min: [f32; 3],
    pub(crate) domain_max: [f32; 3],
    pub(crate) values: Vec<[f32; 3]>,
}

/// What one pass reads; the WGSL prelude's bindings.
pub(crate) struct KernelContext<'a> {
    pub(crate) size: RenderSize,
    pub(crate) definition: &'a EffectDef,
    pub(crate) params: &'a EffectParams<'a>,
    pub(crate) runtime: EffectRuntime,
    pub(crate) max_filter_samples: u32,
    /// The effect source or transition `from`; transparent for a generator.
    pub(crate) source_a: &'a CpuImage,
    /// A transition's `to`; the source again for anything else.
    pub(crate) source_b: &'a CpuImage,
    /// Named inputs, by the names the definition declares.
    pub(crate) inputs: &'a [(&'a str, &'a CpuImage)],
    /// Resources, by the names the definition declares.
    pub(crate) resources: &'a [(&'a str, &'a CpuLut)],
}

impl KernelContext<'_> {
    /// Parameter `name` as supplied, else its default.
    pub(crate) fn param(&self, name: &str) -> ParamValue {
        if let Some(value) = self.params.get(name) {
            return value.clone();
        }
        self.definition
            .params
            .iter()
            .find(|spec| spec.name == name)
            .map_or(ParamValue::Float(0.0), |spec| spec.default.clone())
    }

    pub(crate) fn float(&self, name: &str) -> f32 {
        match self.param(name) {
            ParamValue::Float(value) => value,
            _ => 0.0,
        }
    }

    pub(crate) fn vec2(&self, name: &str) -> [f32; 2] {
        match self.param(name) {
            ParamValue::Vec2(value) => value,
            _ => [0.0; 2],
        }
    }

    /// A `vec4` or color parameter.
    pub(crate) fn vec4(&self, name: &str) -> [f32; 4] {
        match self.param(name) {
            ParamValue::Vec4(value) | ParamValue::Color(value) => value,
            _ => [0.0; 4],
        }
    }

    pub(crate) fn choice(&self, name: &str) -> u32 {
        match self.param(name) {
            ParamValue::Enum(value) => value,
            _ => 0,
        }
    }

    pub(crate) fn boolean(&self, name: &str) -> bool {
        matches!(self.param(name), ParamValue::Bool(true))
    }

    /// `resolution()`: the output size in pixels.
    #[allow(clippy::cast_precision_loss)]
    pub(crate) fn resolution(&self) -> [f32; 2] {
        [self.size.width as f32, self.size.height as f32]
    }

    /// `time_ticks()`: the time's ticks as two words, low first.
    #[allow(clippy::cast_possible_truncation)]
    pub(crate) fn time_ticks(&self) -> [u32; 2] {
        let ticks = self.runtime.time.ticks().cast_unsigned();
        [ticks as u32, (ticks >> 32) as u32]
    }

    /// `time()`: seconds rounded once from the exact quotient, as the runtime uniform holds it.
    #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
    pub(crate) fn time(&self) -> f32 {
        (self.runtime.time.ticks() as f64 / alpha2_time::TICKS_PER_SECOND as f64) as f32
    }

    pub(crate) const fn progress(&self) -> f32 {
        self.runtime.progress
    }

    pub(crate) const fn ratio(&self) -> f32 {
        self.runtime.ratio
    }

    /// `max_filter_samples()`.
    #[allow(clippy::cast_precision_loss)]
    pub(crate) const fn max_filter_samples(&self) -> f32 {
        self.max_filter_samples as f32
    }

    /// `sample_src(uv)`.
    #[inline]
    pub(crate) fn sample_src(&self, uv: [f32; 2]) -> [f32; 4] {
        self.source_a.sample(uv[0], uv[1])
    }

    /// `from_color(uv)`.
    #[inline]
    #[allow(clippy::wrong_self_convention)]
    pub(crate) fn from_color(&self, uv: [f32; 2]) -> [f32; 4] {
        self.source_a.sample(uv[0], uv[1])
    }

    /// `to_color(uv)`.
    #[inline]
    pub(crate) fn to_color(&self, uv: [f32; 2]) -> [f32; 4] {
        self.source_b.sample(uv[0], uv[1])
    }

    /// The lookup table bound as `name`.
    pub(crate) fn lut(&self, name: &str) -> Option<&CpuLut> {
        self.resources
            .iter()
            .find(|(resource, _)| *resource == name)
            .map(|(_, lut)| *lut)
    }
}

/// One built-in pass in native code.
pub(crate) trait CpuKernel: Sync + Send {
    /// Pixels the pass can make non-transparent; the whole picture unless a kernel knows better.
    fn bounds(&self, context: &KernelContext<'_>) -> PixelBox {
        PixelBox::whole(context.size)
    }

    /// Shades pixels `x0..x0 + out.len()` of row `y` into `out`, before storage.
    fn row(&self, context: &KernelContext<'_>, y: u32, x0: u32, out: &mut [[f32; 4]]);
}

/// The kernel that draws the built-in `id` at `version`, if there is one.
pub(crate) fn kernel(id: &str, version: u32) -> Option<&'static dyn CpuKernel> {
    if version != 1 {
        return None;
    }
    effects::kernel(id)
        .or_else(|| transitions::kernel(id))
        .or_else(|| internal::kernel(id))
        .or_else(|| generator::kernel(id))
}