diff --git a/Cargo.lock b/Cargo.lock index 332efec5c..fd5d98fb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,6 +378,29 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bindgen" +version = "0.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +dependencies = [ + "bitflags 2.4.0", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.37", + "which", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -848,7 +871,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24" dependencies = [ - "bindgen", + "bindgen 0.64.0", ] [[package]] @@ -2483,6 +2506,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +[[package]] +name = "jpegxr" +version = "0.3.0" +source = "git+https://github.com/ruffle-rs/jpegxr?branch=ruffle#0251753f3ea4b7e301cb89e92c5707055b1db501" +dependencies = [ + "bindgen 0.68.1", + "cc", + "libc", + "thiserror", +] + [[package]] name = "js-sys" version = "0.3.64" @@ -3537,6 +3571,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.37", +] + [[package]] name = "primal-check" version = "0.3.3" @@ -3875,8 +3919,10 @@ dependencies = [ "futures", "generational-arena", "hashbrown 0.14.0", + "image", "indexmap 2.0.0", "instant", + "jpegxr", "linkme", "lzma-rs", "nellymoser-rs", @@ -5560,6 +5606,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "wide" version = "0.7.11" diff --git a/core/Cargo.toml b/core/Cargo.toml index 3924966b5..539b11cd1 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -56,6 +56,8 @@ egui_extras = { git = "https://github.com/emilk/egui", rev = "98087029e020a1b2d7 png = { version = "0.17.10", optional = true } flv-rs = { path = "../flv" } async-channel = "1.9.0" +jpegxr = { git = "https://github.com/ruffle-rs/jpegxr", branch = "ruffle" } +image = { version = "0.24.7", default-features = false, features = ["tiff"] } [target.'cfg(not(target_family = "wasm"))'.dependencies.futures] version = "0.3.28" diff --git a/core/src/avm2/globals/flash/display3D/textures/CubeTexture.as b/core/src/avm2/globals/flash/display3D/textures/CubeTexture.as index 4da149587..d0e22c6ed 100644 --- a/core/src/avm2/globals/flash/display3D/textures/CubeTexture.as +++ b/core/src/avm2/globals/flash/display3D/textures/CubeTexture.as @@ -1,6 +1,11 @@ package flash.display3D.textures { import flash.display.BitmapData; + import flash.utils.ByteArray; + import __ruffle__.stub_method; + public final class CubeTexture extends TextureBase { - public native function uploadFromBitmapData(source:BitmapData, side:uint, miplevel:uint = 0):void + public native function uploadFromBitmapData(source:BitmapData, side:uint, miplevel:uint = 0):void; + public native function uploadFromByteArray(data:ByteArray, byteArrayOffset:uint, side:uint, miplevel:uint = 0); + public native function uploadCompressedTextureFromByteArray(data:ByteArray, byteArrayOffset:uint, async:Boolean = false):void; } } \ No newline at end of file diff --git a/core/src/avm2/globals/flash/display3D/textures/RectangleTexture.as b/core/src/avm2/globals/flash/display3D/textures/RectangleTexture.as index 966a0e5ae..afbec97a2 100644 --- a/core/src/avm2/globals/flash/display3D/textures/RectangleTexture.as +++ b/core/src/avm2/globals/flash/display3D/textures/RectangleTexture.as @@ -1,6 +1,9 @@ package flash.display3D.textures { import flash.display.BitmapData; + import flash.utils.ByteArray; + public final class RectangleTexture extends TextureBase { - public native function uploadFromBitmapData(source:BitmapData):void + public native function uploadFromBitmapData(source:BitmapData):void; + public native function uploadFromByteArray(data:ByteArray, byteArrayOffset:uint):void; } } \ No newline at end of file diff --git a/core/src/avm2/globals/flash/display3D/textures/Texture.as b/core/src/avm2/globals/flash/display3D/textures/Texture.as index 7a557012b..638ba3d59 100644 --- a/core/src/avm2/globals/flash/display3D/textures/Texture.as +++ b/core/src/avm2/globals/flash/display3D/textures/Texture.as @@ -1,6 +1,11 @@ package flash.display3D.textures { import flash.display.BitmapData; + import flash.utils.ByteArray; + import __ruffle__.stub_method; + public final class Texture extends TextureBase { - public native function uploadFromBitmapData(source:BitmapData, miplevel:uint = 0):void + public native function uploadFromBitmapData(source:BitmapData, miplevel:uint = 0):void; + public native function uploadFromByteArray(data:ByteArray, byteArrayOffset:uint, miplevel:uint = 0):void; + public native function uploadCompressedTextureFromByteArray(data:ByteArray, byteArrayOffset:uint, async:Boolean = false):void; } } \ No newline at end of file diff --git a/core/src/avm2/globals/flash/display3D/textures/cube_texture.rs b/core/src/avm2/globals/flash/display3D/textures/cube_texture.rs index 7c4527658..81ff77136 100644 --- a/core/src/avm2/globals/flash/display3D/textures/cube_texture.rs +++ b/core/src/avm2/globals/flash/display3D/textures/cube_texture.rs @@ -1,9 +1,82 @@ +use ruffle_render::backend::Context3DTextureFormat; + +use crate::avm2::globals::flash::display3D::textures::texture::do_compressed_upload; +use crate::avm2::parameters::ParametersExt; use crate::avm2::Activation; use crate::avm2::TObject; use crate::avm2::Value; use crate::avm2::{Error, Object}; use crate::avm2_stub_method; +use super::texture::do_copy; + +pub fn upload_from_byte_array<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // This should work, but it's currently untested + avm2_stub_method!( + activation, + "flash.display3D.textures.CubeTexture", + "uploadFromByteArray" + ); + let texture = this.as_texture().unwrap(); + let data = args.get_object(activation, 0, "data")?; + let byte_array_offset = args.get_u32(activation, 1)?; + let side = args.get_u32(activation, 2)?; + let mip_level = args.get_u32(activation, 3)?; + + do_copy( + activation, + data, + texture, + byte_array_offset, + side, + mip_level, + )?; + Ok(Value::Undefined) +} + +pub fn upload_compressed_texture_from_byte_array<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // This should work, but it's currently untested + avm2_stub_method!( + activation, + "flash.display3D.textures.CubeTexture", + "uploadCompressedTextureFromByteArray" + ); + + let texture = this.as_texture().unwrap(); + let data = args.get_object(activation, 0, "data")?; + let byte_array_offset = args.get_u32(activation, 1)? as usize; + let async_ = args.get_bool(2); + if async_ { + avm2_stub_method!( + activation, + "flash.display3D.textures.CubeTexture", + "uploadCompressedTextureFromByteArray", + "with async" + ); + } + + if !matches!(texture.original_format(), Context3DTextureFormat::Bgra) { + avm2_stub_method!( + activation, + "flash.display3D.textures.CubeTexture", + "uploadCompressedTextureFromByteArray", + "with unsupported format" + ); + return Ok(Value::Undefined); + } + + do_compressed_upload(activation, texture, data, byte_array_offset, true)?; + Ok(Value::Undefined) +} + pub fn upload_from_bitmap_data<'gc>( activation: &mut Activation<'_, 'gc>, this: Object<'gc>, diff --git a/core/src/avm2/globals/flash/display3D/textures/rectangle_texture.rs b/core/src/avm2/globals/flash/display3D/textures/rectangle_texture.rs index a76b24a21..a113591df 100644 --- a/core/src/avm2/globals/flash/display3D/textures/rectangle_texture.rs +++ b/core/src/avm2/globals/flash/display3D/textures/rectangle_texture.rs @@ -1,8 +1,24 @@ +use crate::avm2::parameters::ParametersExt; use crate::avm2::Activation; use crate::avm2::TObject; use crate::avm2::Value; use crate::avm2::{Error, Object}; +use super::texture::do_copy; + +pub fn upload_from_byte_array<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let texture = this.as_texture().unwrap(); + let data = args.get_object(activation, 0, "data")?; + let byte_array_offset = args.get_u32(activation, 1)?; + + do_copy(activation, data, texture, byte_array_offset, 0, 0)?; + Ok(Value::Undefined) +} + pub fn upload_from_bitmap_data<'gc>( activation: &mut Activation<'_, 'gc>, this: Object<'gc>, diff --git a/core/src/avm2/globals/flash/display3D/textures/texture.rs b/core/src/avm2/globals/flash/display3D/textures/texture.rs index b759969db..a36378140 100644 --- a/core/src/avm2/globals/flash/display3D/textures/texture.rs +++ b/core/src/avm2/globals/flash/display3D/textures/texture.rs @@ -1,8 +1,199 @@ +use std::io::Cursor; + +use gc_arena::GcCell; +use ruffle_render::atf::ATFTexture; +use ruffle_render::backend::Context3DTextureFormat; + +use crate::avm2::object::TextureObject; +use crate::avm2::parameters::ParametersExt; use crate::avm2::Activation; use crate::avm2::TObject; use crate::avm2::Value; use crate::avm2::{Error, Object}; use crate::avm2_stub_method; +use crate::bitmap::bitmap_data::BitmapData; +use crate::bitmap::bitmap_data::BitmapDataWrapper; +use crate::bitmap::bitmap_data::Color; + +pub fn do_copy<'gc>( + activation: &mut Activation<'_, 'gc>, + data: Object<'gc>, + texture: TextureObject<'gc>, + byte_array_offset: u32, + side: u32, + mip_level: u32, +) -> Result<(), Error<'gc>> { + if mip_level != 0 { + avm2_stub_method!( + activation, + "flash.display3D.textures.Texture", + "uploadFromByteArray", + "with miplevel != 0" + ); + return Ok(()); + } + + // FIXME - see if we can avoid this intermediate BitmapDataWrapper, and copy + // directly from a buffer to the target GPU texture + let bitmap_data = match texture.original_format() { + Context3DTextureFormat::Bgra => { + let width = texture.handle().width(); + let height = texture.handle().height(); + + let bytearray = data.as_bytearray().unwrap(); + + let colors: Vec<_> = bytearray + .read_at((4 * width * height) as usize, byte_array_offset as usize) + .expect("Failed to read") + .chunks_exact(4) + .map(|chunk| { + // The ByteArray is in BGRA format. FIXME - should this be premultiplied? + Color::argb(chunk[3], chunk[2], chunk[1], chunk[0]) + }) + .collect(); + + let bitmap_data = BitmapData::new_with_pixels(width, height, true, colors); + BitmapDataWrapper::new(GcCell::new(activation.context.gc_context, bitmap_data)) + } + _ => { + tracing::warn!( + "uploadFromByteArray with unsupported format: {:?}", + texture.original_format() + ); + return Ok(()); + } + }; + texture + .context3d() + .copy_bitmap_to_texture(bitmap_data.sync(), texture.handle(), side); + Ok(()) +} + +pub(super) fn do_compressed_upload<'gc>( + activation: &mut Activation<'_, 'gc>, + texture: TextureObject<'gc>, + data: Object<'gc>, + byte_array_offset: usize, + is_cube: bool, +) -> Result<(), Error<'gc>> { + let atf_texture = + ATFTexture::from_bytes(&data.as_bytearray().unwrap().bytes()[byte_array_offset..]) + .expect("Failed to parse ATF texture"); + + if is_cube != atf_texture.cubemap { + return Err("Stage3D Texture and ATF Texture must both be cube/non-cube".into()); + } + + if atf_texture.width != texture.handle().width() + || atf_texture.height != texture.handle().height() + { + return Err("ATF texture dimensions do not match Texture dimensions".into()); + } + + // Just use the first mip level for now. We ignore the builtin format - the JPEG-XR format + // appears to override it + let mut first_mip = Cursor::new(&atf_texture.face_mip_data[0][0]); + let mut decoder = + jpegxr::ImageDecode::with_reader(&mut first_mip).expect("Failed to decode JPEG-XR image"); + + let pixel_format = decoder + .get_pixel_format() + .expect("Failed to get pixel format"); + let (jpeg_width, jpeg_height) = decoder.get_size().expect("Failed to get JPEG-XR size"); + let jpeg_width = jpeg_width as u32; + let jpeg_height = jpeg_height as u32; + + assert_eq!(jpeg_width, atf_texture.width, "Mismatched JPEG-XR width"); + assert_eq!(jpeg_height, atf_texture.height, "Mismatched JPEG-XR height"); + + let info = jpegxr::PixelInfo::from_format(pixel_format); + let stride = jpeg_width as usize * info.bits_per_pixel() / 8; + let size = stride * jpeg_height as usize; + + // We convert the result to a TIFF - this makes the jpegxr library handle + // all of the weird JPEG-XR alpha formats for us. We can then use the normal + // `image` crate to decode the TIFF to an rgba array. + let mut bmp_buffer = vec![0; size]; + decoder + .convert_to_tiff(&mut Cursor::new(&mut bmp_buffer)) + .expect("Failed to convert to bitmap"); + + let image_reader = + image::io::Reader::with_format(Cursor::new(bmp_buffer), image::ImageFormat::Tiff); + let bitmap = image_reader + .decode() + .expect("Failed to decode Bitmap") + .to_rgba8(); + + // FIXME - are we handling premultiplied alpha correct? + let colors: Vec<_> = bitmap + .chunks_exact(4) + .map(|color| Color::argb(color[3], color[0], color[1], color[2])) + .collect(); + + let bitmap_data = BitmapData::new_with_pixels( + texture.handle().width(), + texture.handle().height(), + true, + colors, + ); + + let bitmap_data = + BitmapDataWrapper::new(GcCell::new(activation.context.gc_context, bitmap_data)); + + texture + .context3d() + .copy_bitmap_to_texture(bitmap_data.sync(), texture.handle(), 0); + + Ok(()) +} + +pub fn upload_compressed_texture_from_byte_array<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let texture = this.as_texture().unwrap(); + let data = args.get_object(activation, 0, "data")?; + let byte_array_offset = args.get_u32(activation, 1)? as usize; + let async_ = args.get_bool(2); + if async_ { + avm2_stub_method!( + activation, + "flash.display3D.textures.Texture", + "uploadCompressedTextureFromByteArray", + "with async" + ); + } + + if !matches!(texture.original_format(), Context3DTextureFormat::Bgra) { + avm2_stub_method!( + activation, + "flash.display3D.textures.Texture", + "uploadCompressedTextureFromByteArray", + "with unsupported format" + ); + return Ok(Value::Undefined); + } + + do_compressed_upload(activation, texture, data, byte_array_offset, false)?; + + Ok(Value::Undefined) +} + +pub fn upload_from_byte_array<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let texture = this.as_texture().unwrap(); + let data = args.get_object(activation, 0, "data")?; + let byte_array_offset = args.get_u32(activation, 1)?; + let mip_level = args.get_u32(activation, 2)?; + + do_copy(activation, data, texture, byte_array_offset, 0, mip_level)?; + Ok(Value::Undefined) +} pub fn upload_from_bitmap_data<'gc>( activation: &mut Activation<'_, 'gc>, diff --git a/core/src/avm2/object/context3d_object.rs b/core/src/avm2/object/context3d_object.rs index b0e013c0f..de78b52ac 100644 --- a/core/src/avm2/object/context3d_object.rs +++ b/core/src/avm2/object/context3d_object.rs @@ -124,7 +124,7 @@ impl<'gc> Context3DObject<'gc> { })?; Ok(Value::Object(TextureObject::from_handle( - activation, *self, texture, class, + activation, *self, texture, format, class, )?)) } @@ -418,7 +418,7 @@ impl<'gc> Context3DObject<'gc> { let class = activation.avm2().classes().cubetexture; Ok(Value::Object(TextureObject::from_handle( - activation, *self, texture, class, + activation, *self, texture, format, class, )?)) } diff --git a/core/src/avm2/object/texture_object.rs b/core/src/avm2/object/texture_object.rs index ff83ae79e..8a95f9d9d 100644 --- a/core/src/avm2/object/texture_object.rs +++ b/core/src/avm2/object/texture_object.rs @@ -8,7 +8,7 @@ use crate::avm2::Error; use gc_arena::barrier::unlock; use gc_arena::lock::RefLock; use gc_arena::{Collect, Gc, GcWeak, Mutation}; -use ruffle_render::backend::Texture; +use ruffle_render::backend::{Context3DTextureFormat, Texture}; use std::cell::{Ref, RefMut}; use std::rc::Rc; @@ -27,6 +27,7 @@ impl<'gc> TextureObject<'gc> { activation: &mut Activation<'_, 'gc>, context3d: Context3DObject<'gc>, handle: Rc, + original_format: Context3DTextureFormat, class: ClassObject<'gc>, ) -> Result, Error<'gc>> { let this: Object<'gc> = TextureObject(Gc::new( @@ -34,6 +35,7 @@ impl<'gc> TextureObject<'gc> { TextureObjectData { base: RefLock::new(ScriptObjectData::new(class)), context3d, + original_format, handle, }, )) @@ -45,6 +47,10 @@ impl<'gc> TextureObject<'gc> { Ok(this) } + pub fn original_format(&self) -> Context3DTextureFormat { + self.0.original_format + } + pub fn handle(&self) -> Rc { self.0.handle.clone() } @@ -62,6 +68,9 @@ pub struct TextureObjectData<'gc> { context3d: Context3DObject<'gc>, + #[collect(require_static)] + original_format: Context3DTextureFormat, + #[collect(require_static)] handle: Rc, } diff --git a/render/src/atf.rs b/render/src/atf.rs new file mode 100644 index 000000000..98549c3ad --- /dev/null +++ b/render/src/atf.rs @@ -0,0 +1,117 @@ +use std::io::Read; + +use byteorder::{BigEndian, ReadBytesExt}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +pub struct ATFTexture { + pub width: u32, + pub height: u32, + pub cubemap: bool, + pub format: ATFFormat, + pub mip_count: u8, + // A nested array of `[0..num_faces][0..mip_count]`, where each + // entry is the texture data for that mip level and face. + pub face_mip_data: Vec>>, +} + +#[derive(FromPrimitive, Debug)] +pub enum ATFFormat { + RGB888 = 0, + RGBA8888 = 1, + Compressed = 2, + RawCompressed = 3, + CompressedAlpha = 4, + RawCompressedAlpha = 5, + CompressedLossy = 0xc, + CompressedLossyAlpha = 0xd, +} + +impl ATFTexture { + pub fn from_bytes(mut bytes: &[u8]) -> Result> { + // Based on https://github.com/openfl/openfl/blob/develop/src/openfl/display3D/_internal/ATFReader.hx + let bytes = &mut bytes; + + let mut string_bytes = [0; 3]; + bytes.read_exact(&mut string_bytes)?; + + if &string_bytes != b"ATF" { + return Err(format!("Invalid ATF signature {string_bytes:?}").into()); + } + + let version; + let _length; + + if bytes[3] == 0xFF { + version = bytes[4]; + *bytes = &bytes[5..]; + _length = bytes.read_u32::()?; + } else { + version = 0; + _length = read_uint24(bytes)?; + } + + let tdata = bytes.read_u8()?; + let cubemap = (tdata >> 7) != 0; + + let format = ATFFormat::from_u8(tdata & 0x7f).ok_or_else(|| { + format!( + "Invalid ATF format {format} (version {version})", + format = tdata & 0x7f, + version = version + ) + })?; + let width = 1 << bytes.read_u8()?; + let height = 1 << bytes.read_u8()?; + + let mip_count = bytes.read_u8()?; + let num_faces = if cubemap { 6 } else { 1 }; + + let mut face_mip_data = vec![vec![]; num_faces]; + + #[allow(clippy::needless_range_loop)] + for face in 0..num_faces { + for _ in 0..mip_count { + // All of the formats consist of a number of (u32_length, data[u32_length]) records. + // For now, we just combine them into a single buffer to allow parsing to succeed. + let num_records = match format { + ATFFormat::RGB888 | ATFFormat::RGBA8888 => 1, + ATFFormat::RawCompressed | ATFFormat::RawCompressedAlpha => 4, + ATFFormat::Compressed => 11, + ATFFormat::CompressedAlpha => { + return Err("CompressedAlpha not supported".into()); + } + ATFFormat::CompressedLossy => 12, + ATFFormat::CompressedLossyAlpha => 17, + }; + let mut all_data = vec![]; + for _ in 0..num_records { + let len = if version == 0 { + read_uint24(bytes)? + } else { + bytes.read_u32::()? + }; + let orig_len = all_data.len(); + all_data.resize(orig_len + len as usize, 0); + bytes.read_exact(&mut all_data[orig_len..])?; + } + face_mip_data[face].push(all_data); + } + } + Ok(ATFTexture { + width, + height, + cubemap, + format, + mip_count, + face_mip_data, + }) + } +} + +fn read_uint24(data: &mut R) -> Result> { + let ch1 = data.read_u8()? as u32; + let ch2 = data.read_u8()? as u32; + let ch3 = data.read_u8()? as u32; + Ok(ch1 | (ch2 << 8) | (ch3 << 16)) +} diff --git a/render/src/backend.rs b/render/src/backend.rs index 81d727553..b2f330288 100644 --- a/render/src/backend.rs +++ b/render/src/backend.rs @@ -117,7 +117,10 @@ impl_downcast!(VertexBuffer); pub trait ShaderModule: Downcast {} impl_downcast!(ShaderModule); -pub trait Texture: Downcast {} +pub trait Texture: Downcast + Debug { + fn width(&self) -> u32; + fn height(&self) -> u32; +} impl_downcast!(Texture); pub trait RawTexture: Downcast + Debug {} diff --git a/render/src/lib.rs b/render/src/lib.rs index 9b639e0d4..aa60637bd 100644 --- a/render/src/lib.rs +++ b/render/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::unwrap_used)] +pub mod atf; pub mod backend; pub mod bitmap; pub mod blend; diff --git a/render/wgpu/src/context3d/mod.rs b/render/wgpu/src/context3d/mod.rs index 3c44a1656..ac6a8e06e 100644 --- a/render/wgpu/src/context3d/mod.rs +++ b/render/wgpu/src/context3d/mod.rs @@ -347,13 +347,21 @@ pub struct VertexBufferWrapper { pub data_32_per_vertex: u8, } +#[derive(Debug)] pub struct TextureWrapper { texture: wgpu::Texture, } impl IndexBuffer for IndexBufferWrapper {} impl VertexBuffer for VertexBufferWrapper {} -impl ruffle_render::backend::Texture for TextureWrapper {} +impl ruffle_render::backend::Texture for TextureWrapper { + fn width(&self) -> u32 { + self.texture.width() + } + fn height(&self) -> u32 { + self.texture.height() + } +} // Context3D.setVertexBufferAt supports up to 8 vertex buffer attributes const MAX_VERTEX_ATTRIBUTES: usize = 8; diff --git a/tests/tests/swfs/avm2/stage3d_texture_bytearray/Test.as b/tests/tests/swfs/avm2/stage3d_texture_bytearray/Test.as new file mode 100644 index 000000000..a27cd7c96 --- /dev/null +++ b/tests/tests/swfs/avm2/stage3d_texture_bytearray/Test.as @@ -0,0 +1,164 @@ +package { + import com.adobe.utils.AGALMiniAssembler; + + import flash.display.Sprite; + import flash.display.Stage3D; + import flash.display.StageAlign; + import flash.display.StageScaleMode; + import flash.display3D.Context3D; + import flash.display3D.Context3DBlendFactor; + import flash.display3D.Context3DCompareMode; + import flash.display3D.Context3DProgramType; + import flash.display3D.Context3DRenderMode; + import flash.display3D.Context3DStencilAction; + import flash.display3D.Context3DTriangleFace; + import flash.display3D.Context3DVertexBufferFormat; + import flash.display3D.Context3DTextureFilter; + import flash.display3D.Context3DWrapMode; + import flash.display3D.IndexBuffer3D; + import flash.display3D.Program3D; + import flash.display3D.VertexBuffer3D; + import flash.events.Event; + import flash.events.KeyboardEvent; + import flash.events.MouseEvent; + import flash.events.TimerEvent; + import flash.geom.Rectangle; + import flash.text.TextField; + import flash.text.TextFormat; + import flash.ui.Keyboard; + import flash.utils.Timer; + import flash.display.MovieClip; + import flash.display.Stage; + import flash.display.BitmapData; + import flash.display.Bitmap; + import flash.utils.ByteArray; + + // Based on example from https://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display3D/Context3D.html#setStencilActions + public class Test extends MovieClip { + public const viewWidth:Number = 500; + public const viewHeight:Number = 500; + + private var stage3D:Stage3D; + private var renderContext:Context3D; + private var indexList:IndexBuffer3D; + private var vertexes:VertexBuffer3D; + + private const VERTEX_SHADER:String = + "add op, va0, vc0 \n" + // copy position to output, adding offset + "mov v0, va1"; // copy uv to varying variable v0 + + private const FRAGMENT_SHADER:String = + "tex oc, v0, fs0 <2d,clamp,linear,mipnone>"; + + private var vertexAssembly:AGALMiniAssembler = new AGALMiniAssembler(false); + private var fragmentAssembly:AGALMiniAssembler = new AGALMiniAssembler(false); + private var programPair:Program3D; + + [Embed(source = "ruffle_logo.png")] + public var RUFFLE_LOGO: Class; + + [Embed(source = "circle.atf", mimeType = "application/octet-stream")] + public var CIRCLE_ATF: Class; + + public function Test() { + stage3D = this.stage.stage3Ds[0]; + + // Add event listener before requesting the context + stage3D.addEventListener(Event.CONTEXT3D_CREATE, contextCreated); + stage3D.requestContext3D(Context3DRenderMode.AUTO, "standard"); + + // Compile shaders + vertexAssembly.assemble(Context3DProgramType.VERTEX, VERTEX_SHADER, 2); + fragmentAssembly.assemble(Context3DProgramType.FRAGMENT, FRAGMENT_SHADER, 2); + } + + // Note, context3DCreate event can happen at any time, such as when the hardware resources are taken by another process + private function contextCreated(event:Event):void { + renderContext = Stage3D(event.target).context3D; + + renderContext.enableErrorChecking = true; // Can slow rendering - only turn on when developing/testing + renderContext.configureBackBuffer(viewWidth, viewHeight, 4, true); + + // Create vertex index list for the triangles + var triangles:Vector. = Vector.([0, 1, 2, 0, 2, 3]); + indexList = renderContext.createIndexBuffer(triangles.length); + indexList.uploadFromVector(triangles, 0, triangles.length); + + // Create vertexes + const dataPerVertex:int = 5; + var vertexData:Vector. = Vector.( + [ + // x, y, z u, v + 0, 0, 0, 0, 1, + 0.5, 0, 0, 1, 1, + 0.5, 0.5, 0, 1, 0, + 0, 0.5, 0, 0, 0 + ]); + vertexes = renderContext.createVertexBuffer(vertexData.length / dataPerVertex, dataPerVertex); + vertexes.uploadFromVector(vertexData, 0, vertexData.length / dataPerVertex); + + // Identify vertex data inputs for vertex program + renderContext.setVertexBufferAt(0, vertexes, 0, Context3DVertexBufferFormat.FLOAT_3); // va0 is position + renderContext.setVertexBufferAt(1, vertexes, 3, Context3DVertexBufferFormat.FLOAT_2); // va1 is texture uv coords + + var logo: BitmapData = new RUFFLE_LOGO().bitmapData; + + var bgraPixels = new ByteArray(); + // Test that we can skip over these first 4 bytse using byteArrayOffset + // during uploading to the texture + bgraPixels.writeInt(0xFFFFFFFF); + var temp = logo.getPixels(logo.rect); + bgraPixels.writeBytes(temp); + + // Convert from big endian to little endian + for (var i = 4; i < bgraPixels.length; i += 4) { + var first = bgraPixels[i]; + var second = bgraPixels[i + 1]; + var third = bgraPixels[i + 2]; + var fourth = bgraPixels[i + 3]; + bgraPixels[i] = fourth; + bgraPixels[i + 1] = third; + bgraPixels[i + 2] = second; + bgraPixels[i + 3] = first; + } + + renderContext.setBlendFactors(Context3DBlendFactor.SOURCE_ALPHA, Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA); + // Upload programs to render context + programPair = renderContext.createProgram(); + programPair.upload(vertexAssembly.agalcode, fragmentAssembly.agalcode); + renderContext.setProgram(programPair); + + var bitmapDataTexture = renderContext.createRectangleTexture(logo.width, logo.height, "bgra", false); + bitmapDataTexture.uploadFromBitmapData(logo); + + var byteArrayTexture = renderContext.createRectangleTexture(logo.width, logo.height, "bgra", false); + byteArrayTexture.uploadFromByteArray(bgraPixels, 4); + + var circleATF: ByteArray = new CIRCLE_ATF(); + var atfTexture = renderContext.createTexture(512, 512, "bgra", false); + atfTexture.uploadCompressedTextureFromByteArray(circleATF, 0); + + + // Clear, setting stencil to 0 + renderContext.clear(.3, .3, .3, 1, 1, 0); + + + renderContext.setTextureAt(0, bitmapDataTexture); + renderContext.setProgramConstantsFromVector("vertex", 0, Vector.([-0.7, 0.5, 0.0, 0.0])); + renderContext.drawTriangles(indexList, 0, 2); + + renderContext.setTextureAt(0, byteArrayTexture); + renderContext.setProgramConstantsFromVector("vertex", 0, Vector.([-0.7, -0.6, 0.0, 0.0])); + renderContext.drawTriangles(indexList, 0, 2); + + renderContext.setTextureAt(0, atfTexture); + renderContext.setProgramConstantsFromVector("vertex", 0, Vector.([0.0, 0.5, 0.0, 0.0])); + renderContext.drawTriangles(indexList, 0, 2); + + + renderContext.present(); + + // this.addChild(new Bitmap(redGreen)); + } + } +} diff --git a/tests/tests/swfs/avm2/stage3d_texture_bytearray/circle.atf b/tests/tests/swfs/avm2/stage3d_texture_bytearray/circle.atf new file mode 100644 index 000000000..c326debd4 Binary files /dev/null and b/tests/tests/swfs/avm2/stage3d_texture_bytearray/circle.atf differ diff --git a/tests/tests/swfs/avm2/stage3d_texture_bytearray/circle.png b/tests/tests/swfs/avm2/stage3d_texture_bytearray/circle.png new file mode 100644 index 000000000..d4a89a74f Binary files /dev/null and b/tests/tests/swfs/avm2/stage3d_texture_bytearray/circle.png differ diff --git a/tests/tests/swfs/avm2/stage3d_texture_bytearray/com/adobe/utils/AGALMiniAssembler.as b/tests/tests/swfs/avm2/stage3d_texture_bytearray/com/adobe/utils/AGALMiniAssembler.as new file mode 100644 index 000000000..3ec2e00e4 --- /dev/null +++ b/tests/tests/swfs/avm2/stage3d_texture_bytearray/com/adobe/utils/AGALMiniAssembler.as @@ -0,0 +1,805 @@ +/* +Copyright (c) 2011, Adobe Systems Incorporated +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of Adobe Systems Incorporated nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package com.adobe.utils +{ + // =========================================================================== + // Imports + // --------------------------------------------------------------------------- + import flash.display3D.*; + import flash.utils.*; + + // =========================================================================== + // Class + // --------------------------------------------------------------------------- + public class AGALMiniAssembler + { // ====================================================================== + // Constants + // ---------------------------------------------------------------------- + protected static const REGEXP_OUTER_SPACES:RegExp = /^\s+|\s+$/g; + + // ====================================================================== + // Properties + // ---------------------------------------------------------------------- + // AGAL bytes and error buffer + private var _agalcode:ByteArray = null; + private var _error:String = ""; + + private var debugEnabled:Boolean = false; + + private static var initialized:Boolean = false; + public var verbose:Boolean = false; + + // ====================================================================== + // Getters + // ---------------------------------------------------------------------- + public function get error():String { return _error; } + public function get agalcode():ByteArray { return _agalcode; } + + // ====================================================================== + // Constructor + // ---------------------------------------------------------------------- + public function AGALMiniAssembler( debugging:Boolean = false ):void + { + debugEnabled = debugging; + if ( !initialized ) + init(); + } + // ====================================================================== + // Methods + // ---------------------------------------------------------------------- + + public function assemble2( ctx3d : Context3D, version:uint, vertexsrc:String, fragmentsrc:String ) : Program3D + { + var agalvertex : ByteArray = assemble ( VERTEX, vertexsrc, version ); + var agalfragment : ByteArray = assemble ( FRAGMENT, fragmentsrc, version ); + var prog : Program3D = ctx3d.createProgram(); + prog.upload(agalvertex,agalfragment); + return prog; + } + + public function assemble( mode:String, source:String, version:uint=1, ignorelimits:Boolean=false ):ByteArray + { + var start:uint = getTimer(); + + _agalcode = new ByteArray(); + _error = ""; + + var isFrag:Boolean = false; + + if ( mode == FRAGMENT ) + isFrag = true; + else if ( mode != VERTEX ) + _error = 'ERROR: mode needs to be "' + FRAGMENT + '" or "' + VERTEX + '" but is "' + mode + '".'; + + agalcode.endian = Endian.LITTLE_ENDIAN; + agalcode.writeByte( 0xa0 ); // tag version + agalcode.writeUnsignedInt( version ); // AGAL version, big endian, bit pattern will be 0x01000000 + agalcode.writeByte( 0xa1 ); // tag program id + agalcode.writeByte( isFrag ? 1 : 0 ); // vertex or fragment + + initregmap(version, ignorelimits); + + var lines:Array = source.replace( /[\f\n\r\v]+/g, "\n" ).split( "\n" ); + var nest:int = 0; + var nops:int = 0; + var i:int; + var lng:int = lines.length; + + for ( i = 0; i < lng && _error == ""; i++ ) + { + var line:String = new String( lines[i] ); + line = line.replace( REGEXP_OUTER_SPACES, "" ); + + // remove comments + var startcomment:int = line.search( "//" ); + if ( startcomment != -1 ) + line = line.slice( 0, startcomment ); + + // grab options + var optsi:int = line.search( /<.*>/g ); + var opts:Array; + if ( optsi != -1 ) + { + opts = line.slice( optsi ).match( /([\w\.\-\+]+)/gi ); + line = line.slice( 0, optsi ); + } + + // find opcode + var opCode:Array = line.match( /^\w{3}/ig ); + if ( !opCode ) + { + if ( line.length >= 3 ) + trace( "warning: bad line "+i+": "+lines[i] ); + continue; + } + var opFound:OpCode = OPMAP[ opCode[0] ]; + + // if debug is enabled, output the opcodes + if ( debugEnabled ) + trace( opFound ); + + if ( opFound == null ) + { + if ( line.length >= 3 ) + trace( "warning: bad line "+i+": "+lines[i] ); + continue; + } + + line = line.slice( line.search( opFound.name ) + opFound.name.length ); + + if ( ( opFound.flags & OP_VERSION2 ) && version<2 ) + { + _error = "error: opcode requires version 2."; + break; + } + + if ( ( opFound.flags & OP_VERT_ONLY ) && isFrag ) + { + _error = "error: opcode is only allowed in vertex programs."; + break; + } + + if ( ( opFound.flags & OP_FRAG_ONLY ) && !isFrag ) + { + _error = "error: opcode is only allowed in fragment programs."; + break; + } + if ( verbose ) + trace( "emit opcode=" + opFound ); + + agalcode.writeUnsignedInt( opFound.emitCode ); + nops++; + + if ( nops > MAX_OPCODES ) + { + _error = "error: too many opcodes. maximum is "+MAX_OPCODES+"."; + break; + } + + // get operands, use regexp + var regs:Array; + + // will match both syntax + regs = line.match( /vc\[([vof][acostdip]?)(\d*)?(\.[xyzw](\+\d{1,3})?)?\](\.[xyzw]{1,4})?|([vof][acostdip]?)(\d*)?(\.[xyzw]{1,4})?/gi ); + + if ( !regs || regs.length != opFound.numRegister ) + { + _error = "error: wrong number of operands. found "+regs.length+" but expected "+opFound.numRegister+"."; + break; + } + + var badreg:Boolean = false; + var pad:uint = 64 + 64 + 32; + var regLength:uint = regs.length; + + for ( var j:int = 0; j < regLength; j++ ) + { + var isRelative:Boolean = false; + var relreg:Array = regs[ j ].match( /\[.*\]/ig ); + if ( relreg && relreg.length > 0 ) + { + regs[ j ] = regs[ j ].replace( relreg[ 0 ], "0" ); + + if ( verbose ) + trace( "IS REL" ); + isRelative = true; + } + + var res:Array = regs[j].match( /^\b[A-Za-z]{1,2}/ig ); + if ( !res ) + { + _error = "error: could not parse operand "+j+" ("+regs[j]+")."; + badreg = true; + break; + } + var regFound:Register = REGMAP[ res[ 0 ] ]; + + // if debug is enabled, output the registers + if ( debugEnabled ) + trace( regFound ); + + if ( regFound == null ) + { + _error = "error: could not find register name for operand "+j+" ("+regs[j]+")."; + badreg = true; + break; + } + + if ( isFrag ) + { + if ( !( regFound.flags & REG_FRAG ) ) + { + _error = "error: register operand "+j+" ("+regs[j]+") only allowed in vertex programs."; + badreg = true; + break; + } + if ( isRelative ) + { + _error = "error: register operand "+j+" ("+regs[j]+") relative adressing not allowed in fragment programs."; + badreg = true; + break; + } + } + else + { + if ( !( regFound.flags & REG_VERT ) ) + { + _error = "error: register operand "+j+" ("+regs[j]+") only allowed in fragment programs."; + badreg = true; + break; + } + } + + regs[j] = regs[j].slice( regs[j].search( regFound.name ) + regFound.name.length ); + //trace( "REGNUM: " +regs[j] ); + var idxmatch:Array = isRelative ? relreg[0].match( /\d+/ ) : regs[j].match( /\d+/ ); + var regidx:uint = 0; + + if ( idxmatch ) + regidx = uint( idxmatch[0] ); + + if ( regFound.range < regidx ) + { + _error = "error: register operand "+j+" ("+regs[j]+") index exceeds limit of "+(regFound.range+1)+"."; + badreg = true; + break; + } + + var regmask:uint = 0; + var maskmatch:Array = regs[j].match( /(\.[xyzw]{1,4})/ ); + var isDest:Boolean = ( j == 0 && !( opFound.flags & OP_NO_DEST ) ); + var isSampler:Boolean = ( j == 2 && ( opFound.flags & OP_SPECIAL_TEX ) ); + var reltype:uint = 0; + var relsel:uint = 0; + var reloffset:int = 0; + + if ( isDest && isRelative ) + { + _error = "error: relative can not be destination"; + badreg = true; + break; + } + + if ( maskmatch ) + { + regmask = 0; + var cv:uint; + var maskLength:uint = maskmatch[0].length; + for ( var k:int = 1; k < maskLength; k++ ) + { + cv = maskmatch[0].charCodeAt(k) - "x".charCodeAt(0); + if ( cv > 2 ) + cv = 3; + if ( isDest ) + regmask |= 1 << cv; + else + regmask |= cv << ( ( k - 1 ) << 1 ); + } + if ( !isDest ) + for ( ; k <= 4; k++ ) + regmask |= cv << ( ( k - 1 ) << 1 ); // repeat last + } + else + { + regmask = isDest ? 0xf : 0xe4; // id swizzle or mask + } + + if ( isRelative ) + { + var relname:Array = relreg[0].match( /[A-Za-z]{1,2}/ig ); + var regFoundRel:Register = REGMAP[ relname[0]]; + if ( regFoundRel == null ) + { + _error = "error: bad index register"; + badreg = true; + break; + } + reltype = regFoundRel.emitCode; + var selmatch:Array = relreg[0].match( /(\.[xyzw]{1,1})/ ); + if ( selmatch.length==0 ) + { + _error = "error: bad index register select"; + badreg = true; + break; + } + relsel = selmatch[0].charCodeAt(1) - "x".charCodeAt(0); + if ( relsel > 2 ) + relsel = 3; + var relofs:Array = relreg[0].match( /\+\d{1,3}/ig ); + if ( relofs.length > 0 ) + reloffset = relofs[0]; + if ( reloffset < 0 || reloffset > 255 ) + { + _error = "error: index offset "+reloffset+" out of bounds. [0..255]"; + badreg = true; + break; + } + if ( verbose ) + trace( "RELATIVE: type="+reltype+"=="+relname[0]+" sel="+relsel+"=="+selmatch[0]+" idx="+regidx+" offset="+reloffset ); + } + + if ( verbose ) + trace( " emit argcode="+regFound+"["+regidx+"]["+regmask+"]" ); + if ( isDest ) + { + agalcode.writeShort( regidx ); + agalcode.writeByte( regmask ); + agalcode.writeByte( regFound.emitCode ); + pad -= 32; + } else + { + if ( isSampler ) + { + if ( verbose ) + trace( " emit sampler" ); + var samplerbits:uint = 5; // type 5 + var optsLength:uint = opts == null ? 0 : opts.length; + var bias:Number = 0; + for ( k = 0; k