mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: split animation helpers by domain
This commit is contained in:
@@ -348,7 +348,6 @@ impl Compositor {
|
||||
) -> Result<(), CompositorError> {
|
||||
let frame = options.frame;
|
||||
self.texture_pool.recycle_frame();
|
||||
context.configure_surface(options.surface, frame.width, frame.height)?;
|
||||
let surface_texture = context.acquire_surface_texture(options.surface)?;
|
||||
let surface_view = surface_texture
|
||||
.texture
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
@@ -17,6 +20,12 @@ impl wgpu::rwh::HasDisplayHandle for WebDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
struct CachedCanvasSurface {
|
||||
surface: wgpu::Surface<'static>,
|
||||
size: (u32, u32),
|
||||
}
|
||||
|
||||
const BLIT_SHADER_SOURCE: &str = include_str!("shaders/blit.wgsl");
|
||||
|
||||
const FULLSCREEN_QUAD_POSITIONS: [[f32; 2]; 6] = [
|
||||
@@ -44,6 +53,8 @@ pub struct GpuContext {
|
||||
/// fallback path. Used by render_texture_via_gl_canvas to output frames on WebGL.
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
gl_canvas: Option<web_sys::HtmlCanvasElement>,
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
gl_surface: RefCell<Option<CachedCanvasSurface>>,
|
||||
}
|
||||
|
||||
impl GpuContext {
|
||||
@@ -170,6 +181,8 @@ impl GpuContext {
|
||||
supports_external_texture_copies,
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
gl_canvas,
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
gl_surface: RefCell::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -184,17 +197,15 @@ impl GpuContext {
|
||||
),
|
||||
GpuError,
|
||||
> {
|
||||
// Temporary fix: force the wasm renderer onto the WebGL backend even when
|
||||
// WebGPU is available while a WebGPU bug is being investigated.
|
||||
// let instance = wgpu::util::new_instance_with_webgpu_detection(
|
||||
// wgpu::InstanceDescriptor::new_without_display_handle(),
|
||||
// )
|
||||
// .await;
|
||||
let instance = wgpu::util::new_instance_with_webgpu_detection(
|
||||
wgpu::InstanceDescriptor::new_without_display_handle(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// match Self::try_request_device(&instance, None).await {
|
||||
// Ok((adapter, device, queue)) => return Ok((instance, adapter, device, queue, None)),
|
||||
// Err(_) => {}
|
||||
// }
|
||||
match Self::try_request_device(&instance, None).await {
|
||||
Ok((adapter, device, queue)) => return Ok((instance, adapter, device, queue, None)),
|
||||
Err(_) => {}
|
||||
}
|
||||
let (gl_instance, adapter, device, queue, canvas) = Self::try_gl_fallback().await?;
|
||||
Ok((gl_instance, adapter, device, queue, Some(canvas)))
|
||||
}
|
||||
@@ -353,6 +364,14 @@ impl GpuContext {
|
||||
self.supports_external_texture_copies
|
||||
}
|
||||
|
||||
/// The HTML canvas that owns the backing WebGL context, if running on the
|
||||
/// WebGL fallback. Callers on that path can mount this canvas directly
|
||||
/// instead of copying pixels out of it every frame.
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
pub fn gl_canvas(&self) -> Option<&web_sys::HtmlCanvasElement> {
|
||||
self.gl_canvas.as_ref()
|
||||
}
|
||||
|
||||
pub fn render_texture_to_surface(
|
||||
&self,
|
||||
texture: &wgpu::Texture,
|
||||
@@ -361,6 +380,14 @@ impl GpuContext {
|
||||
height: u32,
|
||||
) -> Result<(), GpuError> {
|
||||
self.configure_surface(surface, width, height)?;
|
||||
self.present_texture_to_surface(texture, surface)
|
||||
}
|
||||
|
||||
pub fn present_texture_to_surface(
|
||||
&self,
|
||||
texture: &wgpu::Texture,
|
||||
surface: &wgpu::Surface<'_>,
|
||||
) -> Result<(), GpuError> {
|
||||
let surface_texture = self.acquire_surface_texture(surface)?;
|
||||
let target_view = surface_texture
|
||||
.texture
|
||||
@@ -382,16 +409,38 @@ impl GpuContext {
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), GpuError> {
|
||||
let Some(config) = surface.get_default_config(&self.adapter, width, height) else {
|
||||
return Err(GpuError::UnsupportedSurfaceFormat);
|
||||
};
|
||||
if config.format != self.texture_format {
|
||||
return Err(GpuError::UnsupportedSurfaceFormat);
|
||||
}
|
||||
let config = self.build_surface_configuration(surface, width, height)?;
|
||||
surface.configure(&self.device, &config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_surface_configuration(
|
||||
&self,
|
||||
surface: &wgpu::Surface<'_>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<wgpu::SurfaceConfiguration, GpuError> {
|
||||
let caps = surface.get_capabilities(&self.adapter);
|
||||
if !caps.formats.contains(&self.texture_format) {
|
||||
return Err(GpuError::UnsupportedSurfaceFormat);
|
||||
}
|
||||
|
||||
Ok(wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: self.texture_format,
|
||||
width,
|
||||
height,
|
||||
present_mode: wgpu::PresentMode::Fifo,
|
||||
alpha_mode: caps
|
||||
.alpha_modes
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(wgpu::CompositeAlphaMode::Auto),
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn acquire_surface_texture(
|
||||
&self,
|
||||
surface: &wgpu::Surface<'_>,
|
||||
@@ -571,7 +620,7 @@ impl GpuContext {
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
fn render_texture_to_gl_canvas_surface(
|
||||
pub fn render_texture_to_gl_canvas_surface(
|
||||
&self,
|
||||
texture: &wgpu::Texture,
|
||||
width: u32,
|
||||
@@ -585,57 +634,29 @@ impl GpuContext {
|
||||
gl_canvas.set_width(width);
|
||||
gl_canvas.set_height(height);
|
||||
|
||||
let surface = self
|
||||
.instance
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(gl_canvas.clone()))?;
|
||||
|
||||
let caps = surface.get_capabilities(&self.adapter);
|
||||
let surface_format = if caps.formats.contains(&self.texture_format) {
|
||||
self.texture_format
|
||||
} else if !caps.formats.is_empty() {
|
||||
caps.formats[0]
|
||||
} else {
|
||||
return Err(GpuError::UnsupportedSurfaceFormat);
|
||||
let mut cached_surface = self.gl_surface.borrow_mut();
|
||||
let cached_surface = match cached_surface.as_mut() {
|
||||
Some(cached_surface) => cached_surface,
|
||||
None => {
|
||||
let surface = self
|
||||
.instance
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(gl_canvas.clone()))?;
|
||||
cached_surface.replace(CachedCanvasSurface {
|
||||
surface,
|
||||
size: (0, 0),
|
||||
});
|
||||
cached_surface
|
||||
.as_mut()
|
||||
.expect("gl_surface cache should exist after initialization")
|
||||
}
|
||||
};
|
||||
|
||||
if surface_format != self.texture_format {
|
||||
return Err(GpuError::UnsupportedSurfaceFormat);
|
||||
if cached_surface.size != (width, height) {
|
||||
self.configure_surface(&cached_surface.surface, width, height)?;
|
||||
cached_surface.size = (width, height);
|
||||
}
|
||||
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
width,
|
||||
height,
|
||||
present_mode: wgpu::PresentMode::Fifo,
|
||||
alpha_mode: caps
|
||||
.alpha_modes
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(wgpu::CompositeAlphaMode::Auto),
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
};
|
||||
surface.configure(&self.device, &config);
|
||||
|
||||
let surface_texture = self.acquire_surface_texture(&surface)?;
|
||||
let surface_view = surface_texture
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("gpu-gl-canvas-blit-encoder"),
|
||||
});
|
||||
self.encode_texture_blit_to_view(
|
||||
&mut encoder,
|
||||
texture,
|
||||
&surface_view,
|
||||
"gpu-gl-canvas-blit",
|
||||
);
|
||||
self.queue.submit([encoder.finish()]);
|
||||
surface_texture.present();
|
||||
self.present_texture_to_surface(texture, &cached_surface.surface)?;
|
||||
|
||||
Ok(gl_canvas)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "opencut-wasm"
|
||||
version = "0.2.9"
|
||||
version = "0.2.10"
|
||||
edition = "2024"
|
||||
description = "Shared video editor logic compiled to WebAssembly"
|
||||
repository = "https://github.com/opencut/opencut"
|
||||
@@ -24,7 +24,7 @@ serde-wasm-bindgen = "0.6.5"
|
||||
time = { version = "0.1.0", path = "../crates/time", features = ["wasm"] }
|
||||
wasm-bindgen = "0.2.116"
|
||||
wasm-bindgen-futures = "0.4.66"
|
||||
web-sys = { version = "0.3.93", features = ["OffscreenCanvas", "HtmlCanvasElement", "CanvasRenderingContext2d", "Document", "Window"] }
|
||||
web-sys = { version = "0.3.93", features = ["OffscreenCanvas", "HtmlCanvasElement", "CanvasRenderingContext2d", "Document", "Window", "Performance"] }
|
||||
|
||||
[features]
|
||||
default = ["wasm"]
|
||||
|
||||
+82
-34
@@ -11,10 +11,13 @@ use crate::gpu::{
|
||||
import_canvas_texture, read_offscreen_canvas_property, read_serde_property, read_u32_property,
|
||||
with_gpu_runtime,
|
||||
};
|
||||
use crate::perf;
|
||||
|
||||
struct CompositorRuntime {
|
||||
canvas: web_sys::HtmlCanvasElement,
|
||||
compositor: Compositor,
|
||||
surface: wgpu::Surface<'static>,
|
||||
surface_size: (u32, u32),
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -24,20 +27,42 @@ thread_local! {
|
||||
#[wasm_bindgen(js_name = initCompositor)]
|
||||
pub fn init_compositor(width: u32, height: u32) -> Result<(), JsValue> {
|
||||
with_gpu_runtime(|gpu_runtime| {
|
||||
let document = web_sys::window()
|
||||
.and_then(|window| window.document())
|
||||
.ok_or_else(|| JsValue::from_str("Document is not available"))?;
|
||||
let canvas: web_sys::HtmlCanvasElement = document
|
||||
.create_element("canvas")?
|
||||
.dyn_into()
|
||||
.map_err(|_| JsValue::from_str("Failed to create compositor canvas"))?;
|
||||
// On WebGL, wgpu is bound to a specific canvas; reuse it so the UI
|
||||
// can mount the output directly instead of copying pixels through
|
||||
// an intermediate 2D canvas every frame. On WebGPU, surface rendering
|
||||
// works against any canvas so we create a fresh one.
|
||||
let canvas = if let Some(gl_canvas) = gpu_runtime.context.gl_canvas() {
|
||||
gl_canvas.clone()
|
||||
} else {
|
||||
let document = web_sys::window()
|
||||
.and_then(|window| window.document())
|
||||
.ok_or_else(|| JsValue::from_str("Document is not available"))?;
|
||||
document
|
||||
.create_element("canvas")?
|
||||
.dyn_into::<web_sys::HtmlCanvasElement>()
|
||||
.map_err(|_| JsValue::from_str("Failed to create compositor canvas"))?
|
||||
};
|
||||
canvas.set_width(width);
|
||||
canvas.set_height(height);
|
||||
|
||||
let compositor = Compositor::new(&gpu_runtime.context);
|
||||
let surface = gpu_runtime
|
||||
.context
|
||||
.instance()
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
gpu_runtime
|
||||
.context
|
||||
.configure_surface(&surface, width, height)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
|
||||
COMPOSITOR_RUNTIME.with(|runtime| {
|
||||
runtime.replace(Some(CompositorRuntime { canvas, compositor }));
|
||||
runtime.replace(Some(CompositorRuntime {
|
||||
canvas,
|
||||
compositor,
|
||||
surface,
|
||||
surface_size: (width, height),
|
||||
}));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -46,16 +71,25 @@ pub fn init_compositor(width: u32, height: u32) -> Result<(), JsValue> {
|
||||
|
||||
#[wasm_bindgen(js_name = resizeCompositor)]
|
||||
pub fn resize_compositor(width: u32, height: u32) -> Result<(), JsValue> {
|
||||
COMPOSITOR_RUNTIME.with(|runtime| {
|
||||
let mut borrow = runtime.borrow_mut();
|
||||
let Some(runtime) = borrow.as_mut() else {
|
||||
return Err(JsValue::from_str(
|
||||
"Compositor is not initialized. Call initCompositor() first.",
|
||||
));
|
||||
};
|
||||
runtime.canvas.set_width(width);
|
||||
runtime.canvas.set_height(height);
|
||||
Ok(())
|
||||
with_gpu_runtime(|gpu_runtime| {
|
||||
COMPOSITOR_RUNTIME.with(|runtime| {
|
||||
let mut borrow = runtime.borrow_mut();
|
||||
let Some(runtime) = borrow.as_mut() else {
|
||||
return Err(JsValue::from_str(
|
||||
"Compositor is not initialized. Call initCompositor() first.",
|
||||
));
|
||||
};
|
||||
runtime.canvas.set_width(width);
|
||||
runtime.canvas.set_height(height);
|
||||
if runtime.surface_size != (width, height) {
|
||||
gpu_runtime
|
||||
.context
|
||||
.configure_surface(&runtime.surface, width, height)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
runtime.surface_size = (width, height);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,8 +153,12 @@ pub fn release_texture(id: String) -> Result<(), JsValue> {
|
||||
|
||||
#[wasm_bindgen(js_name = renderFrame)]
|
||||
pub fn render_frame(options: JsValue) -> Result<(), JsValue> {
|
||||
perf::reset();
|
||||
|
||||
let t_deserialize = perf::now_ms();
|
||||
let frame: FrameDescriptor = serde_wasm_bindgen::from_value(options)
|
||||
.map_err(|error| JsValue::from_str(&format!("Invalid frame descriptor: {error}")))?;
|
||||
perf::record("wasm.deserialize", perf::now_ms() - t_deserialize);
|
||||
|
||||
with_gpu_runtime(|gpu_runtime| {
|
||||
COMPOSITOR_RUNTIME.with(|runtime| {
|
||||
@@ -131,40 +169,50 @@ pub fn render_frame(options: JsValue) -> Result<(), JsValue> {
|
||||
));
|
||||
};
|
||||
|
||||
if gpu_runtime.context.supports_surface_rendering() {
|
||||
let surface = gpu_runtime
|
||||
if runtime.surface_size != (frame.width, frame.height) {
|
||||
runtime.canvas.set_width(frame.width);
|
||||
runtime.canvas.set_height(frame.height);
|
||||
let t_surface = perf::now_ms();
|
||||
gpu_runtime
|
||||
.context
|
||||
.instance()
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(runtime.canvas.clone()))
|
||||
.configure_surface(&runtime.surface, frame.width, frame.height)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
perf::record("wasm.surfaceConfigure", perf::now_ms() - t_surface);
|
||||
runtime.surface_size = (frame.width, frame.height);
|
||||
}
|
||||
|
||||
runtime
|
||||
if gpu_runtime.context.supports_surface_rendering() {
|
||||
let t_render = perf::now_ms();
|
||||
let result = runtime
|
||||
.compositor
|
||||
.render_frame(
|
||||
&gpu_runtime.context,
|
||||
RenderFrameOptions {
|
||||
frame: &frame,
|
||||
surface: &surface,
|
||||
surface: &runtime.surface,
|
||||
},
|
||||
)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()));
|
||||
perf::record("wasm.renderFrameToSurface", perf::now_ms() - t_render);
|
||||
result
|
||||
} else {
|
||||
// WebGL cannot surface-render to an arbitrary canvas element.
|
||||
// Composite to a texture, then blit to the canvas via the 2D context.
|
||||
// WebGL still needs a separate composition pass, but the output
|
||||
// surface is now persistent just like the WebGPU path.
|
||||
let t_composite = perf::now_ms();
|
||||
let texture = runtime
|
||||
.compositor
|
||||
.render_frame_to_texture(&gpu_runtime.context, &frame)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
perf::record("wasm.compositeToTexture", perf::now_ms() - t_composite);
|
||||
|
||||
let t_present = perf::now_ms();
|
||||
gpu_runtime
|
||||
.context
|
||||
.render_texture_via_gl_canvas(
|
||||
&texture,
|
||||
&runtime.canvas,
|
||||
frame.width,
|
||||
frame.height,
|
||||
)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))
|
||||
.present_texture_to_surface(&texture, &runtime.surface)
|
||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||
perf::record("wasm.presentToSurface", perf::now_ms() - t_present);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
//! Per-frame profile buffer for the render pipeline.
|
||||
//!
|
||||
//! Sub-span timings are recorded into a thread-local during `renderFrame`
|
||||
//! and drained by JS via `getLastFrameProfile()`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use js_sys::{Array, Object, Reflect};
|
||||
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
|
||||
|
||||
thread_local! {
|
||||
static LAST_FRAME_PROFILE: RefCell<Vec<(&'static str, f64)>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
pub(crate) fn now_ms() -> f64 {
|
||||
web_sys::window()
|
||||
.and_then(|window| window.performance())
|
||||
.map(|performance| performance.now())
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub(crate) fn reset() {
|
||||
LAST_FRAME_PROFILE.with(|cell| cell.borrow_mut().clear());
|
||||
}
|
||||
|
||||
pub(crate) fn record(name: &'static str, duration_ms: f64) {
|
||||
LAST_FRAME_PROFILE.with(|cell| cell.borrow_mut().push((name, duration_ms)));
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getLastFrameProfile)]
|
||||
pub fn get_last_frame_profile() -> Array {
|
||||
LAST_FRAME_PROFILE.with(|cell| {
|
||||
let entries = cell.borrow();
|
||||
let array = Array::new_with_length(entries.len() as u32);
|
||||
for (index, (name, duration_ms)) in entries.iter().enumerate() {
|
||||
let entry = Object::new();
|
||||
Reflect::set(&entry, &JsValue::from_str("name"), &JsValue::from_str(name))
|
||||
.expect("set name");
|
||||
Reflect::set(
|
||||
&entry,
|
||||
&JsValue::from_str("durationMs"),
|
||||
&JsValue::from_f64(*duration_ms),
|
||||
)
|
||||
.expect("set durationMs");
|
||||
array.set(index as u32, entry.into());
|
||||
}
|
||||
array
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,8 @@ mod effects;
|
||||
mod gpu;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod masks;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod perf;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use compositor::*;
|
||||
@@ -15,4 +17,6 @@ pub use effects::*;
|
||||
pub use gpu::*;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use masks::*;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use perf::*;
|
||||
pub use time::*;
|
||||
|
||||
Reference in New Issue
Block a user