Browse Source

Basic operations implemented with new dispatch system.

Kestrel 5 months ago
parent
commit
c1ef8254d5
13 changed files with 669 additions and 197 deletions
  1. 41 3
      src/bitmap.rs
  2. 29 1
      src/colour.rs
  3. 0 162
      src/dispatch.rs
  4. 3 13
      src/lib.rs
  5. 71 0
      src/math.rs
  6. 76 5
      src/op.rs
  7. 59 0
      src/op/composite.rs
  8. 54 0
      src/op/copy_from.rs
  9. 205 0
      src/op/dispatch.rs
  10. 21 11
      src/op/fill.rs
  11. 31 0
      src/op/fill_region.rs
  12. 77 0
      src/op/fill_region_masked.rs
  13. 2 2
      tests/common/mod.rs

+ 41 - 3
src/bitmap.rs

@@ -1,16 +1,42 @@
-use crate::{formats::PixelFormatSpec, math};
+use crate::{
+    formats::PixelFormatSpec,
+    math::{self, PixelBox, PixelSize},
+};
 
 pub trait BitmapData {
     fn data(&self) -> &[u8];
     fn width(&self) -> usize;
     fn height(&self) -> usize;
     fn stride(&self) -> usize;
+
+    fn size(&self) -> PixelSize {
+        PixelSize::new(self.width() as i32, self.height() as i32)
+    }
 }
 
 pub trait BitmapDataMut: BitmapData {
     fn data_mut(&mut self) -> &mut [u8];
 }
 
+pub trait MakeMutView {
+    fn make_view_mut<Format: PixelFormatSpec>(&mut self, area: PixelBox) -> BitmapMut<Format>;
+}
+
+// impl<T: BitmapDataMut> MakeMutView for T { }
+impl<'l> MakeMutView for &'l mut dyn BitmapDataMut {
+    fn make_view_mut<Format: PixelFormatSpec>(&mut self, area: PixelBox) -> BitmapMut<Format> {
+        let stride = self.stride();
+        let data_offset =
+            (area.min.y as usize * stride) + area.min.x as usize * Format::PIXEL_WIDTH;
+        BitmapMut::new_with_stride(
+            &mut self.data_mut()[data_offset..],
+            area.width() as usize,
+            area.height() as usize,
+            stride,
+        )
+    }
+}
+
 #[derive(PartialEq, Clone)]
 pub struct OwnedBitmapData {
     pub(crate) data: Vec<u8>,
@@ -101,6 +127,17 @@ pub trait BitmapAccess<Format: PixelFormatSpec> {
     }
 
     fn row_stride(&self) -> usize;
+
+    fn make_view(&self, area: PixelBox) -> BitmapRef<Format> {
+        let data_offset = (area.min.y as usize * self.data().stride())
+            + area.min.x as usize * Format::PIXEL_WIDTH;
+        BitmapRef::new_with_stride(
+            &self.data().data()[data_offset..],
+            area.width() as usize,
+            area.height() as usize,
+            self.data().stride(),
+        )
+    }
 }
 
 pub trait BitmapMutAccess<Format: PixelFormatSpec>: BitmapAccess<Format> {
@@ -171,6 +208,9 @@ impl<'l, Format: PixelFormatSpec, T: BitmapAccess<Format>> BitmapAccess<Format>
     fn row_stride(&self) -> usize {
         (*self).row_stride()
     }
+    fn make_view(&self, area: PixelBox) -> BitmapRef<Format> {
+        (*self).make_view(area)
+    }
 }
 
 impl<Format: PixelFormatSpec> BitmapMutAccess<Format> for Bitmap<Format> {
@@ -189,7 +229,6 @@ impl<Format: PixelFormatSpec> std::fmt::Debug for Bitmap<Format> {
     }
 }
 
-
 /// Borrowed bitmap with static pixel format information.
 #[derive(Clone)]
 pub struct BitmapRef<'l, Format: PixelFormatSpec> {
@@ -272,7 +311,6 @@ impl<'l, Format: PixelFormatSpec> BitmapMut<'l, Format> {
             _ghost: Default::default(),
         }
     }
-    
 }
 
 impl<'l, Format: PixelFormatSpec> BitmapAccess<Format> for BitmapMut<'l, Format> {

+ 29 - 1
src/colour.rs

@@ -1,4 +1,4 @@
-use crate::formats::PixelFormatSpec;
+use crate::formats::{PackedLocation, PixelFormatSpec};
 
 /// Helper type alias for Americans.
 pub type Color = Colour;
@@ -56,6 +56,34 @@ impl Colour {
 
 /// Generic conversion functions
 impl Colour {
+    #[inline]
+    fn convert_pack(
+        dst: &mut [u8],
+        src: &[u8],
+        src_pack: PackedLocation,
+        dst_pack: PackedLocation,
+    ) {
+        match (src_pack.index(), dst_pack.index()) {
+            (Some(srcindex), Some(dstindex)) => {
+                dst[dstindex as usize] = src[srcindex as usize];
+            }
+            (None, Some(dstindex)) => {
+                dst[dstindex as usize] = 255;
+            }
+            (_, None) => (),
+        }
+    }
+
+    pub(crate) fn convert_byte_form<SrcFormat: PixelFormatSpec, DstFormat: PixelFormatSpec>(
+        dst: &mut [u8],
+        src: &[u8],
+    ) {
+        Self::convert_pack(dst, src, SrcFormat::RED_PACK, DstFormat::RED_PACK);
+        Self::convert_pack(dst, src, SrcFormat::GREEN_PACK, DstFormat::GREEN_PACK);
+        Self::convert_pack(dst, src, SrcFormat::BLUE_PACK, DstFormat::BLUE_PACK);
+        Self::convert_pack(dst, src, SrcFormat::ALPHA_PACK, DstFormat::ALPHA_PACK);
+    }
+
     pub(crate) fn write_as_bytes<Format: PixelFormatSpec>(&self, to: &mut [u8]) {
         if let Some(offset) = Format::RED_PACK.index() {
             to[offset as usize] = self.r();

+ 0 - 162
src/dispatch.rs

@@ -1,162 +0,0 @@
-use crate::formats::{self, PixelFormatSpec, NUM_PIXEL_FORMATS};
-use crate::bitmap::{BitmapData, BitmapDataMut};
-use crate::op::{self, UnaryOpSpec};
-
-pub type UnaryFunc<Params> = fn(&mut dyn BitmapDataMut, &Params);
-pub type BinaryFunc<Params> = fn(&mut dyn BitmapDataMut, &dyn BitmapData, &Params);
-
-pub trait GenericUnary<Params> {
-    fn perform<Format: PixelFormatSpec>(write_data: &mut dyn BitmapDataMut, params: &Params);
-
-    fn make_dispatch() -> UnaryDispatch<Params> {
-        UnaryDispatch {
-            op_array: [
-                Self::perform::<formats::A8>,
-                Self::perform::<formats::Abgr32>,
-                Self::perform::<formats::Bgr24>,
-                Self::perform::<formats::Bgr32>,
-                Self::perform::<formats::Rgb24>,
-                Self::perform::<formats::Rgb32>,
-                Self::perform::<formats::Rgba32>,
-            ],
-            changed: Default::default(),
-        }
-    }
-}
-
-pub trait GenericBinary<Params> {
-    fn perform<Format: PixelFormatSpec, ReadFormat: PixelFormatSpec>(write_data: &mut dyn BitmapDataMut, read_data: &dyn BitmapData, params: &Params);
-
-    fn make_dispatch() -> BinaryDispatch<Params> {
-        BinaryDispatch {
-            op_array: [
-                [
-                    Self::perform::<formats::A8, formats::A8>,
-                    Self::perform::<formats::A8, formats::Abgr32>,
-                    Self::perform::<formats::A8, formats::Bgr24>,
-                    Self::perform::<formats::A8, formats::Bgr32>,
-                    Self::perform::<formats::A8, formats::Rgb24>,
-                    Self::perform::<formats::A8, formats::Rgb32>,
-                    Self::perform::<formats::A8, formats::Rgba32>,
-                ],
-                [
-                    Self::perform::<formats::Abgr32, formats::A8>,
-                    Self::perform::<formats::Abgr32, formats::Abgr32>,
-                    Self::perform::<formats::Abgr32, formats::Bgr24>,
-                    Self::perform::<formats::Abgr32, formats::Bgr32>,
-                    Self::perform::<formats::Abgr32, formats::Rgb24>,
-                    Self::perform::<formats::Abgr32, formats::Rgb32>,
-                    Self::perform::<formats::Abgr32, formats::Rgba32>,
-                ],
-                [
-                    Self::perform::<formats::Bgr24, formats::A8>,
-                    Self::perform::<formats::Bgr24, formats::Abgr32>,
-                    Self::perform::<formats::Bgr24, formats::Bgr24>,
-                    Self::perform::<formats::Bgr24, formats::Bgr32>,
-                    Self::perform::<formats::Bgr24, formats::Rgb24>,
-                    Self::perform::<formats::Bgr24, formats::Rgb32>,
-                    Self::perform::<formats::Bgr24, formats::Rgba32>,
-                ],
-                [
-                    Self::perform::<formats::Bgr32, formats::A8>,
-                    Self::perform::<formats::Bgr32, formats::Abgr32>,
-                    Self::perform::<formats::Bgr32, formats::Bgr24>,
-                    Self::perform::<formats::Bgr32, formats::Bgr32>,
-                    Self::perform::<formats::Bgr32, formats::Rgb24>,
-                    Self::perform::<formats::Bgr32, formats::Rgb32>,
-                    Self::perform::<formats::Bgr32, formats::Rgba32>,
-                ],
-                [
-                    Self::perform::<formats::Rgb24, formats::A8>,
-                    Self::perform::<formats::Rgb24, formats::Abgr32>,
-                    Self::perform::<formats::Rgb24, formats::Bgr24>,
-                    Self::perform::<formats::Rgb24, formats::Bgr32>,
-                    Self::perform::<formats::Rgb24, formats::Rgb24>,
-                    Self::perform::<formats::Rgb24, formats::Rgb32>,
-                    Self::perform::<formats::Rgb24, formats::Rgba32>,
-                ],
-                [
-                    Self::perform::<formats::Rgb32, formats::A8>,
-                    Self::perform::<formats::Rgb32, formats::Abgr32>,
-                    Self::perform::<formats::Rgb32, formats::Bgr24>,
-                    Self::perform::<formats::Rgb32, formats::Bgr32>,
-                    Self::perform::<formats::Rgb32, formats::Rgb24>,
-                    Self::perform::<formats::Rgb32, formats::Rgb32>,
-                    Self::perform::<formats::Rgb32, formats::Rgba32>,
-                ],
-                [
-                    Self::perform::<formats::Rgba32, formats::A8>,
-                    Self::perform::<formats::Rgba32, formats::Abgr32>,
-                    Self::perform::<formats::Rgba32, formats::Bgr24>,
-                    Self::perform::<formats::Rgba32, formats::Bgr32>,
-                    Self::perform::<formats::Rgba32, formats::Rgb24>,
-                    Self::perform::<formats::Rgba32, formats::Rgb32>,
-                    Self::perform::<formats::Rgba32, formats::Rgba32>,
-                ],
-            ],
-            changed: Default::default(),
-        }
-    }
-}
-
-pub struct UnaryDispatch<Params> {
-    op_array: [UnaryFunc<Params>; NUM_PIXEL_FORMATS],
-    changed: [bool; NUM_PIXEL_FORMATS],
-}
-
-impl<Params> UnaryDispatch<Params> {
-    pub fn with<Format: PixelFormatSpec>(mut self, specific_imp: UnaryFunc<Params>) -> Self {
-        self.op_array[Format::FORMAT_ENUM as usize] = specific_imp;
-        self.changed[Format::FORMAT_ENUM as usize] = true;
-        self
-    }
-    pub fn with_avx<Format: PixelFormatSpec>(mut self, avx_imp: UnaryFunc<Params>) -> Self {
-        if is_x86_feature_detected!("avx") {
-            self.op_array[Format::FORMAT_ENUM as usize] = avx_imp;
-            self.changed[Format::FORMAT_ENUM as usize] = true;
-            self
-        } else {
-            self
-        }
-    }
-
-    pub fn select<Format: PixelFormatSpec>(&self) -> &UnaryFunc<Params> {
-        &self.op_array[Format::FORMAT_ENUM as usize]
-    }
-}
-
-pub struct BinaryDispatch<Params> {
-    op_array: [[BinaryFunc<Params>; NUM_PIXEL_FORMATS]; NUM_PIXEL_FORMATS],
-    changed: [[bool; NUM_PIXEL_FORMATS]; NUM_PIXEL_FORMATS],
-}
-
-impl<Params> BinaryDispatch<Params> {
-    pub fn with_avx<WriteFormat: PixelFormatSpec, ReadFormat: PixelFormatSpec>(mut self, avx_imp: BinaryFunc<Params>) -> Self {
-        if is_x86_feature_detected!("avx") {
-            self.op_array[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize] = avx_imp;
-            self
-        } else {
-            self
-        }
-    }
-
-    pub fn select<WriteFormat: PixelFormatSpec, ReadFormat: PixelFormatSpec>(&self) -> &BinaryFunc<Params> {
-        &self.op_array[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize]
-    }
-}
-
-pub struct KahloDispatch {
-    pub fill: UnaryDispatch<<op::FillOp as UnaryOpSpec>::Params>,
-}
-
-impl Default for KahloDispatch {
-    fn default() -> Self {
-        Self {
-            fill: op::FillOp::build(),
-        }
-    }
-}
-
-lazy_static::lazy_static! {
-    static ref DISPATCH: KahloDispatch = KahloDispatch::default();
-}

+ 3 - 13
src/lib.rs

@@ -8,9 +8,7 @@
 //! purposes, and are written for clarity and not speed.
 
 mod bitmap;
-// mod ops;
 mod op;
-mod dispatch;
 
 #[cfg(target_endian = "big")]
 std::compile_error!("kahlo only works on little-endian targets!");
@@ -24,20 +22,12 @@ pub mod formats;
 pub mod prelude {
     // pub use crate::ops::{AlphaPaintable, Readable, RgbaPaintable, Paintable, Writable};
     pub use crate::bitmap::{BitmapAccess, BitmapMutAccess};
+    pub use crate::op::KahloOps;
 }
 
-/// Types for 2D mathematics.
-pub mod math {
-    pub struct PixelSpace;
-    pub type PixelPoint = euclid::Point2D<i32, PixelSpace>;
-    pub type PixelSize = euclid::Size2D<i32, PixelSpace>;
-    pub type PixelRect = euclid::Rect<i32, PixelSpace>;
-    pub type PixelBox = euclid::Box2D<i32, PixelSpace>;
-    pub type PixelVector = euclid::Vector2D<i32, PixelSpace>;
-    pub type PixelSideOffsets = euclid::SideOffsets2D<i32, PixelSpace>;
-}
+pub mod math;
 
-pub use bitmap::{Bitmap, BitmapRef, BitmapMut};
+pub use bitmap::{Bitmap, BitmapMut, BitmapRef};
 
 /// Helper type alias for a Rgba32 bitmap.
 pub type RgbaBitmap = Bitmap<formats::Rgba32>;

+ 71 - 0
src/math.rs

@@ -0,0 +1,71 @@
+//! Types for 2D mathematics.
+
+pub struct PixelSpace;
+pub type PixelPoint = euclid::Point2D<i32, PixelSpace>;
+pub type PixelSize = euclid::Size2D<i32, PixelSpace>;
+pub type PixelRect = euclid::Rect<i32, PixelSpace>;
+pub type PixelBox = euclid::Box2D<i32, PixelSpace>;
+pub type PixelVector = euclid::Vector2D<i32, PixelSpace>;
+pub type PixelSideOffsets = euclid::SideOffsets2D<i32, PixelSpace>;
+
+pub fn clip_to(
+    img1_clip_box: PixelBox,
+    img2_clip_box: PixelBox,
+    img1_victim_box: PixelBox,
+    img2_victim_box: PixelBox,
+) -> Option<(PixelBox, PixelBox)> {
+    if img1_victim_box.size() != img2_victim_box.size() {
+        return None;
+    }
+
+    let adj = PixelSideOffsets {
+        left: (img1_clip_box.min.x - img1_victim_box.min.x)
+            .max(img2_clip_box.min.x - img2_victim_box.min.x)
+            .max(0),
+        top: (img1_clip_box.min.y - img1_victim_box.min.y)
+            .max(img2_clip_box.min.y - img2_victim_box.min.y)
+            .max(0),
+        right: (img1_victim_box.max.x - img1_clip_box.max.x)
+            .max(img2_victim_box.max.x - img2_clip_box.max.x)
+            .max(0),
+        bottom: (img1_victim_box.max.y - img1_clip_box.max.y)
+            .max(img2_victim_box.max.y - img2_clip_box.max.y)
+            .max(0),
+        ..Default::default()
+    };
+
+    Some((
+        img1_victim_box.inner_box(adj),
+        img2_victim_box.inner_box(adj),
+    ))
+}
+
+/*pub fn clip_to(
+    img1_size: PixelSize,
+    pt1: PixelPoint,
+    img2_size: PixelSize,
+    pt2: PixelPoint,
+    sz: PixelSize,
+) -> Option<(PixelBox, PixelBox)> {
+    let left = (-pt1.x).max(-pt2.x).max(0);
+    let top = (-pt1.y).max(-pt2.y).max(0);
+    let right = ((sz.width + pt1.x) - img1_size.width as i32)
+        .max((sz.width + pt2.x) - img2_size.width as i32)
+        .max(0);
+    let bottom = ((sz.height + pt1.y) - img1_size.height as i32)
+        .max((sz.height + pt2.y) - img2_size.height as i32)
+        .max(0);
+
+    let adj = PixelSideOffsets {
+        left,
+        right,
+        top,
+        bottom,
+        ..Default::default()
+    };
+
+    Some((
+        PixelBox::from_origin_and_size(pt1, sz).inner_box(adj),
+        PixelBox::from_origin_and_size(pt2, sz).inner_box(adj),
+    ))
+}*/

+ 76 - 5
src/op.rs

@@ -1,10 +1,81 @@
-mod fill;
-pub use fill::FillOp;
+use crate::{
+    bitmap::{BitmapAccess, BitmapMutAccess},
+    colour::{BlendMode, Colour},
+    formats::{self, PixelFormatSpec},
+    math::{PixelBox, PixelPoint},
+};
+
+mod dispatch;
+use dispatch::{BinaryDispatch, GenericBinary, GenericUnary, UnaryDispatch, DISPATCH};
 
-use crate::dispatch::UnaryDispatch;
+// operation implementations
+mod composite;
+mod copy_from;
+mod fill;
+mod fill_region;
+mod fill_region_masked;
 
 pub trait UnaryOpSpec {
-    type Params;
+    type Params<'l>;
 
-    fn build() -> UnaryDispatch<Self::Params>;
+    fn build<'l>() -> UnaryDispatch<Self::Params<'l>>;
 }
+
+pub trait BinaryOpSpec {
+    type Params<'l>;
+
+    fn build<'l>() -> BinaryDispatch<Self::Params<'l>>;
+}
+
+pub trait KahloOps<Format: PixelFormatSpec>: BitmapMutAccess<Format> {
+    fn composite<SrcFormat: PixelFormatSpec>(
+        &mut self,
+        src: &dyn BitmapAccess<SrcFormat>,
+        src_area: PixelBox,
+        dst: PixelPoint,
+        blend: BlendMode,
+    ) {
+        DISPATCH.composite.select::<Format, SrcFormat>()(
+            self.data_mut(),
+            src.data(),
+            &(src_area, dst, blend),
+        )
+    }
+
+    fn copy_from<SrcFormat: PixelFormatSpec>(
+        &mut self,
+        src: &dyn BitmapAccess<SrcFormat>,
+        src_area: PixelBox,
+        dst: PixelPoint,
+    ) {
+        DISPATCH.copy_from.select::<Format, SrcFormat>()(
+            self.data_mut(),
+            src.data(),
+            &(src_area, dst),
+        );
+    }
+
+    fn fill(&mut self, colour: Colour) {
+        DISPATCH.fill.select::<Format>()(self.data_mut(), &colour);
+    }
+
+    fn fill_region(&mut self, region: PixelBox, colour: Colour) {
+        DISPATCH.fill_region.select::<Format>()(self.data_mut(), &(region, colour));
+    }
+
+    fn fill_region_masked<'l>(
+        &mut self,
+        mask: &'l dyn BitmapAccess<formats::A8>,
+        src_area: PixelBox,
+        dst: PixelPoint,
+        colour: Colour,
+        blend: BlendMode,
+    ) {
+        DISPATCH
+            .use_lifetime()
+            .fill_region_masked
+            .select::<Format>()(self.data_mut(), &(mask, src_area, dst, colour, blend))
+    }
+}
+
+impl<Format: PixelFormatSpec, T: BitmapMutAccess<Format>> KahloOps<Format> for T {}

+ 59 - 0
src/op/composite.rs

@@ -0,0 +1,59 @@
+use crate::{
+    bitmap::{BitmapData, BitmapDataMut},
+    colour::{BlendMode, Colour},
+    formats::{PixelFormatSpec, A8},
+    math::{clip_to, PixelBox, PixelPoint},
+};
+
+use super::{
+    dispatch::{BinaryDispatch, GenericBinary},
+    BinaryOpSpec,
+};
+
+pub struct CompositeOp;
+
+impl<'l> GenericBinary<(PixelBox, PixelPoint, BlendMode)> for CompositeOp {
+    fn perform<Format: PixelFormatSpec, SrcFormat: PixelFormatSpec>(
+        write_data: &mut dyn BitmapDataMut,
+        read_data: &dyn BitmapData,
+        params: &(PixelBox, PixelPoint, BlendMode),
+    ) {
+        let (read_box, dst, blend) = params;
+
+        let Some((write_box, read_box)) = clip_to(
+            PixelBox::from_size(write_data.size()),
+            PixelBox::from_size(read_data.size()),
+            PixelBox::from_origin_and_size(*dst, read_data.size()),
+            *read_box,
+        ) else {
+            return;
+        };
+
+        let read_stride = read_data.stride();
+        let write_stride = write_data.stride();
+        let read_data = read_data.data();
+        let write_data = write_data.data_mut();
+        for y in 0..read_box.height() {
+            let y_offset_r = (y + read_box.min.y) as usize * read_stride;
+            let y_offset_w = (y + write_box.min.y) as usize * write_stride;
+            for x in 0..read_box.width() {
+                let x_offset_r = (x + read_box.min.x) as usize * A8::PIXEL_WIDTH;
+                let x_offset_w = (x + write_box.min.x) as usize * Format::PIXEL_WIDTH;
+
+                let existing =
+                    Colour::read_from_bytes::<Format>(&write_data[y_offset_w + x_offset_w..]);
+                let source =
+                    Colour::read_from_bytes::<SrcFormat>(&read_data[y_offset_r + x_offset_r..]);
+                let new_colour = blend.blend(&source, &existing);
+                new_colour.write_as_bytes::<Format>(&mut write_data[y_offset_w + x_offset_w..]);
+            }
+        }
+    }
+}
+
+impl BinaryOpSpec for CompositeOp {
+    type Params<'l> = (PixelBox, PixelPoint, BlendMode);
+    fn build<'l>() -> BinaryDispatch<Self::Params<'l>> {
+        BinaryDispatch::from_generic::<Self>()
+    }
+}

+ 54 - 0
src/op/copy_from.rs

@@ -0,0 +1,54 @@
+use crate::{
+    bitmap::{BitmapData, BitmapDataMut},
+    colour::Colour,
+    formats::PixelFormatSpec,
+    math::{clip_to, PixelBox, PixelPoint},
+};
+
+use super::{dispatch::BinaryDispatch, BinaryOpSpec, GenericBinary};
+
+pub struct CopyFromOp;
+
+impl BinaryOpSpec for CopyFromOp {
+    type Params<'l> = (PixelBox, PixelPoint);
+
+    fn build<'l>() -> BinaryDispatch<Self::Params<'l>> {
+        BinaryDispatch::from_generic::<Self>()
+    }
+}
+
+impl GenericBinary<(PixelBox, PixelPoint)> for CopyFromOp {
+    fn perform<Format: PixelFormatSpec, ReadFormat: PixelFormatSpec>(
+        write_data: &mut dyn BitmapDataMut,
+        read_data: &dyn BitmapData,
+        params: &(PixelBox, PixelPoint),
+    ) {
+        let read_box = params.0;
+        let write_box = PixelBox::from_origin_and_size(params.1, read_box.size());
+        let Some((read_box, write_box)) = clip_to(
+            PixelBox::from_size(read_data.size()),
+            PixelBox::from_size(write_data.size()),
+            read_box,
+            write_box,
+        ) else {
+            // nothing to do
+            return;
+        };
+
+        let write_stride = write_data.stride();
+        let write_data = write_data.data_mut();
+        let read_stride = read_data.stride();
+        let read_data = read_data.data();
+
+        for y in 0..read_box.height() {
+            let write_slice = &mut write_data[(y + write_box.min.y) as usize * write_stride..];
+            let read_slice = &read_data[(y + read_box.min.y) as usize * read_stride..];
+            for x in 0..read_box.width() {
+                Colour::convert_byte_form::<ReadFormat, Format>(
+                    &mut write_slice[(x + write_box.min.x) as usize * Format::PIXEL_WIDTH..],
+                    &read_slice[(x + read_box.min.x) as usize * ReadFormat::PIXEL_WIDTH..],
+                );
+            }
+        }
+    }
+}

+ 205 - 0
src/op/dispatch.rs

@@ -0,0 +1,205 @@
+use crate::bitmap::{BitmapData, BitmapDataMut};
+use crate::formats::{self, PixelFormatSpec, NUM_PIXEL_FORMATS};
+use crate::op::{self, BinaryOpSpec, UnaryOpSpec};
+
+pub type UnaryFunc<Params> = for<'l> fn(&'l mut dyn BitmapDataMut, &'l Params);
+pub type BinaryFunc<Params> = for<'l> fn(&'l mut dyn BitmapDataMut, &'l dyn BitmapData, &'l Params);
+
+pub trait GenericUnary<Params> {
+    fn perform<'l, Format: PixelFormatSpec>(
+        write_data: &'l mut dyn BitmapDataMut,
+        params: &'l Params,
+    );
+}
+
+pub trait GenericBinary<Params> {
+    fn perform<'l, Format: PixelFormatSpec, ReadFormat: PixelFormatSpec>(
+        write_data: &'l mut dyn BitmapDataMut,
+        read_data: &'l dyn BitmapData,
+        params: &'l Params,
+    );
+}
+
+pub struct UnaryDispatch<Params> {
+    op_array: [UnaryFunc<Params>; NUM_PIXEL_FORMATS],
+    changed: [bool; NUM_PIXEL_FORMATS],
+}
+
+impl<Params> UnaryDispatch<Params> {
+    pub fn with<Format: PixelFormatSpec>(mut self, specific_imp: UnaryFunc<Params>) -> Self {
+        self.op_array[Format::FORMAT_ENUM as usize] = specific_imp;
+        self.changed[Format::FORMAT_ENUM as usize] = true;
+        self
+    }
+    pub fn with_avx<Format: PixelFormatSpec>(mut self, avx_imp: UnaryFunc<Params>) -> Self {
+        if is_x86_feature_detected!("avx") {
+            self.op_array[Format::FORMAT_ENUM as usize] = avx_imp;
+            self.changed[Format::FORMAT_ENUM as usize] = true;
+            self
+        } else {
+            self
+        }
+    }
+
+    pub fn select<Format: PixelFormatSpec>(&self) -> UnaryFunc<Params> {
+        self.op_array[Format::FORMAT_ENUM as usize]
+    }
+}
+
+pub struct BinaryDispatch<Params> {
+    op_array: [[BinaryFunc<Params>; NUM_PIXEL_FORMATS]; NUM_PIXEL_FORMATS],
+    changed: [[bool; NUM_PIXEL_FORMATS]; NUM_PIXEL_FORMATS],
+}
+
+impl<Params> BinaryDispatch<Params> {
+    pub fn with_avx<WriteFormat: PixelFormatSpec, ReadFormat: PixelFormatSpec>(
+        mut self,
+        avx_imp: BinaryFunc<Params>,
+    ) -> Self {
+        if is_x86_feature_detected!("avx") {
+            self.op_array[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize] =
+                avx_imp;
+            self
+        } else {
+            self
+        }
+    }
+
+    pub fn select<WriteFormat: PixelFormatSpec, ReadFormat: PixelFormatSpec>(
+        &self,
+    ) -> BinaryFunc<Params> {
+        self.op_array[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize]
+    }
+}
+
+macro_rules! dispatch_entry {
+    (unary, $sn:ident, $opn:ident) => {
+        UnaryDispatch < <op::$sn::$opn as UnaryOpSpec>::Params<'l>>
+    };
+
+    (binary, $sn:ident, $opn:ident) => {
+        BinaryDispatch < <op::$sn::$opn as BinaryOpSpec>::Params<'l>>
+    };
+}
+
+pub struct KahloDispatch<'l> {
+    pub composite: dispatch_entry!(binary, composite, CompositeOp),
+    pub copy_from: dispatch_entry!(binary, copy_from, CopyFromOp),
+    pub fill: dispatch_entry!(unary, fill, FillOp),
+    pub fill_region: dispatch_entry!(unary, fill_region, FillRegionOp),
+    pub fill_region_masked: dispatch_entry!(unary, fill_region_masked, FillRegionMaskedOp),
+}
+
+impl<'l> Default for KahloDispatch<'l> {
+    fn default() -> Self {
+        Self {
+            composite: op::composite::CompositeOp::build(),
+            copy_from: op::copy_from::CopyFromOp::build(),
+            fill: op::fill::FillOp::build(),
+            fill_region: op::fill_region::FillRegionOp::build(),
+            fill_region_masked: op::fill_region_masked::FillRegionMaskedOp::build(),
+        }
+    }
+}
+
+impl<'l> KahloDispatch<'l> {
+    pub fn use_lifetime(&self) -> &KahloDispatch<'_> {
+        unsafe { std::mem::transmute(self) }
+    }
+}
+
+lazy_static::lazy_static! {
+    pub static ref DISPATCH: KahloDispatch<'static> = KahloDispatch::default();
+}
+
+// long but simple implementations
+
+impl<Params> UnaryDispatch<Params> {
+    pub(crate) fn from_generic<G: GenericUnary<Params>>() -> Self {
+        Self {
+            op_array: [
+                G::perform::<formats::A8>,
+                G::perform::<formats::Abgr32>,
+                G::perform::<formats::Bgr24>,
+                G::perform::<formats::Bgr32>,
+                G::perform::<formats::Rgb24>,
+                G::perform::<formats::Rgb32>,
+                G::perform::<formats::Rgba32>,
+            ],
+            changed: Default::default(),
+        }
+    }
+}
+
+impl<Params> BinaryDispatch<Params> {
+    pub(crate) fn from_generic<G: GenericBinary<Params>>() -> Self {
+        Self {
+            op_array: [
+                [
+                    G::perform::<formats::A8, formats::A8>,
+                    G::perform::<formats::A8, formats::Abgr32>,
+                    G::perform::<formats::A8, formats::Bgr24>,
+                    G::perform::<formats::A8, formats::Bgr32>,
+                    G::perform::<formats::A8, formats::Rgb24>,
+                    G::perform::<formats::A8, formats::Rgb32>,
+                    G::perform::<formats::A8, formats::Rgba32>,
+                ],
+                [
+                    G::perform::<formats::Abgr32, formats::A8>,
+                    G::perform::<formats::Abgr32, formats::Abgr32>,
+                    G::perform::<formats::Abgr32, formats::Bgr24>,
+                    G::perform::<formats::Abgr32, formats::Bgr32>,
+                    G::perform::<formats::Abgr32, formats::Rgb24>,
+                    G::perform::<formats::Abgr32, formats::Rgb32>,
+                    G::perform::<formats::Abgr32, formats::Rgba32>,
+                ],
+                [
+                    G::perform::<formats::Bgr24, formats::A8>,
+                    G::perform::<formats::Bgr24, formats::Abgr32>,
+                    G::perform::<formats::Bgr24, formats::Bgr24>,
+                    G::perform::<formats::Bgr24, formats::Bgr32>,
+                    G::perform::<formats::Bgr24, formats::Rgb24>,
+                    G::perform::<formats::Bgr24, formats::Rgb32>,
+                    G::perform::<formats::Bgr24, formats::Rgba32>,
+                ],
+                [
+                    G::perform::<formats::Bgr32, formats::A8>,
+                    G::perform::<formats::Bgr32, formats::Abgr32>,
+                    G::perform::<formats::Bgr32, formats::Bgr24>,
+                    G::perform::<formats::Bgr32, formats::Bgr32>,
+                    G::perform::<formats::Bgr32, formats::Rgb24>,
+                    G::perform::<formats::Bgr32, formats::Rgb32>,
+                    G::perform::<formats::Bgr32, formats::Rgba32>,
+                ],
+                [
+                    G::perform::<formats::Rgb24, formats::A8>,
+                    G::perform::<formats::Rgb24, formats::Abgr32>,
+                    G::perform::<formats::Rgb24, formats::Bgr24>,
+                    G::perform::<formats::Rgb24, formats::Bgr32>,
+                    G::perform::<formats::Rgb24, formats::Rgb24>,
+                    G::perform::<formats::Rgb24, formats::Rgb32>,
+                    G::perform::<formats::Rgb24, formats::Rgba32>,
+                ],
+                [
+                    G::perform::<formats::Rgb32, formats::A8>,
+                    G::perform::<formats::Rgb32, formats::Abgr32>,
+                    G::perform::<formats::Rgb32, formats::Bgr24>,
+                    G::perform::<formats::Rgb32, formats::Bgr32>,
+                    G::perform::<formats::Rgb32, formats::Rgb24>,
+                    G::perform::<formats::Rgb32, formats::Rgb32>,
+                    G::perform::<formats::Rgb32, formats::Rgba32>,
+                ],
+                [
+                    G::perform::<formats::Rgba32, formats::A8>,
+                    G::perform::<formats::Rgba32, formats::Abgr32>,
+                    G::perform::<formats::Rgba32, formats::Bgr24>,
+                    G::perform::<formats::Rgba32, formats::Bgr32>,
+                    G::perform::<formats::Rgba32, formats::Rgb24>,
+                    G::perform::<formats::Rgba32, formats::Rgb32>,
+                    G::perform::<formats::Rgba32, formats::Rgba32>,
+                ],
+            ],
+            changed: Default::default(),
+        }
+    }
+}

+ 21 - 11
src/op/fill.rs

@@ -1,14 +1,17 @@
-use crate::{dispatch::GenericUnary, colour::Colour, bitmap::BitmapDataMut, formats::{MAX_PIXEL_WIDTH, A8}};
+use crate::{
+    bitmap::BitmapDataMut,
+    colour::Colour,
+    formats::{A8, MAX_PIXEL_WIDTH},
+};
 
-use super::UnaryOpSpec;
+use super::{GenericUnary, UnaryDispatch, UnaryOpSpec};
 
 pub struct FillOp;
 
 impl UnaryOpSpec for FillOp {
-    type Params = Colour;
-    fn build() -> crate::dispatch::UnaryDispatch<Self::Params> {
-        FillOp::make_dispatch()
-            .with::<A8>(fill_a8)
+    type Params<'l> = Colour;
+    fn build<'l>() -> UnaryDispatch<Self::Params<'l>> {
+        UnaryDispatch::from_generic::<Self>().with::<A8>(fill_a8)
     }
 }
 
@@ -22,14 +25,14 @@ fn fill_a8(target: &mut dyn BitmapDataMut, with: &Colour) {
 
     // optimization: can we fill the entire thing in one go?
     if stride == row_width {
-        data[0..(stride*height)].fill(alpha);
+        data[0..(stride * height)].fill(alpha);
         return;
     }
 
     for y in 0..height {
         let yoff = y * stride;
 
-        data[yoff..(yoff+row_width)].fill(alpha);
+        data[yoff..(yoff + row_width)].fill(alpha);
     }
 }
 
@@ -59,8 +62,13 @@ fn fill_helper<const WIDTH: usize>(target: &mut dyn BitmapDataMut, pattern: &[u8
 }
 
 impl<'l> GenericUnary<Colour> for FillOp {
-    fn perform<Format: crate::formats::PixelFormatSpec>(write_data: &mut dyn BitmapDataMut, params: &Colour) {
-        if write_data.width() == 0 || write_data.height() == 0 { return }
+    fn perform<Format: crate::formats::PixelFormatSpec>(
+        write_data: &mut dyn BitmapDataMut,
+        params: &Colour,
+    ) {
+        if write_data.width() == 0 || write_data.height() == 0 {
+            return;
+        }
 
         let mut cdata = [0u8; MAX_PIXEL_WIDTH];
         params.write_as_bytes::<Format>(&mut cdata);
@@ -75,7 +83,9 @@ impl<'l> GenericUnary<Colour> for FillOp {
             fill_helper::<3>(write_data, cdata);
         } else if Format::PIXEL_WIDTH == 4 {
             fill_helper::<4>(write_data, cdata);
-        } else { unreachable!() }
+        } else {
+            unreachable!()
+        }
     }
 }
 

+ 31 - 0
src/op/fill_region.rs

@@ -0,0 +1,31 @@
+use crate::{
+    bitmap::{BitmapDataMut, MakeMutView},
+    colour::Colour,
+    formats::PixelFormatSpec,
+    math::PixelBox,
+};
+
+use super::{
+    dispatch::{GenericUnary, UnaryDispatch},
+    KahloOps, UnaryOpSpec,
+};
+
+pub struct FillRegionOp;
+
+impl GenericUnary<(PixelBox, Colour)> for FillRegionOp {
+    fn perform<Format: PixelFormatSpec>(
+        mut write_data: &mut dyn BitmapDataMut,
+        params: &(PixelBox, Colour),
+    ) {
+        // make a view and forward to FillOp
+        let mut subregion = write_data.make_view_mut::<Format>(params.0);
+        subregion.fill(params.1);
+    }
+}
+
+impl UnaryOpSpec for FillRegionOp {
+    type Params<'l> = (PixelBox, Colour);
+    fn build<'l>() -> UnaryDispatch<Self::Params<'l>> {
+        UnaryDispatch::from_generic::<Self>()
+    }
+}

+ 77 - 0
src/op/fill_region_masked.rs

@@ -0,0 +1,77 @@
+use crate::{
+    bitmap::{BitmapAccess, BitmapDataMut},
+    colour::{BlendMode, Colour},
+    formats::{PixelFormatSpec, A8},
+    math::{clip_to, PixelBox, PixelPoint},
+};
+
+use super::{
+    dispatch::{GenericUnary, UnaryDispatch},
+    UnaryOpSpec,
+};
+
+pub struct FillRegionMaskedOp;
+
+impl<'l>
+    GenericUnary<(
+        &'l dyn BitmapAccess<A8>,
+        PixelBox,
+        PixelPoint,
+        Colour,
+        BlendMode,
+    )> for FillRegionMaskedOp
+{
+    fn perform<Format: PixelFormatSpec>(
+        write_data: &mut dyn BitmapDataMut,
+        params: &(
+            &'l dyn BitmapAccess<A8>,
+            PixelBox,
+            PixelPoint,
+            Colour,
+            BlendMode,
+        ),
+    ) {
+        let (read_alphamap, read_box, dst, colour, blend) = params;
+
+        let Some((write_box, read_box)) = clip_to(
+            PixelBox::from_size(write_data.size()),
+            PixelBox::from_size(read_alphamap.size()),
+            PixelBox::from_origin_and_size(*dst, read_alphamap.size()),
+            *read_box,
+        ) else {
+            return;
+        };
+
+        let read_stride = read_alphamap.row_stride();
+        let write_stride = write_data.stride();
+        let read_data = read_alphamap.data().data();
+        let write_data = write_data.data_mut();
+        for y in 0..read_box.height() {
+            let y_offset_r = (y + read_box.min.y) as usize * read_stride;
+            let y_offset_w = (y + write_box.min.y) as usize * write_stride;
+            for x in 0..read_box.width() {
+                let x_offset_r = (x + read_box.min.x) as usize * A8::PIXEL_WIDTH;
+                let x_offset_w = (x + write_box.min.x) as usize * Format::PIXEL_WIDTH;
+
+                let existing =
+                    Colour::read_from_bytes::<Format>(&write_data[y_offset_w + x_offset_w..]);
+                let alpha_override = read_data[y_offset_r + x_offset_r];
+                let new_colour = blend.blend(&colour.with_a(alpha_override), &existing);
+                new_colour.write_as_bytes::<Format>(&mut write_data[y_offset_w + x_offset_w..]);
+            }
+        }
+    }
+}
+
+impl UnaryOpSpec for FillRegionMaskedOp {
+    type Params<'l> = (
+        &'l dyn BitmapAccess<A8>,
+        PixelBox,
+        PixelPoint,
+        Colour,
+        BlendMode,
+    );
+    fn build<'l>() -> UnaryDispatch<Self::Params<'l>> {
+        UnaryDispatch::from_generic::<Self>()
+    }
+}

+ 2 - 2
tests/common/mod.rs

@@ -1,4 +1,4 @@
-use kahlo::{Bitmap, prelude::*};
+use kahlo::{prelude::*, Bitmap};
 
 pub fn load_test_resource(name: &'static str) -> Bitmap<kahlo::formats::Rgba32> {
     let mut path = std::path::PathBuf::new();
@@ -27,7 +27,7 @@ pub fn save_test_result_rgba32(
 
     image::save_buffer(
         path,
-        bitmap.data(),
+        bitmap.data().data(),
         bitmap.width() as u32,
         bitmap.height() as u32,
         image::ExtendedColorType::Rgba8,