darkly 0.5.0

A GPU-native paint engine on wgpu: brushes, layers, blend modes, masks, selections, and undo.
Documentation
// User-facing "Painting" veil. The underlying algorithm is the
// generalized Kuwahara filter — see shader header for prior-art credit.

use crate::gpu::effect::{EffectCache, EffectPipeline};
use crate::gpu::veil::{ParamDef, ParamValue, Veil, VeilRegistration};
use std::sync::Arc;

const PARAMS: &[ParamDef] = &[
    ParamDef::Int {
        name: "kernel_size",
        min: 1,
        max: 7,
        default: 6,
    },
    ParamDef::Float {
        name: "sharpness",
        min: 1.0,
        max: 18.0,
        default: 8.0,
    },
    ParamDef::Float {
        name: "hardness",
        min: 1.0,
        max: 200.0,
        default: 100.0,
    },
];

pub fn register() -> VeilRegistration {
    VeilRegistration {
        type_id: "painting",
        display_name: "Painting",
        params: PARAMS,
        create_pipeline: create_painting_pipeline,
        from_params: |params, shared| {
            let kernel_size = match params.first() {
                Some(ParamValue::Int(v)) => *v,
                _ => 6,
            };
            let sharpness = match params.get(1) {
                Some(ParamValue::Float(v)) => *v,
                _ => 8.0,
            };
            let hardness = match params.get(2) {
                Some(ParamValue::Float(v)) => *v,
                _ => 100.0,
            };
            Box::new(Painting::new(kernel_size, sharpness, hardness, shared))
        },
    }
}

/// GPU uniforms for the Painting shader.
/// Layout must match the WGSL `Params` struct exactly.
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct PaintingUniforms {
    kernel_size: i32,
    sharpness: f32,
    hardness: f32,
    _pad: f32,
    resolution_x: f32,
    resolution_y: f32,
}

#[derive(Clone, Debug)]
pub struct Painting {
    pub kernel_size: i32,
    pub sharpness: f32,
    pub hardness: f32,
    shared: Arc<EffectPipeline>,
}

impl Painting {
    pub fn new(
        kernel_size: i32,
        sharpness: f32,
        hardness: f32,
        shared: Arc<EffectPipeline>,
    ) -> Self {
        Painting {
            kernel_size: kernel_size.max(1),
            sharpness,
            hardness,
            shared,
        }
    }
}

impl Veil for Painting {
    fn type_id(&self) -> &'static str {
        "painting"
    }

    fn clone_boxed(&self) -> Box<dyn Veil> {
        Box::new(self.clone())
    }

    fn perf_scale_factor(&self) -> f32 {
        // O(kernel²) samples per pixel — at default kernel_size=6 that's 169
        // texture taps. Painterly output is inherently smooth/blurry, so the
        // bilinear upscale is visually free.
        0.7
    }

    fn param_values(&self) -> Vec<ParamValue> {
        vec![
            ParamValue::Int(self.kernel_size),
            ParamValue::Float(self.sharpness),
            ParamValue::Float(self.hardness),
        ]
    }

    fn create_cache(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        ping_pong_views: &[wgpu::TextureView; 2],
        sampler: &wgpu::Sampler,
        render_width: u32,
        render_height: u32,
    ) -> EffectCache {
        let uniforms = PaintingUniforms {
            kernel_size: self.kernel_size,
            sharpness: self.sharpness,
            hardness: self.hardness,
            _pad: 0.0,
            resolution_x: render_width as f32,
            resolution_y: render_height as f32,
        };
        let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("painting-uniforms"),
            size: std::mem::size_of::<PaintingUniforms>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        queue.write_buffer(&uniform_buf, 0, bytemuck::bytes_of(&uniforms));

        let layout = &self.shared.bind_group_layout;
        let bind_groups: [wgpu::BindGroup; 2] = std::array::from_fn(|i| {
            device.create_bind_group(&wgpu::BindGroupDescriptor {
                label: Some(&format!("painting-bg-{i}")),
                layout,
                entries: &[
                    wgpu::BindGroupEntry {
                        binding: 0,
                        resource: wgpu::BindingResource::TextureView(&ping_pong_views[i]),
                    },
                    wgpu::BindGroupEntry {
                        binding: 1,
                        resource: wgpu::BindingResource::Sampler(sampler),
                    },
                    wgpu::BindGroupEntry {
                        binding: 2,
                        resource: uniform_buf.as_entire_binding(),
                    },
                ],
            })
        });

        EffectCache {
            uniform_bufs: vec![uniform_buf],
            bind_groups: vec![bind_groups],
            aux_textures: vec![],
            aux_views: vec![],
            aux_pipelines: vec![],
        }
    }

    fn encode(
        &self,
        encoder: &mut wgpu::CommandEncoder,
        cache: &EffectCache,
        src_idx: usize,
        dst_view: &wgpu::TextureView,
    ) {
        let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("painting"),
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                view: dst_view,
                resolve_target: None,
                depth_slice: None,
                ops: wgpu::Operations {
                    load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                    store: wgpu::StoreOp::Store,
                },
            })],
            ..Default::default()
        });
        rpass.set_pipeline(&self.shared.pipeline);
        rpass.set_bind_group(0, &cache.bind_groups[0][src_idx], &[]);
        rpass.draw(0..3, 0..1);
    }
}

fn create_painting_pipeline(device: &wgpu::Device, _format: wgpu::TextureFormat) -> EffectPipeline {
    let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
        label: Some("painting-bgl"),
        entries: &[
            wgpu::BindGroupLayoutEntry {
                binding: 0,
                visibility: wgpu::ShaderStages::FRAGMENT,
                ty: wgpu::BindingType::Texture {
                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
                    view_dimension: wgpu::TextureViewDimension::D2,
                    multisampled: false,
                },
                count: None,
            },
            wgpu::BindGroupLayoutEntry {
                binding: 1,
                visibility: wgpu::ShaderStages::FRAGMENT,
                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                count: None,
            },
            wgpu::BindGroupLayoutEntry {
                binding: 2,
                visibility: wgpu::ShaderStages::FRAGMENT,
                ty: wgpu::BindingType::Buffer {
                    ty: wgpu::BufferBindingType::Uniform,
                    has_dynamic_offset: false,
                    min_binding_size: None,
                },
                count: None,
            },
        ],
    });

    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
        label: Some("painting-pipeline-layout"),
        bind_group_layouts: &[Some(&bind_group_layout)],
        immediate_size: 0,
    });

    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
        label: Some("painting-shader"),
        source: wgpu::ShaderSource::Wgsl(
            include_str!("../../../shaders/veils/painting.wgsl").into(),
        ),
    });

    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
        label: Some("painting-pipeline"),
        layout: Some(&pipeline_layout),
        vertex: wgpu::VertexState {
            module: &shader,
            entry_point: Some("vs_main"),
            buffers: &[],
            compilation_options: Default::default(),
        },
        fragment: Some(wgpu::FragmentState {
            module: &shader,
            entry_point: Some("fs_painting"),
            targets: &[Some(wgpu::ColorTargetState {
                format: wgpu::TextureFormat::Rgba8Unorm,
                blend: None,
                write_mask: wgpu::ColorWrites::ALL,
            })],
            compilation_options: Default::default(),
        }),
        primitive: wgpu::PrimitiveState {
            topology: wgpu::PrimitiveTopology::TriangleList,
            ..Default::default()
        },
        depth_stencil: None,
        multisample: wgpu::MultisampleState::default(),
        multiview_mask: None,
        cache: None,
    });

    EffectPipeline {
        pipeline,
        bind_group_layout,
    }
}