Browse Source

Various added features.

Kestrel 1 week ago
parent
commit
79bee2f5ab
15 changed files with 578 additions and 37 deletions
  1. 5 0
      Cargo.toml
  2. 8 1
      src/bitmap.rs
  3. 21 0
      src/colour.rs
  4. 1 1
      src/formats.rs
  5. 5 0
      src/lib.rs
  6. 22 3
      src/op.rs
  7. 0 0
      src/op/benchmark.rs
  8. 87 1
      src/op/copy_from.rs
  9. 19 0
      src/op/dispatch.rs
  10. 16 9
      src/op/fill.rs
  11. 10 0
      src/op/fill_region.rs
  12. 16 22
      src/op/fill_region_masked.rs
  13. 180 0
      src/op/fuzz.rs
  14. 92 0
      src/op/generate.rs
  15. 96 0
      src/op/generate/impls.rs

+ 5 - 0
Cargo.toml

@@ -10,6 +10,11 @@ euclid = { version = "0.22" }
 enum-map = { version = "2.7" }
 lazy_static = { version = "1.4" }
 bitvec = { version = "1" }
+tuples = "1.15"
 
 [dev-dependencies]
 image = { version = "0.25.1", default-features = false, features = ["png", "jpeg"] }
+# bencher = { version = "0.1.5" }
+criterion = { version = "0.5" }
+rand = { version = "0.8" }
+hex = "0.4.0"

+ 8 - 1
src/bitmap.rs

@@ -22,7 +22,6 @@ 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();
@@ -175,6 +174,14 @@ impl<Format: PixelFormatSpec> Bitmap<Format> {
             _ghost: Default::default(),
         }
     }
+
+    pub fn as_ref(&self) -> BitmapRef<Format> {
+        BitmapRef::new_with_stride(&self.data.data, self.data.width, self.data.height, self.data.stride)
+    }
+
+    pub fn as_mut(&mut self) -> BitmapMut<Format> {
+        BitmapMut::new_with_stride(&mut self.data.data, self.data.width, self.data.height, self.data.stride)
+    }
 }
 
 impl<Format: PixelFormatSpec> BitmapAccess<Format> for Bitmap<Format> {

+ 21 - 0
src/colour.rs

@@ -21,6 +21,26 @@ impl Colour {
         Self([r, g, b, 255])
     }
 
+    pub fn hex_rgb(hex: &str) -> Option<Self> {
+        // acceptable lengths: 6, 7
+        let toparse = match (hex.len(), hex.chars().next()) {
+            (6, _) => {
+                hex
+            },
+            (7, Some('#')) => {
+                &hex[1..]
+            },
+            _ => return None
+        };
+
+        Some(Self([
+            u8::from_str_radix(&toparse[0..2], 16).ok()?,
+            u8::from_str_radix(&toparse[2..4], 16).ok()?,
+            u8::from_str_radix(&toparse[4..6], 16).ok()?,
+            255
+        ]))
+    }
+
     pub const fn r(&self) -> u8 {
         self.0[0]
     }
@@ -139,6 +159,7 @@ impl Colour {
     pub const DARK_BLUE: Colour = Self::rgba(0, 0, 64, 255);
 }
 
+#[derive(Clone, Copy, PartialEq, Debug)]
 /// Describes a blending mode between a backdrop and a source pixel.
 pub enum BlendMode {
     /// Use source colour without doing any blending.

+ 1 - 1
src/formats.rs

@@ -32,7 +32,7 @@ impl PackedLocation {
 }
 
 #[allow(private_bounds)]
-pub trait PixelFormatSpec: Sealed + Clone + Copy {
+pub trait PixelFormatSpec: Sealed + Clone + Copy + PartialEq {
     const FORMAT_ENUM: PixelFormat;
     const CHANNELS: usize;
     const PIXEL_WIDTH: usize;

+ 5 - 0
src/lib.rs

@@ -33,3 +33,8 @@ pub use bitmap::{Bitmap, BitmapMut, BitmapRef};
 pub type RgbaBitmap = Bitmap<formats::Rgba32>;
 /// Helper type alias for an A8 bitmap.
 pub type Alphamap = Bitmap<formats::A8>;
+
+#[cfg(test)]
+pub fn collect_op_benchmarks(cri: &mut criterion::Criterion) {
+    op::collect_benchmarks(cri);
+}

+ 22 - 3
src/op.rs

@@ -14,14 +14,18 @@ mod copy_from;
 mod fill;
 mod fill_region;
 mod fill_region_masked;
+mod rectangle;
 
-pub trait UnaryOpSpec {
+#[cfg(test)]
+mod generate;
+
+pub trait UnaryOpSpec : GenericUnary<Self::Params<'static>> {
     type Params<'l>;
 
     fn build<'l>() -> UnaryDispatch<Self::Params<'l>>;
 }
 
-pub trait BinaryOpSpec {
+pub trait BinaryOpSpec : GenericBinary<Self::Params<'static>> {
     type Params<'l>;
 
     fn build<'l>() -> BinaryDispatch<Self::Params<'l>>;
@@ -56,7 +60,7 @@ pub trait KahloOps<Format: PixelFormatSpec>: BitmapMutAccess<Format> {
     }
 
     fn fill(&mut self, colour: Colour) {
-        DISPATCH.fill.select::<Format>()(self.data_mut(), &colour);
+        DISPATCH.fill.select::<Format>()(self.data_mut(), &(colour,));
     }
 
     fn fill_region(&mut self, region: PixelBox, colour: Colour) {
@@ -76,6 +80,21 @@ pub trait KahloOps<Format: PixelFormatSpec>: BitmapMutAccess<Format> {
             .fill_region_masked
             .select::<Format>()(self.data_mut(), &(mask, src_area, dst, colour, blend))
     }
+
+    fn rectangle(&mut self, area: PixelBox, width: usize, colour: Colour) {
+        DISPATCH.rectangle.select::<Format>()(self.data_mut(), &(area, width, colour));
+    }
 }
 
 impl<Format: PixelFormatSpec, T: BitmapMutAccess<Format>> KahloOps<Format> for T {}
+
+#[cfg(test)]
+mod fuzz;
+
+#[cfg(test)]
+mod benchmark;
+
+#[cfg(test)]
+pub fn collect_benchmarks(cri: &mut criterion::Criterion) {
+    cri.benchmark_group("fill");
+}

+ 0 - 0
src/op/benchmark.rs


+ 87 - 1
src/op/copy_from.rs

@@ -1,7 +1,7 @@
 use crate::{
     bitmap::{BitmapData, BitmapDataMut},
     colour::Colour,
-    formats::PixelFormatSpec,
+    formats::{PixelFormatSpec, Bgr32, Rgba32, A8, Bgr24, Rgb24, Rgb32, Abgr32},
     math::{clip_to, PixelBox, PixelPoint},
 };
 
@@ -9,11 +9,97 @@ use super::{dispatch::BinaryDispatch, BinaryOpSpec, GenericBinary};
 
 pub struct CopyFromOp;
 
+#[cfg(test)]
+const _ : () = {
+    use super::generate;
+    impl generate::BinaryInputSpec for CopyFromOp {
+        fn gen<'l>() -> impl generate::ValueGeneratorList<'l, OutputTuple = Self::Params<'l>> {
+            (generate::src_area, generate::dst_point)
+        }
+    }
+};
+
 impl BinaryOpSpec for CopyFromOp {
     type Params<'l> = (PixelBox, PixelPoint);
 
     fn build<'l>() -> BinaryDispatch<Self::Params<'l>> {
         BinaryDispatch::from_generic::<Self>()
+            .with::<Bgr32, Rgba32>(copy_bgr32_rgba32)
+            .with::<A8, A8>(copy_same_fmt::<{ A8::PIXEL_WIDTH }>)
+            .with::<Abgr32, Abgr32>(copy_same_fmt::<{ Abgr32::PIXEL_WIDTH }>)
+            .with::<Bgr24, Bgr24>(copy_same_fmt::<{ Bgr24::PIXEL_WIDTH }>)
+            .with::<Bgr32, Bgr32>(copy_same_fmt::<{ Bgr32::PIXEL_WIDTH }>)
+            .with::<Rgb24, Rgb24>(copy_same_fmt::<{ Rgb24::PIXEL_WIDTH }>)
+            .with::<Rgb32, Rgb32>(copy_same_fmt::<{ Rgb32::PIXEL_WIDTH }>)
+            .with::<Rgba32, Rgba32>(copy_same_fmt::<{ Rgba32::PIXEL_WIDTH }>)
+    }
+}
+
+/// read rgba32 and write bgr32
+fn copy_bgr32_rgba32(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();
+
+    fn do_copy(target: &mut [u8], source: &[u8], count: usize) {
+        // for now, do the dumb version
+        let source = unsafe { std::slice::from_raw_parts(source.as_ptr() as *const u32, count) };
+        let target = unsafe { std::slice::from_raw_parts_mut(target.as_ptr() as *mut u32, count) };
+        for i in 0..count {
+            target[i] = source[i].to_be() >> 8;
+        }
+    }
+
+    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;
+        do_copy(
+            &mut write_data[(y_offset_w + write_box.min.x as usize * Bgr32::PIXEL_WIDTH)..],
+            &read_data[(y_offset_r + read_box.min.x as usize * Rgba32::PIXEL_WIDTH)..],
+            read_box.width() as usize
+        );
+    }
+}
+
+fn copy_same_fmt<const WIDTH: usize>(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();
+
+    let read_line_offset = read_box.min.x as usize * WIDTH;
+    let write_line_offset = write_box.min.x as usize * WIDTH;
+    let line_width = read_box.width() as usize * WIDTH;
+
+    for y in 0..read_box.height() {
+        let write_slice = &mut write_data[(y + write_box.min.y) as usize * write_stride + write_line_offset..];
+        let read_slice = &read_data[(y + read_box.min.y) as usize * read_stride + read_line_offset..];
+        (&mut write_slice[..line_width]).copy_from_slice(&read_slice[..line_width]);
     }
 }
 

+ 19 - 0
src/op/dispatch.rs

@@ -44,6 +44,10 @@ impl<Params> UnaryDispatch<Params> {
     pub fn select<Format: PixelFormatSpec>(&self) -> UnaryFunc<Params> {
         self.op_array[Format::FORMAT_ENUM as usize]
     }
+
+    pub fn is_generic<Format: PixelFormatSpec>(&self) -> bool {
+        !self.changed[Format::FORMAT_ENUM as usize]
+    }
 }
 
 pub struct BinaryDispatch<Params> {
@@ -52,6 +56,14 @@ pub struct BinaryDispatch<Params> {
 }
 
 impl<Params> BinaryDispatch<Params> {
+    pub fn with<WriteFormat: PixelFormatSpec, ReadFormat: PixelFormatSpec>(
+        mut self,
+        imp: BinaryFunc<Params>,
+    ) -> Self {
+        self.op_array[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize] = imp;
+        self.changed[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize] = true;
+        self
+    }
     pub fn with_avx<WriteFormat: PixelFormatSpec, ReadFormat: PixelFormatSpec>(
         mut self,
         avx_imp: BinaryFunc<Params>,
@@ -59,6 +71,7 @@ impl<Params> BinaryDispatch<Params> {
         if is_x86_feature_detected!("avx") {
             self.op_array[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize] =
                 avx_imp;
+            self.changed[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize] = true;
             self
         } else {
             self
@@ -70,6 +83,10 @@ impl<Params> BinaryDispatch<Params> {
     ) -> BinaryFunc<Params> {
         self.op_array[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize]
     }
+
+    pub fn is_generic<WriteFormat: PixelFormatSpec, ReadFormat: PixelFormatSpec>(&self) -> bool {
+        !self.changed[WriteFormat::FORMAT_ENUM as usize][ReadFormat::FORMAT_ENUM as usize]
+    }
 }
 
 macro_rules! dispatch_entry {
@@ -88,6 +105,7 @@ pub struct KahloDispatch<'l> {
     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),
+    pub rectangle: dispatch_entry!(unary, rectangle, RectangleOp),
 }
 
 impl<'l> Default for KahloDispatch<'l> {
@@ -98,6 +116,7 @@ impl<'l> Default for KahloDispatch<'l> {
             fill: op::fill::FillOp::build(),
             fill_region: op::fill_region::FillRegionOp::build(),
             fill_region_masked: op::fill_region_masked::FillRegionMaskedOp::build(),
+            rectangle: op::rectangle::RectangleOp::build(),
         }
     }
 }

+ 16 - 9
src/op/fill.rs

@@ -8,20 +8,30 @@ use super::{GenericUnary, UnaryDispatch, UnaryOpSpec};
 
 pub struct FillOp;
 
+#[cfg(test)]
+const _ : () = {
+    use super::generate;
+    impl generate::UnaryInputSpec for FillOp {
+        fn gen<'l>() -> impl generate::ValueGeneratorList<'l, OutputTuple = Self::Params<'l>> {
+            (generate::random_colour,)
+        }
+    }
+};
+
 impl UnaryOpSpec for FillOp {
-    type Params<'l> = Colour;
+    type Params<'l> = (Colour,);
     fn build<'l>() -> UnaryDispatch<Self::Params<'l>> {
         UnaryDispatch::from_generic::<Self>().with::<A8>(fill_a8)
     }
 }
 
-fn fill_a8(target: &mut dyn BitmapDataMut, with: &Colour) {
+fn fill_a8(target: &mut dyn BitmapDataMut, with: &(Colour,)) {
     let height = target.height();
     let row_width = target.width();
     let stride = target.stride();
     let data = target.data_mut();
 
-    let alpha = with.a();
+    let alpha = with.0.a();
 
     // optimization: can we fill the entire thing in one go?
     if stride == row_width {
@@ -61,17 +71,17 @@ fn fill_helper<const WIDTH: usize>(target: &mut dyn BitmapDataMut, pattern: &[u8
     }
 }
 
-impl<'l> GenericUnary<Colour> for FillOp {
+impl<'l> GenericUnary<(Colour,)> for FillOp {
     fn perform<Format: crate::formats::PixelFormatSpec>(
         write_data: &mut dyn BitmapDataMut,
-        params: &Colour,
+        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);
+        params.0.write_as_bytes::<Format>(&mut cdata);
 
         let cdata = &cdata[0..Format::PIXEL_WIDTH];
 
@@ -88,6 +98,3 @@ impl<'l> GenericUnary<Colour> for FillOp {
         }
     }
 }
-
-/*#[bench]
-fn */

+ 10 - 0
src/op/fill_region.rs

@@ -12,6 +12,16 @@ use super::{
 
 pub struct FillRegionOp;
 
+#[cfg(test)]
+const _ : () = {
+    use super::generate;
+    impl generate::UnaryInputSpec for FillRegionOp {
+        fn gen<'l>() -> impl generate::ValueGeneratorList<'l, OutputTuple = Self::Params<'l>> {
+            (generate::dst_area, generate::random_colour,)
+        }
+    }
+};
+
 impl GenericUnary<(PixelBox, Colour)> for FillRegionOp {
     fn perform<Format: PixelFormatSpec>(
         mut write_data: &mut dyn BitmapDataMut,

+ 16 - 22
src/op/fill_region_masked.rs

@@ -12,24 +12,24 @@ use super::{
 
 pub struct FillRegionMaskedOp;
 
-impl<'l>
-    GenericUnary<(
-        &'l dyn BitmapAccess<A8>,
-        PixelBox,
-        PixelPoint,
-        Colour,
-        BlendMode,
-    )> for FillRegionMaskedOp
+type OpParams<'l> = (&'l dyn BitmapAccess<A8>, PixelBox, PixelPoint, Colour, BlendMode);
+
+#[cfg(test)]
+const _ : () = {
+    use super::generate;
+    impl generate::UnaryInputSpec for FillRegionMaskedOp {
+        fn gen<'l>() -> impl generate::ValueGeneratorList<'l, OutputTuple = Self::Params<'l>> {
+            (generate::alphamap, generate::alphamap_area, generate::dst_point, generate::random_colour,generate::constant(BlendMode::Simple))
+        }
+    }
+};
+
+
+impl<'l> GenericUnary<OpParams<'l>> for FillRegionMaskedOp
 {
     fn perform<Format: PixelFormatSpec>(
         write_data: &mut dyn BitmapDataMut,
-        params: &(
-            &'l dyn BitmapAccess<A8>,
-            PixelBox,
-            PixelPoint,
-            Colour,
-            BlendMode,
-        ),
+        params: &OpParams<'l>,
     ) {
         let (read_alphamap, read_box, dst, colour, blend) = params;
 
@@ -64,13 +64,7 @@ impl<'l>
 }
 
 impl UnaryOpSpec for FillRegionMaskedOp {
-    type Params<'l> = (
-        &'l dyn BitmapAccess<A8>,
-        PixelBox,
-        PixelPoint,
-        Colour,
-        BlendMode,
-    );
+    type Params<'l> = OpParams<'l>;
     fn build<'l>() -> UnaryDispatch<Self::Params<'l>> {
         UnaryDispatch::from_generic::<Self>()
     }

+ 180 - 0
src/op/fuzz.rs

@@ -0,0 +1,180 @@
+use crate::formats::{self, PixelFormatSpec};
+use crate::bitmap::{BitmapAccess,BitmapMutAccess};
+
+use super::copy_from::CopyFromOp;
+use super::fill::FillOp;
+use super::fill_region::FillRegionOp;
+use super::fill_region_masked::FillRegionMaskedOp;
+use super::{GenericUnary,GenericBinary,UnaryOpSpec,BinaryOpSpec};
+use super::generate::{UnaryInputSpec,BinaryInputSpec,ValueGeneratorList, self};
+
+fn test_unary_format<UIS: UnaryInputSpec, Format: PixelFormatSpec>(n: usize) {
+    let gen = UIS::gen();
+    let mut ctx = generate::Context::default();
+
+    let op = UIS::build();
+    if op.is_generic::<Format>() {
+        // println!("Skipping generic implementation {}", Format::NAME);
+        return
+    } else {
+        println!("Testing specific implementation {}", Format::NAME);
+    }
+
+    for _ in 0..n {
+        let mut specific_img = generate::dst_image_generator::<Format>(&mut ctx).expect("could not generate image");
+        let params = gen.generate(&mut ctx).expect("could not generate parameters");
+        let mut generic_img = specific_img.clone();
+
+        op.select::<Format>()(specific_img.data_mut(), &params);
+        <UIS as GenericUnary<UIS::Params::<'static>>>::perform::<Format>(generic_img.data_mut(), &params);
+
+        assert_eq!(specific_img, generic_img);
+    }
+
+}
+
+fn test_unary<UIS: UnaryInputSpec>(n: usize) {
+    test_unary_format::<UIS, formats::A8>(n);
+    test_unary_format::<UIS, formats::Abgr32>(n);
+    test_unary_format::<UIS, formats::Bgr24>(n);
+    test_unary_format::<UIS, formats::Bgr32>(n);
+    test_unary_format::<UIS, formats::Rgb24>(n);
+    test_unary_format::<UIS, formats::Rgb32>(n);
+    test_unary_format::<UIS, formats::Rgba32>(n);
+}
+
+fn test_binary_format<BIS: BinaryInputSpec, DstFormat: PixelFormatSpec, SrcFormat: PixelFormatSpec>(n: usize) {
+    let gen = BIS::gen();
+    let mut ctx = generate::Context::default();
+
+    let op = BIS::build();
+    if op.is_generic::<DstFormat, SrcFormat>() {
+        println!("Skipping generic implementation ({},{})", DstFormat::NAME, SrcFormat::NAME);
+        return
+    } else {
+        println!("Testing specific implementation ({},{})", DstFormat::NAME, SrcFormat::NAME);
+    }
+
+    for _ in 0..n {
+        let dst_img = generate::dst_image_generator::<DstFormat>(&mut ctx).expect("could not generate image");
+        let src_img = generate::src_image_generator::<SrcFormat>(&mut ctx).expect("could not generate image");
+        let mut specific_dst_img = dst_img.clone();
+        let mut generic_dst_img = dst_img.clone();
+
+        let params = gen.generate(&mut ctx).expect("could not generate parameters");
+
+        op.select::<DstFormat, SrcFormat>()(specific_dst_img.data_mut(), src_img.data(), &params);
+        <BIS as GenericBinary<BIS::Params::<'static>>>::perform::<DstFormat, SrcFormat>(generic_dst_img.data_mut(), src_img.data(), &params);
+
+        if specific_dst_img != generic_dst_img {
+            let mut path = std::path::PathBuf::new();
+            path.push(env!("CARGO_MANIFEST_DIR"));
+            path.push("test_failure");
+            println!("path: {:?}", path.as_path().to_str());
+            std::fs::write(path, format!(
+                concat!(
+                    "dst: {dst_img:?}\n",
+                    "src: {src_img:?}\n",
+                    "dst data: ({dst_fmt}) {dst_data}\n",
+                    "src data: ({src_fmt}) {src_data}\n",
+                    "generic output: {generic_data}\n",
+                    "specific output: {specific_data}\n",
+                ),
+                dst_img = dst_img,
+                src_img = src_img,
+                dst_fmt = DstFormat::NAME,
+                src_fmt = SrcFormat::NAME,
+                dst_data = hex::encode(dst_img.data().data()),
+                src_data = hex::encode(src_img.data().data()),
+                generic_data = hex::encode(generic_dst_img.data().data()),
+                specific_data = hex::encode(specific_dst_img.data().data())).as_str()).unwrap();
+            println!("image mismatch");
+            println!("dst parameters: {:?}", dst_img);
+            println!("src parameters: {:?}", src_img);
+            println!("dst data: {}", hex::encode(dst_img.data().data()));
+            println!("src data: {}", hex::encode(src_img.data().data()));
+            println!("specific output: {}", hex::encode(specific_dst_img.data().data()));
+            println!("generic output: {}", hex::encode(generic_dst_img.data().data()));
+        }
+
+        assert_eq!(specific_dst_img, generic_dst_img);
+    }
+}
+
+fn test_binary<BIS: BinaryInputSpec>(n: usize) {
+    test_binary_format::<BIS, formats::A8, formats::A8>(n);
+    test_binary_format::<BIS, formats::A8, formats::Abgr32>(n);
+    test_binary_format::<BIS, formats::A8, formats::Bgr24>(n);
+    test_binary_format::<BIS, formats::A8, formats::Bgr32>(n);
+    test_binary_format::<BIS, formats::A8, formats::Rgb24>(n);
+    test_binary_format::<BIS, formats::A8, formats::Rgb32>(n);
+    test_binary_format::<BIS, formats::A8, formats::Rgba32>(n);
+
+    test_binary_format::<BIS, formats::Abgr32, formats::A8>(n);
+    test_binary_format::<BIS, formats::Abgr32, formats::Abgr32>(n);
+    test_binary_format::<BIS, formats::Abgr32, formats::Bgr24>(n);
+    test_binary_format::<BIS, formats::Abgr32, formats::Bgr32>(n);
+    test_binary_format::<BIS, formats::Abgr32, formats::Rgb24>(n);
+    test_binary_format::<BIS, formats::Abgr32, formats::Rgb32>(n);
+    test_binary_format::<BIS, formats::Abgr32, formats::Rgba32>(n);
+
+    test_binary_format::<BIS, formats::Bgr24, formats::A8>(n);
+    test_binary_format::<BIS, formats::Bgr24, formats::Abgr32>(n);
+    test_binary_format::<BIS, formats::Bgr24, formats::Bgr24>(n);
+    test_binary_format::<BIS, formats::Bgr24, formats::Bgr32>(n);
+    test_binary_format::<BIS, formats::Bgr24, formats::Rgb24>(n);
+    test_binary_format::<BIS, formats::Bgr24, formats::Rgb32>(n);
+    test_binary_format::<BIS, formats::Bgr24, formats::Rgba32>(n);
+
+    test_binary_format::<BIS, formats::Bgr32, formats::A8>(n);
+    test_binary_format::<BIS, formats::Bgr32, formats::Abgr32>(n);
+    test_binary_format::<BIS, formats::Bgr32, formats::Bgr24>(n);
+    test_binary_format::<BIS, formats::Bgr32, formats::Bgr32>(n);
+    test_binary_format::<BIS, formats::Bgr32, formats::Rgb24>(n);
+    test_binary_format::<BIS, formats::Bgr32, formats::Rgb32>(n);
+    test_binary_format::<BIS, formats::Bgr32, formats::Rgba32>(n);
+
+    test_binary_format::<BIS, formats::Rgb24, formats::A8>(n);
+    test_binary_format::<BIS, formats::Rgb24, formats::Abgr32>(n);
+    test_binary_format::<BIS, formats::Rgb24, formats::Bgr24>(n);
+    test_binary_format::<BIS, formats::Rgb24, formats::Bgr32>(n);
+    test_binary_format::<BIS, formats::Rgb24, formats::Rgb24>(n);
+    test_binary_format::<BIS, formats::Rgb24, formats::Rgb32>(n);
+    test_binary_format::<BIS, formats::Rgb24, formats::Rgba32>(n);
+
+    test_binary_format::<BIS, formats::Rgb32, formats::A8>(n);
+    test_binary_format::<BIS, formats::Rgb32, formats::Abgr32>(n);
+    test_binary_format::<BIS, formats::Rgb32, formats::Bgr24>(n);
+    test_binary_format::<BIS, formats::Rgb32, formats::Bgr32>(n);
+    test_binary_format::<BIS, formats::Rgb32, formats::Rgb24>(n);
+    test_binary_format::<BIS, formats::Rgb32, formats::Rgb32>(n);
+    test_binary_format::<BIS, formats::Rgb32, formats::Rgba32>(n);
+
+    test_binary_format::<BIS, formats::Rgba32, formats::A8>(n);
+    test_binary_format::<BIS, formats::Rgba32, formats::Abgr32>(n);
+    test_binary_format::<BIS, formats::Rgba32, formats::Bgr24>(n);
+    test_binary_format::<BIS, formats::Rgba32, formats::Bgr32>(n);
+    test_binary_format::<BIS, formats::Rgba32, formats::Rgb24>(n);
+    test_binary_format::<BIS, formats::Rgba32, formats::Rgb32>(n);
+    test_binary_format::<BIS, formats::Rgba32, formats::Rgba32>(n);
+}
+
+#[test]
+fn fill_op() {
+    test_unary::<FillOp>(100);
+}
+
+#[test]
+fn fill_region_op() {
+    test_unary::<FillRegionOp>(100);
+}
+
+#[test]
+fn fill_region_masked_op() {
+    test_unary::<FillRegionMaskedOp>(40);
+}
+
+#[test]
+fn copy_from_op() {
+    test_binary::<CopyFromOp>(20);
+}

+ 92 - 0
src/op/generate.rs

@@ -0,0 +1,92 @@
+use std::collections::HashMap;
+
+pub trait ContextValueTag: 'static {
+    type Value: Clone;
+}
+
+#[derive(Default)]
+pub struct Context {
+    values: HashMap<std::any::TypeId, Box<dyn std::any::Any>>,
+    rng: rand::rngs::ThreadRng,
+}
+
+impl Context {
+    pub fn insert<CVT: ContextValueTag>(&mut self, value: CVT::Value) {
+        self.values.insert(std::any::TypeId::of::<CVT>(), Box::new(value) as Box<dyn std::any::Any>);
+    }
+    pub fn lookup<CVT: ContextValueTag>(&self) -> Option<&CVT::Value> {
+        let stored = self.values.get(&std::any::TypeId::of::<CVT>())?;
+
+        stored.downcast_ref::<CVT::Value>()
+    }
+    pub fn rng(&mut self) -> &mut rand::rngs::ThreadRng {
+        &mut self.rng
+    }
+}
+
+pub trait ValueGenerator<'l>: for<'a> Fn(&'a mut Context) -> Option<Self::Value> {
+    type Value: 'l + Clone;
+}
+
+impl<'l, V: 'l + Clone, F: for<'a> Fn(&'a mut Context) -> Option<V>> ValueGenerator<'l> for F {
+    type Value = V;
+}
+
+mod impls;
+pub use impls::*;
+
+pub trait ValueGeneratorList<'l> {
+    type OutputTuple: 'l;
+
+    fn generate(&self, ctx: &mut Context) -> Option<Self::OutputTuple>;
+}
+
+impl<'l> ValueGeneratorList<'l> for () {
+    type OutputTuple = ();
+    fn generate(&self, _ctx: &mut Context) -> Option<Self::OutputTuple> {
+        Some(())
+    }
+}
+impl<'l, V0: ValueGenerator<'l>> ValueGeneratorList<'l> for (V0,) {
+    type OutputTuple = (V0::Value,);
+
+    fn generate(&self, ctx: &mut Context) -> Option<Self::OutputTuple> {
+        Some((self.0(ctx)?,))
+    }
+}
+impl<'l, V0: ValueGenerator<'l>, V1: ValueGenerator<'l>> ValueGeneratorList<'l> for (V0,V1) {
+    type OutputTuple = (V0::Value,V1::Value);
+
+    fn generate(&self, ctx: &mut Context) -> Option<Self::OutputTuple> {
+        Some((self.0(ctx)?,self.1(ctx)?))
+    }
+}
+impl<'l, V0: ValueGenerator<'l>, V1: ValueGenerator<'l>, V2: ValueGenerator<'l>> ValueGeneratorList<'l> for (V0,V1,V2) {
+    type OutputTuple = (V0::Value,V1::Value,V2::Value);
+
+    fn generate(&self, ctx: &mut Context) -> Option<Self::OutputTuple> {
+        Some((self.0(ctx)?,self.1(ctx)?,self.2(ctx)?))
+    }
+}
+impl<'l, V0: ValueGenerator<'l>, V1: ValueGenerator<'l>, V2: ValueGenerator<'l>, V3: ValueGenerator<'l>> ValueGeneratorList<'l> for (V0,V1,V2,V3) {
+    type OutputTuple = (V0::Value,V1::Value,V2::Value,V3::Value);
+
+    fn generate(&self, ctx: &mut Context) -> Option<Self::OutputTuple> {
+        Some((self.0(ctx)?,self.1(ctx)?,self.2(ctx)?,self.3(ctx)?))
+    }
+}
+impl<'l, V0: ValueGenerator<'l>, V1: ValueGenerator<'l>, V2: ValueGenerator<'l>, V3: ValueGenerator<'l>, V4: ValueGenerator<'l>> ValueGeneratorList<'l> for (V0,V1,V2,V3,V4) {
+    type OutputTuple = (V0::Value,V1::Value,V2::Value,V3::Value,V4::Value);
+
+    fn generate(&self, ctx: &mut Context) -> Option<Self::OutputTuple> {
+        Some((self.0(ctx)?,self.1(ctx)?,self.2(ctx)?,self.3(ctx)?,self.4(ctx)?))
+    }
+}
+
+pub trait UnaryInputSpec : super::UnaryOpSpec {
+    fn gen<'l>() -> impl ValueGeneratorList<'l, OutputTuple = Self::Params<'l>>;
+}
+
+pub trait BinaryInputSpec : super::BinaryOpSpec {
+    fn gen<'l>() -> impl ValueGeneratorList<'l, OutputTuple = Self::Params<'l>>;
+}

+ 96 - 0
src/op/generate/impls.rs

@@ -0,0 +1,96 @@
+use std::ops::Range;
+use rand::{prelude::*, distributions::{Uniform, Distribution, Standard, uniform::SampleUniform}, thread_rng, rngs::ThreadRng, Fill};
+
+use crate::{math::{PixelPoint, PixelSize, PixelBox}, formats::{PixelFormatSpec, self}, Bitmap, prelude::*, colour::Colour};
+
+use super::{Context, ContextValueTag};
+
+const DEFAULT_SIZE_BOUNDS: Range<i32> = 4..64;
+pub struct ImageSizeBounds;
+impl ContextValueTag for ImageSizeBounds {
+    type Value = Range<i32>;
+}
+
+pub struct DstSize;
+impl ContextValueTag for DstSize {
+    type Value = PixelSize;
+}
+
+pub struct SrcSize;
+impl ContextValueTag for SrcSize {
+    type Value = PixelSize;
+}
+
+fn point_in_size(rng: &mut ThreadRng, sz: &PixelSize) -> PixelPoint {
+    let xdist = Uniform::new(0, sz.width);
+    let ydist = Uniform::new(0, sz.height);
+    PixelPoint::new(
+        xdist.sample(rng),
+        ydist.sample(rng)
+    )
+}
+
+pub fn constant<V: Clone>(v: V) -> impl Fn(&mut Context) -> Option<V> {
+    move |_| Some(v.clone())
+}
+
+pub fn uniform<V: SampleUniform + Clone>(from: V, to: V) -> impl Fn(&mut Context) -> Option<V> {
+    move |ctx| {
+        let uniform = Uniform::new(from.clone(), to.clone());
+        Some(ctx.rng().sample(uniform))
+    }
+}
+
+pub fn random_colour(ctx: &mut Context) -> Option<Colour> {
+    let rng = ctx.rng();
+    Some(Colour::rgba(rng.gen(), rng.gen(), rng.gen(), rng.gen()))
+}
+
+pub fn dst_image_generator<Format: PixelFormatSpec>(ctx: &mut Context) -> Option<Bitmap<Format>> {
+    let size_bounds = ctx.lookup::<ImageSizeBounds>().unwrap_or(&DEFAULT_SIZE_BOUNDS);
+    let size_dist = Uniform::new(size_bounds.start, size_bounds.end);
+    let mut bitmap = Bitmap::new(size_dist.sample(ctx.rng()) as usize, size_dist.sample(ctx.rng()) as usize);
+    bitmap.data_mut().data_mut().try_fill(ctx.rng()).ok()?;
+    ctx.insert::<DstSize>(bitmap.size());
+    Some(bitmap)
+}
+
+pub fn src_image_generator<Format: PixelFormatSpec>(ctx: &mut Context) -> Option<Bitmap<Format>> {
+    let size_bounds = ctx.lookup::<ImageSizeBounds>().unwrap_or(&DEFAULT_SIZE_BOUNDS);
+    let size_dist = Uniform::new(size_bounds.start, size_bounds.end);
+    let mut bitmap = Bitmap::new(size_dist.sample(ctx.rng()) as usize, size_dist.sample(ctx.rng()) as usize);
+    bitmap.data_mut().data_mut().try_fill(ctx.rng()).ok()?;
+    ctx.insert::<SrcSize>(bitmap.size());
+    Some(bitmap)
+}
+
+pub fn alphamap<'l>(ctx: &mut Context) -> Option<&'l dyn BitmapAccess<formats::A8>> {
+    None
+}
+
+pub fn alphamap_area(ctx: &mut Context) -> Option<PixelBox> {
+    None
+}
+
+pub fn dst_point(ctx: &mut Context) -> Option<PixelPoint> {
+    let mut rng = thread_rng();
+    Some(point_in_size(&mut rng, ctx.lookup::<DstSize>()?))
+}
+
+pub fn dst_area(ctx: &mut Context) -> Option<PixelBox> {
+    let mut rng = thread_rng();
+    let src_size = ctx.lookup::<DstSize>()?;
+    Some(PixelBox::new(
+        point_in_size(&mut rng, src_size),
+        point_in_size(&mut rng, src_size)
+    ))
+}
+
+pub fn src_area(ctx: &mut Context) -> Option<PixelBox> {
+    let mut rng = thread_rng();
+    let src_size = ctx.lookup::<SrcSize>()?;
+    Some(PixelBox::new(
+        point_in_size(&mut rng, src_size),
+        point_in_size(&mut rng, src_size)
+    ))
+}