diff --git a/Cargo.lock b/Cargo.lock index 50a543eea..1e083f077 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2844,6 +2844,7 @@ dependencies = [ "anyhow", "bitflags 2.3.2", "naga", + "naga_oil", "ruffle_render", ] @@ -3855,6 +3856,7 @@ dependencies = [ "thiserror", "tracing", "wasm-bindgen", + "wgpu", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index efa627001..e5295a67c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ gc-arena = { git = "https://github.com/kyren/gc-arena", rev = "63dab12871321e0e5 tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } naga = { version = "0.12.2", features = ["validate", "wgsl-out"] } +naga_oil = "0.7.0" +wgpu = { version = "0.16.1" } # Don't optimize build scripts and macros. [profile.release.build-override] diff --git a/core/src/avm2/filters.rs b/core/src/avm2/filters.rs index 747c7ea40..d166bdf5b 100644 --- a/core/src/avm2/filters.rs +++ b/core/src/avm2/filters.rs @@ -1,4 +1,8 @@ -use ruffle_render::filters::{DisplacementMapFilter, DisplacementMapFilterMode, Filter}; +use gc_arena::{Collect, DynamicRoot, Rootable}; +use ruffle_render::filters::{ + DisplacementMapFilter, DisplacementMapFilterMode, Filter, ShaderFilter, ShaderObject, +}; +use std::fmt::Debug; use swf::{ BevelFilter, BevelFilterFlags, BlurFilter, BlurFilterFlags, Color, ColorMatrixFilter, ConvolutionFilter, ConvolutionFilterFlags, DropShadowFilter, DropShadowFilterFlags, Fixed16, @@ -8,6 +12,8 @@ use swf::{ use crate::avm2::error::{make_error_2008, type_error}; use crate::avm2::{Activation, ArrayObject, ClassObject, Error, Object, TObject, Value}; +use super::globals::flash::display::shader_job::get_shader_args; + pub trait FilterAvm2Ext { fn from_avm2_object<'gc>( activation: &mut Activation<'_, 'gc>, @@ -20,6 +26,26 @@ pub trait FilterAvm2Ext { ) -> Result, Error<'gc>>; } +#[derive(Clone, Collect)] +#[collect(require_static)] +pub struct ObjectWrapper { + root: DynamicRoot]>, +} + +impl ShaderObject for ObjectWrapper { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Debug for ObjectWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ObjectWrapper") + .field("root", &self.root.as_ptr()) + .finish() + } +} + impl FilterAvm2Ext for Filter { fn from_avm2_object<'gc>( activation: &mut Activation<'_, 'gc>, @@ -74,6 +100,13 @@ impl FilterAvm2Ext for Filter { )?)); } + let shader_filter = activation.avm2().classes().shaderfilter; + if object.is_of_type(shader_filter, &mut activation.context) { + return Ok(Filter::ShaderFilter(avm2_to_shader_filter( + activation, object, + )?)); + } + Err(Error::AvmError(type_error( activation, &format!( @@ -105,6 +138,7 @@ impl FilterAvm2Ext for Filter { let gradientglowfilter = activation.avm2().classes().gradientglowfilter; gradient_filter_to_avm2(activation, filter, gradientglowfilter) } + Filter::ShaderFilter(filter) => shader_filter_to_avm2(activation, filter), } } } @@ -702,6 +736,66 @@ fn gradient_filter_to_avm2<'gc>( ) } +fn avm2_to_shader_filter<'gc>( + activation: &mut Activation<'_, 'gc>, + object: Object<'gc>, +) -> Result, Error<'gc>> { + let bottom_extension = object + .get_public_property("bottomExtension", activation)? + .coerce_to_i32(activation)?; + + let left_extension = object + .get_public_property("leftExtension", activation)? + .coerce_to_i32(activation)?; + + let right_extension = object + .get_public_property("rightExtension", activation)? + .coerce_to_i32(activation)?; + + let top_extension = object + .get_public_property("topExtension", activation)? + .coerce_to_i32(activation)?; + + let shader_obj = object + .get_public_property("shader", activation)? + .as_object() + .unwrap(); + + let dyn_root = activation + .context + .dynamic_root + .stash(activation.context.gc_context, shader_obj); + + let (shader_handle, shader_args) = get_shader_args(shader_obj, activation)?; + + Ok(ShaderFilter { + shader_object: Box::new(ObjectWrapper { root: dyn_root }), + shader: shader_handle, + shader_args, + bottom_extension, + left_extension, + right_extension, + top_extension, + }) +} + +fn shader_filter_to_avm2<'gc>( + activation: &mut Activation<'_, 'gc>, + filter: &ShaderFilter<'static>, +) -> Result, Error<'gc>> { + let object_wrapper: &ObjectWrapper = filter + .shader_object + .downcast_ref::() + .expect("ShaderObject was not an ObjectWrapper"); + + let obj = *activation.context.dynamic_root.fetch(&object_wrapper.root); + activation + .avm2() + .classes() + .shaderfilter + .construct(activation, &[obj.into()]) +} + fn get_gradient_colors<'gc>( activation: &mut Activation<'_, 'gc>, object: Object<'gc>, diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index 6b2c8a1d3..eb9dd0ac6 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -139,9 +139,11 @@ pub struct SystemClasses<'gc> { pub cubetexture: ClassObject<'gc>, pub rectangletexture: ClassObject<'gc>, pub morphshape: ClassObject<'gc>, + pub shader: ClassObject<'gc>, pub shaderinput: ClassObject<'gc>, pub shaderparameter: ClassObject<'gc>, pub netstatusevent: ClassObject<'gc>, + pub shaderfilter: ClassObject<'gc>, } impl<'gc> SystemClasses<'gc> { @@ -255,9 +257,11 @@ impl<'gc> SystemClasses<'gc> { cubetexture: object, rectangletexture: object, morphshape: object, + shader: object, shaderinput: object, shaderparameter: object, netstatusevent: object, + shaderfilter: object, } } } @@ -742,6 +746,7 @@ fn load_playerglobal<'gc>( ("flash.filters", "GlowFilter", glowfilter), ("flash.filters", "GradientBevelFilter", gradientbevelfilter), ("flash.filters", "GradientGlowFilter", gradientglowfilter), + ("flash.filters", "ShaderFilter", shaderfilter), ] ); diff --git a/core/src/avm2/globals/flash/display/BitmapData.as b/core/src/avm2/globals/flash/display/BitmapData.as index e6bea37a3..26bfe17dc 100644 --- a/core/src/avm2/globals/flash/display/BitmapData.as +++ b/core/src/avm2/globals/flash/display/BitmapData.as @@ -4,6 +4,7 @@ package flash.display { import flash.geom.Point; import flash.geom.Matrix; import flash.filters.BitmapFilter; + import flash.filters.ShaderFilter; import flash.utils.ByteArray; import __ruffle__.stub_method; @@ -68,6 +69,10 @@ package flash.display { ):int; public function generateFilterRect(sourceRect:Rectangle, filter:BitmapFilter):Rectangle { + // Flash always reports that a ShaderFilter affects the entire BitampData, ignoring SourceRect. + if (filter is ShaderFilter) { + return this.rect.clone(); + } stub_method("flash.display.BitmapData", "generateFilterRect"); return sourceRect.clone(); } diff --git a/core/src/avm2/globals/flash/display/bitmap_data.rs b/core/src/avm2/globals/flash/display/bitmap_data.rs index 8ba89bd0b..863ba5062 100644 --- a/core/src/avm2/globals/flash/display/bitmap_data.rs +++ b/core/src/avm2/globals/flash/display/bitmap_data.rs @@ -9,6 +9,7 @@ use crate::avm2::parameters::{null_parameter_error, ParametersExt}; use crate::avm2::value::Value; use crate::avm2::vector::VectorStorage; use crate::avm2::Error; +use crate::avm2_stub_method; use crate::bitmap::bitmap_data::{ BitmapData, BitmapDataWrapper, ChannelOptions, ThresholdOperation, }; @@ -23,6 +24,7 @@ use gc_arena::GcCell; use ruffle_render::filters::Filter; use ruffle_render::transform::Transform; use std::str::FromStr; +use swf::{Rectangle, Twips}; // Computes the integer x,y,width,height values from // the given `Rectangle`. This method performs `x + width` @@ -1051,7 +1053,40 @@ pub fn apply_filter<'gc>( Error::from(format!("TypeError: Error #1034: Type Coercion failed: cannot convert {} to flash.display.BitmapData.", args[0].coerce_to_string(activation).unwrap_or_default())) })?; let source_rect = args.get_object(activation, 1, "sourceRect")?; - let source_rect = super::display_object::object_to_rectangle(activation, source_rect)?; + let mut source_rect = super::display_object::object_to_rectangle(activation, source_rect)?; + let filter = args.get_object(activation, 3, "filter")?; + let filter = Filter::from_avm2_object(activation, filter)?; + + if matches!(filter, Filter::ShaderFilter(_)) { + let source_bitmap_rect = Rectangle { + x_min: Twips::ZERO, + x_max: Twips::from_pixels(source_bitmap.width() as f64), + y_min: Twips::ZERO, + y_max: Twips::from_pixels(source_bitmap.height() as f64), + }; + // Flash performs an odd translation/cropping behavior when sourceRect + // has a non-zero x or y starting value, which I haven't yet managed to reproduce. + // + // Additionally, when both x and y are 0, the 'width' and 'height' seem to + // be ignored completely in favor of the using the dimensions of the source + // image (even if a larger or smaller rect is passed in) + // + // To make matters worse, the behavior of ShaderFilter seems platform-dependent + // (or at least resolution-dependent). The test + // 'tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.swf' + // renders slightly differently in Linux vs a Windows VM (part of the mandelbrot fractal + // in the top image is cut off in the Windows Flash Player, but not in the Linux Flash Player) + if source_rect != source_bitmap_rect { + avm2_stub_method!( + activation, + "flash.display.BitmapData", + "applyFilter", + "ShaderFilter with non-standard sourceRect" + ); + source_rect = source_bitmap_rect; + } + } + let source_point = ( source_rect.x_min.to_pixels().floor() as u32, source_rect.y_min.to_pixels().floor() as u32, @@ -1069,8 +1104,7 @@ pub fn apply_filter<'gc>( .get_public_property("y", activation)? .coerce_to_u32(activation)?, ); - let filter = args.get_object(activation, 3, "filter")?; - let filter = Filter::from_avm2_object(activation, filter)?; + operations::apply_filter( &mut activation.context, dest_bitmap, @@ -1079,7 +1113,7 @@ pub fn apply_filter<'gc>( source_size, dest_point, filter, - ) + ); } Ok(Value::Undefined) } diff --git a/core/src/avm2/globals/flash/display/shader_job.rs b/core/src/avm2/globals/flash/display/shader_job.rs index e4be03053..7de8befe4 100644 --- a/core/src/avm2/globals/flash/display/shader_job.rs +++ b/core/src/avm2/globals/flash/display/shader_job.rs @@ -1,8 +1,8 @@ use ruffle_render::{ bitmap::PixelRegion, pixel_bender::{ - PixelBenderParam, PixelBenderParamQualifier, PixelBenderShaderArgument, PixelBenderType, - OUT_COORD_NAME, + PixelBenderParam, PixelBenderParamQualifier, PixelBenderShaderArgument, + PixelBenderShaderHandle, PixelBenderType, OUT_COORD_NAME, }, }; @@ -12,28 +12,19 @@ use crate::{ pixel_bender::PixelBenderTypeExt, }; -/// Implements `ShaderJob.start`. -pub fn start<'gc>( +pub fn get_shader_args<'gc>( + shader_obj: Object<'gc>, activation: &mut Activation<'_, 'gc>, - this: Option>, - _args: &[Value<'gc>], -) -> Result, Error<'gc>> { - let this = this.unwrap(); - - avm2_stub_method!( - activation, - "flash.display.ShaderJob", - "start", - "async execution and non-BitmapData inputs" - ); - +) -> Result< + ( + PixelBenderShaderHandle, + Vec>, + ), + Error<'gc>, +> { // FIXME - determine what errors Flash Player throws here // instead of using `expect` - let shader = this - .get_public_property("shader", activation)? - .as_object() - .expect("Missing Shader object"); - let shader_data = shader + let shader_data = shader_obj .get_public_property("data", activation)? .as_object() .expect("Missing ShaderData object") @@ -46,7 +37,7 @@ pub fn start<'gc>( .expect("ShaderData object has no shader"); let shader = shader_handle.0.parsed_shader(); - let arguments: Vec<_> = shader + let args = shader .params .iter() .enumerate() @@ -108,31 +99,58 @@ pub fn start<'gc>( let input = shader_input .get_public_property("input", activation) .expect("Missing input property"); - let input = input - .as_object() - .expect("ShaderInput.input is not an object"); - let bitmap = input.as_bitmap_data().expect( - "ShaderInput.input is not a BitmapData (FIXE - support other types)", - ); + let texture = if let Value::Null = input { + None + } else { + let input = input + .as_object() + .expect("ShaderInput.input is not an object"); - // FIXME - this really only needs to be a CPU->GPU sync - let bitmap = bitmap.sync(); - let mut bitmap_data = bitmap.write(activation.context.gc_context); - bitmap_data.update_dirty_texture(activation.context.renderer); + let bitmap = input.as_bitmap_data().expect( + "ShaderInput.input is not a BitmapData (FIXE - support other types)", + ); + + Some(bitmap.bitmap_handle( + activation.context.gc_context, + activation.context.renderer, + )) + }; Some(PixelBenderShaderArgument::ImageInput { index: *index, channels: *channels, name: name.clone(), - texture: bitmap_data - .bitmap_handle(activation.context.renderer) - .expect("Missing input BitmapHandle"), + texture: texture.map(|t| t.into()), }) } } }) .collect(); + Ok((shader_handle.clone(), args)) +} + +/// Implements `ShaderJob.start`. +pub fn start<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Option>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let this = this.unwrap(); + + avm2_stub_method!( + activation, + "flash.display.ShaderJob", + "start", + "async execution and non-BitmapData inputs" + ); + + let shader = this + .get_public_property("shader", activation)? + .as_object() + .expect("Missing Shader object"); + + let (shader_handle, arguments) = get_shader_args(shader, activation)?; let target = this .get_public_property("target", activation)? @@ -141,7 +159,7 @@ pub fn start<'gc>( let target_bitmap = target .as_bitmap_data() - .expect("ShaderJob.target is not a BitmapData (FIXE - support other types)") + .expect("ShaderJob.target is not a BitmapData (FIXME - support other types)") .sync(); // Perform both a GPU->CPU and CPU->GPU sync before writing to it. @@ -156,7 +174,7 @@ pub fn start<'gc>( let sync_handle = activation .context .renderer - .run_pixelbender_shader(shader_handle.clone(), &arguments, target_handle) + .run_pixelbender_shader(shader_handle, &arguments, target_handle) .expect("Failed to run shader"); let width = target_bitmap_data.width(); diff --git a/core/src/avm2/globals/flash/filters/ShaderFilter.as b/core/src/avm2/globals/flash/filters/ShaderFilter.as index 79e53f1b7..bf2b9bfbf 100644 --- a/core/src/avm2/globals/flash/filters/ShaderFilter.as +++ b/core/src/avm2/globals/flash/filters/ShaderFilter.as @@ -15,6 +15,43 @@ package flash.filters { public function set shader(value:Shader):void { this._shader = value; } + + private var _bottomExtension:int = 0; + private var _leftExtension:int = 0; + private var _rightExtension:int = 0; + private var _topExtension:int = 0; + + public function get bottomExtension():int { + return this._bottomExtension; + } + + public function set bottomExtension(value:int):void { + this._bottomExtension = value; + } + + public function get leftExtension():int { + return this._leftExtension; + } + + public function set leftExtension(value:int):void { + this._leftExtension = value; + } + + public function get rightExtension():int { + return this._rightExtension; + } + + public function set rightExtension(value:int):void { + this._rightExtension = value; + } + + public function get topExtension():int { + return this._topExtension; + } + + public function set topExtension(value:int):void { + this._topExtension = value; + } // ShaderFilter is the only filter class that doesn't override clone } diff --git a/core/src/context.rs b/core/src/context.rs index d1c325f03..b9f541481 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -218,7 +218,6 @@ pub struct UpdateContext<'a, 'gc> { pub stream_manager: &'a mut StreamManager<'gc>, /// Dynamic root for allowing handles to GC objects to exist outside of the GC. - #[cfg(feature = "egui")] pub dynamic_root: gc_arena::DynamicRootSet<'gc>, } @@ -377,7 +376,6 @@ impl<'a, 'gc> UpdateContext<'a, 'gc> { actions_since_timeout_check: self.actions_since_timeout_check, frame_phase: self.frame_phase, stream_manager: self.stream_manager, - #[cfg(feature = "egui")] dynamic_root: self.dynamic_root, } } diff --git a/core/src/pixel_bender.rs b/core/src/pixel_bender.rs index 71960b0c7..ce538810b 100644 --- a/core/src/pixel_bender.rs +++ b/core/src/pixel_bender.rs @@ -36,6 +36,9 @@ impl PixelBenderTypeExt for PixelBenderType { | PixelBenderTypeOpcode::TFloat2 | PixelBenderTypeOpcode::TFloat3 | PixelBenderTypeOpcode::TFloat4 + | PixelBenderTypeOpcode::TFloat2x2 + | PixelBenderTypeOpcode::TFloat3x3 + | PixelBenderTypeOpcode::TFloat4x4 ); match value { @@ -69,6 +72,15 @@ impl PixelBenderTypeExt for PixelBenderType { vals.next().unwrap(), vals.next().unwrap(), )), + PixelBenderTypeOpcode::TFloat2x2 => Ok(PixelBenderType::TFloat2x2( + vals.collect::>().try_into().unwrap(), + )), + PixelBenderTypeOpcode::TFloat3x3 => Ok(PixelBenderType::TFloat3x3( + vals.collect::>().try_into().unwrap(), + )), + PixelBenderTypeOpcode::TFloat4x4 => Ok(PixelBenderType::TFloat4x4( + vals.collect::>().try_into().unwrap(), + )), _ => unreachable!("Unexpected float kind {kind:?}"), } } else { diff --git a/core/src/player.rs b/core/src/player.rs index 251e92aaf..426a5e24d 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -1814,7 +1814,6 @@ impl Player { frame_phase: &mut self.frame_phase, stub_tracker: &mut self.stub_tracker, stream_manager, - #[cfg(feature = "egui")] dynamic_root, }; diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 36849c5dd..ac829e4c0 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -32,7 +32,7 @@ bytemuck = "1.13.1" os_info = { version = "3", default-features = false } unic-langid = "0.9.1" sys-locale = "0.3.0" -wgpu = { version = "0.16.1" } +wgpu = { workspace = true } futures = "0.3.28" chrono = { version = "0.4", default-features = false, features = [] } fluent-templates = "0.8.0" diff --git a/render/Cargo.toml b/render/Cargo.toml index 20efb6ec4..90c0c8ca5 100644 --- a/render/Cargo.toml +++ b/render/Cargo.toml @@ -28,6 +28,7 @@ lru = "0.10.0" num-traits = "0.2" num-derive = "0.3" byteorder = "1.4" +wgpu = { workspace = true, optional = true } [dependencies.jpeg-decoder] version = "0.3.0" @@ -40,3 +41,4 @@ approx = "0.5.1" default = [] tessellator = ["lyon"] web = ["wasm-bindgen"] +wgpu = ["dep:wgpu"] diff --git a/render/naga-pixelbender/Cargo.toml b/render/naga-pixelbender/Cargo.toml index 4eed73691..5af781255 100644 --- a/render/naga-pixelbender/Cargo.toml +++ b/render/naga-pixelbender/Cargo.toml @@ -10,6 +10,7 @@ version.workspace = true [dependencies] ruffle_render = { path = "../" } naga = { workspace = true } +naga_oil = { workspace = true } anyhow = "1.0.71" bitflags = "2.3.2" diff --git a/render/naga-pixelbender/src/lib.rs b/render/naga-pixelbender/src/lib.rs index 9f4d198e2..879200253 100644 --- a/render/naga-pixelbender/src/lib.rs +++ b/render/naga-pixelbender/src/lib.rs @@ -1,4 +1,4 @@ -use std::sync::OnceLock; +use std::{sync::OnceLock, vec}; use anyhow::Result; use naga::{ @@ -7,15 +7,17 @@ use naga::{ EntryPoint, Expression, Function, FunctionArgument, FunctionResult, GlobalVariable, Handle, ImageClass, ImageDimension, ImageQuery, LocalVariable, MathFunction, Module, ResourceBinding, ScalarKind, ScalarValue, ShaderStage, Span, Statement, SwizzleComponent, Type, TypeInner, + VectorSize, }; +use naga_oil::compose::{Composer, NagaModuleDescriptor}; use ruffle_render::pixel_bender::{ Opcode, Operation, PixelBenderParam, PixelBenderParamQualifier, PixelBenderReg, PixelBenderRegChannel, PixelBenderRegKind, PixelBenderShader, PixelBenderTypeOpcode, OUT_COORD_NAME, }; -/// The entrypoint name for the vertex and fragment shaders. -pub const SHADER_ENTRYPOINT: &str = "main"; +pub const VERTEX_SHADER_ENTRYPOINT: &str = "main_vertex"; +pub const FRAGMENT_SHADER_ENTRYPOINT: &str = "main"; pub struct NagaModules { pub vertex: naga::Module, @@ -33,9 +35,23 @@ pub struct ShaderBuilder<'a> { vec2f: Handle, vec4f: Handle, vec4i: Handle, + mat2x2f: Handle, + mat3x3f: Handle, + mat4x4f: Handle, image2d: Handle, sampler: Handle, + // The value 0.0f32 + zerof32: Handle, + // The value 0i32 + zeroi32: Handle, + // The value 1.0f32 + onef32: Handle, + + // A temporary vec4f local variable. + // Currently used during texture sampling. + temp_vec4f_local: Handle, + clamp_nearest: Handle, clamp_linear: Handle, // FIXME - implement the corresponding opcode 'Sample' @@ -54,6 +70,9 @@ pub struct ShaderBuilder<'a> { /// Like float_registesr but with vec4i int_registers: Vec>>, + /// Like float_registers but with matrices + matrix_registers: Vec>>, + // A stack of if/else blocks, using to push statements // into the correct block. blocks: Vec, @@ -79,8 +98,6 @@ enum BlockStackEntry { const TEXTURE_SAMPLER_START_BIND_INDEX: u32 = 0; -// FIXME - this shouldn't actually be clamp - https://www.mcjones.org/paul/PixelBenderReference.pdf -// says that coordinates outside the range are 'transparent black' pub const SAMPLER_CLAMP_NEAREST: u32 = 0; pub const SAMPLER_CLAMP_LINEAR: u32 = 1; pub const SAMPLER_CLAMP_BILINEAR: u32 = 2; @@ -89,21 +106,20 @@ pub const SHADER_FLOAT_PARAMETERS_INDEX: u32 = 3; // This covers ints and bool parameters pub const SHADER_INT_PARAMETERS_INDEX: u32 = 4; -pub const TEXTURE_START_BIND_INDEX: u32 = 5; +// A parameter controlling whether or not we produce transparent black (zero) +// for textures samples with out-of-range coordinates. This is a single floating-point +// uniform - when it's 0.0f32, we use the default clamping behavior, and produce +// transparent black when it's any other value. +// +// Note - https://www.mcjones.org/paul/PixelBenderReference.pdf +// claims that coordinates outside the range are 'transparent black'. +// However, some testing shows that the actual behavior is 'clamp' (at least +// when a shader is run through a ShaderJob, and is only 'transparent black' +// when a ShaderFilter is used. We set this uniform from Ruffle based on +// how the shader is being invoked. +pub const ZEROED_OUT_OF_RANGE_MODE_INDEX: u32 = 5; -const VERTEX_SHADER_WGSL: &str = r#" -struct VertexOutput { - @builtin(position) position: vec4, -}; - -@vertex -fn main( - @location(0) position: vec2, -) -> VertexOutput { - // Map coordinates from [0, 1] to [-1, 1] - return VertexOutput(vec4((position * vec2(2.0, 2.0)) - vec2(1.0, 1.0), 0.0, 1.0)); -}; -"#; +pub const TEXTURE_START_BIND_INDEX: u32 = 6; impl<'a> ShaderBuilder<'a> { pub fn build(shader: &PixelBenderShader) -> Result { @@ -112,9 +128,25 @@ impl<'a> ShaderBuilder<'a> { static VERTEX_SHADER: OnceLock = OnceLock::new(); let vertex_shader = VERTEX_SHADER .get_or_init(|| { - naga::front::wgsl::Frontend::new() - .parse(VERTEX_SHADER_WGSL) - .expect("Failed to parse vertex shader") + let mut composer = Composer::default(); + // [NA] Hack to get all capabilities since nobody exposes this type easily + let capabilities = composer.capabilities; + composer = composer.with_capabilities(!capabilities); + + composer + .make_naga_module(NagaModuleDescriptor { + source: ruffle_render::shader_source::SHADER_FILTER_COMMON, + file_path: "shaders/filter/common.wgsl", + shader_defs: Default::default(), + ..Default::default() + }) + .unwrap_or_else(|e| { + panic!( + "shader_filter_common.wgsl failed to compile:\n{}\n{:#?}", + e.emit_to_string(&composer), + e + ) + }) }) .clone(); @@ -154,6 +186,42 @@ impl<'a> ShaderBuilder<'a> { Span::UNDEFINED, ); + let mat2x2f = module.types.insert( + Type { + name: None, + inner: TypeInner::Matrix { + columns: naga::VectorSize::Bi, + rows: naga::VectorSize::Bi, + width: 4, + }, + }, + Span::UNDEFINED, + ); + + let mat3x3f = module.types.insert( + Type { + name: None, + inner: TypeInner::Matrix { + columns: naga::VectorSize::Tri, + rows: naga::VectorSize::Tri, + width: 4, + }, + }, + Span::UNDEFINED, + ); + + let mat4x4f = module.types.insert( + Type { + name: None, + inner: TypeInner::Matrix { + columns: naga::VectorSize::Quad, + rows: naga::VectorSize::Quad, + width: 4, + }, + }, + Span::UNDEFINED, + ); + let image2d = module.types.insert( Type { name: None, @@ -183,6 +251,17 @@ impl<'a> ShaderBuilder<'a> { ty: vec4f, binding: Some(Binding::BuiltIn(BuiltIn::Position { invariant: false })), }); + // UV coordinates from vertex shader - unused, but wgpu + // requires that we consume all outputs from the vertex shader + func.arguments.push(FunctionArgument { + name: None, + ty: vec2f, + binding: Some(Binding::Location { + location: 0, + interpolation: Some(naga::Interpolation::Perspective), + sampling: Some(naga::Sampling::Center), + }), + }); func.result = Some(FunctionResult { ty: vec4f, @@ -219,14 +298,79 @@ impl<'a> ShaderBuilder<'a> { }) .collect::>(); + let const_zeroi32 = module.constants.append( + Constant { + name: None, + specialization: None, + inner: ConstantInner::Scalar { + width: 4, + value: ScalarValue::Sint(0), + }, + }, + Span::UNDEFINED, + ); + let zeroi32 = func + .expressions + .append(Expression::Constant(const_zeroi32), Span::UNDEFINED); + + let const_zerof32 = module.constants.append( + Constant { + name: None, + specialization: None, + inner: ConstantInner::Scalar { + width: 4, + value: ScalarValue::Float(0.0), + }, + }, + Span::UNDEFINED, + ); + let zerof32 = func + .expressions + .append(Expression::Constant(const_zerof32), Span::UNDEFINED); + + let const_onef32 = module.constants.append( + crate::Constant { + name: None, + specialization: None, + inner: crate::ConstantInner::Scalar { + width: 4, + value: crate::ScalarValue::Float(1.0), + }, + }, + Span::UNDEFINED, + ); + + let onef32 = func + .expressions + .append(Expression::Constant(const_onef32), Span::UNDEFINED); + + let temp_vec4f_local = func.local_variables.append( + LocalVariable { + name: Some("temp_vec4f_local".to_string()), + ty: vec4f, + init: None, + }, + Span::UNDEFINED, + ); + let temp_vec4f_local = func + .expressions + .append(Expression::LocalVariable(temp_vec4f_local), Span::UNDEFINED); + let mut builder = ShaderBuilder { module, func, vec2f, vec4f, vec4i, + mat2x2f, + mat3x3f, + mat4x4f, image2d, sampler, + zerof32, + zeroi32, + onef32, + temp_vec4f_local, clamp_nearest: samplers[SAMPLER_CLAMP_NEAREST as usize], clamp_linear: samplers[SAMPLER_CLAMP_LINEAR as usize], clamp_bilinear: samplers[SAMPLER_CLAMP_BILINEAR as usize], @@ -235,33 +379,76 @@ impl<'a> ShaderBuilder<'a> { textures: Vec::new(), float_registers: Vec::new(), int_registers: Vec::new(), + matrix_registers: Vec::new(), blocks: vec![BlockStackEntry::Normal(Block::new())], }; + let zeroed_out_of_range_mode_global = builder.module.global_variables.append( + GlobalVariable { + name: Some("zeroed_out_of_range_mode".to_string()), + space: naga::AddressSpace::Uniform, + binding: Some(naga::ResourceBinding { + group: 0, + binding: ZEROED_OUT_OF_RANGE_MODE_INDEX, + }), + ty: builder.module.types.insert( + Type { + name: None, + inner: TypeInner::Scalar { + kind: ScalarKind::Float, + width: 4, + }, + }, + Span::UNDEFINED, + ), + init: None, + }, + Span::UNDEFINED, + ); + + let zeroed_out_of_range_expr = builder.func.expressions.append( + Expression::GlobalVariable(zeroed_out_of_range_mode_global), + Span::UNDEFINED, + ); + let zeroed_out_of_range_expr = builder.evaluate_expr(Expression::Load { + pointer: zeroed_out_of_range_expr, + }); + let zeroed_out_of_range_expr = builder.evaluate_expr(Expression::Binary { + op: BinaryOperator::NotEqual, + left: zeroed_out_of_range_expr, + right: builder.zerof32, + }); + let wrapper_func = builder.make_sampler_wrapper(); let (float_parameters_buffer_size, int_parameters_buffer_size) = builder.add_arguments()?; - builder.process_opcodes(wrapper_func)?; + builder.process_opcodes(wrapper_func, zeroed_out_of_range_expr)?; - let dst = shader + let (dst, dst_param_type) = shader .params .iter() .find_map(|p| { if let PixelBenderParam::Normal { qualifier: PixelBenderParamQualifier::Output, reg, + param_type, .. } = p { - Some(reg) + Some((reg, param_type)) } else { None } }) .expect("Missing destination register!"); + + let expected_dst_channels = match dst_param_type { + PixelBenderTypeOpcode::TFloat4 => PixelBenderRegChannel::RGBA.as_slice(), + PixelBenderTypeOpcode::TFloat3 => PixelBenderRegChannel::RGB.as_slice(), + _ => panic!("Invalid destination register type: {:?}", dst_param_type), + }; assert_eq!( - dst.channels, - PixelBenderRegChannel::RGBA, + dst.channels, expected_dst_channels, "Invalid 'dest' parameter register {dst:?}" ); @@ -305,13 +492,19 @@ impl<'a> ShaderBuilder<'a> { } fn add_arguments(&mut self) -> Result<(u64, u64)> { - let mut num_floats = 0; - let mut num_ints = 0; + let mut num_vec4fs = 0; + let mut num_vec4is = 0; let mut param_offsets = Vec::new(); let mut out_coord = None; + enum ParamKind { + Float, + Int, + FloatMatrix, + } + for param in &self.shader.params { match param { PixelBenderParam::Normal { @@ -327,28 +520,42 @@ impl<'a> ShaderBuilder<'a> { continue; } - let float_offset = num_floats; - let int_offset = num_ints; + let float_offset = num_vec4fs; + let int_offset = num_vec4is; - // To meet alignment requirements, each parameter is stored as a vec4 in the constants array. + // To meet alignment requirements, each parameter is stored as some number of vec4s in the constants array. // Smaller types (e.g. Float, Float2, Float3) are padded with zeros. let (offset, is_float) = match param_type { PixelBenderTypeOpcode::TFloat | PixelBenderTypeOpcode::TFloat2 | PixelBenderTypeOpcode::TFloat3 | PixelBenderTypeOpcode::TFloat4 => { - num_floats += 1; - (float_offset, true) + num_vec4fs += 1; + (float_offset, ParamKind::Float) } PixelBenderTypeOpcode::TInt | PixelBenderTypeOpcode::TInt2 | PixelBenderTypeOpcode::TInt3 | PixelBenderTypeOpcode::TInt4 => { - num_ints += 1; - (int_offset, false) + num_vec4is += 1; + (int_offset, ParamKind::Int) } PixelBenderTypeOpcode::TString => continue, - _ => unimplemented!("Unsupported parameter type {:?}", param_type), + PixelBenderTypeOpcode::TFloat2x2 => { + // A 2x2 matrix fits into a single vec4 + num_vec4fs += 1; + (float_offset, ParamKind::FloatMatrix) + } + PixelBenderTypeOpcode::TFloat3x3 => { + // Each row of the matrix is stored in a vec4 (with the last component of each set to 0) + num_vec4fs += 3; + (float_offset, ParamKind::FloatMatrix) + } + PixelBenderTypeOpcode::TFloat4x4 => { + // Each row of the matrix is a vec4 + num_vec4fs += 4; + (float_offset, ParamKind::FloatMatrix) + } }; param_offsets.push((reg, offset, is_float)); @@ -395,7 +602,7 @@ impl<'a> ShaderBuilder<'a> { specialization: None, inner: naga::ConstantInner::Scalar { width: 4, - value: naga::ScalarValue::Uint(num_floats.max(1) as u64), + value: naga::ScalarValue::Uint(num_vec4fs.max(1) as u64), }, }, Span::UNDEFINED, @@ -407,7 +614,7 @@ impl<'a> ShaderBuilder<'a> { specialization: None, inner: naga::ConstantInner::Scalar { width: 4, - value: naga::ScalarValue::Uint(num_ints.max(1) as u64), + value: naga::ScalarValue::Uint(num_vec4is.max(1) as u64), }, }, Span::UNDEFINED, @@ -461,26 +668,132 @@ impl<'a> ShaderBuilder<'a> { Span::UNDEFINED, ); - for (reg, offset, is_float) in param_offsets { - let global = if is_float { - shader_float_parameters - } else { - shader_int_parameters + for (reg, offset, param_kind) in param_offsets { + let param_global = match param_kind { + ParamKind::Float => shader_float_parameters, + ParamKind::Int => shader_int_parameters, + ParamKind::FloatMatrix => shader_float_parameters, }; let global_base = self .func .expressions - .append(Expression::GlobalVariable(global), Span::UNDEFINED); + .append(Expression::GlobalVariable(param_global), Span::UNDEFINED); let src_ptr = self.evaluate_expr(Expression::AccessIndex { base: global_base, index: offset, }); - let src = self.evaluate_expr(Expression::Load { pointer: src_ptr }); + let src_expr = match reg.channels[0] { + // FIXME - add tests for this case + PixelBenderRegChannel::M2x2 => { + // A 2x2 matrix fits exactly into a vec4f + let vec0_load = self.evaluate_expr(Expression::Load { pointer: src_ptr }); + // Only the first two components of `pattern` matter + let row0 = self.evaluate_expr(Expression::Swizzle { + size: VectorSize::Bi, + vector: vec0_load, + pattern: [ + SwizzleComponent::X, + SwizzleComponent::Y, + SwizzleComponent::W, + SwizzleComponent::W, + ], + }); - self.emit_dest_store(src, reg); + // Only the first two components of `pattern` matter (load the Z and W components into the second row) + let row1 = self.evaluate_expr(Expression::Swizzle { + size: VectorSize::Bi, + vector: vec0_load, + pattern: [ + SwizzleComponent::Z, + SwizzleComponent::W, + SwizzleComponent::W, + SwizzleComponent::W, + ], + }); + + self.evaluate_expr(Expression::Compose { + ty: self.mat2x2f, + components: vec![row0, row1], + }) + } + PixelBenderRegChannel::M3x3 | PixelBenderRegChannel::M4x4 => { + let mut row0 = self.evaluate_expr(Expression::Load { pointer: src_ptr }); + + let vec1_ptr = self.evaluate_expr(Expression::AccessIndex { + base: global_base, + index: offset + 1, + }); + let mut row1 = self.evaluate_expr(Expression::Load { pointer: vec1_ptr }); + + let vec2_ptr = self.evaluate_expr(Expression::AccessIndex { + base: global_base, + index: offset + 2, + }); + let mut row2 = self.evaluate_expr(Expression::Load { pointer: vec2_ptr }); + + match reg.channels[0] { + PixelBenderRegChannel::M3x3 => { + row0 = self.evaluate_expr(Expression::Swizzle { + size: VectorSize::Tri, + vector: row0, + pattern: [ + SwizzleComponent::X, + SwizzleComponent::Y, + SwizzleComponent::Z, + SwizzleComponent::W, + ], + }); + + row1 = self.evaluate_expr(Expression::Swizzle { + size: VectorSize::Tri, + vector: row1, + pattern: [ + SwizzleComponent::X, + SwizzleComponent::Y, + SwizzleComponent::Z, + SwizzleComponent::W, + ], + }); + + row2 = self.evaluate_expr(Expression::Swizzle { + size: VectorSize::Tri, + vector: row2, + pattern: [ + SwizzleComponent::X, + SwizzleComponent::Y, + SwizzleComponent::Z, + SwizzleComponent::W, + ], + }); + + self.evaluate_expr(Expression::Compose { + ty: self.mat3x3f, + components: vec![row0, row1, row2], + }) + } + // FIXME - add tests for this case + PixelBenderRegChannel::M4x4 => { + let vec3_ptr = self.evaluate_expr(Expression::AccessIndex { + base: global_base, + index: offset + 3, + }); + let row3 = self.evaluate_expr(Expression::Load { pointer: vec3_ptr }); + + self.evaluate_expr(Expression::Compose { + ty: self.mat4x4f, + components: vec![row0, row1, row2, row3], + }) + } + _ => unreachable!(), + } + } + _ => self.evaluate_expr(Expression::Load { pointer: src_ptr }), + }; + + self.emit_dest_store(src_expr, reg); } // Emit this after all other registers have been initialized @@ -495,11 +808,146 @@ impl<'a> ShaderBuilder<'a> { } Ok(( - num_floats.max(1) as u64 * 4 * std::mem::size_of::() as u64, - num_ints.max(1) as u64 * 4 * std::mem::size_of::() as u64, + num_vec4fs.max(1) as u64 * 4 * std::mem::size_of::() as u64, + num_vec4is.max(1) as u64 * 4 * std::mem::size_of::() as u64, )) } + /// Samples a texture, determining the out-of-range coordinate behavior + /// based on `zeroed_out_of_range_expr`. See the comments on `ZEROED_OUT_OF_RANGE_MODE_INDEX` + /// for more details. + fn sample_texture( + &mut self, + sample_wrapper_func: Handle, + normalized_coord: Handle, + image: Handle, + sampler: Handle, + zeroed_out_of_range_expr: Handle, + ) -> Handle { + // Don't evaluate this expression - it gets evaluated by Statement::Call + let result = self + .func + .expressions + .append(Expression::CallResult(sample_wrapper_func), Span::UNDEFINED); + + // Build up the expression '(coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0)' + + let x_coord: Handle = self.evaluate_expr(Expression::AccessIndex { + base: normalized_coord, + index: 0, + }); + + let y_coord = self.evaluate_expr(Expression::AccessIndex { + base: normalized_coord, + index: 1, + }); + + let x_coord_lt_zero = self.evaluate_expr(Expression::Binary { + op: BinaryOperator::Less, + left: x_coord, + right: self.zerof32, + }); + + let x_coord_gt_one = self.evaluate_expr(Expression::Binary { + op: BinaryOperator::Greater, + left: x_coord, + right: self.onef32, + }); + + let y_coord_lt_zero = self.evaluate_expr(Expression::Binary { + op: BinaryOperator::Less, + left: y_coord, + right: self.zerof32, + }); + + let y_coord_gt_one = self.evaluate_expr(Expression::Binary { + op: BinaryOperator::Greater, + left: y_coord, + right: self.onef32, + }); + + let x_coord_logical_or = self.evaluate_expr(Expression::Binary { + op: BinaryOperator::LogicalOr, + left: x_coord_lt_zero, + right: x_coord_gt_one, + }); + + let y_coord_logical_or = self.evaluate_expr(Expression::Binary { + op: BinaryOperator::LogicalOr, + left: y_coord_lt_zero, + right: y_coord_gt_one, + }); + + let any_coord_out_of_range = self.evaluate_expr(Expression::Binary { + op: BinaryOperator::LogicalOr, + left: x_coord_logical_or, + right: y_coord_logical_or, + }); + + let out_of_range_cond = self.evaluate_expr(Expression::Binary { + op: BinaryOperator::LogicalAnd, + left: zeroed_out_of_range_expr, + right: any_coord_out_of_range, + }); + + // Construct the statements: + // ``` + // if (zeroed_out_of_range_expr && any_coord_out_of_range) { + // temp_local = vec4f(0.0); + // else { + // temp_local = sample_wrapper_func(image, sampler, normalized_coord); + // } + // return temp_local + // ``` + // + // Note that due to the overly restrictive uniformity analysis in wgpu/naga, + // we need this `if/else` at every call site - it cannot be inlined into + // `sample_wrapper_func` + + let mut good_coord_block = Block::new(); + // Call our helper function, which just calls 'Expression::ImageSample' with + // the provided parameters. This works around a uniformity analysis issue + // with wgpu/naga + good_coord_block.push( + Statement::Call { + function: sample_wrapper_func, + arguments: vec![image, sampler, normalized_coord], + result: Some(result), + }, + Span::UNDEFINED, + ); + good_coord_block.push( + Statement::Store { + pointer: self.temp_vec4f_local, + value: result, + }, + Span::UNDEFINED, + ); + + let mut bad_coord_block = Block::new(); + let zero_vec = self.evaluate_expr(Expression::Splat { + size: VectorSize::Quad, + value: self.zerof32, + }); + bad_coord_block.push( + Statement::Store { + pointer: self.temp_vec4f_local, + value: zero_vec, + }, + Span::UNDEFINED, + ); + + self.push_statement(Statement::If { + condition: out_of_range_cond, + accept: bad_coord_block, + reject: good_coord_block, + }); + + self.evaluate_expr(Expression::Load { + pointer: self.temp_vec4f_local, + }) + } + // Works around wgpu requiring naga's strict level of uniformity analysis // See https://github.com/gpuweb/gpuweb/issues/3479#issuecomment-1519140312 fn make_sampler_wrapper(&mut self) -> Handle { @@ -567,11 +1015,19 @@ impl<'a> ShaderBuilder<'a> { self.module.functions.append(func, Span::UNDEFINED) } - fn process_opcodes(&mut self, sample_wrapper_func: Handle) -> Result<()> { + fn process_opcodes( + &mut self, + sample_wrapper_func: Handle, + zeroed_out_of_range_expr: Handle, + ) -> Result<()> { for op in &self.shader.operations { match op { - Operation::Normal { opcode, dst, src } => { - let src = self.load_src_register(src)?; + Operation::Normal { + opcode, + dst, + src: src_reg, + } => { + let src = self.load_src_register(src_reg)?; let mut dst = dst.clone(); let evaluated = match opcode { Opcode::Mov => src, @@ -721,8 +1177,79 @@ impl<'a> ShaderBuilder<'a> { value: length, }) } + Opcode::MatVecMul => { + let right = self.load_src_register_with_padding(&dst, false)?; + self.evaluate_expr(Expression::Binary { + op: BinaryOperator::Multiply, + left: src, + right, + }) + } + Opcode::VecMatMul => { + let vec = self.load_src_register_with_padding(&dst, false)?; + self.evaluate_expr(Expression::Binary { + op: BinaryOperator::Multiply, + left: vec, + right: src, + }) + } + Opcode::Distance => { + let left = self.load_src_register_with_padding(&dst, false)?; + let right = self.load_src_register_with_padding(src_reg, false)?; + let dist = self.evaluate_expr(Expression::Math { + fun: MathFunction::Distance, + arg: left, + arg1: Some(right), + arg2: None, + arg3: None, + }); + self.evaluate_expr(Expression::Splat { + size: VectorSize::Quad, + value: dist, + }) + } + Opcode::Max => { + let right = self.load_src_register(&dst)?; + self.evaluate_expr(Expression::Math { + fun: MathFunction::Max, + arg: src, + arg1: Some(right), + arg2: None, + arg3: None, + }) + } + Opcode::Min => { + let right = self.load_src_register(&dst)?; + self.evaluate_expr(Expression::Math { + fun: MathFunction::Min, + arg: src, + arg1: Some(right), + arg2: None, + arg3: None, + }) + } + Opcode::Normalize => { + let src = self.load_src_register_with_padding(src_reg, false)?; + self.evaluate_expr(Expression::Math { + fun: MathFunction::Normalize, + arg: src, + arg1: None, + arg2: None, + arg3: None, + }) + } + Opcode::Pow => { + let dst_val = self.load_src_register(&dst)?; + self.evaluate_expr(Expression::Math { + fun: MathFunction::Pow, + arg: dst_val, + arg1: Some(src), + arg2: None, + arg3: None, + }) + } _ => { - unimplemented!("Unimplemented opcode {opcode:?}"); + panic!("Unimplemented opcode {opcode:?}") } }; self.emit_dest_store(evaluated, &dst); @@ -767,22 +1294,15 @@ impl<'a> ShaderBuilder<'a> { _ => unreachable!(), }; - // Don't evaluate this expression - it gets evaluated by Statement::Call - let result = self - .func - .expressions - .append(Expression::CallResult(sample_wrapper_func), Span::UNDEFINED); + let sample_result = self.sample_texture( + sample_wrapper_func, + normalized_coord, + image, + sampler, + zeroed_out_of_range_expr, + ); - // Call our helper function, which just calls 'Expression::ImageSample' with - // the provided parameters. This works around a uniformity analysis issue - // with wgpu/naga - self.push_statement(Statement::Call { - function: sample_wrapper_func, - arguments: vec![image, sampler, normalized_coord], - result: Some(result), - }); - - self.emit_dest_store(result, dst); + self.emit_dest_store(sample_result, dst); } Operation::LoadFloat { dst, val } => { let const_val = self.module.constants.append( @@ -831,9 +1351,9 @@ impl<'a> ShaderBuilder<'a> { self.emit_dest_store(const_vec, dst); } Operation::If { src } => { - let scalar_zero = match src.kind { - PixelBenderRegKind::Float => ScalarValue::Float(0.0), - PixelBenderRegKind::Int => ScalarValue::Sint(0), + let expr_zero = match src.kind { + PixelBenderRegKind::Float => self.zerof32, + PixelBenderRegKind::Int => self.zeroi32, }; if src.channels.len() != 1 { panic!("If condition must be a scalar: {src:?}"); @@ -847,23 +1367,6 @@ impl<'a> ShaderBuilder<'a> { index: 0, }); - let const_zero = self.module.constants.append( - Constant { - name: None, - specialization: None, - inner: ConstantInner::Scalar { - width: 4, - value: scalar_zero, - }, - }, - Span::UNDEFINED, - ); - - let expr_zero = self - .func - .expressions - .append(Expression::Constant(const_zero), Span::UNDEFINED); - let is_true = self.evaluate_expr(Expression::Binary { op: BinaryOperator::NotEqual, left: first_component, @@ -929,9 +1432,29 @@ impl<'a> ShaderBuilder<'a> { fn register_pointer(&mut self, reg: &PixelBenderReg) -> Result> { let index = reg.index as usize; - let (ty, registers, register_kind_name) = match reg.kind { - PixelBenderRegKind::Float => (self.vec4f, &mut self.float_registers, "float"), - PixelBenderRegKind::Int => (self.vec4i, &mut self.int_registers, "int"), + let (ty, registers, register_kind_name) = if matches!( + ®.channels.as_slice(), + [PixelBenderRegChannel::M2x2] + | [PixelBenderRegChannel::M3x3] + | [PixelBenderRegChannel::M4x4] + ) { + assert_eq!( + reg.kind, + PixelBenderRegKind::Float, + "Unexpected matrix element type" + ); + let (ty, name) = match reg.channels[0] { + PixelBenderRegChannel::M2x2 => (self.mat2x2f, "mat2x2f"), + PixelBenderRegChannel::M3x3 => (self.mat3x3f, "mat3x3f"), + PixelBenderRegChannel::M4x4 => (self.mat4x4f, "mat4x4f"), + _ => unreachable!(), + }; + (ty, &mut self.matrix_registers, name) + } else { + match reg.kind { + PixelBenderRegKind::Float => (self.vec4f, &mut self.float_registers, "float"), + PixelBenderRegKind::Int => (self.vec4i, &mut self.int_registers, "int"), + } }; if index >= registers.len() { @@ -957,14 +1480,31 @@ impl<'a> ShaderBuilder<'a> { Ok(registers[index].unwrap()) } + fn load_src_register(&mut self, reg: &PixelBenderReg) -> Result> { + self.load_src_register_with_padding(reg, true) + } + /// Loads a vec4f/vec4i from the given register. Note that all registers are 4-component /// vectors - if the `PixelBenderReg` requests fewer components then that, then the extra /// components will be meaningless. This greatly simplifies the code, since we don't need /// to track whether or not we have a scalar or a vector everywhere. - fn load_src_register(&mut self, reg: &PixelBenderReg) -> Result> { + fn load_src_register_with_padding( + &mut self, + reg: &PixelBenderReg, + padding: bool, + ) -> Result> { let reg_ptr = self.register_pointer(reg)?; let reg_value = self.evaluate_expr(Expression::Load { pointer: reg_ptr }); + if matches!( + reg.channels.as_slice(), + [PixelBenderRegChannel::M2x2] + | [PixelBenderRegChannel::M3x3] + | [PixelBenderRegChannel::M4x4] + ) { + return Ok(reg_value); + } + let mut swizzle_components = reg .channels .iter() @@ -973,17 +1513,30 @@ impl<'a> ShaderBuilder<'a> { PixelBenderRegChannel::G => SwizzleComponent::Y, PixelBenderRegChannel::B => SwizzleComponent::Z, PixelBenderRegChannel::A => SwizzleComponent::W, + _ => panic!("Unexpected source register channel: {c:?}"), }) .collect::>(); + let size = if padding { + VectorSize::Quad + } else { + match reg.channels.len() { + 1 => panic!("Cannot load single channel as vector for reg {reg:?}"), + 2 => VectorSize::Bi, + 3 => VectorSize::Tri, + 4 => VectorSize::Quad, + _ => unreachable!(), + } + }; + if swizzle_components.len() < 4 { // Pad with W - these components will be ignored, since whatever uses // the result will only use the components corresponding to 'reg.channels' - swizzle_components.resize(4, SwizzleComponent::W); + swizzle_components.resize(4_usize, SwizzleComponent::W); } Ok(self.evaluate_expr(Expression::Swizzle { - size: naga::VectorSize::Quad, + size, vector: reg_value, pattern: swizzle_components.try_into().unwrap(), })) @@ -1001,9 +1554,31 @@ impl<'a> ShaderBuilder<'a> { // Emits a store of `expr` to the destination register, taking into account the store mask. fn emit_dest_store(&mut self, expr: Handle, dst: &PixelBenderReg) { let dst_register = self.register_pointer(dst).unwrap(); + + if matches!( + dst.channels.as_slice(), + [PixelBenderRegChannel::M2x2] + | [PixelBenderRegChannel::M3x3] + | [PixelBenderRegChannel::M4x4] + ) { + self.push_statement(Statement::Store { + pointer: dst_register, + value: expr, + }); + return; + } + for (dst_channel, src_channel) in dst.channels.iter().zip(PixelBenderRegChannel::RGBA.iter()) { + if matches!( + dst_channel, + PixelBenderRegChannel::M2x2 + | PixelBenderRegChannel::M3x3 + | PixelBenderRegChannel::M4x4 + ) { + panic!("Unexpected to matrix channel for dst {dst:?}"); + } // Write each channel of the source to the channel specified by the destiation mask let src_component_index = *src_channel as u32; let dst_component_index = *dst_channel as u32; diff --git a/render/src/backend.rs b/render/src/backend.rs index 7fc37657e..463e1bc22 100644 --- a/render/src/backend.rs +++ b/render/src/backend.rs @@ -120,6 +120,12 @@ impl_downcast!(ShaderModule); pub trait Texture: Downcast + Collect {} impl_downcast!(Texture); +pub trait RawTexture: Downcast + Debug {} +impl_downcast!(RawTexture); + +#[cfg(feature = "wgpu")] +impl RawTexture for wgpu::Texture {} + #[derive(Collect, Debug, Copy, Clone)] #[collect(require_static)] pub enum Context3DTextureFormat { diff --git a/render/src/filters.rs b/render/src/filters.rs index 23bd17100..625b5964e 100644 --- a/render/src/filters.rs +++ b/render/src/filters.rs @@ -1,4 +1,10 @@ -use crate::bitmap::BitmapHandle; +use crate::{ + bitmap::BitmapHandle, + pixel_bender::{PixelBenderShaderArgument, PixelBenderShaderHandle}, +}; +use downcast_rs::{impl_downcast, Downcast}; +use gc_arena::Collect; +use std::fmt::Debug; use swf::Color; #[derive(Debug, Clone)] @@ -12,6 +18,33 @@ pub enum Filter { GlowFilter(swf::GlowFilter), GradientBevelFilter(swf::GradientFilter), GradientGlowFilter(swf::GradientFilter), + ShaderFilter(ShaderFilter<'static>), +} + +#[derive(Debug, Clone)] +pub struct ShaderFilter<'a> { + pub bottom_extension: i32, + pub left_extension: i32, + pub right_extension: i32, + pub top_extension: i32, + /// The AVM2 `flash.display.Shader` object that we extracted + /// the `shader` and `shader_args` fields from. This is used when + /// we reconstruct a `ShaderFilter` object in the AVM2 `DisplayObject.filters` + /// (Flash re-uses the same object) + pub shader_object: Box, + pub shader: PixelBenderShaderHandle, + pub shader_args: Vec>, +} + +pub trait ShaderObject: Downcast + Collect + Debug { + fn clone_box(&self) -> Box; +} +impl_downcast!(ShaderObject); + +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_box() + } } impl From<&swf::Filter> for Filter { diff --git a/render/src/lib.rs b/render/src/lib.rs index be6818b52..91f112c4e 100644 --- a/render/src/lib.rs +++ b/render/src/lib.rs @@ -6,6 +6,7 @@ pub mod error; pub mod filters; pub mod matrix; pub mod pixel_bender; +pub mod shader_source; pub mod shape_utils; pub mod transform; pub mod utils; diff --git a/render/src/pixel_bender.rs b/render/src/pixel_bender.rs index 0bb296a26..55d5084ef 100644 --- a/render/src/pixel_bender.rs +++ b/render/src/pixel_bender.rs @@ -14,7 +14,7 @@ use std::{ sync::Arc, }; -use crate::bitmap::BitmapHandle; +use crate::{backend::RawTexture, bitmap::BitmapHandle}; /// The name of a special parameter, which gets automatically filled in with the coordinates /// of the pixel being processed. @@ -69,9 +69,17 @@ pub enum PixelBenderRegChannel { G = 1, B = 2, A = 3, + M2x2 = 4, + M3x3 = 5, + M4x4 = 6, } impl PixelBenderRegChannel { + pub const RGB: [PixelBenderRegChannel; 3] = [ + PixelBenderRegChannel::R, + PixelBenderRegChannel::G, + PixelBenderRegChannel::B, + ]; pub const RGBA: [PixelBenderRegChannel; 4] = [ PixelBenderRegChannel::R, PixelBenderRegChannel::G, @@ -229,12 +237,12 @@ pub enum Operation { } #[derive(Debug, Clone)] -pub enum PixelBenderShaderArgument { +pub enum PixelBenderShaderArgument<'a> { ImageInput { index: u8, channels: u8, name: String, - texture: BitmapHandle, + texture: Option>, }, ValueInput { index: u8, @@ -242,6 +250,28 @@ pub enum PixelBenderShaderArgument { }, } +/// An image input. This accepts both an owned BitmapHandle, +/// and a borrowed texture (used when applying a filter to +/// a texture that we don't have ownership of, and therefore +/// cannot construct a BitmapHandle for). +#[derive(Debug, Clone)] +pub enum ImageInputTexture<'a> { + Bitmap(BitmapHandle), + TextureRef(&'a dyn RawTexture), +} + +impl From for ImageInputTexture<'_> { + fn from(b: BitmapHandle) -> Self { + ImageInputTexture::Bitmap(b) + } +} + +impl<'a> From<&'a dyn RawTexture> for ImageInputTexture<'a> { + fn from(t: &'a dyn RawTexture) -> Self { + ImageInputTexture::TextureRef(t) + } +} + #[derive(Debug, PartialEq, Clone)] pub struct PixelBenderShader { pub name: String, @@ -292,14 +322,17 @@ pub fn parse_shader(mut data: &[u8]) -> Result Result> { - const CHANNELS: [PixelBenderRegChannel; 4] = [ - PixelBenderRegChannel::R, - PixelBenderRegChannel::G, - PixelBenderRegChannel::B, - PixelBenderRegChannel::A, - ]; +const CHANNELS: [PixelBenderRegChannel; 7] = [ + PixelBenderRegChannel::R, + PixelBenderRegChannel::G, + PixelBenderRegChannel::B, + PixelBenderRegChannel::A, + PixelBenderRegChannel::M2x2, + PixelBenderRegChannel::M3x3, + PixelBenderRegChannel::M4x4, +]; +fn read_src_reg(val: u32, size: u8) -> Result> { let swizzle = val >> 16; let mut channels = Vec::new(); for i in 0..size { @@ -320,6 +353,10 @@ fn read_src_reg(val: u32, size: u8) -> Result PixelBenderReg { + read_reg(val, vec![CHANNELS[(mask + 3) as usize]]) +} + fn read_dst_reg(val: u16, mask: u8) -> Result> { let mut channels = Vec::new(); if mask & 0x8 != 0 { @@ -335,18 +372,22 @@ fn read_dst_reg(val: u16, mask: u8) -> Result) -> PixelBenderReg { let kind = if val & 0x8000 != 0 { PixelBenderRegKind::Int } else { PixelBenderRegKind::Float }; - Ok(PixelBenderReg { + PixelBenderReg { // Mask off the 0x8000 bit index: (val & 0x7FFF) as u32, channels, kind, - }) + } } fn read_op( @@ -385,21 +426,45 @@ fn read_op( let param_type = PixelBenderTypeOpcode::from_u8(param_type).unwrap_or_else(|| { panic!("Unexpected param type {param_type}"); }); + + // Note - we deviate from Haxe's parser code here. We assert that the provided mask value + // is as expected, but we then construct a Matrix channel register as the dest reg, + // which helps our naga-pixelbender backend. + let dst_reg = match param_type { + PixelBenderTypeOpcode::TFloat2x2 => { + assert_eq!(mask, 2); + PixelBenderReg { + index: reg as u32, + channels: vec![PixelBenderRegChannel::M2x2], + kind: PixelBenderRegKind::Float, + } + } + PixelBenderTypeOpcode::TFloat3x3 => { + assert_eq!(mask, 3); + PixelBenderReg { + index: reg as u32, + channels: vec![PixelBenderRegChannel::M3x3], + kind: PixelBenderRegKind::Float, + } + } + PixelBenderTypeOpcode::TFloat4x4 => { + assert_eq!(mask, 4); + PixelBenderReg { + index: reg as u32, + channels: vec![PixelBenderRegChannel::M4x4], + kind: PixelBenderRegKind::Float, + } + } + _ => { + assert_eq!(mask >> 4, 0); + read_dst_reg(reg, mask)? + } + }; + let qualifier = PixelBenderParamQualifier::from_u8(qualifier) .unwrap_or_else(|| panic!("Unexpected param qualifier {qualifier:?}")); apply_metadata(shader, metadata); - match param_type { - PixelBenderTypeOpcode::TFloat2x2 - | PixelBenderTypeOpcode::TFloat3x3 - | PixelBenderTypeOpcode::TFloat4x4 => { - panic!("Unsupported param type {param_type:?}"); - } - _ => {} - } - - let dst_reg = read_dst_reg(reg, mask)?; - shader.params.push(PixelBenderParam::Normal { qualifier, param_type, @@ -498,19 +563,26 @@ fn read_op( assert_eq!(data.read_u8()?, 0, "Unexpected u8 for opcode {opcode:?}"); mask >>= 4; - let src_reg = read_src_reg(src, size)?; - let dst_reg = if matrix != 0 { + if matrix != 0 { assert_eq!(src >> 16, 0); assert_eq!(size, 1); - panic!("Matrix with mask {mask:b} matrix {matrix:b}"); + let dst = if mask == 0 { + read_matrix_reg(dst, matrix) + } else { + read_dst_reg(dst, mask)? + }; + shader.operations.push(Operation::Normal { + opcode, + dst, + src: read_matrix_reg(src as u16, matrix), + }); } else { - read_dst_reg(dst, mask)? + let dst = read_dst_reg(dst, mask)?; + let src = read_src_reg(src, size)?; + shader + .operations + .push(Operation::Normal { opcode, dst, src }) }; - shader.operations.push(Operation::Normal { - opcode, - dst: dst_reg, - src: src_reg, - }) } }; Ok(()) diff --git a/render/wgpu/shaders/filter/common.wgsl b/render/src/shader_filter_common.wgsl similarity index 97% rename from render/wgpu/shaders/filter/common.wgsl rename to render/src/shader_filter_common.wgsl index e049256a9..2a3b62f59 100644 --- a/render/wgpu/shaders/filter/common.wgsl +++ b/render/src/shader_filter_common.wgsl @@ -1,5 +1,4 @@ #define_import_path filter -#import common struct VertexOutput { @builtin(position) position: vec4, diff --git a/render/src/shader_source.rs b/render/src/shader_source.rs new file mode 100644 index 000000000..ad29da2a4 --- /dev/null +++ b/render/src/shader_source.rs @@ -0,0 +1 @@ +pub const SHADER_FILTER_COMMON: &str = include_str!("shader_filter_common.wgsl"); diff --git a/render/wgpu/Cargo.toml b/render/wgpu/Cargo.toml index 4f3a0403b..3df4b4417 100644 --- a/render/wgpu/Cargo.toml +++ b/render/wgpu/Cargo.toml @@ -8,9 +8,9 @@ repository.workspace = true version.workspace = true [dependencies] -wgpu = { version = "0.16", features = ["naga"] } +wgpu = { workspace = true, features = ["naga"] } tracing = { workspace = true } -ruffle_render = { path = "..", features = ["tessellator"] } +ruffle_render = { path = "..", features = ["tessellator", "wgpu"] } bytemuck = { version = "1.13.1", features = ["derive"] } raw-window-handle = "0.5" clap = { version = "4.3.8", features = ["derive"], optional = true } @@ -18,7 +18,7 @@ enum-map = "2.5.0" fnv = "1.0.7" swf = { path = "../../swf" } image = { version = "0.24.6", default-features = false } -naga_oil = "0.7.0" +naga_oil = { workspace = true } ouroboros = "0.17.0" typed-arena = "2.0.2" gc-arena = { workspace = true } diff --git a/render/wgpu/src/backend.rs b/render/wgpu/src/backend.rs index 2c9852d87..1ddee2b77 100644 --- a/render/wgpu/src/backend.rs +++ b/render/wgpu/src/backend.rs @@ -3,6 +3,7 @@ use crate::buffer_pool::{BufferPool, TexturePool}; use crate::context3d::WgpuContext3D; use crate::filters::FilterSource; use crate::mesh::{Mesh, PendingDraw}; +use crate::pixel_bender::{run_pixelbender_shader_impl, ShaderMode}; use crate::surface::{LayerRef, Surface}; use crate::target::{MaybeOwnedBuffer, TextureTarget}; use crate::target::{RenderTargetFrame, TextureBufferInfo}; @@ -34,6 +35,7 @@ use std::path::Path; use std::sync::Arc; use swf::Color; use tracing::instrument; +use wgpu::SubmissionIndex; /// How many times a texture must be written to & read back from, /// before it's automatically allocated a buffer on each write. @@ -309,6 +311,58 @@ impl WgpuRenderBackend { pub fn device(&self) -> &wgpu::Device { &self.descriptors.device } + + pub fn make_queue_sync_handle( + &self, + target: TextureTarget, + index: SubmissionIndex, + destination: BitmapHandle, + copy_area: PixelRegion, + ) -> Box { + match target.take_buffer() { + None => Box::new(QueueSyncHandle::NotCopied { + handle: destination, + copy_area, + descriptors: self.descriptors.clone(), + pool: self.offscreen_buffer_pool.clone(), + }), + Some(TextureBufferInfo { + buffer: MaybeOwnedBuffer::Borrowed(buffer, copy_dimensions), + .. + }) => Box::new(QueueSyncHandle::AlreadyCopied { + index, + buffer, + copy_dimensions, + descriptors: self.descriptors.clone(), + }), + Some(TextureBufferInfo { + buffer: MaybeOwnedBuffer::Owned(..), + .. + }) => unreachable!("Buffer must be Borrowed as it was set to be Borrowed earlier"), + } + } + + fn get_texture_buffer_info( + &self, + texture: &Texture, + copy_area: PixelRegion, + ) -> Option { + if texture.copy_count.get() >= TEXTURE_READS_BEFORE_PROMOTION { + let copy_dimensions = BufferDimensions::new( + texture.texture.width() as usize, + texture.texture.height() as usize, + ); + let buffer = self + .offscreen_buffer_pool + .take(&self.descriptors, copy_dimensions.clone()); + Some(TextureBufferInfo { + buffer: MaybeOwnedBuffer::Borrowed(buffer, copy_dimensions), + copy_area, + }) + } else { + None + } + } } impl RenderBackend for WgpuRenderBackend { @@ -685,19 +739,7 @@ impl RenderBackend for WgpuRenderBackend { depth_or_array_layers: 1, }; - let buffer_info = if texture.copy_count.get() > TEXTURE_READS_BEFORE_PROMOTION { - let copy_dimensions = - BufferDimensions::new(bounds.width() as usize, bounds.height() as usize); - let buffer = self - .offscreen_buffer_pool - .take(&self.descriptors, copy_dimensions.clone()); - Some(TextureBufferInfo { - buffer: MaybeOwnedBuffer::Borrowed(buffer, copy_dimensions), - copy_area: bounds, - }) - } else { - None - }; + let buffer_info = self.get_texture_buffer_info(texture, bounds); let mut target = TextureTarget { size: extent, @@ -757,31 +799,14 @@ impl RenderBackend for WgpuRenderBackend { self.uniform_buffers_storage.recall(); self.color_buffers_storage.recall(); - match target.take_buffer() { - None => Some(Box::new(QueueSyncHandle::NotCopied { - handle, - copy_area: bounds, - descriptors: self.descriptors.clone(), - pool: self.offscreen_buffer_pool.clone(), - })), - Some(TextureBufferInfo { - buffer: MaybeOwnedBuffer::Borrowed(buffer, copy_dimensions), - .. - }) => Some(Box::new(QueueSyncHandle::AlreadyCopied { - index, - buffer, - copy_dimensions, - descriptors: self.descriptors.clone(), - })), - Some(TextureBufferInfo { - buffer: MaybeOwnedBuffer::Owned(..), - .. - }) => unreachable!("Buffer must be Borrowed as it was set to be Borrowed earlier"), - } + Some(self.make_queue_sync_handle(target, index, handle, bounds)) } fn is_filter_supported(&self, filter: &Filter) -> bool { - matches!(filter, Filter::BlurFilter(_) | Filter::ColorMatrixFilter(_)) + matches!( + filter, + Filter::BlurFilter(_) | Filter::ColorMatrixFilter(_) | Filter::ShaderFilter(_) + ) } fn is_offscreen_supported(&self) -> bool { @@ -804,21 +829,8 @@ impl RenderBackend for WgpuRenderBackend { dest_texture.texture.width(), dest_texture.texture.height(), ); - let buffer_info = if dest_texture.copy_count.get() >= TEXTURE_READS_BEFORE_PROMOTION { - let copy_dimensions = BufferDimensions::new( - dest_texture.texture.width() as usize, - dest_texture.texture.height() as usize, - ); - let buffer = self - .offscreen_buffer_pool - .take(&self.descriptors, copy_dimensions.clone()); - Some(TextureBufferInfo { - buffer: MaybeOwnedBuffer::Borrowed(buffer, copy_dimensions), - copy_area, - }) - } else { - None - }; + + let buffer_info = self.get_texture_buffer_info(dest_texture, copy_area); let mut target = TextureTarget { size: wgpu::Extent3d { @@ -840,6 +852,7 @@ impl RenderBackend for WgpuRenderBackend { .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: label.as_deref(), }); + let applied_filter = self.descriptors.filters.apply( &self.descriptors, &mut draw_encoder, @@ -881,27 +894,7 @@ impl RenderBackend for WgpuRenderBackend { frame_output, ); - match target.take_buffer() { - None => Some(Box::new(QueueSyncHandle::NotCopied { - handle: destination, - copy_area, - descriptors: self.descriptors.clone(), - pool: self.offscreen_buffer_pool.clone(), - })), - Some(TextureBufferInfo { - buffer: MaybeOwnedBuffer::Borrowed(buffer, copy_dimensions), - .. - }) => Some(Box::new(QueueSyncHandle::AlreadyCopied { - index, - buffer, - copy_dimensions, - descriptors: self.descriptors.clone(), - })), - Some(TextureBufferInfo { - buffer: MaybeOwnedBuffer::Owned(..), - .. - }) => unreachable!("Buffer must be Borrowed as it was set to be Borrowed earlier"), - } + Some(self.make_queue_sync_handle(target, index, destination, copy_area)) } fn compile_pixelbender_shader( @@ -917,7 +910,67 @@ impl RenderBackend for WgpuRenderBackend { arguments: &[PixelBenderShaderArgument], target_handle: BitmapHandle, ) -> Result, BitmapError> { - self.run_pixelbender_shader_impl(shader, arguments, target_handle) + let target = as_texture(&target_handle); + + let extent = wgpu::Extent3d { + width: target.texture.width(), + height: target.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()), + ); + + let mut texture_target = TextureTarget { + size: extent, + texture: target.texture.clone(), + format: wgpu::TextureFormat::Rgba8Unorm, + buffer: buffer_info, + }; + + let frame_output = texture_target + .get_next_texture() + .expect("TextureTargetFrame.get_next_texture is infallible"); + + let mut render_command_encoder = + self.descriptors + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: create_debug_label!("Render command encoder").as_deref(), + }); + + run_pixelbender_shader_impl( + &self.descriptors, + shader, + ShaderMode::ShaderJob, + arguments, + &target.texture, + &mut render_command_encoder, + Some(wgpu::RenderPassColorAttachment { + view: frame_output.view(), + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: true, + }, + }), + // When running a standalone shader, we always process the entire image + &FilterSource::for_entire_texture(&target.texture), + )?; + + let index = self + .descriptors + .queue + .submit(Some(render_command_encoder.finish())); + + Ok(self.make_queue_sync_handle( + texture_target, + index, + target_handle, + PixelRegion::for_whole_size(extent.width, extent.height), + )) } fn create_empty_texture( diff --git a/render/wgpu/src/filters.rs b/render/wgpu/src/filters.rs index 34742adba..e56b0d69a 100644 --- a/render/wgpu/src/filters.rs +++ b/render/wgpu/src/filters.rs @@ -1,16 +1,19 @@ mod blur; mod color_matrix; +mod shader; use crate::buffer_pool::TexturePool; use crate::descriptors::Descriptors; use crate::filters::blur::BlurFilter; use crate::filters::color_matrix::ColorMatrixFilter; +use crate::filters::shader::ShaderFilter; use crate::surface::target::CommandTarget; use bytemuck::{Pod, Zeroable}; use ruffle_render::filters::Filter; use wgpu::util::DeviceExt; use wgpu::vertex_attr_array; +#[derive(Debug)] pub struct FilterSource<'a> { pub texture: &'a wgpu::Texture, pub point: (u32, u32), @@ -61,6 +64,7 @@ impl<'a> FilterSource<'a> { pub struct Filters { pub blur: BlurFilter, pub color_matrix: ColorMatrixFilter, + pub shader: ShaderFilter, } impl Filters { @@ -68,6 +72,7 @@ impl Filters { Self { blur: BlurFilter::new(device), color_matrix: ColorMatrixFilter::new(device), + shader: ShaderFilter::new(), } } @@ -94,6 +99,13 @@ impl Filters { &source, &filter, ), + Filter::ShaderFilter(shader) => Some(descriptors.filters.shader.apply( + descriptors, + texture_pool, + draw_encoder, + &source, + shader, + )), _ => { tracing::warn!("Unsupported filter {filter:?}"); None diff --git a/render/wgpu/src/filters/shader.rs b/render/wgpu/src/filters/shader.rs new file mode 100644 index 000000000..b4f0377e1 --- /dev/null +++ b/render/wgpu/src/filters/shader.rs @@ -0,0 +1,72 @@ +use ruffle_render::{ + filters::ShaderFilter as ShaderFilterArgs, + pixel_bender::{ImageInputTexture, PixelBenderShaderArgument}, +}; + +use crate::{ + backend::RenderTargetMode, + buffer_pool::TexturePool, + descriptors::Descriptors, + pixel_bender::{run_pixelbender_shader_impl, ShaderMode}, + surface::target::CommandTarget, +}; + +use super::FilterSource; + +/// All of the data is stored in the `ShaderFilterArgs` +#[derive(Default)] +pub struct ShaderFilter; + +impl ShaderFilter { + pub fn new() -> Self { + Self + } + + #[allow(clippy::too_many_arguments)] + pub fn apply<'a>( + &self, + descriptors: &Descriptors, + texture_pool: &mut TexturePool, + draw_encoder: &mut wgpu::CommandEncoder, + source: &FilterSource<'a>, + mut filter: ShaderFilterArgs<'a>, + ) -> CommandTarget { + let sample_count = source.texture.sample_count(); + let format = source.texture.format(); + + let target = CommandTarget::new( + descriptors, + texture_pool, + wgpu::Extent3d { + width: source.size.0, + height: source.size.1, + depth_or_array_layers: 1, + }, + format, + sample_count, + RenderTargetMode::FreshWithColor(wgpu::Color::TRANSPARENT), + draw_encoder, + ); + + for arg in &mut filter.shader_args { + if let PixelBenderShaderArgument::ImageInput { texture, .. } = arg { + *texture = Some(ImageInputTexture::TextureRef(source.texture)); + // Only bind the first input from the source texture + break; + } + } + + run_pixelbender_shader_impl( + descriptors, + filter.shader, + ShaderMode::Filter, + &filter.shader_args, + target.color_texture(), + draw_encoder, + target.color_attachments(), + source, + ) + .expect("Failed to run pixelbender shader"); + target + } +} diff --git a/render/wgpu/src/lib.rs b/render/wgpu/src/lib.rs index 6514e3246..98325862c 100644 --- a/render/wgpu/src/lib.rs +++ b/render/wgpu/src/lib.rs @@ -14,6 +14,7 @@ use crate::utils::{ use bytemuck::{Pod, Zeroable}; use descriptors::Descriptors; use enum_map::Enum; +use ruffle_render::backend::RawTexture; use ruffle_render::bitmap::{BitmapHandle, BitmapHandleImpl, PixelRegion, RgbaBufRead, SyncHandle}; use ruffle_render::shape_utils::GradientType; use ruffle_render::tessellator::{Gradient as TessGradient, Vertex as TessVertex}; @@ -54,6 +55,10 @@ pub fn as_texture(handle: &BitmapHandle) -> &Texture { ::downcast_ref(&*handle.0).unwrap() } +pub fn raw_texture_as_texture(handle: &dyn RawTexture) -> &wgpu::Texture { + ::downcast_ref(handle).unwrap() +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Enum)] pub enum MaskState { NoMask, diff --git a/render/wgpu/src/pixel_bender.rs b/render/wgpu/src/pixel_bender.rs index 66b2c1b78..49fc8bffd 100644 --- a/render/wgpu/src/pixel_bender.rs +++ b/render/wgpu/src/pixel_bender.rs @@ -5,27 +5,25 @@ use std::{borrow::Cow, cell::Cell, sync::Arc}; use indexmap::IndexMap; use ruffle_render::error::Error as BitmapError; use ruffle_render::pixel_bender::{ - PixelBenderShaderHandle, PixelBenderShaderImpl, PixelBenderType, OUT_COORD_NAME, + ImageInputTexture, PixelBenderShaderHandle, PixelBenderShaderImpl, PixelBenderType, + OUT_COORD_NAME, }; use ruffle_render::{ - bitmap::{BitmapHandle, PixelRegion, SyncHandle}, + bitmap::BitmapHandle, pixel_bender::{PixelBenderParam, PixelBenderShader, PixelBenderShaderArgument}, }; use wgpu::util::StagingBelt; use wgpu::{ BindGroupEntry, BindingResource, BlendComponent, BufferDescriptor, BufferUsages, - ColorTargetState, ColorWrites, FrontFace, ImageCopyTexture, RenderPipelineDescriptor, - SamplerBindingType, ShaderModuleDescriptor, TextureDescriptor, TextureFormat, TextureView, - VertexState, + ColorTargetState, ColorWrites, CommandEncoder, FrontFace, ImageCopyTexture, + RenderPipelineDescriptor, SamplerBindingType, ShaderModuleDescriptor, TextureDescriptor, + TextureFormat, TextureView, VertexState, }; +use crate::filters::{FilterSource, VERTEX_BUFFERS_DESCRIPTION_FILTERS}; +use crate::raw_texture_as_texture; use crate::{ - as_texture, - backend::WgpuRenderBackend, - descriptors::Descriptors, - pipelines::VERTEX_BUFFERS_DESCRIPTION_POS, - target::{RenderTarget, RenderTargetFrame, TextureTarget}, - QueueSyncHandle, Texture, + as_texture, backend::WgpuRenderBackend, descriptors::Descriptors, target::RenderTarget, Texture, }; #[derive(Debug)] @@ -37,6 +35,7 @@ pub struct PixelBenderWgpuShader { float_parameters_buffer_size: u64, int_parameters_buffer: wgpu::Buffer, int_parameters_buffer_size: u64, + zeroed_out_of_range_mode: wgpu::Buffer, staging_belt: RefCell, } @@ -94,6 +93,16 @@ impl PixelBenderWgpuShader { }, count: None, }, + wgpu::BindGroupLayoutEntry { + binding: naga_pixelbender::ZEROED_OUT_OF_RANGE_MODE_INDEX, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ]; for param in &shader.params { @@ -156,6 +165,14 @@ impl PixelBenderWgpuShader { mapped_at_creation: false, }); + let zeroed_out_of_range_mode = descriptors.device.create_buffer(&BufferDescriptor { + label: create_debug_label!("PixelBender zeroed_out_of_range_mode parameter buffer") + .as_deref(), + size: shaders.int_parameters_buffer_size, + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let vertex_shader = descriptors .device .create_shader_module(ShaderModuleDescriptor { @@ -177,12 +194,12 @@ impl PixelBenderWgpuShader { layout: Some(&pipeline_layout), vertex: VertexState { module: &vertex_shader, - entry_point: naga_pixelbender::SHADER_ENTRYPOINT, - buffers: &VERTEX_BUFFERS_DESCRIPTION_POS, + entry_point: naga_pixelbender::VERTEX_SHADER_ENTRYPOINT, + buffers: &VERTEX_BUFFERS_DESCRIPTION_FILTERS, }, fragment: Some(wgpu::FragmentState { module: &fragment_shader, - entry_point: naga_pixelbender::SHADER_ENTRYPOINT, + entry_point: naga_pixelbender::FRAGMENT_SHADER_ENTRYPOINT, targets: &[Some(ColorTargetState { format: TextureFormat::Rgba8Unorm, // FIXME - what should this be? @@ -215,12 +232,20 @@ impl PixelBenderWgpuShader { float_parameters_buffer_size: shaders.float_parameters_buffer_size, int_parameters_buffer, int_parameters_buffer_size: shaders.int_parameters_buffer_size, + zeroed_out_of_range_mode, // FIXME - come up with a good chunk size staging_belt: RefCell::new(StagingBelt::new(8)), } } } +fn image_input_as_texture<'a>(input: &'a ImageInputTexture<'a>) -> &wgpu::Texture { + match input { + ImageInputTexture::Bitmap(handle) => &as_texture(handle).texture, + ImageInputTexture::TextureRef(raw_texture) => raw_texture_as_texture(*raw_texture), + } +} + impl WgpuRenderBackend { pub(super) fn compile_pixelbender_shader_impl( &mut self, @@ -229,262 +254,277 @@ impl WgpuRenderBackend { let handle = PixelBenderWgpuShader::new(&self.descriptors, shader); Ok(PixelBenderShaderHandle(Arc::new(handle))) } +} - pub(super) fn run_pixelbender_shader_impl( - &mut self, - shader: PixelBenderShaderHandle, - arguments: &[PixelBenderShaderArgument], - target_handle: BitmapHandle, - ) -> Result, BitmapError> { - let compiled_shader = &as_cache_holder(&shader); - let mut staging_belt = compiled_shader.staging_belt.borrow_mut(); +pub enum ShaderMode { + ShaderJob, + Filter, +} - let mut arguments = arguments.to_vec(); +#[allow(clippy::too_many_arguments)] +pub(super) fn run_pixelbender_shader_impl( + descriptors: &Descriptors, + shader: PixelBenderShaderHandle, + mode: ShaderMode, + arguments: &[PixelBenderShaderArgument], + target: &wgpu::Texture, + render_command_encoder: &mut CommandEncoder, + color_attachment: Option, + // FIXME - do we cover the whole source or the whole dest? + source: &FilterSource, +) -> Result<(), BitmapError> { + let compiled_shader = &as_cache_holder(&shader); + let mut staging_belt = compiled_shader.staging_belt.borrow_mut(); - let target = as_texture(&target_handle); - let extent = wgpu::Extent3d { - width: target.texture.width(), - height: target.texture.height(), - depth_or_array_layers: 1, - }; + let mut arguments = arguments.to_vec(); - let mut texture_target = TextureTarget { - size: extent, - texture: target.texture.clone(), - format: wgpu::TextureFormat::Rgba8Unorm, - buffer: None, - }; + let mut bind_group_entries = vec![ + BindGroupEntry { + binding: naga_pixelbender::SAMPLER_CLAMP_NEAREST, + resource: BindingResource::Sampler(&descriptors.bitmap_samplers.clamp_nearest), + }, + BindGroupEntry { + binding: naga_pixelbender::SAMPLER_CLAMP_LINEAR, + resource: BindingResource::Sampler(&descriptors.bitmap_samplers.clamp_linear), + }, + BindGroupEntry { + binding: naga_pixelbender::SAMPLER_CLAMP_BILINEAR, + // FIXME - create bilinear sampler + resource: BindingResource::Sampler(&descriptors.bitmap_samplers.clamp_linear), + }, + BindGroupEntry { + binding: naga_pixelbender::SHADER_FLOAT_PARAMETERS_INDEX, + resource: BindingResource::Buffer(wgpu::BufferBinding { + buffer: &compiled_shader.float_parameters_buffer, + offset: 0, + size: Some(NonZeroU64::new(compiled_shader.float_parameters_buffer_size).unwrap()), + }), + }, + BindGroupEntry { + binding: naga_pixelbender::SHADER_INT_PARAMETERS_INDEX, + resource: BindingResource::Buffer(wgpu::BufferBinding { + buffer: &compiled_shader.int_parameters_buffer, + offset: 0, + size: Some(NonZeroU64::new(compiled_shader.int_parameters_buffer_size).unwrap()), + }), + }, + BindGroupEntry { + binding: naga_pixelbender::ZEROED_OUT_OF_RANGE_MODE_INDEX, + resource: BindingResource::Buffer(wgpu::BufferBinding { + buffer: &compiled_shader.zeroed_out_of_range_mode, + offset: 0, + size: Some(NonZeroU64::new(std::mem::size_of::() as u64).unwrap()), + }), + }, + ]; - let frame_output = texture_target - .get_next_texture() - .expect("TextureTargetFrame.get_next_texture is infallible"); + let mut zeroed_out_of_range_mode_slice = staging_belt.write_buffer( + render_command_encoder, + &compiled_shader.zeroed_out_of_range_mode, + 0, + NonZeroU64::new(std::mem::size_of::() as u64).unwrap(), + &descriptors.device, + ); - let mut bind_group_entries = vec![ - BindGroupEntry { - binding: naga_pixelbender::SAMPLER_CLAMP_NEAREST, - resource: BindingResource::Sampler(&self.descriptors.bitmap_samplers.clamp_nearest), - }, - BindGroupEntry { - binding: naga_pixelbender::SAMPLER_CLAMP_LINEAR, - resource: BindingResource::Sampler(&self.descriptors.bitmap_samplers.clamp_linear), - }, - BindGroupEntry { - binding: naga_pixelbender::SAMPLER_CLAMP_BILINEAR, - // FIXME - create bilinear sampler - resource: BindingResource::Sampler(&self.descriptors.bitmap_samplers.clamp_linear), - }, - BindGroupEntry { - binding: naga_pixelbender::SHADER_FLOAT_PARAMETERS_INDEX, - resource: BindingResource::Buffer(wgpu::BufferBinding { - buffer: &compiled_shader.float_parameters_buffer, - offset: 0, - size: Some( - NonZeroU64::new(compiled_shader.float_parameters_buffer_size).unwrap(), - ), - }), - }, - BindGroupEntry { - binding: naga_pixelbender::SHADER_INT_PARAMETERS_INDEX, - resource: BindingResource::Buffer(wgpu::BufferBinding { - buffer: &compiled_shader.int_parameters_buffer, - offset: 0, - size: Some( - NonZeroU64::new(compiled_shader.int_parameters_buffer_size).unwrap(), - ), - }), - }, - ]; + zeroed_out_of_range_mode_slice.copy_from_slice(bytemuck::cast_slice(&[match mode { + // When a Shader is run via a ShaderJob, out-of-range texture sample coordinates + // seem to be clamped to the edge of the texture (despite what the docs describe) + ShaderMode::ShaderJob => 0.0f32, + // When a Shader is run through a ShaderFilter, out-of-range texture sample coordinates + // return transparent black (0.0, 0.0, 0.0, 0.0). This is easiest to observe with + // BitmapData.applyFilter when the BitampData destination is larger than the source. + ShaderMode::Filter => 1.0f32, + }])); + drop(zeroed_out_of_range_mode_slice); - let mut texture_views: IndexMap = Default::default(); + let mut texture_views: IndexMap = Default::default(); - let mut render_command_encoder = - self.descriptors - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: create_debug_label!("Render command encoder").as_deref(), - }); + let mut target_clone = None; - let mut target_clone = None; + let mut float_offset = 0; + let mut int_offset = 0; - let mut float_offset = 0; - let mut int_offset = 0; + let mut first_image = None; - for input in &mut arguments { - match input { - PixelBenderShaderArgument::ImageInput { index, texture, .. } => { + 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) { // 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 - if std::ptr::eq( - Arc::as_ptr(&texture.0) as *const (), - Arc::as_ptr(&target_handle.0) as *const (), - ) { - let cached_fresh_handle = target_clone.get_or_insert_with(|| { - let extent = wgpu::Extent3d { - width: target.texture.width(), - height: target.texture.height(), - depth_or_array_layers: 1, - }; - let fresh_texture = - self.descriptors.device.create_texture(&TextureDescriptor { - label: Some("PixelBenderShader target clone"), - size: extent, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[wgpu::TextureFormat::Rgba8Unorm], - }); - render_command_encoder.copy_texture_to_texture( - ImageCopyTexture { - texture: &target.texture, - mip_level: 0, - origin: Default::default(), - aspect: Default::default(), - }, - ImageCopyTexture { - texture: &fresh_texture, - mip_level: 0, - origin: Default::default(), - aspect: Default::default(), - }, - extent, - ); - - BitmapHandle(Arc::new(Texture { - texture: Arc::new(fresh_texture), - bind_linear: Default::default(), - bind_nearest: Default::default(), - copy_count: Cell::new(0), - })) + let cached_fresh_handle = target_clone.get_or_insert_with(|| { + let extent = wgpu::Extent3d { + width: target.width(), + height: target.height(), + depth_or_array_layers: 1, + }; + let fresh_texture = descriptors.device.create_texture(&TextureDescriptor { + label: Some("PixelBenderShader target clone"), + size: extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[wgpu::TextureFormat::Rgba8Unorm], }); - *texture = cached_fresh_handle.clone(); - } - texture_views.insert( - *index, - as_texture(texture) - .texture - .create_view(&wgpu::TextureViewDescriptor::default()), - ); - } - PixelBenderShaderArgument::ValueInput { index, value } => { - let param = &compiled_shader.shader.params[*index as usize]; + render_command_encoder.copy_texture_to_texture( + ImageCopyTexture { + texture: target, + mip_level: 0, + origin: Default::default(), + aspect: Default::default(), + }, + ImageCopyTexture { + texture: &fresh_texture, + mip_level: 0, + origin: Default::default(), + aspect: Default::default(), + }, + extent, + ); - let name = match param { - PixelBenderParam::Normal { name, .. } => name, - _ => unreachable!(), - }; - - if name == OUT_COORD_NAME { - continue; - } - - let (value_vec, is_float): ([f32; 4], bool) = match value { - PixelBenderType::TFloat(f1) => ([*f1, 0.0, 0.0, 0.0], true), - PixelBenderType::TFloat2(f1, f2) => ([*f1, *f2, 0.0, 0.0], true), - PixelBenderType::TFloat3(f1, f2, f3) => ([*f1, *f2, *f3, 0.0], true), - PixelBenderType::TFloat4(f1, f2, f3, f4) => ([*f1, *f2, *f3, *f4], true), - PixelBenderType::TInt(i1) => ([*i1 as f32, 0.0, 0.0, 0.0], false), - PixelBenderType::TInt2(i1, i2) => { - ([*i1 as f32, *i2 as f32, 0.0, 0.0], false) - } - PixelBenderType::TInt3(i1, i2, i3) => { - ([*i1 as f32, *i2 as f32, *i3 as f32, 0.0], false) - } - PixelBenderType::TInt4(i1, i2, i3, i4) => { - ([*i1 as f32, *i2 as f32, *i3 as f32, *i4 as f32], false) - } - _ => unreachable!("Unimplemented value {value:?}"), - }; - - // Both float32 and int are 4 bytes - let component_size_bytes = 4; - - let (buffer, vec4_count) = if is_float { - let res = (&compiled_shader.float_parameters_buffer, float_offset); - float_offset += 1; - res - } else { - let res = (&compiled_shader.int_parameters_buffer, int_offset); - int_offset += 1; - res - }; - - let mut buffer_slice = staging_belt.write_buffer( - &mut render_command_encoder, - buffer, - vec4_count * 4 * component_size_bytes, - NonZeroU64::new(4 * component_size_bytes).unwrap(), - &self.descriptors.device, - ); - buffer_slice.copy_from_slice(bytemuck::cast_slice(&value_vec)); - } - } - } - - // This needs to be a separate loop, so that we can get references into `texture_views` - for input in &arguments { - match input { - PixelBenderShaderArgument::ImageInput { index, .. } => { - let binding = naga_pixelbender::TEXTURE_START_BIND_INDEX + *index as u32; - bind_group_entries.push(BindGroupEntry { - binding, - resource: BindingResource::TextureView(&texture_views[index]), + BitmapHandle(Arc::new(Texture { + texture: Arc::new(fresh_texture), + bind_linear: Default::default(), + bind_nearest: Default::default(), + copy_count: Cell::new(0), + })) }); + *texture = Some(cached_fresh_handle.clone().into()); } - PixelBenderShaderArgument::ValueInput { .. } => {} + let wgpu_texture = image_input_as_texture(texture.as_ref().unwrap()); + texture_views.insert( + *index, + wgpu_texture.create_view(&wgpu::TextureViewDescriptor::default()), + ); + } + PixelBenderShaderArgument::ValueInput { index, value } => { + let param = &compiled_shader.shader.params[*index as usize]; + + let name = match param { + PixelBenderParam::Normal { name, .. } => name, + _ => unreachable!(), + }; + + if name == OUT_COORD_NAME { + continue; + } + + let (value_vec, is_float): (Vec, bool) = match value { + PixelBenderType::TFloat(f1) => (vec![*f1, 0.0, 0.0, 0.0], true), + PixelBenderType::TFloat2(f1, f2) => (vec![*f1, *f2, 0.0, 0.0], true), + PixelBenderType::TFloat3(f1, f2, f3) => (vec![*f1, *f2, *f3, 0.0], true), + PixelBenderType::TFloat4(f1, f2, f3, f4) => (vec![*f1, *f2, *f3, *f4], true), + PixelBenderType::TInt(i1) => (vec![*i1 as f32, 0.0, 0.0, 0.0], false), + PixelBenderType::TInt2(i1, i2) => { + (vec![*i1 as f32, *i2 as f32, 0.0, 0.0], false) + } + PixelBenderType::TInt3(i1, i2, i3) => { + (vec![*i1 as f32, *i2 as f32, *i3 as f32, 0.0], false) + } + PixelBenderType::TInt4(i1, i2, i3, i4) => { + (vec![*i1 as f32, *i2 as f32, *i3 as f32, *i4 as f32], false) + } + PixelBenderType::TFloat2x2(arr) => (arr.to_vec(), true), + PixelBenderType::TFloat3x3(arr) => { + // Add a zero after every 3 values to created zero-padded vec4s + let mut vec4_arr = Vec::with_capacity(16); + for (i, val) in arr.iter().enumerate() { + vec4_arr.push(*val); + if i % 3 == 2 { + vec4_arr.push(0.0); + } + } + (vec4_arr, true) + } + PixelBenderType::TFloat4x4(arr) => (arr.to_vec(), true), + _ => unreachable!("Unimplemented value {value:?}"), + }; + + assert_eq!( + value_vec.len() % 4, + 0, + "value_vec should represent concatenated vec4fs" + ); + let num_vec4s = value_vec.len() / 4; + // Both float32 and int are 4 bytes + let component_size_bytes = 4; + + let (buffer, vec4_count) = if is_float { + let res = (&compiled_shader.float_parameters_buffer, float_offset); + float_offset += num_vec4s; + res + } else { + let res = (&compiled_shader.int_parameters_buffer, int_offset); + int_offset += num_vec4s; + res + }; + + let mut buffer_slice = staging_belt.write_buffer( + render_command_encoder, + buffer, + vec4_count as u64 * 4 * component_size_bytes, + NonZeroU64::new(value_vec.len() as u64 * component_size_bytes).unwrap(), + &descriptors.device, + ); + buffer_slice.copy_from_slice(bytemuck::cast_slice(&value_vec)); } } - - let bind_group = self - .descriptors - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &compiled_shader.bind_group_layout, - entries: &bind_group_entries, - }); - - staging_belt.finish(); - - let mut render_pass = - render_command_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("PixelBender render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: frame_output.view(), - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - store: true, - }, - })], - depth_stencil_attachment: None, - }); - render_pass.set_bind_group(0, &bind_group, &[]); - render_pass.set_pipeline(&compiled_shader.pipeline); - - render_pass.set_vertex_buffer(0, self.descriptors.quad.vertices_pos.slice(..)); - render_pass.set_index_buffer( - self.descriptors.quad.indices.slice(..), - wgpu::IndexFormat::Uint32, - ); - - render_pass.draw_indexed(0..6, 0, 0..1); - - drop(render_pass); - - self.descriptors - .queue - .submit(Some(render_command_encoder.finish())); - - staging_belt.recall(); - - Ok(Box::new(QueueSyncHandle::NotCopied { - handle: target_handle, - copy_area: PixelRegion::for_whole_size(extent.width, extent.height), - descriptors: self.descriptors.clone(), - pool: self.offscreen_buffer_pool.clone(), - })) } + + // This needs to be a separate loop, so that we can get references into `texture_views` + for input in &arguments { + match input { + PixelBenderShaderArgument::ImageInput { index, texture, .. } => { + let wgpu_texture = image_input_as_texture(texture.as_ref().unwrap()); + + if first_image.is_none() { + first_image = Some(wgpu_texture); + } + + let binding = naga_pixelbender::TEXTURE_START_BIND_INDEX + *index as u32; + bind_group_entries.push(BindGroupEntry { + binding, + resource: BindingResource::TextureView(&texture_views[index]), + }); + } + PixelBenderShaderArgument::ValueInput { .. } => {} + } + } + + let bind_group = descriptors + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &compiled_shader.bind_group_layout, + entries: &bind_group_entries, + }); + + staging_belt.finish(); + + let vertices = source.vertices(&descriptors.device); + + let mut render_pass = render_command_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("PixelBender render pass"), + color_attachments: &[color_attachment], + depth_stencil_attachment: None, + }); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.set_pipeline(&compiled_shader.pipeline); + + render_pass.set_vertex_buffer(0, vertices.slice(..)); + render_pass.set_index_buffer( + descriptors.quad.indices.slice(..), + wgpu::IndexFormat::Uint32, + ); + + render_pass.draw_indexed(0..6, 0, 0..1); + + // Note - we just drop the staging belt, instead of recalling it, + // since we're not going to use it again. + + Ok(()) } diff --git a/render/wgpu/src/shaders.rs b/render/wgpu/src/shaders.rs index 098b70113..7179232d9 100644 --- a/render/wgpu/src/shaders.rs +++ b/render/wgpu/src/shaders.rs @@ -132,8 +132,8 @@ fn composer() -> Result { ..Default::default() })?; composer.add_composable_module(ComposableModuleDescriptor { - source: include_str!("../shaders/filter/common.wgsl"), - file_path: "filter/common.wgsl", + source: ruffle_render::shader_source::SHADER_FILTER_COMMON, + file_path: "shader_filter_common.wgsl", ..Default::default() })?; Ok(composer) diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/Test.as b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/Test.as new file mode 100755 index 000000000..0567ad162 --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/Test.as @@ -0,0 +1,40 @@ +package { + import flash.display.BitmapData; + import flash.display.ShaderJob; + import flash.display.Shader; + import flash.display.Bitmap; + import flash.display.MovieClip; + import flash.display.ShaderParameter; + + public class Test { + + [Embed(source = "YellowFlowers.png")] + public static var FLOWERS: Class; + + [Embed(source = "mandelbrot.png")] + public static var MANDELBROT: Class; + + // Shader from + [Embed(source = "glassDisplace.pbj", mimeType="application/octet-stream")] + public static var GLASSDISPLACE_BYTES: Class; + + public function Test(main: MovieClip) { + main.stage.scaleMode = "noScale"; + var flowers: Bitmap = new FLOWERS(); + var mandelbrot: Bitmap = new MANDELBROT(); + main.addChild(new Bitmap(glassDisplace(flowers.bitmapData.clone(), mandelbrot.bitmapData.clone()))); + } + + private function glassDisplace(input1: BitmapData, input2): BitmapData { + var out = new BitmapData(Math.max(input1.width, input2.width), Math.max(input1.height, input2.height), true, 0xFF00FF00); + var shader = new ShaderJob(new Shader(new GLASSDISPLACE_BYTES()), out); + shader.shader.data.center.value = [80, 420]; + shader.shader.data.stretch.value = [180, 20]; + shader.shader.data.alpha.value = [0.27]; + shader.shader.data.src.input = input1; + shader.shader.data.src2.input = input2; + shader.start(true); + return out; + } + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/YellowFlowers.png b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/YellowFlowers.png new file mode 100755 index 000000000..8603f4ddd Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/YellowFlowers.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/expected.png b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/expected.png new file mode 100644 index 000000000..bc29cb44b Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/expected.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/glassDisplace.pbj b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/glassDisplace.pbj new file mode 100755 index 000000000..39c3f18bd Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/glassDisplace.pbj differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/glassDisplace.pbk b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/glassDisplace.pbk new file mode 100755 index 000000000..c26b43f79 --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/glassDisplace.pbk @@ -0,0 +1,43 @@ +// Based on https://github.com/nojvek/pixelbender/blob/master/selfDisplace/glassDisplace.pbk + + +kernel NewFilter +< namespace : "com.om-labs.pixelbender.selfDisplace"; + vendor : "Om Labs"; + version : 1; + description : "depending on the color values of the image,displacement, centrepoint and stretch, create freaky images."; +> +{ + + parameter float2 center< + minValue:float2(-1000.0); + maxValue:float2(1000.0); + defaultValue:float2(500.0,400.0); + >; + + parameter float2 stretch< + minValue: float2(-1000.0); + maxValue: float2(1000.0); + defaultValue: float2(0.0); + >; + + parameter float alpha< + minValue:0.0; + maxValue:1.0; + defaultValue:0.0; + >; + + input image4 src; + input image4 src2; + output float4 dst; + + void evaluatePixel(){ + float2 pos = outCoord(); + float4 heightColor = sampleNearest(src,pos); + float height = (heightColor.r+heightColor.g+heightColor.b)/3.0; + float2 direction = normalize(pos-center); + pos += float2(-height*direction.x*stretch.x,-height*direction.y*stretch.y); + dst = mix(sampleNearest(src,pos),sampleNearest(src2,pos),alpha); + + } +} diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/mandelbrot.png b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/mandelbrot.png new file mode 100755 index 000000000..7f169b437 Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/mandelbrot.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/output.txt b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/output.txt new file mode 100755 index 000000000..e69de29bb diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/test.fla b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/test.fla new file mode 100755 index 000000000..e95ca578e Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/test.fla differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/test.swf b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/test.swf new file mode 100755 index 000000000..aad673e1b Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/test.swf differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/test.toml b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/test.toml new file mode 100755 index 000000000..0f1e9a8e7 --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace/test.toml @@ -0,0 +1,9 @@ +num_frames = 1 + +[image_comparison] +tolerance = 3 +max_outliers = 1003 + +[player_options] +viewport_dimensions = { width = 600, height = 700, scale_factor = 1 } +with_renderer = { optional = false, sample_count = 1 } diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/Test.as b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/Test.as new file mode 100755 index 000000000..3c0f6a393 --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/Test.as @@ -0,0 +1,76 @@ +package { + import flash.display.BitmapData; + import flash.display.ShaderJob; + import flash.display.Shader; + import flash.display.Bitmap; + import flash.display.MovieClip; + import flash.display.ShaderParameter; + import flash.geom.Rectangle; + import flash.geom.Point; + import flash.filters.ShaderFilter; + + public class Test { + + [Embed(source = "YellowFlowers.png")] + public static var FLOWERS: Class; + + [Embed(source = "mandelbrot.png")] + public static var MANDELBROT: Class; + + // Shader from + [Embed(source = "glassDisplace.pbj", mimeType="application/octet-stream")] + public static var GLASSDISPLACE_BYTES: Class; + + public function Test(main: MovieClip) { + //main.stage.scaleMode = "noScale"; + var flowers: Bitmap = new FLOWERS(); + var mandelbrot: Bitmap = new MANDELBROT(); + var shader = glassDisplace(mandelbrot.bitmapData); + + var width = Math.max(flowers.width, mandelbrot.width); + var height = Math.max(flowers.height, mandelbrot.height); + + + trace("Flowers rect: " + flowers.bitmapData.rect); + + var out1 = new Bitmap(flowers.bitmapData.clone()); + var out2 = new Bitmap(new BitmapData(width, height, true, 0xFF0000FF)); + //var out2 = new Bitmap(new BitmapData(flowers.bitmapData.width, flowers.bitmapData.height, true, 0xFF0000FF)); + + var filter = new ShaderFilter(shader); + + out1.filters = [filter]; + + trace("ShaderFilter equal: " + (out1.filters[0] === filter)); + trace("Shader equal: " + (out1.filters[0].shader === filter.shader)); + + trace("Dest rect: " + out2.bitmapData.generateFilterRect(new Rectangle(100, 10, 400, 20), filter)); + out2.bitmapData.applyFilter(flowers.bitmapData, new Rectangle(0, 0, 20, 20), new Point(0, 0), filter); + out2.y = 390; + + + main.addChild(out1); + main.addChild(out2); + } + + private function glassDisplace(input2: BitmapData): Shader { + // This should be unused, since it's bounded to the first image input + // (which gets overwritten when applying ShaderFilter) + var fake = new BitmapData(300, 100, true, 0xFFFF0000); + var shader = new Shader(new GLASSDISPLACE_BYTES()); + shader.data.center.value = [80, 420]; + shader.data.stretch.value = [180, 20]; + + + // Uncomment the following lines to simplify the shader output + // to make comparisons between Ruffle and Flash easier. + //shader.data.center.value =[0, 0]; + //shader.data.stretch.value = [0, 0]; + + shader.data.alpha.value = [1.0]; + shader.data.src.input = fake; + shader.data.src2.input = input2; + return shader; + } + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/YellowFlowers.png b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/YellowFlowers.png new file mode 100755 index 000000000..8603f4ddd Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/YellowFlowers.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/expected.png b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/expected.png new file mode 100644 index 000000000..9eec9d16b Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/expected.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/glassDisplace.pbj b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/glassDisplace.pbj new file mode 100755 index 000000000..39c3f18bd Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/glassDisplace.pbj differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/glassDisplace.pbk b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/glassDisplace.pbk new file mode 100755 index 000000000..c26b43f79 --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/glassDisplace.pbk @@ -0,0 +1,43 @@ +// Based on https://github.com/nojvek/pixelbender/blob/master/selfDisplace/glassDisplace.pbk + + +kernel NewFilter +< namespace : "com.om-labs.pixelbender.selfDisplace"; + vendor : "Om Labs"; + version : 1; + description : "depending on the color values of the image,displacement, centrepoint and stretch, create freaky images."; +> +{ + + parameter float2 center< + minValue:float2(-1000.0); + maxValue:float2(1000.0); + defaultValue:float2(500.0,400.0); + >; + + parameter float2 stretch< + minValue: float2(-1000.0); + maxValue: float2(1000.0); + defaultValue: float2(0.0); + >; + + parameter float alpha< + minValue:0.0; + maxValue:1.0; + defaultValue:0.0; + >; + + input image4 src; + input image4 src2; + output float4 dst; + + void evaluatePixel(){ + float2 pos = outCoord(); + float4 heightColor = sampleNearest(src,pos); + float height = (heightColor.r+heightColor.g+heightColor.b)/3.0; + float2 direction = normalize(pos-center); + pos += float2(-height*direction.x*stretch.x,-height*direction.y*stretch.y); + dst = mix(sampleNearest(src,pos),sampleNearest(src2,pos),alpha); + + } +} diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/mandelbrot.png b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/mandelbrot.png new file mode 100755 index 000000000..7f169b437 Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/mandelbrot.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/output.txt b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/output.txt new file mode 100755 index 000000000..c6a91b865 --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/output.txt @@ -0,0 +1,4 @@ +Flowers rect: (x=0, y=0, w=500, h=375) +ShaderFilter equal: false +Shader equal: true +Dest rect: (x=0, y=0, w=512, h=512) diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.fla b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.fla new file mode 100755 index 000000000..a7d408ffc Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.fla differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.swf b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.swf new file mode 100755 index 000000000..6943348f0 Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.swf differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.toml b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.toml new file mode 100755 index 000000000..0125c41fa --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_glassDisplace_shaderfilter/test.toml @@ -0,0 +1,8 @@ +num_frames = 10 + +[image_comparison] +tolerance = 3 +max_outliers = 380 + +[player_options] +with_renderer = { optional = false, sample_count = 1 } diff --git a/tests/tests/swfs/avm2/pixelbender_effect_smudge/Test.as b/tests/tests/swfs/avm2/pixelbender_effect_smudge/Test.as new file mode 100755 index 000000000..65052349d --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_smudge/Test.as @@ -0,0 +1,35 @@ +package { + import flash.display.BitmapData; + import flash.display.ShaderJob; + import flash.display.Shader; + import flash.display.Bitmap; + import flash.display.MovieClip; + + public class Test { + + [Embed(source = "YellowFlowers.png")] + public static var FLOWERS: Class; + + // Shader from + [Embed(source = "smudge.pbj", mimeType="application/octet-stream")] + public static var SMUDGE_BYTES: Class; + + public function Test(main: MovieClip) { + var flowers: Bitmap = new FLOWERS(); + main.addChild(new Bitmap(smudge(flowers.bitmapData.clone()))); + } + + private function smudge(input: BitmapData): BitmapData { + var shader = new ShaderJob(new Shader(new SMUDGE_BYTES()), input); + shader.shader.data.bBox.value = [210, 200, 0, 260]; + shader.shader.data.exponent.value = [-7.2]; + shader.shader.data.factor.value = [-6.4]; + shader.shader.data.center.value = [-1.12, 0.5]; + shader.shader.data.size.value = [1.02]; + shader.shader.data.smudge.value = [0.38]; + shader.shader.data.src.input = input; + shader.start(true); + return input + } + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/pixelbender_effect_smudge/YellowFlowers.png b/tests/tests/swfs/avm2/pixelbender_effect_smudge/YellowFlowers.png new file mode 100755 index 000000000..8603f4ddd Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_smudge/YellowFlowers.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_smudge/expected.png b/tests/tests/swfs/avm2/pixelbender_effect_smudge/expected.png new file mode 100644 index 000000000..c9da8008f Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_smudge/expected.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_smudge/output.txt b/tests/tests/swfs/avm2/pixelbender_effect_smudge/output.txt new file mode 100755 index 000000000..e69de29bb diff --git a/tests/tests/swfs/avm2/pixelbender_effect_smudge/smudge.pbj b/tests/tests/swfs/avm2/pixelbender_effect_smudge/smudge.pbj new file mode 100755 index 000000000..7450b3b2c Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_smudge/smudge.pbj differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_smudge/smudge.pbk b/tests/tests/swfs/avm2/pixelbender_effect_smudge/smudge.pbk new file mode 100755 index 000000000..b16aa27e5 --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_smudge/smudge.pbk @@ -0,0 +1,73 @@ +// Based on https://github.com/hoojaoh/PhotoFilterShaders/blob/master/Smudge.pbk + + +kernel Smudge +< namespace : "Smudge"; + vendor : "Paperless Post"; + version : 1; +> +{ + input image4 src; + output pixel4 dst; + + //left, right, top, bottom + parameter float4 bBox + < + minValue:float4(0.0,0.0,0.0,0.0); + maxValue:float4(1000.0,1000.0,1000.0,1000.0); + defaultValue:float4(0.0,600.0,0.0,400.0); + >; + + parameter float exponent + < + minValue:float(-10.0); + maxValue:float(10.0); + defaultValue:float(0.0); + >; + + parameter float factor + < + minValue:float(-10.0); + maxValue:float(10.0); + defaultValue:float(0.0); + >; + + parameter float2 center + < + minValue:float2(-2.0,-2.0); + maxValue:float2(2.0,2.0); + defaultValue:float2(0.5,0.5); + >; + + //controls size of the gradient + parameter float size + < + minValue:float(0); + maxValue:float(2); + defaultValue:float(1); + >; + + //smudge factor + parameter float smudge + < + minValue:float(0.0); + maxValue:float(1.0); + defaultValue:float(0.0); + >; + + void + evaluatePixel() + { + dst = sampleNearest(src, outCoord()); + float alpha = dst.a; + float2 centerPos = float2(mix(bBox[0],bBox[1],center[0]),mix(bBox[2],bBox[3],center[1])); + float dist = distance(centerPos,outCoord()); + dist = dist * (1.0/size); + dist = dist/distance(centerPos,float2(bBox[0],bBox[3])); // normalize the distance between 0 and 1 + dist = 1.0 - factor * pow(dist,exponent); // darken colors in relation to distance from center + dist *= smudge; + + dst *= 1.0 + dist; + dst.a = alpha; + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/pixelbender_effect_smudge/test.fla b/tests/tests/swfs/avm2/pixelbender_effect_smudge/test.fla new file mode 100755 index 000000000..e95ca578e Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_smudge/test.fla differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_smudge/test.swf b/tests/tests/swfs/avm2/pixelbender_effect_smudge/test.swf new file mode 100755 index 000000000..cd7de7569 Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_smudge/test.swf differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_smudge/test.toml b/tests/tests/swfs/avm2/pixelbender_effect_smudge/test.toml new file mode 100755 index 000000000..d8a3f915a --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_smudge/test.toml @@ -0,0 +1,7 @@ +num_frames = 1 + +[image_comparison] +tolerance = 1 + +[player_options] +with_renderer = { optional = false, sample_count = 1 } diff --git a/tests/tests/swfs/avm2/pixelbender_effect_tintype/Test.as b/tests/tests/swfs/avm2/pixelbender_effect_tintype/Test.as new file mode 100755 index 000000000..b27a64f11 --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_tintype/Test.as @@ -0,0 +1,35 @@ +package { + import flash.display.BitmapData; + import flash.display.ShaderJob; + import flash.display.Shader; + import flash.display.Bitmap; + import flash.display.MovieClip; + + public class Test { + + [Embed(source = "YellowFlowers.png")] + public static var FLOWERS: Class; + + // Shader from + [Embed(source = "tintype.pbj", mimeType="application/octet-stream")] + public static var TINTYPE_BYTES: Class; + + public function Test(main: MovieClip) { + var flowers: Bitmap = new FLOWERS(); + main.addChild(new Bitmap(tintype(flowers.bitmapData.clone()))); + } + + private function tintype(input: BitmapData): BitmapData { + var shader = new ShaderJob(new Shader(new TINTYPE_BYTES()), input); + shader.shader.data.grayScale.value = [ + 0.9, 0.6094, 0.082, + 0.3086, 0.8, 0.082, + 0.3086, 1.2, 0.7]; + shader.shader.data.contrast.value = [1.83]; + shader.shader.data.mid.value = [1]; + shader.shader.data.src.input = input; + shader.start(true); + return input + } + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/pixelbender_effect_tintype/YellowFlowers.png b/tests/tests/swfs/avm2/pixelbender_effect_tintype/YellowFlowers.png new file mode 100755 index 000000000..8603f4ddd Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_tintype/YellowFlowers.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_tintype/expected.png b/tests/tests/swfs/avm2/pixelbender_effect_tintype/expected.png new file mode 100644 index 000000000..555444921 Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_tintype/expected.png differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_tintype/output.txt b/tests/tests/swfs/avm2/pixelbender_effect_tintype/output.txt new file mode 100755 index 000000000..e69de29bb diff --git a/tests/tests/swfs/avm2/pixelbender_effect_tintype/test.fla b/tests/tests/swfs/avm2/pixelbender_effect_tintype/test.fla new file mode 100755 index 000000000..e95ca578e Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_tintype/test.fla differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_tintype/test.swf b/tests/tests/swfs/avm2/pixelbender_effect_tintype/test.swf new file mode 100755 index 000000000..1e1063d51 Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_tintype/test.swf differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_tintype/test.toml b/tests/tests/swfs/avm2/pixelbender_effect_tintype/test.toml new file mode 100755 index 000000000..d8a3f915a --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_tintype/test.toml @@ -0,0 +1,7 @@ +num_frames = 1 + +[image_comparison] +tolerance = 1 + +[player_options] +with_renderer = { optional = false, sample_count = 1 } diff --git a/tests/tests/swfs/avm2/pixelbender_effect_tintype/tintype.pbj b/tests/tests/swfs/avm2/pixelbender_effect_tintype/tintype.pbj new file mode 100755 index 000000000..4e1346555 Binary files /dev/null and b/tests/tests/swfs/avm2/pixelbender_effect_tintype/tintype.pbj differ diff --git a/tests/tests/swfs/avm2/pixelbender_effect_tintype/tintype.pbk b/tests/tests/swfs/avm2/pixelbender_effect_tintype/tintype.pbk new file mode 100755 index 000000000..249476be7 --- /dev/null +++ b/tests/tests/swfs/avm2/pixelbender_effect_tintype/tintype.pbk @@ -0,0 +1,45 @@ +// Based on https://github.com/hoojaoh/PhotoFilterShaders/blob/master/TinType.pbk + + +kernel TinType +< namespace : "TinType"; + vendor : "Paperless Post"; + version : 1; +> +{ + input image4 src; + output pixel4 dst; + + parameter float3x3 grayScale + < + defaultValue:float3x3(.3086,.6094,.0820, + .3086,.6094,.0820, + .3086,.6094,.0820); + >; + + parameter float contrast + < + minValue:float(0.0); + maxValue:float(3.0); + defaultValue:float(1.0); + >; + + parameter float mid + < + minValue:float(0.0); + maxValue:float(1.0); + defaultValue:float(0.5); + >; + + void + evaluatePixel() + { + float2 pos = outCoord(); + dst = sampleNearest(src,pos); + dst.rgb = dst.rgb * grayScale; + dst.r = ((dst.r - mid) * contrast) + mid; + dst.g = ((dst.g - mid) * contrast) + mid; + dst.b = ((dst.b - mid) * contrast) + mid; + + } +} diff --git a/tests/tests/swfs/avm2/pixelbender_images/test.swf b/tests/tests/swfs/avm2/pixelbender_images/test.swf index cab75ce7f..3258ef381 100755 Binary files a/tests/tests/swfs/avm2/pixelbender_images/test.swf and b/tests/tests/swfs/avm2/pixelbender_images/test.swf differ