wgpu + wasm + worker (#8)

initial setup
This commit is contained in:
Heaust Azure
2025-08-04 22:23:37 +05:30
committed by GitHub
parent 9d2bf4055d
commit ba2d1c064b
33 changed files with 4207 additions and 963 deletions
+21
View File
@@ -22,3 +22,24 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
/pkg
/wasm-pack.log
.vscode/
.idea/
.cargo
Generated
+1244
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
[package]
name = "base"
description = "wgpu + webpack base"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = "z"
lto = true
[dependencies]
wasm-bindgen = "0.2.100"
wasm-bindgen-futures = "0.4.50"
console_error_panic_hook = "0.1.7"
console_log = "1.0.0"
log = "0.4.27"
web-sys = { version = "0.3.77", features = [
"Window",
"Document",
"Element",
"HtmlCanvasElement",
"OffscreenCanvas",
"MouseEvent",
"Worker",
"DedicatedWorkerGlobalScope",
"Event",
"MessageEvent",
"Blob",
"BlobPropertyBag",
"Url",
]}
js-sys = "0.3.77"
bytemuck = { version = "1.23.1", features = [
"derive"
]}
cgmath = "0.18"
raw-window-handle = "0.6.2"
wgpu = "26.0.1"
-108
View File
@@ -1,108 +0,0 @@
const MessageType = {
domEvent: 0,
custom: 1,
attachCanvas: 2,
} as const;
type MessageTypeI = typeof MessageType;
const EventTypes = {
pointermove: 0,
pointerdown: 1,
pointerup: 2,
keydown: 3,
keyup: 4,
onWheel: 5,
} as const;
type EventTypesI = typeof EventTypes;
/**
* @param worker The worker you're running the canvas on
* @returns DisconnectWorker()
*/
const connectWorker = (worker: Worker) => {
const eventPassers = {
[EventTypes.pointermove]: (e: PointerEvent) => {
worker.postMessage([
MessageType.domEvent,
EventTypes.pointermove,
e.clientX,
e.clientY,
]);
},
[EventTypes.pointerup]: (e: PointerEvent) => {
worker.postMessage([
MessageType.domEvent,
EventTypes.pointerup,
e.clientX,
e.clientY,
]);
},
[EventTypes.pointerdown]: (e: PointerEvent) => {
worker.postMessage([
MessageType.domEvent,
EventTypes.pointerdown,
e.clientX,
e.clientY,
]);
},
[EventTypes.keydown]: (e: KeyboardEvent) => {
worker.postMessage([MessageType.domEvent, EventTypes.keydown, e.key]);
},
[EventTypes.keyup]: (e: KeyboardEvent) => {
worker.postMessage([MessageType.domEvent, EventTypes.keyup, e.key]);
},
[EventTypes.onWheel]: () => {
worker.postMessage([MessageType.domEvent, EventTypes.onWheel]);
},
} as const;
document.addEventListener(
"pointermove",
eventPassers[EventTypes.pointermove],
);
document.addEventListener(
"pointerdown",
eventPassers[EventTypes.pointerdown],
);
document.addEventListener("pointerup", eventPassers[EventTypes.pointerup]);
//
document.addEventListener("keydown", eventPassers[EventTypes.keydown]);
document.addEventListener("keyup", eventPassers[EventTypes.keyup]);
const disconnectWorker = () => {
document.removeEventListener(
"pointermove",
eventPassers[EventTypes.pointermove],
);
document.removeEventListener(
"pointerdown",
eventPassers[EventTypes.pointerdown],
);
document.removeEventListener(
"pointerup",
eventPassers[EventTypes.pointerup],
);
//
document.removeEventListener("keydown", eventPassers[EventTypes.keydown]);
document.removeEventListener("keyup", eventPassers[EventTypes.keyup]);
};
return disconnectWorker;
};
const attachCanvas = (worker: Worker, canvas: HTMLCanvasElement | string) => {
if (typeof canvas === "string")
canvas = document.getElementById("rendering-canvas") as HTMLCanvasElement;
if (!canvas) {
throw new Error("Fatal: Canvas Not Found!");
}
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const canvasWorker = canvas.transferControlToOffscreen();
worker.postMessage([MessageType.attachCanvas, canvasWorker], [canvasWorker]);
};
export type { EventTypesI, MessageTypeI };
export { EventTypes, connectWorker, attachCanvas, MessageType };
-42
View File
@@ -1,42 +0,0 @@
import { Engine } from "../renderer/engine";
import { MessageType } from "./mainThread";
const cubeVertices = [
1, -1, 1, -1, -1, 1, -1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1,
-1, -1, -1, -1, 1, 1, -1, -1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, -1, 1, -1,
1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, 1, -1,
-1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1,
1, -1, 1, 1, -1, -1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1, 1,
];
const handleConnection = (msg: MessageEvent<any>) => {
const { data } = msg;
if (!(data instanceof Array)) return;
if (!data.length) return;
switch (data[0]) {
case MessageType.attachCanvas:
const canvas = data[1];
const engine = new Engine(canvas);
const scene = engine.createScene();
scene.addMesh("box", cubeVertices);
scene.addCamera("cam", true);
const raf = () => {
requestAnimationFrame(() => {
scene.runSystems();
raf();
});
};
raf();
break;
}
console.log(...data);
};
export { handleConnection };
-110
View File
@@ -1,110 +0,0 @@
type ComponentMap<CT> = {
uid: (number | null)[];
} & {
[K in keyof CT]?: Array<CT[K] | null>;
};
namespace ECS {
export type System<ComponentsType extends Record<string | number, any>> = (
arg: ComponentsType & { uid: (number | null)[] },
) => Partial<ComponentsType> | null;
}
class ECS<ComponentsType extends Record<string | number, any>> {
private components: ComponentMap<ComponentsType> = { uid: [] };
private componentDefaults: Record<
string,
null | ComponentsType[keyof ComponentsType]
> = { uid: null };
private systems: Record<string, ECS.System<ComponentsType>> = {};
/**
* System
*/
runSystems() {
const limit = this.components.uid.length;
for (let i = 0; i < limit; i++) {
const obj = {} as Record<string, unknown>;
for (const key in this.components) {
obj[key] = this.components[key]![i];
}
const systems = Object.values(this.systems);
for (let j = 0; j < systems.length; j++) {
const res = systems[j](
obj as ComponentsType & { uid: (number | null)[] },
);
if (!res) continue;
for (const key in res) {
if (!this.components[key]) continue;
this.components[key][i] = res[key]!;
}
}
}
}
addSystem(system: ECS.System<ComponentsType>) {
const id = crypto.randomUUID();
this.systems[id] = system;
return id;
}
removeSystem(id: string) {
delete this.systems[id];
}
getById(uid: number) {
if (this.components.uid.length <= uid) return {};
const obj = {} as Record<string, unknown>;
for (const key in this.components) {
obj[key] = this.components[key]![uid];
}
return obj;
}
/**
* Entity
*/
addEntity(components: ComponentsType) {
for (const key in this.components) {
if (key === "uid") {
this.components[key].push(this.components[key].length);
continue;
}
this.components[key]!.push(
components[key] ?? this.componentDefaults[key],
);
}
return this.components.uid.length - 1;
}
/**
* Component
*/
addComponent(
name: keyof ComponentsType,
fillValue = null as null | ComponentsType[keyof ComponentsType],
defaultValue = null as null | ComponentsType[keyof ComponentsType],
) {
if (this.components[name]) throw new Error("Component already exists");
this.components[name] = new Array(this.components.uid.length).fill(
fillValue,
) as ComponentMap<ComponentsType>[keyof ComponentsType];
this.componentDefaults[name as string] = defaultValue;
}
removeComponent(name: keyof ComponentsType) {
delete this.components[name];
}
}
export { ECS };
-1
View File
@@ -1 +0,0 @@
/// <reference types="vite-plugin-glsl/ext" />
-31
View File
@@ -1,31 +0,0 @@
import { Scene } from "./scene";
class Engine {
canvas: HTMLCanvasElement;
ctx: WebGL2RenderingContext;
scenes = [] as Scene[];
constructor(canvas: string | HTMLCanvasElement) {
/** get canvas by id */
if (typeof canvas === "string") {
canvas = document.getElementById(canvas)! as HTMLCanvasElement;
}
if (!canvas) {
throw new Error("canvas not found");
}
this.canvas = canvas;
this.ctx = canvas.getContext("webgl2")!;
if (!this.ctx) {
throw new Error("can't get webgl2 context");
}
}
createScene() {
const scene = new Scene(this.canvas, this.ctx);
this.scenes.push(scene);
return scene;
}
}
export { Engine };
-107
View File
@@ -1,107 +0,0 @@
import { mat4 } from "gl-matrix";
export const compileShader = (
gl: WebGL2RenderingContext,
type: number,
source: string,
) => {
const shader = gl.createShader(type);
if (!shader) {
throw new Error(`couldn't make the shader ${source}`);
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(
`Error in ${source}: ${gl.getShaderInfoLog(shader) ?? "no error returned"}`,
);
}
return shader;
};
export const createProgram = (
gl: WebGL2RenderingContext,
vs: string,
fs: string,
): WebGLProgram => {
const program = gl.createProgram()!;
const vShader = compileShader(gl, gl.VERTEX_SHADER, vs);
const fShader = compileShader(gl, gl.FRAGMENT_SHADER, fs);
gl.attachShader(program, vShader);
gl.attachShader(program, fShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program) || "");
}
return program;
};
export type AttributeCollection = {
name: string;
data: Float32Array;
size: number;
};
export const setupVAO = (
gl: WebGL2RenderingContext,
program: WebGLProgram,
attributeCollections: AttributeCollection[],
) => {
const vao = gl.createVertexArray()!;
gl.bindVertexArray(vao);
for (const attr of attributeCollections) {
const loc = gl.getAttribLocation(program, attr.name);
if (loc >= 0) {
const buf = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, attr.data, gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, attr.size, gl.FLOAT, false, 0, 0);
}
}
gl.bindVertexArray(null);
return vao;
};
export const updateVAO = (
gl: WebGL2RenderingContext,
program: WebGLProgram,
vao: WebGLVertexArrayObject,
attributeCollections: AttributeCollection[],
) => {
gl.bindVertexArray(vao);
for (const attr of attributeCollections) {
const loc = gl.getAttribLocation(program, attr.name);
if (loc >= 0) {
const buf = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, attr.data, gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, attr.size, gl.FLOAT, false, 0, 0);
}
}
gl.bindVertexArray(null);
return vao;
};
export const setMat4 = (
gl: WebGL2RenderingContext,
program: WebGLProgram,
name: string,
mat: mat4,
) => {
const loc = gl.getUniformLocation(program, name);
if (loc) gl.uniformMatrix4fv(loc, false, mat);
};
-197
View File
@@ -1,197 +0,0 @@
import { mat4, vec3 } from "gl-matrix";
import { ECS } from "../ecs/ecs";
import { createProgram, setMat4, setupVAO, updateVAO } from "./gl";
import defaultVertexSrc from "../shaders/triangle/vertex.glsl";
import defaultFragSrc from "../shaders/triangle/frag.glsl";
class Scene {
canvas: HTMLCanvasElement;
gl: WebGL2RenderingContext;
ecs: ECS<Record<string | number, any>>;
activeCamera: number;
constructor(canvas: HTMLCanvasElement, ctx: WebGL2RenderingContext) {
this.canvas = canvas;
this.gl = ctx;
this.activeCamera = -1;
/** make ecs */
this.ecs = new ECS();
this.initECS();
}
private initECS() {
const projection = mat4.create();
mat4.perspective(
projection,
45,
this.canvas.width / this.canvas.height,
0.1,
100.0,
);
this.ecs.addComponent("isMesh");
this.ecs.addComponent("isCamera");
this.ecs.addComponent("isMaterial");
this.ecs.addComponent("program");
this.ecs.addComponent("vao");
this.ecs.addComponent("name", "deafult", "default");
this.ecs.addComponent("attr_pos", [], []);
this.ecs.addComponent("attr_normals", [], []);
this.ecs.addComponent("u_model_matrix", mat4.create(), mat4.create());
this.ecs.addComponent("u_projection_matrix", projection, projection);
const defaultMaterial = this.addMaterial(
"default-material",
defaultVertexSrc,
defaultFragSrc,
);
this.ecs.addComponent("applied_material", defaultMaterial, defaultMaterial);
this.ecs.addSystem((data) => {
this.render(data);
const model = mat4.create();
const angle = Date.now() * 0.001;
mat4.translate(model, model, vec3.fromValues(0, 0, -5));
mat4.rotate(model, model, angle, [0.5, 1, 0]);
if (data["isMesh"]) {
return {
u_model_matrix: model,
};
}
return null;
});
}
runSystems() {
this.ecs.runSystems();
}
private render(data: any) {
if (this.activeCamera === -1) return;
const camera = this.ecs.getById(this.activeCamera);
const view = mat4.create();
mat4.invert(view, camera["u_model_matrix"] as mat4);
const projection = camera["u_projection_matrix"] as mat4;
const gl = this.gl;
gl.enable(gl.DEPTH_TEST);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
if (data["isMesh"]) {
const { program, vao } = this.ecs.getById(data["applied_material"]);
const attrCollection = [
{
name: "attr_pos",
data: data["attr_pos"],
size: 3,
},
{
name: "attr_normals",
data: data["attr_normals"],
size: 3,
},
];
updateVAO(gl, program as any, vao as any, attrCollection);
gl.useProgram(program!);
const model = data["u_model_matrix"];
setMat4(gl, program as any, "model", model);
setMat4(gl, program as any, "view", view);
setMat4(gl, program as any, "projection", projection);
gl.bindVertexArray(vao as any);
const vertexCount = data["attr_pos"].length / 3;
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
console.log(vertexCount);
}
gl.bindVertexArray(null);
gl.useProgram(null);
}
addMaterial(name: string, vs: string, fs: string) {
const program = createProgram(this.gl, vs, fs);
const vao = setupVAO(this.gl, program, []);
return this.ecs.addEntity({
name,
isMaterial: true,
program,
vao,
});
}
addCamera(name: string, setActive = true) {
const uid = this.ecs.addEntity({
name,
});
if (setActive) this.activeCamera = uid;
}
addMesh(name: string, positions: number[], normals?: number[]) {
const numVerts = Math.floor(positions.length / 3);
const numTris = Math.floor(numVerts / 3);
positions.length = numTris * 9;
if (!normals) {
normals = [];
const a = vec3.create();
const b = vec3.create();
const c = vec3.create();
const e1 = vec3.create();
const e2 = vec3.create();
for (let i = 0; i < numTris; i++) {
const base = i * 9;
vec3.set(
a,
positions[base + 0],
positions[base + 1],
positions[base + 2],
);
vec3.set(
b,
positions[base + 3],
positions[base + 4],
positions[base + 5],
);
vec3.set(
c,
positions[base + 6],
positions[base + 7],
positions[base + 8],
);
vec3.subtract(e1, b, a);
vec3.subtract(e2, c, a);
vec3.cross(e1, e2, e1);
vec3.normalize(e1, e1);
normals.push(e1[0], e1[1], e1[2]);
normals.push(e1[0], e1[1], e1[2]);
normals.push(e1[0], e1[1], e1[2]);
}
}
this.ecs.addEntity({
name,
attr_pos: new Float32Array(positions),
attr_normals: new Float32Array(normals),
isMesh: true,
});
}
}
export { Scene };
-16
View File
@@ -1,16 +0,0 @@
#version 300 es
precision highp float;
in vec3 vNormal;
out vec4 fragColor;
void main() {
vec3 normal = normalize(vNormal);
vec3 lightDir = normalize(vec3(0.0, 0.0, 1.0));
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuseColor = vec3(1.0, 0.5, 0.3) * diff;
fragColor = vec4(diffuseColor, 1.0);
}
-20
View File
@@ -1,20 +0,0 @@
#version 300 es
precision highp float;
in vec3 attr_pos;
in vec3 attr_normals;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 vNormal;
void main() {
mat3 normalMatrix = transpose(inverse(mat3(model)));
vNormal = normalize(normalMatrix * attr_normals);
gl_Position = projection * view * model * vec4(attr_pos, 1.0);
}
-3
View File
@@ -1,3 +0,0 @@
import { handleConnection } from "../core/connection/workerThread";
self.onmessage = handleConnection;
-14
View File
@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div id="app"><canvas id="rendering-canvas"></canvas></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+18 -13
View File
@@ -1,20 +1,25 @@
{
"name": "yawn",
"private": true,
"version": "0.0.0",
"type": "module",
"author": "ecoricemon",
"name": "basic",
"version": "0.1.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
"wasm-dev": "wasm-pack build --dev --out-name wasm-index --target web .",
"wasm-release": "wasm-pack build --release --out-name wasm-index --target web .",
"bundle-dev": "vite build --mode development",
"bundle-release": "vite build",
"build": "run-s clean wasm-dev bundle-dev",
"build-release": "run-s clean wasm-release bundle-release",
"start": "vite preview",
"clean": "rimraf --glob dist **/pkg",
"clean-all": "rimraf --glob dist **/pkg target node_modules"
},
"devDependencies": {
"typescript": "^5.2.2",
"vite": "^5.3.4",
"vite-plugin-glsl": "^1.4.1"
},
"dependencies": {
"gl-matrix": "^3.4.3",
"tslog": "^4.9.3"
"@wasm-tool/wasm-pack-plugin": "^1.7.0",
"vite": "^5.1.6",
"vite-plugin-wasm": "^3.3.0",
"rollup-plugin-copy": "^3.5.0",
"rimraf": "^5.0.1",
"npm-run-all": "^4.1.5"
}
}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+135
View File
@@ -0,0 +1,135 @@
use std::{ops::Deref, ptr::NonNull};
use wasm_bindgen::{JsCast, JsValue};
#[derive(Debug, Clone)]
pub struct Canvas {
element: web_sys::HtmlCanvasElement,
handle: u32,
}
impl Canvas {
pub fn new(selectors: &str, handle: u32) -> Self {
// 0 is reserved for window itself.
assert!(handle > 0);
// Injects `data-raw-handle` attribute into the canvas element.
// This is required by `wgpu::Surface` and `raw-window-handle`.
let element = Self::get_canvas_element(selectors);
element
.set_attribute("data-raw-handle", handle.to_string().as_str())
.unwrap();
Self { element, handle }
}
pub fn get_canvas_element(selectors: &str) -> web_sys::HtmlCanvasElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let element = document.query_selector(selectors).unwrap().unwrap();
let canvas = element.dyn_into::<web_sys::HtmlCanvasElement>().unwrap();
let scale_factor = window.device_pixel_ratio();
let width = (canvas.client_width() as f64 * scale_factor) as u32;
let height = (canvas.client_height() as f64 * scale_factor) as u32;
canvas.set_width(width);
canvas.set_height(height);
canvas
}
#[inline]
pub fn handle(&self) -> u32 {
self.handle
}
}
impl Deref for Canvas {
type Target = web_sys::HtmlCanvasElement;
fn deref(&self) -> &Self::Target {
&self.element
}
}
impl raw_window_handle::HasWindowHandle for Canvas {
fn window_handle(
&self,
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
use raw_window_handle::{RawWindowHandle, WebCanvasWindowHandle, WindowHandle};
let value: &JsValue = &self.element;
let obj: NonNull<std::ffi::c_void> = NonNull::from(value).cast();
let handle = WebCanvasWindowHandle::new(obj);
let raw = RawWindowHandle::WebCanvas(handle);
unsafe { Ok(WindowHandle::borrow_raw(raw)) }
}
}
impl raw_window_handle::HasDisplayHandle for Canvas {
fn display_handle(
&self,
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
use raw_window_handle::{DisplayHandle, RawDisplayHandle, WebDisplayHandle};
let handle = WebDisplayHandle::new();
let raw = RawDisplayHandle::Web(handle);
unsafe { Ok(DisplayHandle::borrow_raw(raw)) }
}
}
#[derive(Debug, Clone)]
pub struct OffscreenCanvas {
inner: web_sys::OffscreenCanvas,
handle: u32,
}
impl OffscreenCanvas {
pub const fn new(canvas: web_sys::OffscreenCanvas, handle: u32) -> Self {
Self {
inner: canvas,
handle,
}
}
pub fn each(self) -> (web_sys::OffscreenCanvas, u32) {
(self.inner, self.handle)
}
}
impl Deref for OffscreenCanvas {
type Target = web_sys::OffscreenCanvas;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl From<&Canvas> for OffscreenCanvas {
fn from(value: &Canvas) -> Self {
let offscreen = value.element.transfer_control_to_offscreen().unwrap();
let handle = value.handle;
Self::new(offscreen, handle)
}
}
impl raw_window_handle::HasWindowHandle for OffscreenCanvas {
fn window_handle(
&self,
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
use raw_window_handle::{RawWindowHandle, WebOffscreenCanvasWindowHandle, WindowHandle};
let value: &JsValue = &self.inner;
let obj: NonNull<std::ffi::c_void> = NonNull::from(value).cast();
let handle = WebOffscreenCanvasWindowHandle::new(obj);
let raw = RawWindowHandle::WebOffscreenCanvas(handle);
unsafe { Ok(WindowHandle::borrow_raw(raw)) }
}
}
impl raw_window_handle::HasDisplayHandle for OffscreenCanvas {
fn display_handle(
&self,
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
use raw_window_handle::{DisplayHandle, RawDisplayHandle, WebDisplayHandle};
let handle = WebDisplayHandle::new();
let raw = RawDisplayHandle::Web(handle);
unsafe { Ok(DisplayHandle::borrow_raw(raw)) }
}
}
+34
View File
@@ -0,0 +1,34 @@
struct UniformData {
mouse_move: vec2<f32>,
mouse_click: vec2<f32>,
resolution: vec2<f32>,
time: f32,
}
@group(0) @binding(0) var<uniform> uni: UniformData;
struct VertexInput {
@location(0) pos: vec3<f32>,
@location(1) color: vec3<f32>
}
struct VertexOutput {
@builtin(position) pos: vec4<f32>,
@location(1) color: vec3<f32>
}
@vertex
fn v_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.pos = vec4<f32>(in.pos, 1.0);
let fluc = sin(modf(uni.time).fract * 3.141592) * 0.3 + 0.7;
out.color = in.color * fluc;
return out;
}
@fragment
fn f_main(in: VertexOutput) -> @location(0) vec4<f32> {
let x = select(0.0, 0.3, distance(in.pos.xy, uni.mouse_move) < 25.0);
let y = select(0.0, 0.3, distance(in.pos.xy, uni.mouse_click) < 25.0);
return vec4f(in.color + x - y, 1.0);
}
+552
View File
@@ -0,0 +1,552 @@
use std::{cell::RefCell, mem, rc::Rc};
use wasm_bindgen::prelude::*;
use wgpu::util::DeviceExt;
mod worker;
use worker::*;
mod canvas;
use canvas::*;
mod message;
use message::*;
/// `App` is responsible for accessing window elements.
/// Also, it creates main worker and passes window events to the worker.
/// Main worker, on the other hand, does all works such as drawing.
#[allow(dead_code)]
#[wasm_bindgen]
pub struct App {
canvas: Canvas,
worker: Rc<RefCell<MainWorker>>,
}
#[wasm_bindgen]
impl App {
/// `index.js` creates `App` using this constructor.
#[allow(clippy::new_without_default)]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
// Gets the canvas.
let canvas = Canvas::new("#canvas0", 1);
// Spawns main worker.
let worker = MainWorker::spawn("main-worker", 1).unwrap();
// Transfers offscreen canvas to the worker.
let offscreen = OffscreenCanvas::from(&canvas);
let (offscreen, handle) = offscreen.each();
let msg = js_sys::Array::new_with_length(3);
msg.set(0, JsMessage::INIT.into_jsvalue());
msg.set(1, JsValue::from(offscreen.clone()));
msg.set(2, JsValue::from(handle));
let t = js_sys::Array::new_with_length(1);
t.set(0, JsValue::from(offscreen));
worker.post_message_with_transfer(&msg, &t).unwrap();
let worker = Rc::new(RefCell::new(worker));
// Registers "resize" event proxy.
let worker_cloned = Rc::clone(&worker);
let msg = js_sys::Array::new_with_length(JsResizeMessage::field_num() + 1);
msg.set(0, JsMessage::WINDOW_RESIZE.into_jsvalue());
let canvas_cloned = canvas.clone();
let listener = Closure::<dyn Fn()>::new(move || {
let window = web_sys::window().unwrap();
let scale_factor = window.device_pixel_ratio();
JsResizeMessage::set_js_array(&msg, &canvas_cloned, scale_factor, 1);
worker_cloned.borrow_mut().post_message(&msg).unwrap();
});
let window = web_sys::window().unwrap();
window
.add_event_listener_with_callback("resize", listener.as_ref().unchecked_ref())
.unwrap();
listener.forget(); // Leak, but just once.
// Registers "mousemove" event proxy.
let worker_cloned = Rc::clone(&worker);
let msg = js_sys::Array::new_with_length(JsMouseMessage::field_num() + 1);
let listener = Closure::<dyn Fn(_)>::new(move |event: web_sys::MouseEvent| {
let window = web_sys::window().unwrap();
let scale_factor = window.device_pixel_ratio();
msg.set(0, JsMessage::MOUSE_MOVE.into_jsvalue());
JsMouseMessage::set_js_array(&msg, event, scale_factor, 1);
worker_cloned.borrow_mut().post_message(&msg).unwrap();
});
canvas
.add_event_listener_with_callback("mousemove", listener.as_ref().unchecked_ref())
.unwrap();
listener.forget(); // Leak, but just once.
// Registers "click" event proxy.
let worker_cloned = Rc::clone(&worker);
let msg = js_sys::Array::new_with_length(JsMouseMessage::field_num() + 1);
let listener = Closure::<dyn Fn(_)>::new(move |event: web_sys::MouseEvent| {
let window = web_sys::window().unwrap();
let scale_factor = window.device_pixel_ratio();
msg.set(0, JsMessage::MOUSE_CLICK.into_jsvalue());
JsMouseMessage::set_js_array(&msg, event, scale_factor, 1);
worker_cloned.borrow_mut().post_message(&msg).unwrap();
});
canvas
.add_event_listener_with_callback("click", listener.as_ref().unchecked_ref())
.unwrap();
listener.forget(); // Leak, but just once.
Self { canvas, worker }
}
}
thread_local! {
/// Main render state.
static STATE: RefCell<State> = panic!();
}
/// Initializes [`STATE`] from worker side, not in window context.
/// That's because window and worker don't share memory (We can use shared memory with some restrictions).
/// When initialization is over, JS replaces this event handler with [`main_onmessage`].
#[wasm_bindgen]
pub async fn main_onmessage_init(event: web_sys::MessageEvent) -> bool {
let data = event.data();
debug_assert!(data.is_array());
let data: js_sys::Array = data.unchecked_into();
match JsMessage::from_f64(data.get(0)).0 {
JsMessage::INIT_INNER => {
// Initializes State.
let canvas: web_sys::OffscreenCanvas = data.get(1).unchecked_into();
let handle = data.get(2).as_f64().unwrap() as u32;
let canvas = OffscreenCanvas::new(canvas, handle);
let state = State::new(canvas).await;
STATE.set(state);
// Registers animation callback to the State and activate it.
let animation_cb = Closure::<dyn FnMut(f32)>::new(move |time: f32| {
STATE.with_borrow_mut(|state| {
state.render(time);
state.request_animation_frame();
})
});
STATE.with_borrow_mut(move |state| {
state.animation_cb = animation_cb;
state.request_animation_frame();
});
// Ready.
true
}
_ => false, // Not ready yet.
}
}
/// Main worker's message handler for various events.
#[wasm_bindgen]
pub fn main_onmessage(event: web_sys::MessageEvent) {
let data = event.data();
debug_assert!(data.is_array());
let data: js_sys::Array = data.unchecked_into();
match JsMessage::from_f64(data.get(0)).0 {
JsMessage::WINDOW_RESIZE_INNER => {
STATE.with_borrow_mut(|state| {
state.resize(JsResizeMessage::from_js_array(data, 1));
});
}
JsMessage::MOUSE_MOVE_INNER => {
STATE.with_borrow_mut(|state| {
state.mouse_move(JsMouseMessage::from_js_array(data, 1));
});
}
JsMessage::MOUSE_CLICK_INNER => {
STATE.with_borrow_mut(|state| {
state.mouse_click(JsMouseMessage::from_js_array(data, 1));
});
}
other => {
crate::log!("unsupported message: {:?}", other);
}
}
}
/// Drawing relative data.
/// Note that this belongs to main worker.
struct State {
canvas: OffscreenCanvas,
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
surface_config: wgpu::SurfaceConfiguration,
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_num: u32,
uniform_data: UniformData,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
render_pipeline: wgpu::RenderPipeline,
animation_cb: Closure<dyn FnMut(f32)>,
}
impl State {
pub async fn new(canvas: OffscreenCanvas) -> Self {
let id = wgpu::InstanceDescriptor {
backends: wgpu::Backends::BROWSER_WEBGPU,
..Default::default()
};
// wgpu instance
let instance = wgpu::Instance::new(&id);
// wgpu surface
let surface = instance
.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(
web_sys::OffscreenCanvas::clone(&canvas),
))
.unwrap();
// wgpu adapter
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
force_fallback_adapter: false,
..Default::default()
})
.await
.unwrap();
log!("Adapter info: {:?}", adapter.get_info());
log!("Adapter features: {:?}", adapter.features());
log!("Adapter limits: {:?}", adapter.limits());
// wgpu device and queue
let descriptor = wgpu::DeviceDescriptor {
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
label: None,
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::default(),
};
let (device, queue) = adapter.request_device(&descriptor).await.unwrap();
log!("after");
// wgpu surface configuration
let surface_caps = surface.get_capabilities(&adapter);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_caps.formats[0],
width: canvas.width(),
height: canvas.height(),
present_mode: surface_caps.present_modes[0],
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
log!(
"suface size: {} x {}",
surface_config.width,
surface_config.height
);
surface.configure(&device, &surface_config);
// wgpu vertex buffer
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex buffer"),
contents: bytemuck::cast_slice(VERTICES),
usage: wgpu::BufferUsages::VERTEX,
});
// wgpu index buffer
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index buffer"),
contents: bytemuck::cast_slice(INDICES),
usage: wgpu::BufferUsages::INDEX,
});
// wgpu uniform buffer
let uniform_data = UniformData {
resolution: [canvas.width() as f32, canvas.height() as f32],
mouse_move: [std::f32::MIN, std::f32::MIN],
mouse_click: [std::f32::MIN, std::f32::MIN],
..Default::default()
};
let (uniform_buffer, uniform_layout, uniform_bind_group) =
State::create_uniform_buffer(&device, bytemuck::cast_slice(&[uniform_data][..]));
// wgpu shader module
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shader module"),
source: wgpu::ShaderSource::Wgsl(include_str!("example.wgsl").into()),
});
// wgpu render pipeline
let render_pipeline = State::create_render_pipeline(
&device,
&[&uniform_layout],
&shader_module,
&surface_config,
);
// dummy animation callback.
let animation_cb = Closure::<dyn FnMut(f32)>::new(|_| {});
Self {
canvas,
surface,
device,
queue,
surface_config,
vertex_buffer,
index_buffer,
index_num: INDICES.len() as u32,
uniform_data,
uniform_buffer,
uniform_bind_group,
render_pipeline,
animation_cb,
}
}
fn create_uniform_buffer(
device: &wgpu::Device,
contents: &[u8],
) -> (wgpu::Buffer, wgpu::BindGroupLayout, wgpu::BindGroup) {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Uniform buffer"),
contents,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Uniform bind group layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Uniform bind group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
(buffer, bind_group_layout, bind_group)
}
fn create_render_pipeline(
device: &wgpu::Device,
bind_group_layouts: &[&wgpu::BindGroupLayout],
shader_module: &wgpu::ShaderModule,
surface_config: &wgpu::SurfaceConfiguration,
) -> wgpu::RenderPipeline {
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render pipeline layout"),
bind_group_layouts,
push_constant_ranges: &[],
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
cache: None,
label: Some("Render pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
compilation_options: wgpu::PipelineCompilationOptions::default(),
module: shader_module,
entry_point: Some("v_main"),
buffers: &[Vertex::layout()],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
compilation_options: wgpu::PipelineCompilationOptions::default(),
module: shader_module,
entry_point: Some("f_main"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
})
}
fn render(&mut self, time: f32) {
// Write uniform data to its buffer
self.uniform_data.time = time * 0.001;
self.queue.write_buffer(
&self.uniform_buffer,
0,
bytemuck::cast_slice(&[self.uniform_data][..]),
);
let surface_texture = self.surface.get_current_texture().unwrap();
let texture_view = surface_texture.texture.create_view(&Default::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render command encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
depth_slice: None,
view: &texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.draw_indexed(0..self.index_num, 0, 0..1);
}
self.queue.submit(std::iter::once(encoder.finish()));
surface_texture.present();
}
pub fn request_animation_frame(&self) {
let global = js_sys::global().unchecked_into::<web_sys::DedicatedWorkerGlobalScope>();
global
.request_animation_frame(self.animation_cb.as_ref().unchecked_ref())
.unwrap();
}
fn resize(&mut self, msg: JsResizeMessage) {
let new_width = (msg.width * msg.scale_factor) as u32;
let new_height = (msg.height * msg.scale_factor) as u32;
if new_width != self.canvas.width() || new_height != self.canvas.height() {
self.surface_config.width = new_width;
self.surface_config.height = new_height;
self.surface.configure(&self.device, &self.surface_config);
// Update uniform data
self.uniform_data.resolution = [new_width as f32, new_height as f32];
log!(
"Resized: ({}, {}), scale: {}",
new_width,
new_height,
msg.scale_factor
);
}
}
pub fn mouse_move(&mut self, msg: JsMouseMessage) {
// Update uniform data
let x = (msg.offset_x * msg.scale_factor) as f32;
let y = (msg.offset_y * msg.scale_factor) as f32;
self.uniform_data.mouse_move = [x, y];
}
pub fn mouse_click(&mut self, msg: JsMouseMessage) {
// Update uniform data
let x = (msg.offset_x * msg.scale_factor) as f32;
let y = (msg.offset_y * msg.scale_factor) as f32;
self.uniform_data.mouse_click = [x, y];
}
}
/// Simple vertex format.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
pos: [f32; 3],
color: [f32; 3],
}
impl Vertex {
fn layout() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
// pos
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
// color
offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
],
}
}
}
/// Vertex example.
const VERTICES: &[Vertex] = &[
Vertex {
pos: [-1.0, 1.0, 0.0], // Top-left
color: [1.0, 0.0, 1.0], // Magenta
},
Vertex {
pos: [-1.0, -1.0, 0.0], // Bottom-left
color: [0.0, 0.0, 1.0], // Blue
},
Vertex {
pos: [1.0, 1.0, 0.0], // Top-right
color: [1.0, 1.0, 0.0], // Yellow
},
Vertex {
pos: [1.0, -1.0, 0.0], // Bottom-right
color: [0.0, 1.0, 0.0], // Green
},
];
const INDICES: &[u32] = &[0, 1, 2, 2, 1, 3]; // CCW, quad
/// Simple uniform data.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug, Default)]
struct UniformData {
mouse_move: [f32; 2],
mouse_click: [f32; 2],
resolution: [f32; 2],
time: f32,
_padding: f32,
}
/// Boilerplate initialization for wasm debugging.
#[wasm_bindgen]
pub fn main() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Warn).unwrap();
}
/// Utility
#[macro_export]
macro_rules! log {
($($t:tt)*) => {
#[cfg(debug_assertions)]
{
$crate::console_log(format!($($t)*));
}
}
}
/// Utility
pub fn console_log(s: String) {
web_sys::console::log_1(&s.into());
}
-10
View File
@@ -1,10 +0,0 @@
import "./style.css";
import { attachCanvas, connectWorker } from "../core/connection/mainThread.ts";
import MainSceneWorker from "../examples/starterScene?worker";
import { ECS } from "../core/ecs/ecs";
const mainSceneWorker = new MainSceneWorker();
connectWorker(mainSceneWorker);
attachCanvas(mainSceneWorker, "rendering-canvas");
(window as any).ecs = new ECS();
+139
View File
@@ -0,0 +1,139 @@
use super::canvas::Canvas;
use std::mem;
use wasm_bindgen::JsValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JsMessage(pub u64);
impl JsMessage {
/// Common message group.
const COMMON: u64 = 0;
/// Window message group.
const WINDOW: u64 = 1 << 32;
/// Mouse message group.
const MOUSE: u64 = 2 << 32;
/// A common message requesting initialization of main object.
pub const INIT_INNER: u64 = Self::COMMON | 1;
pub const INIT: Self = Self(Self::INIT_INNER);
/// Window resize message.
pub const WINDOW_RESIZE_INNER: u64 = Self::WINDOW | 1;
pub const WINDOW_RESIZE: Self = Self(Self::WINDOW_RESIZE_INNER);
/// Mouse move message.
pub const MOUSE_MOVE_INNER: u64 = Self::MOUSE | 1;
pub const MOUSE_MOVE: Self = Self(Self::MOUSE_MOVE_INNER);
/// Mouse click message.
pub const MOUSE_CLICK_INNER: u64 = Self::MOUSE | 2;
pub const MOUSE_CLICK: Self = Self(Self::MOUSE_CLICK_INNER);
/// Reinterprets value to f64 in bit level.
/// Then convert it into JsValue.
/// Use [`Self::from_f64()`] to recover.
#[inline]
pub fn into_jsvalue(self) -> JsValue {
let f: f64 = unsafe { mem::transmute(self) };
JsValue::from(f)
}
/// Converts message given from JS side into original type.
#[inline]
pub fn from_f64(value: JsValue) -> Self {
let f = value.as_f64().unwrap();
Self(f.to_bits())
}
}
#[derive(Debug, Clone)]
pub struct JsResizeMessage {
pub scale_factor: f64,
pub handle: f64,
pub width: f64,
pub height: f64,
}
impl JsResizeMessage {
/// Returns number of fileds.
/// Use this to prepare [`js_sys::Array`] buffer.
#[inline]
pub const fn field_num() -> u32 {
4
}
/// Helper
pub fn set_js_array(arr: &js_sys::Array, canvas: &Canvas, scale_factor: f64, offset: u32) {
arr.set(offset, JsValue::from(scale_factor));
arr.set(offset + 1, JsValue::from(canvas.handle()));
arr.set(offset + 2, JsValue::from(canvas.client_width()));
arr.set(offset + 3, JsValue::from(canvas.client_height()));
}
/// Helper
pub fn from_js_array(arr: js_sys::Array, offset: u32) -> Self {
// Safety: Infallible.
unsafe {
Self {
scale_factor: arr.get(offset).as_f64().unwrap_unchecked(),
handle: arr.get(offset + 1).as_f64().unwrap_unchecked(),
width: arr.get(offset + 2).as_f64().unwrap_unchecked(),
height: arr.get(offset + 3).as_f64().unwrap_unchecked(),
}
}
}
}
#[derive(Debug, Clone)]
pub struct JsMouseMessage {
pub scale_factor: f64,
pub button: f64,
pub client_x: f64,
pub client_y: f64,
pub movement_x: f64,
pub movement_y: f64,
pub offset_x: f64,
pub offset_y: f64,
}
impl JsMouseMessage {
/// Returns number of fileds.
/// Use this to prepare [`js_sys::Array`] buffer.
#[inline]
pub const fn field_num() -> u32 {
8
}
/// Helper
pub fn set_js_array(
arr: &js_sys::Array,
event: web_sys::MouseEvent,
scale_factor: f64,
offset: u32,
) {
arr.set(offset, JsValue::from(scale_factor));
arr.set(offset + 1, JsValue::from(event.button()));
arr.set(offset + 2, JsValue::from(event.client_x()));
arr.set(offset + 3, JsValue::from(event.client_y()));
arr.set(offset + 4, JsValue::from(event.movement_x()));
arr.set(offset + 5, JsValue::from(event.movement_y()));
arr.set(offset + 6, JsValue::from(event.offset_x()));
arr.set(offset + 7, JsValue::from(event.offset_y()));
}
/// Helper
pub fn from_js_array(arr: js_sys::Array, offset: u32) -> Self {
// Safety: Infallible.
unsafe {
Self {
scale_factor: arr.get(offset).as_f64().unwrap_unchecked(),
button: arr.get(offset + 1).as_f64().unwrap_unchecked(),
client_x: arr.get(offset + 2).as_f64().unwrap_unchecked(),
client_y: arr.get(offset + 3).as_f64().unwrap_unchecked(),
movement_x: arr.get(offset + 4).as_f64().unwrap_unchecked(),
movement_y: arr.get(offset + 5).as_f64().unwrap_unchecked(),
offset_x: arr.get(offset + 6).as_f64().unwrap_unchecked(),
offset_y: arr.get(offset + 7).as_f64().unwrap_unchecked(),
}
}
}
}
-104
View File
@@ -1,104 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#rendering-canvas {
top: 0;
left: 0;
position: fixed;
width: 100vw;
height: 100vh;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #3178c6aa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

-1
View File
@@ -1 +0,0 @@
/// <reference types="vite/client" />
+24
View File
@@ -0,0 +1,24 @@
// Imports wasm statically.
// Reasonable because worker can't do anything without wasm.
// And worker's loading can still be determined from external dynamically.
import * as wasm from '../../../..';
export function attachMain() {}
onmessage = event => {
// Initailzes wasm.
const { default: wbg_init } = wasm;
wbg_init(event.data[0]).then(() => {
// Initializes our main worker.
onmessage = async (event) => {
const ready = await wasm.main_onmessage_init(event);
if (ready) {
// Now, worker is ready for work.
onmessage = wasm.main_onmessage;
}
}
});
}
+79
View File
@@ -0,0 +1,79 @@
use std::{fmt::Debug, ops::Deref};
use wasm_bindgen::prelude::*;
/// Binds JS.
#[wasm_bindgen(module = "/src/worker/workerGen.js")]
extern "C" {
/// Spawn new worker in JS side in order to make bundler know about dependency.
#[wasm_bindgen(js_name = "createWorker")]
fn create_worker(kind: &str, name: &str) -> web_sys::Worker;
}
/// Binds JS.
/// This makes wasm-bindgen bring `mainWorker.js` to the `pkg` directory.
/// So that bundler can bundle it together.
#[wasm_bindgen(module = "/src/worker/mainWorker.js")]
extern "C" {
/// Nothing to do.
#[wasm_bindgen]
fn attachMain();
}
pub struct MainWorker {
handle: web_sys::Worker,
name: String,
_callback: Closure<dyn FnMut(web_sys::Event)>,
}
impl Drop for MainWorker {
/// Terminates web worker *immediately*.
fn drop(&mut self) {
self.handle.terminate();
crate::log!("Worker({}) was terminated", &self.name);
}
}
impl Debug for MainWorker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MainWorker")
.field("handle", &self.handle)
.field("name", &self.name)
.finish()
}
}
impl MainWorker {
/// Spawns main worker from the window context.
pub fn spawn(name: &str, id: usize) -> Result<Self, JsValue> {
// Creates a new worker.
let handle = create_worker("main", name);
// Sets default callback.
let callback = Closure::new(|_ev| {
// Implement if you want.
unimplemented!()
});
handle.set_onmessage(Some(callback.as_ref().unchecked_ref()));
// Initializes the worker.
let msg = js_sys::Array::new_with_length(2);
msg.set(0, wasm_bindgen::module());
msg.set(1, id.into());
handle.post_message(&msg)?;
Ok(Self {
handle,
name: name.to_owned(),
_callback: callback,
})
}
}
impl Deref for MainWorker {
type Target = web_sys::Worker;
#[inline]
fn deref(&self) -> &Self::Target {
&self.handle
}
}
+13
View File
@@ -0,0 +1,13 @@
export function createWorker(kind, name) {
switch (kind) {
case 'main':
const main = new Worker(new URL('./mainWorker.js', import.meta.url), {
type: 'module',
/* @vite-ignore */ name, // vite doesn't allow non static value here.
});
return main;
default:
console.log("unsurpported type of worker: ", kind);
return undefined;
}
}
+43
View File
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
}
canvas {
width: 90%;
height: 300px;
background-color: yellowgreen;
}
</style>
</head>
<body>
<header>
<nav>
<ul>
</ul>
</nav>
<h1 class="title">Enjoy WebGPU + WASM + Webpack with 🍰☕</h1>
</header>
<main>
<section>
<!-- TODO: Intro -->
</section>
<section>
<canvas id="canvas0"></canvas>
<script type="module" src="./index.js"></script>
</section>
</main>
<footer>
</footer>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
import wbg_init, { main, App } from "../pkg/wasm-index.js";
const start = async () => {
await wbg_init();
main();
const app = new App();
};
start();
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+30
View File
@@ -0,0 +1,30 @@
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";
export default defineConfig({
build: {
rollupOptions: {
input: {
app: "static/index.html",
},
},
// Relative to 'root'.
outDir: "../dist",
},
// For getting out of index.html from dist/static directory.
root: "static",
worker: {
format: "es",
},
plugins: [
// Makes us be able to use top level await for wasm.
// Otherwise, we can restrict build.target to 'es2022', which allows top level await.
wasm(),
],
server: {
port: 8080,
},
preview: {
port: 8080,
},
});
-6
View File
@@ -1,6 +0,0 @@
import { defineConfig } from "vite";
import glsl from "vite-plugin-glsl";
export default defineConfig({
plugins: [glsl({})],
});
+1824 -154
View File
File diff suppressed because it is too large Load Diff