Skip to content

Commit

Permalink
avm2: Implement BitmapData.draw for wgpu backend (#7254)
Browse files Browse the repository at this point in the history
* avm2: Implement `BitmapData.draw` for `wgpu` backend

This method requires us to have the ability to render directly to a
texture. Fortunately, the `wgpu` backend already supports this in
the form of `TextureTarget`. However, the rendering code required
some refactoring in order to avoid creating duplicate `wgpu` resources.

The current implementation blocks on copying the pixels back
from the GPU to the CPU, so that we can immediately set them in
the Ruffle `BitmapData`. This is likely very inefficient, but will
work for a first implementation.

In the future, we could explore allowing the CPU image data and GPU
texture to be out of sync, and only synchronized when explicitly
necessary (e.g. on `getPixel` or `setPixel` calls).

* Rename `with_offscreen_backend` to `render_offscreen` and use Bitmap

* Don't panic when backend doesn't implement `render_offscreen`
  • Loading branch information
Aaron1011 authored Sep 6, 2022
1 parent 54bf3d2 commit 93607aa
Show file tree
Hide file tree
Showing 31 changed files with 751 additions and 116 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ flash-lso = { git = "https://github.com/ruffle-rs/rust-flash-lso", rev = "19fecd
lzma-rs = {version = "0.2.0", optional = true }
dasp = { git = "https://github.com/RustAudio/dasp", rev = "f05a703", features = ["interpolate", "interpolate-linear", "signal"], optional = true }
symphonia = { version = "0.5.1", default-features = false, features = ["mp3"], optional = true }
image = "0.24.2"

[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
version = "0.3.24"
Expand Down
81 changes: 69 additions & 12 deletions core/src/avm1/globals/bitmap_data.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
//! flash.display.BitmapData object
use super::{color_transform::object_to_color_transform, matrix::object_to_matrix};
use crate::avm1::activation::Activation;
use crate::avm1::error::Error;
use crate::avm1::function::{Executable, FunctionObject};
use crate::avm1::object::bitmap_data::BitmapDataObject;
use crate::avm1::property_decl::{define_properties_on, Declaration};
use crate::avm1::{Object, TObject, Value};
use crate::avm_error;
use crate::bitmap::bitmap_data::IBitmapDrawable;
use crate::bitmap::bitmap_data::{BitmapData, ChannelOptions, Color};
use crate::bitmap::is_size_valid;
use crate::character::Character;
use crate::display_object::TDisplayObject;
use crate::swf::BlendMode;
use gc_arena::{GcCell, MutationContext};
use ruffle_render::transform::Transform;

const PROTO_DECLS: &[Declaration] = declare_properties! {
"height" => property(height);
Expand Down Expand Up @@ -483,30 +488,82 @@ pub fn noise<'gc>(
Ok((-1).into())
}

pub fn apply_filter<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
log::warn!("BitmapData.applyFilter - not yet implemented");
Ok((-1).into())
}

pub fn draw<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
activation: &mut Activation<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
if let Some(bitmap_data) = this.as_bitmap_data_object() {
if !bitmap_data.disposed() {
log::warn!("BitmapData.draw - not yet implemented");
let matrix = args
.get(1)
.map(|o| o.coerce_to_object(activation))
.and_then(|o| object_to_matrix(o, activation).ok())
.unwrap_or_default();

let color_transform = args
.get(2)
.map(|o| o.coerce_to_object(activation))
.and_then(|o| object_to_color_transform(o, activation).ok())
.unwrap_or_default();

if args.get(3).is_some() {
log::warn!("BitmapData.draw with blend mode - not implemented")
}
if args.get(4).is_some() {
log::warn!("BitmapData.draw with clip rect - not implemented")
}
let smoothing = args
.get(5)
.unwrap_or(&false.into())
.as_bool(activation.swf_version());

let source = args
.get(0)
.unwrap_or(&Value::Undefined)
.coerce_to_object(activation);
let source = if let Some(source_object) = source.as_display_object() {
IBitmapDrawable::DisplayObject(source_object)
} else if let Some(source_bitmap) = source.as_bitmap_data_object() {
IBitmapDrawable::BitmapData(source_bitmap.bitmap_data())
} else {
avm_error!(
activation,
"BitmapData.draw: Unexpected source {:?} {:?}",
source,
args.get(0)
);
return Ok(Value::Undefined);
};

let bmd = bitmap_data.bitmap_data();
let mut write = bmd.write(activation.context.gc_context);
write.draw(
source,
Transform {
matrix,
color_transform,
},
smoothing,
BlendMode::Normal,
&mut activation.context,
);
return Ok(Value::Undefined);
}
}

Ok((-1).into())
}

pub fn apply_filter<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
log::warn!("BitmapData.applyFilter - not yet implemented");
Ok((-1).into())
}

pub fn generate_filter_rect<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
this: Object<'gc>,
Expand Down
76 changes: 76 additions & 0 deletions core/src/avm2/globals/flash/display/bitmapdata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ use crate::avm2::Multiname;
use crate::avm2::Namespace;
use crate::avm2::QName;
use crate::bitmap::bitmap_data::BitmapData;
use crate::bitmap::bitmap_data::IBitmapDrawable;
use crate::bitmap::is_size_valid;
use crate::character::Character;
use crate::swf::BlendMode;
use gc_arena::{GcCell, MutationContext};
use ruffle_render::transform::Transform;
use std::str::FromStr;

/// Implements `flash.display.BitmapData`'s instance constructor.
pub fn instance_init<'gc>(
Expand Down Expand Up @@ -67,6 +71,8 @@ pub fn instance_init<'gc>(
name
);
}
} else {
log::error!("Failed to get bitmap handle for {:?}", bd);
}
} else {
if character.is_some() {
Expand Down Expand Up @@ -290,6 +296,75 @@ pub fn lock<'gc>(
Ok(Value::Undefined)
}

/// Implements `BitmapData.draw`
pub fn draw<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(bitmap_data) = this.and_then(|this| this.as_bitmap_data()) {
let mut transform = Transform::default();
let mut blend_mode = BlendMode::Normal;

let matrix = args.get(1).unwrap_or(&Value::Null);
if !matches!(matrix, Value::Null) {
transform.matrix = crate::avm2::globals::flash::geom::transform::object_to_matrix(
matrix.coerce_to_object(activation)?,
activation,
)?;
}

let color_transform = args.get(2).unwrap_or(&Value::Null);
if !matches!(color_transform, Value::Null) {
transform.color_transform =
crate::avm2::globals::flash::geom::transform::object_to_color_transform(
color_transform.coerce_to_object(activation)?,
activation,
)?;
}

let mode = args.get(3).unwrap_or(&Value::Null);
if !matches!(mode, Value::Null) {
if let Ok(mode) = BlendMode::from_str(&mode.coerce_to_string(activation)?.to_string()) {
blend_mode = mode;
} else {
log::error!("Unknown blend mode {:?}", mode);
return Err("ArgumentError: Error #2008: Parameter blendMode must be one of the accepted values.".into());
}
}

if args.get(4).is_some() {
log::warn!("BitmapData.draw with clip rect - not implemented")
}

let mut bitmap_data = bitmap_data.write(activation.context.gc_context);
// FIXME - handle other arguments
let smoothing = args.get(5).unwrap_or(&false.into()).coerce_to_boolean();

let source = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| format!("BitmapData.draw: source {:?} is not an Object", args.get(0)))?;

let source = if let Some(source_object) = source.as_display_object() {
IBitmapDrawable::DisplayObject(source_object)
} else if let Some(source_bitmap) = source.as_bitmap_data() {
IBitmapDrawable::BitmapData(source_bitmap)
} else {
return Err(format!("BitmapData.draw: unexpected source {:?}", source).into());
};

bitmap_data.draw(
source,
transform,
smoothing,
blend_mode,
&mut activation.context,
);
}
Ok(Value::Undefined)
}

/// Construct `BitmapData`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
Expand Down Expand Up @@ -324,6 +399,7 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
("lock", lock),
("unlock", lock), // sic, it's a noop (TODO)
("copyPixels", copy_pixels),
("draw", draw),
];
write.define_public_builtin_instance_methods(mc, PUBLIC_INSTANCE_METHODS);

Expand Down
109 changes: 108 additions & 1 deletion core/src/bitmap/bitmap_data.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use gc_arena::Collect;
use gc_arena::{Collect, GcCell};
use swf::BlendMode;

use crate::avm2::{Object as Avm2Object, Value as Avm2Value};
use crate::bitmap::color_transform_params::ColorTransformParams;
use crate::bitmap::turbulence::Turbulence;
use crate::context::RenderContext;
use crate::context::UpdateContext;
use crate::display_object::DisplayObject;
use crate::display_object::TDisplayObject;
use bitflags::bitflags;
use ruffle_render::backend::RenderBackend;
use ruffle_render::bitmap::{Bitmap, BitmapFormat, BitmapHandle};
use ruffle_render::transform::Transform;
use std::ops::Range;

/// An implementation of the Lehmer/Park-Miller random number generator
Expand Down Expand Up @@ -932,4 +938,105 @@ impl<'gc> BitmapData<'gc> {
pub fn init_object2(&mut self, object: Avm2Object<'gc>) {
self.avm2_object = Some(object)
}

pub fn draw(
&mut self,
mut source: IBitmapDrawable<'gc>,
transform: Transform,
smoothing: bool,
blend_mode: BlendMode,
context: &mut UpdateContext<'_, 'gc, '_>,
) {
let bitmapdata_width = self.width();
let bitmapdata_height = self.height();

let mut transform_stack = ruffle_render::transform::TransformStack::new();
transform_stack.push(&transform);
let handle = self.bitmap_handle(context.renderer).unwrap();

let image = context.renderer.render_offscreen(
handle,
bitmapdata_width,
bitmapdata_height,
&mut |renderer| {
let mut render_context = RenderContext {
renderer,
gc_context: context.gc_context,
ui: context.ui,
library: &context.library,
transform_stack: &mut transform_stack,
is_offscreen: true,
stage: context.stage,
clip_depth_stack: vec![],
allow_mask: true,
};

render_context
.renderer
.begin_frame(swf::Color::from_rgb(0x000000, 0));
render_context.renderer.push_blend_mode(blend_mode);
match &mut source {
IBitmapDrawable::BitmapData(data) => {
let source_handle = data
.write(context.gc_context)
.bitmap_handle(render_context.renderer)
.unwrap();
render_context.renderer.render_bitmap(
source_handle,
render_context.transform_stack.transform(),
smoothing,
);
}
IBitmapDrawable::DisplayObject(object) => {
// Note that we do *not* use `render_base`,
// as we want to ignore the object's mask and normal transform
object.render_self(&mut render_context);
}
}
render_context.renderer.pop_blend_mode();
render_context.renderer.end_frame();

Ok(())
},
);

match image {
Ok(image) => copy_pixels_to_bitmapdata(self, image.data()),
Err(ruffle_render::error::Error::Unimplemented) => {
log::warn!("BitmapData.draw: Not yet implemented")
}
Err(e) => panic!("BitmapData.draw failed: {:?}", e),
}
}
}

pub enum IBitmapDrawable<'gc> {
BitmapData(GcCell<'gc, BitmapData<'gc>>),
DisplayObject(DisplayObject<'gc>),
}

fn copy_pixels_to_bitmapdata(write: &mut BitmapData, bytes: &[u8]) {
let height = write.height();
let width = write.width();

for y in 0..height {
for x in 0..width {
// note: this order of conversions helps llvm realize the index is 4-byte-aligned
let ind = ((x + y * width) as usize) * 4;

// TODO(mid): optimize this A LOT
let r = bytes[ind];
let g = bytes[ind + 1usize];
let b = bytes[ind + 2usize];
let a = bytes[ind + 3usize];

// TODO(later): we might want to swap Color storage from argb to rgba, to make it cheaper
let nc = Color::argb(a, r, g, b);

let oc = write.get_pixel_raw(x, y).unwrap();

// FIXME: this blending is completely broken on transparent content
write.set_pixel32_raw(x, y, oc.blend_over(&nc));
}
}
}
3 changes: 3 additions & 0 deletions core/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,9 @@ pub struct RenderContext<'a, 'gc, 'gc_context> {
/// The transform stack controls the matrix and color transform as we traverse the display hierarchy.
pub transform_stack: &'a mut TransformStack,

/// Whether we're rendering offscreen. This can disable some logic like Ruffle-side render culling
pub is_offscreen: bool,

/// The current player's stage (including all loaded levels)
pub stage: Stage<'gc>,

Expand Down
Loading

0 comments on commit 93607aa

Please sign in to comment.