mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
fix: WebGL frame rendering and opencut-wasm 0.2.8
This commit is contained in:
Generated
+1
-1
@@ -3820,7 +3820,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opencut-wasm"
|
name = "opencut-wasm"
|
||||||
version = "0.2.6"
|
version = "0.2.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bridge",
|
"bridge",
|
||||||
"compositor",
|
"compositor",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
|
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString, Window,
|
||||||
prelude::*, px, rgb, size,
|
WindowBounds, WindowOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AppWindow {
|
struct AppWindow {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "16.1.3",
|
"next": "16.1.3",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"opencut-wasm": "^0.2.6",
|
"opencut-wasm": "^0.2.8",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import { useKeybindingsStore } from "@/stores/keybindings-store";
|
|||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
import { useEditorActions } from "@/hooks/actions/use-editor-actions";
|
import { useEditorActions } from "@/hooks/actions/use-editor-actions";
|
||||||
import { loadFontAtlas } from "@/lib/fonts/google-fonts";
|
import { loadFontAtlas } from "@/lib/fonts/google-fonts";
|
||||||
import { initializeGpuRenderer, isGpuAvailable } from "@/services/renderer/gpu-renderer";
|
import {
|
||||||
|
initializeGpuRenderer,
|
||||||
|
isGpuAvailable,
|
||||||
|
} from "@/services/renderer/gpu-renderer";
|
||||||
|
|
||||||
interface EditorProviderProps {
|
interface EditorProviderProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -62,9 +65,16 @@ export function EditorProvider({ projectId, children }: EditorProviderProps) {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setError(
|
const wasmPanic = (window as Window & { __wasmPanic?: string })
|
||||||
err instanceof Error ? err.message : "Failed to load project",
|
.__wasmPanic;
|
||||||
);
|
if (wasmPanic) {
|
||||||
|
delete (window as Window & { __wasmPanic?: string }).__wasmPanic;
|
||||||
|
setError(wasmPanic);
|
||||||
|
} else {
|
||||||
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to load project",
|
||||||
|
);
|
||||||
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,9 +167,13 @@ export class ProjectManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!project.metadata.thumbnail) {
|
if (!project.metadata.thumbnail) {
|
||||||
const didUpdateThumbnail = await this.updateThumbnailFromTimeline();
|
try {
|
||||||
if (didUpdateThumbnail) {
|
const didUpdateThumbnail = await this.updateThumbnailFromTimeline();
|
||||||
await this.saveCurrentProject();
|
if (didUpdateThumbnail) {
|
||||||
|
await this.saveCurrentProject();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to generate project thumbnail:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ class EffectPreviewService {
|
|||||||
this.loadPreviewImage();
|
this.loadPreviewImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
onPreviewImageReady({
|
onPreviewImageReady({ callback }: { callback: () => void }): () => void {
|
||||||
callback,
|
|
||||||
}: {
|
|
||||||
callback: () => void;
|
|
||||||
}): () => void {
|
|
||||||
this.onReadyCallbacks.add(callback);
|
this.onReadyCallbacks.add(callback);
|
||||||
return () => this.onReadyCallbacks.delete(callback);
|
return () => this.onReadyCallbacks.delete(callback);
|
||||||
}
|
}
|
||||||
@@ -39,35 +35,47 @@ class EffectPreviewService {
|
|||||||
uniformDimensions?: { width: number; height: number };
|
uniformDimensions?: { width: number; height: number };
|
||||||
}): void {
|
}): void {
|
||||||
const size = PREVIEW_SIZE;
|
const size = PREVIEW_SIZE;
|
||||||
const source = this.getTestSource({ width: size, height: size });
|
|
||||||
if (!source) return;
|
|
||||||
|
|
||||||
const definition = effectsRegistry.get(effectType);
|
|
||||||
const resolvedParams =
|
|
||||||
Object.keys(params).length > 0
|
|
||||||
? params
|
|
||||||
: buildDefaultParamValues(definition.params);
|
|
||||||
|
|
||||||
const passes = resolveEffectPasses({
|
|
||||||
definition,
|
|
||||||
effectParams: resolvedParams,
|
|
||||||
width: uniformDimensions?.width ?? size,
|
|
||||||
height: uniformDimensions?.height ?? size,
|
|
||||||
});
|
|
||||||
const result = this.applyGpuEffect({
|
|
||||||
source,
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
passes,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetCtx = targetCanvas.getContext(
|
const targetCtx = targetCanvas.getContext(
|
||||||
"2d",
|
"2d",
|
||||||
) as CanvasRenderingContext2D | null;
|
) as CanvasRenderingContext2D | null;
|
||||||
if (targetCtx) {
|
if (!targetCtx) {
|
||||||
targetCanvas.width = size;
|
return;
|
||||||
targetCanvas.height = size;
|
}
|
||||||
|
|
||||||
|
targetCanvas.width = size;
|
||||||
|
targetCanvas.height = size;
|
||||||
|
|
||||||
|
const source = this.getTestSource({ width: size, height: size });
|
||||||
|
if (!source) {
|
||||||
|
targetCtx.clearRect(0, 0, size, size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const definition = effectsRegistry.get(effectType);
|
||||||
|
const resolvedParams =
|
||||||
|
Object.keys(params).length > 0
|
||||||
|
? params
|
||||||
|
: buildDefaultParamValues(definition.params);
|
||||||
|
|
||||||
|
const passes = resolveEffectPasses({
|
||||||
|
definition,
|
||||||
|
effectParams: resolvedParams,
|
||||||
|
width: uniformDimensions?.width ?? size,
|
||||||
|
height: uniformDimensions?.height ?? size,
|
||||||
|
});
|
||||||
|
const result = this.applyGpuEffect({
|
||||||
|
source,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
passes,
|
||||||
|
});
|
||||||
|
|
||||||
targetCtx.drawImage(result, 0, 0, size, size);
|
targetCtx.drawImage(result, 0, 0, size, size);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to render effect preview", { effectType, error });
|
||||||
|
targetCtx.clearRect(0, 0, size, size);
|
||||||
|
targetCtx.drawImage(source, 0, 0, size, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "16.1.3",
|
"next": "16.1.3",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"opencut-wasm": "file:../../rust/wasm/pkg",
|
"opencut-wasm": "^0.2.8",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
@@ -785,7 +785,7 @@
|
|||||||
|
|
||||||
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="],
|
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
||||||
|
|
||||||
"@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="],
|
"@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="],
|
||||||
|
|
||||||
@@ -875,7 +875,7 @@
|
|||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
@@ -1359,6 +1359,8 @@
|
|||||||
|
|
||||||
"onnxruntime-web": ["onnxruntime-web@1.22.0-dev.20250409-89f8206ba4", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ=="],
|
"onnxruntime-web": ["onnxruntime-web@1.22.0-dev.20250409-89f8206ba4", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ=="],
|
||||||
|
|
||||||
|
"opencut-wasm": ["opencut-wasm@0.2.8", "", {}, "sha512-R1cXB4HuTC5RdbGRYD0Q2n9rWQWDRB7KMwGJnbjrhnnwxBbR5XDu/3tL7hlzb/RZ7VZcaX5OGHZ1QgL6ex/2cQ=="],
|
||||||
|
|
||||||
"p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="],
|
"p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="],
|
||||||
|
|
||||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
@@ -1721,8 +1723,6 @@
|
|||||||
|
|
||||||
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||||
|
|
||||||
"@opencut/web/opencut-wasm": ["opencut-wasm@file:rust/wasm/pkg", {}],
|
|
||||||
|
|
||||||
"@opencut/web/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
"@opencut/web/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
"@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||||
|
|||||||
@@ -288,6 +288,59 @@ impl Compositor {
|
|||||||
self.textures.remove(id);
|
self.textures.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Composites all frame items into a texture and returns it.
|
||||||
|
/// Used on backends that cannot surface-render to an arbitrary canvas (e.g. WebGL).
|
||||||
|
pub fn render_frame_to_texture(
|
||||||
|
&mut self,
|
||||||
|
context: &GpuContext,
|
||||||
|
frame: &FrameDescriptor,
|
||||||
|
) -> Result<wgpu::Texture, CompositorError> {
|
||||||
|
self.texture_pool.recycle_frame();
|
||||||
|
let mut encoder =
|
||||||
|
context
|
||||||
|
.device()
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("compositor-frame-encoder"),
|
||||||
|
});
|
||||||
|
let mut scene = self.create_cleared_texture(
|
||||||
|
context,
|
||||||
|
&mut encoder,
|
||||||
|
frame.width,
|
||||||
|
frame.height,
|
||||||
|
frame.clear.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
for item in &frame.items {
|
||||||
|
match item {
|
||||||
|
FrameItemDescriptor::Layer(layer) => {
|
||||||
|
let layer_texture = self.render_layer(context, &mut encoder, frame, layer)?;
|
||||||
|
scene = self.blend_texture(
|
||||||
|
context,
|
||||||
|
&mut encoder,
|
||||||
|
&scene,
|
||||||
|
&layer_texture,
|
||||||
|
layer.blend_mode,
|
||||||
|
frame.width,
|
||||||
|
frame.height,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
FrameItemDescriptor::SceneEffect { effect_pass_groups } => {
|
||||||
|
scene = self.apply_effect_groups(
|
||||||
|
context,
|
||||||
|
&mut encoder,
|
||||||
|
&scene,
|
||||||
|
frame.width,
|
||||||
|
frame.height,
|
||||||
|
effect_pass_groups,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.queue().submit([encoder.finish()]);
|
||||||
|
Ok(scene)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_frame(
|
pub fn render_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &GpuContext,
|
context: &GpuContext,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ struct LayerUniforms {
|
|||||||
opacity: f32,
|
opacity: f32,
|
||||||
flip_x: f32,
|
flip_x: f32,
|
||||||
flip_y: f32,
|
flip_y: f32,
|
||||||
|
_padding: vec2f,
|
||||||
}
|
}
|
||||||
|
|
||||||
@group(0) @binding(0) var source_texture: texture_2d<f32>;
|
@group(0) @binding(0) var source_texture: texture_2d<f32>;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ wgpu = "29.0.1"
|
|||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
wasm-bindgen = "0.2.116"
|
wasm-bindgen = "0.2.116"
|
||||||
web-sys = { version = "0.3.93", features = ["Document", "Window", "HtmlCanvasElement", "OffscreenCanvasRenderingContext2d", "ImageData"] }
|
web-sys = { version = "0.3.93", features = ["Document", "Window", "HtmlCanvasElement", "CanvasRenderingContext2d", "OffscreenCanvasRenderingContext2d", "ImageData"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
+161
-62
@@ -40,10 +40,17 @@ pub struct GpuContext {
|
|||||||
texture_sampler_bind_group_layout: wgpu::BindGroupLayout,
|
texture_sampler_bind_group_layout: wgpu::BindGroupLayout,
|
||||||
blit_pipeline: wgpu::RenderPipeline,
|
blit_pipeline: wgpu::RenderPipeline,
|
||||||
supports_external_texture_copies: bool,
|
supports_external_texture_copies: bool,
|
||||||
|
/// The HTML canvas that the WebGL context is bound to. Only populated on the WebGL
|
||||||
|
/// 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpuContext {
|
impl GpuContext {
|
||||||
pub async fn new() -> Result<Self, GpuError> {
|
pub async fn new() -> Result<Self, GpuError> {
|
||||||
|
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||||
|
let (instance, adapter, device, queue, gl_canvas) = Self::acquire_device().await?;
|
||||||
|
#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
|
||||||
let (instance, adapter, device, queue) = Self::acquire_device().await?;
|
let (instance, adapter, device, queue) = Self::acquire_device().await?;
|
||||||
let texture_format = if adapter.get_info().backend == wgpu::Backend::Gl {
|
let texture_format = if adapter.get_info().backend == wgpu::Backend::Gl {
|
||||||
wgpu::TextureFormat::Rgba8Unorm
|
wgpu::TextureFormat::Rgba8Unorm
|
||||||
@@ -161,9 +168,37 @@ impl GpuContext {
|
|||||||
texture_sampler_bind_group_layout,
|
texture_sampler_bind_group_layout,
|
||||||
blit_pipeline,
|
blit_pipeline,
|
||||||
supports_external_texture_copies,
|
supports_external_texture_copies,
|
||||||
|
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||||
|
gl_canvas,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||||
|
async fn acquire_device() -> Result<
|
||||||
|
(
|
||||||
|
wgpu::Instance,
|
||||||
|
wgpu::Adapter,
|
||||||
|
wgpu::Device,
|
||||||
|
wgpu::Queue,
|
||||||
|
Option<web_sys::HtmlCanvasElement>,
|
||||||
|
),
|
||||||
|
GpuError,
|
||||||
|
> {
|
||||||
|
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(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (gl_instance, adapter, device, queue, canvas) = Self::try_gl_fallback().await?;
|
||||||
|
Ok((gl_instance, adapter, device, queue, Some(canvas)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
|
||||||
async fn acquire_device()
|
async fn acquire_device()
|
||||||
-> Result<(wgpu::Instance, wgpu::Adapter, wgpu::Device, wgpu::Queue), GpuError> {
|
-> Result<(wgpu::Instance, wgpu::Adapter, wgpu::Device, wgpu::Queue), GpuError> {
|
||||||
let instance = wgpu::util::new_instance_with_webgpu_detection(
|
let instance = wgpu::util::new_instance_with_webgpu_detection(
|
||||||
@@ -180,8 +215,16 @@ impl GpuContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||||
async fn try_gl_fallback()
|
async fn try_gl_fallback() -> Result<
|
||||||
-> Result<(wgpu::Instance, wgpu::Adapter, wgpu::Device, wgpu::Queue), GpuError> {
|
(
|
||||||
|
wgpu::Instance,
|
||||||
|
wgpu::Adapter,
|
||||||
|
wgpu::Device,
|
||||||
|
wgpu::Queue,
|
||||||
|
web_sys::HtmlCanvasElement,
|
||||||
|
),
|
||||||
|
GpuError,
|
||||||
|
> {
|
||||||
let mut gl_desc = wgpu::InstanceDescriptor::new_without_display_handle();
|
let mut gl_desc = wgpu::InstanceDescriptor::new_without_display_handle();
|
||||||
gl_desc.backends = wgpu::Backends::GL;
|
gl_desc.backends = wgpu::Backends::GL;
|
||||||
gl_desc.display = Some(Box::new(WebDisplay));
|
gl_desc.display = Some(Box::new(WebDisplay));
|
||||||
@@ -196,11 +239,11 @@ impl GpuContext {
|
|||||||
.unchecked_into();
|
.unchecked_into();
|
||||||
canvas.set_width(1);
|
canvas.set_width(1);
|
||||||
canvas.set_height(1);
|
canvas.set_height(1);
|
||||||
let surface = gl_instance.create_surface(wgpu::SurfaceTarget::Canvas(canvas))?;
|
let surface = gl_instance.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))?;
|
||||||
|
|
||||||
let (adapter, device, queue) =
|
let (adapter, device, queue) =
|
||||||
Self::try_request_device(&gl_instance, Some(&surface)).await?;
|
Self::try_request_device(&gl_instance, Some(&surface)).await?;
|
||||||
Ok((gl_instance, adapter, device, queue))
|
Ok((gl_instance, adapter, device, queue, canvas))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
|
#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
|
||||||
@@ -302,6 +345,13 @@ impl GpuContext {
|
|||||||
&self.blit_pipeline
|
&self.blit_pipeline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the GPU backend can render to arbitrary canvas surfaces.
|
||||||
|
/// True for WebGPU, false for WebGL which can only surface-render to
|
||||||
|
/// the specific canvas its GL context was originally created on.
|
||||||
|
pub fn supports_surface_rendering(&self) -> bool {
|
||||||
|
self.supports_external_texture_copies
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_texture_to_surface(
|
pub fn render_texture_to_surface(
|
||||||
&self,
|
&self,
|
||||||
texture: &wgpu::Texture,
|
texture: &wgpu::Texture,
|
||||||
@@ -493,72 +543,18 @@ impl GpuContext {
|
|||||||
return self.render_texture_to_surface(texture, &surface, width, height);
|
return self.render_texture_to_surface(texture, &surface, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.readback_texture_to_offscreen_canvas(texture, canvas, width, height)
|
self.render_texture_to_offscreen_canvas_via_gl_canvas(texture, canvas, width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||||
fn readback_texture_to_offscreen_canvas(
|
fn render_texture_to_offscreen_canvas_via_gl_canvas(
|
||||||
&self,
|
&self,
|
||||||
texture: &wgpu::Texture,
|
texture: &wgpu::Texture,
|
||||||
canvas: &wgpu::web_sys::OffscreenCanvas,
|
canvas: &wgpu::web_sys::OffscreenCanvas,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
) -> Result<(), GpuError> {
|
) -> Result<(), GpuError> {
|
||||||
let buffer_size = (width * height * 4) as u64;
|
let gl_canvas = self.render_texture_to_gl_canvas_surface(texture, width, height)?;
|
||||||
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
|
||||||
label: Some("gpu-readback-buffer"),
|
|
||||||
size: buffer_size,
|
|
||||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut encoder = self
|
|
||||||
.device
|
|
||||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
||||||
label: Some("gpu-readback-encoder"),
|
|
||||||
});
|
|
||||||
encoder.copy_texture_to_buffer(
|
|
||||||
wgpu::TexelCopyTextureInfo {
|
|
||||||
texture,
|
|
||||||
mip_level: 0,
|
|
||||||
origin: wgpu::Origin3d::ZERO,
|
|
||||||
aspect: wgpu::TextureAspect::All,
|
|
||||||
},
|
|
||||||
wgpu::TexelCopyBufferInfo {
|
|
||||||
buffer: &buffer,
|
|
||||||
layout: wgpu::TexelCopyBufferLayout {
|
|
||||||
offset: 0,
|
|
||||||
bytes_per_row: Some(width * 4),
|
|
||||||
rows_per_image: Some(height),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wgpu::Extent3d {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
self.queue.submit([encoder.finish()]);
|
|
||||||
|
|
||||||
let slice = buffer.slice(..);
|
|
||||||
slice.map_async(wgpu::MapMode::Read, |_| {});
|
|
||||||
let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
|
|
||||||
|
|
||||||
let data = slice.get_mapped_range();
|
|
||||||
let mut rgba_bytes = data.to_vec();
|
|
||||||
drop(data);
|
|
||||||
buffer.unmap();
|
|
||||||
|
|
||||||
if self.texture_format == wgpu::TextureFormat::Bgra8Unorm {
|
|
||||||
for pixel in rgba_bytes.chunks_exact_mut(4) {
|
|
||||||
pixel.swap(0, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let clamped = wasm_bindgen::Clamped(&rgba_bytes[..]);
|
|
||||||
let image_data =
|
|
||||||
web_sys::ImageData::new_with_u8_clamped_array_and_sh(clamped, width, height)
|
|
||||||
.map_err(|_| GpuError::AdapterUnavailable)?;
|
|
||||||
|
|
||||||
let ctx: web_sys::OffscreenCanvasRenderingContext2d = canvas
|
let ctx: web_sys::OffscreenCanvasRenderingContext2d = canvas
|
||||||
.get_context("2d")
|
.get_context("2d")
|
||||||
@@ -566,7 +562,110 @@ impl GpuContext {
|
|||||||
.flatten()
|
.flatten()
|
||||||
.ok_or(GpuError::AdapterUnavailable)?
|
.ok_or(GpuError::AdapterUnavailable)?
|
||||||
.unchecked_into();
|
.unchecked_into();
|
||||||
ctx.put_image_data(&image_data, 0.0, 0.0)
|
ctx.clear_rect(0.0, 0.0, width as f64, height as f64);
|
||||||
|
ctx.draw_image_with_html_canvas_element(gl_canvas, 0.0, 0.0)
|
||||||
|
.map_err(|_| GpuError::AdapterUnavailable)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||||
|
fn render_texture_to_gl_canvas_surface(
|
||||||
|
&self,
|
||||||
|
texture: &wgpu::Texture,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> Result<&web_sys::HtmlCanvasElement, GpuError> {
|
||||||
|
let gl_canvas = self
|
||||||
|
.gl_canvas
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(GpuError::AdapterUnavailable)?;
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
if surface_format != self.texture_format {
|
||||||
|
return Err(GpuError::UnsupportedSurfaceFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
Ok(gl_canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a texture to an arbitrary HTML canvas on the WebGL backend.
|
||||||
|
///
|
||||||
|
/// WebGL can only surface-render to the canvas its GL context was originally created on.
|
||||||
|
/// This method renders the texture to the GL canvas, then uses drawImage to copy the
|
||||||
|
/// result to the target canvas — avoiding the async buffer readback issue entirely.
|
||||||
|
///
|
||||||
|
/// The HTML canvas default color space is sRGB, so the surface may report
|
||||||
|
/// `Rgba8UnormSrgb` as its preferred format even though our render textures use
|
||||||
|
/// `Rgba8Unorm`. We explicitly select `Rgba8Unorm` from the surface's supported
|
||||||
|
/// format list (WebGL2 supports both) to avoid the format mismatch.
|
||||||
|
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||||
|
pub fn render_texture_via_gl_canvas(
|
||||||
|
&self,
|
||||||
|
texture: &wgpu::Texture,
|
||||||
|
target_canvas: &web_sys::HtmlCanvasElement,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> Result<(), GpuError> {
|
||||||
|
let gl_canvas = self.render_texture_to_gl_canvas_surface(texture, width, height)?;
|
||||||
|
|
||||||
|
let ctx: web_sys::CanvasRenderingContext2d = target_canvas
|
||||||
|
.get_context("2d")
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.ok_or(GpuError::AdapterUnavailable)?
|
||||||
|
.unchecked_into();
|
||||||
|
ctx.draw_image_with_html_canvas_element(gl_canvas, 0.0, 0.0)
|
||||||
.map_err(|_| GpuError::AdapterUnavailable)?;
|
.map_err(|_| GpuError::AdapterUnavailable)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "opencut-wasm"
|
name = "opencut-wasm"
|
||||||
version = "0.2.5"
|
version = "0.2.8"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Shared video editor logic compiled to WebAssembly"
|
description = "Shared video editor logic compiled to WebAssembly"
|
||||||
repository = "https://github.com/opencut/opencut"
|
repository = "https://github.com/opencut/opencut"
|
||||||
|
|||||||
+34
-16
@@ -131,22 +131,41 @@ pub fn render_frame(options: JsValue) -> Result<(), JsValue> {
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let surface = gpu_runtime
|
if gpu_runtime.context.supports_surface_rendering() {
|
||||||
.context
|
let surface = gpu_runtime
|
||||||
.instance()
|
.context
|
||||||
.create_surface(wgpu::SurfaceTarget::Canvas(runtime.canvas.clone()))
|
.instance()
|
||||||
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
.create_surface(wgpu::SurfaceTarget::Canvas(runtime.canvas.clone()))
|
||||||
|
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||||
|
|
||||||
runtime
|
runtime
|
||||||
.compositor
|
.compositor
|
||||||
.render_frame(
|
.render_frame(
|
||||||
&gpu_runtime.context,
|
&gpu_runtime.context,
|
||||||
RenderFrameOptions {
|
RenderFrameOptions {
|
||||||
frame: &frame,
|
frame: &frame,
|
||||||
surface: &surface,
|
surface: &surface,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|error| JsValue::from_str(&error.to_string()))
|
.map_err(|error| JsValue::from_str(&error.to_string()))
|
||||||
|
} else {
|
||||||
|
// WebGL cannot surface-render to an arbitrary canvas element.
|
||||||
|
// Composite to a texture, then blit to the canvas via the 2D context.
|
||||||
|
let texture = runtime
|
||||||
|
.compositor
|
||||||
|
.render_frame_to_texture(&gpu_runtime.context, &frame)
|
||||||
|
.map_err(|error| JsValue::from_str(&error.to_string()))?;
|
||||||
|
|
||||||
|
gpu_runtime
|
||||||
|
.context
|
||||||
|
.render_texture_via_gl_canvas(
|
||||||
|
&texture,
|
||||||
|
&runtime.canvas,
|
||||||
|
frame.width,
|
||||||
|
frame.height,
|
||||||
|
)
|
||||||
|
.map_err(|error| JsValue::from_str(&error.to_string()))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -171,4 +190,3 @@ fn parse_upload_texture_options(value: JsValue) -> Result<UploadTextureOptions,
|
|||||||
height: read_u32_property(&object, "height")?,
|
height: read_u32_property(&object, "height")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-1
@@ -19,9 +19,27 @@ thread_local! {
|
|||||||
static GPU_RUNTIME: RefCell<Option<GpuRuntime>> = const { RefCell::new(None) };
|
static GPU_RUNTIME: RefCell<Option<GpuRuntime>> = const { RefCell::new(None) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_panic_hook() {
|
||||||
|
static SET_HOOK: std::sync::Once = std::sync::Once::new();
|
||||||
|
SET_HOOK.call_once(|| {
|
||||||
|
std::panic::set_hook(Box::new(|info| {
|
||||||
|
// Store the full panic message in window.__wasmPanic so the JS catch block
|
||||||
|
// can surface it instead of the opaque "Unreachable" WASM trap message.
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let _ = Reflect::set(
|
||||||
|
&window,
|
||||||
|
&JsValue::from_str("__wasmPanic"),
|
||||||
|
&JsValue::from_str(&info.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console_error_panic_hook::hook(info);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = initializeGpu)]
|
#[wasm_bindgen(js_name = initializeGpu)]
|
||||||
pub async fn initialize_gpu() -> Result<(), JsValue> {
|
pub async fn initialize_gpu() -> Result<(), JsValue> {
|
||||||
console_error_panic_hook::set_once();
|
set_panic_hook();
|
||||||
|
|
||||||
if GPU_RUNTIME.with(|runtime| runtime.borrow().is_some()) {
|
if GPU_RUNTIME.with(|runtime| runtime.borrow().is_some()) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
Reference in New Issue
Block a user