core: Free render backend resources on `BitmapData.dispose`

Currently, all three render backends hold on texture-related
resources indefinitely (`register_bitmap` pushes to a `Vec`,
and never removes anything). As a result, the resources used
by the render backend (which may include GPU memory) will grow
over time, even if the corresponding `BitmapData` has been deallocated.

This commit adds a new `unregister_bitmap` method, which is called from
`BitmapData.dispose`. All render backs are changed to now use an
`FnvHashMap<BitmapHandle, _>` instead of a `Vec`, allowing us to
remove individual entries.

Currently, we only call `unregister_bitmap in response to
`BitmapData.dispose` - when `BitmapData` is freed by the
garbage collector, `unregister_bitmap` is *not* called.
This will be addressed in a future PR.
This commit is contained in:
Aaron Hill 2022-06-29 17:16:43 -05:00 committed by GitHub
parent 4d1bf7e00c
commit a79aa08f08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 123 additions and 54 deletions

3
Cargo.lock generated
View File

@ -3088,6 +3088,7 @@ dependencies = [
name = "ruffle_render_canvas"
version = "0.1.0"
dependencies = [
"fnv",
"js-sys",
"log",
"ruffle_core",
@ -3110,6 +3111,7 @@ name = "ruffle_render_webgl"
version = "0.1.0"
dependencies = [
"bytemuck",
"fnv",
"js-sys",
"log",
"ruffle_core",
@ -3126,6 +3128,7 @@ dependencies = [
"bytemuck",
"clap",
"enum-map",
"fnv",
"futures",
"image",
"log",

View File

@ -405,7 +405,7 @@ pub fn dispose<'gc>(
) -> Result<Value<'gc>, Error<'gc>> {
if let Some(bitmap_data) = this.as_bitmap_data_object() {
if !bitmap_data.disposed() {
bitmap_data.dispose(activation.context.gc_context);
bitmap_data.dispose(&mut activation.context);
return Ok(Value::Undefined);
}
}

View File

@ -1,5 +1,6 @@
use crate::add_field_accessors;
use crate::avm1::{Object, ScriptObject, TObject};
use crate::context::UpdateContext;
use crate::impl_custom_object;
use gc_arena::{Collect, GcCell, MutationContext};
@ -54,9 +55,11 @@ impl<'gc> BitmapDataObject<'gc> {
))
}
pub fn dispose(&self, gc_context: MutationContext<'gc, '_>) {
self.bitmap_data().write(gc_context).dispose();
self.0.write(gc_context).disposed = true;
pub fn dispose(&self, context: &mut UpdateContext<'_, 'gc, '_>) {
self.bitmap_data()
.write(context.gc_context)
.dispose(context.renderer);
self.0.write(context.gc_context).disposed = true;
}
}

View File

@ -86,6 +86,9 @@ pub trait RenderBackend: Downcast {
fn get_bitmap_pixels(&mut self, bitmap: BitmapHandle) -> Option<Bitmap>;
fn register_bitmap(&mut self, bitmap: Bitmap) -> Result<BitmapHandle, Error>;
// Frees memory used by the bitmap. After this call, `handle` can no longer
// be used.
fn unregister_bitmap(&mut self, handle: BitmapHandle) -> Result<(), Error>;
fn update_texture(
&mut self,
bitmap: BitmapHandle,
@ -177,6 +180,9 @@ impl RenderBackend for NullRenderer {
fn register_bitmap(&mut self, _bitmap: Bitmap) -> Result<BitmapHandle, Error> {
Ok(BitmapHandle(0))
}
fn unregister_bitmap(&mut self, _bitmap: BitmapHandle) -> Result<(), Error> {
Ok(())
}
fn update_texture(
&mut self,

View File

@ -168,11 +168,18 @@ impl<'gc> BitmapData<'gc> {
self.dirty = true;
}
pub fn dispose(&mut self) {
pub fn dispose(&mut self, renderer: &mut dyn RenderBackend) {
self.width = 0;
self.height = 0;
self.pixels.clear();
self.dirty = true;
if let Some(handle) = self.bitmap_handle {
if let Err(e) = renderer.unregister_bitmap(handle) {
log::warn!("Failed to unregister bitmap {:?}: {:?}", handle, e);
}
self.bitmap_handle = None;
}
// There's no longer a handle to update
self.dirty = false;
}
pub fn bitmap_handle(&mut self, renderer: &mut dyn RenderBackend) -> Option<BitmapHandle> {

View File

@ -10,6 +10,7 @@ js-sys = "0.3.57"
log = "0.4"
ruffle_web_common = { path = "../../web/common" }
wasm-bindgen = "=0.2.80"
fnv = "1.0.7"
[dependencies.ruffle_core]
path = "../../core"

View File

@ -1,3 +1,4 @@
use fnv::FnvHashMap;
use ruffle_core::backend::render::{
swf, Bitmap, BitmapFormat, BitmapHandle, BitmapSource, Color, NullBitmapSource, RenderBackend,
ShapeHandle, Transform,
@ -21,11 +22,12 @@ pub struct WebCanvasRenderBackend {
context: CanvasRenderingContext2d,
color_matrix: Element,
shapes: Vec<ShapeData>,
bitmaps: Vec<BitmapData>,
bitmaps: FnvHashMap<BitmapHandle, BitmapData>,
viewport_width: u32,
viewport_height: u32,
rect: Path2d,
mask_state: MaskState,
next_bitmap_handle: BitmapHandle,
}
/// Canvas-drawable shape data extracted from an SWF file.
@ -237,11 +239,12 @@ impl WebCanvasRenderBackend {
color_matrix,
context,
shapes: vec![],
bitmaps: vec![],
bitmaps: Default::default(),
viewport_width: 0,
viewport_height: 0,
rect,
mask_state: MaskState::DrawContent,
next_bitmap_handle: BitmapHandle(0),
};
Ok(renderer)
}
@ -369,7 +372,7 @@ impl RenderBackend for WebCanvasRenderBackend {
self.set_transform(&transform.matrix);
self.set_color_filter(transform);
if let Some(bitmap) = self.bitmaps.get(bitmap.0) {
if let Some(bitmap) = self.bitmaps.get(&bitmap) {
let _ = self
.context
.draw_image_with_html_canvas_element(&bitmap.canvas, 0.0, 0.0);
@ -590,17 +593,23 @@ impl RenderBackend for WebCanvasRenderBackend {
}
fn get_bitmap_pixels(&mut self, bitmap: BitmapHandle) -> Option<Bitmap> {
let bitmap = &self.bitmaps[bitmap.0];
let bitmap = &self.bitmaps[&bitmap];
bitmap.get_pixels()
}
fn register_bitmap(&mut self, bitmap: Bitmap) -> Result<BitmapHandle, Error> {
let handle = BitmapHandle(self.bitmaps.len());
let handle = self.next_bitmap_handle;
self.next_bitmap_handle = BitmapHandle(self.next_bitmap_handle.0 + 1);
let bitmap_data = BitmapData::new(bitmap)?;
self.bitmaps.push(bitmap_data);
self.bitmaps.insert(handle, bitmap_data);
Ok(handle)
}
fn unregister_bitmap(&mut self, bitmap: BitmapHandle) -> Result<(), Error> {
self.bitmaps.remove(&bitmap);
Ok(())
}
fn update_texture(
&mut self,
handle: BitmapHandle,
@ -610,8 +619,10 @@ impl RenderBackend for WebCanvasRenderBackend {
) -> Result<BitmapHandle, Error> {
// TODO: Could be optimized to a single put_image_data call
// in case it is already stored as a canvas+context.
self.bitmaps[handle.0] =
BitmapData::new(Bitmap::new(width, height, BitmapFormat::Rgba, rgba))?;
self.bitmaps.insert(
handle,
BitmapData::new(Bitmap::new(width, height, BitmapFormat::Rgba, rgba))?,
);
Ok(handle)
}
}
@ -647,7 +658,7 @@ fn draw_commands_to_path2d(commands: &[DrawCommand], is_closed: bool) -> Path2d
fn swf_shape_to_canvas_commands(
shape: &DistilledShape,
bitmap_source: &dyn BitmapSource,
bitmaps: &[BitmapData],
bitmaps: &FnvHashMap<BitmapHandle, BitmapData>,
context: &CanvasRenderingContext2d,
) -> ShapeData {
use ruffle_core::shape_utils::DrawPath;
@ -710,7 +721,7 @@ fn swf_shape_to_canvas_commands(
} => {
if let Some(bitmap) = bitmap_source
.bitmap(*id)
.and_then(|bitmap| bitmaps.get(bitmap.handle.0))
.and_then(|bitmap| bitmaps.get(&bitmap.handle))
{
let repeat = if !*is_repeating {
// NOTE: The WebGL backend does clamping in this case, just like

View File

@ -12,6 +12,7 @@ ruffle_render_common_tess = { path = "../common_tess" }
ruffle_web_common = { path = "../../web/common" }
wasm-bindgen = "=0.2.80"
bytemuck = { version = "1.9.1", features = ["derive"] }
fnv = "1.0.7"
[dependencies.ruffle_core]
path = "../../core"

View File

@ -1,4 +1,5 @@
use bytemuck::{Pod, Zeroable};
use fnv::FnvHashMap;
use ruffle_core::backend::render::{
Bitmap, BitmapFormat, BitmapHandle, BitmapSource, Color, NullBitmapSource, RenderBackend,
ShapeHandle, Transform,
@ -9,7 +10,6 @@ use ruffle_render_common_tess::{
Gradient as TessGradient, GradientType, ShapeTessellator, Vertex as TessVertex,
};
use ruffle_web_common::JsResult;
use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
HtmlCanvasElement, OesVertexArrayObject, WebGl2RenderingContext as Gl2, WebGlBuffer,
@ -75,7 +75,6 @@ pub struct WebGlRenderBackend {
shape_tessellator: ShapeTessellator,
textures: Vec<Texture>,
meshes: Vec<Mesh>,
color_quad_shape: ShapeHandle,
@ -95,7 +94,13 @@ pub struct WebGlRenderBackend {
renderbuffer_height: i32,
view_matrix: [[f32; 4]; 4],
bitmap_registry: HashMap<BitmapHandle, Bitmap>,
bitmap_registry: FnvHashMap<BitmapHandle, RegistryData>,
next_bitmap_handle: BitmapHandle,
}
struct RegistryData {
bitmap: Bitmap,
texture_wrapper: Texture,
}
const MAX_GRADIENT_COLORS: usize = 15;
@ -233,7 +238,6 @@ impl WebGlRenderBackend {
meshes: vec![],
color_quad_shape: ShapeHandle(0),
bitmap_quad_shape: ShapeHandle(1),
textures: vec![],
renderbuffer_width: 1,
renderbuffer_height: 1,
view_matrix: [[0.0; 4]; 4],
@ -247,7 +251,8 @@ impl WebGlRenderBackend {
blend_func: (Gl::ONE, Gl::ONE_MINUS_SRC_ALPHA),
mult_color: None,
add_color: None,
bitmap_registry: HashMap::new(),
bitmap_registry: Default::default(),
next_bitmap_handle: BitmapHandle(0),
};
let color_quad_mesh = renderer.build_quad_mesh(&renderer.color_program)?;
@ -821,7 +826,8 @@ impl RenderBackend for WebGlRenderBackend {
fn render_bitmap(&mut self, bitmap: BitmapHandle, transform: &Transform, smoothing: bool) {
self.set_stencil_state();
if let Some(bitmap) = self.textures.get(bitmap.0) {
if let Some(entry) = self.bitmap_registry.get(&bitmap) {
let bitmap = &entry.texture_wrapper;
let texture = &bitmap.texture;
// Adjust the quad draw to use the target bitmap.
let mesh = &self.meshes[self.bitmap_quad_shape.0];
@ -1019,8 +1025,8 @@ impl RenderBackend for WebGlRenderBackend {
);
}
DrawType::Bitmap(bitmap) => {
let texture = if let Some(texture) = self.textures.get(bitmap.handle.0) {
texture
let texture = if let Some(entry) = self.bitmap_registry.get(&bitmap.handle) {
&entry.texture_wrapper
} else {
// Bitmap not registered
continue;
@ -1168,7 +1174,7 @@ impl RenderBackend for WebGlRenderBackend {
}
fn get_bitmap_pixels(&mut self, bitmap: BitmapHandle) -> Option<Bitmap> {
self.bitmap_registry.get(&bitmap).cloned()
self.bitmap_registry.get(&bitmap).map(|e| e.bitmap.clone())
}
fn register_bitmap(&mut self, bitmap: Bitmap) -> Result<BitmapHandle, Error> {
@ -1203,20 +1209,30 @@ impl RenderBackend for WebGlRenderBackend {
self.gl
.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as i32);
let handle = BitmapHandle(self.textures.len());
let handle = self.next_bitmap_handle;
self.next_bitmap_handle = BitmapHandle(self.next_bitmap_handle.0 + 1);
let width = bitmap.width();
let height = bitmap.height();
self.bitmap_registry.insert(handle, bitmap);
self.textures.push(Texture {
self.bitmap_registry.insert(
handle,
RegistryData {
bitmap,
texture_wrapper: Texture {
width,
height,
texture,
});
},
},
);
Ok(handle)
}
fn unregister_bitmap(&mut self, bitmap: BitmapHandle) -> Result<(), Error> {
self.bitmap_registry.remove(&bitmap);
Ok(())
}
fn update_texture(
&mut self,
handle: BitmapHandle,
@ -1224,8 +1240,8 @@ impl RenderBackend for WebGlRenderBackend {
height: u32,
rgba: Vec<u8>,
) -> Result<BitmapHandle, Error> {
let texture = if let Some(texture) = self.textures.get(handle.0) {
texture
let texture = if let Some(entry) = self.bitmap_registry.get(&handle) {
&entry.texture_wrapper
} else {
return Err("update_texture: Bitmap is not regsitered".into());
};

View File

@ -13,6 +13,7 @@ bytemuck = { version = "1.9.1", features = ["derive"] }
raw-window-handle = "0.4"
clap = { version = "3.2.2", features = ["derive"], optional = true }
enum-map = "2.4.0"
fnv = "1.0.7"
# desktop
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.futures]

View File

@ -3,6 +3,7 @@ use crate::target::{RenderTarget, RenderTargetFrame, SwapChainTarget};
use crate::utils::{create_buffer_with_data, format_list, get_backend_names};
use bytemuck::{Pod, Zeroable};
use enum_map::Enum;
use fnv::FnvHashMap;
use ruffle_core::backend::render::{
Bitmap, BitmapHandle, BitmapSource, Color, RenderBackend, ShapeHandle, Transform,
};
@ -32,7 +33,6 @@ pub mod clap;
use crate::bitmaps::BitmapSamplers;
use crate::globals::Globals;
use crate::uniform_buffer::UniformBuffer;
use std::collections::HashMap;
use std::path::Path;
pub use wgpu;
@ -149,12 +149,17 @@ pub struct WgpuRenderBackend<T: RenderTarget> {
meshes: Vec<Mesh>,
mask_state: MaskState,
shape_tessellator: ShapeTessellator,
textures: Vec<Texture>,
num_masks: u32,
quad_vbo: wgpu::Buffer,
quad_ibo: wgpu::Buffer,
quad_tex_transforms: wgpu::Buffer,
bitmap_registry: HashMap<BitmapHandle, Bitmap>,
bitmap_registry: FnvHashMap<BitmapHandle, RegistryData>,
next_bitmap_handle: BitmapHandle,
}
struct RegistryData {
bitmap: Bitmap,
texture_wrapper: Texture,
}
#[allow(dead_code)]
@ -481,7 +486,6 @@ impl<T: RenderTarget> WgpuRenderBackend<T> {
current_frame: None,
meshes: Vec::new(),
shape_tessellator: ShapeTessellator::new(),
textures: Vec::new(),
num_masks: 0,
mask_state: MaskState::NoMask,
@ -489,7 +493,8 @@ impl<T: RenderTarget> WgpuRenderBackend<T> {
quad_vbo,
quad_ibo,
quad_tex_transforms,
bitmap_registry: HashMap::new(),
bitmap_registry: Default::default(),
next_bitmap_handle: BitmapHandle(0),
})
}
@ -658,8 +663,11 @@ impl<T: RenderTarget> WgpuRenderBackend<T> {
}
}
TessDrawType::Bitmap(bitmap) => {
let texture = self.textures.get(bitmap.bitmap.0).unwrap();
let texture_view = texture.texture.create_view(&Default::default());
let entry = self.bitmap_registry.get(&bitmap.bitmap).unwrap();
let texture_view = entry
.texture_wrapper
.texture
.create_view(&Default::default());
// TODO: Extract to function?
let mut texture_transform = [[0.0; 4]; 4];
@ -952,7 +960,8 @@ impl<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
}
fn render_bitmap(&mut self, bitmap: BitmapHandle, transform: &Transform, smoothing: bool) {
if let Some(texture) = self.textures.get(bitmap.0) {
if let Some(entry) = self.bitmap_registry.get(&bitmap) {
let texture = &entry.texture_wrapper;
let frame = if let Some(frame) = &mut self.current_frame {
frame.get()
} else {
@ -1320,7 +1329,7 @@ impl<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
}
fn get_bitmap_pixels(&mut self, bitmap: BitmapHandle) -> Option<Bitmap> {
self.bitmap_registry.get(&bitmap).cloned()
self.bitmap_registry.get(&bitmap).map(|e| e.bitmap.clone())
}
fn register_bitmap(&mut self, bitmap: Bitmap) -> Result<BitmapHandle, Error> {
@ -1361,7 +1370,8 @@ impl<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
extent,
);
let handle = BitmapHandle(self.textures.len());
let handle = self.next_bitmap_handle;
self.next_bitmap_handle = BitmapHandle(self.next_bitmap_handle.0 + 1);
let width = bitmap.width();
let height = bitmap.height();
@ -1391,17 +1401,27 @@ impl<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
label: create_debug_label!("Bitmap {} bind group", handle.0).as_deref(),
});
self.bitmap_registry.insert(handle, bitmap);
self.textures.push(Texture {
self.bitmap_registry.insert(
handle,
RegistryData {
bitmap,
texture_wrapper: Texture {
width,
height,
texture,
bind_group,
});
},
},
);
Ok(handle)
}
fn unregister_bitmap(&mut self, handle: BitmapHandle) -> Result<(), Error> {
self.bitmap_registry.remove(&handle);
Ok(())
}
fn update_texture(
&mut self,
handle: BitmapHandle,
@ -1409,8 +1429,8 @@ impl<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
height: u32,
rgba: Vec<u8>,
) -> Result<BitmapHandle, Error> {
let texture = if let Some(texture) = self.textures.get(handle.0) {
&texture.texture
let texture = if let Some(entry) = self.bitmap_registry.get(&handle) {
&entry.texture_wrapper.texture
} else {
return Err("update_texture: Bitmap not registered".into());
};