1/**
2 * A WebGPU view of the scene: the surface colored by the surface field, the
3 * three planes, and the source. For the room, walls whose outward normal faces
4 * the camera are cut away in the fragment shader, so the view looks into the
5 * room through the nearer walls; the cymbal is drawn whole. Drag to orbit,
6 * scroll or pinch to zoom.
7 */
8import { basis2d, type Surface } from '../surface.ts';
9import { DIVERGING, SEQUENTIAL } from './colormaps.ts';
10import type { Field, Mode } from './field.ts';
12/** Multisampling; 1 when the query string has ?nomsaa (SwiftShader's GPU process does not survive the resolve in headless Chrome). */
13const SAMPLES = typeof location !== 'undefined' && location.search.includes('nomsaa') ? 1 : 4;
15const SHARED = /* wgsl */ `
16struct View {
17 viewProj: mat4x4f,
18 eye: vec4f,
19 p1: vec4f, // cos wt, sin wt, umax, 0
20 p2: vec4f, // mode (0 pressure, 1 level), db min, db max, level colormap (0 sequential, 1 diverging)
21 p3: vec4f, // cut away surfaces facing the camera (0, 1), 0, 0, 0
22}
23@group(0) @binding(0) var<uniform> V: View;
24@group(0) @binding(1) var cmapD: texture_2d<f32>;
25@group(0) @binding(2) var cmapS: texture_2d<f32>;
26@group(0) @binding(3) var samp: sampler;
28// lref: the reference of the level at this point ('ref' is reserved in WGSL)
29fn colorOf(re: f32, im: f32, lref: f32) -> vec3f {
30 if (V.p2.x < 0.5) {
31 let t = clamp(0.5 + 0.5 * (re * V.p1.x + im * V.p1.y) / V.p1.z, 0.0, 1.0);
32 return textureSampleLevel(cmapD, samp, vec2f(t, 0.5), 0.0).rgb;
33 }
34 let db = 20.0 * log2(max(length(vec2f(re, im)) / lref, 1e-30)) * 0.30102999566;
35 let t = clamp((db - V.p2.y) / (V.p2.z - V.p2.y), 0.0, 1.0);
36 if (V.p2.w > 0.5) { return textureSampleLevel(cmapD, samp, vec2f(t, 0.5), 0.0).rgb; }
37 return textureSampleLevel(cmapS, samp, vec2f(t, 0.5), 0.0).rgb;
38}
39`;
41const WALL = /* wgsl */ `
42${SHARED}
43struct VOut {
44 @builtin(position) pos: vec4f,
45 @location(0) world: vec3f,
46 @location(1) nrm: vec3f,
47 @location(2) u: vec2f,
48 @location(3) lref: f32,
49}
50@vertex fn vs(@location(0) p: vec3f, @location(1) n: vec3f, @location(2) u: vec2f, @location(3) lref: f32) -> VOut {
51 var o: VOut;
52 o.pos = V.viewProj * vec4f(p, 1.0);
53 o.world = p;
54 o.nrm = n;
55 o.u = u;
56 o.lref = lref;
57 return o;
58}
59@fragment fn fs(i: VOut) -> @location(0) vec4f {
60 let toEye = normalize(V.eye.xyz - i.world);
61 let n = normalize(i.nrm);
62 if (V.p3.x > 0.5 && dot(n, toEye) > 0.0) { discard; }
63 let shade = 0.6 + 0.4 * abs(dot(n, toEye));
64 return vec4f(colorOf(i.u.x, i.u.y, i.lref) * shade, 1.0);
65}
66`;
68const PLANE = /* wgsl */ `
69${SHARED}
70@group(0) @binding(4) var field: texture_2d<f32>;
71struct VOut {
72 @builtin(position) pos: vec4f,
73 @location(0) uv: vec2f,
74}
75@vertex fn vs(@location(0) p: vec3f, @location(1) uv: vec2f) -> VOut {
76 var o: VOut;
77 o.pos = V.viewProj * vec4f(p, 1.0);
78 o.uv = uv;
79 return o;
80}
81@fragment fn fs(i: VOut) -> @location(0) vec4f {
82 // bilinear by hand: r32float textures are not filterable everywhere
83 let dim = vec2f(textureDimensions(field));
84 let x = clamp(i.uv * (dim - 1.0), vec2f(0.0), dim - 1.0);
85 let x0 = vec2i(floor(x));
86 let x1 = min(x0 + 1, vec2i(dim) - 1);
87 let f = x - vec2f(x0);
88 // (Re u, Im u, level reference)
89 let a = textureLoad(field, x0, 0).xyz;
90 let b = textureLoad(field, vec2i(x1.x, x0.y), 0).xyz;
91 let c = textureLoad(field, vec2i(x0.x, x1.y), 0).xyz;
92 let d = textureLoad(field, x1, 0).xyz;
93 // NaN marks points where the field is not computed
94 if (any(a != a) || any(b != b) || any(c != c) || any(d != d)) { discard; }
95 let u = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
96 return vec4f(colorOf(u.x, u.y, u.z), 1.0);
97}
98`;
100const MARKER = /* wgsl */ `
101${SHARED}
102@vertex fn vs(@location(0) p: vec3f) -> @builtin(position) vec4f {
103 return V.viewProj * vec4f(p, 1.0);
104}
105@fragment fn fs() -> @location(0) vec4f {
106 return vec4f(0.05, 0.05, 0.05, 1.0);
107}
108`;
110type Mat4 = Float32Array;
111const BG = { r: 0.043, g: 0.055, b: 0.071, a: 1 };
113function perspective(fovy: number, aspect: number, near: number, far: number): Mat4 {
114 const f = 1 / Math.tan(fovy / 2);
115 const m = new Float32Array(16);
116 m[0] = f / aspect;
117 m[5] = f;
118 m[10] = far / (near - far);
119 m[11] = -1;
120 m[14] = (near * far) / (near - far);
121 return m;
122}
124function lookAt(eye: number[], target: number[], up: number[]): Mat4 {
125 const z = norm(sub(eye, target));
126 const x = norm(cross(up, z));
127 const y = cross(z, x);
128 const m = new Float32Array(16);
129 m.set([x[0], y[0], z[0], 0, x[1], y[1], z[1], 0, x[2], y[2], z[2], 0, -dot(x, eye), -dot(y, eye), -dot(z, eye), 1]);
130 return m;
131}
133function mul(a: Mat4, b: Mat4): Mat4 {
134 const m = new Float32Array(16);
135 for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) {
136 let s = 0;
137 for (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k];
138 m[c * 4 + r] = s;
139 }
140 return m;
141}
143const sub = (a: number[], b: number[]) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
144const dot = (a: number[], b: number[]) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
145const cross = (a: number[], b: number[]) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
146const norm = (a: number[]) => {
147 const l = Math.hypot(a[0], a[1], a[2]);
148 return [a[0] / l, a[1] / l, a[2] / l];
149};
151export interface Show {
152 walls: boolean;
153 planes: [boolean, boolean, boolean];
154}
156export class RoomView {
157 private ctx: GPUCanvasContext;
158 private format: GPUTextureFormat;
159 private wallPipe: GPURenderPipeline;
160 private planePipe: GPURenderPipeline;
161 private markerPipe: GPURenderPipeline;
162 private ubuf: GPUBuffer;
163 private cmapD: GPUTexture;
164 private cmapS: GPUTexture;
165 private sampler: GPUSampler;
166 private baseGroup: GPUBindGroup;
167 private wall: { vbuf: GPUBuffer; ibuf: GPUBuffer; count: number } | null = null;
168 private planes: { vbuf: GPUBuffer; tex: GPUTexture; group: GPUBindGroup }[] = [];
169 private marker: { vbuf: GPUBuffer; count: number } | null = null;
170 private msaa: GPUTexture | null = null;
171 private depth: GPUTexture | null = null;
172 private theta = 0.75;
173 private phi = 0.95;
174 private dist = 5.2;
175 private field: Field | null = null;
176 private dirty = true;
178 constructor(
179 private device: GPUDevice,
180 private canvas: HTMLCanvasElement,
181 ) {
182 this.ctx = canvas.getContext('webgpu') as GPUCanvasContext;
183 this.format = navigator.gpu.getPreferredCanvasFormat();
184 this.ctx.configure({ device, format: this.format, alphaMode: 'opaque' });
185 this.ubuf = device.createBuffer({ size: 128, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
186 const mkmap = (lut: Uint8Array<ArrayBuffer>) => {
187 const t = device.createTexture({ size: [256, 1], format: 'rgba8unorm', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
188 device.queue.writeTexture({ texture: t }, lut, { bytesPerRow: 1024 }, [256, 1]);
189 return t;
190 };
191 this.cmapD = mkmap(DIVERGING);
192 this.cmapS = mkmap(SEQUENTIAL);
193 this.sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear', addressModeU: 'clamp-to-edge' });
195 const baseEntries: GPUBindGroupLayoutEntry[] = [
196 { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
197 { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
198 { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
199 { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
200 ];
201 const baseLayout = device.createBindGroupLayout({ entries: baseEntries });
202 const planeLayout = device.createBindGroupLayout({
203 entries: [...baseEntries, { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'unfilterable-float' } }],
204 });
205 this.baseGroup = device.createBindGroup({ layout: baseLayout, entries: this.baseEntries() });
206 const common = (code: string, layout: GPUBindGroupLayout, buffers: GPUVertexBufferLayout[], topology: GPUPrimitiveTopology = 'triangle-list'): GPURenderPipeline => {
207 const module = device.createShaderModule({ code });
208 return device.createRenderPipeline({
209 layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
210 vertex: { module, entryPoint: 'vs', buffers },
211 fragment: { module, entryPoint: 'fs', targets: [{ format: this.format }] },
212 primitive: { topology, cullMode: 'none' },
213 depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
214 multisample: { count: SAMPLES },
215 });
216 };
217 this.wallPipe = common(WALL, baseLayout, [
218 {
219 arrayStride: 36,
220 attributes: [
221 { shaderLocation: 0, offset: 0, format: 'float32x3' },
222 { shaderLocation: 1, offset: 12, format: 'float32x3' },
223 { shaderLocation: 2, offset: 24, format: 'float32x2' },
224 { shaderLocation: 3, offset: 32, format: 'float32' },
225 ],
226 },
227 ]);
228 this.planePipe = common(PLANE, planeLayout, [
229 {
230 arrayStride: 20,
231 attributes: [
232 { shaderLocation: 0, offset: 0, format: 'float32x3' },
233 { shaderLocation: 1, offset: 12, format: 'float32x2' },
234 ],
235 },
236 ]);
237 this.planeLayout = planeLayout;
238 this.markerPipe = common(MARKER, baseLayout, [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }]);
239 this.attachControls();
240 new ResizeObserver(() => {
241 this.dirty = true;
242 }).observe(canvas);
243 }
245 private planeLayout: GPUBindGroupLayout;
247 private baseEntries(): GPUBindGroupEntry[] {
248 return [
249 { binding: 0, resource: { buffer: this.ubuf } },
250 { binding: 1, resource: this.cmapD.createView() },
251 { binding: 2, resource: this.cmapS.createView() },
252 { binding: 3, resource: this.sampler },
253 ];
254 }
256 private attachControls() {
257 const c = this.canvas;
258 const pointers = new Map<number, { x: number; y: number }>();
259 let pinch = 0;
260 c.addEventListener('pointerdown', (e) => {
261 c.setPointerCapture(e.pointerId);
262 pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
263 if (pointers.size === 2) {
264 const [a, b] = [...pointers.values()];
265 pinch = Math.hypot(a.x - b.x, a.y - b.y);
266 }
267 });
268 c.addEventListener('pointermove', (e) => {
269 const p = pointers.get(e.pointerId);
270 if (!p) return;
271 if (pointers.size === 1) {
272 this.theta -= (e.clientX - p.x) * 0.008;
273 this.phi = Math.min(Math.PI - 0.05, Math.max(0.05, this.phi - (e.clientY - p.y) * 0.008));
274 }
275 p.x = e.clientX;
276 p.y = e.clientY;
277 if (pointers.size === 2) {
278 const [a, b] = [...pointers.values()];
279 const d = Math.hypot(a.x - b.x, a.y - b.y);
280 if (pinch > 0) this.zoom(pinch / d);
281 pinch = d;
282 }
283 this.dirty = true;
284 });
285 const up = (e: PointerEvent) => {
286 pointers.delete(e.pointerId);
287 pinch = 0;
288 };
289 c.addEventListener('pointerup', up);
290 c.addEventListener('pointercancel', up);
291 c.addEventListener(
292 'wheel',
293 (e) => {
294 e.preventDefault();
295 this.zoom(Math.exp(e.deltaY * 0.001));
296 },
297 { passive: false },
298 );
299 }
301 /** the scene's radius: 1 for the room */
302 private scale = 1;
303 private center: [number, number, number] = [0, 0, 0];
305 private zoom(f: number) {
306 this.dist = Math.min(12 * this.scale, Math.max(2.2 * this.scale, this.dist * f));
307 this.dirty = true;
308 }
310 /** Drop the displayed field, for while a new one is computed. */
311 clear() {
312 this.wall?.vbuf.destroy();
313 this.wall?.ibuf.destroy();
314 this.wall = null;
315 for (const p of this.planes) {
316 p.vbuf.destroy();
317 p.tex.destroy();
318 }
319 this.planes = [];
320 this.marker?.vbuf.destroy();
321 this.marker = null;
322 this.field = null;
323 this.dirty = true;
324 }
326 /** The surface mesh: every patch sampled on an nv x nv grid, with the surface field interpolated by the patch's Legendre expansion. */
327 setField(surf: Surface, f: Field, nv = 9) {
328 const { device } = this;
329 this.field = f;
330 if (f.scene.radius !== this.scale) {
331 this.dist *= f.scene.radius / this.scale;
332 this.scale = f.scene.radius;
333 }
334 this.center = f.scene.center;
335 const npols = surf.npols;
336 const npatches = surf.npatches;
337 const npts = surf.npts;
338 const uv = new Float64Array(2 * nv * nv);
339 for (let a = 0; a < nv; a++) for (let b = 0; b < nv; b++) {
340 uv[a * nv + b] = -1 + (2 * b) / (nv - 1);
341 uv[nv * nv + a * nv + b] = -1 + (2 * a) / (nv - 1);
342 }
343 const B = basis2d(uv, surf.norder); // (npols, nv^2)
344 const ng = nv * nv;
345 const verts = new Float32Array(9 * ng * npatches);
346 const idx = new Uint32Array(6 * (nv - 1) * (nv - 1) * npatches);
347 const cr = new Float64Array(npols);
348 const ci = new Float64Array(npols);
349 let ii = 0;
350 for (let j = 0; j < npatches; j++) {
351 const g = surf.evalGeometry(j, uv);
352 for (let n = 0; n < npols; n++) {
353 let sr = 0;
354 let si = 0;
355 for (let l = 0; l < npols; l++) {
356 const U = surf.umat[n * npols + l];
357 sr += U * f.uWall[2 * (j * npols + l)];
358 si += U * f.uWall[2 * (j * npols + l) + 1];
359 }
360 cr[n] = sr;
361 ci[n] = si;
362 }
363 for (let q = 0; q < ng; q++) {
364 const o = 9 * (j * ng + q);
365 const ru = [g[3 * ng + q], g[4 * ng + q], g[5 * ng + q]];
366 const rv = [g[6 * ng + q], g[7 * ng + q], g[8 * ng + q]];
367 const nn = norm(cross(ru, rv));
368 let ur = 0;
369 let ui = 0;
370 for (let n = 0; n < npols; n++) {
371 ur += cr[n] * B[n * ng + q];
372 ui += ci[n] * B[n * ng + q];
373 }
374 const ref = f.freeFieldLevel ? 1 / (4 * Math.PI * Math.hypot(g[q] - f.x0[0], g[ng + q] - f.x0[1], g[2 * ng + q] - f.x0[2])) : f.ref;
375 verts.set([g[q], g[ng + q], g[2 * ng + q], nn[0], nn[1], nn[2], ur, ui, ref], o);
376 }
377 for (let a = 0; a < nv - 1; a++) for (let b = 0; b < nv - 1; b++) {
378 const v0 = j * ng + a * nv + b;
379 idx.set([v0, v0 + 1, v0 + nv + 1, v0, v0 + nv + 1, v0 + nv], ii);
380 ii += 6;
381 }
382 }
383 void npts;
384 this.wall?.vbuf.destroy();
385 this.wall?.ibuf.destroy();
386 const vbuf = device.createBuffer({ size: verts.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
387 device.queue.writeBuffer(vbuf, 0, verts);
388 const ibuf = device.createBuffer({ size: idx.byteLength, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST });
389 device.queue.writeBuffer(ibuf, 0, idx);
390 this.wall = { vbuf, ibuf, count: idx.length };
392 // the three planes, as textures of (Re u, Im u, level reference) with NaN where there is no field
393 for (const p of this.planes) {
394 p.vbuf.destroy();
395 p.tex.destroy();
396 }
397 this.planes = f.slices.map(({ slice, u, ref }) => {
398 const n = f.ngrid;
399 const tex = device.createTexture({ size: [n, n], format: 'rgba32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
400 const data = new Float32Array(4 * n * n);
401 for (let q = 0; q < n * n; q++) {
402 data[4 * q] = slice.mask[q] ? u[2 * q] : NaN;
403 data[4 * q + 1] = slice.mask[q] ? u[2 * q + 1] : NaN;
404 data[4 * q + 2] = ref ? ref[q] : f.ref;
405 }
406 device.queue.writeTexture({ texture: tex }, data, { bytesPerRow: 16 * n }, [n, n]);
407 const [i, jx] = slice.axes;
408 const [ea, eb] = slice.extent;
409 const corner = (s: number, t: number) => {
410 const x = [0, 0, 0];
411 x[i] = ea[0] + (ea[1] - ea[0]) * s;
412 x[jx] = eb[0] + (eb[1] - eb[0]) * t;
413 x[slice.fixed] = slice.value;
414 return [...x, s, t];
415 };
416 const quad = new Float32Array([...corner(0, 0), ...corner(1, 0), ...corner(1, 1), ...corner(0, 0), ...corner(1, 1), ...corner(0, 1)]);
417 const vb = device.createBuffer({ size: quad.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
418 device.queue.writeBuffer(vb, 0, quad);
419 const group = device.createBindGroup({ layout: this.planeLayout, entries: [...this.baseEntries(), { binding: 4, resource: tex.createView() }] });
420 return { vbuf: vb, tex, group };
421 });
423 // the source: a small octahedron
424 const r = 0.035 * this.scale;
425 const [x, y, z] = f.x0;
426 const P = [
427 [x + r, y, z], [x - r, y, z], [x, y + r, z], [x, y - r, z], [x, y, z + r], [x, y, z - r],
428 ];
429 const faces = [[0, 2, 4], [2, 1, 4], [1, 3, 4], [3, 0, 4], [2, 0, 5], [1, 2, 5], [3, 1, 5], [0, 3, 5]];
430 const mv = new Float32Array(faces.flat().flatMap((k) => P[k]));
431 this.marker?.vbuf.destroy();
432 const mb = device.createBuffer({ size: mv.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
433 device.queue.writeBuffer(mb, 0, mv);
434 this.marker = { vbuf: mb, count: faces.length * 3 };
435 this.dirty = true;
436 }
438 private targets() {
439 const dpr = Math.min(2, window.devicePixelRatio || 1);
440 const w = Math.max(1, Math.round(this.canvas.clientWidth * dpr));
441 const h = Math.max(1, Math.round(this.canvas.clientHeight * dpr));
442 if (this.canvas.width !== w || this.canvas.height !== h || !this.depth) {
443 this.canvas.width = w;
444 this.canvas.height = h;
445 this.msaa?.destroy();
446 this.depth?.destroy();
447 this.msaa = this.device.createTexture({ size: [w, h], format: this.format, sampleCount: SAMPLES, usage: GPUTextureUsage.RENDER_ATTACHMENT });
448 this.depth = this.device.createTexture({ size: [w, h], format: 'depth24plus', sampleCount: SAMPLES, usage: GPUTextureUsage.RENDER_ATTACHMENT });
449 }
450 return { w, h };
451 }
453 draw(mode: Mode, phase: number, show: Show, animating: boolean) {
454 if (!animating && !this.dirty) return;
455 this.dirty = false;
456 const { device } = this;
457 const { w, h } = this.targets();
458 const c = this.center;
459 const eye = [
460 c[0] + this.dist * Math.sin(this.phi) * Math.cos(this.theta),
461 c[1] + this.dist * Math.sin(this.phi) * Math.sin(this.theta),
462 c[2] + this.dist * Math.cos(this.phi),
463 ];
464 const vp = mul(perspective((35 * Math.PI) / 180, w / h, 0.01 * this.scale, 50 * this.scale), lookAt(eye, c, [0, 0, 1]));
465 const f = this.field;
466 const u = new Float32Array(32);
467 u.set(vp, 0);
468 u.set([eye[0], eye[1], eye[2], 1], 16);
469 u.set([Math.cos(phase), Math.sin(phase), f?.umax ?? 1, 0], 20);
470 u.set([mode === 'pressure' ? 0 : 1, f?.level.min ?? -30, f?.level.max ?? 15, f?.level.diverging ? 1 : 0], 24);
471 u.set([f?.scene.cutaway === false ? 0 : 1, 0, 0, 0], 28);
472 device.queue.writeBuffer(this.ubuf, 0, u);
473 const enc = device.createCommandEncoder();
474 const pass = enc.beginRenderPass({
475 colorAttachments: [
476 SAMPLES > 1
477 ? { view: this.msaa!.createView(), resolveTarget: this.ctx.getCurrentTexture().createView(), clearValue: BG, loadOp: 'clear', storeOp: 'discard' }
478 : { view: this.ctx.getCurrentTexture().createView(), clearValue: BG, loadOp: 'clear', storeOp: 'store' },
479 ],
480 depthStencilAttachment: { view: this.depth!.createView(), depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'discard' },
481 });
482 if (this.wall && show.walls) {
483 pass.setPipeline(this.wallPipe);
484 pass.setBindGroup(0, this.baseGroup);
485 pass.setVertexBuffer(0, this.wall.vbuf);
486 pass.setIndexBuffer(this.wall.ibuf, 'uint32');
487 pass.drawIndexed(this.wall.count);
488 }
489 if (this.planes.length) {
490 pass.setPipeline(this.planePipe);
491 this.planes.forEach((p, s) => {
492 if (!show.planes[s]) return;
493 pass.setBindGroup(0, p.group);
494 pass.setVertexBuffer(0, p.vbuf);
495 pass.draw(6);
496 });
497 }
498 if (this.marker) {
499 pass.setPipeline(this.markerPipe);
500 pass.setBindGroup(0, this.baseGroup);
501 pass.setVertexBuffer(0, this.marker.vbuf);
502 pass.draw(this.marker.count);
503 }
504 pass.end();
505 device.queue.submit([enc.finish()]);
506 }
508 invalidate() {
509 this.dirty = true;
510 }
511}