use crate::avm2::{Object as Avm2Object, Value as Avm2Value}; use crate::display_object::{DisplayObject, TDisplayObject}; use bitflags::bitflags; use gc_arena::Collect; use ruffle_render::backend::RenderBackend; use ruffle_render::bitmap::{Bitmap, BitmapFormat, BitmapHandle, PixelRegion, SyncHandle}; use ruffle_wstr::WStr; use std::fmt::Debug; use std::ops::Range; use swf::{Rectangle, Twips}; use tracing::instrument; /// An implementation of the Lehmer/Park-Miller random number generator /// Uses the fixed parameters m = 2,147,483,647 and a = 16,807 pub struct LehmerRng { x: u32, } impl LehmerRng { pub fn with_seed(seed: u32) -> Self { Self { x: seed } } /// Generate the next value in the sequence via the following formula /// X_(k+1) = a * X_k mod m pub fn gen(&mut self) -> u32 { self.x = ((self.x as u64).overflowing_mul(16_807).0 % 2_147_483_647) as u32; self.x } pub fn gen_range(&mut self, rng: Range) -> u8 { rng.start + (self.gen() % ((rng.end - rng.start) as u32 + 1)) as u8 } } /// This can represent both a premultiplied and an unmultiplied ARGB color value. /// /// Note that most operations only make sense on one of these representations: /// For example, blending on premultiplied values, and applying a `ColorTransform` on /// unmultiplied values. Make sure to convert the color to the correct form beforehand. // TODO: Maybe split the type into `PremultipliedColor(u32)` and // `UnmultipliedColor(u32)`? #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Collect)] #[collect(no_drop)] pub struct Color(u32); #[derive(Debug, Clone)] pub enum BitmapDataDrawError { Unimplemented, } impl Color { pub fn blue(&self) -> u8 { (self.0 & 0xFF) as u8 } pub fn green(&self) -> u8 { ((self.0 >> 8) & 0xFF) as u8 } pub fn red(&self) -> u8 { ((self.0 >> 16) & 0xFF) as u8 } pub fn alpha(&self) -> u8 { ((self.0 >> 24) & 0xFF) as u8 } #[must_use] pub fn to_premultiplied_alpha(self, transparency: bool) -> Self { // This has some accuracy issues with some alpha values let old_alpha = if transparency { self.alpha() } else { 255 }; let a = old_alpha as u32; let r = ((self.red() as u32 * a + 127) / 255) as u8; let g = ((self.green() as u32 * a + 127) / 255) as u8; let b = ((self.blue() as u32 * a + 127) / 255) as u8; Self::argb(old_alpha, r, g, b) } #[must_use] pub fn to_un_multiplied_alpha(self) -> Self { // We need to match Flash's results, and this lookup table was generated by brute force. // For each alpha value, every value between 0..256^3 was tested to see if it produced the // correct color value when reversing the premultiplication. // Source code used to generate this table can be found at: // https://gist.github.com/pdewacht/614b428cd42c2052dc0fd292516c9f9f const FLASH_PREMUL_FACTOR: [u32; 256] = [ 0, 16678912, 8339456, 5559638, 4169728, 3335783, 2779819, 2386603, 2086230, 1855488, 1667892, 1518251, 1391151, 1285234, 1193302, 1111928, 1043895, 981113, 927744, 879275, 834621, 795535, 759126, 726358, 695839, 668183, 642538, 618737, 596651, 576171, 555964, 538706, 522104, 506319, 490557, 477321, 464038, 451353, 439544, 428244, 417582, 407500, 397768, 388535, 379630, 371117, 363179, 355235, 348050, 340965, 334052, 327038, 321269, 315077, 309159, 303586, 298189, 293092, 287981, 283080, 278251, 273892, 269268, 265179, 261087, 256971, 253160, 249322, 245508, 242164, 238575, 235245, 231859, 228848, 225785, 222712, 219616, 216827, 213985, 211432, 208835, 206075, 203750, 201196, 198895, 196223, 194301, 191987, 189686, 187636, 185559, 183426, 181453, 179444, 177638, 175855, 174054, 171948, 170489, 168695, 166889, 165365, 163519, 162045, 160508, 158970, 157429, 156150, 154610, 153081, 151803, 150511, 148986, 147709, 146420, 145116, 143868, 142586, 141545, 140277, 139194, 137957, 136954, 135676, 134652, 133621, 132604, 131577, 130552, 129527, 128508, 127476, 126451, 125432, 124670, 123645, 122818, 121847, 121082, 120060, 119288, 118263, 117502, 116720, 115967, 115195, 114424, 113655, 112893, 112125, 111356, 110563, 109811, 109048, 108287, 107766, 107004, 106236, 105724, 104953, 104434, 103676, 102904, 102375, 101879, 101119, 100604, 99834, 99321, 98813, 98112, 97533, 97019, 96509, 95994, 95486, 94713, 94185, 93689, 93179, 92667, 92149, 91643, 91129, 90621, 90068, 89597, 89342, 88829, 88318, 87804, 87294, 87034, 86523, 85994, 85499, 85245, 84732, 84222, 83956, 83450, 82937, 82685, 82173, 81840, 81405, 80889, 80638, 80127, 79862, 79354, 79103, 78590, 78332, 78077, 77565, 77308, 76795, 76541, 76284, 75766, 75518, 75262, 74748, 74493, 74238, 73691, 73470, 73214, 72959, 72447, 72189, 71935, 71671, 71166, 70911, 70651, 70399, 70140, 69886, 69615, 69116, 68861, 68603, 68350, 68093, 67839, 67576, 67326, 67070, 66813, 66556, 66302, 66046, 65791, 65408, ]; let alpha_factor = FLASH_PREMUL_FACTOR[self.alpha() as usize]; let unmultiply = |c| ((c as u32 * alpha_factor + 0x8000) >> 16) as u8; let r = unmultiply(self.red()); let g = unmultiply(self.green()); let b = unmultiply(self.blue()); Self::argb(self.alpha(), r, g, b) } #[must_use] pub fn argb(alpha: u8, red: u8, green: u8, blue: u8) -> Self { Self(u32::from_le_bytes([blue, green, red, alpha])) } #[must_use] pub fn with_alpha(&self, alpha: u8) -> Self { Self::argb(alpha, self.red(), self.green(), self.blue()) } /// # Arguments /// /// * `self` - Must be in premultiplied form. /// * `source` - Must be in premultiplied form. #[must_use] pub fn blend_over(&self, source: &Self) -> Self { let sa = source.alpha(); let r = source.red() + ((self.red() as u16 * (255 - sa as u16)) >> 8) as u8; let g = source.green() + ((self.green() as u16 * (255 - sa as u16)) >> 8) as u8; let b = source.blue() + ((self.blue() as u16 * (255 - sa as u16)) >> 8) as u8; let a = source.alpha() + ((self.alpha() as u16 * (255 - sa as u16)) >> 8) as u8; Self::argb(a, r, g, b) } } impl std::fmt::Display for Color { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&format!("{:#x}", self.0)) } } impl From for u32 { fn from(c: Color) -> Self { c.0 } } impl From for Color { fn from(i: u32) -> Self { Color(i) } } impl From for Color { fn from(c: swf::Color) -> Self { Self::argb(c.a, c.r, c.g, c.b) } } impl From for swf::Color { fn from(c: Color) -> Self { let r = c.red(); let g = c.green(); let b = c.blue(); let a = c.alpha(); Self { r, g, b, a } } } bitflags! { pub struct ChannelOptions: u8 { const RED = 1 << 0; const GREEN = 1 << 1; const BLUE = 1 << 2; const ALPHA = 1 << 3; const RGB = Self::RED.bits() | Self::GREEN.bits() | Self::BLUE.bits(); } } #[derive(Clone, Collect)] #[collect(no_drop)] pub struct BitmapData<'gc> { /// The pixels in the bitmap, stored as a array of pre-multiplied ARGB colour values pixels: Vec, width: u32, height: u32, transparency: bool, // Note that it's technically possible to have a BitmapData with zero width and height, // (by embedding it in the SWF instead of using the BitmapData constructor), // so we need a separate 'disposed' flag. disposed: bool, /// The bitmap handle for this data. /// /// This is lazily initialized; a value of `None` indicates that /// initialization has not yet happened. #[collect(require_static)] bitmap_handle: Option, /// The AVM2 side of this `BitmapData`. /// /// AVM1 cannot retrieve `BitmapData` back from the display object tree, so /// this does not need to hold an AVM1 object. avm2_object: Option>, dirty_state: DirtyState, } #[derive(Clone, Collect, Debug)] #[collect(require_static)] enum DirtyState { // Both the CPU and GPU pixels are up to date. We do not need to wait for any syncs to complete Clean, // The CPU pixels have been modified, and need to be synced to the GPU via `update_dirty_texture` CpuModified(PixelRegion), // The GPU pixels have been modified, and need to be synced to the CPU via `BitmapDataWrapper::sync` GpuModified(Box, PixelRegion), } mod wrapper { use crate::avm2::{Object as Avm2Object, Value as Avm2Value}; use crate::context::RenderContext; use gc_arena::{Collect, GcCell, MutationContext}; use ruffle_render::backend::RenderBackend; use ruffle_render::bitmap::{BitmapHandle, PixelRegion}; use ruffle_render::commands::CommandHandler; use std::cell::Ref; use super::{copy_pixels_to_bitmapdata, BitmapData, DirtyState}; /// A wrapper type that ensures that we always wait for a pending /// GPU -> CPU sync to complete (using `sync_handle`) before accessing /// the CPU-side pixels. /// /// This is overly conservative - we perform a sync before allowing any access /// to the underlying `BitmapData`, even if we wouldn't be accessing the pixels. /// Implementing more fine-grained tracking turned out to be extremely invasive, /// and made the code much less readable. This should be enough for the simple /// case where ActionScript calls `BitmapData.draw`, and then doesn't interact /// with the Bitmap/BitmapData object at all for some time. /// /// There are three ways that this type gets used: /// 1. Blocking on the current GPU->CPU sync via the `sync` method, /// and obtainng a `GcCell<'gc, BitmapData<'gc>>` (or implicily through `as_bitmap_data`). /// This is done for the vast majority of BitmapData AS2/AS3 methods, as they need to access CPU-side pixels. /// 2. Ignoring the current GPU->CPU sync state. This is done by the `render` method defined on this type, /// since rendering only uses GPU-side data, and ignores CPU-side pixels entirely. /// 3. Explicitly cancelling any in-progress GPU->CPU sync via `overwrite_cpu_pixels_from_gpu`. This is /// used by `BitmapData.draw` and `BitmapData.apply_filter`, since the new rendering result will completely /// replace the current CPU-side pixels. This performs a CPU -> GPU sync, to ensure that the GPU side /// is up to date before we overwrite the CPU-side pixels. /// In the future, we could explore using this in additional /// cases where we know that the entire CPU-side pixel array will be overwritten without being read /// (e.g. `BitmapData.fillRect` with a rectangle covering the entire bitmap). However, `overwrite_cpu_pixels` /// is always a performance optimization, and can always be safely replaced with `sync` (at the cost of worse performance) /// /// Note that we also perform CPU-GPU syncs from `BitmapData.update_dirty_texture` when `dirty` is set. /// `sync_handle` and `dirty` can never be set at the same time - we can only have one of them set, or none of them set. #[derive(Copy, Clone, Collect, Debug)] #[collect(no_drop)] pub struct BitmapDataWrapper<'gc>(GcCell<'gc, BitmapData<'gc>>); impl<'gc> BitmapDataWrapper<'gc> { pub fn new(data: GcCell<'gc, BitmapData<'gc>>) -> Self { BitmapDataWrapper(data) } // Creates a dummy BitmapData with no pixels or handle, marked as disposed. // This is used for AS3 `Bitmap` instances without a corresponding AS3 `BitmapData` instance. // Marking it as disposed skips rendering, and the unset `avm2_object` will cause this to // be inaccessible to AS3 code. pub fn dummy(mc: MutationContext<'gc, '_>) -> Self { BitmapDataWrapper(GcCell::allocate( mc, BitmapData { pixels: Vec::new(), width: 0, height: 0, transparency: false, disposed: true, bitmap_handle: None, avm2_object: None, dirty_state: DirtyState::Clean, }, )) } // Provides access to the underlying `BitmapData`. If a GPU -> CPU sync // is in progress, waits for it to complete pub fn sync(&self) -> GcCell<'gc, BitmapData<'gc>> { // SAFETY: The only field that can store gc pointers is `avm2_object`, // which we don't update here. Ideally, we would refactor this so that // `BitmapData` doesn't contain any gc pointers, allowing us to use a normal // `RefCell` instead of a `GcCell`. let mut write = unsafe { self.0.borrow_mut() }; match std::mem::replace(&mut write.dirty_state, DirtyState::Clean) { DirtyState::GpuModified(sync_handle, bounds) => { sync_handle .retrieve_offscreen_texture(Box::new(|buffer, buffer_width| { copy_pixels_to_bitmapdata(&mut write, buffer, buffer_width, bounds) })) .expect("Failed to sync BitmapData"); write.dirty_state = DirtyState::Clean } old_state => write.dirty_state = old_state, } self.0 } /// Provides access to the underlying `BitmapHandle`. /// If the CPU pixels are dirty, syncs them to the GPU. /// If the GPU pixels are dirty, then handle is returned immediately /// without waiting for the sync to complete, as a BitmapHandle can /// only be used to access the GPU data. Unlike `overwrite_cpu_pixels_from_gpu`, /// this does not cancel the GPU -> CPU sync. pub fn bitmap_handle( &self, gc_context: MutationContext<'gc, '_>, renderer: &mut dyn RenderBackend, ) -> BitmapHandle { let mut bitmap_data = self.0.write(gc_context); bitmap_data.update_dirty_texture(renderer); bitmap_data.bitmap_handle(renderer).unwrap() } /// Provides access to the underlying `BitmapData`. /// This should only be used when you will be overwriting the entire /// `pixels` vec without reading from it. Cancels any in-progress GPU -> CPU sync. /// This does not sync from cpu to gpu. #[allow(clippy::type_complexity)] pub fn overwrite_cpu_pixels_from_gpu( &self, mc: MutationContext<'gc, '_>, ) -> (GcCell<'gc, BitmapData<'gc>>, Option) { let mut write = self.0.write(mc); let dirty_rect = match write.dirty_state { DirtyState::GpuModified(_, rect) => { write.dirty_state = DirtyState::Clean; Some(rect) } DirtyState::CpuModified(_) | DirtyState::Clean => None, }; (self.0, dirty_rect) } /// Provides read access to the BitmapData pixels. /// Only the provided region is guaranteed to be up-to-date. /// It is an error to access any other pixels outside of that region. pub fn read_area(&self, read_area: PixelRegion) -> Ref<'_, BitmapData<'gc>> { let needs_update = if let DirtyState::GpuModified(_, area) = self.0.read().dirty_state { area.intersects(read_area) } else { false }; if needs_update { self.sync(); } self.0.read() } // These methods do not require a sync to complete, as they do not depend on the // CPU-side pixels. They are implemented directly on `BitmapDataWrapper`, allowing // callers to avoid calling sync() pub fn height(&self) -> u32 { self.0.read().height } pub fn width(&self) -> u32 { self.0.read().width } pub fn object2(&self) -> Avm2Value<'gc> { self.0.read().object2() } pub fn disposed(&self) -> bool { self.0.read().disposed } pub fn transparency(&self) -> bool { self.0.read().transparency } pub fn check_valid( &self, activation: &mut crate::avm2::Activation<'_, 'gc>, ) -> Result<(), crate::avm2::Error<'gc>> { if self.disposed() { return Err(crate::avm2::Error::AvmError( crate::avm2::error::argument_error( activation, "Error #2015: Invalid BitmapData.", 2015, )?, )); } Ok(()) } pub fn dispose(&self, mc: MutationContext<'gc, '_>) { self.0.write(mc).dispose(); } pub fn init_object2(&self, mc: MutationContext<'gc, '_>, object: Avm2Object<'gc>) { self.0.write(mc).avm2_object = Some(object) } pub fn render(&self, smoothing: bool, context: &mut RenderContext<'_, 'gc>) { let mut inner_bitmap_data = self.0.write(context.gc_context); if inner_bitmap_data.disposed() { return; } // Note - we do a CPU -> GPU sync, but we do *not* do a GPU -> CPU sync // (rendering is done on the GPU, so the CPU pixels don't need to be up-to-date). inner_bitmap_data.update_dirty_texture(context.renderer); let handle = inner_bitmap_data .bitmap_handle(context.renderer) .expect("Missing bitmap handle"); context .commands .render_bitmap(handle, context.transform_stack.transform(), smoothing); } pub fn can_read(&self, read_area: PixelRegion) -> bool { if let DirtyState::GpuModified(_, area) = self.0.read().dirty_state { !area.intersects(read_area) } else { true } } #[cfg(feature = "egui")] pub fn debug_sync_status(&self) -> std::borrow::Cow<'static, str> { match self.0.read().dirty_state { DirtyState::Clean => std::borrow::Cow::Borrowed("Clean"), DirtyState::CpuModified(area) => std::borrow::Cow::Owned(format!( "CPU modified from {}, {} to {}, {}", area.x_min, area.y_min, area.x_max, area.y_max )), DirtyState::GpuModified(_, area) => std::borrow::Cow::Owned(format!( "GPU modified from {}, {} to {}, {}", area.x_min, area.y_min, area.x_max, area.y_max )), } } pub fn is_point_in_bounds(&self, x: i32, y: i32) -> bool { x >= 0 && x < self.width() as i32 && y >= 0 && y < self.height() as i32 } pub fn ptr_eq(&self, other: BitmapDataWrapper<'gc>) -> bool { GcCell::ptr_eq(self.0, other.0) } } } pub use wrapper::BitmapDataWrapper; impl std::fmt::Debug for BitmapData<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BitmapData") .field("dirty_state", &self.dirty_state) .field("width", &self.width) .field("height", &self.height) .field("transparency", &self.transparency) .field("disposed", &self.disposed) .field("bitmap_handle", &self.bitmap_handle) .finish() } } impl<'gc> BitmapData<'gc> { pub fn new(width: u32, height: u32, transparency: bool, fill_color: u32) -> Self { Self { pixels: vec![ Color(fill_color).to_premultiplied_alpha(transparency); width as usize * height as usize ], width, height, transparency, disposed: false, bitmap_handle: None, avm2_object: None, dirty_state: DirtyState::Clean, } } pub fn new_with_pixels( width: u32, height: u32, transparency: bool, pixels: Vec, ) -> Self { Self { pixels, width, height, transparency, bitmap_handle: None, avm2_object: None, disposed: false, dirty_state: DirtyState::Clean, } } pub fn disposed(&self) -> bool { self.disposed } pub fn dispose(&mut self) { self.width = 0; self.height = 0; self.pixels.clear(); self.bitmap_handle = None; // There's no longer a handle to update self.dirty_state = DirtyState::Clean; self.disposed = true; } pub fn bitmap_handle(&mut self, renderer: &mut dyn RenderBackend) -> Option { if self.bitmap_handle.is_none() { let bitmap = Bitmap::new( self.width(), self.height(), BitmapFormat::Rgba, self.pixels_rgba(), ); let bitmap_handle = renderer.register_bitmap(bitmap); if let Err(e) = &bitmap_handle { tracing::warn!("Failed to register raw bitmap for BitmapData: {:?}", e); } self.bitmap_handle = bitmap_handle.ok(); } self.bitmap_handle.clone() } pub fn transparency(&self) -> bool { self.transparency } pub fn set_gpu_dirty(&mut self, sync_handle: Box, region: PixelRegion) { self.dirty_state = DirtyState::GpuModified(sync_handle, region); } pub fn set_cpu_dirty(&mut self, region: PixelRegion) { debug_assert!(region.x_max <= self.width); debug_assert!(region.y_max <= self.height); match &mut self.dirty_state { DirtyState::CpuModified(old_region) => old_region.union(region), DirtyState::Clean => self.dirty_state = DirtyState::CpuModified(region), DirtyState::GpuModified(_, _) => { panic!("Attempted to modify CPU dirty state while GPU sync is in progress!") } } } pub fn pixels(&self) -> &[Color] { &self.pixels } pub fn pixels_rgba(&self) -> Vec { // TODO: This could have been implemented as follows: // // self.pixels // .iter() // .flat_map(|p| [p.red(), p.green(), p.blue(), p.alpha()]) // .collect() // // But currently Rust emits suboptimal code in that case. For now we use // `Vec::with_capacity` manually to avoid unnecessary re-allocations. let mut output = Vec::with_capacity(self.pixels.len() * 4); for p in &self.pixels { output.extend_from_slice(&[p.red(), p.green(), p.blue(), p.alpha()]) } output } pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } pub fn is_point_in_bounds(&self, x: i32, y: i32) -> bool { x >= 0 && x < self.width() as i32 && y >= 0 && y < self.height() as i32 } #[inline] pub fn set_pixel32_raw(&mut self, x: u32, y: u32, color: Color) { self.pixels[(x + y * self.width) as usize] = color; } #[inline] pub fn get_pixel32_raw(&self, x: u32, y: u32) -> Color { self.pixels[(x + y * self.width()) as usize] } pub fn raw_pixels_mut(&mut self) -> &mut Vec { &mut self.pixels } pub fn raw_pixels(&self) -> &[Color] { &self.pixels } // Updates the data stored with our `BitmapHandle` if this `BitmapData` // is dirty pub fn update_dirty_texture(&mut self, renderer: &mut dyn RenderBackend) { let handle = self.bitmap_handle(renderer).unwrap(); match &self.dirty_state { DirtyState::CpuModified(region) => { if let Err(e) = renderer.update_texture( &handle, Bitmap::new( self.width(), self.height(), BitmapFormat::Rgba, self.pixels_rgba(), ), *region, ) { tracing::error!("Failed to update dirty bitmap {:?}: {:?}", handle, e); } self.dirty_state = DirtyState::Clean; } DirtyState::Clean | DirtyState::GpuModified(_, _) => {} } } pub fn object2(&self) -> Avm2Value<'gc> { self.avm2_object .map(|o| o.into()) .unwrap_or(Avm2Value::Null) } pub fn init_object2(&mut self, object: Avm2Object<'gc>) { self.avm2_object = Some(object) } } pub enum IBitmapDrawable<'gc> { BitmapData(BitmapDataWrapper<'gc>), DisplayObject(DisplayObject<'gc>), } impl IBitmapDrawable<'_> { pub fn bounds(&self) -> Rectangle { match self { IBitmapDrawable::BitmapData(bmd) => Rectangle { x_min: Twips::ZERO, x_max: Twips::from_pixels(bmd.width() as f64), y_min: Twips::ZERO, y_max: Twips::from_pixels(bmd.height() as f64), }, IBitmapDrawable::DisplayObject(o) => o.bounds(), } } } #[instrument(level = "debug", skip_all)] fn copy_pixels_to_bitmapdata( write: &mut BitmapData, buffer: &[u8], buffer_width: u32, area: PixelRegion, ) { let buffer_width_pixels = buffer_width / 4; for y in area.y_min..area.y_max { for x in area.x_min..area.x_max { // note: this order of conversions helps llvm realize the index is 4-byte-aligned let ind = (((x - area.x_min) + (y - area.y_min) * buffer_width_pixels) as usize) * 4; // TODO(mid): optimize this A LOT let r = buffer[ind]; let g = buffer[ind + 1usize]; let b = buffer[ind + 2usize]; let a = if write.transparency() { buffer[ind + 3usize] } else { 255 }; // TODO(later): we might want to swap Color storage from argb to rgba, to make it cheaper let nc = Color::argb(a, r, g, b); // Ignore the original color entirely - the blending (including alpha) // was done by the renderer when it wrote over the previous texture contents. write.set_pixel32_raw(x, y, nc); } } write.set_cpu_dirty(area); } #[derive(Copy, Clone, Debug)] pub enum ThresholdOperation { Equals, NotEquals, LessThan, LessThanOrEquals, GreaterThan, GreaterThanOrEquals, } impl ThresholdOperation { pub fn from_wstr(str: &WStr) -> Option { if str == b"==" { Some(Self::Equals) } else if str == b"!=" { Some(Self::NotEquals) } else if str == b"<" { Some(Self::LessThan) } else if str == b"<=" { Some(Self::LessThanOrEquals) } else if str == b">" { Some(Self::GreaterThan) } else if str == b">=" { Some(Self::GreaterThanOrEquals) } else { None } } pub fn matches(&self, value: u32, masked_threshold: u32) -> bool { match self { ThresholdOperation::Equals => value == masked_threshold, ThresholdOperation::NotEquals => value != masked_threshold, ThresholdOperation::LessThan => value < masked_threshold, ThresholdOperation::LessThanOrEquals => value <= masked_threshold, ThresholdOperation::GreaterThan => value > masked_threshold, ThresholdOperation::GreaterThanOrEquals => value >= masked_threshold, } } }