diff --git a/core/src/avm2/globals/flash/display/ShaderInput.as b/core/src/avm2/globals/flash/display/ShaderInput.as index baa454405..57346ad9d 100644 --- a/core/src/avm2/globals/flash/display/ShaderInput.as +++ b/core/src/avm2/globals/flash/display/ShaderInput.as @@ -14,6 +14,10 @@ package flash.display { return _height; } + public function set height(value:int):void { + _height = value; + } + public function get index():int { return _index; } @@ -22,6 +26,10 @@ package flash.display { return _width; } + public function set width(value:int):void { + _width = value; + } + public function get input():Object { return _object; } diff --git a/core/src/avm2/globals/flash/display/ShaderJob.as b/core/src/avm2/globals/flash/display/ShaderJob.as index 36513306a..3bd696d6e 100644 --- a/core/src/avm2/globals/flash/display/ShaderJob.as +++ b/core/src/avm2/globals/flash/display/ShaderJob.as @@ -9,10 +9,14 @@ package flash.display { private var _shader:Shader; private var _target:Object; + private var _width:int; + private var _height:int; public function ShaderJob(shader:Shader = null, target:Object = null, width:int = 0, height:int = 0) { this._shader = shader; this._target = target; + this._width = width; + this._height = height; stub_constructor("flash.display.ShaderJob"); } @@ -23,12 +27,19 @@ package flash.display { public native function start(waitForCompletion:Boolean = false):void; public function get height():int { - stub_getter("flash.display.ShaderJob", "height"); - return 0; + return this._height; } public function set height(value:int):void { - stub_setter("flash.display.ShaderJob", "height"); + this._height = value; + } + + public function get width():int { + return this._width; + } + + public function set width(value:int):void { + this._width = value; } public function get progress():Number { diff --git a/core/src/avm2/globals/flash/display/shader_job.rs b/core/src/avm2/globals/flash/display/shader_job.rs index e669523c2..25a763450 100644 --- a/core/src/avm2/globals/flash/display/shader_job.rs +++ b/core/src/avm2/globals/flash/display/shader_job.rs @@ -1,13 +1,17 @@ use ruffle_render::{ + backend::{PixelBenderOutput, PixelBenderTarget}, bitmap::PixelRegion, pixel_bender::{ - PixelBenderParam, PixelBenderParamQualifier, PixelBenderShaderArgument, + ImageInputTexture, PixelBenderParam, PixelBenderParamQualifier, PixelBenderShaderArgument, PixelBenderShaderHandle, PixelBenderType, OUT_COORD_NAME, }, }; use crate::{ - avm2::{string::AvmString, Activation, Error, Object, TObject, Value}, + avm2::{ + bytearray::Endian, parameters::ParametersExt, string::AvmString, Activation, Error, Object, + TObject, Value, + }, avm2_stub_method, pixel_bender::PixelBenderTypeExt, }; @@ -100,6 +104,25 @@ pub fn get_shader_args<'gc>( .get_public_property("input", activation) .expect("Missing input property"); + let width = shader_input + .get_public_property("width", activation) + .unwrap() + .as_u32(activation.context.gc_context) + .unwrap(); + let height = shader_input + .get_public_property("height", activation) + .unwrap() + .as_u32(activation.context.gc_context) + .unwrap(); + + let input_channels = shader_input + .get_public_property("channels", activation) + .unwrap() + .as_u32(activation.context.gc_context) + .unwrap(); + + assert_eq!(*channels as u32, input_channels); + let texture = if let Value::Null = input { None } else { @@ -107,21 +130,49 @@ pub fn get_shader_args<'gc>( .as_object() .expect("ShaderInput.input is not an object"); - let bitmap = input.as_bitmap_data().expect( - "ShaderInput.input is not a BitmapData (FIXME - support other types)", - ); - - Some(bitmap.bitmap_handle( - activation.context.gc_context, - activation.context.renderer, - )) + let input_texture = if let Some(bitmap) = input.as_bitmap_data() { + ImageInputTexture::Bitmap(bitmap.bitmap_handle( + activation.context.gc_context, + activation.context.renderer, + )) + } else if let Some(byte_array) = input.as_bytearray() { + let expected_len = (width * height * input_channels) as usize + * std::mem::size_of::(); + assert_eq!(byte_array.len(), expected_len); + assert_eq!(byte_array.endian(), Endian::Little); + ImageInputTexture::Bytes { + width, + height, + channels: input_channels, + bytes: byte_array.read_at(0, byte_array.len()).unwrap().to_vec(), + } + } else if let Some(vector) = input.as_vector_storage() { + let expected_len = (width * height * input_channels) as usize; + assert_eq!(vector.length(), expected_len); + ImageInputTexture::Bytes { + width, + height, + channels: input_channels, + bytes: vector + .iter() + .flat_map(|val| { + (val.as_number(activation.context.gc_context).unwrap() + as f32) + .to_le_bytes() + }) + .collect(), + } + } else { + panic!("Unexpected input object {input:?}"); + }; + Some(input_texture) }; Some(PixelBenderShaderArgument::ImageInput { index: *index, channels: *channels, name: name.clone(), - texture: texture.map(|t| t.into()), + texture, }) } } @@ -134,15 +185,17 @@ pub fn get_shader_args<'gc>( pub fn start<'gc>( activation: &mut Activation<'_, 'gc>, this: Object<'gc>, - _args: &[Value<'gc>], + args: &[Value<'gc>], ) -> Result, Error<'gc>> { - avm2_stub_method!( - activation, - "flash.display.ShaderJob", - "start", - "async execution and non-BitmapData inputs" - ); - + let wait_for_completion = args.get_bool(0); + if !wait_for_completion { + avm2_stub_method!( + activation, + "flash.display.ShaderJob", + "start", + "with waitForCompletion=false" + ); + } let shader = this .get_public_property("shader", activation)? .as_object() @@ -155,33 +208,69 @@ pub fn start<'gc>( .as_object() .expect("ShaderJob.target is not an object"); - let target_bitmap = target - .as_bitmap_data() - .expect("ShaderJob.target is not a BitmapData (FIXME - support other types)") - .sync(); + let output_width = this + .get_public_property("width", activation)? + .as_u32(activation.context.gc_context) + .expect("ShaderJob.width is not a number"); - // Perform both a GPU->CPU and CPU->GPU sync before writing to it. - // FIXME - are both necessary? - let mut target_bitmap_data = target_bitmap.write(activation.context.gc_context); - target_bitmap_data.update_dirty_texture(activation.context.renderer); + let output_height = this + .get_public_property("height", activation)? + .as_u32(activation.context.gc_context) + .expect("ShaderJob.height is not a number"); - let target_handle = target_bitmap_data - .bitmap_handle(activation.context.renderer) - .expect("Missing handle"); + let pixel_bender_target = if let Some(bitmap) = target.as_bitmap_data() { + let target_bitmap = bitmap.sync(); + // Perform both a GPU->CPU and CPU->GPU sync before writing to it. + // FIXME - are both necessary? + let mut target_bitmap_data = target_bitmap.write(activation.context.gc_context); + target_bitmap_data.update_dirty_texture(activation.context.renderer); - let sync_handle = activation + PixelBenderTarget::Bitmap( + target_bitmap_data + .bitmap_handle(activation.context.renderer) + .expect("Missing handle"), + ) + } else { + PixelBenderTarget::Bytes { + width: output_width, + height: output_height, + } + }; + + let output = activation .context .renderer - .run_pixelbender_shader(shader_handle, &arguments, target_handle) + .run_pixelbender_shader(shader_handle, &arguments, &pixel_bender_target) .expect("Failed to run shader"); - let width = target_bitmap_data.width(); - let height = target_bitmap_data.height(); - target_bitmap_data.set_gpu_dirty( - activation.context.gc_context, - sync_handle, - PixelRegion::for_whole_size(width, height), - ); + match output { + PixelBenderOutput::Bitmap(sync_handle) => { + let target_bitmap = target.as_bitmap_data().unwrap().sync(); + let mut target_bitmap_data = target_bitmap.write(activation.context.gc_context); + let width = target_bitmap_data.width(); + let height = target_bitmap_data.height(); + target_bitmap_data.set_gpu_dirty( + activation.context.gc_context, + sync_handle, + PixelRegion::for_whole_size(width, height), + ); + } + PixelBenderOutput::Bytes(pixels) => { + if let Some(mut bytearray) = target.as_bytearray_mut(activation.context.gc_context) { + bytearray.write_at(&pixels, 0).unwrap(); + } else if let Some(mut vector) = + target.as_vector_storage_mut(activation.context.gc_context) + { + let new_storage: Vec<_> = bytemuck::cast_slice::(&pixels) + .iter() + .map(|p| Value::from(*p as f64)) + .collect(); + vector.replace_storage(new_storage); + } else { + panic!("Unexpected target object {target:?}"); + } + } + } Ok(Value::Undefined) } diff --git a/render/canvas/src/lib.rs b/render/canvas/src/lib.rs index 2e43b38a1..3fd78235f 100644 --- a/render/canvas/src/lib.rs +++ b/render/canvas/src/lib.rs @@ -3,8 +3,8 @@ #![allow(clippy::arc_with_non_send_sync)] use ruffle_render::backend::{ - BitmapCacheEntry, Context3D, Context3DProfile, RenderBackend, ShapeHandle, ShapeHandleImpl, - ViewportDimensions, + BitmapCacheEntry, Context3D, Context3DProfile, PixelBenderOutput, PixelBenderTarget, + RenderBackend, ShapeHandle, ShapeHandleImpl, ViewportDimensions, }; use ruffle_render::bitmap::{ Bitmap, BitmapHandle, BitmapHandleImpl, BitmapSource, PixelRegion, PixelSnapping, SyncHandle, @@ -523,8 +523,8 @@ impl RenderBackend for WebCanvasRenderBackend { &mut self, _handle: ruffle_render::pixel_bender::PixelBenderShaderHandle, _arguments: &[ruffle_render::pixel_bender::PixelBenderShaderArgument], - _target: BitmapHandle, - ) -> Result, Error> { + _target: &PixelBenderTarget, + ) -> Result { Err(Error::Unimplemented("run_pixelbender_shader".into())) } diff --git a/render/src/backend.rs b/render/src/backend.rs index 6100f89a3..0e8ec89ec 100644 --- a/render/src/backend.rs +++ b/render/src/backend.rs @@ -104,11 +104,26 @@ pub trait RenderBackend: Downcast { &mut self, handle: PixelBenderShaderHandle, arguments: &[PixelBenderShaderArgument], - target: BitmapHandle, - ) -> Result, Error>; + target: &PixelBenderTarget, + ) -> Result; } impl_downcast!(RenderBackend); +pub enum PixelBenderTarget { + // The shader will write to the provided bitmap texture, + // producing a `PixelBenderOutput::Bitmap` with the corresponding + // `SyncHandle` + Bitmap(BitmapHandle), + // The shader will write to a temporary texture, which will then + // be immediately read back as bytes (in `PixelBenderOutput::Bytes`) + Bytes { width: u32, height: u32 }, +} + +pub enum PixelBenderOutput { + Bitmap(Box), + Bytes(Vec), +} + pub trait IndexBuffer: Downcast {} impl_downcast!(IndexBuffer); pub trait VertexBuffer: Downcast {} diff --git a/render/src/backend/null.rs b/render/src/backend/null.rs index 79440ed44..0e6de0b90 100644 --- a/render/src/backend/null.rs +++ b/render/src/backend/null.rs @@ -14,7 +14,7 @@ use crate::quality::StageQuality; use crate::shape_utils::DistilledShape; use swf::Color; -use super::{Context3D, Context3DProfile}; +use super::{Context3D, Context3DProfile, PixelBenderOutput, PixelBenderTarget}; pub struct NullBitmapSource; @@ -115,8 +115,8 @@ impl RenderBackend for NullRenderer { &mut self, _shader: PixelBenderShaderHandle, _arguments: &[PixelBenderShaderArgument], - _target: BitmapHandle, - ) -> Result, Error> { + _target: &PixelBenderTarget, + ) -> Result { Err(Error::Unimplemented("Pixel bender shader".into())) } diff --git a/render/src/pixel_bender.rs b/render/src/pixel_bender.rs index c415c4dfe..e86668379 100644 --- a/render/src/pixel_bender.rs +++ b/render/src/pixel_bender.rs @@ -280,6 +280,12 @@ pub enum PixelBenderShaderArgument<'a> { pub enum ImageInputTexture<'a> { Bitmap(BitmapHandle), TextureRef(&'a dyn RawTexture), + Bytes { + width: u32, + height: u32, + channels: u32, + bytes: Vec, + }, } impl PartialEq for ImageInputTexture<'_> { diff --git a/render/webgl/src/lib.rs b/render/webgl/src/lib.rs index d1e227f15..5e32e6321 100644 --- a/render/webgl/src/lib.rs +++ b/render/webgl/src/lib.rs @@ -4,8 +4,8 @@ use bytemuck::{Pod, Zeroable}; use ruffle_render::backend::{ - BitmapCacheEntry, Context3D, Context3DProfile, RenderBackend, ShapeHandle, ShapeHandleImpl, - ViewportDimensions, + BitmapCacheEntry, Context3D, Context3DProfile, PixelBenderOutput, PixelBenderTarget, + RenderBackend, ShapeHandle, ShapeHandleImpl, ViewportDimensions, }; use ruffle_render::bitmap::{ Bitmap, BitmapFormat, BitmapHandle, BitmapHandleImpl, BitmapSource, PixelRegion, PixelSnapping, @@ -1139,8 +1139,8 @@ impl RenderBackend for WebGlRenderBackend { &mut self, _handle: ruffle_render::pixel_bender::PixelBenderShaderHandle, _arguments: &[ruffle_render::pixel_bender::PixelBenderShaderArgument], - _target: BitmapHandle, - ) -> Result, BitmapError> { + _target: &PixelBenderTarget, + ) -> Result { Err(BitmapError::Unimplemented("run_pixelbender_shader".into())) } diff --git a/render/wgpu/src/backend.rs b/render/wgpu/src/backend.rs index 220f676dc..c183cce7e 100644 --- a/render/wgpu/src/backend.rs +++ b/render/wgpu/src/backend.rs @@ -14,7 +14,9 @@ use crate::{ QueueSyncHandle, RenderTarget, SwapChainTarget, Texture, Transforms, }; use image::imageops::FilterType; -use ruffle_render::backend::{BitmapCacheEntry, Context3D, Context3DProfile}; +use ruffle_render::backend::{ + BitmapCacheEntry, Context3D, Context3DProfile, PixelBenderOutput, PixelBenderTarget, +}; use ruffle_render::backend::{RenderBackend, ShapeHandle, ViewportDimensions}; use ruffle_render::bitmap::{ Bitmap, BitmapFormat, BitmapHandle, BitmapSource, PixelRegion, SyncHandle, @@ -23,7 +25,8 @@ use ruffle_render::commands::CommandList; use ruffle_render::error::Error as BitmapError; use ruffle_render::filters::Filter; use ruffle_render::pixel_bender::{ - PixelBenderShader, PixelBenderShaderArgument, PixelBenderShaderHandle, + PixelBenderParam, PixelBenderParamQualifier, PixelBenderShader, PixelBenderShaderArgument, + PixelBenderShaderHandle, }; use ruffle_render::quality::StageQuality; use ruffle_render::shape_utils::DistilledShape; @@ -374,6 +377,7 @@ impl WgpuRenderBackend { let copy_dimensions = BufferDimensions::new( texture.texture.width() as usize, texture.texture.height() as usize, + texture.texture.format(), ); let buffer = self .offscreen_buffer_pool @@ -948,25 +952,87 @@ impl RenderBackend for WgpuRenderBackend { &mut self, shader: PixelBenderShaderHandle, arguments: &[PixelBenderShaderArgument], - target_handle: BitmapHandle, - ) -> Result, BitmapError> { - let target = as_texture(&target_handle); + target: &PixelBenderTarget, + ) -> Result { + let mut output_channels = None; + + for param in &shader.0.parsed_shader().params { + if let PixelBenderParam::Normal { + qualifier: PixelBenderParamQualifier::Output, + reg, + .. + } = param + { + if output_channels.is_some() { + panic!("Multiple output parameters"); + } + output_channels = Some(reg.channels.len()); + break; + } + } + + let output_channels = output_channels.expect("No output parameter"); + let has_padding = output_channels == 3; + + let texture_format = + crate::pixel_bender::temporary_texture_format_for_channels(output_channels as u32); + + let target_handle = match target { + PixelBenderTarget::Bitmap(handle) => handle.clone(), + PixelBenderTarget::Bytes { width, height } => { + let extent = wgpu::Extent3d { + width: *width, + height: *height, + depth_or_array_layers: 1, + }; + // FIXME - cache this texture somehow. We might also want to consider using + // a compute shader + let texture_label = create_debug_label!("Temporary pixelbender output texture"); + let texture = self + .descriptors + .device + .create_texture(&wgpu::TextureDescriptor { + label: texture_label.as_deref(), + size: extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: texture_format, + view_formats: &[texture_format], + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC, + }); + BitmapHandle(Arc::new(Texture { + texture: Arc::new(texture), + bind_linear: Default::default(), + bind_nearest: Default::default(), + copy_count: Cell::new(0), + })) + } + }; + + let target_texture = as_texture(&target_handle); let extent = wgpu::Extent3d { - width: target.texture.width(), - height: target.texture.height(), + width: target_texture.texture.width(), + height: target_texture.texture.height(), depth_or_array_layers: 1, }; let buffer_info = self.get_texture_buffer_info( - target, - PixelRegion::for_whole_size(target.texture.width(), target.texture.height()), + target_texture, + PixelRegion::for_whole_size( + target_texture.texture.width(), + target_texture.texture.height(), + ), ); let mut texture_target = TextureTarget { size: extent, - texture: target.texture.clone(), - format: wgpu::TextureFormat::Rgba8Unorm, + texture: target_texture.texture.clone(), + format: target_texture.texture.format(), buffer: buffer_info, }; @@ -986,7 +1052,7 @@ impl RenderBackend for WgpuRenderBackend { shader, ShaderMode::ShaderJob, arguments, - &target.texture, + &target_texture.texture, &mut render_command_encoder, Some(wgpu::RenderPassColorAttachment { view: frame_output.view(), @@ -998,7 +1064,7 @@ impl RenderBackend for WgpuRenderBackend { }), 1, // When running a standalone shader, we always process the entire image - &FilterSource::for_entire_texture(&target.texture), + &FilterSource::for_entire_texture(&target_texture.texture), )?; let index = self @@ -1006,12 +1072,53 @@ impl RenderBackend for WgpuRenderBackend { .queue .submit(Some(render_command_encoder.finish())); - Ok(self.make_queue_sync_handle( + let sync_handle = self.make_queue_sync_handle( texture_target, index, target_handle, PixelRegion::for_whole_size(extent.width, extent.height), - )) + ); + + match target { + PixelBenderTarget::Bitmap(_) => Ok(PixelBenderOutput::Bitmap(sync_handle)), + PixelBenderTarget::Bytes { width, .. } => { + let mut output = None; + sync_handle.retrieve_offscreen_texture(Box::new(|raw_pixels, buffer_width| { + if buffer_width as usize + != *width as usize * output_channels * std::mem::size_of::() + { + let channels_in_raw_pixels = if has_padding { 4usize } else { 3usize }; + + let mut new_pixels = Vec::new(); + for row in raw_pixels.chunks(buffer_width as usize) { + // Ignore any wgpu-added padding (this is distinct from the alpha-channel padding + // that we add for pixelbender) + let actual_row = &row[0..(*width as usize + * channels_in_raw_pixels + * std::mem::size_of::())]; + + for pixel in actual_row + .chunks_exact(channels_in_raw_pixels * std::mem::size_of::()) + { + if has_padding { + // Take the first three channels + new_pixels.extend_from_slice( + &pixel[0..(3 * std::mem::size_of::())], + ); + } else { + // Copy the pixel as-is + new_pixels.extend_from_slice(pixel); + } + } + } + output = Some(new_pixels); + } else { + output = Some(raw_pixels.to_vec()); + }; + }))?; + Ok(PixelBenderOutput::Bytes(output.unwrap())) + } + } } fn create_empty_texture( @@ -1113,6 +1220,7 @@ async fn request_device( wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, wgpu::Features::SHADER_UNUSED_VERTEX_OUTPUT, wgpu::Features::TEXTURE_COMPRESSION_BC, + wgpu::Features::FLOAT32_FILTERABLE, ]; for feature in try_features { diff --git a/render/wgpu/src/lib.rs b/render/wgpu/src/lib.rs index 4af2baf2f..e94ef6e50 100644 --- a/render/wgpu/src/lib.rs +++ b/render/wgpu/src/lib.rs @@ -222,8 +222,12 @@ impl QueueSyncHandle { } => { let texture = as_texture(&handle); - let buffer_dimensions = - BufferDimensions::new(copy_area.width() as usize, copy_area.height() as usize); + let buffer_dimensions = BufferDimensions::new( + copy_area.width() as usize, + copy_area.height() as usize, + texture.texture.format(), + ); + let buffer = pool.take(&descriptors, buffer_dimensions.clone()); let label = create_debug_label!("Render target transfer encoder"); let mut encoder = diff --git a/render/wgpu/src/pixel_bender.rs b/render/wgpu/src/pixel_bender.rs index c9b298793..2269866b6 100644 --- a/render/wgpu/src/pixel_bender.rs +++ b/render/wgpu/src/pixel_bender.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; +use std::collections::HashMap; use std::num::NonZeroU64; -use std::sync::OnceLock; use std::{borrow::Cow, cell::Cell, sync::Arc}; use indexmap::IndexMap; @@ -23,7 +23,6 @@ use wgpu::{ use crate::filters::{FilterSource, VERTEX_BUFFERS_DESCRIPTION_FILTERS}; use crate::raw_texture_as_texture; -use crate::utils::SampleCountMap; use crate::{ as_texture, backend::WgpuRenderBackend, descriptors::Descriptors, target::RenderTarget, Texture, }; @@ -32,7 +31,7 @@ use crate::{ pub struct PixelBenderWgpuShader { bind_group_layout: wgpu::BindGroupLayout, pipeline_layout: PipelineLayout, - pipelines: SampleCountMap>, + pipelines: RefCell>>, vertex_shader: wgpu::ShaderModule, fragment_shader: wgpu::ShaderModule, shader: PixelBenderShader, @@ -46,41 +45,52 @@ pub struct PixelBenderWgpuShader { impl PixelBenderWgpuShader { /// Gets a `RenderPipeline` for the specified sample count - fn get_pipeline(&self, descriptors: &Descriptors, samples: u32) -> &wgpu::RenderPipeline { - self.pipelines.get_or_init(samples, || { - descriptors - .device - .create_render_pipeline(&RenderPipelineDescriptor { - label: create_debug_label!("PixelBender shader pipeline").as_deref(), - layout: Some(&self.pipeline_layout), - vertex: VertexState { - module: &self.vertex_shader, - entry_point: naga_pixelbender::VERTEX_SHADER_ENTRYPOINT, - buffers: &VERTEX_BUFFERS_DESCRIPTION_FILTERS, - }, - fragment: Some(wgpu::FragmentState { - module: &self.fragment_shader, - entry_point: naga_pixelbender::FRAGMENT_SHADER_ENTRYPOINT, - targets: &[Some(ColorTargetState { - format: TextureFormat::Rgba8Unorm, - // FIXME - what should this be? - blend: Some(wgpu::BlendState { - color: BlendComponent::OVER, - alpha: BlendComponent::OVER, + fn get_pipeline( + &self, + descriptors: &Descriptors, + samples: u32, + format: TextureFormat, + ) -> Arc { + self.pipelines + .borrow_mut() + .entry((samples, format)) + .or_insert_with(|| { + Arc::new( + descriptors + .device + .create_render_pipeline(&RenderPipelineDescriptor { + label: create_debug_label!("PixelBender shader pipeline").as_deref(), + layout: Some(&self.pipeline_layout), + vertex: VertexState { + module: &self.vertex_shader, + entry_point: naga_pixelbender::VERTEX_SHADER_ENTRYPOINT, + buffers: &VERTEX_BUFFERS_DESCRIPTION_FILTERS, + }, + fragment: Some(wgpu::FragmentState { + module: &self.fragment_shader, + entry_point: naga_pixelbender::FRAGMENT_SHADER_ENTRYPOINT, + targets: &[Some(ColorTargetState { + format, + // FIXME - what should this be? + blend: Some(wgpu::BlendState { + color: BlendComponent::OVER, + alpha: BlendComponent::OVER, + }), + write_mask: ColorWrites::all(), + })], }), - write_mask: ColorWrites::all(), - })], - }), - primitive: Default::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: samples, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: Default::default(), - }) - }) + primitive: Default::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: samples, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: Default::default(), + }), + ) + }) + .clone() } } @@ -250,10 +260,104 @@ impl PixelBenderWgpuShader { } } -fn image_input_as_texture<'a>(input: &'a ImageInputTexture<'a>) -> &wgpu::Texture { +enum BorrowedOrOwnedTexture<'a> { + Borrowed(&'a wgpu::Texture), + Owned(wgpu::Texture), +} + +impl<'a> std::ops::Deref for BorrowedOrOwnedTexture<'a> { + type Target = wgpu::Texture; + + fn deref(&self) -> &Self::Target { + match self { + BorrowedOrOwnedTexture::Borrowed(t) => t, + BorrowedOrOwnedTexture::Owned(t) => t, + } + } +} + +/// The texture format to use for the temporary texture we create when reading/writing +/// from raw bytes (ByteArray to Vector.). We use a Float texture to be able to +/// pass in floating-point values directly, without converting on the host side. +/// In the special case with 3 channels, we use `Rgba32Float` since wgpu lacks a `Rgb32Float` +/// texture. We handle this by manually inserting and removing padding to keep the pixels +/// at the correct positions. This isn't ideal, but allows us to keep the naga code generation +/// simple. +pub(super) fn temporary_texture_format_for_channels(channels: u32) -> wgpu::TextureFormat { + match channels { + 1 => wgpu::TextureFormat::R32Float, + 2 => wgpu::TextureFormat::Rg32Float, + 3 => wgpu::TextureFormat::Rgba32Float, + 4 => wgpu::TextureFormat::Rgba32Float, + _ => panic!("Unsupported number of channels: {}", channels), + } +} + +fn image_input_as_texture<'a>( + descriptors: &Descriptors, + input: &'a ImageInputTexture<'a>, +) -> BorrowedOrOwnedTexture<'a> { match input { - ImageInputTexture::Bitmap(handle) => &as_texture(handle).texture, - ImageInputTexture::TextureRef(raw_texture) => raw_texture_as_texture(*raw_texture), + ImageInputTexture::Bitmap(handle) => { + BorrowedOrOwnedTexture::Borrowed(&as_texture(handle).texture) + } + ImageInputTexture::TextureRef(raw_texture) => { + BorrowedOrOwnedTexture::Borrowed(raw_texture_as_texture(*raw_texture)) + } + ImageInputTexture::Bytes { + width, + height, + channels, + bytes, + } => { + let extent = wgpu::Extent3d { + width: *width, + height: *height, + depth_or_array_layers: 1, + }; + let texture_format = temporary_texture_format_for_channels(*channels); + // We're going to be using an Rgba32Float texture, so we need to pad the bytes + // with zeros for the alpha channel. The PixelBender code will only ever try to + // use the first 3 channels (since it was compiled with a 3-channel input), + // so it doesn't matter what value we choose here. + let padded_bytes = if *channels == 3 { + let mut padded_bytes = Vec::with_capacity(bytes.len() * 4 / 3); + for chunk in bytes.chunks_exact(12) { + padded_bytes.extend_from_slice(chunk); + padded_bytes.extend_from_slice(&[0, 0, 0, 0]); + } + Cow::Owned(padded_bytes) + } else { + Cow::Borrowed(bytes) + }; + + let fresh_texture = descriptors.device.create_texture(&TextureDescriptor { + label: Some("Temporary PixelBender output texture"), + size: extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: texture_format, + usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[texture_format], + }); + descriptors.queue.write_texture( + wgpu::ImageCopyTexture { + texture: &fresh_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &padded_bytes, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(padded_bytes.len() as u32 / height), + rows_per_image: None, + }, + extent, + ); + BorrowedOrOwnedTexture::Owned(fresh_texture) + } } } @@ -362,8 +466,15 @@ pub(super) fn run_pixelbender_shader_impl( for input in &mut arguments { match input { PixelBenderShaderArgument::ImageInput { index, texture, .. } => { - let input_texture = &image_input_as_texture(texture.as_ref().unwrap()); - if std::ptr::eq(*input_texture, target) { + let input_texture = &image_input_as_texture(descriptors, texture.as_ref().unwrap()); + let same_source_dest = + if let BorrowedOrOwnedTexture::Borrowed(input_texture) = input_texture { + std::ptr::eq(*input_texture, target) + } else { + // When we create a fresh texture, it can never be equal to the pre-existing target + false + }; + if same_source_dest { // The input is the same as the output - we need to clone the input. // We will write to the original output, and use a clone of the input as a texture input binding let cached_fresh_handle = target_clone.get_or_insert_with(|| { @@ -408,7 +519,7 @@ pub(super) fn run_pixelbender_shader_impl( }); *texture = Some(cached_fresh_handle.clone().into()); } - let wgpu_texture = image_input_as_texture(texture.as_ref().unwrap()); + let wgpu_texture = image_input_as_texture(descriptors, texture.as_ref().unwrap()); texture_views.insert( *index, wgpu_texture.create_view(&wgpu::TextureViewDescriptor::default()), @@ -520,7 +631,7 @@ pub(super) fn run_pixelbender_shader_impl( for input in &arguments { match input { PixelBenderShaderArgument::ImageInput { index, texture, .. } => { - let wgpu_texture = image_input_as_texture(texture.as_ref().unwrap()); + let wgpu_texture = image_input_as_texture(descriptors, texture.as_ref().unwrap()); if first_image.is_none() { first_image = Some(wgpu_texture); @@ -548,7 +659,7 @@ pub(super) fn run_pixelbender_shader_impl( let vertices = source.vertices(&descriptors.device); - let pipeline = compiled_shader.get_pipeline(descriptors, sample_count); + let pipeline = compiled_shader.get_pipeline(descriptors, sample_count, target.format()); let mut render_pass = render_command_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("PixelBender render pass"), @@ -557,7 +668,7 @@ pub(super) fn run_pixelbender_shader_impl( ..Default::default() }); render_pass.set_bind_group(0, &bind_group, &[]); - render_pass.set_pipeline(pipeline); + render_pass.set_pipeline(&pipeline); render_pass.set_vertex_buffer(0, vertices.slice(..)); render_pass.set_index_buffer( diff --git a/render/wgpu/src/target.rs b/render/wgpu/src/target.rs index f7115e2a9..08b83508d 100644 --- a/render/wgpu/src/target.rs +++ b/render/wgpu/src/target.rs @@ -199,14 +199,14 @@ impl TextureTarget { ) .into()); } - let buffer_dimensions = BufferDimensions::new(size.0 as usize, size.1 as usize); + let format = wgpu::TextureFormat::Rgba8Unorm; + let buffer_dimensions = BufferDimensions::new(size.0 as usize, size.1 as usize, format); let size = wgpu::Extent3d { width: size.0, height: size.1, depth_or_array_layers: 1, }; let texture_label = create_debug_label!("Render target texture"); - let format = wgpu::TextureFormat::Rgba8Unorm; let texture = device.create_texture(&wgpu::TextureDescriptor { label: texture_label.as_deref(), size, diff --git a/render/wgpu/src/utils.rs b/render/wgpu/src/utils.rs index 0ec7b7f1d..e870b5deb 100644 --- a/render/wgpu/src/utils.rs +++ b/render/wgpu/src/utils.rs @@ -3,9 +3,8 @@ use crate::descriptors::Descriptors; use crate::globals::Globals; use crate::Transforms; use std::borrow::Cow; -use std::mem::size_of; use wgpu::util::DeviceExt; -use wgpu::CommandEncoder; +use wgpu::{CommandEncoder, TextureFormat}; macro_rules! create_debug_label { ($($arg:tt)*) => ( @@ -99,8 +98,8 @@ pub struct BufferDimensions { impl BufferDimensions { #[allow(dead_code)] - pub fn new(width: usize, height: usize) -> Self { - let bytes_per_pixel = size_of::(); + pub fn new(width: usize, height: usize, format: TextureFormat) -> Self { + let bytes_per_pixel = format.block_copy_size(None).unwrap() as usize; let unpadded_bytes_per_row = width * bytes_per_pixel; let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align; diff --git a/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/input.json b/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/input.json new file mode 100644 index 000000000..443fdafbc --- /dev/null +++ b/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/input.json @@ -0,0 +1,47 @@ +[ + { + "type": "Wait" + }, + { + "type": "Wait" + }, + { + "type": "Wait" + }, + { + "type": "Wait" + }, + { + "type": "Wait" + }, + { + "type": "Wait" + }, + { + "type": "Wait" + }, + { + "type": "Wait" + }, + { + "type": "Wait" + }, + { + "type": "Wait" + }, + { + "type": "MouseMove", + "pos": [ + 450.0, + 450.0 + ] + }, + { + "type": "MouseDown", + "pos": [ + 450.0, + 450.0 + ], + "btn": "Left" + } +] \ No newline at end of file diff --git a/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/output.expected.png b/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/output.expected.png new file mode 100644 index 000000000..9ca0e0605 Binary files /dev/null and b/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/output.expected.png differ diff --git a/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/output.txt b/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/output.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/test.swf b/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/test.swf new file mode 100644 index 000000000..498a7032a Binary files /dev/null and b/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/test.swf differ diff --git a/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/test.toml b/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/test.toml new file mode 100644 index 000000000..3c99103c3 --- /dev/null +++ b/tests/tests/swfs/avm2/away3d_advanced_shallow_water_demo/test.toml @@ -0,0 +1,8 @@ +num_ticks = 20 + +[image_comparisons.output] +tolerance = 5 +max_outliers = 389 + +[player_options] +with_renderer = { optional = false, sample_count = 1 } \ No newline at end of file