@aardworx/wombat.rendering 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ // ElementType — the data type of a single element in a BufferView.
2
+ //
3
+ // Mirrors Aardvark.Rendering's `BufferView.ElementType : System.Type`
4
+ // pattern: structural information about the element kept separate
5
+ // from the per-frame buffer content. The runtime maps `ElementType`
6
+ // to a `GPUVertexFormat`/`GPUIndexFormat` and a byte size at
7
+ // pipeline-layout time.
8
+
9
+ export type ElementType =
10
+ // scalar
11
+ | "f32" | "u32" | "i32"
12
+ | "u16" | "i16"
13
+ | "u8" | "i8"
14
+ // vec — float
15
+ | "v2f" | "v3f" | "v4f"
16
+ // vec — int
17
+ | "v2i" | "v3i" | "v4i"
18
+ // vec — uint
19
+ | "v2u" | "v3u" | "v4u"
20
+ // matrix (float, square)
21
+ | "m22f" | "m33f" | "m44f"
22
+ // packed colour
23
+ | "c4b" | "c3b";
24
+
25
+ export const ElementType = {
26
+ /** Size of one element in bytes. */
27
+ byteSize(t: ElementType): number {
28
+ switch (t) {
29
+ case "f32": case "u32": case "i32": return 4;
30
+ case "u16": case "i16": return 2;
31
+ case "u8": case "i8": return 1;
32
+ case "v2f": case "v2i": case "v2u": return 8;
33
+ case "v3f": case "v3i": case "v3u": return 12;
34
+ case "v4f": case "v4i": case "v4u": return 16;
35
+ case "m22f": return 16;
36
+ case "m33f": return 36;
37
+ case "m44f": return 64;
38
+ case "c4b": return 4;
39
+ case "c3b": return 3;
40
+ }
41
+ },
42
+
43
+ /**
44
+ * GPUVertexFormat for use as a vertex / instance attribute.
45
+ * `normalized=true` only affects integer types — picks the `unorm`
46
+ * / `snorm` form. Matrix types are not directly supported as
47
+ * vertex formats; the auto-instancing pass splits them into
48
+ * column attributes upstream.
49
+ */
50
+ toVertexFormat(t: ElementType, normalized = false): GPUVertexFormat {
51
+ switch (t) {
52
+ case "f32": return "float32";
53
+ case "u32": return "uint32";
54
+ case "i32": return "sint32";
55
+ case "v2f": return "float32x2";
56
+ case "v3f": return "float32x3";
57
+ case "v4f": return "float32x4";
58
+ case "v2i": return "sint32x2";
59
+ case "v3i": return "sint32x3";
60
+ case "v4i": return "sint32x4";
61
+ case "v2u": return "uint32x2";
62
+ case "v3u": return "uint32x3";
63
+ case "v4u": return "uint32x4";
64
+ case "u16": return normalized ? "unorm16x2" as GPUVertexFormat : "uint16";
65
+ case "i16": return normalized ? "snorm16x2" as GPUVertexFormat : "sint16";
66
+ case "u8": return normalized ? "unorm8x4" as GPUVertexFormat : "uint8";
67
+ case "i8": return normalized ? "snorm8x4" as GPUVertexFormat : "sint8";
68
+ case "c4b": return normalized ? "unorm8x4" : "uint8x4";
69
+ case "c3b":
70
+ // No 3-component byte format in WebGPU; promoted to x4 with
71
+ // a dummy fourth byte by the upload path.
72
+ throw new Error(`ElementType.toVertexFormat: c3b cannot be a vertex format directly; pad to c4b`);
73
+ case "m22f": case "m33f": case "m44f":
74
+ throw new Error(`ElementType.toVertexFormat: ${t} is a matrix; split into column attributes first`);
75
+ }
76
+ },
77
+
78
+ /** Index format. Throws unless the type is `u16` or `u32`. */
79
+ toIndexFormat(t: ElementType): GPUIndexFormat {
80
+ if (t === "u16") return "uint16";
81
+ if (t === "u32") return "uint32";
82
+ throw new Error(`ElementType.toIndexFormat: ${t} is not a valid index type (use u16 or u32)`);
83
+ },
84
+ } as const;
package/src/core/index.ts CHANGED
@@ -11,10 +11,14 @@ export type {
11
11
  HostBufferSource,
12
12
  } from "./buffer.js";
13
13
 
14
- export type {
14
+ export {
15
15
  BufferView,
16
16
  } from "./bufferView.js";
17
17
 
18
+ export {
19
+ ElementType,
20
+ } from "./elementType.js";
21
+
18
22
  export type {
19
23
  ClearColor,
20
24
  ClearValues,
@@ -45,9 +45,9 @@ export interface RenderObject {
45
45
  * individual buffer values are reactive. The shader's required-input
46
46
  * set is fixed at compile; the map must contain at least those names.
47
47
  */
48
- readonly vertexAttributes: HashMap<string, aval<BufferView>>;
48
+ readonly vertexAttributes: HashMap<string, BufferView>;
49
49
  /** name → instance buffer view; e.g. "modelMatrix", "instanceColor". */
50
- readonly instanceAttributes?: HashMap<string, aval<BufferView>>;
50
+ readonly instanceAttributes?: HashMap<string, BufferView>;
51
51
  /** name → uniform value; runtime packs into UBO based on shader layout. */
52
52
  readonly uniforms: HashMap<string, aval<unknown>>;
53
53
  /** name → texture source (CPU image or pre-built GPUTexture). */
@@ -58,6 +58,6 @@ export interface RenderObject {
58
58
  readonly storageBuffers?: HashMap<string, aval<IBuffer>>;
59
59
 
60
60
  /** Index buffer for indexed draws. */
61
- readonly indices?: aval<BufferView>;
61
+ readonly indices?: BufferView;
62
62
  readonly drawCall: aval<DrawCall>;
63
63
  }
@@ -14,6 +14,8 @@
14
14
 
15
15
  import {
16
16
  AdaptiveResource,
17
+ BufferView,
18
+ ElementType,
17
19
  type CompiledEffect,
18
20
  type FramebufferSignature,
19
21
  type ProgramInterface,
@@ -25,7 +27,6 @@ import {
25
27
  HashMap,
26
28
  cval,
27
29
  } from "@aardworx/wombat.adaptive";
28
- import type { BufferView } from "../core/bufferView.js";
29
30
  import type { Type } from "@aardworx/wombat.shader/ir";
30
31
  import { prepareAdaptiveBuffer } from "./adaptiveBuffer.js";
31
32
  import { prepareAdaptiveTexture } from "./adaptiveTexture.js";
@@ -34,32 +35,6 @@ import { prepareUniformBuffer } from "./uniformBuffer.js";
34
35
  import { compileRenderPipeline, type CompileRenderPipelineDescription } from "./renderPipeline.js";
35
36
  import { BufferUsage, ShaderStage } from "./webgpuFlags.js";
36
37
 
37
- // ---------------------------------------------------------------------------
38
- // Helpers
39
- // ---------------------------------------------------------------------------
40
-
41
- function isIndexFormat(fmt: string): boolean {
42
- return fmt === "uint16" || fmt === "uint32";
43
- }
44
-
45
- function vertexFormatStride(fmt: GPUVertexFormat): number {
46
- switch (fmt) {
47
- case "float32": return 4;
48
- case "float32x2": return 8;
49
- case "float32x3": return 12;
50
- case "float32x4": return 16;
51
- case "uint32": case "sint32": return 4;
52
- case "uint32x2": case "sint32x2": return 8;
53
- case "uint32x3": case "sint32x3": return 12;
54
- case "uint32x4": case "sint32x4": return 16;
55
- case "uint16x2": case "sint16x2": case "float16x2": return 4;
56
- case "uint16x4": case "sint16x4": case "float16x4": return 8;
57
- case "uint8x2": case "sint8x2": case "unorm8x2": case "snorm8x2": return 2;
58
- case "uint8x4": case "sint8x4": case "unorm8x4": case "snorm8x4": return 4;
59
- default: throw new Error(`vertexFormatStride: unsupported format ${fmt}`);
60
- }
61
- }
62
-
63
38
  interface VertexBindingInfo {
64
39
  readonly name: string;
65
40
  readonly slot: number;
@@ -371,10 +346,10 @@ export class PreparedRenderObject {
371
346
  private readonly device: GPUDevice,
372
347
  private readonly vertexBindings: readonly VertexBindingInfo[],
373
348
  private readonly vertexBuffers: ReadonlyMap<string, AdaptiveResource<GPUBuffer>>,
374
- private readonly vertexViews: ReadonlyMap<string, aval<import("../core/index.js").BufferView>>,
349
+ private readonly vertexViews: ReadonlyMap<string, BufferView>,
375
350
  private readonly indexBuffer: AdaptiveResource<GPUBuffer> | undefined,
376
351
  private readonly indexFormat: GPUIndexFormat | undefined,
377
- private readonly indices: aval<import("../core/index.js").BufferView> | undefined,
352
+ private readonly indices: BufferView | undefined,
378
353
  groups: readonly GroupDesc[],
379
354
  private readonly drawCall: aval<import("../core/index.js").DrawCall>,
380
355
  pipelineCtx: PipelineBuildContext,
@@ -479,15 +454,12 @@ export class PreparedRenderObject {
479
454
  for (const vb of this.vertexBindings) {
480
455
  const res = this.vertexBuffers.get(vb.name);
481
456
  if (res === undefined) throw new Error(`PreparedRenderObject.record: missing vertex buffer for "${vb.name}"`);
482
- const viewAval = this.vertexViews.get(vb.name);
483
- const view = viewAval !== undefined ? viewAval.getValue(token) : undefined;
457
+ const view = this.vertexViews.get(vb.name);
484
458
  const buf = res.getValue(token);
485
- if (view !== undefined && view.offset > 0) {
486
- // size omitted → WebGPU uses (buffer.size offset). The
487
- // BufferView's count·stride span doesn't include the offset,
488
- // so passing it here would request bytes past the end of the
489
- // buffer for any non-zero offset; let WebGPU default.
490
- pass.setVertexBuffer(vb.slot, buf, view.offset);
459
+ const offset = view !== undefined ? BufferView.offsetOf(view) : 0;
460
+ if (offset > 0) {
461
+ // size omitted WebGPU uses (buffer.size offset).
462
+ pass.setVertexBuffer(vb.slot, buf, offset);
491
463
  } else {
492
464
  pass.setVertexBuffer(vb.slot, buf);
493
465
  }
@@ -495,8 +467,8 @@ export class PreparedRenderObject {
495
467
 
496
468
  if (this.indexBuffer !== undefined && this.indices !== undefined && this.indexFormat !== undefined) {
497
469
  const buf = this.indexBuffer.getValue(token);
498
- const view = this.indices.getValue(token);
499
- pass.setIndexBuffer(buf, this.indexFormat, view.offset, view.count * (this.indexFormat === "uint16" ? 2 : 4));
470
+ // size omitted → WebGPU uses (buffer.size − offset).
471
+ pass.setIndexBuffer(buf, this.indexFormat, BufferView.offsetOf(this.indices));
500
472
  }
501
473
 
502
474
  const dc = this.drawCall.getValue(token);
@@ -541,12 +513,16 @@ export function prepareRenderObject(
541
513
 
542
514
  // Per-attribute resolution + grouping. Multiple attributes can
543
515
  // share one underlying GPUBuffer slot when they all point at the
544
- // same `IBuffer` with the same stride + stepMode (the
545
- // auto-instancing rewrite emits 4 vec4 column attributes per
546
- // M44 instance trafo — without packing we'd hit
547
- // `maxVertexBuffers` (8) for any moderately rich instancing
548
- // scope). Each attribute's `view.offset` becomes its
516
+ // same `aval<IBuffer>` (by reference identity) with the same
517
+ // stride + stepMode (the auto-instancing rewrite emits 4 vec4
518
+ // column attributes per M44 instance trafo — without packing
519
+ // we'd hit `maxVertexBuffers` (8) for any moderately rich
520
+ // instancing scope). Each attribute's `view.offset` becomes its
549
521
  // shader-location offset within the shared layout.
522
+ //
523
+ // BufferView is now structural-eager (elementType/offset/stride),
524
+ // so no `force()` is needed to discover layout — the attribute
525
+ // grouping reads the plain fields directly.
550
526
  interface ResolvedBinding {
551
527
  readonly name: string;
552
528
  readonly shaderLocation: number;
@@ -554,8 +530,7 @@ export function prepareRenderObject(
554
530
  readonly stride: number;
555
531
  readonly offset: number;
556
532
  readonly format: GPUVertexFormat;
557
- readonly viewAval: aval<import("../core/index.js").BufferView>;
558
- readonly buffer: import("../core/index.js").IBuffer;
533
+ readonly view: BufferView;
559
534
  }
560
535
  const resolved: ResolvedBinding[] = [];
561
536
  for (const vb of vertexBindings) {
@@ -567,33 +542,28 @@ export function prepareRenderObject(
567
542
  const isInstance = fromVertex === undefined && fromInstance !== undefined;
568
543
  const stepMode: GPUVertexStepMode = isInstance ? "instance" : "vertex";
569
544
 
570
- const viewAval = (fromVertex ?? fromInstance)!;
571
- const initialView = viewAval.force();
572
- const viewFormat = initialView.format as GPUVertexFormat | undefined;
573
- const format = (viewFormat !== undefined && !isIndexFormat(viewFormat))
574
- ? viewFormat : vb.format;
575
- const stride = initialView.stride === 0
576
- ? 0
577
- : (initialView.stride > 0 ? initialView.stride : vertexFormatStride(format));
545
+ const view = (fromVertex ?? fromInstance)!;
546
+ const format = ElementType.toVertexFormat(view.elementType, view.normalized ?? false);
547
+ const stride = BufferView.strideOf(view);
548
+ const offset = BufferView.offsetOf(view);
578
549
  resolved.push({
579
550
  name: vb.name,
580
551
  shaderLocation: vb.slot,
581
- stepMode, stride, offset: initialView.offset,
582
- format, viewAval, buffer: initialView.buffer,
552
+ stepMode, stride, offset,
553
+ format, view,
583
554
  });
584
555
  }
585
556
 
586
- // Group resolved bindings by (IBuffer, stride, stepMode). Each
587
- // group becomes one GPUVertexBufferLayout with one or more
588
- // attributes; multiple bindings sharing one buffer collapse to a
589
- // single setVertexBuffer call at draw time.
557
+ // Group resolved bindings by (IBuffer-aval identity, stride, stepMode).
558
+ // The aval reference is the natural identity now that BufferView
559
+ // owns its `aval<IBuffer>` directly.
590
560
  type GroupKey = string;
591
561
  interface BindingGroup {
592
562
  readonly key: GroupKey;
593
563
  readonly slot: number; // GPU vertex-buffer slot index
594
564
  readonly stride: number;
595
565
  readonly stepMode: GPUVertexStepMode;
596
- readonly viewAval: aval<import("../core/index.js").BufferView>;
566
+ readonly view: BufferView;
597
567
  readonly bufferRes: AdaptiveResource<GPUBuffer>;
598
568
  readonly members: ResolvedBinding[];
599
569
  }
@@ -607,26 +577,25 @@ export function prepareRenderObject(
607
577
  const vbGroups = new Map<GroupKey, BindingGroup>();
608
578
  let nextSlot = 0;
609
579
  const vertexBuffers = new Map<string, AdaptiveResource<GPUBuffer>>();
610
- const vertexViews = new Map<string, aval<import("../core/index.js").BufferView>>();
580
+ const vertexViews = new Map<string, BufferView>();
611
581
  for (const r of resolved) {
612
- const bufId = idOf(r.buffer as unknown as object);
582
+ const bufId = idOf(r.view.buffer as unknown as object);
613
583
  const key = `${bufId}|${r.stride}|${r.stepMode}`;
614
584
  let group = vbGroups.get(key);
615
585
  if (!group) {
616
- const bufAval = r.viewAval.map(view => view.buffer);
617
- const bufferRes = prepareAdaptiveBuffer(device, bufAval, {
586
+ const bufferRes = prepareAdaptiveBuffer(device, r.view.buffer, {
618
587
  usage: BufferUsage.VERTEX,
619
588
  ...(opts.label !== undefined ? { label: `${opts.label}.${r.name}` } : {}),
620
589
  });
621
590
  group = {
622
591
  key, slot: nextSlot++, stride: r.stride, stepMode: r.stepMode,
623
- viewAval: r.viewAval, bufferRes, members: [],
592
+ view: r.view, bufferRes, members: [],
624
593
  };
625
594
  vbGroups.set(key, group);
626
595
  }
627
596
  group.members.push(r);
628
597
  vertexBuffers.set(r.name, group.bufferRes);
629
- vertexViews.set(r.name, r.viewAval);
598
+ vertexViews.set(r.name, r.view);
630
599
  }
631
600
  const vertexLayouts: GPUVertexBufferLayout[] = [];
632
601
  for (const g of vbGroups.values()) {
@@ -663,21 +632,16 @@ export function prepareRenderObject(
663
632
  }
664
633
  vertexBindings = drawTimeBindings;
665
634
 
666
- // Index buffer — `indexFormat` from BufferView wins; defaults to uint32.
635
+ // Index buffer — `elementType` (u16 / u32) determines `GPUIndexFormat`.
636
+ // Structural fields are eager on BufferView, so no force needed.
667
637
  let indexBuffer: AdaptiveResource<GPUBuffer> | undefined;
668
638
  let indexFormat: GPUIndexFormat | undefined;
669
639
  if (obj.indices !== undefined) {
670
- const bufAval = obj.indices.map(v => v.buffer);
671
- indexBuffer = prepareAdaptiveBuffer(device, bufAval, {
640
+ indexBuffer = prepareAdaptiveBuffer(device, obj.indices.buffer, {
672
641
  usage: BufferUsage.INDEX,
673
642
  ...(opts.label !== undefined ? { label: `${opts.label}.indices` } : {}),
674
643
  });
675
- // Construction-time read to discover the index format. The
676
- // BufferView aval is expected to settle on a stable format —
677
- // changing index format would require a fresh PreparedRO.
678
- const initial = obj.indices.force();
679
- indexFormat = initial.indexFormat
680
- ?? (initial.format === "uint16" ? "uint16" : "uint32");
644
+ indexFormat = ElementType.toIndexFormat(obj.indices.elementType);
681
645
  }
682
646
 
683
647
  const slotOf = (e: { group: number; slot: number }) => e.slot;
@@ -767,8 +731,8 @@ export function prepareRenderObject(
767
731
  );
768
732
  }
769
733
 
770
- function emptyAttrMap(): HashMap<string, aval<BufferView>> {
771
- return HashMap.empty<string, aval<BufferView>>();
734
+ function emptyAttrMap(): HashMap<string, BufferView> {
735
+ return HashMap.empty<string, BufferView>();
772
736
  }
773
737
 
774
738
  /**