From 40febd9dd1224a15a3bc877e2fdf010c4c893e0e Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Fri, 19 Jan 2024 22:49:14 +0530 Subject: [PATCH] feat:: External webgpu surfaces / BYOW (#21835) This PR contains the implementation of the External webgpu surfaces / BYOW proposal. BYOW stands for "Bring your own window". Closes #21713 Adds `Deno.UnsafeWindowSurface` ( `--unstable-webgpu` API) to the `Deno` namespace: ```typescript class UnsafeWindowSurface { constructor( system: "cocoa" | "x11" | "win32", winHandle: Deno.PointerValue, displayHandle: Deno.PointerValue | null ); getContext(type: "webgpu"): GPUCanvasContext; present(): void; } ``` For the initial pass, I've opted to support the three major windowing systems. The parameters correspond to the table below: | system | winHandle | displayHandle | | ----------------- | ---------- | ------- | | "cocoa" (macOS) | `NSView*` | - | | "win32" (Windows) | `HWND` | `HINSTANCE` | | "x11" (Linux) | Xlib `Window` | Xlib `Display*` | Ecosystem support: - [x] deno_sdl2 (sdl2) - [mod.ts#L1209](https://github.com/littledivy/deno_sdl2/blob/7e177bc6524750a8849c25ce421798b2e71ec943/mod.ts#L1209) - [x] dwm (glfw) - https://github.com/deno-windowing/dwm/issues/29 - [ ] pane (winit)
Example ```typescript // A simple clear screen pass, colors based on mouse position. import { EventType, WindowBuilder } from "https://deno.land/x/sdl2@0.7.0/mod.ts"; const window = new WindowBuilder("sdl2 + deno + webgpu", 640, 480).build(); const [system, windowHandle, displayHandle] = window.rawHandle(); const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const context = Deno.createWindowSurface(system, windowHandle, displayHandle); context.configure({ device: device, format: "bgra8unorm", height: 480, width: 640, }); let r = 0.0; let g = 0.0; let b = 0.0; for (const event of window.events()) { if (event.type === EventType.Quit) { break; } else if (event.type === EventType.Draw) { const textureView = context.getCurrentTexture().createView(); const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ { view: textureView, clearValue: { r, g, b, a: 1.0 }, loadOp: "clear", storeOp: "store", }, ], }; const commandEncoder = device.createCommandEncoder(); const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); Deno.presentGPUCanvasContext(context); } if (event.type === EventType.MouseMotion) { r = event.x / 640; g = event.y / 480; b = 1.0 - r - g; } } ``` You can find more examples in the linked tracking issue.
--------- Signed-off-by: Divy Srivastava --- ext/webgpu/02_surface.js | 27 ++++++++- ext/webgpu/Cargo.toml | 2 +- ext/webgpu/byow.rs | 127 +++++++++++++++++++++++++++++++++++++++ ext/webgpu/lib.rs | 5 +- runtime/js/90_deno_ns.js | 6 +- 5 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 ext/webgpu/byow.rs diff --git a/ext/webgpu/02_surface.js b/ext/webgpu/02_surface.js index 319179dc1e..1c9751f623 100644 --- a/ext/webgpu/02_surface.js +++ b/ext/webgpu/02_surface.js @@ -16,6 +16,7 @@ const { ObjectPrototypeIsPrototypeOf, Symbol, SymbolFor, + TypeError, } = primordials; import * as webidl from "ext:deno_webidl/00_webidl.js"; @@ -166,8 +167,28 @@ function createCanvasContext(options) { return canvasContext; } -function presentGPUCanvasContext(ctx) { - ctx[_present](); +// External webgpu surfaces + +// TODO(@littledivy): This will extend `OffscreenCanvas` when we add it. +class UnsafeWindowSurface { + #ctx; + #surfaceRid; + + constructor(system, win, display) { + this.#surfaceRid = ops.op_webgpu_surface_create(system, win, display); + } + + getContext(context) { + if (context !== "webgpu") { + throw new TypeError("Only 'webgpu' context is supported."); + } + this.#ctx = createCanvasContext({ surfaceRid: this.#surfaceRid }); + return this.#ctx; + } + + present() { + this.#ctx[_present](); + } } -export { createCanvasContext, GPUCanvasContext, presentGPUCanvasContext }; +export { GPUCanvasContext, UnsafeWindowSurface }; diff --git a/ext/webgpu/Cargo.toml b/ext/webgpu/Cargo.toml index b98ae20d88..bdc7bb3069 100644 --- a/ext/webgpu/Cargo.toml +++ b/ext/webgpu/Cargo.toml @@ -20,7 +20,7 @@ deno_core.workspace = true serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["full"] } wgpu-types = { workspace = true, features = ["trace", "replay", "serde"] } -raw-window-handle = { workspace = true, optional = true } +raw-window-handle = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies.wgpu-core] workspace = true diff --git a/ext/webgpu/byow.rs b/ext/webgpu/byow.rs new file mode 100644 index 0000000000..984eaae1bd --- /dev/null +++ b/ext/webgpu/byow.rs @@ -0,0 +1,127 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::op2; +use deno_core::OpState; +use deno_core::ResourceId; +use std::ffi::c_void; + +use crate::surface::WebGpuSurface; + +#[op2(fast)] +#[smi] +pub fn op_webgpu_surface_create( + state: &mut OpState, + #[string] system: &str, + p1: *const c_void, + p2: *const c_void, +) -> Result { + let instance = state.borrow::(); + // Security note: + // + // The `p1` and `p2` parameters are pointers to platform-specific window + // handles. + // + // The code below works under the assumption that: + // + // - handles can only be created by the FFI interface which + // enforces --allow-ffi. + // + // - `*const c_void` deserizalizes null and v8::External. + // + // - Only FFI can export v8::External to user code. + if p1.is_null() { + return Err(type_error("Invalid parameters")); + } + + let (win_handle, display_handle) = raw_window(system, p1, p2)?; + let surface = + instance.instance_create_surface(display_handle, win_handle, ()); + + let rid = state + .resource_table + .add(WebGpuSurface(instance.clone(), surface)); + Ok(rid) +} + +type RawHandles = ( + raw_window_handle::RawWindowHandle, + raw_window_handle::RawDisplayHandle, +); + +#[cfg(target_os = "macos")] +fn raw_window( + system: &str, + ns_window: *const c_void, + ns_view: *const c_void, +) -> Result { + if system != "cocoa" { + return Err(type_error("Invalid system on macOS")); + } + + let win_handle = { + let mut handle = raw_window_handle::AppKitWindowHandle::empty(); + handle.ns_window = ns_window as *mut c_void; + handle.ns_view = ns_view as *mut c_void; + + raw_window_handle::RawWindowHandle::AppKit(handle) + }; + let display_handle = raw_window_handle::RawDisplayHandle::AppKit( + raw_window_handle::AppKitDisplayHandle::empty(), + ); + Ok((win_handle, display_handle)) +} + +#[cfg(target_os = "windows")] +fn raw_window( + system: &str, + window: *const c_void, + hinstance: *const c_void, +) -> Result { + use raw_window_handle::WindowsDisplayHandle; + if system != "win32" { + return Err(type_error("Invalid system on Windows")); + } + + let win_handle = { + use raw_window_handle::Win32WindowHandle; + + let mut handle = Win32WindowHandle::empty(); + handle.hwnd = window as *mut c_void; + handle.hinstance = hinstance as *mut c_void; + + raw_window_handle::RawWindowHandle::Win32(handle) + }; + + let display_handle = + raw_window_handle::RawDisplayHandle::Windows(WindowsDisplayHandle::empty()); + Ok((win_handle, display_handle)) +} + +#[cfg(target_os = "linux")] +fn raw_window( + system: &str, + window: *const c_void, + display: *const c_void, +) -> Result { + if system != "x11" { + return Err(type_error("Invalid system on Linux")); + } + + let win_handle = { + let mut handle = raw_window_handle::XlibWindowHandle::empty(); + handle.window = window as *mut c_void as _; + + raw_window_handle::RawWindowHandle::Xlib(handle) + }; + + let display_handle = { + let mut handle = raw_window_handle::XlibDisplayHandle::empty(); + handle.display = display as *mut c_void; + + raw_window_handle::RawDisplayHandle::Xlib(handle) + }; + + Ok((win_handle, display_handle)) +} diff --git a/ext/webgpu/lib.rs b/ext/webgpu/lib.rs index abb36fb8dd..99c8fcf6b2 100644 --- a/ext/webgpu/lib.rs +++ b/ext/webgpu/lib.rs @@ -67,6 +67,7 @@ mod macros { pub mod binding; pub mod buffer; pub mod bundle; +pub mod byow; pub mod command_encoder; pub mod compute_pass; pub mod error; @@ -214,7 +215,9 @@ deno_core::extension!( // surface surface::op_webgpu_surface_configure, surface::op_webgpu_surface_get_current_texture, - surface::op_webgpu_surface_present + surface::op_webgpu_surface_present, + // byow + byow::op_webgpu_surface_create, ], esm = ["00_init.js", "02_surface.js"], lazy_loaded_esm = ["01_webgpu.js"], diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index 97b4a9531f..058985fbf9 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -29,6 +29,7 @@ import * as tty from "ext:runtime/40_tty.js"; import * as httpRuntime from "ext:runtime/40_http.js"; import * as kv from "ext:deno_kv/01_db.ts"; import * as cron from "ext:deno_cron/01_cron.ts"; +import * as webgpuSurface from "ext:deno_webgpu/02_surface.js"; const denoNs = { metrics: core.metrics, @@ -222,7 +223,9 @@ denoNsUnstableById[unstableIds.net] = { // denoNsUnstableById[unstableIds.unsafeProto] = {} -// denoNsUnstableById[unstableIds.webgpu] = {} +denoNsUnstableById[unstableIds.webgpu] = { + UnsafeWindowSurface: webgpuSurface.UnsafeWindowSurface, +}; // denoNsUnstableById[unstableIds.workerOptions] = {} @@ -242,6 +245,7 @@ const denoNsUnstable = { UnsafePointer: ffi.UnsafePointer, UnsafePointerView: ffi.UnsafePointerView, UnsafeFnPointer: ffi.UnsafeFnPointer, + UnsafeWindowSurface: webgpuSurface.UnsafeWindowSurface, flock: fs.flock, flockSync: fs.flockSync, funlock: fs.funlock,