use deno_core::GarbageCollected;
use deno_core::WebIDL;
use deno_core::cppgc::Ref;
use deno_core::op2;
use deno_core::v8;
use deno_core::v8::cppgc::Visitor;
use deno_core::webidl::Nullable;
use deno_core::webidl::WebIdlConverter;
use deno_error::JsErrorBox;
use deno_image::bitmap::ImageBitmap;
use deno_image::image;
use deno_image::image::DynamicImage;
use deno_image::image::GenericImageView;
use deno_webgpu::canvas::ContextData;
use deno_webgpu::error::GPUError;
use deno_webgpu::wgpu_core;
use deno_webgpu::wgpu_types;
#[derive(Clone)]
pub struct SurfaceBitmap {
pub instance: deno_webgpu::Instance,
pub adapter: wgpu_core::id::AdapterId,
pub device: wgpu_core::id::DeviceId,
pub queue: wgpu_core::id::QueueId,
surface_configuration:
wgpu_types::SurfaceConfiguration<Vec<wgpu_types::TextureFormat>>,
shader: wgpu_core::id::ShaderModuleId,
render_pipeline_layout: wgpu_core::id::PipelineLayoutId,
render_pipeline: wgpu_core::id::RenderPipelineId,
vertex_buffer: wgpu_core::id::BufferId,
index_buffer: wgpu_core::id::BufferId,
bind_group_layout: wgpu_core::id::BindGroupLayoutId,
sampler: wgpu_core::id::SamplerId,
}
impl Drop for SurfaceBitmap {
fn drop(&mut self) {
self.instance.render_pipeline_drop(self.render_pipeline);
self
.instance
.pipeline_layout_drop(self.render_pipeline_layout);
self.instance.shader_module_drop(self.shader);
self.instance.buffer_drop(self.vertex_buffer);
self.instance.buffer_drop(self.index_buffer);
self.instance.bind_group_layout_drop(self.bind_group_layout);
self.instance.sampler_drop(self.sampler);
self.instance.queue_drop(self.queue);
self.instance.device_drop(self.device);
self.instance.adapter_drop(self.adapter);
}
}
pub struct ImageBitmapRenderingContext {
canvas: v8::Global<v8::Object>,
data: ContextData,
pub surface_only: Option<SurfaceBitmap>,
#[allow(dead_code, reason = "TODO(@crowlkats): support alpha option")]
alpha: bool,
}
unsafe impl GarbageCollected for ImageBitmapRenderingContext {
fn trace(&self, _visitor: &mut Visitor) {}
fn get_name(&self) -> &'static std::ffi::CStr {
c"ImageBitmapRenderingContext"
}
}
#[op2]
impl ImageBitmapRenderingContext {
#[getter]
fn canvas(&self) -> v8::Global<v8::Object> {
self.canvas.clone()
}
fn transfer_from_image_bitmap(
&self,
#[webidl] bitmap: Nullable<Ref<ImageBitmap>>,
) -> Result<(), JsErrorBox> {
if let Some(bitmap) = bitmap.into_option() {
if bitmap.detached.get().is_some() {
return Err(JsErrorBox::new(
"DOMExceptionInvalidStateError",
"The provided bitmap is detached.",
));
}
let _ = bitmap.detached.set(());
let new_data =
bitmap
.data
.replace(DynamicImage::new(0, 0, image::ColorType::Rgba8));
match &self.data {
ContextData::Canvas(image) => {
*image.borrow_mut() = new_data;
}
ContextData::Surface(surface_data) => {
let surface = surface_data.borrow().id;
let SurfaceBitmap {
instance,
device,
queue,
render_pipeline,
vertex_buffer,
index_buffer,
bind_group_layout,
sampler,
..
} = self.surface_only.as_ref().unwrap();
let (width, height) = new_data.dimensions();
let size = wgpu_types::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let texture_desc = wgpu_core::resource::TextureDescriptor {
label: None,
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu_types::TextureDimension::D2,
format: wgpu_types::TextureFormat::Rgba8UnormSrgb,
usage: wgpu_types::TextureUsages::TEXTURE_BINDING
| wgpu_types::TextureUsages::COPY_DST,
view_formats: vec![],
};
let (texture, err) =
instance.device_create_texture(*device, &texture_desc, None);
maybe_err_to_err(err)?;
instance
.queue_write_texture(
*queue,
&wgpu_types::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu_types::Origin3d::ZERO,
aspect: wgpu_types::TextureAspect::All,
},
&new_data.to_rgba8(),
&wgpu_types::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
&size,
)
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
let (view, err) = instance.texture_create_view(
texture,
&wgpu_core::resource::TextureViewDescriptor::default(),
None,
);
maybe_err_to_err(err)?;
let (bind_group, err) = instance.device_create_bind_group(
*device,
&wgpu_core::binding_model::BindGroupDescriptor {
label: None,
layout: *bind_group_layout,
entries:
vec![
wgpu_core::binding_model::BindGroupEntry {
binding: 0,
resource:
wgpu_core::binding_model::BindingResource::TextureView(
view,
),
},
wgpu_core::binding_model::BindGroupEntry {
binding: 1,
resource:
wgpu_core::binding_model::BindingResource::Sampler(
*sampler,
),
},
]
.into(),
},
None,
);
maybe_err_to_err(err)?;
let surface_output = instance
.surface_get_current_texture(surface, None)
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
let Some(frame) = surface_output.texture else {
return Ok(());
};
let (command_encoder, err) = instance.device_create_command_encoder(
*device,
&wgpu_types::CommandEncoderDescriptor { label: None },
None,
);
maybe_err_to_err(err)?;
let (swap_view_id, err) = instance.texture_create_view(
frame,
&wgpu_core::resource::TextureViewDescriptor::default(),
None,
);
maybe_err_to_err(err)?;
let desc_with_view = wgpu_core::command::RenderPassDescriptor {
label: None,
color_attachments: vec![Some(
wgpu_core::command::RenderPassColorAttachment {
view: swap_view_id,
depth_slice: None,
resolve_target: None,
load_op: wgpu_types::LoadOp::Clear(wgpu_types::Color::BLACK),
store_op: Default::default(),
},
)]
.into(),
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
};
{
let (mut pass, err) = instance.command_encoder_begin_render_pass(
command_encoder,
&desc_with_view,
);
maybe_err_to_err(err)?;
instance
.render_pass_set_pipeline(&mut pass, *render_pipeline)
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
instance
.render_pass_set_bind_group(&mut pass, 0, Some(bind_group), &[])
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
instance
.render_pass_set_vertex_buffer(
&mut pass,
0,
*vertex_buffer,
0,
std::num::NonZeroU64::new(size_of_val(VERTICES) as _),
)
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
instance
.render_pass_set_index_buffer(
&mut pass,
*index_buffer,
wgpu_types::IndexFormat::Uint16,
0,
std::num::NonZeroU64::new(size_of_val(INDICES) as _),
)
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
instance
.render_pass_draw_indexed(&mut pass, 6, 1, 0, 0, 0)
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
instance
.render_pass_end(&mut pass)
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
}
let (command_buffer, err) = instance.command_encoder_finish(
command_encoder,
&wgpu_types::CommandBufferDescriptor { label: None },
None,
);
if let Some((_, err)) = err {
maybe_err_to_err(Some(err))?;
}
instance
.queue_submit(*queue, &[command_buffer])
.map_err(|(_, e)| JsErrorBox::from_err(GPUError::from(e)))?;
instance.texture_view_drop(swap_view_id).unwrap();
instance.command_encoder_drop(command_encoder);
instance.bind_group_drop(bind_group);
instance.texture_view_drop(view).unwrap();
instance.texture_drop(texture);
}
}
} else {
match &self.data {
ContextData::Canvas(image) => {
image.replace_with(|image| {
let (width, height) = image.dimensions();
DynamicImage::new(width, height, image::ColorType::Rgba8)
});
}
ContextData::Surface(surface_data) => {
let surface = surface_data.borrow().id;
let SurfaceBitmap {
instance,
device,
queue,
..
} = self.surface_only.as_ref().unwrap();
let surface_output = instance
.surface_get_current_texture(surface, None)
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
let Some(frame) = surface_output.texture else {
return Ok(());
};
let (command_encoder, err) = instance.device_create_command_encoder(
*device,
&wgpu_types::CommandEncoderDescriptor { label: None },
None,
);
maybe_err_to_err(err)?;
let (swap_view_id, err) = instance.texture_create_view(
frame,
&wgpu_core::resource::TextureViewDescriptor::default(),
None,
);
maybe_err_to_err(err)?;
{
let (mut pass, err) = instance.command_encoder_begin_render_pass(
command_encoder,
&wgpu_core::command::RenderPassDescriptor {
label: None,
color_attachments: vec![Some(
wgpu_core::command::RenderPassColorAttachment {
view: swap_view_id,
depth_slice: None,
resolve_target: None,
load_op: wgpu_types::LoadOp::Clear(
wgpu_types::Color::BLACK,
),
store_op: Default::default(),
},
)]
.into(),
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
);
maybe_err_to_err(err)?;
instance
.render_pass_end(&mut pass)
.map_err(|e| JsErrorBox::from_err(GPUError::from(e)))?;
}
let (command_buffer, err) = instance.command_encoder_finish(
command_encoder,
&wgpu_types::CommandBufferDescriptor { label: None },
None,
);
if let Some((_, err)) = err {
maybe_err_to_err(Some(err))?;
}
instance
.queue_submit(*queue, &[command_buffer])
.map_err(|(_, e)| JsErrorBox::from_err(GPUError::from(e)))?;
instance.texture_view_drop(swap_view_id).unwrap();
instance.command_encoder_drop(command_encoder);
}
}
}
Ok(())
}
}
impl ImageBitmapRenderingContext {
pub fn resize(&self) -> Result<(), JsErrorBox> {
let SurfaceBitmap {
instance,
device,
surface_configuration,
..
} = self.surface_only.as_ref().unwrap();
let ContextData::Surface(surface_data) = &self.data else {
unreachable!()
};
let deno_webgpu::canvas::SurfaceData {
id, width, height, ..
} = &*surface_data.borrow();
let err = instance.surface_configure(
*id,
*device,
&wgpu_types::SurfaceConfiguration {
width: *width,
height: *height,
..surface_configuration.clone()
},
);
maybe_err_to_err(err)
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 3],
tex_coords: [f32; 2],
}
const VERTICES: &[Vertex] = &[
Vertex {
position: [-1.0, 1.0, 0.0],
tex_coords: [0.0, 0.0],
}, Vertex {
position: [-1.0, -1.0, 0.0],
tex_coords: [0.0, 1.0],
}, Vertex {
position: [1.0, -1.0, 0.0],
tex_coords: [1.0, 1.0],
}, Vertex {
position: [1.0, 1.0, 0.0],
tex_coords: [1.0, 0.0],
}, ];
const INDICES: &[u16] = &[0, 1, 2, 0, 2, 3];
pub const CONTEXT_ID: &str = "bitmaprenderer";
pub fn create<'s>(
instance: Option<deno_webgpu::Instance>,
canvas: v8::Global<v8::Object>,
data: ContextData,
scope: &mut v8::PinScope<'s, '_>,
options: v8::Local<'s, v8::Value>,
prefix: &'static str,
context: &'static str,
) -> Result<v8::Global<v8::Value>, JsErrorBox> {
let settings = ImageBitmapRenderingContextSettings::convert(
scope,
options,
prefix.into(),
(|| context.into()).into(),
&(),
)
.map_err(JsErrorBox::from_err)?;
let surface_only = if let ContextData::Surface(surface_data) = &data {
let deno_webgpu::canvas::SurfaceData {
id, width, height, ..
} = &*surface_data.borrow();
let instance = instance.unwrap();
let backends = std::env::var("DENO_WEBGPU_BACKEND").map_or_else(
|_| wgpu_types::Backends::all(),
|s| wgpu_types::Backends::from_comma_list(&s),
);
let adapter = instance
.request_adapter(
&wgpu_core::instance::RequestAdapterOptions {
power_preference: Default::default(),
force_fallback_adapter: false,
compatible_surface: Some(*id),
},
backends,
None,
)
.unwrap();
let (device, queue) = instance
.adapter_request_device(
adapter,
&wgpu_core::device::DeviceDescriptor {
label: None,
required_features: Default::default(),
required_limits: Default::default(),
experimental_features: Default::default(),
memory_hints: Default::default(),
trace: Default::default(),
},
None,
None,
)
.unwrap();
let caps = instance.surface_get_capabilities(*id, adapter).unwrap();
let format = caps.formats[0];
let config = wgpu_types::SurfaceConfiguration {
usage: wgpu_types::TextureUsages::RENDER_ATTACHMENT,
format,
width: *width,
height: *height,
present_mode: wgpu_types::PresentMode::Fifo,
desired_maximum_frame_latency: 0,
alpha_mode: wgpu_types::CompositeAlphaMode::Opaque,
view_formats: vec![],
};
let err = instance.surface_configure(*id, device, &config);
maybe_err_to_err(err)?;
let (sampler, err) = instance.device_create_sampler(
device,
&wgpu_core::resource::SamplerDescriptor {
label: None,
address_modes: [
wgpu_types::AddressMode::ClampToEdge,
wgpu_types::AddressMode::ClampToEdge,
wgpu_types::AddressMode::ClampToEdge,
],
mag_filter: wgpu_types::FilterMode::Nearest,
min_filter: wgpu_types::FilterMode::Nearest,
mipmap_filter: wgpu_types::MipmapFilterMode::Nearest,
lod_min_clamp: 0.0,
lod_max_clamp: 0.0,
compare: None,
anisotropy_clamp: 0,
border_color: None,
},
None,
);
maybe_err_to_err(err)?;
let (bind_group_layout, err) = instance.device_create_bind_group_layout(
device,
&wgpu_core::binding_model::BindGroupLayoutDescriptor {
label: None,
entries: vec![
wgpu_types::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu_types::ShaderStages::FRAGMENT,
ty: wgpu_types::BindingType::Texture {
multisampled: false,
view_dimension: wgpu_types::TextureViewDimension::D2,
sample_type: wgpu_types::TextureSampleType::Float {
filterable: true,
},
},
count: None,
},
wgpu_types::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu_types::ShaderStages::FRAGMENT,
ty: wgpu_types::BindingType::Sampler(
wgpu_types::SamplerBindingType::Filtering,
),
count: None,
},
]
.into(),
},
None,
);
maybe_err_to_err(err)?;
let (shader, err) = instance.device_create_shader_module(
device,
&wgpu_core::pipeline::ShaderModuleDescriptor {
label: None,
runtime_checks: Default::default(),
},
wgpu_core::pipeline::ShaderModuleSource::Wgsl(SHADER.into()),
None,
);
maybe_err_to_err(err)?;
let (render_pipeline_layout, err) = instance.device_create_pipeline_layout(
device,
&wgpu_core::binding_model::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: vec![bind_group_layout].into(),
immediate_size: 0,
},
None,
);
maybe_err_to_err(err)?;
let (render_pipeline, err) = instance.device_create_render_pipeline(
device,
&wgpu_core::pipeline::RenderPipelineDescriptor {
label: None,
layout: Some(render_pipeline_layout),
vertex: wgpu_core::pipeline::VertexState {
stage: wgpu_core::pipeline::ProgrammableStageDescriptor {
module: shader,
entry_point: Some("vs_main".into()),
constants: Default::default(),
zero_initialize_workgroup_memory: false,
},
buffers: vec![wgpu_core::pipeline::VertexBufferLayout {
array_stride: size_of::<Vertex>() as _,
step_mode: wgpu_types::VertexStepMode::Vertex,
attributes: vec![
wgpu_types::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu_types::VertexFormat::Float32x3,
},
wgpu_types::VertexAttribute {
offset: size_of::<[f32; 3]>() as _,
shader_location: 1,
format: wgpu_types::VertexFormat::Float32x2,
},
]
.into(),
}]
.into(),
},
fragment: Some(wgpu_core::pipeline::FragmentState {
stage: wgpu_core::pipeline::ProgrammableStageDescriptor {
module: shader,
entry_point: Some("fs_main".into()),
constants: Default::default(),
zero_initialize_workgroup_memory: false,
},
targets: vec![Some(wgpu_types::ColorTargetState {
format: config.format,
blend: Some(wgpu_types::BlendState::REPLACE),
write_mask: wgpu_types::ColorWrites::ALL,
})]
.into(),
}),
multiview_mask: None,
primitive: wgpu_types::PrimitiveState {
topology: wgpu_types::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu_types::FrontFace::Ccw,
cull_mode: Some(wgpu_types::Face::Back),
polygon_mode: wgpu_types::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu_types::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
cache: None,
},
None,
);
maybe_err_to_err(err)?;
let vertex_buffer_data = bytemuck::cast_slice(VERTICES);
let (vertex_buffer, err) = instance.device_create_buffer(
device,
&wgpu_core::resource::BufferDescriptor {
label: None,
usage: wgpu_types::BufferUsages::VERTEX
| wgpu_types::BufferUsages::COPY_DST,
size: {
let unpadded_size =
vertex_buffer_data.len() as wgpu_types::BufferAddress;
let align_mask = wgpu_types::COPY_BUFFER_ALIGNMENT - 1;
((unpadded_size + align_mask) & !align_mask)
.max(wgpu_types::COPY_BUFFER_ALIGNMENT)
},
mapped_at_creation: true,
},
None,
);
maybe_err_to_err(err)?;
let (range_ptr, len) = instance
.buffer_get_mapped_range(
vertex_buffer,
0,
Some(size_of_val(VERTICES) as _),
)
.unwrap();
unsafe {
let slice = std::slice::from_raw_parts_mut(range_ptr.as_ptr(), len as _);
slice.copy_from_slice(vertex_buffer_data);
}
instance.buffer_unmap(vertex_buffer).unwrap();
let index_buffer_data = bytemuck::cast_slice(INDICES);
let (index_buffer, err) = instance.device_create_buffer(
device,
&wgpu_core::resource::BufferDescriptor {
label: None,
usage: wgpu_types::BufferUsages::INDEX
| wgpu_types::BufferUsages::COPY_DST,
size: {
let unpadded_size =
index_buffer_data.len() as wgpu_types::BufferAddress;
let align_mask = wgpu_types::COPY_BUFFER_ALIGNMENT - 1;
((unpadded_size + align_mask) & !align_mask)
.max(wgpu_types::COPY_BUFFER_ALIGNMENT)
},
mapped_at_creation: true,
},
None,
);
maybe_err_to_err(err)?;
let (range_ptr, len) = instance
.buffer_get_mapped_range(index_buffer, 0, Some(size_of_val(INDICES) as _))
.unwrap();
unsafe {
let slice = std::slice::from_raw_parts_mut(range_ptr.as_ptr(), len as _);
slice.copy_from_slice(index_buffer_data);
}
instance.buffer_unmap(index_buffer).unwrap();
Some(SurfaceBitmap {
instance,
adapter,
device,
queue,
surface_configuration: config,
shader,
render_pipeline_layout,
render_pipeline,
vertex_buffer,
index_buffer,
bind_group_layout,
sampler,
})
} else {
None
};
let obj = deno_core::cppgc::make_cppgc_object(
scope,
ImageBitmapRenderingContext {
alpha: settings.alpha,
canvas,
data,
surface_only,
},
);
Ok(v8::Global::new(scope, obj.cast()))
}
#[derive(WebIDL)]
#[webidl(dictionary)]
struct ImageBitmapRenderingContextSettings {
#[webidl(default = true)]
alpha: bool,
}
const SHADER: &str = r#"
// Vertex Shader
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) tex_coords: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.tex_coords = model.tex_coords;
out.clip_position = vec4<f32>(model.position, 1.0);
return out;
}
// Fragment Shader
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}
"#;
fn maybe_err_to_err<E>(err: Option<E>) -> Result<(), JsErrorBox>
where
GPUError: From<E>,
{
if let Some(err) = err {
Err(JsErrorBox::from_err(GPUError::from(err)))
} else {
Ok(())
}
}