@driftengine/texture 4.0.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.
Files changed (99) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +29 -0
  3. package/README.md +106 -0
  4. package/dist/decodeCpu.d.ts +59 -0
  5. package/dist/decodeCpu.js +234 -0
  6. package/dist/decodeGraph.d.ts +105 -0
  7. package/dist/decodeGraph.js +180 -0
  8. package/dist/half.d.ts +24 -0
  9. package/dist/half.js +86 -0
  10. package/dist/index.d.ts +66 -0
  11. package/dist/index.js +55 -0
  12. package/dist/inference.d.ts +53 -0
  13. package/dist/inference.js +243 -0
  14. package/dist/materialArray.d.ts +38 -0
  15. package/dist/materialArray.js +40 -0
  16. package/dist/mipNdf.d.ts +29 -0
  17. package/dist/mipNdf.js +53 -0
  18. package/dist/overlay/journal.d.ts +78 -0
  19. package/dist/overlay/journal.js +171 -0
  20. package/dist/overlay/sparse.d.ts +68 -0
  21. package/dist/overlay/sparse.js +212 -0
  22. package/dist/progressive.d.ts +30 -0
  23. package/dist/progressive.js +56 -0
  24. package/dist/residency/pageCache.d.ts +103 -0
  25. package/dist/residency/pageCache.js +184 -0
  26. package/dist/residency/predict.d.ts +55 -0
  27. package/dist/residency/predict.js +51 -0
  28. package/dist/residency/predictor.d.ts +16 -0
  29. package/dist/residency/predictor.js +44 -0
  30. package/dist/residency/queue.d.ts +26 -0
  31. package/dist/residency/queue.js +52 -0
  32. package/dist/residency/stream.d.ts +66 -0
  33. package/dist/residency/stream.js +142 -0
  34. package/dist/residency/table.d.ts +36 -0
  35. package/dist/residency/table.js +72 -0
  36. package/dist/residency/viewTiles.d.ts +108 -0
  37. package/dist/residency/viewTiles.js +419 -0
  38. package/dist/semantics.d.ts +52 -0
  39. package/dist/semantics.js +76 -0
  40. package/dist/tensor/architecture.d.ts +53 -0
  41. package/dist/tensor/architecture.js +96 -0
  42. package/dist/tensor/attention.d.ts +5 -0
  43. package/dist/tensor/attention.js +62 -0
  44. package/dist/tensor/denseOperators.d.ts +2 -0
  45. package/dist/tensor/denseOperators.js +136 -0
  46. package/dist/tensor/graph.d.ts +83 -0
  47. package/dist/tensor/graph.js +175 -0
  48. package/dist/tensor/linear.d.ts +49 -0
  49. package/dist/tensor/linear.js +136 -0
  50. package/dist/tensor/operatorKit.d.ts +27 -0
  51. package/dist/tensor/operatorKit.js +45 -0
  52. package/dist/tensor/operators.d.ts +3 -0
  53. package/dist/tensor/operators.js +24 -0
  54. package/dist/tensor/resize.d.ts +6 -0
  55. package/dist/tensor/resize.js +107 -0
  56. package/dist/tensor/reuse.d.ts +33 -0
  57. package/dist/tensor/reuse.js +59 -0
  58. package/dist/tensor/shapeOperators.d.ts +3 -0
  59. package/dist/tensor/shapeOperators.js +173 -0
  60. package/dist/tensor/spatial.d.ts +34 -0
  61. package/dist/tensor/spatial.js +131 -0
  62. package/dist/tensor/spatialOperators.d.ts +2 -0
  63. package/dist/tensor/spatialOperators.js +138 -0
  64. package/dist/tileHash.d.ts +29 -0
  65. package/dist/tileHash.js +50 -0
  66. package/dist/timeNodes.d.ts +26 -0
  67. package/dist/timeNodes.js +48 -0
  68. package/package.json +59 -0
  69. package/src/decodeCpu.ts +308 -0
  70. package/src/decodeGraph.ts +214 -0
  71. package/src/half.ts +86 -0
  72. package/src/index.ts +175 -0
  73. package/src/inference.ts +278 -0
  74. package/src/materialArray.ts +67 -0
  75. package/src/mipNdf.ts +63 -0
  76. package/src/overlay/journal.ts +218 -0
  77. package/src/overlay/sparse.ts +275 -0
  78. package/src/progressive.ts +60 -0
  79. package/src/residency/pageCache.ts +233 -0
  80. package/src/residency/predict.ts +74 -0
  81. package/src/residency/predictor.ts +62 -0
  82. package/src/residency/queue.ts +78 -0
  83. package/src/residency/stream.ts +194 -0
  84. package/src/residency/table.ts +89 -0
  85. package/src/residency/viewTiles.ts +553 -0
  86. package/src/semantics.ts +114 -0
  87. package/src/tensor/architecture.ts +140 -0
  88. package/src/tensor/attention.ts +75 -0
  89. package/src/tensor/denseOperators.ts +153 -0
  90. package/src/tensor/graph.ts +244 -0
  91. package/src/tensor/linear.ts +153 -0
  92. package/src/tensor/operatorKit.ts +76 -0
  93. package/src/tensor/operators.ts +28 -0
  94. package/src/tensor/resize.ts +140 -0
  95. package/src/tensor/shapeOperators.ts +173 -0
  96. package/src/tensor/spatial.ts +178 -0
  97. package/src/tensor/spatialOperators.ts +182 -0
  98. package/src/tileHash.ts +60 -0
  99. package/src/timeNodes.ts +60 -0
@@ -0,0 +1,173 @@
1
+ /** The shape operators' rows: they move values and compute nothing. */
2
+ import { type Operator, list, num, product, strides, type Shape } from './operatorKit.ts';
3
+
4
+ export const SHAPE_OPERATORS: readonly (readonly [string, Operator])[] = [
5
+ [
6
+ 'permute',
7
+ {
8
+ arity: [1, 1],
9
+ shape: ([x], attributes) => {
10
+ const order = list(attributes, 'order');
11
+ const shape = x as Shape;
12
+ const valid =
13
+ order.length === shape.length &&
14
+ [...order].sort((p, q) => p - q).every((d, i) => d === i);
15
+ return valid
16
+ ? order.map((d) => shape[d] as number)
17
+ : `order [${order.join(', ')}] is not a permutation of ${shape.length} axes`;
18
+ },
19
+ evaluate: ([x], [xs], attributes, out) => {
20
+ const shape = xs as Shape;
21
+ const order = list(attributes, 'order');
22
+ const from = strides(shape);
23
+ const to = order.map((d) => shape[d] as number);
24
+ const index = new Array<number>(shape.length).fill(0);
25
+ for (let at = 0; at < out.length; at += 1) {
26
+ let source = 0;
27
+ for (let d = 0; d < order.length; d += 1)
28
+ source += (index[d] as number) * (from[order[d] as number] as number);
29
+ out[at] = (x as Float32Array)[source] as number;
30
+ for (let d = order.length - 1; d >= 0; d -= 1) {
31
+ index[d] = (index[d] as number) + 1;
32
+ if ((index[d] as number) < (to[d] as number)) break;
33
+ index[d] = 0;
34
+ }
35
+ }
36
+ },
37
+ },
38
+ ],
39
+ [
40
+ 'reshape',
41
+ {
42
+ arity: [1, 1],
43
+ shape: ([x], attributes) => {
44
+ const shape = list(attributes, 'shape');
45
+ return product(shape) === product(x as Shape)
46
+ ? [...shape]
47
+ : `[${shape.join(', ')}] does not hold [${(x as Shape).join(', ')}]`;
48
+ },
49
+ evaluate: ([x], _shapes, _attributes, out) => out.set(x as Float32Array),
50
+ },
51
+ ],
52
+ [
53
+ 'concat',
54
+ {
55
+ arity: [2, 16],
56
+ shape: (inputs, attributes) => {
57
+ const axis = num(attributes, 'axis');
58
+ const first = inputs[0] as Shape;
59
+ let along = 0;
60
+ for (const shape of inputs) {
61
+ if (shape.length !== first.length || shape.some((d, i) => i !== axis && d !== first[i])) {
62
+ return `inputs differ off axis ${axis}`;
63
+ }
64
+ along += shape[axis] as number;
65
+ }
66
+ return first.map((d, i) => (i === axis ? along : d));
67
+ },
68
+ evaluate: (inputs, shapes, attributes, out) => {
69
+ const axis = num(attributes, 'axis');
70
+ const first = shapes[0] as Shape;
71
+ const outer = product(first, 0, axis);
72
+ let at = 0;
73
+ for (let o = 0; o < outer; o += 1) {
74
+ for (let i = 0; i < inputs.length; i += 1) {
75
+ const block = product(shapes[i] as Shape, axis);
76
+ out.set((inputs[i] as Float32Array).subarray(o * block, (o + 1) * block), at);
77
+ at += block;
78
+ }
79
+ }
80
+ },
81
+ },
82
+ ],
83
+ [
84
+ 'slice',
85
+ {
86
+ arity: [1, 1],
87
+ shape: ([x], attributes) => {
88
+ const axis = num(attributes, 'axis');
89
+ const start = num(attributes, 'start');
90
+ const end = num(attributes, 'end');
91
+ const shape = x as Shape;
92
+ return start >= 0 && end <= (shape[axis] as number) && start < end
93
+ ? shape.map((d, i) => (i === axis ? end - start : d))
94
+ : `[${start}, ${end}) is outside axis ${axis} of length ${shape[axis]}`;
95
+ },
96
+ evaluate: ([x], [xs], attributes, out) => {
97
+ const axis = num(attributes, 'axis');
98
+ const start = num(attributes, 'start');
99
+ const end = num(attributes, 'end');
100
+ const shape = xs as Shape;
101
+ const inner = product(shape, axis + 1);
102
+ const outer = product(shape, 0, axis);
103
+ const length = shape[axis] as number;
104
+ const take = (end - start) * inner;
105
+ for (let o = 0; o < outer; o += 1) {
106
+ const from = (o * length + start) * inner;
107
+ out.set((x as Float32Array).subarray(from, from + take), o * take);
108
+ }
109
+ },
110
+ },
111
+ ],
112
+ [
113
+ /*
114
+ * Zeros after the end of each axis, `after[axis]` of them, as a window partition pads a grid it
115
+ * does not divide. Every value keeps its index along every axis.
116
+ */
117
+ 'pad',
118
+ {
119
+ arity: [1, 1],
120
+ shape: ([x], attributes) => {
121
+ const after = list(attributes, 'after');
122
+ const shape = x as Shape;
123
+ if (after.length !== shape.length) {
124
+ return `the pad names ${after.length} axes and the value has ${shape.length} axes`;
125
+ }
126
+ if (after.some((d) => d < 0)) return `a pad of [${after.join(', ')}] is negative`;
127
+ return shape.map((d, i) => d + (after[i] as number));
128
+ },
129
+ evaluate: ([x], [xs], attributes, out) => {
130
+ const shape = xs as Shape;
131
+ const after = list(attributes, 'after');
132
+ const outShape = shape.map((d, i) => d + (after[i] as number));
133
+ const from = strides(shape);
134
+ const to = strides(outShape);
135
+ out.fill(0);
136
+ const values = x as Float32Array;
137
+ for (let i = 0; i < values.length; i += 1) {
138
+ let at = 0;
139
+ for (let axis = 0; axis < shape.length; axis += 1) {
140
+ at +=
141
+ (Math.floor(i / (from[axis] as number)) % (shape[axis] as number)) *
142
+ (to[axis] as number);
143
+ }
144
+ out[at] = values[i] as number;
145
+ }
146
+ },
147
+ },
148
+ ],
149
+ [
150
+ /*
151
+ * Rows of a table by index, as a token's embedding is looked up: `indices` holds whole numbers
152
+ * as values, and row i of the output is row `indices[i]` of the table. An index that is not one
153
+ * of the table's rows is refused here; a device cannot refuse, and clamps it to the last row.
154
+ */
155
+ 'gather',
156
+ {
157
+ ranks: [2, 1],
158
+ arity: [2, 2],
159
+ shape: ([table, indices]) => [(indices as Shape)[0] as number, (table as Shape)[1] as number],
160
+ evaluate: ([table, indices], [ts], _attributes, out) => {
161
+ const [rows, width] = ts as Shape as [number, number];
162
+ const at = indices as Float32Array;
163
+ for (let i = 0; i < at.length; i += 1) {
164
+ const row = at[i] as number;
165
+ if (!Number.isInteger(row) || row < 0 || row >= rows) {
166
+ throw new RangeError(`gather: index ${row} at ${i} is not a row of a table of ${rows}`);
167
+ }
168
+ out.set((table as Float32Array).subarray(row * width, (row + 1) * width), i * width);
169
+ }
170
+ },
171
+ },
172
+ ],
173
+ ];
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The spatial operators: convolution, its transpose, a patch embedding and max pooling. Resizing is
3
+ * `resize.ts`'s.
4
+ *
5
+ * **Channel-major images, one at a time** — `[channels][height][width]`, the layout the upstream
6
+ * frameworks call NCHW with a batch of one — and **weights in the upstream layout**: a convolution's
7
+ * `[out][in][kh][kw]`, a transposed convolution's `[in][out][kh][kw]`. A converted checkpoint is
8
+ * then read as it was stored, and a rearrangement is never a place for a transposed weight to hide.
9
+ */
10
+
11
+ /**
12
+ * `out[cout][oh][ow]` from `input[cin][h][w]`, with `oh = ⌊(h + 2·padding − kh)/stride⌋ + 1` and
13
+ * likewise `ow`. `bias` may be null.
14
+ *
15
+ * **Grouped, the channels split into `groups` runs** and output `o` reads only the run `o` falls in:
16
+ * the weight is `[cout][cin / groups][kh][kw]`, as the upstream stores it, and a depthwise
17
+ * convolution is `groups = cin = cout`.
18
+ */
19
+ export function conv2d(
20
+ out: Float32Array,
21
+ input: Float32Array,
22
+ cin: number,
23
+ h: number,
24
+ w: number,
25
+ weight: Float32Array,
26
+ bias: Float32Array | null,
27
+ cout: number,
28
+ kh: number,
29
+ kw: number,
30
+ stride: number,
31
+ padding: number,
32
+ groups = 1,
33
+ ): void {
34
+ const oh = Math.floor((h + 2 * padding - kh) / stride) + 1;
35
+ const ow = Math.floor((w + 2 * padding - kw) / stride) + 1;
36
+ const inPerGroup = cin / groups;
37
+ const outPerGroup = cout / groups;
38
+ for (let o = 0; o < cout; o += 1) {
39
+ const first = Math.floor(o / outPerGroup) * inPerGroup;
40
+ for (let y = 0; y < oh; y += 1) {
41
+ for (let x = 0; x < ow; x += 1) {
42
+ let sum = bias === null ? 0 : (bias[o] as number);
43
+ for (let c = 0; c < inPerGroup; c += 1) {
44
+ for (let ky = 0; ky < kh; ky += 1) {
45
+ const sy = y * stride - padding + ky;
46
+ if (sy < 0 || sy >= h) continue;
47
+ for (let kx = 0; kx < kw; kx += 1) {
48
+ const sx = x * stride - padding + kx;
49
+ if (sx < 0 || sx >= w) continue;
50
+ sum +=
51
+ (input[((first + c) * h + sy) * w + sx] as number) *
52
+ (weight[((o * inPerGroup + c) * kh + ky) * kw + kx] as number);
53
+ }
54
+ }
55
+ }
56
+ out[(o * oh + y) * ow + x] = sum;
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * `out[cout][oh][ow]` from `input[cin][h][w]`, with `oh = (h − 1)·stride − 2·padding + kh`: each input
64
+ * pixel spread over the kernel at its strided place, which is how a decoder head upsamples.
65
+ */
66
+ export function convTranspose2d(
67
+ out: Float32Array,
68
+ input: Float32Array,
69
+ cin: number,
70
+ h: number,
71
+ w: number,
72
+ weight: Float32Array,
73
+ bias: Float32Array | null,
74
+ cout: number,
75
+ kh: number,
76
+ kw: number,
77
+ stride: number,
78
+ padding: number,
79
+ ): void {
80
+ const oh = (h - 1) * stride - 2 * padding + kh;
81
+ const ow = (w - 1) * stride - 2 * padding + kw;
82
+ for (let o = 0; o < cout; o += 1) {
83
+ const value = bias === null ? 0 : (bias[o] as number);
84
+ for (let i = 0; i < oh * ow; i += 1) out[o * oh * ow + i] = value;
85
+ }
86
+ for (let c = 0; c < cin; c += 1) {
87
+ for (let y = 0; y < h; y += 1) {
88
+ for (let x = 0; x < w; x += 1) {
89
+ const pixel = input[(c * h + y) * w + x] as number;
90
+ for (let o = 0; o < cout; o += 1) {
91
+ for (let ky = 0; ky < kh; ky += 1) {
92
+ const ty = y * stride - padding + ky;
93
+ if (ty < 0 || ty >= oh) continue;
94
+ for (let kx = 0; kx < kw; kx += 1) {
95
+ const tx = x * stride - padding + kx;
96
+ if (tx < 0 || tx >= ow) continue;
97
+ const at = (o * oh + ty) * ow + tx;
98
+ out[at] =
99
+ (out[at] as number) +
100
+ pixel * (weight[((c * cout + o) * kh + ky) * kw + kx] as number);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * A patch embedding: a convolution with stride and kernel both `patch`, written token-major —
111
+ * `out[tokens][dim]`, tokens row by row — which is the order a transformer reads them in.
112
+ */
113
+ export function patchEmbed(
114
+ out: Float32Array,
115
+ image: Float32Array,
116
+ cin: number,
117
+ h: number,
118
+ w: number,
119
+ weight: Float32Array,
120
+ bias: Float32Array | null,
121
+ dim: number,
122
+ patch: number,
123
+ ): void {
124
+ const rows = Math.floor(h / patch);
125
+ const cols = Math.floor(w / patch);
126
+ for (let ty = 0; ty < rows; ty += 1) {
127
+ for (let tx = 0; tx < cols; tx += 1) {
128
+ const token = ty * cols + tx;
129
+ for (let o = 0; o < dim; o += 1) {
130
+ let sum = bias === null ? 0 : (bias[o] as number);
131
+ for (let c = 0; c < cin; c += 1) {
132
+ for (let ky = 0; ky < patch; ky += 1) {
133
+ for (let kx = 0; kx < patch; kx += 1) {
134
+ sum +=
135
+ (image[(c * h + ty * patch + ky) * w + tx * patch + kx] as number) *
136
+ (weight[((o * cin + c) * patch + ky) * patch + kx] as number);
137
+ }
138
+ }
139
+ }
140
+ out[token * dim + o] = sum;
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * `out[channels][oh][ow]`, each the largest of its `kernel`-square window at `stride`, with
148
+ * `oh = ⌊(h − kernel)/stride⌋ + 1`: no padding, and a ragged last row or column dropped, as
149
+ * PyTorch's `max_pool2d` does without `ceil_mode`.
150
+ */
151
+ export function maxPool2d(
152
+ out: Float32Array,
153
+ input: Float32Array,
154
+ channels: number,
155
+ h: number,
156
+ w: number,
157
+ kernel: number,
158
+ stride: number,
159
+ ): void {
160
+ const oh = Math.floor((h - kernel) / stride) + 1;
161
+ const ow = Math.floor((w - kernel) / stride) + 1;
162
+ for (let c = 0; c < channels; c += 1) {
163
+ for (let y = 0; y < oh; y += 1) {
164
+ for (let x = 0; x < ow; x += 1) {
165
+ let largest = Number.NEGATIVE_INFINITY;
166
+ for (let ky = 0; ky < kernel; ky += 1) {
167
+ for (let kx = 0; kx < kernel; kx += 1) {
168
+ largest = Math.max(
169
+ largest,
170
+ input[(c * h + y * stride + ky) * w + x * stride + kx] as number,
171
+ );
172
+ }
173
+ }
174
+ out[(c * oh + y) * ow + x] = largest;
175
+ }
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,182 @@
1
+ /** The spatial operators' rows: convolution, its transpose, patch embedding, resizing, pooling. */
2
+ import { resize } from './resize.ts';
3
+ import { conv2d, convTranspose2d, maxPool2d, patchEmbed } from './spatial.ts';
4
+ import { type Attributes, type Operator, num, type Shape } from './operatorKit.ts';
5
+
6
+ /*
7
+ * A resize's `stepHeight` and `stepWidth`: the source pixels one destination pixel covers, where
8
+ * that is not the ratio of the sizes — PyTorch's interpolate handed a scale factor samples by the
9
+ * factor's inverse, and DINOv2's positional embeddings are resized that way. Both or neither.
10
+ */
11
+ function stepOf(attributes: Attributes): readonly [number, number] | undefined {
12
+ const height = attributes['stepHeight'];
13
+ const width = attributes['stepWidth'];
14
+ return typeof height === 'number' && typeof width === 'number' ? [height, width] : undefined;
15
+ }
16
+
17
+ /* A resize's mode, bilinear when none is named, and undefined for one the runtime lacks. */
18
+ function modeOf(attributes: Attributes): 'bilinear' | 'bicubic' | 'nearest' | undefined {
19
+ const mode = attributes['mode'] ?? 'bilinear';
20
+ return mode === 'bilinear' || mode === 'bicubic' || mode === 'nearest' ? mode : undefined;
21
+ }
22
+
23
+ export const SPATIAL_OPERATORS: readonly (readonly [string, Operator])[] = [
24
+ [
25
+ 'conv2d',
26
+ {
27
+ ranks: [3, 4, 1],
28
+ arity: [2, 3],
29
+ shape: ([x, w], attributes) => {
30
+ const [c, h, width] = x as Shape as [number, number, number];
31
+ const [o, ci, kh, kw] = w as Shape as [number, number, number, number];
32
+ const groups = num(attributes, 'groups', 1);
33
+ if (c % groups !== 0) return `x's ${c} channels do not split into ${groups} groups`;
34
+ if (o % groups !== 0) return `the weight's ${o} outputs do not split into ${groups} groups`;
35
+ if (c !== ci * groups) return `x has ${c} channels and the weight takes ${ci * groups}`;
36
+ const stride = num(attributes, 'stride', 1);
37
+ const padding = num(attributes, 'padding', 0);
38
+ return [
39
+ o,
40
+ Math.floor((h + 2 * padding - kh) / stride) + 1,
41
+ Math.floor((width + 2 * padding - kw) / stride) + 1,
42
+ ];
43
+ },
44
+ evaluate: ([x, w, b], [xs, ws], attributes, out) => {
45
+ const [c, h, width] = xs as Shape as [number, number, number];
46
+ const [o, , kh, kw] = ws as Shape as [number, number, number, number];
47
+ conv2d(
48
+ out,
49
+ x as Float32Array,
50
+ c,
51
+ h,
52
+ width,
53
+ w as Float32Array,
54
+ b ?? null,
55
+ o,
56
+ kh,
57
+ kw,
58
+ num(attributes, 'stride', 1),
59
+ num(attributes, 'padding', 0),
60
+ num(attributes, 'groups', 1),
61
+ );
62
+ },
63
+ },
64
+ ],
65
+ [
66
+ 'convTranspose2d',
67
+ {
68
+ ranks: [3, 4, 1],
69
+ arity: [2, 3],
70
+ shape: ([x, w], attributes) => {
71
+ const [c, h, width] = x as Shape as [number, number, number];
72
+ const [ci, o, kh, kw] = w as Shape as [number, number, number, number];
73
+ if (c !== ci) return `x has ${c} channels and the weight takes ${ci}`;
74
+ const stride = num(attributes, 'stride', 1);
75
+ const padding = num(attributes, 'padding', 0);
76
+ return [o, (h - 1) * stride - 2 * padding + kh, (width - 1) * stride - 2 * padding + kw];
77
+ },
78
+ evaluate: ([x, w, b], [xs, ws], attributes, out) => {
79
+ const [c, h, width] = xs as Shape as [number, number, number];
80
+ const [, o, kh, kw] = ws as Shape as [number, number, number, number];
81
+ convTranspose2d(
82
+ out,
83
+ x as Float32Array,
84
+ c,
85
+ h,
86
+ width,
87
+ w as Float32Array,
88
+ b ?? null,
89
+ o,
90
+ kh,
91
+ kw,
92
+ num(attributes, 'stride', 1),
93
+ num(attributes, 'padding', 0),
94
+ );
95
+ },
96
+ },
97
+ ],
98
+ [
99
+ 'patchEmbed',
100
+ {
101
+ ranks: [3, 4, 1],
102
+ arity: [2, 3],
103
+ shape: ([x, w], attributes) => {
104
+ const [c, h, width] = x as Shape as [number, number, number];
105
+ const [dim, ci] = w as Shape as [number, number];
106
+ if (c !== ci) return `x has ${c} channels and the weight takes ${ci}`;
107
+ const patch = num(attributes, 'patch');
108
+ return [Math.floor(h / patch) * Math.floor(width / patch), dim];
109
+ },
110
+ evaluate: ([x, w, b], [xs, ws], attributes, out) => {
111
+ const [c, h, width] = xs as Shape as [number, number, number];
112
+ patchEmbed(
113
+ out,
114
+ x as Float32Array,
115
+ c,
116
+ h,
117
+ width,
118
+ w as Float32Array,
119
+ b ?? null,
120
+ (ws as Shape)[0] as number,
121
+ num(attributes, 'patch'),
122
+ );
123
+ },
124
+ },
125
+ ],
126
+ [
127
+ 'resize',
128
+ {
129
+ ranks: [3],
130
+ arity: [1, 1],
131
+ shape: ([x], attributes) => {
132
+ const stepped = ['stepHeight', 'stepWidth'].filter((name) => name in attributes).length;
133
+ if (stepped === 1) return 'a step is given for one axis and not the other';
134
+ if (stepped === 2 && attributes['alignCorners'] === true) {
135
+ return 'a step has no meaning with aligned corners, which fix the corners instead';
136
+ }
137
+ const mode = modeOf(attributes);
138
+ if (mode === undefined)
139
+ return `the runtime has no resize mode "${String(attributes['mode'])}"`;
140
+ if (mode === 'nearest' && attributes['alignCorners'] === true) {
141
+ return 'nearest has no aligned corners, as PyTorch has none';
142
+ }
143
+ return [(x as Shape)[0] as number, num(attributes, 'height'), num(attributes, 'width')];
144
+ },
145
+ evaluate: ([x], [xs], attributes, out) => {
146
+ const [c, h, width] = xs as Shape as [number, number, number];
147
+ const mode = modeOf(attributes) ?? 'bilinear';
148
+ resize(
149
+ out,
150
+ x as Float32Array,
151
+ c,
152
+ h,
153
+ width,
154
+ num(attributes, 'height'),
155
+ num(attributes, 'width'),
156
+ mode,
157
+ attributes['alignCorners'] === true,
158
+ stepOf(attributes),
159
+ );
160
+ },
161
+ },
162
+ ],
163
+ [
164
+ 'maxPool2d',
165
+ {
166
+ ranks: [3],
167
+ arity: [1, 1],
168
+ shape: ([x], attributes) => {
169
+ const [c, h, w] = x as Shape as [number, number, number];
170
+ const kernel = num(attributes, 'kernel');
171
+ const stride = num(attributes, 'stride', kernel);
172
+ if (kernel > h || kernel > w) return `a window of ${kernel} does not fit ${h} × ${w}`;
173
+ return [c, Math.floor((h - kernel) / stride) + 1, Math.floor((w - kernel) / stride) + 1];
174
+ },
175
+ evaluate: ([x], [xs], attributes, out) => {
176
+ const [c, h, w] = xs as Shape as [number, number, number];
177
+ const kernel = num(attributes, 'kernel');
178
+ maxPool2d(out, x as Float32Array, c, h, w, kernel, num(attributes, 'stride', kernel));
179
+ },
180
+ },
181
+ ],
182
+ ];
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Content addressing for latent tiles: the same surface stored once however many assets use it.
3
+ *
4
+ * **One mechanism doing three jobs**, which is why it earns its own module rather than being a
5
+ * detail of the encoder. The hash is the deduplication key, the streaming unit and the cache key —
6
+ * so a tile that arrives for one material is already resident for the other thirty-nine that share
7
+ * it.
8
+ *
9
+ * **No seed, no platform dependency, and nothing that varies per process.** A hash that changes
10
+ * between runs makes the baker non-reproducible, which breaks every content comparison downstream
11
+ * and does it silently, because the output is still valid — just different.
12
+ */
13
+
14
+ /** FNV-1a over 64 bits, carried as two 32-bit halves because JavaScript has no 64-bit integer. */
15
+ export function hashTile(bytes: Uint8Array, offset = 0, length = bytes.length - offset): string {
16
+ let lo = 0x84222325;
17
+ let hi = 0xcbf29ce4;
18
+ for (let i = 0; i < length; i += 1) {
19
+ lo ^= bytes[offset + i] as number;
20
+ /* 64-bit multiply by the FNV prime 0x100000001b3, split across the two halves. */
21
+ const loLow = lo & 0xffff;
22
+ const loHigh = lo >>> 16;
23
+ const l0 = loLow * 0x01b3;
24
+ const l1 = loHigh * 0x01b3 + (l0 >>> 16);
25
+ const newLo = ((l1 << 16) | (l0 & 0xffff)) >>> 0;
26
+ hi = (Math.imul(hi, 0x01b3) + Math.imul(lo, 0x0100) + (l1 >>> 16)) >>> 0;
27
+ lo = newLo;
28
+ }
29
+ return (hi >>> 0).toString(16).padStart(8, '0') + (lo >>> 0).toString(16).padStart(8, '0');
30
+ }
31
+
32
+ export interface TileIndex {
33
+ /** Slot per hash. */
34
+ slots: Map<string, number>;
35
+ /** The bytes each slot holds. */
36
+ bytes: Uint8Array[];
37
+ }
38
+
39
+ export function createTileIndex(): TileIndex {
40
+ return { slots: new Map(), bytes: [] };
41
+ }
42
+
43
+ /**
44
+ * Give this tile a slot, reusing the one its content already has.
45
+ *
46
+ * Returns the slot. Two calls with identical content return the same slot and store one copy,
47
+ * which is the whole of the deduplication.
48
+ */
49
+ export function internTile(index: TileIndex, hash: string, bytes: Uint8Array): number {
50
+ const existing = index.slots.get(hash);
51
+ if (existing !== undefined) return existing;
52
+ const slot = index.bytes.length;
53
+ index.slots.set(hash, slot);
54
+ index.bytes.push(bytes);
55
+ return slot;
56
+ }
57
+
58
+ export function tileSlotCount(index: TileIndex): number {
59
+ return index.bytes.length;
60
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Time as a sampling argument, which is what makes an animated texture replay-exact.
3
+ *
4
+ * **`t` is the caller's, from the simulation's clock, and is never read from a platform.** Every
5
+ * other engine's animated texture reads a wall clock, so replaying the same simulation shows
6
+ * different texels — which matters for a rollback session, for a deterministic capture, and for
7
+ * every automated visual comparison this repository runs.
8
+ *
9
+ * **Latent interpolation is why a long animation is nearly free.** A sixty-frame animation shares
10
+ * one latent and moves only the time slice, so it costs a little more than one frame rather than
11
+ * sixty times one.
12
+ */
13
+
14
+ /** Which frame of a flipbook this time lands on. Never negative, never past the end. */
15
+ export function flipbookFrame(t: number, frames: number, fps: number, loop: boolean): number {
16
+ if (frames <= 0) return 0;
17
+ const raw = Math.floor(t * fps);
18
+ if (loop) {
19
+ /* Modulo that stays non-negative for negative time, which `%` alone does not. */
20
+ return ((raw % frames) + frames) % frames;
21
+ }
22
+ return Math.min(frames - 1, Math.max(0, raw));
23
+ }
24
+
25
+ export interface LerpWeights {
26
+ a: number;
27
+ b: number;
28
+ mix: number;
29
+ }
30
+
31
+ /**
32
+ * The two keys surrounding this time, and how far between them it sits.
33
+ *
34
+ * At or past the end it lands on the last key with no mix, rather than wrapping — a caller that
35
+ * wants looping says so by wrapping `t` before calling.
36
+ */
37
+ export function latentLerpWeights(
38
+ t: number,
39
+ keyCount: number,
40
+ duration: number,
41
+ out: LerpWeights,
42
+ ): void {
43
+ if (keyCount <= 1 || duration <= 0) {
44
+ out.a = 0;
45
+ out.b = 0;
46
+ out.mix = 0;
47
+ return;
48
+ }
49
+ const spans = keyCount - 1;
50
+ const position = Math.min(spans, Math.max(0, (t / duration) * spans));
51
+ const index = Math.min(spans - 1, Math.floor(position));
52
+ out.a = index;
53
+ out.b = index + 1;
54
+ out.mix = position - index;
55
+ if (position >= spans) {
56
+ out.a = spans - 1;
57
+ out.b = spans;
58
+ out.mix = 1;
59
+ }
60
+ }