vello_gpu 0.3.0

A GPU renderer for Vello with CPU-side preprocessing.
Documentation
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! GPU instance construction for non-default layer blending.

use crate::copy::GpuCopyInstance;
use crate::schedule::round::BlendOp;
use crate::target::TextureParity;
use crate::util::{pack_opacity, pack_u16_pair};
use bytemuck::{Pod, Zeroable};
use vello_common::geometry::{RectU16, SizeU16};
use vello_common::peniko::{Compose, Mix};

/// Per-instance data for one blend pass.
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
pub(crate) struct GpuBlendInstance {
    /// Atlas-space geometry origin, packed as `u16x2`.
    pub(crate) geometry_origin: u32,
    /// Geometry width in the low 16 bits and height in the high 16 bits.
    pub(crate) geometry_size: u32,
    /// Alpha texture column index for strip geometry.
    pub(crate) geometry_alpha_col_idx: u32,
    /// Width and height of the parent layer texture, packed as `u16x2`.
    pub(crate) parent_texture_size: u32,
    /// Atlas-space origin of the layer in the child layer texture, packed as `u16x2`.
    pub(crate) child_texture_origin: u32,
    /// Origin of the child layer expressed in parent texture coordinates, packed as `u16x2`.
    pub(crate) child_parent_origin: u32,
    /// Scene-space width and height of the sampled child layer, packed as `u16x2`.
    pub(crate) child_rect_size: u32,
    /// Packed blend mode, opacity, parent/child texture indices, and alpha-presence flag.
    pub(crate) blend_config: u32,
}

impl GpuBlendInstance {
    pub(crate) fn new(
        blend: &BlendOp,
        clip_strip: Option<BlendStrip>,
        parent_texture_size: SizeU16,
    ) -> Self {
        let parent_rect = blend.parent_region.texture_rect(blend.blend_bbox);
        let child_parent_rect = blend
            .parent_region
            .texture_rect(blend.child_region.layer_bbox);
        let geometry = clip_strip.map_or_else(
            || BlendGeometry {
                rect: parent_rect,
                alpha_col_idx: None,
            },
            |strip| BlendGeometry {
                rect: strip.rect,
                alpha_col_idx: strip.alpha_col_idx,
            },
        );

        Self {
            geometry_origin: pack_u16_pair(geometry.rect.x0, geometry.rect.y0),
            geometry_size: pack_u16_pair(geometry.rect.width(), geometry.rect.height()),
            geometry_alpha_col_idx: geometry.alpha_col_idx.unwrap_or(0),
            parent_texture_size: pack_u16_pair(
                parent_texture_size.width(),
                parent_texture_size.height(),
            ),
            child_texture_origin: pack_u16_pair(
                blend.child_region.texture.rect.x0,
                blend.child_region.texture.rect.y0,
            ),
            child_parent_origin: pack_u16_pair(child_parent_rect.x0, child_parent_rect.y0),
            child_rect_size: pack_u16_pair(
                blend.child_region.layer_bbox.width(),
                blend.child_region.layer_bbox.height(),
            ),
            blend_config: pack_blend_config(
                blend.blend_mode.mix,
                blend.blend_mode.compose,
                blend.opacity,
                blend.parent_region.texture.target.texture_parity,
                blend.child_region.texture.target.texture_parity,
                geometry.alpha_col_idx.is_some(),
            ),
        }
    }

    pub(crate) fn copy_from_scratch(self) -> GpuCopyInstance {
        GpuCopyInstance {
            // Remember that for blending, the scratch texture
            // exactly mirrors the allocations of the blend target texture, so they are
            // the same.
            dest_texture_origin: self.geometry_origin,
            source_texture_origin: self.geometry_origin,
            copy_rect_size: self.geometry_size,
            dest_texture_size: self.parent_texture_size,
        }
    }
}

/// A strip for performing a clipped blend operation.
#[derive(Debug, Copy, Clone)]
pub(crate) struct BlendStrip {
    /// Atlas-space bounds of the segment.
    rect: RectU16,
    /// Alpha texture column index, or `None` for a plain fill segment.
    alpha_col_idx: Option<u32>,
}

impl BlendStrip {
    pub(crate) fn from_fill_segment(rect: RectU16, alpha_col_idx: Option<u32>) -> Self {
        Self {
            rect,
            alpha_col_idx,
        }
    }
}

/// Geometry shared while constructing a [`GpuBlendInstance`].
///
/// Either represents a simple rectangle, or a strip with potential alpha, in case the blend layer
/// has clipping.
#[derive(Debug, Copy, Clone)]
struct BlendGeometry {
    /// Atlas-space bounds of the geometry.
    rect: RectU16,
    /// Alpha texture column used by clipped strip geometry, if present.
    alpha_col_idx: Option<u32>,
}

fn pack_blend_config(
    mix: Mix,
    compose: Compose,
    opacity: f32,
    parent_texture_parity: TextureParity,
    child_texture_parity: TextureParity,
    has_alpha: bool,
) -> u32 {
    pack_compose(compose)
        | (pack_mix(mix) << 8)
        | (u32::from(pack_opacity(opacity)) << 16)
        | (u32::from(parent_texture_parity) << 24)
        | (u32::from(child_texture_parity) << 25)
        | u32::from(has_alpha) << 26
}

fn pack_mix(mix: Mix) -> u32 {
    match mix {
        Mix::Normal => 0,
        Mix::Multiply => 1,
        Mix::Screen => 2,
        Mix::Overlay => 3,
        Mix::Darken => 4,
        Mix::Lighten => 5,
        Mix::ColorDodge => 6,
        Mix::ColorBurn => 7,
        Mix::HardLight => 8,
        Mix::SoftLight => 9,
        Mix::Difference => 10,
        Mix::Exclusion => 11,
        Mix::Hue => 12,
        Mix::Saturation => 13,
        Mix::Color => 14,
        Mix::Luminosity => 15,
    }
}

fn pack_compose(compose: Compose) -> u32 {
    match compose {
        Compose::Clear => 0,
        Compose::Copy => 1,
        Compose::Dest => 2,
        Compose::SrcOver => 3,
        Compose::DestOver => 4,
        Compose::SrcIn => 5,
        Compose::DestIn => 6,
        Compose::SrcOut => 7,
        Compose::DestOut => 8,
        Compose::SrcAtop => 9,
        Compose::DestAtop => 10,
        Compose::Xor => 11,
        Compose::Plus => 12,
        Compose::PlusLighter => 13,
    }
}