diff --git a/core/src/avm2/globals/flash/display3D/index_buffer_3d.rs b/core/src/avm2/globals/flash/display3D/index_buffer_3d.rs index c2a5c1a42ad2..d69bfc044f31 100644 --- a/core/src/avm2/globals/flash/display3D/index_buffer_3d.rs +++ b/core/src/avm2/globals/flash/display3D/index_buffer_3d.rs @@ -62,10 +62,6 @@ pub fn upload_from_vector<'gc>( index_buffer.set_count(count as usize, activation.context.gc_context); - if start_offset != 0 { - panic!("What exactly does start_offset do?"); - } - let data: Result, _> = vector .iter() .map(|val| { diff --git a/core/src/avm2/object/context3d_object.rs b/core/src/avm2/object/context3d_object.rs index 21e1e307f9e7..7274fcdb68e2 100644 --- a/core/src/avm2/object/context3d_object.rs +++ b/core/src/avm2/object/context3d_object.rs @@ -182,6 +182,7 @@ impl<'gc> Context3DObject<'gc> { start_offset: usize, activation: &mut Activation<'_, 'gc>, ) { + let mut handle = buffer.handle(activation.context.gc_context); self.0 .write(activation.context.gc_context) .render_context @@ -189,7 +190,7 @@ impl<'gc> Context3DObject<'gc> { .unwrap() .process_command( Context3DCommand::UploadToIndexBuffer { - buffer: buffer.handle(), + buffer: &mut *handle, data, start_offset, }, @@ -279,6 +280,7 @@ impl<'gc> Context3DObject<'gc> { // FIXME - should we error if the number of indices isn't a multiple of 3? num_triangles = (index_buffer.count() / 3) as i32; } + let handle = index_buffer.handle(activation.context.gc_context); self.0 .write(activation.context.gc_context) @@ -287,7 +289,7 @@ impl<'gc> Context3DObject<'gc> { .unwrap() .process_command( Context3DCommand::DrawTriangles { - index_buffer: index_buffer.handle(), + index_buffer: &*handle, first_index: first_index as usize, num_triangles: num_triangles as isize, }, diff --git a/core/src/avm2/object/index_buffer_3d_object.rs b/core/src/avm2/object/index_buffer_3d_object.rs index fc4997f63ace..ac67bae6ea64 100644 --- a/core/src/avm2/object/index_buffer_3d_object.rs +++ b/core/src/avm2/object/index_buffer_3d_object.rs @@ -8,7 +8,6 @@ use crate::avm2::Error; use gc_arena::{Collect, GcCell, MutationContext}; use ruffle_render::backend::IndexBuffer; use std::cell::{Ref, RefMut}; -use std::rc::Rc; use super::Context3DObject; @@ -20,7 +19,7 @@ impl<'gc> IndexBuffer3DObject<'gc> { pub fn from_handle( activation: &mut Activation<'_, 'gc>, context3d: Context3DObject<'gc>, - handle: Rc, + handle: Box, ) -> Result, Error<'gc>> { let class = activation.avm2().classes().indexbuffer3d; let base = ScriptObjectData::new(class); @@ -50,8 +49,8 @@ impl<'gc> IndexBuffer3DObject<'gc> { self.0.write(mc).count = val; } - pub fn handle(&self) -> Rc { - self.0.read().handle.clone() + pub fn handle(&self, mc: MutationContext<'gc, '_>) -> RefMut<'_, dyn IndexBuffer> { + RefMut::map(self.0.write(mc), |data| &mut *data.handle) } pub fn context3d(&self) -> Context3DObject<'gc> { @@ -65,7 +64,7 @@ pub struct IndexBuffer3DObjectData<'gc> { /// Base script object base: ScriptObjectData<'gc>, - handle: Rc, + handle: Box, count: usize, diff --git a/render/src/backend.rs b/render/src/backend.rs index b9414309eab1..6c3eaae08cf3 100644 --- a/render/src/backend.rs +++ b/render/src/backend.rs @@ -192,7 +192,8 @@ pub trait Context3D: Collect + Downcast { // objects after dispose() has been called. fn disposed_vertex_buffer_handle(&self) -> Rc; - fn create_index_buffer(&mut self, usage: BufferUsage, num_indices: u32) -> Rc; + fn create_index_buffer(&mut self, usage: BufferUsage, num_indices: u32) + -> Box; fn create_vertex_buffer( &mut self, usage: BufferUsage, @@ -218,7 +219,7 @@ pub trait Context3D: Collect + Downcast { fn process_command<'gc>( &mut self, - command: Context3DCommand<'gc>, + command: Context3DCommand<'_, 'gc>, mc: MutationContext<'gc, '_>, ); } @@ -338,7 +339,7 @@ impl Context3DTextureFilter { #[derive(Collect)] #[collect(no_drop)] -pub enum Context3DCommand<'gc> { +pub enum Context3DCommand<'a, 'gc> { Clear { red: f64, green: f64, @@ -365,7 +366,7 @@ pub enum Context3DCommand<'gc> { SetRenderToBackBuffer, UploadToIndexBuffer { - buffer: Rc, + buffer: &'a mut dyn IndexBuffer, start_offset: usize, data: Vec, }, @@ -378,7 +379,7 @@ pub enum Context3DCommand<'gc> { }, DrawTriangles { - index_buffer: Rc, + index_buffer: &'a dyn IndexBuffer, first_index: usize, num_triangles: isize, }, diff --git a/render/wgpu/src/context3d/mod.rs b/render/wgpu/src/context3d/mod.rs index 708a660f58f2..5228c80884e0 100644 --- a/render/wgpu/src/context3d/mod.rs +++ b/render/wgpu/src/context3d/mod.rs @@ -298,7 +298,12 @@ impl WgpuContext3D { #[derive(Collect)] #[collect(require_static)] -pub struct IndexBufferWrapper(wgpu::Buffer); +pub struct IndexBufferWrapper { + pub buffer: wgpu::Buffer, + /// A cpu-side copy of the buffer data. This is used to allow us to + /// perform unaligned writes to the GPU buffer, which is required by ActionScript. + pub data: Vec, +} #[derive(Collect, Debug)] #[collect(require_static)] @@ -347,14 +352,18 @@ impl Context3D for WgpuContext3D { &mut self, _ruffle_usage: ruffle_render::backend::BufferUsage, num_indices: u32, - ) -> Rc { + ) -> Box { + let size = align_copy_buffer_size(num_indices as usize * std::mem::size_of::()) as u32; let buffer = self.descriptors.device.create_buffer(&BufferDescriptor { label: None, - size: num_indices as u64 * 2, + size: size as u64, usage: BufferUsages::INDEX | BufferUsages::COPY_DST, mapped_at_creation: false, }); - Rc::new(IndexBufferWrapper(buffer)) + Box::new(IndexBufferWrapper { + buffer, + data: vec![0; size as usize], + }) } fn create_vertex_buffer( @@ -459,7 +468,7 @@ impl Context3D for WgpuContext3D { fn process_command<'gc>( &mut self, - command: Context3DCommand<'gc>, + command: Context3DCommand<'_, 'gc>, mc: MutationContext<'gc, '_>, ) { match command { @@ -645,20 +654,40 @@ impl Context3D for WgpuContext3D { start_offset, data, } => { - let buffer: &IndexBufferWrapper = buffer - .as_any() - .downcast_ref::() + if data.is_empty() { + return; + } + let buffer: &mut IndexBufferWrapper = buffer + .as_any_mut() + .downcast_mut::() .unwrap(); + // Unfortunately, ActionScript works with 2-byte indices, while wgpu requires + // copy offsets and sizes to have 4-byte alignment. To support this, we need + // to keep a copy of the data on the CPU side. We round *down* the offset to + // the closest multiple of 4 bytes, and round *up* the length to the closest + // multiple of 4 bytes. We then perform a copy from our CPU-side buffer, which + // which uses the existing data (at the beiginning or end) to fill out the copy + // to the required length and offset. Without this, we would lose data in the CPU + // buffer whenever we performed a copy with an unalignd offset or length. + let offset_bytes = start_offset * std::mem::size_of::(); + let rounded_down_offset = + offset_bytes - (offset_bytes % COPY_BUFFER_ALIGNMENT as usize); + let rounded_up_length = align_copy_buffer_size(data.len()); + + buffer.data[offset_bytes..(offset_bytes + data.len())].copy_from_slice(&data); self.buffer_staging_belt .write_buffer( &mut self.buffer_command_encoder, - &buffer.0, - (start_offset * std::mem::size_of::()) as u64, - NonZeroU64::new(data.len() as u64).unwrap(), + &buffer.buffer, + rounded_down_offset as u64, + NonZeroU64::new(rounded_up_length as u64).unwrap(), &self.descriptors.device, ) - .copy_from_slice(&data); + .copy_from_slice( + &buffer.data + [rounded_down_offset..(rounded_down_offset + rounded_up_length)], + ); } Context3DCommand::UploadToVertexBuffer { @@ -667,21 +696,24 @@ impl Context3D for WgpuContext3D { data32_per_vertex, data, } => { + if data.is_empty() { + return; + } + let buffer: Rc = buffer .clone() .into_any_rc() .downcast::() .unwrap(); - let align = COPY_BUFFER_ALIGNMENT as usize; - let rounded_size = (data.len() + align - 1) & !(align - 1); - + // ActionScript can only work with 32-bit chunks of data, so our `write_buffer` + // offset and size will always be a multiple of `COPY_BUFFER_ALIGNMENT` (4 bytes) self.buffer_staging_belt.write_buffer( &mut self.buffer_command_encoder, &buffer.buffer, (start_vertex * (data32_per_vertex as usize) * std::mem::size_of::()) as u64, - NonZeroU64::new(rounded_size as u64).unwrap(), + NonZeroU64::new(data.len() as u64).unwrap(), &self.descriptors.device, )[..data.len()] .copy_from_slice(&data); @@ -805,7 +837,8 @@ impl Context3D for WgpuContext3D { let mut render_pass = self.make_render_pass(&mut render_command_encoder); - render_pass.set_index_buffer(index_buffer.0.slice(..), wgpu::IndexFormat::Uint16); + render_pass + .set_index_buffer(index_buffer.buffer.slice(..), wgpu::IndexFormat::Uint16); render_pass.draw_indexed(indices, 0, 0..1); // A `RenderPass` needs to hold references to several fields in `self`, so we can't @@ -1126,3 +1159,9 @@ fn convert_texture_format(input: Context3DTextureFormat) -> Result usize { + let align = COPY_BUFFER_ALIGNMENT as usize; + (len + align - 1) & !(align - 1) +} diff --git a/tests/tests/swfs/avm2/stage3d_triangle_index_upload/AGALMiniAssembler.as b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/AGALMiniAssembler.as new file mode 100644 index 000000000000..4c1d68b75a40 --- /dev/null +++ b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/AGALMiniAssembler.as @@ -0,0 +1,819 @@ +/* +Copyright (c) 2015, 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 +{ + // =========================================================================== + // 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\[([vofi][acostdip]?[d]?)(\d*)?((\.[xyzw])?(\+\d{1,3})?)?\](\.[xyzw]{1,4})?|([vofi][acostdip]?[d]?)(\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,3}/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,3}/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 = Vector.( [ 0, 1, 2, 0, 3, 4 ] ); + indexList = renderContext.createIndexBuffer( triangles.length ); + + indexList.uploadFromVector( triangles, 0, 1 ); + indexList.uploadFromVector( triangles.slice(1), 1, 2 ); + indexList.uploadFromVector( triangles.slice(3), 3, 3 ); + + //Create vertexes + const dataPerVertex:int = 8; + var vertexData:Vector. = Vector.( + [ + // x, y, z w r, g, b, w format + 0, 0, 0, 1, 1, 1, 1, 1, + -1, 1, 0, 1, 0, 0,.5, 1, + 1, 1, 0, 1, 0, 1, 1, 1, + 1,-1, 0, 1, .5, 0, 0, 1, + -1,-1, 0, 1, 1, 0, 0, 1 + ] + ); + 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_4 ); //va0 is position + renderContext.setVertexBufferAt( 1, vertexes, 4, Context3DVertexBufferFormat.FLOAT_4 ); //va1 is color + + //Upload programs to render context + programPair = renderContext.createProgram(); + programPair.upload( vertexAssembly.agalcode, fragmentAssembly.agalcode ); + renderContext.setProgram( programPair ); + + //Clear required before first drawTriangles() call + renderContext.clear( .3,.3,.3 ); + + + //Draw the 2 triangles + renderContext.drawTriangles( indexList, 0, 2 ); + + //var renderedBitmapData:BitmapData = new BitmapData( viewWidth, viewHeight, true ); + //renderContext.drawToBitmapData( renderedBitmapData ); + + renderContext.present(); + + /* + //Add to stage + bitmap = new Bitmap( renderedBitmapData ); + this.addChild( bitmap ); + bitmap.x = 55; + bitmap.y = 25; + bitmap.filters = [new DropShadowFilter( 8, 235, .4 )]; + */ + } + } +} diff --git a/tests/tests/swfs/avm2/stage3d_triangle_index_upload/expected.png b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/expected.png new file mode 100644 index 000000000000..6148499aabab Binary files /dev/null and b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/expected.png differ diff --git a/tests/tests/swfs/avm2/stage3d_triangle_index_upload/output.txt b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/output.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/tests/swfs/avm2/stage3d_triangle_index_upload/test.fla b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/test.fla new file mode 100644 index 000000000000..7a0f57d06c1d Binary files /dev/null and b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/test.fla differ diff --git a/tests/tests/swfs/avm2/stage3d_triangle_index_upload/test.swf b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/test.swf new file mode 100644 index 000000000000..95ace03f135d Binary files /dev/null and b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/test.swf differ diff --git a/tests/tests/swfs/avm2/stage3d_triangle_index_upload/test.toml b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/test.toml new file mode 100644 index 000000000000..d8a3f915ab19 --- /dev/null +++ b/tests/tests/swfs/avm2/stage3d_triangle_index_upload/test.toml @@ -0,0 +1,7 @@ +num_frames = 1 + +[image_comparison] +tolerance = 1 + +[player_options] +with_renderer = { optional = false, sample_count = 1 }