render: Implement more PixelBender features (#11800)
|
@ -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]]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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<Object<'gc>, Error<'gc>>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Collect)]
|
||||
#[collect(require_static)]
|
||||
pub struct ObjectWrapper {
|
||||
root: DynamicRoot<Rootable![Object<'gc>]>,
|
||||
}
|
||||
|
||||
impl ShaderObject for ObjectWrapper {
|
||||
fn clone_box(&self) -> Box<dyn ShaderObject> {
|
||||
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<ShaderFilter<'static>, 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<Object<'gc>, Error<'gc>> {
|
||||
let object_wrapper: &ObjectWrapper = filter
|
||||
.shader_object
|
||||
.downcast_ref::<ObjectWrapper>()
|
||||
.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>,
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<Object<'gc>>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error<'gc>> {
|
||||
let this = this.unwrap();
|
||||
|
||||
avm2_stub_method!(
|
||||
activation,
|
||||
"flash.display.ShaderJob",
|
||||
"start",
|
||||
"async execution and non-BitmapData inputs"
|
||||
);
|
||||
|
||||
) -> Result<
|
||||
(
|
||||
PixelBenderShaderHandle,
|
||||
Vec<PixelBenderShaderArgument<'static>>,
|
||||
),
|
||||
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<Object<'gc>>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, 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();
|
||||
|
|
|
@ -16,6 +16,43 @@ package flash.filters {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<Vec<_>>().try_into().unwrap(),
|
||||
)),
|
||||
PixelBenderTypeOpcode::TFloat3x3 => Ok(PixelBenderType::TFloat3x3(
|
||||
vals.collect::<Vec<_>>().try_into().unwrap(),
|
||||
)),
|
||||
PixelBenderTypeOpcode::TFloat4x4 => Ok(PixelBenderType::TFloat4x4(
|
||||
vals.collect::<Vec<_>>().try_into().unwrap(),
|
||||
)),
|
||||
_ => unreachable!("Unexpected float kind {kind:?}"),
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<dyn ShaderObject>,
|
||||
pub shader: PixelBenderShaderHandle,
|
||||
pub shader_args: Vec<PixelBenderShaderArgument<'a>>,
|
||||
}
|
||||
|
||||
pub trait ShaderObject: Downcast + Collect + Debug {
|
||||
fn clone_box(&self) -> Box<dyn ShaderObject>;
|
||||
}
|
||||
impl_downcast!(ShaderObject);
|
||||
|
||||
impl Clone for Box<dyn ShaderObject> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_box()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&swf::Filter> for Filter {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<ImageInputTexture<'a>>,
|
||||
},
|
||||
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<BitmapHandle> 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<PixelBenderShader, Box<dyn std::e
|
|||
Ok(shader)
|
||||
}
|
||||
|
||||
fn read_src_reg(val: u32, size: u8) -> Result<PixelBenderReg, Box<dyn std::error::Error>> {
|
||||
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<PixelBenderReg, Box<dyn std::error::Error>> {
|
||||
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, Box<dyn std::error
|
|||
})
|
||||
}
|
||||
|
||||
fn read_matrix_reg(val: u16, mask: u8) -> PixelBenderReg {
|
||||
read_reg(val, vec![CHANNELS[(mask + 3) as usize]])
|
||||
}
|
||||
|
||||
fn read_dst_reg(val: u16, mask: u8) -> Result<PixelBenderReg, Box<dyn std::error::Error>> {
|
||||
let mut channels = Vec::new();
|
||||
if mask & 0x8 != 0 {
|
||||
|
@ -335,18 +372,22 @@ fn read_dst_reg(val: u16, mask: u8) -> Result<PixelBenderReg, Box<dyn std::error
|
|||
channels.push(PixelBenderRegChannel::A);
|
||||
}
|
||||
|
||||
Ok(read_reg(val, channels))
|
||||
}
|
||||
|
||||
fn read_reg(val: u16, channels: Vec<PixelBenderRegChannel>) -> 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<R: Read>(
|
||||
|
@ -385,21 +426,45 @@ fn read_op<R: Read>(
|
|||
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<R: Read>(
|
|||
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(())
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#define_import_path filter
|
||||
#import common
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
|
@ -0,0 +1 @@
|
|||
pub const SHADER_FILTER_COMMON: &str = include_str!("shader_filter_common.wgsl");
|
|
@ -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 }
|
||||
|
|
|
@ -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<T: RenderTarget> WgpuRenderBackend<T> {
|
|||
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<QueueSyncHandle> {
|
||||
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<TextureBufferInfo> {
|
||||
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<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
|
||||
|
@ -685,19 +739,7 @@ impl<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
|
|||
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<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
|
|||
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<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
|
|||
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<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
|
|||
.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<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
|
|||
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<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
|
|||
arguments: &[PixelBenderShaderArgument],
|
||||
target_handle: BitmapHandle,
|
||||
) -> Result<Box<dyn SyncHandle>, 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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
|||
<dyn BitmapHandleImpl>::downcast_ref(&*handle.0).unwrap()
|
||||
}
|
||||
|
||||
pub fn raw_texture_as_texture(handle: &dyn RawTexture) -> &wgpu::Texture {
|
||||
<dyn RawTexture>::downcast_ref(handle).unwrap()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Enum)]
|
||||
pub enum MaskState {
|
||||
NoMask,
|
||||
|
|
|
@ -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<StagingBelt>,
|
||||
}
|
||||
|
||||
|
@ -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<T: RenderTarget> WgpuRenderBackend<T> {
|
||||
pub(super) fn compile_pixelbender_shader_impl(
|
||||
&mut self,
|
||||
|
@ -229,262 +254,277 @@ impl<T: RenderTarget> WgpuRenderBackend<T> {
|
|||
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<Box<dyn SyncHandle>, 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<wgpu::RenderPassColorAttachment>,
|
||||
// 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::<f32>() 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::<f32>() 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<u8, TextureView> = Default::default();
|
||||
let mut texture_views: IndexMap<u8, TextureView> = 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<f32>, 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(())
|
||||
}
|
||||
|
|
|
@ -132,8 +132,8 @@ fn composer() -> Result<Composer, ComposerError> {
|
|||
..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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 262 KiB |
After Width: | Height: | Size: 412 KiB |
|
@ -0,0 +1,43 @@
|
|||
// Based on https://github.com/nojvek/pixelbender/blob/master/selfDisplace/glassDisplace.pbk
|
||||
<languageVersion : 1.0;>
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 115 KiB |
|
@ -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 }
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 262 KiB |
After Width: | Height: | Size: 299 KiB |
|
@ -0,0 +1,43 @@
|
|||
// Based on https://github.com/nojvek/pixelbender/blob/master/selfDisplace/glassDisplace.pbk
|
||||
<languageVersion : 1.0;>
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 115 KiB |
|
@ -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)
|
|
@ -0,0 +1,8 @@
|
|||
num_frames = 10
|
||||
|
||||
[image_comparison]
|
||||
tolerance = 3
|
||||
max_outliers = 380
|
||||
|
||||
[player_options]
|
||||
with_renderer = { optional = false, sample_count = 1 }
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 262 KiB |
After Width: | Height: | Size: 262 KiB |
|
@ -0,0 +1,73 @@
|
|||
// Based on https://github.com/hoojaoh/PhotoFilterShaders/blob/master/Smudge.pbk
|
||||
<languageVersion : 1.0;>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
num_frames = 1
|
||||
|
||||
[image_comparison]
|
||||
tolerance = 1
|
||||
|
||||
[player_options]
|
||||
with_renderer = { optional = false, sample_count = 1 }
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 262 KiB |
After Width: | Height: | Size: 241 KiB |
|
@ -0,0 +1,7 @@
|
|||
num_frames = 1
|
||||
|
||||
[image_comparison]
|
||||
tolerance = 1
|
||||
|
||||
[player_options]
|
||||
with_renderer = { optional = false, sample_count = 1 }
|
|
@ -0,0 +1,45 @@
|
|||
// Based on https://github.com/hoojaoh/PhotoFilterShaders/blob/master/TinType.pbk
|
||||
<languageVersion : 1.0;>
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
}
|