web: WebGL render backend (merge #520)

Initial implementation of a WebGL render backend.
Switch the web target to use WebGL by default. Put the canvas backend behind a feature flag.
This commit is contained in:
Mike Welsh 2020-05-02 11:55:34 -07:00 committed by GitHub
commit c586991a88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1947 additions and 270 deletions

20
Cargo.lock generated
View File

@ -1826,6 +1826,23 @@ dependencies = [
"ruffle_core 0.1.0", "ruffle_core 0.1.0",
] ]
[[package]]
name = "ruffle_render_webgl"
version = "0.1.0"
dependencies = [
"fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"jpeg-decoder 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)",
"js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"png 0.16.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ruffle_core 0.1.0",
"ruffle_render_common_tess 0.1.0",
"ruffle_web_common 0.1.0",
"wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
"web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "ruffle_render_wgpu" name = "ruffle_render_wgpu"
version = "0.1.0" version = "0.1.0"
@ -1870,6 +1887,7 @@ dependencies = [
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"ruffle_core 0.1.0", "ruffle_core 0.1.0",
"ruffle_render_canvas 0.1.0", "ruffle_render_canvas 0.1.0",
"ruffle_render_webgl 0.1.0",
"ruffle_web_common 0.1.0", "ruffle_web_common 0.1.0",
"url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1882,8 +1900,10 @@ dependencies = [
name = "ruffle_web_common" name = "ruffle_web_common"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
"web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]

View File

@ -10,6 +10,7 @@ members = [
"render/canvas", "render/canvas",
"render/wgpu", "render/wgpu",
"render/common_tess", "render/common_tess",
"render/webgl",
] ]
# Don't optimize build scripts and macros. # Don't optimize build scripts and macros.

View File

@ -8,15 +8,26 @@ use lyon::tessellation::{FillOptions, StrokeOptions};
use ruffle_core::backend::render::swf::{self, FillStyle, Twips}; use ruffle_core::backend::render::swf::{self, FillStyle, Twips};
use ruffle_core::shape_utils::{DrawCommand, DrawPath}; use ruffle_core::shape_utils::{DrawCommand, DrawPath};
pub fn tessellate_shape<F>(shape: &swf::Shape, get_bitmap_dimenions: F) -> Mesh pub struct ShapeTessellator {
fill_tess: FillTessellator,
stroke_tess: StrokeTessellator,
}
impl ShapeTessellator {
pub fn new() -> Self {
Self {
fill_tess: FillTessellator::new(),
stroke_tess: StrokeTessellator::new(),
}
}
pub fn tessellate_shape<F>(&mut self, shape: &swf::Shape, get_bitmap_dimensions: F) -> Mesh
where where
F: Fn(swf::CharacterId) -> Option<(u32, u32)>, F: Fn(swf::CharacterId) -> Option<(u32, u32)>,
{ {
let paths = ruffle_core::shape_utils::swf_shape_to_paths(shape); let paths = ruffle_core::shape_utils::swf_shape_to_paths(shape);
let mut mesh = Vec::new(); let mut mesh = Vec::new();
let mut fill_tess = FillTessellator::new();
let mut stroke_tess = StrokeTessellator::new();
let mut lyon_mesh: VertexBuffers<_, u32> = VertexBuffers::new(); let mut lyon_mesh: VertexBuffers<_, u32> = VertexBuffers::new();
fn flush_draw(draw: DrawType, mesh: &mut Mesh, lyon_mesh: &mut VertexBuffers<Vertex, u32>) { fn flush_draw(draw: DrawType, mesh: &mut Mesh, lyon_mesh: &mut VertexBuffers<Vertex, u32>) {
@ -36,17 +47,15 @@ where
match path { match path {
DrawPath::Fill { style, commands } => match style { DrawPath::Fill { style, commands } => match style {
FillStyle::Color(color) => { FillStyle::Color(color) => {
let color = [ let color = ((color.a as u32) << 24)
f32::from(color.r) / 255.0, | ((color.b as u32) << 16)
f32::from(color.g) / 255.0, | ((color.g as u32) << 8)
f32::from(color.b) / 255.0, | (color.r as u32);
f32::from(color.a) / 255.0,
];
let mut buffers_builder = let mut buffers_builder =
BuffersBuilder::new(&mut lyon_mesh, RuffleVertexCtor { color }); BuffersBuilder::new(&mut lyon_mesh, RuffleVertexCtor { color });
if let Err(e) = fill_tess.tessellate_path( if let Err(e) = self.fill_tess.tessellate_path(
&ruffle_path_to_lyon_path(commands, true), &ruffle_path_to_lyon_path(commands, true),
&FillOptions::even_odd(), &FillOptions::even_odd(),
&mut buffers_builder, &mut buffers_builder,
@ -61,12 +70,10 @@ where
let mut buffers_builder = BuffersBuilder::new( let mut buffers_builder = BuffersBuilder::new(
&mut lyon_mesh, &mut lyon_mesh,
RuffleVertexCtor { RuffleVertexCtor { color: 0xffff_ffff },
color: [1.0, 1.0, 1.0, 1.0],
},
); );
if let Err(e) = fill_tess.tessellate_path( if let Err(e) = self.fill_tess.tessellate_path(
&ruffle_path_to_lyon_path(commands, true), &ruffle_path_to_lyon_path(commands, true),
&FillOptions::even_odd(), &FillOptions::even_odd(),
&mut buffers_builder, &mut buffers_builder,
@ -89,12 +96,12 @@ where
} }
let gradient = Gradient { let gradient = Gradient {
gradient_type: 0, gradient_type: GradientType::Linear,
ratios, ratios,
colors, colors,
num_colors: gradient.records.len() as u32, num_colors: gradient.records.len() as u32,
matrix: swf_to_gl_matrix(gradient.matrix.clone()), matrix: swf_to_gl_matrix(gradient.matrix.clone()),
repeat_mode: 0, repeat_mode: gradient.spread,
focal_point: 0.0, focal_point: 0.0,
}; };
@ -105,12 +112,10 @@ where
let mut buffers_builder = BuffersBuilder::new( let mut buffers_builder = BuffersBuilder::new(
&mut lyon_mesh, &mut lyon_mesh,
RuffleVertexCtor { RuffleVertexCtor { color: 0xffff_ffff },
color: [1.0, 1.0, 1.0, 1.0],
},
); );
if let Err(e) = fill_tess.tessellate_path( if let Err(e) = self.fill_tess.tessellate_path(
&ruffle_path_to_lyon_path(commands, true), &ruffle_path_to_lyon_path(commands, true),
&FillOptions::even_odd(), &FillOptions::even_odd(),
&mut buffers_builder, &mut buffers_builder,
@ -133,12 +138,12 @@ where
} }
let gradient = Gradient { let gradient = Gradient {
gradient_type: 1, gradient_type: GradientType::Radial,
ratios, ratios,
colors, colors,
num_colors: gradient.records.len() as u32, num_colors: gradient.records.len() as u32,
matrix: swf_to_gl_matrix(gradient.matrix.clone()), matrix: swf_to_gl_matrix(gradient.matrix.clone()),
repeat_mode: 0, repeat_mode: gradient.spread,
focal_point: 0.0, focal_point: 0.0,
}; };
@ -152,12 +157,10 @@ where
let mut buffers_builder = BuffersBuilder::new( let mut buffers_builder = BuffersBuilder::new(
&mut lyon_mesh, &mut lyon_mesh,
RuffleVertexCtor { RuffleVertexCtor { color: 0xffff_ffff },
color: [1.0, 1.0, 1.0, 1.0],
},
); );
if let Err(e) = fill_tess.tessellate_path( if let Err(e) = self.fill_tess.tessellate_path(
&ruffle_path_to_lyon_path(commands, true), &ruffle_path_to_lyon_path(commands, true),
&FillOptions::even_odd(), &FillOptions::even_odd(),
&mut buffers_builder, &mut buffers_builder,
@ -180,12 +183,12 @@ where
} }
let gradient = Gradient { let gradient = Gradient {
gradient_type: 1, gradient_type: GradientType::Focal,
ratios, ratios,
colors, colors,
num_colors: gradient.records.len() as u32, num_colors: gradient.records.len() as u32,
matrix: swf_to_gl_matrix(gradient.matrix.clone()), matrix: swf_to_gl_matrix(gradient.matrix.clone()),
repeat_mode: 0, repeat_mode: gradient.spread,
focal_point: *focal_point, focal_point: *focal_point,
}; };
@ -201,12 +204,10 @@ where
let mut buffers_builder = BuffersBuilder::new( let mut buffers_builder = BuffersBuilder::new(
&mut lyon_mesh, &mut lyon_mesh,
RuffleVertexCtor { RuffleVertexCtor { color: 0xffff_ffff },
color: [1.0, 1.0, 1.0, 1.0],
},
); );
if let Err(e) = fill_tess.tessellate_path( if let Err(e) = self.fill_tess.tessellate_path(
&ruffle_path_to_lyon_path(commands, true), &ruffle_path_to_lyon_path(commands, true),
&FillOptions::even_odd(), &FillOptions::even_odd(),
&mut buffers_builder, &mut buffers_builder,
@ -216,7 +217,8 @@ where
continue; continue;
} }
let (bitmap_width, bitmap_height) = get_bitmap_dimenions(*id).unwrap_or((1, 1)); let (bitmap_width, bitmap_height) =
(get_bitmap_dimensions)(*id).unwrap_or((1, 1));
let bitmap = Bitmap { let bitmap = Bitmap {
matrix: swf_bitmap_to_gl_matrix( matrix: swf_bitmap_to_gl_matrix(
@ -237,12 +239,10 @@ where
commands, commands,
is_closed, is_closed,
} => { } => {
let color = [ let color = ((style.color.a as u32) << 24)
f32::from(style.color.r) / 255.0, | ((style.color.b as u32) << 16)
f32::from(style.color.g) / 255.0, | ((style.color.g as u32) << 8)
f32::from(style.color.b) / 255.0, | (style.color.r as u32);
f32::from(style.color.a) / 255.0,
];
let mut buffers_builder = let mut buffers_builder =
BuffersBuilder::new(&mut lyon_mesh, RuffleVertexCtor { color }); BuffersBuilder::new(&mut lyon_mesh, RuffleVertexCtor { color });
@ -276,7 +276,7 @@ where
options = options.with_miter_limit(limit); options = options.with_miter_limit(limit);
} }
if let Err(e) = stroke_tess.tessellate_path( if let Err(e) = self.stroke_tess.tessellate_path(
&ruffle_path_to_lyon_path(commands, is_closed), &ruffle_path_to_lyon_path(commands, is_closed),
&options, &options,
&mut buffers_builder, &mut buffers_builder,
@ -293,6 +293,13 @@ where
mesh mesh
} }
}
impl Default for ShapeTessellator {
fn default() -> Self {
Self::new()
}
}
type Mesh = Vec<Draw>; type Mesh = Vec<Draw>;
@ -311,18 +318,19 @@ pub enum DrawType {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Gradient { pub struct Gradient {
pub matrix: [[f32; 3]; 3], pub matrix: [[f32; 3]; 3],
pub gradient_type: i32, pub gradient_type: GradientType,
pub ratios: Vec<f32>, pub ratios: Vec<f32>,
pub colors: Vec<[f32; 4]>, pub colors: Vec<[f32; 4]>,
pub num_colors: u32, pub num_colors: u32,
pub repeat_mode: i32, pub repeat_mode: GradientSpread,
pub focal_point: f32, pub focal_point: f32,
} }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct Vertex { pub struct Vertex {
pub position: [f32; 2], pub position: [f32; 2],
pub color: [f32; 4], pub color: u32,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -411,7 +419,7 @@ fn ruffle_path_to_lyon_path(commands: Vec<DrawCommand>, is_closed: bool) -> Path
} }
struct RuffleVertexCtor { struct RuffleVertexCtor {
color: [f32; 4], color: u32,
} }
impl FillVertexConstructor<Vertex> for RuffleVertexCtor { impl FillVertexConstructor<Vertex> for RuffleVertexCtor {
@ -431,3 +439,12 @@ impl StrokeVertexConstructor<Vertex> for RuffleVertexCtor {
} }
} }
} }
pub use swf::GradientSpread;
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum GradientType {
Linear,
Radial,
Focal,
}

29
render/webgl/Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[package]
name = "ruffle_render_webgl"
version = "0.1.0"
authors = ["Mike Welsh <mwelsh@gmail.com>"]
edition = "2018"
[dependencies]
fnv = "1.0.3"
js-sys = "0.3.25"
log = "0.4"
percent-encoding = "2.1.0"
png = "0.16.3"
ruffle_render_common_tess = { path = "../common_tess" }
ruffle_web_common = { path = "../../web/common" }
wasm-bindgen = "0.2.57"
[dependencies.jpeg-decoder]
version = "0.1.18"
default-features = false
[dependencies.ruffle_core]
path = "../../core"
default-features = false
[dependencies.web-sys]
version = "0.3.34"
features = ["HtmlCanvasElement", "HtmlElement", "Node", "OesVertexArrayObject", "WebGlBuffer", "WebGlFramebuffer", "WebGlProgram",
"WebGlRenderbuffer", "WebGlRenderingContext", "WebGl2RenderingContext", "WebGlShader", "WebGlTexture", "WebGlUniformLocation",
"WebGlVertexArrayObject"]

View File

@ -0,0 +1,25 @@
#version 100
precision mediump float;
uniform mat4 view_matrix;
uniform mat4 world_matrix;
uniform vec4 mult_color;
uniform vec4 add_color;
uniform mat3 u_matrix;
uniform sampler2D u_texture;
varying vec2 frag_uv;
void main() {
vec4 color = texture2D(u_texture, frag_uv);
// Unmultiply alpha before apply color transform.
if( color.a > 0.0 ) {
color.rgb /= color.a;
color = mult_color * color + add_color;
color.rgb *= color.a;
}
gl_FragColor = color;
}

View File

@ -0,0 +1,13 @@
#version 100
precision mediump float;
uniform mat4 view_matrix;
uniform mat4 world_matrix;
uniform vec4 mult_color;
uniform vec4 add_color;
varying vec4 frag_color;
void main() {
gl_FragColor = frag_color;
}

View File

@ -0,0 +1,16 @@
#version 100
precision mediump float;
uniform mat4 view_matrix;
uniform mat4 world_matrix;
uniform vec4 mult_color;
uniform vec4 add_color;
attribute vec2 position;
attribute vec4 color;
varying vec4 frag_color;
void main() {
frag_color = color * mult_color + add_color;
gl_Position = view_matrix * world_matrix * vec4(position, 0.0, 1.0);
}

View File

@ -0,0 +1,94 @@
#version 100
precision mediump float;
uniform mat4 view_matrix;
uniform mat4 world_matrix;
uniform vec4 mult_color;
uniform vec4 add_color;
uniform mat3 u_matrix;
uniform int u_gradient_type;
uniform float u_ratios[8];
uniform vec4 u_colors[8];
uniform int u_num_colors;
uniform int u_repeat_mode;
uniform float u_focal_point;
varying vec2 frag_uv;
void main() {
float t;
if( u_gradient_type == 0 )
{
t = frag_uv.x;
}
else if( u_gradient_type == 1 )
{
t = length(frag_uv * 2.0 - 1.0);
}
else if( u_gradient_type == 2 )
{
vec2 uv = frag_uv * 2.0 - 1.0;
vec2 d = vec2(u_focal_point, 0.0) - uv;
float l = length(d);
d /= l;
t = l / (sqrt(1.0 - u_focal_point*u_focal_point*d.y*d.y) + u_focal_point*d.x);
}
if( u_repeat_mode == 0 )
{
// Clamp
t = clamp(t, 0.0, 1.0);
}
else if( u_repeat_mode == 1 )
{
// Repeat
t = fract(t);
}
else
{
// Mirror
if( t < 0.0 )
{
t = -t;
}
if( int(mod(t, 2.0)) == 0 ) {
t = fract(t);
} else {
t = 1.0 - fract(t);
}
}
// TODO: No non-constant array access in WebGL 1, so the following is kind of painful.
// We'd probably be better off passing in the gradient as a texture and sampling from there.
vec4 color;
float a;
if( t <= u_ratios[0] ) {
color = u_colors[0];
} else if( t <= u_ratios[1] ) {
a = (t - u_ratios[0]) / (u_ratios[1] - u_ratios[0]);
color = mix(u_colors[0], u_colors[1], a);
} else if( t <= u_ratios[2] ) {
a = (t - u_ratios[1]) / (u_ratios[2] - u_ratios[1]);
color = mix(u_colors[1], u_colors[2], a);
} else if( t <= u_ratios[3] ) {
a = (t - u_ratios[2]) / (u_ratios[3] - u_ratios[2]);
color = mix(u_colors[2], u_colors[3], a);
} else if( t <= u_ratios[4] ) {
a = (t - u_ratios[3]) / (u_ratios[4] - u_ratios[3]);
color = mix(u_colors[3], u_colors[4], a);
} else if( t <= u_ratios[5] ) {
a = (t - u_ratios[4]) / (u_ratios[5] - u_ratios[4]);
color = mix(u_colors[4], u_colors[5], a);
} else if( t <= u_ratios[6] ) {
a = (t - u_ratios[5]) / (u_ratios[6] - u_ratios[5]);
color = mix(u_colors[5], u_colors[6], a);
} else if( t <= u_ratios[7] ) {
a = (t - u_ratios[6]) / (u_ratios[7] - u_ratios[6]);
color = mix(u_colors[6], u_colors[7], a);
} else {
color = u_colors[7];
}
gl_FragColor = mult_color * color + add_color;
}

View File

@ -0,0 +1,18 @@
#version 100
precision mediump float;
uniform mat4 view_matrix;
uniform mat4 world_matrix;
uniform vec4 mult_color;
uniform vec4 add_color;
uniform mat3 u_matrix;
attribute vec2 position;
attribute vec4 color;
varying vec2 frag_uv;
void main() {
frag_uv = vec2(u_matrix * vec3(position, 1.0));
gl_Position = view_matrix * world_matrix * vec4(position, 0.0, 1.0);
}

1404
render/webgl/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,10 @@ edition = "2018"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features] [features]
default = ["console_error_panic_hook", "console_log", "webgl"]
lzma = ["ruffle_core/lzma"] lzma = ["ruffle_core/lzma"]
default = ["console_error_panic_hook", "console_log"] canvas = ["ruffle_render_canvas"]
webgl = ["ruffle_render_webgl"]
[dependencies] [dependencies]
byteorder = "1.3.4" byteorder = "1.3.4"
@ -19,8 +21,9 @@ fnv = "1.0.3"
generational-arena = "0.2.7" generational-arena = "0.2.7"
js-sys = "0.3.25" js-sys = "0.3.25"
log = "0.4" log = "0.4"
ruffle_render_canvas = { path = "../render/canvas" } ruffle_render_canvas = { path = "../render/canvas", optional = true }
ruffle_web_common = { path = "common" } ruffle_web_common = { path = "common" }
ruffle_render_webgl = { path = "../render/webgl", optional = true }
url = "2.1.1" url = "2.1.1"
wasm-bindgen = "0.2.57" wasm-bindgen = "0.2.57"
wasm-bindgen-futures = "0.4.4" wasm-bindgen-futures = "0.4.4"

View File

@ -5,5 +5,10 @@ authors = ["Mike Welsh <mwelsh@gmail.com>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
js-sys = "0.3.25"
log = "0.4" log = "0.4"
wasm-bindgen = "0.2.57" wasm-bindgen = "0.2.57"
[dependencies.web-sys]
version = "0.3.34"
features = ["Window"]

View File

@ -33,3 +33,14 @@ impl<T> JsResult<T> for Result<T, JsValue> {
self.map_err(|value| JsError { value }) self.map_err(|value| JsError { value })
} }
} }
/// Very bad way to guess if we're running on a tablet/mobile.
pub fn is_mobile_or_tablet() -> bool {
if let Some(window) = web_sys::window() {
if let Ok(val) = js_sys::Reflect::get(&window, &JsValue::from("orientation")) {
return !val.is_undefined();
}
}
false
}

View File

@ -6,8 +6,8 @@ mod navigator;
use crate::{audio::WebAudioBackend, input::WebInputBackend, navigator::WebNavigatorBackend}; use crate::{audio::WebAudioBackend, input::WebInputBackend, navigator::WebNavigatorBackend};
use generational_arena::{Arena, Index}; use generational_arena::{Arena, Index};
use js_sys::Uint8Array; use js_sys::Uint8Array;
use ruffle_core::backend::render::RenderBackend;
use ruffle_core::PlayerEvent; use ruffle_core::PlayerEvent;
use ruffle_render_canvas::WebCanvasRenderBackend;
use std::mem::drop; use std::mem::drop;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::{cell::RefCell, error::Error, num::NonZeroI32}; use std::{cell::RefCell, error::Error, num::NonZeroI32};
@ -101,7 +101,7 @@ impl Ruffle {
swf_data.copy_to(&mut data[..]); swf_data.copy_to(&mut data[..]);
let window = web_sys::window().ok_or_else(|| "Expected window")?; let window = web_sys::window().ok_or_else(|| "Expected window")?;
let renderer = Box::new(WebCanvasRenderBackend::new(&canvas)?); let renderer = create_renderer(&canvas)?;
let audio = Box::new(WebAudioBackend::new()?); let audio = Box::new(WebAudioBackend::new()?);
let navigator = Box::new(WebNavigatorBackend::new()); let navigator = Box::new(WebNavigatorBackend::new());
let input = Box::new(WebInputBackend::new(&canvas)); let input = Box::new(WebInputBackend::new(&canvas));
@ -468,3 +468,24 @@ impl Ruffle {
}); });
} }
} }
fn create_renderer(canvas: &HtmlCanvasElement) -> Result<Box<dyn RenderBackend>, Box<dyn Error>> {
#[cfg(not(any(feature = "canvas", feature = "webgl")))]
std::compile_error!("You must enable one of the render backend features (e.g., webgl).");
#[cfg(feature = "webgl")]
{
if let Ok(renderer) = ruffle_render_webgl::WebGlRenderBackend::new(canvas) {
return Ok(Box::new(renderer));
}
}
#[cfg(feature = "canvas")]
{
if let Ok(renderer) = ruffle_render_canvas::WebCanvasRenderBackend::new(canvas) {
return Ok(Box::new(renderer));
}
}
Err("Unable to create renderer".into())
}