├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── CAVEATS.md ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── lib └── index.js ├── scripts ├── .gitignore ├── idl.ts └── lib │ └── idl2rust.ts ├── src ├── .gitignore ├── encoding.ts ├── lib.rs ├── mod.ts └── promisify.ts ├── tests ├── gpu.spec.ts └── integration_tests.rs └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Deno WebGPU CI 2 | 3 | # Reference: https://github.com/eliassjogreen/deno_webview/blob/66ae7d3790955a856a3d65ece18e5131bc270375/.github/workflows/ci.yml 4 | 5 | on: push 6 | # schedule: 7 | # - cron: '0 0 * * SUN' 8 | 9 | jobs: 10 | build: 11 | name: ${{ matrix.kind }} ${{ matrix.os }} 12 | timeout-minutes: 60 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [macOS-latest, ubuntu-16.04] 17 | 18 | env: 19 | GH_ACTIONS: true 20 | RUST_BACKTRACE: full 21 | DENO_BUILD_MODE: release 22 | V8_BINARY: true 23 | 24 | steps: 25 | - uses: actions/checkout@v1 26 | - name: Update Git Submodules 27 | run: git submodule update --init --recursive 28 | - name: Setup Deno 29 | uses: denolib/setup-deno@master 30 | with: 31 | deno-version: 1.x 32 | - name: Use Rust 1.47.0 33 | uses: hecrj/setup-rust-action@v1 34 | with: 35 | rust-version: "1.47.0" 36 | - name: Log versions 37 | run: | 38 | deno --version 39 | python --version 40 | rustc --version 41 | cargo --version 42 | # - name: Use Node.js 12.x 43 | # uses: actions/setup-node@v1 44 | # with: 45 | # node-version: 12.x 46 | 47 | # Cache cargo libs 48 | - name: Cache Cargo registry 49 | uses: actions/cache@v1 50 | with: 51 | path: ~/.cargo/registry 52 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 53 | - name: Cache Cargo index 54 | uses: actions/cache@v1 55 | with: 56 | path: ~/.cargo/git 57 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 58 | - name: Cache Cargo build 59 | uses: actions/cache@v1 60 | with: 61 | path: target 62 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 63 | 64 | - name: Build 65 | env: 66 | RUST_BACKTRACE: 1 67 | run: cargo build --release 68 | 69 | # TODO: Deno tests 70 | # - name: Install Dependencies 71 | # run: | 72 | # npm install 73 | # - name: Test 74 | # if: success() 75 | # run: | 76 | # make test-ci 77 | # env: 78 | # CI: true 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # Compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | lerna-debug.log* 19 | 20 | # Diagnostic reports (https://nodejs.org/api/report.html) 21 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Directory for instrumented libs generated by jscoverage/JSCover 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | coverage 34 | 35 | # nyc test coverage 36 | .nyc_output 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # dotenv environment variables file 64 | .env 65 | .env.test 66 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/wgpu"] 2 | path = third_party/wgpu 3 | url = https://github.com/gfx-rs/wgpu.git 4 | [submodule "third_party/gpuweb/types"] 5 | path = third_party/gpuweb/types 6 | url = git://github.com/gpuweb/types.git 7 | [submodule "third_party/gpuweb/spec"] 8 | path = third_party/gpuweb/spec 9 | url = git://github.com/gpuweb/gpuweb 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Napi", 4 | "deno", 5 | "promisify", 6 | "wgpu", 7 | "winit" 8 | ], 9 | "deno.enable": true 10 | } 11 | -------------------------------------------------------------------------------- /CAVEATS.md: -------------------------------------------------------------------------------- 1 | # Caveats 2 | 3 | ## Linux (elementary OS 5.1 Hera - Built on Ubuntu 18.04.3 LTS) 4 | 5 | To build the project, [`rusty_v8`](https://crates.io/crates/rusty_v8) requires `glib2.0`, so point cargo to it via: 6 | 7 | - `find /usr/ -iname "*glib*.pc"` 8 | - `PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig cargo build` 9 | 10 | Sometimes the Rust VS Code extension can get confused about Rustup's current toolchain. 11 | 12 | Add in VS Code's settings: 13 | 14 | ```json 15 | "rust.rustup": { 16 | "toolchain": "" 17 | } 18 | ``` 19 | 20 | See, this [AskUbuntu answer](https://askubuntu.com/a/1027329/177764). 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wgpu_deno" 3 | version = "0.1.0" 4 | authors = ["Chance Snow "] 5 | edition = "2018" 6 | publish = false # TODO: Should I publish this package? 7 | 8 | keywords = ["deno-module", "deno-bindings", "deno-plugin", "gfx-rs", "webgpu"] 9 | # See more categories at https://crates.io/category_slugs 10 | categories = ["game-development", "graphics", "rendering"] 11 | homepage = "https://github.com/chances/deno-wgpu" 12 | repository = "https://github.com/chances/deno-wgpu.git" 13 | readme = "README.md" 14 | license = "MIT" 15 | 16 | [badges] 17 | maintenance = { status = "experimental" } 18 | 19 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 20 | 21 | # Referencing the Deno plugin example: 22 | # https://github.com/denoland/deno/blob/47a580293eb5c176497264f1c3f108bf6b2c480d/test_plugin/Cargo.toml#L9 23 | 24 | [lib] 25 | name = "wgpu_deno" 26 | # https://doc.rust-lang.org/reference/linkage.html 27 | crate-type = ["cdylib"] 28 | 29 | # pkg-config --variable pc_path pkg-config 30 | 31 | [dependencies] 32 | deno_core = "0.58.0" 33 | wgpu = "0.5.0" 34 | winit = "0.22" 35 | serde = { version = "1.0", features = ["derive"] } 36 | serde_json = "1.0" 37 | futures = "0.3.4" 38 | futures-executor = "0.3.4" 39 | lazy_static = "1.4.0" 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chance Snow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno WebGPU 2 | 3 | [![Deno WebGPU CI](https://github.com/chances/deno-wgpu/workflows/Deno%20WebGPU%20CI/badge.svg)](https://github.com/chances/deno-wgpu/actions) 4 | [![deno version](https://img.shields.io/badge/deno-0.41.0-success)](https://github.com/denoland/deno) 5 | 6 | ### **WebGPU is being integrated into Deno itself. Follow [denoland/deno#7863](https://github.com/denoland/deno/issues/7863) for details.** 7 | 8 | Experimental [Deno](https://github.com/denoland/deno) plugin for [wgpu-rs](https://github.com/gfx-rs/wgpu-rs) Rust bindings to Mozilla's gfx-rs [WebGPU](https://gpuweb.github.io/gpuweb/) implementation. 9 | 10 | ### **This is unfinished software. Use at your own risk!** 11 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | // Reference: https://doc.rust-lang.org/cargo/reference/build-script-examples.html#code-generation 4 | // `cargo build -vv` to see debug output 5 | 6 | fn main() { 7 | let mut gen_idl = deno_cmd(); 8 | 9 | gen_idl.args(&["run", "--allow-read", "--allow-write", "--allow-run", "scripts/idl.ts"]); 10 | 11 | let gen_idl_err = String::from("failed to generate rust interfaces from WebGPU IDL"); 12 | let output = gen_idl.output().expect(gen_idl_err.as_str()); 13 | if output.status.code().expect("deno status code") != 0 { 14 | println!("{}", std::str::from_utf8(&output.stdout).unwrap()); 15 | println!("{}", std::str::from_utf8(&output.stderr).unwrap()); 16 | panic!(gen_idl_err); 17 | } 18 | 19 | println!("{}", std::str::from_utf8(&output.stdout).unwrap()); 20 | println!("cargo:rerun-if-changed=third_party/gpuweb/spec/spec/webgpu.idl"); 21 | } 22 | 23 | fn is_program_in_path(program: &str) -> bool { 24 | if let Ok(path) = std::env::var("PATH") { 25 | for p in path.split(":") { 26 | let p_str = format!("{}/{}", p, program); 27 | if std::fs::metadata(p_str).is_ok() { 28 | return true; 29 | } 30 | } 31 | } 32 | false 33 | } 34 | 35 | fn deno_cmd() -> Command { 36 | assert!(is_program_in_path("deno")); 37 | Command::new("deno") 38 | } 39 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const teraflop = require('bindings')('teraflop'); 2 | const GPU = teraflop.GPU; 3 | 4 | module.exports = GPU; 5 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | lib/webidl2.d.ts 2 | -------------------------------------------------------------------------------- /scripts/idl.ts: -------------------------------------------------------------------------------- 1 | import { exists } from 'https://deno.land/std@0.67.0/fs/mod.ts' 2 | import * as path from 'https://deno.land/std@0.67.0/path/mod.ts' 3 | 4 | import * as idl2rust from './lib/idl2rust.ts' 5 | 6 | const encoder = new TextEncoder() 7 | function writeFileStr(filePath: string, content: string) { 8 | return Deno.writeFile(filePath, encoder.encode(content)) 9 | } 10 | 11 | const gpuWebPath = 'third_party/gpuweb/spec' 12 | 13 | export async function genBindings() { 14 | const status = await makeWebGpuIdl() 15 | if (status !== true) { 16 | console.error('Failed to generate webgpu.idl') 17 | Deno.exit(status || 1) 18 | } 19 | 20 | const idlFilePath = path.join(gpuWebPath, 'spec', 'webgpu.idl') 21 | const idlFileExists = await exists(idlFilePath) 22 | if (!idlFileExists) { 23 | console.error(`Failed to find WebGPU IDL at "${idl2rust}"`) 24 | Deno.exit(1) 25 | } 26 | 27 | const idl = await idl2rust.parse(idlFilePath) 28 | 29 | // console.log(JSON.stringify(idl.enums.find(dict => dict.name === 'GPUPowerPreference'), null, 2)) 30 | // console.log(JSON.stringify(idl.dictionaries.find(dict => dict.name === 'GPURequestAdapterOptions'), null, 2)) 31 | // console.log(JSON.stringify(idl.interfaces.find(i => i.name === 'GPU'), null, 2)) 32 | 33 | // Emit enums 34 | // Use from string impls like: string.parse::() 35 | const enums = idl.enums.map(_enum => { 36 | const variants = _enum.values.map(variant => `${idl2rust.indent(idl2rust.enumVariant(variant))},\n`).join('') 37 | return `enum ${_enum.name} {\n${variants}}\n\n${idl2rust.fromStrImpl(_enum)}` 38 | }).join('\n\n') 39 | 40 | const enumsFilePath = path.join('src', 'enums.rs') 41 | await writeFileStr(enumsFilePath, `use std::str::FromStr;\n\n${enums}\n`) 42 | console.log(`Generated ${enumsFilePath}`) 43 | 44 | // Emit method params 45 | const methodParams = idl.interfaces.map(_interface => { 46 | const interfaceComment = `// ${_interface.name}` 47 | const methodParams = _interface.methods 48 | .filter(idl2rust.methodWithParams) 49 | .map(method => `${idl2rust.methodParams(_interface, method)}\n\n`).join('') 50 | 51 | const noMethodsComment = methodParams.length === 0 ? ' has no methods\n' : '' 52 | return `${interfaceComment}${noMethodsComment}\n${methodParams}` 53 | }).join('') 54 | 55 | const paramsFilePath = path.join('src', 'params.rs') 56 | await writeFileStr(paramsFilePath, `use serde::Deserialize;\n\n${methodParams}`) 57 | console.log(`Generated ${paramsFilePath}`) 58 | } 59 | 60 | async function makeWebGpuIdl(): Promise { 61 | const process = Deno.run({ 62 | cmd: ['make', 'webgpu.idl'], 63 | cwd: path.join(gpuWebPath, 'spec'), 64 | }) 65 | 66 | const status = await process.status(); 67 | if (!status.success) { 68 | return status.code 69 | } 70 | 71 | return true 72 | } 73 | 74 | if (import.meta.main) { 75 | await genBindings() 76 | } 77 | -------------------------------------------------------------------------------- /scripts/lib/idl2rust.ts: -------------------------------------------------------------------------------- 1 | import { pascalCase, snakeCase } from 'https://github.com/chances/deno-change-case/raw/deno-v0.40.0/mod.ts' 2 | import * as webidl from 'https://cdn.skypack.dev/webidl2@^23.10.1' 3 | /// 4 | 5 | function readFileStr(filePath: string) { 6 | return Deno.readTextFile(filePath) 7 | } 8 | 9 | export async function parse(file: string): Promise { 10 | const fileContents = await readFileStr(file); 11 | return new IDLDocument(webidl.parse(fileContents)) 12 | } 13 | 14 | export type IDLType = 'interface' 15 | | 'interface mixin' 16 | | 'namespace' 17 | | 'callback' 18 | | 'dictionary' 19 | | 'enum' 20 | | 'typedef' 21 | | 'includes' 22 | | 'constructor' 23 | 24 | export class IDLDocument { 25 | constructor(private declarations: Array) { 26 | } 27 | 28 | public declarationsOfType(type: IDLType) { 29 | return this.declarations.filter(declaration => declaration.type === type) 30 | } 31 | 32 | public get enums(): Enum[] { 33 | return this.declarationsOfType('enum').map(_enum => ({ 34 | name: _enum.name, 35 | values: _enum.values.map((v: any) => v.value) 36 | })) 37 | } 38 | 39 | public get dictionaries(): Dictionary[] { 40 | return this.declarationsOfType('dictionary').map(dict => ({ 41 | name: dict.name, 42 | fields: dict.members.map((field: any) => ({ 43 | name: field.name, 44 | type: `${field.idlType.idlType}`, 45 | nullable: field.idlType.nullable 46 | })) 47 | })) 48 | } 49 | 50 | public get interfaces(): Interface[] { 51 | return this.declarationsOfType('interface').map(_interface => ({ 52 | name: _interface.name, 53 | inheritedInterface: _interface.inheritance, 54 | methods: _interface.members.filter((member: any) => member.type === 'operation').map((operation: any) => ({ 55 | name: operation.name, 56 | returnType: operation.idlType.generic 57 | ? operation.idlType.idlType[0]/* First generic type arg */.idlType 58 | : operation.idlType.idlType, 59 | returnsPromise: operation.idlType.generic === 'Promise', 60 | arguments: operation.arguments.map((arg: any) => ({ 61 | name: arg.name, 62 | type: arg.idlType.idlType, 63 | optional: arg.optional 64 | })) 65 | })) 66 | })) 67 | } 68 | 69 | public enum(name: string) { 70 | return this.enums.find(namedDeclaration(name)) 71 | } 72 | 73 | public dictionary(name: string) { 74 | return this.dictionaries.find(namedDeclaration(name)) 75 | } 76 | } 77 | 78 | function namedDeclaration(name: string) { 79 | return (named: NamedDeclaration) => named.name === name 80 | } 81 | 82 | export function methodWithParams(method: Operation) { 83 | return method.arguments.length > 0 84 | } 85 | 86 | // Code gen functions 87 | 88 | export function indent(line: string, amount: number = 2) { 89 | return `${' '.repeat(amount)}${line}` 90 | } 91 | 92 | export function enumVariant(variant: string) { 93 | const isFirstCharNumeric = Number.isNaN(parseInt(variant.substr(0, 1), 10)) === false 94 | const prefix = isFirstCharNumeric ? '_' : '' 95 | return `${prefix}${pascalCase(variant.split('-').join(' '))}` 96 | } 97 | 98 | export function fromStrImpl(_enum: Enum) { 99 | function matchVariant(variant: string) { 100 | return `"${variant}" => Ok(${_enum.name}::${enumVariant(variant)})` 101 | } 102 | 103 | return `impl FromStr for ${_enum.name} { 104 | type Err = String; 105 | 106 | fn from_str(s: &str) -> Result { 107 | match s { 108 | ${_enum.values.map(matchVariant).map(v => `${indent(v, 6)},`).join('\n')} 109 | _ => Err(String::from("Invalid value for ${_enum.name} enum")), 110 | } 111 | } 112 | }` 113 | } 114 | 115 | export function methodParams(_interface: Interface, method: Operation) { 116 | let methodName = method.name 117 | methodName = `${methodName.substr(0, 1).toUpperCase()}${methodName.substring(1)}` 118 | if (methodName === 'Finish') { 119 | methodName = `${_interface.name}${methodName}` 120 | } 121 | const params = method.arguments.map(arg => { 122 | let type = 'String' 123 | if (arg.optional) { 124 | type = `Option<${type}>` 125 | } 126 | 127 | const allowUnused = indent('#[allow(dead_code)]') // https://stackoverflow.com/a/32908553/1363247 128 | const paramDecl = indent(`${snakeCase(arg.name)}: ${type}`) 129 | return `${allowUnused}\n${paramDecl},` 130 | }).join('\n') 131 | 132 | // Serde attributes, field name format (https://serde.rs/attr-rename.html#serialize-fields-as-camelcase) 133 | const attrs = '#[derive(Deserialize)]\n#[serde(rename_all = "camelCase")]' 134 | return `${attrs}\nstruct ${methodName}Params {\n${params}\n}` 135 | } 136 | 137 | interface NamedDeclaration { 138 | name: string 139 | } 140 | 141 | export interface Enum extends NamedDeclaration { 142 | values: string[] 143 | } 144 | 145 | export interface Field extends NamedDeclaration { 146 | typeName: string 147 | nullable: boolean 148 | } 149 | 150 | export interface Dictionary extends NamedDeclaration { 151 | fields: Field[] 152 | } 153 | 154 | export interface Interface extends NamedDeclaration { 155 | inheritedInterface: string | null 156 | // IDLInterfaceMemberType 157 | methods: Operation[] 158 | } 159 | 160 | export interface Operation extends NamedDeclaration { 161 | returnType: string 162 | returnsPromise: boolean 163 | arguments: Argument[] 164 | } 165 | 166 | export interface Argument extends NamedDeclaration { 167 | type: string 168 | optional: boolean 169 | } 170 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | enums.rs 2 | params.rs 3 | -------------------------------------------------------------------------------- /src/encoding.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the result of running UTF-8's encoder. 3 | */ 4 | export const encode = (new TextEncoder()).encode; 5 | /** 6 | * Returns the result of running UTF-8's decoder. 7 | */ 8 | export const decode = (new TextDecoder()).decode; 9 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate deno_core; 3 | extern crate futures; 4 | extern crate futures_executor; 5 | extern crate serde; 6 | extern crate serde_json; 7 | 8 | use deno_core::plugin_api::Interface; 9 | use deno_core::plugin_api::Op; 10 | use deno_core::plugin_api::ZeroCopyBuf; 11 | use futures::future::FutureExt; 12 | use futures_executor::block_on; 13 | use serde::{Deserialize, Serialize}; 14 | 15 | // References: 16 | // https://github.com/eliassjogreen/deno_webview/blob/e706cf83bd7230e528afef07c6aa8ea669eb48e9/src/lib.rs 17 | // https://github.com/eliassjogreen/deno_webview/blob/master/src/lib.rs 18 | // 19 | // https://github.com/denoland/deno/blob/47a580293eb5c176497264f1c3f108bf6b2c480d/test_plugin/src/lib.rs 20 | // https://github.com/denoland/deno/blob/master/test_plugin/src/lib.rs 21 | 22 | // Generated modules 23 | // deno --allow-read --allow-write --allow-run scripts/idl.ts 24 | mod enums; 25 | mod params; 26 | 27 | use winit::{ 28 | event::{Event, WindowEvent}, 29 | event_loop::{ControlFlow, EventLoop}, 30 | window::{Window, WindowId}, 31 | }; 32 | 33 | #[no_mangle] 34 | pub fn deno_plugin_init(interface: &mut dyn Interface) { 35 | interface.register_op("testSync", op_test_sync); 36 | interface.register_op("requestAdapter", op_request_adapter); 37 | } 38 | 39 | // TODO: Post about this project to https://github.com/denoland/deno/issues/1629 and a plugin wip issue, (i.e. a la https://github.com/denoland/deno/issues/4481) 40 | 41 | #[derive(Serialize)] 42 | struct OpResponse { 43 | err: Option, 44 | ok: Option, 45 | } 46 | 47 | // TODO: Generate struct decls given WebGPU WebIDL definitions 48 | // https://crates.io/crates/webidl 49 | 50 | #[derive(Serialize)] 51 | struct RequestAdapterResult { 52 | id: u32, 53 | } 54 | 55 | fn serialize_response(response: Result) -> Box<[u8]> where T: Serialize { 56 | let response: OpResponse = match response { 57 | Err(message) => OpResponse { 58 | err: Some(message), 59 | ok: None 60 | }, 61 | Ok(data) => OpResponse { 62 | err: None, 63 | ok: Some(data) 64 | } 65 | }; 66 | serde_json::to_vec(&response).unwrap().into_boxed_slice() 67 | } 68 | 69 | pub fn op_test_sync(_interface: &mut dyn Interface, zero_copy: &mut [ZeroCopyBuf]) -> Op { 70 | let buf = &zero_copy[0][..]; 71 | let buf_str = std::str::from_utf8(buf).unwrap(); 72 | println!( 73 | "Hello from plugin. zero_copy: {}", 74 | buf_str 75 | ); 76 | 77 | let result = b"test"; 78 | let result_box = Box::new(*result); 79 | Op::Sync(result_box) 80 | } 81 | 82 | use lazy_static::lazy_static; 83 | use std::sync::Mutex; 84 | use std::collections::HashMap; 85 | 86 | // https://stackoverflow.com/a/27826181/1363247 87 | lazy_static! { 88 | static ref WINDOWS: Mutex> = Mutex::new(Vec::new()); 89 | static ref ADAPTERS: Mutex> = Mutex::new(HashMap::new()); 90 | } 91 | 92 | // TODO: Consider changing this to an op that creates the window w/ an adapter 93 | pub fn op_request_adapter(_interface: &mut dyn Interface, zero_copy: &mut [ZeroCopyBuf]) -> Op { 94 | let fut = async move { 95 | // TODO: Deserialize the params data 96 | // let buf = &zero_copy[0][..]; 97 | // let buf_str = std::str::from_utf8(buf).unwrap(); 98 | 99 | let event_loop = EventLoop::new(); 100 | let window = Window::new(&event_loop).unwrap(); 101 | let window_id = window.id(); 102 | let surface = wgpu::Surface::create(&window); 103 | 104 | let mut windows = WINDOWS.lock().unwrap(); 105 | windows.push(window); 106 | 107 | let satisfactory_backends = wgpu::BackendBit::from_bits( 108 | wgpu::BackendBit::PRIMARY.bits() | wgpu::BackendBit::SECONDARY.bits(), 109 | ) 110 | .unwrap(); 111 | let adapter_options = wgpu::RequestAdapterOptions { 112 | power_preference: wgpu::PowerPreference::Default, 113 | compatible_surface: Some(&surface), 114 | }; 115 | 116 | let future_adapter = 117 | wgpu::Adapter::request(&adapter_options, satisfactory_backends).map(|maybe_adapter| { 118 | let adapter_or_err = match maybe_adapter { 119 | None => { 120 | Err(String::from("Could not find satisfactory adapter")) 121 | }, 122 | Some(adapter) => { 123 | let mut adapters = ADAPTERS.lock().unwrap(); 124 | adapters.insert(window_id, adapter); 125 | let adapters_count = adapters.len() as u32; 126 | Ok(RequestAdapterResult { id: adapters_count }) 127 | } 128 | }; 129 | serialize_response(adapter_or_err) 130 | }); 131 | block_on(future_adapter) 132 | }; 133 | 134 | Op::Async(fut.boxed()) 135 | } 136 | 137 | // TODO: Add window resizing ops 138 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | import promisify from "./promisify.ts"; 2 | import { encode } from "./encoding.ts"; 3 | 4 | const filenameBase = "wgpu_deno"; 5 | 6 | let filenamePrefix = "lib"; 7 | let filenameSuffix = ".so"; 8 | 9 | if (Deno.build.os === "win") { 10 | filenamePrefix = ""; 11 | filenameSuffix = ".dll"; 12 | } 13 | if (Deno.build.os === "mac") { 14 | filenameSuffix = ".dylib"; 15 | } 16 | 17 | const filename = `./target/${Deno.args[0] || 18 | "debug"}/${filenamePrefix}${filenameBase}${filenameSuffix}`; 19 | const plugin = Deno.openPlugin(filename).ops; 20 | // Promisify the ops 21 | const ops = Object.keys(plugin).reduce( 22 | (promisifiedOps, opName) => { 23 | promisifiedOps[opName] = (...args) => promisify(plugin[opName], encode(JSON.stringify(args))); 24 | return promisifiedOps; 25 | }, 26 | {} as { 27 | [name: string]: (...args: any[]) => ReturnType; 28 | } 29 | ); 30 | 31 | console.log("Ops:", Deno.inspect(ops)); 32 | 33 | function notImplemented(opName: string) { 34 | return Promise.reject( 35 | new Deno.DenoError(Deno.ErrorKind.OpNotAvailable, `${opName} op not implemented`) 36 | ); 37 | } 38 | 39 | // Construct Web GPU interface 40 | export default { 41 | requestAdapter(options?: Object): Promise { 42 | options = options ? { ...options } : {}; 43 | return ops.requestAdapter(options); 44 | } 45 | }; 46 | 47 | export * from "../third_party/gpuweb/types/src/constants.ts"; 48 | -------------------------------------------------------------------------------- /src/promisify.ts: -------------------------------------------------------------------------------- 1 | import { decode } from "./encoding.ts"; 2 | 3 | function decodeResponse(response: Uint8Array): any[] { 4 | return JSON.parse(decode(response)) 5 | } 6 | 7 | export default function promisify( 8 | op: Deno.PluginOp, control: Uint8Array, zeroCopy?: ArrayBufferView | null 9 | ): Promise { 10 | return new Promise((resolve) => { 11 | try { 12 | const syncResponse = op.dispatch(control, zeroCopy); 13 | if (syncResponse) { 14 | resolve(syncResponse); 15 | } 16 | op.setAsyncHandler((response) => { 17 | resolve(response); 18 | }); 19 | } 20 | catch (e) { 21 | console.error(e); 22 | throw new Deno.DenoError(Deno.ErrorKind.NoAsyncSupport, "Promisify-ed plugin op failed"); 23 | } 24 | }).then(decodeResponse); 25 | } 26 | -------------------------------------------------------------------------------- /tests/gpu.spec.ts: -------------------------------------------------------------------------------- 1 | // @deno-types="../third_party/gpuweb/types/dist/index.d.ts" 2 | import GPU from '../src/mod.ts' 3 | import * as gpuConstants from '../src/mod.ts' 4 | 5 | // Tests 6 | GPU.requestAdapter() 7 | -------------------------------------------------------------------------------- /tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | // TODO(ry) Re-enable this test on windows. It is flaky for an unknown reason. 2 | #![cfg(not(windows))] 3 | 4 | use deno::test_util::*; 5 | use std::process::Command; 6 | 7 | fn is_program_in_path(program: &str) -> bool { 8 | if let Ok(path) = std::env::var("PATH") { 9 | for p in path.split(":") { 10 | let p_str = format!("{}/{}", p, program); 11 | if std::fs::metadata(p_str).is_ok() { 12 | return true; 13 | } 14 | } 15 | } 16 | false 17 | } 18 | 19 | fn deno_cmd() -> Command { 20 | assert!(is_program_in_path("deno")); 21 | Command::new("deno") 22 | } 23 | 24 | #[cfg(debug_assertions)] 25 | const BUILD_VARIANT: &str = "debug"; 26 | 27 | #[cfg(not(debug_assertions))] 28 | const BUILD_VARIANT: &str = "release"; 29 | 30 | #[test] 31 | fn basic() { 32 | // let mut build_plugin_base = Command::new("cargo"); 33 | // let mut build_plugin = 34 | // build_plugin_base.arg("build").arg("-p").arg("test_plugin"); 35 | // if BUILD_VARIANT == "release" { 36 | // build_plugin = build_plugin.arg("--release"); 37 | // } 38 | // let _build_plugin_output = build_plugin.output().unwrap(); 39 | let output = deno_cmd() 40 | .arg("--allow-plugin") 41 | .arg("tests/gpu.spec.ts") 42 | .arg(BUILD_VARIANT) 43 | .output() 44 | .unwrap(); 45 | let stdout = std::str::from_utf8(&output.stdout).unwrap(); 46 | let stderr = std::str::from_utf8(&output.stderr).unwrap(); 47 | if !output.status.success() { 48 | println!("stdout {}", stdout); 49 | println!("stderr {}", stderr); 50 | } 51 | assert!(output.status.success()); 52 | let expected = if cfg!(target_os = "windows") { 53 | "Hello from plugin. data: test | zero_copy: test\nPlugin Sync Response: test\r\nHello from plugin. data: test | zero_copy: test\nPlugin Async Response: test\r\n" 54 | } else { 55 | "Hello from plugin. data: test | zero_copy: test\nPlugin Sync Response: test\nHello from plugin. data: test | zero_copy: test\nPlugin Async Response: test\n" 56 | }; 57 | assert_eq!(stdout, expected); 58 | assert_eq!(stderr, ""); 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "lib": [ 6 | "ES2015", 7 | "ES2016", 8 | "ES2017", 9 | "ES2018", 10 | "ES2019", 11 | "WebWorker" 12 | ], 13 | "strict": true, 14 | "typeRoots": [ 15 | "third_party/gpuweb/types/dist" 16 | ] 17 | }, 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } 22 | --------------------------------------------------------------------------------