web: Support wgpu on web

Add wgpu feature to web build (disabled by default currently).
This commit is contained in:
Mike Welsh 2021-09-08 17:54:13 -07:00
parent df11cd6a04
commit 4141909bcb
10 changed files with 159 additions and 52 deletions

3
Cargo.lock generated
View File

@ -3102,6 +3102,8 @@ dependencies = [
"raw-window-handle",
"ruffle_core",
"ruffle_render_common_tess",
"wasm-bindgen-futures",
"web-sys",
"wgpu",
]
@ -3140,6 +3142,7 @@ dependencies = [
"ruffle_core",
"ruffle_render_canvas",
"ruffle_render_webgl",
"ruffle_render_wgpu",
"ruffle_web_common",
"serde",
"thiserror",

View File

@ -6,22 +6,34 @@ edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
image = { version = "0.23.14", default-features = false }
log = "0.4"
ruffle_core = { path = "../../core" }
ruffle_core = { path = "../../core", default-features = false }
ruffle_render_common_tess = { path = "../common_tess" }
futures = "0.3.17"
bytemuck = { version = "1.7.0", features = ["derive"] }
raw-window-handle = "0.3.3"
clap = { version = "3.0.0-beta.5", optional = true }
enum-map = "1.1.1"
# wgpu desktop
# desktop
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.futures]
version = "0.3.17"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.image]
version = "0.23.14"
default-features = false
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.wgpu]
version = "0.11"
features = ["spirv"]
# wgpu wasm
# wasm
[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen-futures]
version = "0.4"
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
version = "0.3"
features = ["HtmlCanvasElement"]
[target.'cfg(target_arch = "wasm32")'.dependencies.wgpu]
version = "0.11"
features = ["spirv-web"]

View File

@ -5,11 +5,8 @@ use ruffle_core::backend::render::{
use ruffle_core::shape_utils::DistilledShape;
use ruffle_core::swf;
use std::{borrow::Cow, num::NonZeroU32};
use target::TextureTarget;
use bytemuck::{Pod, Zeroable};
use futures::executor::block_on;
use raw_window_handle::HasRawWindowHandle;
use crate::pipelines::Pipelines;
use crate::target::{RenderTarget, RenderTargetFrame, SwapChainTarget};
@ -276,7 +273,29 @@ enum DrawType {
}
impl WgpuRenderBackend<SwapChainTarget> {
pub fn for_window<W: HasRawWindowHandle>(
#[cfg(target_family = "wasm")]
pub async fn for_canvas(canvas: &web_sys::HtmlCanvasElement) -> Result<Self, Error> {
let instance = wgpu::Instance::new(wgpu::Backends::BROWSER_WEBGPU);
let surface = unsafe { instance.create_surface_from_canvas(canvas) };
let descriptors = Self::build_descriptors(
wgpu::Backends::BROWSER_WEBGPU,
instance,
Some(&surface),
wgpu::PowerPreference::HighPerformance,
None,
)
.await?;
let target = SwapChainTarget::new(
surface,
descriptors.surface_format,
(1, 1),
&descriptors.device,
);
Self::new(descriptors, target)
}
#[cfg(not(target_family = "wasm"))]
pub fn for_window<W: raw_window_handle::HasRawWindowHandle>(
window: &W,
size: (u32, u32),
backend: wgpu::Backends,
@ -291,7 +310,7 @@ impl WgpuRenderBackend<SwapChainTarget> {
}
let instance = wgpu::Instance::new(backend);
let surface = unsafe { instance.create_surface(window) };
let descriptors = block_on(Self::build_descriptors(
let descriptors = futures::executor::block_on(Self::build_descriptors(
backend,
instance,
Some(&surface),
@ -308,7 +327,8 @@ impl WgpuRenderBackend<SwapChainTarget> {
}
}
impl WgpuRenderBackend<TextureTarget> {
#[cfg(not(target_family = "wasm"))]
impl WgpuRenderBackend<target::TextureTarget> {
pub fn for_offscreen(
size: (u32, u32),
backend: wgpu::Backends,
@ -322,14 +342,14 @@ impl WgpuRenderBackend<TextureTarget> {
);
}
let instance = wgpu::Instance::new(backend);
let descriptors = block_on(Self::build_descriptors(
let descriptors = futures::executor::block_on(Self::build_descriptors(
backend,
instance,
None,
power_preference,
trace_path,
))?;
let target = TextureTarget::new(&descriptors.device, size);
let target = target::TextureTarget::new(&descriptors.device, size);
Self::new(descriptors, target)
}
}

View File

@ -1,7 +1,7 @@
#[cfg(not(target_family = "wasm"))]
use crate::utils::BufferDimensions;
use futures::executor::block_on;
use image::buffer::ConvertBuffer;
use image::{Bgra, ImageBuffer, RgbaImage};
#[cfg(not(target_family = "wasm"))]
use image::{buffer::ConvertBuffer, Bgra, ImageBuffer, RgbaImage};
use std::fmt::Debug;
pub trait RenderTargetFrame: Debug {
@ -109,6 +109,7 @@ impl RenderTarget for SwapChainTarget {
}
}
#[cfg(not(target_family = "wasm"))]
#[derive(Debug)]
pub struct TextureTarget {
size: wgpu::Extent3d,
@ -118,17 +119,21 @@ pub struct TextureTarget {
buffer_dimensions: BufferDimensions,
}
#[cfg(not(target_family = "wasm"))]
#[derive(Debug)]
pub struct TextureTargetFrame(wgpu::TextureView);
#[cfg(not(target_family = "wasm"))]
type BgraImage = ImageBuffer<Bgra<u8>, Vec<u8>>;
#[cfg(not(target_family = "wasm"))]
impl RenderTargetFrame for TextureTargetFrame {
fn view(&self) -> &wgpu::TextureView {
&self.0
}
}
#[cfg(not(target_family = "wasm"))]
impl TextureTarget {
pub fn new(device: &wgpu::Device, size: (u32, u32)) -> Self {
let buffer_dimensions = BufferDimensions::new(size.0 as usize, size.1 as usize);
@ -168,7 +173,7 @@ impl TextureTarget {
pub fn capture(&self, device: &wgpu::Device) -> Option<RgbaImage> {
let buffer_future = self.buffer.slice(..).map_async(wgpu::MapMode::Read);
device.poll(wgpu::Maintain::Wait);
match block_on(buffer_future) {
match futures::executor::block_on(buffer_future) {
Ok(()) => {
let map = self.buffer.slice(..).get_mapped_range();
let mut buffer = Vec::with_capacity(
@ -195,6 +200,7 @@ impl TextureTarget {
}
}
#[cfg(not(target_family = "wasm"))]
impl RenderTarget for TextureTarget {
type Frame = TextureTargetFrame;

View File

@ -1,8 +1,4 @@
use bytemuck::Pod;
use futures::{
executor::{LocalPool, LocalSpawner},
task::LocalSpawnExt,
};
use std::{convert::TryInto, marker::PhantomData, mem};
use wgpu::util::StagingBelt;
@ -13,8 +9,7 @@ pub struct UniformBuffer<T: Pod> {
blocks: Vec<Block>,
buffer_layout: wgpu::BindGroupLayout,
staging_belt: StagingBelt,
executor: LocalPool,
spawner: LocalSpawner,
executor: Executor,
aligned_uniforms_size: u32,
cur_block: usize,
cur_offset: u32,
@ -32,10 +27,6 @@ impl<T: Pod> UniformBuffer<T> {
/// Creates a new `UniformBuffer` with the given uniform layout.
pub fn new(buffer_layout: wgpu::BindGroupLayout, uniform_alignment: u32) -> Self {
// Create local executor for uniform uploads.
let executor = LocalPool::new();
let spawner = executor.spawner();
// Calculate alignment of uniforms.
let align_mask = uniform_alignment - 1;
let aligned_uniforms_size = (Self::UNIFORMS_SIZE as u32 + align_mask) & !align_mask;
@ -43,8 +34,7 @@ impl<T: Pod> UniformBuffer<T> {
Self {
blocks: Vec::with_capacity(8),
buffer_layout,
executor,
spawner,
executor: Executor::new(),
staging_belt: StagingBelt::new(u64::from(Self::BLOCK_SIZE) / 2),
aligned_uniforms_size,
cur_block: 0,
@ -63,7 +53,7 @@ impl<T: Pod> UniformBuffer<T> {
pub fn reset(&mut self) {
self.cur_block = 0;
self.cur_offset = 0;
let _ = self.spawner.spawn_local(self.staging_belt.recall());
self.executor.spawn_local(self.staging_belt.recall());
self.executor.run_until_stalled();
}
@ -146,3 +136,49 @@ struct Block {
buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
#[cfg(not(target_family = "wasm"))]
struct Executor {
executor: futures::executor::LocalPool,
spawner: futures::executor::LocalSpawner,
}
#[cfg(not(target_family = "wasm"))]
impl Executor {
fn new() -> Self {
let executor = futures::executor::LocalPool::new();
let spawner = executor.spawner();
Self { executor, spawner }
}
fn spawn_local<Fut>(&self, future: Fut)
where
Fut: std::future::Future<Output = ()> + 'static,
{
use futures::task::LocalSpawnExt;
let _ = self.spawner.spawn_local(future);
}
fn run_until_stalled(&mut self) {
self.executor.run_until_stalled();
}
}
#[cfg(target_family = "wasm")]
struct Executor;
#[cfg(target_family = "wasm")]
impl Executor {
fn new() -> Self {
Self
}
fn spawn_local<Fut>(&self, future: Fut)
where
Fut: std::future::Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
fn run_until_stalled(&mut self) {}
}

View File

@ -75,6 +75,7 @@ pub struct BufferDimensions {
}
impl BufferDimensions {
#[allow(dead_code)]
pub fn new(width: usize, height: usize) -> Self {
let bytes_per_pixel = size_of::<u32>();
let unpadded_bytes_per_row = width * bytes_per_pixel;

View File

@ -24,6 +24,7 @@ lzma = ["ruffle_core/lzma"]
# web features
canvas = ["ruffle_render_canvas"]
webgl = ["ruffle_render_webgl"]
wgpu = ["ruffle_render_wgpu"]
[dependencies]
byteorder = "1.4"
@ -36,6 +37,7 @@ log = { version = "0.4", features = ["serde"] }
ruffle_render_canvas = { path = "../render/canvas", optional = true }
ruffle_web_common = { path = "common" }
ruffle_render_webgl = { path = "../render/webgl", optional = true }
ruffle_render_wgpu = { path = "../render/wgpu", optional = true }
url = "2.2.2"
wasm-bindgen = { version = "=0.2.78", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4.28"
@ -55,7 +57,7 @@ version = "0.3.50"
features = [
"AddEventListenerOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioParam", "AudioProcessingEvent", "AudioContext", "AudioDestinationNode",
"AudioNode", "CanvasRenderingContext2d", "ChannelMergerNode", "ChannelSplitterNode", "CssStyleDeclaration", "Document",
"Element", "Event", "EventTarget", "GainNode", "HtmlCanvasElement", "HtmlElement", "HtmlImageElement", "MouseEvent",
"Element", "Event", "EventTarget", "GainNode", "Gpu", "HtmlCanvasElement", "HtmlElement", "HtmlImageElement", "MouseEvent",
"Navigator", "Node", "Performance", "PointerEvent", "ScriptProcessorNode", "UiEvent", "Window", "Location", "HtmlFormElement",
"KeyboardEvent", "Path2d", "CanvasGradient", "CanvasPattern", "SvgMatrix", "SvgsvgElement", "Response", "Request", "RequestInit",
"Blob", "BlobPropertyBag", "Storage", "WheelEvent", "ImageData"]

View File

@ -1230,8 +1230,9 @@ export class RufflePlayer extends HTMLElement {
}
protected debugPlayerInfo(): string {
return `Allows script access: ${this.options?.allowScriptAccess ?? false
}\n`;
return `Allows script access: ${
this.options?.allowScriptAccess ?? false
}\n`;
}
private setMetadata(metadata: MovieMetadata) {

View File

@ -6,7 +6,7 @@
"private": true,
"scripts": {
"build": "webpack",
"serve": "webpack serve"
"serve": "webpack serve --port 8081"
},
"dependencies": {
"ruffle-core": "^0.1.0"
@ -16,4 +16,4 @@
"style-loader": "^3.3.0",
"webpack-cli": "^4.8.0"
}
}
}

View File

@ -13,7 +13,7 @@ mod storage;
mod ui;
use generational_arena::{Arena, Index};
use js_sys::{Array, Function, Object, Uint8Array};
use js_sys::{Array, Function, Object, Promise, Uint8Array};
use ruffle_core::backend::{
audio::{AudioBackend, NullAudioBackend},
render::RenderBackend,
@ -205,22 +205,23 @@ pub struct Ruffle(Index);
#[wasm_bindgen]
impl Ruffle {
#[allow(clippy::new_ret_no_self)]
#[wasm_bindgen(constructor)]
pub fn new(
parent: HtmlElement,
js_player: JavascriptPlayer,
config: &JsValue,
) -> Result<Ruffle, JsValue> {
if RUFFLE_GLOBAL_PANIC.is_completed() {
// If an actual panic happened, then we can't trust the state it left us in.
// Prevent future players from loading so that they can inform the user about the error.
return Err("Ruffle is panicking!".into());
}
set_panic_handler();
pub fn new(parent: HtmlElement, js_player: JavascriptPlayer, config: &JsValue) -> Promise {
let config: Config = config.into_serde().unwrap_or_default();
wasm_bindgen_futures::future_to_promise(async move {
if RUFFLE_GLOBAL_PANIC.is_completed() {
// If an actual panic happened, then we can't trust the state it left us in.
// Prevent future players from loading so that they can inform the user about the error.
return Err("Ruffle is panicking!".into());
}
set_panic_handler();
Ruffle::new_internal(parent, js_player, config).map_err(|_| "Error creating player".into())
let ruffle = Ruffle::new_internal(parent, js_player, config)
.await
.map_err(|_| JsValue::from("Error creating player"))?;
Ok(JsValue::from(ruffle))
})
}
/// Stream an arbitrary movie file from (presumably) the Internet.
@ -450,7 +451,7 @@ impl Ruffle {
}
impl Ruffle {
fn new_internal(
async fn new_internal(
parent: HtmlElement,
js_player: JavascriptPlayer,
config: Config,
@ -461,7 +462,7 @@ impl Ruffle {
let window = web_sys::window().ok_or("Expected window")?;
let document = window.document().ok_or("Expected document")?;
let (canvas, renderer) = create_renderer(&document)?;
let (canvas, renderer) = create_renderer(&document).await?;
parent
.append_child(&canvas.clone().into())
.into_js_result()?;
@ -1216,12 +1217,37 @@ fn external_to_js_value(external: ExternalValue) -> JsValue {
}
}
fn create_renderer(
async fn create_renderer(
document: &web_sys::Document,
) -> Result<(HtmlCanvasElement, 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).");
// Try to create a backend, falling through to the next backend on failure.
// We must recreate the canvas each attempt, as only a single context may be created per canvas
// with `getContext`.
#[cfg(feature = "wgpu")]
{
// Check that we have access to WebGPU (navigator.gpu should exist).
if web_sys::window()
.ok_or(JsValue::FALSE)
.and_then(|window| js_sys::Reflect::has(&window.navigator(), &JsValue::from_str("gpu")))
.unwrap_or_default()
{
log::info!("Creating wgpu renderer...");
let canvas: HtmlCanvasElement = document
.create_element("canvas")
.into_js_result()?
.dyn_into()
.map_err(|_| "Expected HtmlCanvasElement")?;
match ruffle_render_wgpu::WgpuRenderBackend::for_canvas(&canvas).await {
Ok(renderer) => return Ok((canvas, Box::new(renderer))),
Err(error) => log::error!("Error creating wgpu renderer: {}", error),
}
}
}
// Try to create a backend, falling through to the next backend on failure.
// We must recreate the canvas each attempt, as only a single context may be created per canvas
// with `getContext`.