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