@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,108 @@
1
+ /**
2
+ * Which tiles a view would sample, asked of a view nobody drew.
3
+ *
4
+ * **This is the step that makes prediction mean something for a real scene.** `predictViews` knows
5
+ * where the camera will be; a streaming system needs the tiles that camera would sample, and every
6
+ * other engine learns those from a feedback buffer the frame writes after it has already needed
7
+ * them. Here there is no frame: for each instance whose bounds are inside the predicted view, the
8
+ * projected size says which level a sampler would choose, and the instance's texture coordinates
9
+ * say which tiles of that level it covers.
10
+ *
11
+ * **It is an estimate, and it is allowed to be generous and never short.** A tile named that the
12
+ * frame does not sample is a wasted fetch. A tile the frame samples that is not named arrives late,
13
+ * which is the defect this wave exists to remove. So every approximation below leans one way:
14
+ *
15
+ * - **The level comes from the sphere's nearest point**, where the surface is finest on screen, and
16
+ * a surface seen obliquely only ever asks for coarser levels than that.
17
+ * - **Within `LEVEL_MARGIN` of a boundary, the finer level is named too.** An estimate an eighth of
18
+ * a level from a boundary is not sure which side the sampler is on.
19
+ * - **Every coarser level is named**, because a sampler with a fine tile missing falls back to a
20
+ * coarse one, and a coarse tile missing too is a hole rather than a blur. The whole tail costs at
21
+ * most a third of the finest level's tiles.
22
+ * - **Bilinear filtering's neighbour is inside the span**, so a span ending a fraction of a texel
23
+ * before a tile edge names the tile after it.
24
+ *
25
+ * **The level is the one `surfaceLod` computes on the device**: the log of the UV step a pixel
26
+ * takes, in texels of the latent's longer edge, which is the one edge the device's interpreter has.
27
+ *
28
+ * **The budget keeps what matters most, and does bounded work doing it.** Priority is the screen
29
+ * size a tile covers — its world edge at that level, capped at its instance's diameter, in pixels —
30
+ * so an instance's coarse tiles come before its fine ones and a large instance's before a small
31
+ * one's. The answer at any budget is the first entries of the answer with none. Prediction runs
32
+ * several times a frame, so a level of sixty-five thousand tiles is never walked to name thirty-two
33
+ * of them: an instance's levels only lose priority as they get finer, and the first tile the
34
+ * budget refuses ends that instance.
35
+ *
36
+ * **The frustum is extracted here and not imported.** This package is standalone and its size
37
+ * floor says so; the planes are Gribb and Hartmann exactly as `@driftengine/core`'s
38
+ * `frustumFromViewProjection` takes them, and the test holds the two to the same answer on two
39
+ * thousand spheres.
40
+ */
41
+ import type { LatentImage } from '../decodeCpu.ts';
42
+ /**
43
+ * How close to a level boundary, in levels, an estimate names the finer level as well.
44
+ *
45
+ * An eighth of a level is a ninth of the distance. A caller's texel density is an estimate of a
46
+ * mesh's UV layout, and an estimate that good is the most this is asked to forgive; a wider margin
47
+ * fetches four times a level's tiles for a quarter of all instances rather than an eighth.
48
+ */
49
+ export declare const LEVEL_MARGIN = 0.125;
50
+ /**
51
+ * One latent's tiles, level by level, by content.
52
+ *
53
+ * Level `k` is `max(1, width >> k)` by `max(1, height >> k)` texels, cut into tiles of `tileSize`
54
+ * from the top-left, the last row and column short. `levels[k][ty * across + tx]` is that tile's
55
+ * hash, where `across` is `ceil(levelWidth / tileSize)`.
56
+ */
57
+ export interface MaterialTileGrid {
58
+ /** Level 0's texels across. */
59
+ readonly width: number;
60
+ /** Level 0's texels down. */
61
+ readonly height: number;
62
+ /** Texels along a tile's edge, the same at every level. */
63
+ readonly tileSize: number;
64
+ /** One of `ADDRESS_MODE`, from the graph that samples this latent. */
65
+ readonly addressMode: number;
66
+ readonly levels: readonly (readonly string[])[];
67
+ }
68
+ /**
69
+ * What `tilesForView` needs besides the view.
70
+ *
71
+ * **The target's size is here because the plan's signature had nowhere for it**, and a projection
72
+ * alone has no pixels: the level a sampler chooses is texels per *pixel*.
73
+ */
74
+ export interface InstanceTileInfo {
75
+ readonly count: number;
76
+ /** Four floats an instance: its world-space bounding sphere's centre and radius. */
77
+ readonly spheres: Float32Array;
78
+ /** Four floats an instance: the texture coordinates its surface spans, `u0, v0, u1, v1`. */
79
+ readonly uvs: Float32Array;
80
+ /**
81
+ * World units one unit of texture coordinate spans on each instance's surface. Anything that is
82
+ * not a positive number is unknown, and an unknown density names every level.
83
+ */
84
+ readonly worldPerUv: Float32Array;
85
+ /** Which entry of `materials` each instance samples. */
86
+ readonly material: Uint32Array;
87
+ /** Per material, the latents its programs sample. */
88
+ readonly materials: readonly (readonly MaterialTileGrid[])[];
89
+ readonly targetWidth: number;
90
+ readonly targetHeight: number;
91
+ }
92
+ /**
93
+ * Cut a latent and its chain into content-addressed tiles.
94
+ *
95
+ * **Hashed as every tile in this package is**, by `hashTile` over the texels' own bytes row by
96
+ * row, so a tile shared by two materials — or by a material and an overlay — is one address and
97
+ * one fetch. Refuses a chain whose levels do not halve, because the grid could not then say which
98
+ * texels a tile holds.
99
+ */
100
+ export declare function latentTileGrid(image: LatentImage, tileSize: number, addressMode: number): MaterialTileGrid;
101
+ /**
102
+ * Fill `out` with the tiles this view would sample, most important first, at most `budget` of
103
+ * them. Returns how many.
104
+ *
105
+ * `view` and `proj` are column-major and OpenGL-convention, as every camera in this engine builds
106
+ * them. `out` is cleared past what is written.
107
+ */
108
+ export declare function tilesForView(view: Float32Array, proj: Float32Array, instances: InstanceTileInfo, out: string[], budget: number): number;
@@ -0,0 +1,419 @@
1
+ import { ADDRESS_MODE } from '../decodeGraph.js';
2
+ import { hashTile } from '../tileHash.js';
3
+ /**
4
+ * How close to a level boundary, in levels, an estimate names the finer level as well.
5
+ *
6
+ * An eighth of a level is a ninth of the distance. A caller's texel density is an estimate of a
7
+ * mesh's UV layout, and an estimate that good is the most this is asked to forgive; a wider margin
8
+ * fetches four times a level's tiles for a quarter of all instances rather than an eighth.
9
+ */
10
+ export const LEVEL_MARGIN = 0.125;
11
+ /**
12
+ * Cut a latent and its chain into content-addressed tiles.
13
+ *
14
+ * **Hashed as every tile in this package is**, by `hashTile` over the texels' own bytes row by
15
+ * row, so a tile shared by two materials — or by a material and an overlay — is one address and
16
+ * one fetch. Refuses a chain whose levels do not halve, because the grid could not then say which
17
+ * texels a tile holds.
18
+ */
19
+ export function latentTileGrid(image, tileSize, addressMode) {
20
+ if (!(Number.isInteger(tileSize) && tileSize >= 1)) {
21
+ throw new Error(`latentTileGrid: an edge of ${String(tileSize)} texels is not a tile`);
22
+ }
23
+ const chain = image.mips ?? [];
24
+ const levels = [];
25
+ for (let level = 0; level <= chain.length; level += 1) {
26
+ const source = level === 0 ? image : chain[level - 1];
27
+ const width = Math.max(1, image.width >> level);
28
+ const height = Math.max(1, image.height >> level);
29
+ if (source === undefined || source.width !== width || source.height !== height) {
30
+ throw new Error(`latentTileGrid: level ${String(level)} is ${String(source?.width)} by ` +
31
+ `${String(source?.height)} texels where the chain needs ${String(width)} by ${String(height)}`);
32
+ }
33
+ if (source.data.length < width * height * image.channels) {
34
+ throw new Error(`latentTileGrid: level ${String(level)} holds ${String(source.data.length)} values, ` +
35
+ `short of ${String(width * height * image.channels)}`);
36
+ }
37
+ levels.push(cutLevel(source.data, width, height, image.channels, tileSize));
38
+ }
39
+ return { width: image.width, height: image.height, tileSize, addressMode, levels };
40
+ }
41
+ function cutLevel(data, width, height, channels, tile) {
42
+ const hashes = [];
43
+ for (let ty = 0; ty * tile < height; ty += 1) {
44
+ for (let tx = 0; tx * tile < width; tx += 1) {
45
+ const w = Math.min(tile, width - tx * tile);
46
+ const h = Math.min(tile, height - ty * tile);
47
+ const texels = new Float32Array(w * h * channels);
48
+ for (let y = 0; y < h; y += 1) {
49
+ const from = ((ty * tile + y) * width + tx * tile) * channels;
50
+ texels.set(data.subarray(from, from + w * channels), y * w * channels);
51
+ }
52
+ hashes.push(hashTile(new Uint8Array(texels.buffer)));
53
+ }
54
+ }
55
+ return hashes;
56
+ }
57
+ /* Scratch, grown and never shrunk, so a call allocates nothing once warm. */
58
+ const PLANES = new Float32Array(24);
59
+ const SPAN_U = new Int32Array(4);
60
+ const SPAN_V = new Int32Array(4);
61
+ const NO_GRIDS = [];
62
+ /**
63
+ * A pixel's footprint is taken no nearer than this, in clip `w`. It only matters for an instance
64
+ * whose centre is at or behind the eye, which is on screen because the camera is inside it.
65
+ */
66
+ const NEAREST_W = 1e-6;
67
+ /* The best `cap` candidates so far, as a heap with the worst at its root. */
68
+ const heapHash = [];
69
+ let heapPriority = new Float64Array(64);
70
+ let heapOrder = new Float64Array(64);
71
+ const heapAt = new Map();
72
+ let heapSize = 0;
73
+ let heapCap = 0;
74
+ /**
75
+ * Fill `out` with the tiles this view would sample, most important first, at most `budget` of
76
+ * them. Returns how many.
77
+ *
78
+ * `view` and `proj` are column-major and OpenGL-convention, as every camera in this engine builds
79
+ * them. `out` is cleared past what is written.
80
+ */
81
+ export function tilesForView(view, proj, instances, out, budget) {
82
+ heapSize = 0;
83
+ heapAt.clear();
84
+ heapCap = budget >= 1 ? Math.floor(budget) : 0;
85
+ if (heapCap === 0) {
86
+ out.length = 0;
87
+ return 0;
88
+ }
89
+ planesOf(proj, PLANES);
90
+ const p3 = proj[3] ?? 0;
91
+ const p7 = proj[7] ?? 0;
92
+ const p11 = proj[11] ?? 0;
93
+ const p15 = proj[15] ?? 0;
94
+ const across = Math.abs(proj[0] ?? 0) * instances.targetWidth;
95
+ const down = Math.abs(proj[5] ?? 0) * instances.targetHeight;
96
+ /* Pixels a world unit covers at clip w = 1, along the axis where it covers fewest. */
97
+ const pixelsAtUnitW = Math.min(across, down) / 2;
98
+ const wPerRadius = Math.hypot(p3, p7, p11);
99
+ let order = 0;
100
+ for (let i = 0; i < instances.count; i += 1) {
101
+ const s = i * 4;
102
+ const x = instances.spheres[s] ?? 0;
103
+ const y = instances.spheres[s + 1] ?? 0;
104
+ const z = instances.spheres[s + 2] ?? 0;
105
+ const given = instances.spheres[s + 3] ?? 0;
106
+ const radius = given > 0 ? given : 0;
107
+ const vx = (view[0] ?? 0) * x + (view[4] ?? 0) * y + (view[8] ?? 0) * z + (view[12] ?? 0);
108
+ const vy = (view[1] ?? 0) * x + (view[5] ?? 0) * y + (view[9] ?? 0) * z + (view[13] ?? 0);
109
+ const vz = (view[2] ?? 0) * x + (view[6] ?? 0) * y + (view[10] ?? 0) * z + (view[14] ?? 0);
110
+ if (!sphereInside(PLANES, vx, vy, vz, radius))
111
+ continue;
112
+ const w = p3 * vx + p7 * vy + p11 * vz + p15;
113
+ const nearW = w - wPerRadius * radius;
114
+ /* Zero rather than not-a-number for a target or a matrix with no answer, so ties stay ties. */
115
+ const footprint = pixelsAtUnitW / Math.max(w, NEAREST_W);
116
+ const pixelsPerWorld = footprint > 0 ? footprint : 0;
117
+ const density = instances.worldPerUv[i] ?? 0;
118
+ /* An infinite density needs no case of its own: its level is 0 and its tiles' edges infinite. */
119
+ const known = density > 0 && pixelsAtUnitW > 0 && nearW > 0;
120
+ const grids = instances.materials[instances.material[i] ?? 0] ?? NO_GRIDS;
121
+ for (const grid of grids) {
122
+ const top = grid.levels.length - 1;
123
+ if (top < 0 || !(grid.tileSize >= 1))
124
+ continue;
125
+ const size = Math.max(grid.width, grid.height);
126
+ const finest = known
127
+ ? clamp(Math.floor(Math.log2((size * nearW) / (density * pixelsAtUnitW)) - LEVEL_MARGIN), 0, top)
128
+ : 0;
129
+ levels: for (let level = top; level >= finest; level -= 1) {
130
+ const edge = known ? (grid.tileSize * 2 ** level * density) / size : Infinity;
131
+ const priority = pixelsPerWorld * Math.min(edge, 2 * radius);
132
+ const width = Math.max(1, grid.width >> level);
133
+ const height = Math.max(1, grid.height >> level);
134
+ const nu = tileSpans(instances.uvs, s, width, grid, SPAN_U);
135
+ const nv = tileSpans(instances.uvs, s + 1, height, grid, SPAN_V);
136
+ const row = grid.levels[level] ?? NO_HASHES;
137
+ const tilesAcross = Math.ceil(width / grid.tileSize);
138
+ for (let a = 0; a < nv; a += 1) {
139
+ const lastY = SPAN_V[a * 2 + 1];
140
+ for (let ty = SPAN_V[a * 2]; ty <= lastY; ty += 1) {
141
+ for (let b = 0; b < nu; b += 1) {
142
+ const lastX = SPAN_U[b * 2 + 1];
143
+ for (let tx = SPAN_U[b * 2]; tx <= lastX; tx += 1) {
144
+ /*
145
+ * Everything after a refused candidate in this instance's walk is refused too: the
146
+ * rest of this level ties it and comes later, and a finer level is worth no more.
147
+ */
148
+ if (heapSize === heapCap && !beatsWorst(priority))
149
+ break levels;
150
+ const hash = row[ty * tilesAcross + tx];
151
+ if (hash !== undefined)
152
+ offer(hash, priority, order);
153
+ order += 1;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ const count = heapSize;
162
+ out.length = count;
163
+ for (let at = count - 1; at >= 0; at -= 1) {
164
+ out[at] = heapHash[0];
165
+ popWorst();
166
+ }
167
+ heapAt.clear();
168
+ return count;
169
+ }
170
+ const NO_HASHES = [];
171
+ function clamp(value, low, high) {
172
+ return Math.min(high, Math.max(low, value));
173
+ }
174
+ /**
175
+ * Left, right, bottom, top, near, far, normalised, inward — `frustumFromViewProjection`'s planes,
176
+ * taken from the projection alone so the test below them is in view space.
177
+ */
178
+ function planesOf(m, out) {
179
+ const r0x = m[0] ?? 0;
180
+ const r0y = m[4] ?? 0;
181
+ const r0z = m[8] ?? 0;
182
+ const r0w = m[12] ?? 0;
183
+ const r1x = m[1] ?? 0;
184
+ const r1y = m[5] ?? 0;
185
+ const r1z = m[9] ?? 0;
186
+ const r1w = m[13] ?? 0;
187
+ const r2x = m[2] ?? 0;
188
+ const r2y = m[6] ?? 0;
189
+ const r2z = m[10] ?? 0;
190
+ const r2w = m[14] ?? 0;
191
+ const r3x = m[3] ?? 0;
192
+ const r3y = m[7] ?? 0;
193
+ const r3z = m[11] ?? 0;
194
+ const r3w = m[15] ?? 0;
195
+ setPlane(out, 0, r3x + r0x, r3y + r0y, r3z + r0z, r3w + r0w);
196
+ setPlane(out, 1, r3x - r0x, r3y - r0y, r3z - r0z, r3w - r0w);
197
+ setPlane(out, 2, r3x + r1x, r3y + r1y, r3z + r1z, r3w + r1w);
198
+ setPlane(out, 3, r3x - r1x, r3y - r1y, r3z - r1z, r3w - r1w);
199
+ /* Near is row 4 plus row 3: the OpenGL convention every matrix here is built in. */
200
+ setPlane(out, 4, r3x + r2x, r3y + r2y, r3z + r2z, r3w + r2w);
201
+ setPlane(out, 5, r3x - r2x, r3y - r2y, r3z - r2z, r3w - r2w);
202
+ }
203
+ function setPlane(out, index, x, y, z, d) {
204
+ const length = Math.sqrt(x * x + y * y + z * z);
205
+ /* A degenerate plane accepts everything, which is the direction a cull may fail in. */
206
+ const scale = length > 1e-12 ? 1 / length : 0;
207
+ const at = index * 4;
208
+ out[at] = x * scale;
209
+ out[at + 1] = y * scale;
210
+ out[at + 2] = z * scale;
211
+ out[at + 3] = d * scale;
212
+ }
213
+ /** Outside only when wholly beyond one plane; a sphere across an edge is on screen. */
214
+ function sphereInside(planes, x, y, z, r) {
215
+ for (let at = 0; at < 24; at += 4) {
216
+ const distance = (planes[at] ?? 0) * x +
217
+ (planes[at + 1] ?? 0) * y +
218
+ (planes[at + 2] ?? 0) * z +
219
+ (planes[at + 3] ?? 0);
220
+ if (distance < -r)
221
+ return false;
222
+ }
223
+ return true;
224
+ }
225
+ /**
226
+ * The tile columns (or rows) a span of texture coordinates reaches at one level, as one or two
227
+ * inclusive ranges in `out`. Returns how many.
228
+ *
229
+ * `first` indexes the span's low end in `uvs`, and `first + 2` its high end. Texels are addressed as
230
+ * `decodeCpu` addresses them — a lattice puts coordinate 0 on the first texel's centre and 1 on the
231
+ * last's, a centre mode puts them on the edges — and each end reaches the texel bilinear filtering
232
+ * reads beside it. A wrapping span that crosses the seam is two ranges; one that repeats, or that
233
+ * is not a number, is every tile, because an unknown span is not a reason to name nothing.
234
+ */
235
+ function tileSpans(uvs, first, size, grid, out) {
236
+ const a = uvs[first] ?? 0;
237
+ const b = uvs[first + 2] ?? 0;
238
+ const lo = Math.min(a, b);
239
+ const hi = Math.max(a, b);
240
+ const last = size - 1;
241
+ let count;
242
+ if (!(hi - lo >= 0)) {
243
+ count = whole(last, out);
244
+ }
245
+ else {
246
+ switch (grid.addressMode) {
247
+ case ADDRESS_MODE.LATTICE_CLAMP: {
248
+ const from = Math.floor(clamp(lo, 0, 1) * last);
249
+ count = one(from, Math.min(Math.floor(clamp(hi, 0, 1) * last) + 1, last), out);
250
+ break;
251
+ }
252
+ case ADDRESS_MODE.LATTICE_WRAP: {
253
+ if (!(hi - lo < 1)) {
254
+ count = whole(last, out);
255
+ break;
256
+ }
257
+ const low = Math.floor(lo);
258
+ const high = Math.floor(hi);
259
+ const from = Math.floor((lo - low) * last);
260
+ const to = Math.min(Math.floor((hi - high) * last) + 1, last);
261
+ count = low === high ? one(from, to, out) : two(0, to, from, last, out);
262
+ break;
263
+ }
264
+ case ADDRESS_MODE.CENTRE_CLAMP:
265
+ case ADDRESS_MODE.CENTRE_WRAP: {
266
+ const from = Math.floor(lo * size - 0.5);
267
+ const to = Math.floor(hi * size - 0.5) + 1;
268
+ if (grid.addressMode === ADDRESS_MODE.CENTRE_CLAMP) {
269
+ count = one(clamp(from, 0, last), clamp(to, 0, last), out);
270
+ }
271
+ else if (!(to - from < size)) {
272
+ count = whole(last, out);
273
+ }
274
+ else {
275
+ const start = ((from % size) + size) % size;
276
+ const end = start + (to - from);
277
+ count = end <= last ? one(start, end, out) : two(0, end - size, start, last, out);
278
+ }
279
+ break;
280
+ }
281
+ default:
282
+ count = whole(last, out);
283
+ }
284
+ }
285
+ /* Texels to tiles, and two ranges that meet or overlap as tiles are one. */
286
+ const tile = grid.tileSize;
287
+ for (let i = 0; i < count * 2; i += 1)
288
+ out[i] = Math.floor(out[i] / tile);
289
+ if (count === 2 && out[2] <= out[1] + 1) {
290
+ out[1] = out[3];
291
+ count = 1;
292
+ }
293
+ return count;
294
+ }
295
+ function whole(last, out) {
296
+ return one(0, last, out);
297
+ }
298
+ function one(from, to, out) {
299
+ out[0] = from;
300
+ out[1] = to;
301
+ return 1;
302
+ }
303
+ /** Two ranges, the one starting at zero first. */
304
+ function two(from0, to0, from1, to1, out) {
305
+ out[0] = from0;
306
+ out[1] = to0;
307
+ out[2] = from1;
308
+ out[3] = to1;
309
+ return 2;
310
+ }
311
+ /*
312
+ * The heap. `worse(i, j)` is the order it keeps: lower priority is worse, and between equals the
313
+ * later candidate is, so the answer is the same however the ties fell in the walk.
314
+ */
315
+ function worse(i, j) {
316
+ const pi = heapPriority[i];
317
+ const pj = heapPriority[j];
318
+ return pi < pj || (pi === pj && heapOrder[i] > heapOrder[j]);
319
+ }
320
+ /**
321
+ * Whether a candidate would displace the worst entry of a full heap.
322
+ *
323
+ * **A tie never does**, and not by choice: candidates are numbered in the order they are named, so
324
+ * every entry already held was named before this one and wins the tie.
325
+ */
326
+ function beatsWorst(priority) {
327
+ return priority > heapPriority[0];
328
+ }
329
+ function offer(hash, priority, order) {
330
+ const held = heapAt.get(hash);
331
+ if (held !== undefined) {
332
+ /* One entry a tile, carrying its best claim; a later equal claim is not a better one. */
333
+ if (priority > heapPriority[held]) {
334
+ heapPriority[held] = priority;
335
+ heapOrder[held] = order;
336
+ siftDown(held);
337
+ }
338
+ return;
339
+ }
340
+ let at;
341
+ if (heapSize < heapCap) {
342
+ at = heapSize;
343
+ heapSize += 1;
344
+ if (at >= heapPriority.length)
345
+ grow(at + 1);
346
+ }
347
+ else {
348
+ heapAt.delete(heapHash[0]);
349
+ at = 0;
350
+ }
351
+ heapHash[at] = hash;
352
+ heapPriority[at] = priority;
353
+ heapOrder[at] = order;
354
+ heapAt.set(hash, at);
355
+ if (at === 0)
356
+ siftDown(0);
357
+ else
358
+ siftUp(at);
359
+ }
360
+ function popWorst() {
361
+ heapSize -= 1;
362
+ if (heapSize === 0)
363
+ return;
364
+ move(heapSize, 0);
365
+ siftDown(0);
366
+ }
367
+ function siftUp(start) {
368
+ let at = start;
369
+ while (at > 0) {
370
+ const parent = (at - 1) >> 1;
371
+ if (!worse(at, parent))
372
+ return;
373
+ swap(at, parent);
374
+ at = parent;
375
+ }
376
+ }
377
+ function siftDown(start) {
378
+ let at = start;
379
+ for (;;) {
380
+ const left = at * 2 + 1;
381
+ if (left >= heapSize)
382
+ return;
383
+ const right = left + 1;
384
+ const child = right < heapSize && worse(right, left) ? right : left;
385
+ if (!worse(child, at))
386
+ return;
387
+ swap(at, child);
388
+ at = child;
389
+ }
390
+ }
391
+ function swap(i, j) {
392
+ const hash = heapHash[i];
393
+ const priority = heapPriority[i];
394
+ const order = heapOrder[i];
395
+ move(j, i);
396
+ heapHash[j] = hash;
397
+ heapPriority[j] = priority;
398
+ heapOrder[j] = order;
399
+ heapAt.set(hash, j);
400
+ }
401
+ function move(from, to) {
402
+ const hash = heapHash[from];
403
+ heapHash[to] = hash;
404
+ heapPriority[to] = heapPriority[from];
405
+ heapOrder[to] = heapOrder[from];
406
+ heapAt.set(hash, to);
407
+ }
408
+ /** The only place the heap's storage grows, doubling. */
409
+ function grow(needed) {
410
+ let length = heapPriority.length;
411
+ while (length < needed)
412
+ length *= 2;
413
+ const priority = new Float64Array(length);
414
+ const order = new Float64Array(length);
415
+ priority.set(heapPriority);
416
+ order.set(heapOrder);
417
+ heapPriority = priority;
418
+ heapOrder = order;
419
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What a channel *is*, declared rather than conventional.
3
+ *
4
+ * **The family of defects this removes is the one nobody attributes correctly.** A normal map
5
+ * upside down in one asset and not in another; an albedo that is linear here and sRGB there; a
6
+ * gloss map read as roughness, which inverts every highlight in the scene. Each is a convention
7
+ * held in somebody's head, and each survives review because the file looks fine on its own.
8
+ *
9
+ * Declared, the format knows. A consumer never sees a convention at all, because `normaliseSample`
10
+ * has already applied it — a Y-down normal comes back Y-up, an sRGB value comes back linear, and
11
+ * gloss comes back as the roughness the engine shades with.
12
+ */
13
+ export type ChannelSemantic = 'albedo-srgb' | 'albedo-linear' | 'normal-tangent-yup' | 'normal-tangent-ydown' | 'roughness-linear' | 'gloss-linear' | 'metallic-linear' | 'occlusion-linear' | 'height-linear' | 'emissive-srgb' | 'mask-linear';
14
+ /**
15
+ * Every semantic, in the order a file stores them by.
16
+ *
17
+ * **A name crosses a package boundary as a number**, because `@driftengine/drft` is the container
18
+ * and knows nothing about what a channel means — a `DTEX` chunk stores `(semanticIndex << 4) |
19
+ * component` and would have to carry strings otherwise. The index is therefore part of the format:
20
+ * **a semantic may be appended and none may be reordered or removed**, or every file written before
21
+ * the change decodes its channels as something else. `semantics.test.ts` holds the order.
22
+ */
23
+ export declare const CHANNEL_SEMANTICS: readonly ChannelSemantic[];
24
+ /** Where a semantic sits in that order, or −1 for one this build does not know. */
25
+ export declare function semanticIndex(semantic: ChannelSemantic): number;
26
+ /** The semantic an index names, or null where a file names one from a later version. */
27
+ export declare function semanticAt(index: number): ChannelSemantic | null;
28
+ export interface ChannelSpec {
29
+ readonly semantic: ChannelSemantic;
30
+ /** Which component of the decoded vector this channel occupies. */
31
+ readonly component: number;
32
+ }
33
+ /** Whether this channel carries colour, and therefore whether a transfer curve applies to it. */
34
+ export declare function isColour(semantic: ChannelSemantic): boolean;
35
+ /** Whether mipping this channel must preserve the normal distribution. See `mipNdf.ts`. */
36
+ export declare function needsVarianceMips(semantic: ChannelSemantic): boolean;
37
+ /**
38
+ * The exact sRGB transfer function, piecewise, not the 2.2 power approximation.
39
+ *
40
+ * The approximation is within about two percent almost everywhere and wrong by more than that in
41
+ * the dark end, which in a texture pipeline is a colour shift nobody attributes to the right cause
42
+ * for weeks. The midpoint is the value that tells them apart: 0.5 linearises to 0.2140 exactly and
43
+ * to 0.2176 approximately.
44
+ */
45
+ export declare function srgbToLinear(value: number): number;
46
+ export declare function linearToSrgb(value: number): number;
47
+ /**
48
+ * Write one raw channel value into its component of `out`, with its convention already applied.
49
+ *
50
+ * A consumer of this never asks which way a normal points or which curve an albedo carries.
51
+ */
52
+ export declare function normaliseSample(out: Float32Array, spec: ChannelSpec, raw: number): void;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Every semantic, in the order a file stores them by.
3
+ *
4
+ * **A name crosses a package boundary as a number**, because `@driftengine/drft` is the container
5
+ * and knows nothing about what a channel means — a `DTEX` chunk stores `(semanticIndex << 4) |
6
+ * component` and would have to carry strings otherwise. The index is therefore part of the format:
7
+ * **a semantic may be appended and none may be reordered or removed**, or every file written before
8
+ * the change decodes its channels as something else. `semantics.test.ts` holds the order.
9
+ */
10
+ export const CHANNEL_SEMANTICS = [
11
+ 'albedo-srgb',
12
+ 'albedo-linear',
13
+ 'normal-tangent-yup',
14
+ 'normal-tangent-ydown',
15
+ 'roughness-linear',
16
+ 'gloss-linear',
17
+ 'metallic-linear',
18
+ 'occlusion-linear',
19
+ 'height-linear',
20
+ 'emissive-srgb',
21
+ 'mask-linear',
22
+ ];
23
+ /** Where a semantic sits in that order, or −1 for one this build does not know. */
24
+ export function semanticIndex(semantic) {
25
+ return CHANNEL_SEMANTICS.indexOf(semantic);
26
+ }
27
+ /** The semantic an index names, or null where a file names one from a later version. */
28
+ export function semanticAt(index) {
29
+ return CHANNEL_SEMANTICS[index] ?? null;
30
+ }
31
+ /** Whether this channel carries colour, and therefore whether a transfer curve applies to it. */
32
+ export function isColour(semantic) {
33
+ return semantic === 'albedo-srgb' || semantic === 'albedo-linear' || semantic === 'emissive-srgb';
34
+ }
35
+ /** Whether mipping this channel must preserve the normal distribution. See `mipNdf.ts`. */
36
+ export function needsVarianceMips(semantic) {
37
+ return semantic === 'normal-tangent-yup' || semantic === 'normal-tangent-ydown';
38
+ }
39
+ /**
40
+ * The exact sRGB transfer function, piecewise, not the 2.2 power approximation.
41
+ *
42
+ * The approximation is within about two percent almost everywhere and wrong by more than that in
43
+ * the dark end, which in a texture pipeline is a colour shift nobody attributes to the right cause
44
+ * for weeks. The midpoint is the value that tells them apart: 0.5 linearises to 0.2140 exactly and
45
+ * to 0.2176 approximately.
46
+ */
47
+ export function srgbToLinear(value) {
48
+ return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
49
+ }
50
+ export function linearToSrgb(value) {
51
+ return value <= 0.0031308 ? value * 12.92 : 1.055 * Math.pow(value, 1 / 2.4) - 0.055;
52
+ }
53
+ /**
54
+ * Write one raw channel value into its component of `out`, with its convention already applied.
55
+ *
56
+ * A consumer of this never asks which way a normal points or which curve an albedo carries.
57
+ */
58
+ export function normaliseSample(out, spec, raw) {
59
+ const at = spec.component;
60
+ switch (spec.semantic) {
61
+ case 'albedo-srgb':
62
+ case 'emissive-srgb':
63
+ out[at] = srgbToLinear(raw);
64
+ return;
65
+ case 'normal-tangent-ydown':
66
+ /* Flipped about the midpoint, because a tangent-space normal is stored biased into 0..1. */
67
+ out[at] = 1 - raw;
68
+ return;
69
+ case 'gloss-linear':
70
+ /* The engine shades with roughness. One of the two has to win, and it is not this one. */
71
+ out[at] = 1 - raw;
72
+ return;
73
+ default:
74
+ out[at] = raw;
75
+ }
76
+ }