diff --git a/moli-renderer-v8/src/blob.rs b/moli-renderer-v8/src/blob.rs index 0607c49ead..ea3f0f828c 100644 --- a/moli-renderer-v8/src/blob.rs +++ b/moli-renderer-v8/src/blob.rs @@ -295,7 +295,7 @@ pub(super) fn build_blob_object<'s>( Some(object) } -fn blob_id_from_object<'s>( +pub(crate) fn blob_id_from_object<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, ) -> Option { diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index f0669fc848..e08d3dc9d9 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -226,9 +226,10 @@ pub(crate) use self::broadcast_channel::{ dispatch_authorized_page_broadcast_channel_event, dispatch_broadcast_channel_events_for_channel, }; pub(crate) use self::canvas::{ - CanvasContextKind, attach_canvas_like_context_object, build_canvas_rendering_context_2d_object, - build_offscreen_canvas_object, build_webgl_context_object, build_webgl2_context_object, - canvas_like_to_data_url, reset_html_canvas_backing_store_for_dimension_assignment, + BitmapRejection, BitmapTaskResult, CanvasContextKind, attach_canvas_like_context_object, + build_canvas_rendering_context_2d_object, build_offscreen_canvas_object, + build_webgl_context_object, build_webgl2_context_object, canvas_like_to_data_url, + reset_html_canvas_backing_store_for_dimension_assignment, settle_bitmap_task_result, }; use self::constructors::illegal_constructor_callback; pub(crate) use self::constructors::{ diff --git a/moli-renderer-v8/src/context_bootstrap/canvas.rs b/moli-renderer-v8/src/context_bootstrap/canvas.rs index 49185bd56d..39eb72a3ad 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas.rs @@ -242,7 +242,10 @@ pub(crate) use context2d::{ canvas_context_transform_callback, canvas_context_translate_callback, canvas_gradient_add_color_stop_callback, }; -pub(crate) use image_bitmap::window_create_image_bitmap_callback; +pub(crate) use image_bitmap::{ + BitmapRejection, BitmapTaskResult, settle_bitmap_task_result, + window_create_image_bitmap_callback, +}; pub(crate) use objects::{ build_canvas_rendering_context_2d_object, build_offscreen_canvas_object, build_webgl_context_object, build_webgl2_context_object, diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs b/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs index fa3eb1a1cf..03610804cd 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs @@ -43,14 +43,6 @@ pub(crate) fn attach_canvas_like_context_object<'s>( let _ = ensure_canvas_like_backing_store(scope, canvas); } -pub(super) fn canvas_like_has_context<'s>( - scope: &mut v8::PinScope<'s, '_>, - canvas: v8::Local<'s, v8::Object>, -) -> bool { - get_private_value(scope, canvas, CANVAS_HAS_CONTEXT_SLOT) - .is_some_and(|value| value.boolean_value(scope)) -} - pub(crate) fn reset_canvas_like_backing_store<'s>( scope: &mut v8::PinScope<'s, '_>, canvas: v8::Local<'s, v8::Object>, @@ -221,6 +213,13 @@ pub(super) fn canvas_like_dimensions<'s>( scope: &mut v8::PinScope<'s, '_>, canvas: v8::Local<'s, v8::Object>, ) -> Option<(u32, u32)> { + if html_canvas_identity(scope, canvas).is_some() { + let width = + crate::native_bridge::element::canvas_dimension_value(scope, canvas, "width", 300); + let height = + crate::native_bridge::element::canvas_dimension_value(scope, canvas, "height", 150); + return Some((width, height)); + } let width = canvas_like_dimension(scope, canvas, OFFSCREEN_CANVAS_WIDTH_SLOT, "width")?; let height = canvas_like_dimension(scope, canvas, OFFSCREEN_CANVAS_HEIGHT_SLOT, "height")?; Some((width, height)) diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs b/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs index 4880584f22..9dffda7735 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs @@ -1657,8 +1657,22 @@ pub(crate) fn canvas_context_draw_image_callback<'s>( let Ok(source) = v8::Local::::try_from(args.get(0)) else { return; }; - let Some((source_pixels, source_width, source_height)) = - html_image_pixels_copy(scope, source).or_else(|| canvas_like_pixels_copy(scope, source)) + let bitmap_pixels = if super::image_bitmap::image_bitmap_receiver_branded(scope, source) { + let Some(pixels) = super::image_bitmap::image_bitmap_pixels_copy(scope, source) else { + crate::context_bootstrap::throw_dom_exception_value( + scope, + "The ImageBitmap is detached.", + "InvalidStateError", + ); + return; + }; + Some(pixels) + } else { + None + }; + let Some((source_pixels, source_width, source_height)) = bitmap_pixels + .or_else(|| html_image_pixels_copy(scope, source)) + .or_else(|| canvas_like_pixels_copy(scope, source)) else { return; }; diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap.rs b/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap.rs index f6bfafb26e..fb3093189b 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap.rs @@ -1,15 +1,34 @@ -use super::backing_store::{canvas_like_has_context, canvas_like_pixels_copy}; +use super::backing_store::canvas_like_pixels_copy; use super::offscreen::offscreen_canvas_receiver_branded; use super::*; use crate::context_bootstrap::new_dom_exception_value; use crate::util::{ - callback_data_index_value, callback_data_item, get_private_value, set_private_value, + callback_data_index_value, callback_data_item, context_host_ptr_from_global_bridge, + get_private_value, set_private_value, }; use crate::web_api_interfaces; use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; +mod options; +use options::{BitmapOptions, BitmapParameters}; + +#[derive(Debug)] +pub(crate) struct BitmapTaskResult { + width: u32, + height: u32, + pixels: Vec, + premultiplied: bool, +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum BitmapRejection { + InvalidState, +} + const IMAGE_BITMAP_WIDTH_SLOT: &str = "__moliImageBitmapWidth"; const IMAGE_BITMAP_HEIGHT_SLOT: &str = "__moliImageBitmapHeight"; +const IMAGE_BITMAP_PIXELS_SLOT: &str = "__moliImageBitmapPixels"; +const IMAGE_BITMAP_PREMULTIPLIED_SLOT: &str = "__moliImageBitmapPremultiplied"; const IMAGE_BITMAP_DIMENSION_SLOTS: &[&str] = &[IMAGE_BITMAP_WIDTH_SLOT, IMAGE_BITMAP_HEIGHT_SLOT]; @@ -60,66 +79,278 @@ pub(crate) fn window_create_image_bitmap_callback<'s>( throw_type_error(scope, "createImageBitmap is not a constructor"); return; } - if args.length() == 0 { - throw_type_error( - scope, - "Failed to execute 'createImageBitmap' on 'Window': 1 argument required, but only 0 present.", - ); - return; - } - let Ok(source) = v8::Local::::try_from(args.get(0)) else { - throw_type_error( - scope, - "Failed to execute 'createImageBitmap' on 'Window': The provided value is not a valid image source.", - ); - return; - }; - if !offscreen_canvas_receiver_branded(scope, source) { - throw_type_error( - scope, - "Failed to execute 'createImageBitmap' on 'Window': The provided value is not a valid image source.", - ); - return; - } - let Some(resolver) = v8::PromiseResolver::new(scope) else { rv.set_undefined(); return; }; let promise = resolver.get_promise(scope); - if !canvas_like_has_context(scope, source) { - let error = new_dom_exception_value( - scope, - "Failed to execute 'createImageBitmap' on 'Window': The ImageBitmap could not be allocated.", - "InvalidStateError", - ); + rv.set(promise.into()); + + // Promise-returning Web IDL operations turn conversion failures (including + // arbitrary getter exceptions) into rejections of the returned Promise. + let parsed = { + let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); + let mut conversion_scope = try_catch.init(); + match parse_bitmap_arguments(&mut conversion_scope, &args) { + Ok(parsed) => Ok(parsed), + Err(error) => { + webidl::throw_error(&mut conversion_scope, &error); + let exception = conversion_scope + .exception() + .unwrap_or_else(|| v8::undefined(&conversion_scope).into()); + conversion_scope.reset(); + Err(exception) + } + } + }; + let (source, parameters) = match parsed { + Ok(parsed) => parsed, + Err(exception) => { + let _ = resolver.reject(scope, exception); + return; + } + }; + if parameters + .crop + .is_some_and(|[_, _, width, height]| width == 0 || height == 0) + { + let message = crate::util::v8str(scope, "The crop rectangle has a zero dimension."); + let error = v8::Exception::range_error(scope, message); let _ = resolver.reject(scope, error); - rv.set(promise.into()); return; } + if parameters.options.resize_width == Some(0) || parameters.options.resize_height == Some(0) { + reject_bitmap(scope, resolver); + return; + } + // Snapshot only after all observable conversions, which may mutate or + // detach the source. No V8 handles or live DOM state cross the task boundary. + let input = match source.snapshot(scope) { + Some(input) => input, + None => { + reject_bitmap(scope, resolver); + return; + } + }; + let producer = context_host_ptr_from_global_bridge(scope).and_then(|host_ptr| { + // SAFETY: the bridge points at this callback's live Window host. + unsafe { &mut *host_ptr }.register_pending_bitmap_task(scope, resolver) + }); + let Some(producer) = producer else { + reject_bitmap(scope, resolver); + return; + }; + match input { + BitmapInput::Pixels(image) => { + let _ = producer.send(parameters.apply(image)); + } + BitmapInput::Blob(bytes) => { + let decode = move || { + let result = moli_image::decode_raster_image(&bytes) + .map_err(|_| BitmapRejection::InvalidState) + .and_then(|decoded| parameters.apply(decoded.image)); + let _ = producer.send(result); + }; + if let Ok(handle) = tokio::runtime::Handle::try_current() { + handle.spawn_blocking(decode); + } else { + std::thread::spawn(decode); + } + } + } +} - let Some((_pixels, width, height)) = canvas_like_pixels_copy(scope, source) else { - let error = new_dom_exception_value( - scope, - "Failed to execute 'createImageBitmap' on 'Window': The ImageBitmap could not be allocated.", - "InvalidStateError", - ); - let _ = resolver.reject(scope, error); - rv.set(promise.into()); - return; +#[derive(Clone, Copy)] +enum BitmapSourceKind { + Blob, + Canvas, + ImageData, + ImageBitmap, +} + +struct BitmapSource<'s> { + object: v8::Local<'s, v8::Object>, + kind: BitmapSourceKind, +} + +impl<'s> webidl::WebIdlConverter<'s> for BitmapSource<'s> { + type Options = (); + + fn convert( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + context: webidl::Context, + _options: &(), + ) -> Result { + let object = webidl::convert::>(scope, value, context)?; + let kind = if crate::blob::blob_id_from_object(scope, object).is_some() { + BitmapSourceKind::Blob + } else if offscreen_canvas_receiver_branded(scope, object) || is_html_canvas(scope, object) + { + BitmapSourceKind::Canvas + } else if crate::context_bootstrap::image_data::is_image_data_object(scope, object) { + BitmapSourceKind::ImageData + } else if image_bitmap_receiver_branded(scope, object) { + BitmapSourceKind::ImageBitmap + } else { + return Err(webidl::WebIdlError::custom_message( + "The value is not a supported ImageBitmapSource.", + )); + }; + Ok(Self { object, kind }) + } +} + +fn is_html_canvas<'s>(scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>) -> bool { + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, object) + .ok() + .is_some_and(|(host, handle)| { + unsafe { &*host } + .dom_host() + .is_html_element_named(handle, "canvas") + }) +} + +enum BitmapInput { + Blob(Vec), + Pixels(moli_image::RgbaImage), +} + +impl<'s> BitmapSource<'s> { + fn snapshot(self, scope: &mut v8::PinScope<'s, '_>) -> Option { + let (pixels, width, height) = match self.kind { + BitmapSourceKind::Blob => { + return crate::blob::blob_bytes_from_object(scope, self.object) + .map(BitmapInput::Blob); + } + BitmapSourceKind::Canvas => canvas_like_pixels_copy(scope, self.object)?, + BitmapSourceKind::ImageData => { + let data = + crate::context_bootstrap::image_data::image_data_clone_payload_from_object( + scope, + self.object, + )?; + (data.bytes, data.width, data.height) + } + BitmapSourceKind::ImageBitmap => image_bitmap_pixels_copy(scope, self.object)?, + }; + if width == 0 || height == 0 { + return None; + } + moli_image::RgbaImage::try_new(width, height, pixels) + .ok() + .map(BitmapInput::Pixels) + } +} + +fn parse_bitmap_arguments<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, +) -> Result<(BitmapSource<'s>, BitmapParameters), webidl::WebIdlError> { + let context = |index| webidl::Context::argument("createImageBitmap", index); + if args.length() == 0 { + return Err(webidl::WebIdlError::missing_required(context(1))); + } + if matches!(args.length(), 3 | 4) { + return Err(webidl::WebIdlError::custom_message( + "The crop overload requires five arguments.", + )); + } + let source = webidl::argument::(scope, args, 0, context(1))?; + let crop = if args.length() >= 5 { + let mut crop = [0; 4]; + for (index, coordinate) in crop.iter_mut().enumerate() { + *coordinate = webidl::argument::( + scope, + args, + index as i32 + 1, + context(index + 2), + )? + .0; + } + Some(crop) + } else { + None }; - let Some(bitmap) = build_image_bitmap_object(scope, width, height) else { - let error = new_dom_exception_value( + let options_index = if crop.is_some() { 5 } else { 1 }; + let options = webidl::parse_dictionary::( + scope, + args.get(options_index), + context(options_index as usize + 1), + )? + .unwrap_or_default(); + Ok((source, BitmapParameters { crop, options })) +} + +pub(crate) fn settle_bitmap_task_result<'s>( + scope: &mut v8::PinScope<'s, '_>, + resolver: v8::Local<'s, v8::PromiseResolver>, + result: Result, +) { + let bitmap = result.ok().and_then(|result| { + let bitmap = build_image_bitmap_object(scope, result.width, result.height)?; + let pixels = + super::super::image_data::new_uint8_clamped_array_from_bytes(scope, result.pixels)?; + set_private_value(scope, bitmap, IMAGE_BITMAP_PIXELS_SLOT, pixels.into()); + set_private_value( scope, - "Failed to execute 'createImageBitmap' on 'Window': The ImageBitmap could not be allocated.", - "InvalidStateError", + bitmap, + IMAGE_BITMAP_PREMULTIPLIED_SLOT, + v8::Boolean::new(scope, result.premultiplied).into(), ); - let _ = resolver.reject(scope, error); - rv.set(promise.into()); - return; - }; - let _ = resolver.resolve(scope, bitmap.into()); - rv.set(promise.into()); + Some(bitmap) + }); + match bitmap { + Some(bitmap) => { + let _ = resolver.resolve(scope, bitmap.into()); + } + None => reject_bitmap(scope, resolver), + } +} + +fn reject_bitmap<'s>( + scope: &mut v8::PinScope<'s, '_>, + resolver: v8::Local<'s, v8::PromiseResolver>, +) { + let error = new_dom_exception_value( + scope, + "The image source could not be decoded or allocated.", + "InvalidStateError", + ); + let _ = resolver.reject(scope, error); +} + +pub(super) fn image_bitmap_pixels_copy<'s>( + scope: &mut v8::PinScope<'s, '_>, + bitmap: v8::Local<'s, v8::Object>, +) -> Option<(Vec, u32, u32)> { + if !image_bitmap_receiver_branded(scope, bitmap) { + return None; + } + let width = get_private_value(scope, bitmap, IMAGE_BITMAP_WIDTH_SLOT)?.uint32_value(scope)?; + let height = get_private_value(scope, bitmap, IMAGE_BITMAP_HEIGHT_SLOT)?.uint32_value(scope)?; + let view = v8::Local::::try_from(get_private_value( + scope, + bitmap, + IMAGE_BITMAP_PIXELS_SLOT, + )?) + .ok()?; + let mut pixels = vec![0; view.byte_length()]; + view.copy_contents(&mut pixels); + if get_private_value(scope, bitmap, IMAGE_BITMAP_PREMULTIPLIED_SLOT) + .is_some_and(|value| value.boolean_value(scope)) + { + for pixel in pixels.chunks_exact_mut(4) { + let alpha = u32::from(pixel[3]); + for channel in &mut pixel[..3] { + *channel = (u32::from(*channel) * 255 + alpha / 2) + .checked_div(alpha) + .unwrap_or(0) + .min(255) as u8; + } + } + } + Some((pixels, width, height)) } fn build_image_bitmap_object<'s>( @@ -174,10 +405,16 @@ fn image_bitmap_close_callback<'s>( for slot in IMAGE_BITMAP_DIMENSION_SLOTS { set_private_value(scope, args.this(), slot, v8::Number::new(scope, 0.0).into()); } + set_private_value( + scope, + args.this(), + IMAGE_BITMAP_PIXELS_SLOT, + v8::undefined(scope).into(), + ); rv.set_undefined(); } -fn image_bitmap_receiver_branded<'s>( +pub(super) fn image_bitmap_receiver_branded<'s>( scope: &mut v8::PinScope<'s, '_>, receiver: v8::Local<'s, v8::Object>, ) -> bool { diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap/options.rs b/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap/options.rs new file mode 100644 index 0000000000..e6bd222f9f --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/canvas/image_bitmap/options.rs @@ -0,0 +1,191 @@ +use super::{BitmapRejection, BitmapTaskResult}; +use crate::webidl; +use moli_canvas::{DrawImageBlit, ScaleFilter, blit_draw_image_filtered, byte_len}; +use moli_image::RgbaImage; +use std::str::FromStr; + +#[derive(Clone, Copy, Default, strum::EnumString, webidl::WebIdlEnum)] +#[webidl(name = "ImageOrientation", parse_with = Self::parse)] +enum ImageOrientation { + #[default] + #[strum(serialize = "from-image")] + FromImage, + #[strum(serialize = "flipY")] + FlipY, +} + +impl ImageOrientation { + fn parse(value: &str) -> Option { + Self::from_str(value).ok() + } +} + +#[derive(Clone, Copy, Default, strum::EnumString, webidl::WebIdlEnum)] +#[webidl(name = "PremultiplyAlpha", parse_with = Self::parse)] +#[strum(serialize_all = "lowercase")] +enum PremultiplyAlpha { + None, + Premultiply, + #[default] + Default, +} + +impl PremultiplyAlpha { + fn parse(value: &str) -> Option { + Self::from_str(value).ok() + } +} + +#[derive(Clone, Copy, Default, strum::EnumString, webidl::WebIdlEnum)] +#[webidl(name = "ColorSpaceConversion", parse_with = Self::parse)] +#[strum(serialize_all = "lowercase")] +enum ColorSpaceConversion { + None, + #[default] + Default, +} + +impl ColorSpaceConversion { + fn parse(value: &str) -> Option { + Self::from_str(value).ok() + } +} + +#[derive(Clone, Copy, Default, strum::EnumString, webidl::WebIdlEnum)] +#[webidl(name = "ResizeQuality", parse_with = Self::parse)] +#[strum(serialize_all = "lowercase")] +enum ResizeQuality { + Pixelated, + #[default] + Low, + Medium, + High, +} + +impl ResizeQuality { + fn parse(value: &str) -> Option { + Self::from_str(value).ok() + } +} + +#[derive(Default, webidl::WebIdlDictionary)] +#[webidl(prefix = "ImageBitmapOptions")] +pub(super) struct BitmapOptions { + // The raster backend leaves color profiles unconverted in both modes. + // Members are declared in Web IDL's observable lexicographic read order. + #[webidl(name = "colorSpaceConversion", converter = "enum", default = ColorSpaceConversion::Default)] + _color_space_conversion: ColorSpaceConversion, + #[webidl(converter = "enum", default = ImageOrientation::FromImage)] + image_orientation: ImageOrientation, + #[webidl(converter = "enum", default = PremultiplyAlpha::Default)] + premultiply_alpha: PremultiplyAlpha, + #[webidl(converter = "enforce_range_unsigned_long")] + pub(super) resize_height: Option, + #[webidl(converter = "enum", default = ResizeQuality::Low)] + resize_quality: ResizeQuality, + #[webidl(converter = "enforce_range_unsigned_long")] + pub(super) resize_width: Option, +} + +pub(super) struct BitmapParameters { + pub(super) crop: Option<[i32; 4]>, + pub(super) options: BitmapOptions, +} + +impl BitmapParameters { + pub(super) fn apply(self, image: RgbaImage) -> Result { + if image.width == 0 || image.height == 0 { + return Err(BitmapRejection::InvalidState); + } + let [mut x, mut y, mut width, mut height] = self + .crop + .map(|crop| crop.map(f64::from)) + .unwrap_or([0.0, 0.0, f64::from(image.width), f64::from(image.height)]); + // Negative extents select the rectangle on the other side of the + // origin; they do not mirror its pixels. + if width < 0.0 { + x += width; + width = -width; + } + if height < 0.0 { + y += height; + height = -height; + } + let (output_width, output_height) = + match (self.options.resize_width, self.options.resize_height) { + (Some(w), Some(h)) => (w, h), + (Some(w), None) => (w, (height * f64::from(w) / width).ceil() as u32), + (None, Some(h)) => ((width * f64::from(h) / height).ceil() as u32, h), + (None, None) => (width as u32, height as u32), + }; + if output_width == 0 || output_height == 0 { + return Err(BitmapRejection::InvalidState); + } + let len = byte_len(output_width, output_height).ok_or(BitmapRejection::InvalidState)?; + let mut pixels = if x == 0.0 + && y == 0.0 + && width == f64::from(image.width) + && height == f64::from(image.height) + && output_width == image.width + && output_height == image.height + { + image.rgba + } else { + let mut pixels = vec![0; len]; + let blit = DrawImageBlit::new( + x, + y, + width, + height, + 0.0, + 0.0, + f64::from(output_width), + f64::from(output_height), + ) + .ok_or(BitmapRejection::InvalidState)?; + let filter = if matches!(self.options.resize_quality, ResizeQuality::Pixelated) { + ScaleFilter::Nearest + } else { + ScaleFilter::Bilinear + }; + blit_draw_image_filtered( + &mut pixels, + output_width, + output_height, + &image.rgba, + image.width, + image.height, + blit, + filter, + ); + pixels + }; + if matches!(self.options.image_orientation, ImageOrientation::FlipY) { + let row_len = output_width as usize * 4; + for y in 0..output_height as usize / 2 { + let bottom = (output_height as usize - y - 1) * row_len; + let (top_rows, bottom_rows) = pixels.split_at_mut(bottom); + top_rows[y * row_len..(y + 1) * row_len] + .swap_with_slice(&mut bottom_rows[..row_len]); + } + } + let premultiplied = matches!( + self.options.premultiply_alpha, + PremultiplyAlpha::Premultiply + ); + if premultiplied { + for pixel in pixels.chunks_exact_mut(4) { + let alpha = u32::from(pixel[3]); + for channel in &mut pixel[..3] { + *channel = ((u32::from(*channel) * alpha + 127) / 255) as u8; + } + } + } + Ok(BitmapTaskResult { + width: output_width, + height: output_height, + pixels, + premultiplied, + }) + } +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/bitmap_tasks.rs b/moli-renderer-v8/src/native_bridge/context_host/bitmap_tasks.rs new file mode 100644 index 0000000000..02d8a32a6b --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/bitmap_tasks.rs @@ -0,0 +1,110 @@ +use super::*; + +pub(crate) struct PendingBitmapTask { + pub(crate) execution_context: super::WindowExecutionContextIdentity, + pub(crate) relevant_context: super::WindowExecutionContextBinding, + pub(crate) resolver: v8::Global, +} + +impl JsContextHost { + pub(crate) fn register_pending_bitmap_task( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + resolver: v8::Local<'_, v8::PromiseResolver>, + ) -> Option { + let task_id = self.next_bitmap_task_id; + self.next_bitmap_task_id = self + .next_bitmap_task_id + .checked_next() + .expect("Page Bitmap task id overflow"); + let execution_context = self.current_runtime_window_execution_context_identity(scope)?; + let relevant_context = super::WindowExecutionContextBinding::new( + execution_context.owner(), + execution_context.dispatch_scope(), + execution_context.realm_token(), + v8::Global::new(scope, scope.get_current_context()), + ); + let producer = self + .page_bitmap_task_sender() + .bind_task(execution_context, task_id); + let replaced = self.pending_bitmap_tasks.insert( + task_id, + PendingBitmapTask { + execution_context, + relevant_context, + resolver: v8::Global::new(scope, resolver), + }, + ); + assert!( + replaced.is_none(), + "Page Bitmap task ids must never be reused" + ); + tracing::debug!( + task_id = task_id.task_id(), + ?execution_context, + "registered Bitmap task with Window execution context" + ); + Some(producer) + } + + pub(crate) fn current_pending_bitmap_task_execution_context( + &self, + task: crate::page_task_queue::RendererPageBitmapTaskId, + ) -> Option { + let pending = self.pending_bitmap_tasks.get(&task)?; + if !self.window_execution_context_identity_is_current(pending.execution_context) { + return None; + } + Some(pending.execution_context) + } + + pub(crate) fn take_pending_bitmap_task_for_exact_owner( + &mut self, + execution_context: super::WindowExecutionContextIdentity, + task: crate::page_task_queue::RendererPageBitmapTaskId, + ) -> Option { + let pending = self.pending_bitmap_tasks.get(&task)?; + if pending.execution_context != execution_context { + return None; + } + self.pending_bitmap_tasks.remove(&task) + } + + pub(crate) fn retire_bitmap_execution_context_owner( + &mut self, + retired_owner: super::WindowExecutionContextOwner, + ) -> usize { + let count_before = self.pending_bitmap_tasks.len(); + self.pending_bitmap_tasks + .retain(|_, pending| pending.relevant_context.owner() != retired_owner); + let retired_count = count_before - self.pending_bitmap_tasks.len(); + tracing::debug!( + ?retired_owner, + retired_count, + "retired Bitmap tasks with Window execution context" + ); + retired_count + } + + pub(crate) fn retire_bitmap_context_token( + &mut self, + context_token: super::RuntimeObservableContextToken, + ) -> usize { + let count_before = self.pending_bitmap_tasks.len(); + self.pending_bitmap_tasks + .retain(|_, pending| pending.relevant_context.realm_token() != context_token); + let retired_count = count_before - self.pending_bitmap_tasks.len(); + if retired_count > 0 { + tracing::debug!( + ?context_token, + retired_count, + "retired Bitmap tasks with destroyed V8 context" + ); + } + retired_count + } + + pub(crate) fn has_pending_bitmap_tasks(&self) -> bool { + !self.pending_bitmap_tasks.is_empty() + } +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index b59cf5535b..382519c948 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -330,7 +330,9 @@ impl JsContextHost { next_image_decode_id: 1, pending_image_decode_requests: HashMap::new(), resource_timing_buffers: SharedResourceTimingBufferRegistry::new(), + next_bitmap_task_id: crate::page_task_queue::RendererPageBitmapTaskId::first(), next_webcrypto_task_id: crate::page_task_queue::RendererPageWebCryptoTaskId::first(), + pending_bitmap_tasks: HashMap::new(), pending_webcrypto_tasks: HashMap::new(), opfs_owner_state: None, history_queue: HistoryQueueState::default(), @@ -966,6 +968,16 @@ impl JsContextHost { .worker_host_bridge() } + pub(crate) fn page_bitmap_task_sender( + &self, + ) -> &crate::page_task_queue::RendererPageBitmapTaskSender { + self.page_task_capabilities + .get() + .expect( + "a live Page Window must install its complete Page task capabilities before Bitmap registration", + ) + .bitmap_task() + } pub(crate) fn page_webcrypto_task_sender( &self, ) -> &crate::page_task_queue::RendererPageWebCryptoTaskSender { diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 042123c7a3..66af7f3bf8 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -148,6 +148,7 @@ mod security_policy; mod selection_records; mod service_workers; pub(crate) use service_workers::ServiceWorkerWindowOwner; +mod bitmap_tasks; mod shared_workers; mod signal_bridge; mod storage_events; @@ -945,7 +946,10 @@ pub(crate) struct JsContextHost { next_image_decode_id: u64, pending_image_decode_requests: HashMap, resource_timing_buffers: SharedResourceTimingBufferRegistry, + next_bitmap_task_id: crate::page_task_queue::RendererPageBitmapTaskId, next_webcrypto_task_id: crate::page_task_queue::RendererPageWebCryptoTaskId, + pending_bitmap_tasks: + HashMap, pending_webcrypto_tasks: HashMap< crate::page_task_queue::RendererPageWebCryptoTaskId, webcrypto_tasks::PendingWebCryptoTask, diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index cafd44d51c..3d75ba9d2c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -2075,6 +2075,8 @@ impl JsContextHost { .retire_owner(execution_context_owner); let retired_timer_count = unsafe { &mut *self.runtime } .cancel_window_execution_context_timers(execution_context_owner); + let retired_bitmap_count = + self.retire_bitmap_execution_context_owner(execution_context_owner); let retired_webcrypto_count = self.retire_webcrypto_execution_context_owner(execution_context_owner); self.retire_opfs_execution_context_owner(execution_context_owner); @@ -2102,6 +2104,7 @@ impl JsContextHost { tracing::debug!( ?execution_context_owner, retired_timer_count, + retired_bitmap_count, retired_webcrypto_count, retired_worker_count, retired_shared_worker_count, diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 0e34328c7a..7c071ceb39 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -161,7 +161,7 @@ pub(super) use attributes::{ node_toggle_attribute_callback, }; pub(crate) use canvas::{ - canvas_get_context_callback, canvas_to_data_url_callback, + canvas_dimension_value, canvas_get_context_callback, canvas_to_data_url_callback, canvas_transfer_control_to_offscreen_callback, }; pub(crate) use canvas::{ diff --git a/moli-renderer-v8/src/native_bridge/element/canvas.rs b/moli-renderer-v8/src/native_bridge/element/canvas.rs index 447b7d3127..0071fbf9ee 100644 --- a/moli-renderer-v8/src/native_bridge/element/canvas.rs +++ b/moli-renderer-v8/src/native_bridge/element/canvas.rs @@ -125,7 +125,7 @@ fn set_canvas_dimension_attribute<'s>( true } -fn canvas_dimension_value<'s>( +pub(crate) fn canvas_dimension_value<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, name: &str, diff --git a/moli-renderer-v8/src/page_task_queue.rs b/moli-renderer-v8/src/page_task_queue.rs index 9f7bf500bc..0721dc2676 100644 --- a/moli-renderer-v8/src/page_task_queue.rs +++ b/moli-renderer-v8/src/page_task_queue.rs @@ -1,3 +1,4 @@ +mod bitmap_task; mod broadcast_channel_delivery; mod child_frame_task; mod child_module_dependency_fetch_start; @@ -155,6 +156,11 @@ pub(crate) struct PageWindowDocumentTaskTurnAction { impl PageWindowDocumentTaskTurnAction {} +pub(crate) use self::bitmap_task::{ + PageBitmapTaskTargetEffect, PageBitmapTaskTurnAction, PageBitmapTaskTurnOutcome, + RendererPageBitmapTask, RendererPageBitmapTaskId, RendererPageBitmapTaskOwner, + RendererPageBitmapTaskProducer, RendererPageBitmapTaskSender, +}; pub(crate) use self::broadcast_channel_delivery::{ PageBroadcastChannelDeliveryDocumentEffect, PageBroadcastChannelDeliveryTurnAction, PageBroadcastChannelDeliveryTurnOutcome, RendererPageBroadcastChannelDeliveryOwner, diff --git a/moli-renderer-v8/src/page_task_queue/bitmap_task.rs b/moli-renderer-v8/src/page_task_queue/bitmap_task.rs new file mode 100644 index 0000000000..901374829c --- /dev/null +++ b/moli-renderer-v8/src/page_task_queue/bitmap_task.rs @@ -0,0 +1,257 @@ +use moli_owner_queue::{OwnerReadyTaskRoute, OwnerReadyTaskSource}; + +use crate::{ + context_bootstrap::{BitmapRejection, BitmapTaskResult}, + native_bridge::WindowExecutionContextIdentity, + resource_ready::{ReadyPageTask, RendererPageTaskReadyMetadata}, + runtime::{PageOwnerTurnOutcome, RendererDocumentToken}, +}; + +use super::{RendererOwnerWakeSender, RendererOwnerWakeSource, RendererPageTaskReadySignal}; + +/// PageVm-local identity of one pending Bitmap Promise. +/// +/// The id is never reused within a PageVm. The enclosing task owner carries +/// the root Page and Window-realm identities, so `document.open()` can preserve +/// Window-owned Bitmap work without projecting a Document identity into +/// the task identity. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) struct RendererPageBitmapTaskId(u64); + +impl RendererPageBitmapTaskId { + pub(crate) const fn first() -> Self { + Self(1) + } + + #[cfg(test)] + pub(crate) const fn new(task_id: u64) -> Self { + assert!(task_id != 0, "Bitmap task id must be non-zero"); + Self(task_id) + } + + pub(crate) const fn task_id(self) -> u64 { + self.0 + } + + pub(crate) const fn checked_next(self) -> Option { + match self.0.checked_add(1) { + Some(task_id) => Some(Self(task_id)), + None => None, + } + } +} + +/// Exact owner of one page-side Bitmap completion. +/// +/// The root token prevents PageVm-local ids from colliding across navigation. +/// The Window identity binds the Promise relevant realm. The task id then +/// identifies the pending resolver within that PageVm/realm. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct RendererPageBitmapTaskOwner { + root_document: RendererDocumentToken, + execution_context: WindowExecutionContextIdentity, + task: RendererPageBitmapTaskId, +} + +impl RendererPageBitmapTaskOwner { + pub(crate) const fn new( + root_document: RendererDocumentToken, + execution_context: WindowExecutionContextIdentity, + task: RendererPageBitmapTaskId, + ) -> Self { + Self { + root_document, + execution_context, + task, + } + } + + pub(crate) const fn root_document(self) -> RendererDocumentToken { + self.root_document + } + + pub(crate) const fn execution_context(self) -> WindowExecutionContextIdentity { + self.execution_context + } + + pub(crate) const fn task(self) -> RendererPageBitmapTaskId { + self.task + } +} + +#[derive(Debug)] +pub(crate) struct RendererPageBitmapTask { + owner: RendererPageBitmapTaskOwner, + result: Result, +} + +impl RendererPageBitmapTask { + fn new( + owner: RendererPageBitmapTaskOwner, + result: Result, + ) -> Self { + Self { owner, result } + } + + pub(crate) const fn owner(&self) -> RendererPageBitmapTaskOwner { + self.owner + } + + pub(crate) fn into_result(self) -> Result { + self.result + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct RendererPageBitmapTaskRouteClosed; + +#[derive(Clone, Debug)] +pub(crate) struct RendererPageBitmapTaskRoute { + task_route: + OwnerReadyTaskRoute, RendererPageTaskReadySignal>, +} + +impl RendererPageBitmapTaskRoute { + pub(crate) fn sender( + &self, + root_document: RendererDocumentToken, + ) -> RendererPageBitmapTaskSender { + RendererPageBitmapTaskSender { + task_route: self.task_route.clone(), + root_document, + } + } + + fn same_route_as(&self, source: &RendererPageBitmapTaskSource) -> bool { + self.task_route.same_source_as(&source.source) + } +} + +/// PageVm-stamped route used only while a Bitmap Promise is registered. +#[derive(Clone, Debug)] +pub(crate) struct RendererPageBitmapTaskSender { + task_route: + OwnerReadyTaskRoute, RendererPageTaskReadySignal>, + root_document: RendererDocumentToken, +} + +impl RendererPageBitmapTaskSender { + pub(crate) fn bind_task( + &self, + execution_context: WindowExecutionContextIdentity, + task: RendererPageBitmapTaskId, + ) -> RendererPageBitmapTaskProducer { + RendererPageBitmapTaskProducer { + task_route: self.task_route.clone(), + owner: RendererPageBitmapTaskOwner::new(self.root_document, execution_context, task), + } + } +} + +/// Single-use completion capability retained by one blocking bitmap job. +/// +/// Consuming `self` makes duplicate delivery impossible without cloning and +/// rebuilding the exact task at registration time. +#[derive(Debug)] +pub(crate) struct RendererPageBitmapTaskProducer { + task_route: + OwnerReadyTaskRoute, RendererPageTaskReadySignal>, + owner: RendererPageBitmapTaskOwner, +} + +impl RendererPageBitmapTaskProducer { + #[cfg(test)] + pub(crate) const fn owner(&self) -> RendererPageBitmapTaskOwner { + self.owner + } + + pub(crate) fn send( + self, + result: Result, + ) -> Result<(), RendererPageBitmapTaskRouteClosed> { + self.task_route + .send_and_signal_if_newly_ready(ReadyPageTask::new(RendererPageBitmapTask::new( + self.owner, result, + ))) + .map_err(|_| RendererPageBitmapTaskRouteClosed) + } +} + +/// Unique Page-lifetime consumer for completed Bitmap operations. +#[derive(Debug)] +pub(crate) struct RendererPageBitmapTaskSource { + source: + OwnerReadyTaskSource, RendererPageTaskReadySignal>, +} + +impl RendererPageBitmapTaskSource { + pub(crate) fn new(owner_wake: RendererOwnerWakeSender) -> Self { + Self { + source: OwnerReadyTaskSource::new(RendererPageTaskReadySignal::new( + owner_wake, + RendererOwnerWakeSource::BitmapTask, + )), + } + } + + pub(crate) fn route(&self) -> RendererPageBitmapTaskRoute { + RendererPageBitmapTaskRoute { + task_route: self.source.route(), + } + } + + pub(crate) fn next_ready_metadata(&mut self) -> Option { + self.source.front().map(ReadyPageTask::metadata) + } + + pub(crate) fn next_ready_owner(&mut self) -> Option { + self.source.front().map(|ready| ready.value().owner()) + } + + pub(crate) fn pop_front( + &mut self, + ) -> Option<(RendererPageTaskReadyMetadata, RendererPageBitmapTask)> { + self.source.pop_front().map(ReadyPageTask::into_parts) + } + + pub(crate) fn has_ready_task(&mut self) -> bool { + !self.source.is_empty() + } + + pub(crate) fn clear(&mut self) { + self.source.clear_local(); + } + + pub(crate) fn route_matches(&self, route: &RendererPageBitmapTaskRoute) -> bool { + route.same_route_as(self) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum PageBitmapTaskTargetEffect { + SettledCurrentOwner, + IgnoredStaleOwner { + current_owner: Option, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct PageBitmapTaskTurnAction { + pub(crate) owner: RendererPageBitmapTaskOwner, + pub(crate) target_effect: PageBitmapTaskTargetEffect, +} + +impl PageBitmapTaskTurnAction { + /// Whether the exact pending Promise was settled in its relevant realm. + /// + /// This reports the domain effect only. The selected-task dispatcher + /// decides which task-end checkpoint that effect requires. + pub(crate) const fn settled_current_owner(self) -> bool { + matches!( + self.target_effect, + PageBitmapTaskTargetEffect::SettledCurrentOwner + ) + } +} + +pub(crate) type PageBitmapTaskTurnOutcome = PageOwnerTurnOutcome; diff --git a/moli-renderer-v8/src/page_task_queue/owner_sources.rs b/moli-renderer-v8/src/page_task_queue/owner_sources.rs index 3eed42940b..2b79f032c5 100644 --- a/moli-renderer-v8/src/page_task_queue/owner_sources.rs +++ b/moli-renderer-v8/src/page_task_queue/owner_sources.rs @@ -17,6 +17,10 @@ use std::{cell::RefCell, rc::Rc}; use super::{ PageRuntimeWakeSignal, RendererOwnerWakeSender, RendererOwnerWakeSource, + bitmap_task::{ + RendererPageBitmapTask, RendererPageBitmapTaskOwner, RendererPageBitmapTaskRoute, + RendererPageBitmapTaskSender, RendererPageBitmapTaskSource, + }, child_frame_task::{ RendererPageChildFrameTask, RendererPageChildFrameTaskOwner, RendererPageChildFrameTaskRoute, RendererPageChildFrameTaskSender, @@ -177,6 +181,7 @@ pub(crate) struct RendererPageOwnedTaskSources { shared_worker_client_event: RendererPageSharedWorkerClientEventSource, service_worker_internal: RendererPageServiceWorkerInternalSource, service_worker_client_message: RendererPageServiceWorkerClientMessageSource, + bitmap_task: RendererPageBitmapTaskSource, webcrypto_task: RendererPageWebCryptoTaskSource, indexed_db_task: RendererPageIndexedDbTaskSource, opfs_task: RendererPageOpfsTaskSource, @@ -213,6 +218,7 @@ pub(crate) struct RendererPageTaskProducerRoutes { shared_worker_client_event: RendererPageSharedWorkerClientEventRoute, service_worker_internal: RendererPageServiceWorkerInternalRoute, service_worker_client_message: RendererPageServiceWorkerClientMessageRoute, + bitmap_task: RendererPageBitmapTaskRoute, webcrypto_task: RendererPageWebCryptoTaskRoute, indexed_db_task: RendererPageIndexedDbTaskRoute, opfs_task: RendererPageOpfsTaskRoute, @@ -293,6 +299,10 @@ pub(crate) enum RendererPageReadyDescriptor { ready: RendererPageTaskReadyMetadata, owner: RendererPageServiceWorkerClientMessageOwner, }, + BitmapTask { + ready: RendererPageTaskReadyMetadata, + owner: RendererPageBitmapTaskOwner, + }, WebCryptoTask { ready: RendererPageTaskReadyMetadata, owner: RendererPageWebCryptoTaskOwner, @@ -382,6 +392,7 @@ impl RendererPageReadyDescriptor { | Self::SharedWorkerClientEvent { ready, .. } | Self::ServiceWorkerInternal { ready, .. } | Self::ServiceWorkerClientMessage { ready, .. } + | Self::BitmapTask { ready, .. } | Self::WebCryptoTask { ready, .. } | Self::IndexedDbTask { ready, .. } | Self::OpfsTask { ready, .. } @@ -424,6 +435,7 @@ impl RendererPageReadyDescriptor { Self::ServiceWorkerClientMessage { .. } => { RendererPageTaskSourceKind::ServiceWorkerClientMessage } + Self::BitmapTask { .. } => RendererPageTaskSourceKind::BitmapTask, Self::WebCryptoTask { .. } => RendererPageTaskSourceKind::WebCryptoTask, Self::IndexedDbTask { .. } => RendererPageTaskSourceKind::IndexedDbTask, Self::OpfsTask { .. } => RendererPageTaskSourceKind::OpfsTask, @@ -468,6 +480,7 @@ impl RendererPageReadyDescriptor { | Self::SharedWorkerClientEvent { ready, .. } | Self::ServiceWorkerInternal { ready, .. } | Self::ServiceWorkerClientMessage { ready, .. } + | Self::BitmapTask { ready, .. } | Self::WebCryptoTask { ready, .. } | Self::IndexedDbTask { ready, .. } | Self::OpfsTask { ready, .. } @@ -512,6 +525,7 @@ pub(crate) enum RendererPageTaskSourceKind { SharedWorkerClientEvent, ServiceWorkerInternal, ServiceWorkerClientMessage, + BitmapTask, WebCryptoTask, IndexedDbTask, OpfsTask, @@ -531,7 +545,7 @@ pub(crate) enum RendererPageTaskSourceKind { } impl RendererPageTaskSourceKind { - pub(crate) const ALL: [Self; 29] = [ + pub(crate) const ALL: [Self; 30] = [ Self::ActionWindow, Self::Timer, Self::DomManipulation, @@ -545,6 +559,7 @@ impl RendererPageTaskSourceKind { Self::SharedWorkerClientEvent, Self::ServiceWorkerInternal, Self::ServiceWorkerClientMessage, + Self::BitmapTask, Self::WebCryptoTask, Self::IndexedDbTask, Self::OpfsTask, @@ -584,6 +599,7 @@ pub(crate) enum RendererPageSchedulerTask { SharedWorkerClientEvent(RendererPageSharedWorkerClientEventTask), ServiceWorkerInternal(RendererPageServiceWorkerInternalTask), ServiceWorkerClientMessage(RendererPageServiceWorkerClientMessageTask), + BitmapTask(RendererPageBitmapTask), WebCryptoTask(RendererPageWebCryptoTask), IndexedDbTask(RendererPageIndexedDbTask), OpfsTask(RendererPageOpfsTask), @@ -646,6 +662,7 @@ impl RendererPageOwnedTaskSources { RendererPageServiceWorkerInternalSource::new(owner_wake.clone()); let service_worker_client_message = RendererPageServiceWorkerClientMessageSource::new(owner_wake.clone()); + let bitmap_task = RendererPageBitmapTaskSource::new(owner_wake.clone()); let webcrypto_task = RendererPageWebCryptoTaskSource::new(owner_wake.clone()); let indexed_db_task = RendererPageIndexedDbTaskSource::new(owner_wake.clone()); let opfs_task = RendererPageOpfsTaskSource::new(owner_wake.clone()); @@ -680,6 +697,7 @@ impl RendererPageOwnedTaskSources { shared_worker_client_event: shared_worker_client_event.route(), service_worker_internal: service_worker_internal.route(), service_worker_client_message: service_worker_client_message.route(), + bitmap_task: bitmap_task.route(), webcrypto_task: webcrypto_task.route(), indexed_db_task: indexed_db_task.route(), opfs_task: opfs_task.route(), @@ -711,6 +729,7 @@ impl RendererPageOwnedTaskSources { shared_worker_client_event, service_worker_internal, service_worker_client_message, + bitmap_task, webcrypto_task, indexed_db_task, opfs_task, @@ -764,6 +783,7 @@ impl RendererPageOwnedTaskSources { && self .service_worker_client_message .route_matches(&routes.service_worker_client_message) + && self.bitmap_task.route_matches(&routes.bitmap_task) && self.webcrypto_task.route_matches(&routes.webcrypto_task) && self.indexed_db_task.route_matches(&routes.indexed_db_task) && self.opfs_task.route_matches(&routes.opfs_task) @@ -832,6 +852,7 @@ impl RendererPageOwnedTaskSources { shared_worker_client_event: self.shared_worker_client_event.route(), service_worker_internal: self.service_worker_internal.route(), service_worker_client_message: self.service_worker_client_message.route(), + bitmap_task: self.bitmap_task.route(), webcrypto_task: self.webcrypto_task.route(), indexed_db_task: self.indexed_db_task.route(), opfs_task: self.opfs_task.route(), @@ -942,6 +963,15 @@ impl RendererPageOwnedTaskSources { .expect("ready SharedWorker client event must retain its exact owner"), }, ); + let bitmap_task = self.bitmap_task.next_ready_metadata().map(|ready| { + RendererPageReadyDescriptor::BitmapTask { + ready, + owner: self + .bitmap_task + .next_ready_owner() + .expect("ready Bitmap task must retain its exact owner"), + } + }); let webcrypto_task = self.webcrypto_task.next_ready_metadata().map(|ready| { RendererPageReadyDescriptor::WebCryptoTask { ready, @@ -1145,6 +1175,7 @@ impl RendererPageOwnedTaskSources { shared_worker_client_event, service_worker_internal, service_worker_client_message, + bitmap_task, webcrypto_task, indexed_db_task, opfs_task, @@ -1297,6 +1328,14 @@ impl RendererPageOwnedTaskSources { ); RendererPageSchedulerTask::ServiceWorkerClientMessage(task) } + RendererPageReadyDescriptor::BitmapTask { ready, .. } => { + let (actual, task) = self + .bitmap_task + .pop_front() + .expect("selected Bitmap task must remain queued"); + assert_eq!(actual, ready, "selected Bitmap head changed before dequeue"); + RendererPageSchedulerTask::BitmapTask(task) + } RendererPageReadyDescriptor::WebCryptoTask { ready, .. } => { let (actual, task) = self .webcrypto_task @@ -1522,6 +1561,7 @@ impl RendererPageOwnedTaskSources { || self.shared_worker_client_event.has_ready_task() || self.service_worker_internal.has_ready_task() || self.service_worker_client_message.has_ready_task() + || self.bitmap_task.has_ready_task() || self.webcrypto_task.has_ready_task() || self.indexed_db_task.has_ready_task() || self.opfs_task.has_ready_task() @@ -1566,6 +1606,7 @@ impl RendererPageOwnedTaskSources { self.shared_worker_client_event.clear(); self.service_worker_internal.clear(); self.service_worker_client_message.clear(); + self.bitmap_task.clear(); self.webcrypto_task.clear(); self.indexed_db_task.clear(); self.opfs_task.clear(); @@ -1681,6 +1722,12 @@ impl RendererPageTaskProducerRoutes { RendererWorkerHostBridgeEventSender::new(self.networking.clone(), root_document) } + pub(crate) fn bitmap_task_sender( + &self, + root_document: RendererDocumentToken, + ) -> RendererPageBitmapTaskSender { + self.bitmap_task.sender(root_document) + } pub(crate) fn webcrypto_task_sender( &self, root_document: RendererDocumentToken, diff --git a/moli-renderer-v8/src/page_task_queue/owner_sources/conformance_tests.rs b/moli-renderer-v8/src/page_task_queue/owner_sources/conformance_tests.rs index 4ed8688280..98715cbdcb 100644 --- a/moli-renderer-v8/src/page_task_queue/owner_sources/conformance_tests.rs +++ b/moli-renderer-v8/src/page_task_queue/owner_sources/conformance_tests.rs @@ -9,7 +9,7 @@ use url::Url; use crate::{ PageId, - context_bootstrap::{IndexedDbTaskId, WebCryptoTaskResult}, + context_bootstrap::{BitmapRejection, IndexedDbTaskId, WebCryptoTaskResult}, document_runtime::DomHandle, frame_owner_model::{ ChildDocumentModuleFetchTarget, DocumentId, @@ -38,7 +38,8 @@ use crate::{ page_task_queue::{ MainDocumentMetaRefreshNavigationTask, PageOwnedInternalLoadingTask, RendererDedicatedWorkerClientEvent, RendererDedicatedWorkerMessageEvent, RendererOwnerWake, - RendererPageChildFrameTaskSender, RendererPageChildRealmMaterializationTarget, + RendererPageBitmapTaskId, RendererPageBitmapTaskSender, RendererPageChildFrameTaskSender, + RendererPageChildRealmMaterializationTarget, RendererPageDedicatedWorkerClientEventProducer, RendererPageHistoryTraversalSender, RendererPageHistoryTraversalTaskId, RendererPageHistoryTraversalTaskKind, RendererPageIndexedDbTaskKind, RendererPageIndexedDbTaskSender, @@ -65,7 +66,8 @@ use crate::{ use super::{ PageRuntimeWakeSignal, RendererOwnerWakeSender, RendererOwnerWakeSource, - RendererPageChildFrameTaskSource, RendererPageChildModuleDependencyFetchStartSender, + RendererPageBitmapTaskSource, RendererPageChildFrameTaskSource, + RendererPageChildModuleDependencyFetchStartSender, RendererPageChildModuleDependencyFetchStartSource, RendererPageChildModuleScriptTerminalSender, RendererPageChildModuleScriptTerminalSource, RendererPageChildModulepreloadEventActionSender, RendererPageChildModulepreloadEventActionSource, RendererPageDedicatedWorkerClientEventSource, @@ -261,6 +263,14 @@ struct SharedWorkerClientEventLane { wake_rx: tokio::sync::mpsc::UnboundedReceiver, } +struct BitmapTaskLane { + source: Option, + initial_sender: RendererPageBitmapTaskSender, + replacement_sender: RendererPageBitmapTaskSender, + execution_context: WindowExecutionContextIdentity, + wake_rx: tokio::sync::mpsc::UnboundedReceiver, +} + struct WebCryptoTaskLane { source: Option, initial_sender: RendererPageWebCryptoTaskSender, @@ -729,6 +739,58 @@ impl TypedPageSourceConformance for IndexedDbTaskLane { } } +impl BitmapTaskLane { + fn new() -> Self { + let (wake_tx, wake_rx) = tokio::sync::mpsc::unbounded_channel(); + let source = RendererPageBitmapTaskSource::new(RendererOwnerWakeSender::new( + wake_tx, + RendererPageToken::new_for_testing(document_token(1).page_id), + )); + let route = source.route(); + let initial_sender = route.sender(document_token(1)); + let replacement_sender = route.sender(document_token(2)); + Self { + source: Some(source), + initial_sender, + replacement_sender, + execution_context: window_execution_context(76), + wake_rx, + } + } + + fn enqueue_with( + sender: &RendererPageBitmapTaskSender, + execution_context: WindowExecutionContextIdentity, + sequence: u64, + ) -> bool { + let producer = sender.bind_task(execution_context, RendererPageBitmapTaskId::new(sequence)); + assert_eq!(producer.owner().task().task_id(), sequence); + producer.send(Err(BitmapRejection::InvalidState)).is_ok() + } +} + +impl TypedPageSourceConformance for BitmapTaskLane { + fn enqueue_initial(&mut self, sequence: u64) -> bool { + Self::enqueue_with(&self.initial_sender, self.execution_context, sequence) + } + + fn enqueue_replacement(&mut self, sequence: u64) -> bool { + Self::enqueue_with(&self.replacement_sender, self.execution_context, sequence) + } + + fn pop_ready_metadata(&mut self) -> Option { + self.source.as_mut()?.pop_front().map(|(ready, _)| ready) + } + + fn take_wake(&mut self) -> Option { + self.wake_rx.try_recv().ok() + } + + fn retire_consumer(&mut self) { + drop(self.source.take()); + } +} + impl WebCryptoTaskLane { fn new() -> Self { let (wake_tx, wake_rx) = tokio::sync::mpsc::unbounded_channel(); @@ -2206,6 +2268,11 @@ fn shared_worker_client_event_source_conforms_to_page_queue_contract() { assert_fifo_replacement_and_route_retirement(SharedWorkerClientEventLane::new()); } +#[test] +fn bitmap_task_source_conforms_to_page_queue_contract() { + assert_fifo_replacement_and_route_retirement(BitmapTaskLane::new()); +} + #[test] fn webcrypto_task_source_conforms_to_page_queue_contract() { assert_fifo_replacement_and_route_retirement(WebCryptoTaskLane::new()); @@ -2438,6 +2505,14 @@ fn unified_ready_descriptors_expose_one_fifo_head_per_typed_source() { .bind_client(allocated_shared_worker_client_id()) .send(SharedWorkerClientEvent::Closed) .expect("SharedWorker client event should enter the unified source set"); + routes + .bitmap_task_sender(document_token(1)) + .bind_task( + window_execution_context(88), + RendererPageBitmapTaskId::new(13), + ) + .send(Err(BitmapRejection::InvalidState)) + .expect("Bitmap task should enter the unified source set"); routes .webcrypto_task_sender(document_token(1)) .bind_task( @@ -2536,7 +2611,7 @@ fn unified_ready_descriptors_expose_one_fifo_head_per_typed_source() { .expect("dynamic-import action should enter the unified source set"); let descriptors = sources.ready_descriptors(); - assert_eq!(descriptors.len(), 18); + assert_eq!(descriptors.len(), 19); assert!(descriptors.iter().any(|descriptor| matches!( descriptor, RendererPageReadyDescriptor::DomManipulation { .. } @@ -2557,6 +2632,11 @@ fn unified_ready_descriptors_expose_one_fifo_head_per_typed_source() { descriptor, RendererPageReadyDescriptor::SharedWorkerClientEvent { .. } ))); + assert!( + descriptors + .iter() + .any(|descriptor| matches!(descriptor, RendererPageReadyDescriptor::BitmapTask { .. })) + ); assert!(descriptors.iter().any(|descriptor| matches!( descriptor, RendererPageReadyDescriptor::WebCryptoTask { .. } @@ -2626,7 +2706,7 @@ fn unified_ready_descriptors_expose_one_fifo_head_per_typed_source() { )); assert_eq!( sources.ready_descriptors().len(), - 17, + 18, "one Page turn must remove only the selected source head" ); } diff --git a/moli-renderer-v8/src/page_task_queue/resource_completions.rs b/moli-renderer-v8/src/page_task_queue/resource_completions.rs index 5992a30dab..513090e92e 100644 --- a/moli-renderer-v8/src/page_task_queue/resource_completions.rs +++ b/moli-renderer-v8/src/page_task_queue/resource_completions.rs @@ -200,6 +200,7 @@ pub(crate) enum RendererOwnerWakeSource { ServiceWorkerClientMessage, /// A completed WebCrypto operation entered its exact Page/Window task /// source. The queued task, not this admission hint, owns identity. + BitmapTask, WebCryptoTask, /// A Page-side IndexedDB task entered its exact Window-realm source. IndexedDbTask, diff --git a/moli-renderer-v8/src/page_task_queue/senders.rs b/moli-renderer-v8/src/page_task_queue/senders.rs index bc78b7c375..dc28371816 100644 --- a/moli-renderer-v8/src/page_task_queue/senders.rs +++ b/moli-renderer-v8/src/page_task_queue/senders.rs @@ -17,6 +17,7 @@ use super::{ resource_completions::RendererOwnerWakeSender, }; use crate::page_resource_completion::RendererPageResourceCompletionSender; +use crate::page_task_queue::RendererPageBitmapTaskSender; use crate::page_task_queue::RendererPageChildFrameTaskSender; use crate::page_task_queue::RendererPageChildModuleDependencyFetchStartSender; use crate::page_task_queue::RendererPageChildModuleScriptTerminalSender; @@ -80,6 +81,7 @@ pub(crate) struct RendererPageJsContextTaskSenders { dedicated_worker_client_event: RendererPageDedicatedWorkerClientEventSender, shared_worker_client_event: RendererPageSharedWorkerClientEventSender, worker_host_bridge: RendererWorkerHostBridgeEventSender, + bitmap_task: RendererPageBitmapTaskSender, webcrypto_task: RendererPageWebCryptoTaskSender, indexed_db_task: RendererPageIndexedDbTaskSender, opfs_task: RendererPageOpfsTaskSender, @@ -173,6 +175,9 @@ impl RendererPageJsContextTaskSenders { &self.worker_host_bridge } + pub(crate) fn bitmap_task(&self) -> &RendererPageBitmapTaskSender { + &self.bitmap_task + } pub(crate) fn webcrypto_task(&self) -> &RendererPageWebCryptoTaskSender { &self.webcrypto_task } @@ -346,6 +351,7 @@ impl PageRuntimeTaskSource { .dedicated_worker_client_event_sender(root_document), shared_worker_client_event: routes.shared_worker_client_event_sender(root_document), worker_host_bridge: routes.worker_host_bridge_event_sender(root_document), + bitmap_task: routes.bitmap_task_sender(root_document), webcrypto_task: routes.webcrypto_task_sender(root_document), indexed_db_task: routes.indexed_db_task_sender(root_document), opfs_task: routes.opfs_task_sender(root_document), diff --git a/moli-renderer-v8/src/runtime/mod.rs b/moli-renderer-v8/src/runtime/mod.rs index fbb7d57b73..00e7573203 100644 --- a/moli-renderer-v8/src/runtime/mod.rs +++ b/moli-renderer-v8/src/runtime/mod.rs @@ -93,6 +93,7 @@ pub(crate) use self::page_turn_scheduler::{ PageOwnerBlockedReason, PageOwnerTurnOutcome, PageOwnerTurnReadiness, }; pub(crate) use self::page_vm::AuthorizedCurrentBroadcastChannelDelivery; +pub(crate) use self::page_vm::AuthorizedCurrentPageBitmapTask; pub(crate) use self::page_vm::AuthorizedCurrentPageChildClassicScriptSourceLoad; pub(crate) use self::page_vm::AuthorizedCurrentPageChildDocumentLifecycle; pub(crate) use self::page_vm::AuthorizedCurrentPageChildDocumentScriptReady; diff --git a/moli-renderer-v8/src/runtime/owner.rs b/moli-renderer-v8/src/runtime/owner.rs index 808a5bdd44..9333c5a731 100644 --- a/moli-renderer-v8/src/runtime/owner.rs +++ b/moli-renderer-v8/src/runtime/owner.rs @@ -1067,6 +1067,7 @@ fn page_turn_trigger_log_label(trigger: PageTurnTrigger) -> &'static str { Some(RendererOwnerWakeSource::ServiceWorkerClientMessage) => { "service-worker-client-message-wake" } + Some(RendererOwnerWakeSource::BitmapTask) => "bitmap-task-wake", Some(RendererOwnerWakeSource::WebCryptoTask) => "webcrypto-task-wake", Some(RendererOwnerWakeSource::IndexedDbTask) => "indexed-db-task-wake", Some(RendererOwnerWakeSource::OpfsTask) => "opfs-task-wake", diff --git a/moli-renderer-v8/src/runtime/page_dom.rs b/moli-renderer-v8/src/runtime/page_dom.rs index 0130bdab0b..74ffdf8b38 100644 --- a/moli-renderer-v8/src/runtime/page_dom.rs +++ b/moli-renderer-v8/src/runtime/page_dom.rs @@ -1612,6 +1612,7 @@ impl PageVm { let pending_runtime_work = self .vm_mut() .has_post_domcontentloaded_runtime_work_for_wait() + || self.vm().has_pending_bitmap_tasks() || self.vm().has_pending_webcrypto_tasks() || self.vm().has_pending_opfs_tasks(); state.saw_post_domcontentloaded_runtime_work |= pending_runtime_work; diff --git a/moli-renderer-v8/src/runtime/page_turn_scheduler.rs b/moli-renderer-v8/src/runtime/page_turn_scheduler.rs index 731449d61e..31c70fa86f 100644 --- a/moli-renderer-v8/src/runtime/page_turn_scheduler.rs +++ b/moli-renderer-v8/src/runtime/page_turn_scheduler.rs @@ -482,6 +482,7 @@ mod tests { }, page_resource_completion::RendererPageResourceCompletionOwner, page_task_queue::{ + RendererPageBitmapTaskId, RendererPageBitmapTaskOwner, RendererPageBroadcastChannelDeliveryOwner, RendererPageChildFrameTaskOwner, RendererPageChildFrameTaskTarget, RendererPageChildModuleDependencyFetchStartOwner, RendererPageChildModuleScriptTerminalOwner, @@ -762,6 +763,22 @@ mod tests { } } + fn bitmap_descriptor(ready_at: Instant, order: u64) -> RendererPageReadyDescriptor { + RendererPageReadyDescriptor::BitmapTask { + ready: ready_metadata(ready_at, order), + owner: RendererPageBitmapTaskOwner::new( + RendererDocumentToken::new_for_testing(crate::PageId::new_for_testing(1), 1), + WindowExecutionContextIdentity::new( + WindowExecutionContextOwner::Frame(LocalWindowId(7)), + OwnerDispatchScope::Top, + RuntimeObservableContextToken::from_raw(11), + WindowExecutionContextAccessPolicy::EnforceWebOrigin, + ), + RendererPageBitmapTaskId::new(order), + ), + } + } + fn webcrypto_descriptor(ready_at: Instant, order: u64) -> RendererPageReadyDescriptor { RendererPageReadyDescriptor::WebCryptoTask { ready: ready_metadata(ready_at, order), @@ -1045,6 +1062,7 @@ mod tests { RendererPageTaskSourceKind::ServiceWorkerClientMessage => { service_worker_client_message_descriptor(runnable_since, order) } + RendererPageTaskSourceKind::BitmapTask => bitmap_descriptor(runnable_since, order), RendererPageTaskSourceKind::WebCryptoTask => { webcrypto_descriptor(runnable_since, order) } diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index 8e9179201e..b1d9e45482 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -172,6 +172,7 @@ pub(crate) use page_selected_task_test_harness::{ ClaimedPageSelectedTaskForTest, PageSelectedTaskTestSelector, }; pub(crate) use page_task_completion::{IntoPageTaskCompletion, PageTaskCompletion}; +mod page_bitmap_task; mod page_text_track_default_mode; mod page_text_track_default_mode_task_completion; mod page_text_track_load; @@ -197,6 +198,7 @@ mod parser_owned_module_completion; mod parser_task_completion; mod selected_page_task; +pub(crate) use page_bitmap_task::AuthorizedCurrentPageBitmapTask; pub(crate) use page_broadcast_channel_delivery::AuthorizedCurrentBroadcastChannelDelivery; pub(crate) use page_child_frame_task::{ AuthorizedCurrentPageChildClassicScriptSourceLoad, AuthorizedCurrentPageChildDocumentLifecycle, diff --git a/moli-renderer-v8/src/runtime/page_vm/page_bitmap_task.rs b/moli-renderer-v8/src/runtime/page_vm/page_bitmap_task.rs new file mode 100644 index 0000000000..551545b666 --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/page_bitmap_task.rs @@ -0,0 +1,68 @@ +use crate::page_task_queue::{ + PageBitmapTaskTargetEffect, PageBitmapTaskTurnAction, PageBitmapTaskTurnOutcome, + RendererPageBitmapTask, RendererPageBitmapTaskOwner, +}; + +use super::PageVm; + +/// Proof that the Page arbiter matched a selected completion against the exact +/// root PageVm, Window realm, and pending Promise entry. +pub(crate) struct AuthorizedCurrentPageBitmapTask(RendererPageBitmapTask); + +impl AuthorizedCurrentPageBitmapTask { + fn new(task: RendererPageBitmapTask) -> Self { + Self(task) + } + + pub(crate) fn into_task(self) -> RendererPageBitmapTask { + self.0 + } +} + +impl PageVm { + fn current_page_bitmap_task_owner( + &self, + expected: RendererPageBitmapTaskOwner, + ) -> Option { + let execution_context = self + .vm() + .current_pending_bitmap_task_execution_context(expected.task())?; + Some(RendererPageBitmapTaskOwner::new( + self.document_lifecycle.identity().document, + execution_context, + expected.task(), + )) + } + + pub(in crate::runtime) fn apply_selected_page_bitmap_task_turn( + &mut self, + task: RendererPageBitmapTask, + ) -> anyhow::Result { + let owner = task.owner(); + let current_owner = self.current_page_bitmap_task_owner(owner); + let target_effect = if current_owner == Some(owner) { + self.vm_mut() + .apply_current_bitmap_task_body(AuthorizedCurrentPageBitmapTask::new(task))?; + PageBitmapTaskTargetEffect::SettledCurrentOwner + } else { + // A root mismatch means `task_id` belongs to another PageVm + // namespace and must not be used to touch this PageVm's pending + // map. Within the same root, exact cleanup is safe and prevents a + // retired realm from retaining its resolver. + if owner.root_document() == self.document_lifecycle.identity().document { + self.vm_mut().discard_stale_bitmap_task(owner); + } + tracing::debug!( + ?owner, + ?current_owner, + "ignored stale exact-owner Bitmap task" + ); + PageBitmapTaskTargetEffect::IgnoredStaleOwner { current_owner } + }; + let action = PageBitmapTaskTurnAction { + owner, + target_effect, + }; + Ok(PageBitmapTaskTurnOutcome::new(action)) + } +} diff --git a/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs b/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs index 34988b1d3b..bb77c451b9 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs @@ -76,6 +76,7 @@ pub(crate) enum PageSelectedTaskTestSelector { StylesheetCompletion, TextTrackNetworking, UserInteraction, + BitmapTask, WebCryptoTask, WebSocket, WindowMessage, @@ -281,6 +282,9 @@ impl PageSelectedTaskTestSelector { RendererPageReadyDescriptor::MiscPlatformApi { .. } ) } + Self::BitmapTask => { + matches!(descriptor, RendererPageReadyDescriptor::BitmapTask { .. }) + } Self::WebCryptoTask => matches!( descriptor, RendererPageReadyDescriptor::WebCryptoTask { .. } @@ -406,6 +410,7 @@ impl PageSelectedTaskTestSelector { RendererPageSchedulerTask::SharedWorkerClientEvent(_), ) | (Self::UserInteraction, RendererPageSchedulerTask::UserInteraction(_)) + | (Self::BitmapTask, RendererPageSchedulerTask::BitmapTask(_)) | (Self::WebCryptoTask, RendererPageSchedulerTask::WebCryptoTask(_)) | (Self::WindowMessage, RendererPageSchedulerTask::WindowMessage(_)) => true, (Self::WebSocket, RendererPageSchedulerTask::WebSocket(_)) => true, diff --git a/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs b/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs index f45fa2e4f9..bce2e08304 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs @@ -83,6 +83,7 @@ impl PageVm { | RendererPageReadyDescriptor::SharedWorkerClientEvent { .. } | RendererPageReadyDescriptor::ServiceWorkerInternal { .. } | RendererPageReadyDescriptor::ServiceWorkerClientMessage { .. } + | RendererPageReadyDescriptor::BitmapTask { .. } | RendererPageReadyDescriptor::WebCryptoTask { .. } | RendererPageReadyDescriptor::IndexedDbTask { .. } | RendererPageReadyDescriptor::OpfsTask { .. } diff --git a/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs b/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs index cca600a1cd..35d63f8740 100644 --- a/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs +++ b/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs @@ -142,6 +142,13 @@ impl PageVm { .await?; Ok(()) } + RendererPageSchedulerTask::BitmapTask(task) => { + let outcome = self.apply_selected_page_bitmap_task_turn(task)?; + if outcome.action.settled_current_owner() { + self.finish_selected_page_task_checkpoint()?; + } + Ok(()) + } RendererPageSchedulerTask::WebCryptoTask(task) => { let outcome = self.apply_selected_page_webcrypto_task_turn(task)?; if outcome.action.settled_current_owner() { diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/bitmap.rs b/moli-renderer-v8/src/runtime/page_vm/tests/bitmap.rs new file mode 100644 index 0000000000..51373e55ab --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/tests/bitmap.rs @@ -0,0 +1,431 @@ +use super::*; + +#[tokio::test(flavor = "current_thread")] +async fn bitmap_argument_errors_reject_promises_without_queuing_decode_tasks() { + run_page_vm_async_test(async move { + let mut page_vm = test_page_vm(); + page_vm.vm_mut().eval(r#" + globalThis.outcomes = []; + globalThis.getters = []; + const source = new OffscreenCanvas(2, 3); + const marker = {}; + const probes = [ + () => createImageBitmap(), + () => createImageBitmap({width: 2, height: 3}), + () => createImageBitmap(source, 0, 0), + () => createImageBitmap(source, 0, 0, 0, 1), + () => createImageBitmap(source, {resizeWidth: 0}), + () => createImageBitmap(source, {resizeHeight: Infinity}), + () => createImageBitmap(source, {resizeWidth: -1}), + () => createImageBitmap(source, {imageOrientation: 'invalid'}), + () => createImageBitmap(new OffscreenCanvas(0, 3)), + () => createImageBitmap(source, { + get colorSpaceConversion() { getters.push('colorSpaceConversion'); }, + get imageOrientation() { getters.push('imageOrientation'); }, + get premultiplyAlpha() { getters.push('premultiplyAlpha'); throw marker; }, + get resizeHeight() { getters.push('unexpected'); } + }), + ]; + for (const probe of probes) { + const promise = probe(); + if (!(promise instanceof Promise)) throw new Error('missing Promise'); + promise.then(() => outcomes.push('unexpected success'), error => outcomes.push(error === marker ? 'marker' : error.name)); + } + 'scheduled' + "#)?; + assert!(!page_vm.vm().has_pending_bitmap_tasks()); + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcomes)")?, r#"["TypeError","TypeError","TypeError","RangeError","InvalidStateError","TypeError","TypeError","TypeError","InvalidStateError","marker"]"#); + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(getters)")?, r#"["colorSpaceConversion","imageOrientation","premultiplyAlpha"]"#); + Ok::<_, anyhow::Error>(()) + }).await.expect("bitmap Web IDL rejections"); +} + +async fn run_ready_bitmap_task(page_vm: &mut PageVm) -> anyhow::Result<()> { + let loader = page_vm.request_client.clone(); + let deadline = Instant::now() + Duration::from_secs(5); + while !page_vm + .run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::BitmapTask, &loader) + .await? + { + anyhow::ensure!( + Instant::now() < deadline, + "bitmap decode did not enqueue its result" + ); + tokio::time::sleep(Duration::from_millis(1)).await; + } + Ok(()) +} + +#[tokio::test(flavor = "current_thread")] +async fn bitmap_blob_decodes_bytes_and_settles_in_a_later_task_in_the_intrinsic_realm() { + run_page_vm_async_test(async move { + let mut page_vm = test_page_vm(); + let pixels = moli_image::RgbaImage::try_new(1, 2, vec![255, 0, 0, 255, 0, 255, 0, 255])?; + let encoded = moli_image::encode_png(&pixels)?.bytes; + page_vm.vm_mut().eval(&format!("globalThis.pngBytes = new Uint8Array({encoded:?}); 'ready'"))?; + page_vm.vm_mut().eval(r#" + globalThis.outcomes = []; + const prototype = ImageBitmap.prototype; + const valid = createImageBitmap(new Blob([pngBytes], {type: 'text/plain'}), {imageOrientation: 'flipY'}); + const invalid = createImageBitmap(new Blob(['not an image'], {type: 'image/png'})); + const empty = createImageBitmap(new Blob()); + globalThis.ImageBitmap = function ReplacedImageBitmap() {}; + pngBytes.fill(0); + valid.then(bitmap => { + const canvas = new OffscreenCanvas(1, 2); + const ctx = canvas.getContext('2d'); + ctx.drawImage(bitmap, 0, 0); + outcomes.push([Object.getPrototypeOf(bitmap) === prototype, bitmap.width, bitmap.height, Array.from(ctx.getImageData(0, 0, 1, 2).data)]); + }, error => outcomes.push(error.name)); + invalid.catch(error => outcomes.push('invalid:' + error.name)); + empty.catch(error => outcomes.push('empty:' + error.name)); + Promise.resolve().then(() => outcomes.push('microtask')); + 'scheduled' + "#)?; + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcomes)")?, r#"["microtask"]"#); + for _ in 0..3 { run_ready_bitmap_task(&mut page_vm).await?; } + assert!(!page_vm.vm().has_pending_bitmap_tasks()); + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcomes.filter(Array.isArray))")?, "[[true,1,2,[0,255,0,255,255,0,0,255]]]"); + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcomes.filter(value => typeof value === 'string').sort())")?, r#"["empty:InvalidStateError","invalid:InvalidStateError","microtask"]"#); + Ok::<_, anyhow::Error>(()) + }).await.expect("bitmap Blob decode and Promise task timing"); +} + +#[tokio::test(flavor = "current_thread")] +async fn bitmap_snapshots_pixels_crops_resizes_and_rejects_closed_sources() { + run_page_vm_async_test(async move { + let mut page_vm = test_page_vm(); + page_vm.vm_mut().eval(r#" + globalThis.results = []; + const source = new OffscreenCanvas(2, 1); + const ctx = source.getContext('2d'); + ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 2, 1); + const pending = createImageBitmap(source, 2, 0, -2, 1, {resizeWidth: 4, resizeQuality: 'pixelated'}); + ctx.fillStyle = 'blue'; ctx.fillRect(0, 0, 2, 1); + pending.then(async bitmap => { + const dest = new OffscreenCanvas(4, 2); + const destCtx = dest.getContext('2d'); + destCtx.drawImage(bitmap, 0, 0); + results.push([bitmap.width, bitmap.height, Array.from(destCtx.getImageData(0, 0, 1, 1).data)]); + const copyPromise = createImageBitmap(bitmap); + bitmap.close(); bitmap.close(); + try { destCtx.drawImage(bitmap, 0, 0); } catch(error) { results.push(error.name); } + await createImageBitmap(bitmap).catch(error => results.push(error.name)); + const copy = await copyPromise; + results.push([copy.width, copy.height]); + }); + const data = new ImageData(new Uint8ClampedArray([0, 255, 0, 255]), 1, 1); + createImageBitmap(data, -1, 0, 2, 1, {resizeQuality: 'pixelated'}).then(bitmap => { + const dest = new OffscreenCanvas(2, 1); + const ctx = dest.getContext('2d'); + ctx.drawImage(bitmap, 0, 0); + globalThis.cropPixels = Array.from(ctx.getImageData(0, 0, 2, 1).data); + }); + data.data.fill(0); + const htmlCanvas = document.createElement('canvas'); + htmlCanvas.width = 5; htmlCanvas.height = 3; + Object.defineProperty(htmlCanvas, 'width', {get() { throw new Error('observable width getter'); }}); + createImageBitmap(htmlCanvas).then(bitmap => globalThis.htmlDimensions = [bitmap.width, bitmap.height]); + 'scheduled' + "#)?; + for _ in 0..4 { run_ready_bitmap_task(&mut page_vm).await?; } + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(results)")?, r#"[[4,2,[255,0,0,255]],"InvalidStateError","InvalidStateError",[4,2]]"#); + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(cropPixels)")?, "[0,0,0,0,0,255,0,255]"); + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(htmlDimensions)")?, "[5,3]"); + Ok::<_, anyhow::Error>(()) + }).await.expect("bitmap snapshots and close"); +} + +#[tokio::test(flavor = "current_thread")] +async fn bitmap_task_survives_document_open_in_the_same_window() { + run_page_vm_async_test(async move { + let mut page_vm = test_page_vm(); + page_vm.vm_mut().eval(r#" + globalThis.outcome = 'pending'; + createImageBitmap(new OffscreenCanvas(3, 2)).then(bitmap => outcome = [bitmap.width, bitmap.height]); + document.open(); + 'replaced' + "#)?; + assert!(page_vm.vm().has_pending_bitmap_tasks()); + run_ready_bitmap_task(&mut page_vm).await?; + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcome)")?, "[3,2]"); + assert!(!page_vm.vm().has_pending_bitmap_tasks()); + Ok::<_, anyhow::Error>(()) + }).await.expect("bitmap Window lifetime"); +} + +#[tokio::test(flavor = "current_thread")] +async fn bitmap_task_uses_child_intrinsics_and_retires_when_the_child_window_is_removed() { + run_page_vm_async_test(async move { + let mut page_vm = test_page_vm(); + let loader = page_vm.request_client.clone(); + page_vm.vm_mut().eval(r#" + const frame = document.createElement('iframe'); + globalThis.bitmapFrame = frame; + globalThis.bitmapChildOutcome = 'pending'; + (document.body || document.documentElement || document).appendChild(frame); + void frame.contentWindow.Function; + const script = frame.contentDocument.createElement('script'); + script.textContent = "createImageBitmap(new OffscreenCanvas(3, 4)).then(bitmap => { parent.bitmapChildOutcome = [bitmap.width, bitmap.height, Object.getPrototypeOf(bitmap) === ImageBitmap.prototype]; createImageBitmap(new OffscreenCanvas(1, 1)).then(() => parent.bitmapChildOutcome = 'stale'); });"; + frame.contentDocument.body.appendChild(script); + 'scheduled' + "#)?; + run_expected_child_realm_materialization_for_wait(&mut page_vm, "child bitmap realm").await; + assert!(page_vm.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::ChildDocumentScriptReady, &loader).await?); + run_ready_bitmap_task(&mut page_vm).await?; + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(bitmapChildOutcome)")?, "[3,4,true]"); + assert!(page_vm.vm().has_pending_bitmap_tasks()); + page_vm.vm_mut().eval("document.open(); 'retired'")?; + assert!(!page_vm.vm().has_pending_bitmap_tasks()); + run_ready_bitmap_task(&mut page_vm).await?; + assert_eq!(page_vm.vm_mut().eval("JSON.stringify(bitmapChildOutcome)")?, "[3,4,true]"); + Ok::<_, anyhow::Error>(()) + }).await.expect("bitmap child realm settlement and retirement"); +} +use crate::context_bootstrap::BitmapRejection; +use crate::page_task_queue::{ + MainDocumentMetaRefreshNavigationTask, PageInternalLoadingTargetEffect, + PageOwnedInternalLoadingTask, +}; + +#[tokio::test(flavor = "current_thread")] +async fn create_image_bitmap_offscreen_canvas_and_close_contract() { + run_page_vm_async_test(async move { + let mut page_vm = test_page_vm(); + let loader = page_vm.request_client.clone(); + + page_vm.vm_mut().exec( + r#" + (() => { + const caughtName = callback => { + try { + callback(); + return "none"; + } catch (error) { + return error.name; + } + }; + const descriptor = Object.getOwnPropertyDescriptor(window, "createImageBitmap"); + const blank = new OffscreenCanvas(16, 9); + const drawn = new OffscreenCanvas(16, 9); + drawn.getContext("2d").fillRect(0, 0, 1, 1); + globalThis.__imageBitmapProbe = { + functionShape: [ + typeof createImageBitmap, + createImageBitmap.name, + createImageBitmap.length, + Object.prototype.hasOwnProperty.call(createImageBitmap, "prototype"), + descriptor.enumerable, + descriptor.configurable, + descriptor.writable, + caughtName(() => new createImageBitmap(drawn)), + caughtName(() => createImageBitmap().catch(() => {})), + ], + constructorShape: [ + typeof ImageBitmap, + ImageBitmap.name, + ImageBitmap.length, + Object.prototype.toString.call(ImageBitmap.prototype), + Object.getPrototypeOf(ImageBitmap.prototype) === Object.prototype, + caughtName(() => new ImageBitmap()), + ], + settled: false, + }; + Promise.all([ + createImageBitmap(blank).then( + () => "resolved", + error => `rejected:${error.name}`, + ), + createImageBitmap(drawn).then(bitmap => { + const before = [ + Object.prototype.toString.call(bitmap), + bitmap instanceof ImageBitmap, + Object.getPrototypeOf(bitmap) === ImageBitmap.prototype, + Object.getOwnPropertyNames(bitmap).length, + bitmap.width, + bitmap.height, + typeof bitmap.close, + ]; + const closeResult = bitmap.close(); + return [before, typeof closeResult, bitmap.width, bitmap.height]; + }), + ]).then(([blankOutcome, bitmapOutcome]) => { + __imageBitmapProbe.blankOutcome = blankOutcome; + __imageBitmapProbe.bitmapOutcome = bitmapOutcome; + __imageBitmapProbe.settled = true; + }); + })() + "#, + None, + ) + .expect("createImageBitmap probe should execute"); + + assert_eq!(page_vm.vm_mut().eval("String(__imageBitmapProbe.settled)")?, "false"); + for _ in 0..2 { + assert!(page_vm.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::BitmapTask, &loader).await?); + } + let result = page_vm.vm_mut() + .eval("JSON.stringify(globalThis.__imageBitmapProbe)") + .expect("createImageBitmap probe should be readable"); + assert_eq!( + result, + r#"{"functionShape":["function","createImageBitmap",1,false,true,true,true,"TypeError","none"],"constructorShape":["function","ImageBitmap",0,"[object ImageBitmap]",true,"TypeError"],"settled":true,"blankOutcome":"resolved","bitmapOutcome":[["[object ImageBitmap]",true,true,0,16,9,"function"],"undefined",0,0]}"# + ); + Ok::<_, anyhow::Error>(()) + }).await.expect("bitmap task surface"); +} + +#[test] +fn bitmap_task_rejects_a_real_page_vm_replacement_identity_collision() { + run_page_vm_large_stack_async_test( + "bitmap-real-page-vm-replacement-collision", + || async move { + let (base_url, server) = spawn_path_response_http_server(vec![( + "/replacement.html", + "HTTP/1.1 200 OK", + "replacement".to_owned(), + Duration::ZERO, + )]) + .await; + let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()) + .expect("loader"); + let document_url = Url::parse(&format!("{base_url}/initial.html")).unwrap(); + let (page_vm, _resource_source, _owner_wake_rx) = + page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url); + let local_executor = page_vm.local_executor.clone(); + + local_executor + .run(async move { + let mut page_vm = page_vm; + let retired_producer = page_vm + .vm_mut() + .register_pending_bitmap_task_producer_for_executor_test()?; + let retired_owner = retired_producer.owner(); + assert_eq!( + retired_owner.root_document(), + page_vm.document_lifecycle.identity().document + ); + retired_producer + .send(Err(BitmapRejection::InvalidState)) + .expect("retired Bitmap task should enter the stable Page source"); + + let replacement_url = format!("{base_url}/replacement.html"); + page_vm + .vm_mut() + .eval(&format!("location.href = {replacement_url:?}; 'queued'"))?; + let mut pending_document_lifecycle_turn = None; + let navigation = page_vm + .follow_pending_location_navigation_one_turn_async( + &mut pending_document_lifecycle_turn, + PageVmInitStage::Load, + ) + .await?; + assert!(matches!( + navigation, + crate::runtime::PageVmFollowNavigationTurnOutcome::Completed + | crate::runtime::PageVmFollowNavigationTurnOutcome::PostParseLifecycle { + .. + } + )); + + let current_producer = page_vm + .vm_mut() + .register_pending_bitmap_task_producer_for_executor_test()?; + let current_owner = current_producer.owner(); + assert_eq!( + retired_owner.task(), + current_owner.task(), + "fresh PageVm counters should naturally reuse the first Bitmap task id and transport generation" + ); + assert_eq!( + retired_owner.execution_context(), + current_owner.execution_context(), + "fresh PageVm counters should naturally reuse the top Window/realm identity" + ); + assert_ne!( + retired_owner.root_document(), + current_owner.root_document(), + "the stable Page queue must namespace identical local owners by root Document" + ); + assert_eq!( + current_owner.root_document(), + page_vm.document_lifecycle.identity().document + ); + current_producer + .send(Err(BitmapRejection::InvalidState)) + .expect("replacement Bitmap task should enter the same stable Page source"); + + let current_document_owner = page_vm + .vm() + .current_main_document_task_owner() + .expect("replacement main Document owner"); + page_vm + .vm() + .schedule_page_internal_loading_task( + PageOwnedInternalLoadingTask::MetaRefreshNavigation( + MainDocumentMetaRefreshNavigationTask::new( + current_document_owner, + 0, + Url::parse("https://example.test/refresh").unwrap(), + ), + ), + Instant::now(), + ) + .expect("internal-loading task should enter the stable Page source"); + park_current_document_websocket_for_test( + &mut page_vm, + moli_websocket::Event::TextMessage { + socket_id: 41, + data: "blocked".to_owned(), + }, + ) + .await; + assert!( + page_vm + .run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::BitmapTask, &loader) + .await?, + "retired Bitmap task should remain runnable beside independent internal-loading and backpressured WebSocket work" + ); + + assert_eq!( + page_vm + .vm() + .current_pending_bitmap_task_execution_context(current_owner.task()), + Some(current_owner.execution_context()), + "discarding the old completion must not remove the colliding replacement Promise" + ); + + assert!( + page_vm + .run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::BitmapTask, &loader) + .await?, + "replacement Bitmap task should consume the following turn" + ); + + assert_eq!( + page_vm + .vm() + .current_pending_bitmap_task_execution_context(current_owner.task()), + None, + "the current completion must settle exactly the replacement Promise" + ); + let internal_loading = page_vm + .run_internal_loading_body_for_test() + .expect("the independent internal-loading task should remain queued"); + assert_eq!( + internal_loading.action.target_effect, + PageInternalLoadingTargetEffect::AppliedToCurrentOwner { + effect: crate::page_task_queue::PageOwnedInternalLoadingTaskEffect::MetaRefreshNavigationNotActivated, + }, + "the synthetic refresh must still enforce its own post-load prerequisite" + ); + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("Bitmap replacement should run through the typed task executor"); + server + .await + .expect("Bitmap PageVm replacement server should finish"); + }, + ); +} diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index c3e7e1b230..352c8f4df9 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -1,3 +1,4 @@ +mod bitmap; use std::{ sync::Arc, time::{Duration, Instant}, diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index af5fe7af19..52970e057d 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -782,6 +782,7 @@ mod subresource_command_completion; mod subresource_fetch; pub(crate) use subresource_command_completion::AsyncSubresourceCommandExecution; pub(crate) use subresource_fetch::AsyncSubresourceFetchBodyActivity; +mod bitmap_tasks; mod page_resource_completion_task_completion; mod text_search; mod text_track_default_mode; @@ -3850,6 +3851,8 @@ impl ScriptVm { let retired_image_decode_count = host.retire_image_decode_requests_for_context_token( context.runtime_observable_context_token, ); + let retired_bitmap_count = + host.retire_bitmap_context_token(context.runtime_observable_context_token); let retired_webcrypto_count = host.retire_webcrypto_context_token(context.runtime_observable_context_token); host.retire_opfs_context_token(context.runtime_observable_context_token); @@ -3880,6 +3883,7 @@ impl ScriptVm { retired_message_port_count, retired_window_message_count, retired_window_execution_context_count, + retired_bitmap_count, retired_webcrypto_count, retired_worker_count, retired_shared_worker_count, @@ -3896,12 +3900,13 @@ impl ScriptVm { retired_message_port_count = runtime_binding_retirement.2, retired_window_message_count = runtime_binding_retirement.3, retired_window_execution_context_count = runtime_binding_retirement.4, - retired_webcrypto_count = runtime_binding_retirement.5, - retired_worker_count = runtime_binding_retirement.6, - retired_shared_worker_count = runtime_binding_retirement.7, - retired_xhr_count = runtime_binding_retirement.8, - aborted_fetch_count = runtime_binding_retirement.9.0, - detached_keepalive_fetch_count = runtime_binding_retirement.9.1, + retired_bitmap_count = runtime_binding_retirement.5, + retired_webcrypto_count = runtime_binding_retirement.6, + retired_worker_count = runtime_binding_retirement.7, + retired_shared_worker_count = runtime_binding_retirement.8, + retired_xhr_count = runtime_binding_retirement.9, + aborted_fetch_count = runtime_binding_retirement.10.0, + detached_keepalive_fetch_count = runtime_binding_retirement.10.1, retired_timer_count, "retired child Runtime binding context" ); @@ -3961,6 +3966,7 @@ impl ScriptVm { runtime_binding_retirement, retired_image_decode_count, retired_message_port_count, + retired_bitmap_count, retired_webcrypto_count, retired_worker_count, retired_shared_worker_count, @@ -3973,6 +3979,8 @@ impl ScriptVm { let retired_image_decode_count = host.retire_image_decode_requests_for_context_token( context.runtime_observable_context_token, ); + let retired_bitmap_count = + host.retire_bitmap_context_token(context.runtime_observable_context_token); let retired_webcrypto_count = host.retire_webcrypto_context_token(context.runtime_observable_context_token); host.retire_opfs_context_token(context.runtime_observable_context_token); @@ -3995,6 +4003,7 @@ impl ScriptVm { runtime_binding_retirement, retired_image_decode_count, retired_message_port_count, + retired_bitmap_count, retired_webcrypto_count, retired_worker_count, retired_shared_worker_count, @@ -4023,6 +4032,7 @@ impl ScriptVm { .retired_execution_context_count(), retired_image_decode_count, retired_message_port_count, + retired_bitmap_count, retired_webcrypto_count, retired_worker_count, retired_shared_worker_count, @@ -7349,6 +7359,9 @@ impl ScriptVm { .dedicated_worker_running_worker_isolate_count_for_diagnostics() } + pub(crate) fn has_pending_bitmap_tasks(&self) -> bool { + self._context_host.borrow().has_pending_bitmap_tasks() + } pub(crate) fn has_pending_webcrypto_tasks(&self) -> bool { self._context_host.borrow().has_pending_webcrypto_tasks() } diff --git a/moli-renderer-v8/src/script_vm/bitmap_tasks.rs b/moli-renderer-v8/src/script_vm/bitmap_tasks.rs new file mode 100644 index 0000000000..c38c9aca17 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/bitmap_tasks.rs @@ -0,0 +1,83 @@ +use anyhow::Result; + +use super::ScriptVm; +use crate::{ + context_bootstrap::settle_bitmap_task_result, page_task_queue::RendererPageBitmapTaskOwner, + runtime::AuthorizedCurrentPageBitmapTask, +}; + +impl ScriptVm { + #[cfg(test)] + pub(crate) fn register_pending_bitmap_task_producer_for_executor_test( + &mut self, + ) -> Result { + self.with_default_context_scope_and_checkpoint_for_test(|scope, host_ptr| { + let resolver = v8::PromiseResolver::new(scope) + .expect("Bitmap executor test resolver should exist"); + unsafe { &mut *host_ptr } + .register_pending_bitmap_task(scope, resolver) + .ok_or_else(|| { + anyhow::anyhow!("Bitmap executor test must capture the current Window realm") + }) + }) + } + + pub(crate) fn current_pending_bitmap_task_execution_context( + &self, + task: crate::page_task_queue::RendererPageBitmapTaskId, + ) -> Option { + self._context_host + .borrow() + .current_pending_bitmap_task_execution_context(task) + } + + /// Settle one page-side Bitmap Promise body only after the Page arbiter + /// has authorized its exact PageVm and Window realm. + /// + /// The selected Page-task dispatcher owns the enclosing task's microtask + /// checkpoint. This method deliberately leaves reactions queued after + /// resolving or rejecting the Promise. + pub(crate) fn apply_current_bitmap_task_body( + &mut self, + authorization: AuthorizedCurrentPageBitmapTask, + ) -> Result<()> { + let task = authorization.into_task(); + let owner = task.owner(); + let pending = self + ._context_host + .borrow_mut() + .take_pending_bitmap_task_for_exact_owner(owner.execution_context(), owner.task()) + .ok_or_else(|| { + anyhow::anyhow!("authorized Bitmap task lost its exact pending Promise") + })?; + + let (bound_owner, bound_dispatch_scope, _realm_token, context) = + pending.relevant_context.into_parts(); + debug_assert_eq!(bound_owner, owner.execution_context().owner()); + debug_assert_eq!( + bound_dispatch_scope, + owner.execution_context().dispatch_scope() + ); + let context_ptr: *const v8::Global = &context; + let resolver = pending.resolver; + self.with_context_scope_by_ptr(context_ptr, move |scope, _host_ptr| { + let previous_dispatch_scope = bound_dispatch_scope.enter(scope); + let resolver = v8::Local::new(scope, &resolver); + settle_bitmap_task_result(scope, resolver, task.into_result()); + bound_dispatch_scope.defer_restore(scope, previous_dispatch_scope); + tracing::debug!( + task_id = owner.task().task_id(), + execution_context = ?owner.execution_context(), + "settled Bitmap task body in relevant Window execution context" + ); + Ok(()) + }) + } + + pub(crate) fn discard_stale_bitmap_task(&mut self, owner: RendererPageBitmapTaskOwner) { + let _ = self + ._context_host + .borrow_mut() + .take_pending_bitmap_task_for_exact_owner(owner.execution_context(), owner.task()); + } +} diff --git a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs index 75394f0120..0ea8a9e148 100644 --- a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs +++ b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs @@ -29,85 +29,6 @@ async fn run_one_canvas_image_load_event_task( ); } -#[test] -fn create_image_bitmap_matches_chromium_offscreen_canvas_and_close_contract() { - let mut vm = new_storage_test_vm("https://image-bitmap-surface.test/"); - - vm.exec( - r#" - (() => { - const caughtName = callback => { - try { - callback(); - return "none"; - } catch (error) { - return error.name; - } - }; - const descriptor = Object.getOwnPropertyDescriptor(window, "createImageBitmap"); - const blank = new OffscreenCanvas(16, 9); - const drawn = new OffscreenCanvas(16, 9); - drawn.getContext("2d").fillRect(0, 0, 1, 1); - globalThis.__imageBitmapProbe = { - functionShape: [ - typeof createImageBitmap, - createImageBitmap.name, - createImageBitmap.length, - Object.prototype.hasOwnProperty.call(createImageBitmap, "prototype"), - descriptor.enumerable, - descriptor.configurable, - descriptor.writable, - caughtName(() => new createImageBitmap(drawn)), - caughtName(() => createImageBitmap()), - ], - constructorShape: [ - typeof ImageBitmap, - ImageBitmap.name, - ImageBitmap.length, - Object.prototype.toString.call(ImageBitmap.prototype), - Object.getPrototypeOf(ImageBitmap.prototype) === Object.prototype, - caughtName(() => new ImageBitmap()), - ], - settled: false, - }; - Promise.all([ - createImageBitmap(blank).then( - () => "resolved", - error => `rejected:${error.name}`, - ), - createImageBitmap(drawn).then(bitmap => { - const before = [ - Object.prototype.toString.call(bitmap), - bitmap instanceof ImageBitmap, - Object.getPrototypeOf(bitmap) === ImageBitmap.prototype, - Object.getOwnPropertyNames(bitmap).length, - bitmap.width, - bitmap.height, - typeof bitmap.close, - ]; - const closeResult = bitmap.close(); - return [before, typeof closeResult, bitmap.width, bitmap.height]; - }), - ]).then(([blankOutcome, bitmapOutcome]) => { - __imageBitmapProbe.blankOutcome = blankOutcome; - __imageBitmapProbe.bitmapOutcome = bitmapOutcome; - __imageBitmapProbe.settled = true; - }); - })() - "#, - None, - ) - .expect("createImageBitmap probe should execute"); - - let result = vm - .eval("JSON.stringify(globalThis.__imageBitmapProbe)") - .expect("createImageBitmap probe should be readable"); - assert_eq!( - result, - r#"{"functionShape":["function","createImageBitmap",1,false,true,true,true,"TypeError","TypeError"],"constructorShape":["function","ImageBitmap",0,"[object ImageBitmap]",true,"TypeError"],"settled":true,"blankOutcome":"rejected:InvalidStateError","bitmapOutcome":[["[object ImageBitmap]",true,true,0,16,9,"function"],"undefined",0,0]}"# - ); -} - #[test] fn webgl_viewport_state_matches_chromium_on_html_and_offscreen_contexts() { let mut vm = new_storage_test_vm("https://webgl-viewport.test/");