WgpuCompact.ts

src/wgpu/WgpuCompact.ts · raw · all examples · site home

Exports: WgpuCompact

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import type { WebGPURenderer } from "three/webgpu";
import { TSL, type TslNode } from "../gpu/tsl";
import { WgpuBuffer } from "./WgpuBuffer";
import { WgpuCompute, WgpuComputeBatch, requireWebGPU } from "./WgpuCompute";
import { SCAN_WORKGROUP, scanTier, threadRank, workgroupExclusiveScan, type ScanTier } from "./workgroupScan";

const { If, Loop, select, uint, uniform, workgroupArray } = TSL;

const KEYS_PER_THREAD = 8;
const BLOCK = SCAN_WORKGROUP * KEYS_PER_THREAD;

// Stream compaction (WebGPU backend only): the keys key(i) of the first n
// indices that are not `invalid`, with their indices as values, written to
// the front of `keys` and `values` in input order; their number to
// count[0]. Deterministic (a block prefix scan, no atomics), so a stable
// sort afterwards matches sorting the uncompacted keys. `key` is any TSL
// function of the index: a buffer element, a texture read. Three
// dispatches in one batch:
//   blockCounts (one workgroup per block): valid elements per block
//   scan        (one workgroup): exclusive scan of the block counts, total
//   scatter     (one workgroup per block): per-thread scan, then the writes
export class WgpuCompact {
  readonly renderer: WebGPURenderer;
  readonly tier: ScanTier;
  readonly keys: WgpuBuffer<"uint32">;
  readonly values: WgpuBuffer<"uint32">;
  // count[0]: pairs written by the latest compact()
  readonly count: WgpuBuffer<"uint32">;
  // The key of the indices to drop
  readonly invalid = uniform(0xffffffff, "uint");

  private blockOffsets: WgpuBuffer<"uint32">;
  private n = uniform(0, "uint");
  private numBlocks = uniform(0, "uint");
  private kernels: WgpuCompute[];

  constructor({ renderer, key, tier = "auto" }: { renderer: WebGPURenderer; key: (index: TslNode) => TslNode; tier?: ScanTier | "auto" }) {
    requireWebGPU(renderer, "WgpuCompact");
    this.renderer = renderer;
    this.tier = scanTier(renderer, tier);
    const buffer = (label: string) => new WgpuBuffer({ renderer, format: "uint32", count: 1, label });
    this.keys = buffer("radix_compactKeys");
    this.values = buffer("radix_compactValues");
    this.count = buffer("radix_compactCount");
    this.blockOffsets = buffer("radix_compactBlockOffsets");

    const { n, numBlocks, blockOffsets, count, keys, values, invalid } = this;
    const tierOf = this.tier;
    const valid = (i: TslNode) => i.lessThan(n).and(key(i).notEqual(invalid));
    // Valid elements of this thread's run of KEYS_PER_THREAD
    const threadCount = (base: TslNode) => {
      const sum = uint(0).toVar();
      for (let j = 0; j < KEYS_PER_THREAD; j += 1) {
        sum.addAssign(select(valid(base.add(uint(j))), uint(1), uint(0)));
      }
      return sum;
    };
    const threadBase = (workgroupId: TslNode, rank: TslNode) =>
      workgroupId.x.mul(uint(BLOCK)).add(rank.mul(uint(KEYS_PER_THREAD))).toVar();

    const blockCounts = new WgpuCompute({
      label: "radix_compactBlockCounts",
      workgroupSize: [SCAN_WORKGROUP],
      body: ({ localId, workgroupId }) => {
        const rank = threadRank(tierOf, localId);
        const scratch = workgroupArray("uint", SCAN_WORKGROUP);
        const { total } = workgroupExclusiveScan(tierOf, scratch, rank, threadCount(threadBase(workgroupId, rank)).toVar());
        If(rank.equal(uint(0)), () => {
          blockOffsets.element(workgroupId.x).assign(total);
        });
      },
    });

    const scan = new WgpuCompute({
      label: "radix_compactScan",
      workgroupSize: [SCAN_WORKGROUP],
      body: ({ localId }) => {
        const rank = threadRank(tierOf, localId);
        const scratch = workgroupArray("uint", SCAN_WORKGROUP);
        const carry = uint(0).toVar();
        Loop({ start: uint(0), end: numBlocks, type: "uint", condition: "<", update: SCAN_WORKGROUP }, ({ i }: { i: TslNode }) => {
          const index = i.add(rank).toVar();
          const inRange = index.lessThan(numBlocks).toVar();
          const value = select(inRange, blockOffsets.element(index), uint(0)).toVar();
          const { exclusive, total } = workgroupExclusiveScan(tierOf, scratch, rank, value);
          If(inRange, () => {
            blockOffsets.element(index).assign(carry.add(exclusive));
          });
          carry.addAssign(total);
        });
        If(rank.equal(uint(0)), () => {
          count.element(uint(0)).assign(carry);
        });
      },
    });

    const scatter = new WgpuCompute({
      label: "radix_compactScatter",
      workgroupSize: [SCAN_WORKGROUP],
      body: ({ localId, workgroupId }) => {
        const rank = threadRank(tierOf, localId);
        const scratch = workgroupArray("uint", SCAN_WORKGROUP);
        const base = threadBase(workgroupId, rank);
        const { exclusive } = workgroupExclusiveScan(tierOf, scratch, rank, threadCount(base).toVar());
        const at = blockOffsets.element(workgroupId.x).add(exclusive).toVar();
        for (let j = 0; j < KEYS_PER_THREAD; j += 1) {
          const i = base.add(uint(j)).toVar();
          const k = key(i).toVar();
          If(i.lessThan(n).and(k.notEqual(invalid)), () => {
            keys.element(at).assign(k);
            values.element(at).assign(i);
            at.addAssign(uint(1));
          });
        }
      },
    });

    this.kernels = [blockCounts, scan, scatter];
  }

  // Compact the first n indices (queued synchronously)
  compact(n: number) {
    const blocks = Math.max(1, Math.ceil(n / BLOCK));
    this.keys.ensureCapacity(n);
    this.values.ensureCapacity(n);
    this.blockOffsets.ensureCapacity(blocks);
    this.n.value = n;
    this.numBlocks.value = blocks;

    const [blockCounts, scan, scatter] = this.kernels;
    new WgpuComputeBatch(this.renderer).add(blockCounts, [blocks]).add(scan, [1]).add(scatter, [blocks]).submit();
  }

  dispose() {
    this.kernels.forEach((kernel) => kernel.dispose());
    [this.keys, this.values, this.count, this.blockOffsets].forEach((buffer) => buffer.dispose());
  }
}