@driftengine/drft 3.62.0 → 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.
package/dist/index.d.ts CHANGED
@@ -12,8 +12,20 @@ export type { AnimationClip, DrftSkin, Joint, JointTrack, TrackPath } from './an
12
12
  export type { DrftMorph, DrftNode } from './drftSkin.ts';
13
13
  export type { DrftSubs, DrftSubstanceEntry } from './drftSubs.ts';
14
14
  export { buildSubs, readSubs } from './drftSubs.ts';
15
+ export type { DrftSdfv, DrftSdfvEntry } from './sdfv.ts';
16
+ export { buildSdfv, readSdfv, SDFV_ENTRY_BYTES, SDFV_MAX_SAMPLES } from './sdfv.ts';
17
+ export { NNET_ENTRY_BYTES, NNET_MAX_HIDDEN, NNET_MAX_WIDTH, buildNnet, nnetWeightCount, readNnet, } from './nnet.ts';
18
+ export type { DrftNetwork, DrftNnet } from './nnet.ts';
19
+ export { buildNgrf, readNgrf } from './ngrf.ts';
20
+ export type { DrftAttribute, DrftGraph, DrftGraphNode, DrftGraphTensor, DrftGraphValue, } from './ngrf.ts';
15
21
  export { buildClip, buildMorph, buildNodes, buildSkin, readClip, readMorph, readNodes, readSkin, } from './drftSkin.ts';
16
22
  export { MAX_COLLIDER_HULLS, MAX_COLLIDER_POINTS, buildColliders, readColliders, } from './drftColliders.ts';
23
+ export { buildDtex, readDtex, DTEX_ADDRESS_MODES, DTEX_MAX_TILES, DTEX_REGISTERS } from './dtex.ts';
24
+ export type { DtexEntry, DtexMaterial } from './dtex.ts';
25
+ export { buildNavm, readNavm } from './navm.ts';
26
+ export type { NavPolyMesh } from './navm.ts';
27
+ export { buildEnts, readEnts } from './ents.ts';
28
+ export type { EntsScene } from './ents.ts';
17
29
  export { readDrft } from './drftRead.ts';
18
30
  export type { DrftAsset, DrftTexture } from './drftRead.ts';
19
31
  export { DrftStream, streamDrft } from './drftStream.ts';
@@ -24,3 +36,5 @@ export { DrftError, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR } from './drftFormat.
24
36
  export { CODEC_PNG, CODEC_JPEG, CODEC_WEBP, CODEC_RAW, codecName } from './drftFormat.ts';
25
37
  export type { DrftHead, DrftMaterial, DrftSplatBlock, DrftSplats } from './drftFormat.ts';
26
38
  export { coarseFirstOrder } from './coarseFirst.ts';
39
+ export { MAX_CLUSTERS, buildMeshlets, readMeshlets } from './drftMeshlets.ts';
40
+ export type { MeshletLevel } from './drftMeshlets.ts';
package/dist/index.js CHANGED
@@ -8,6 +8,9 @@
8
8
  */
9
9
  export { validateMeshData } from './meshData.js';
10
10
  export { buildSubs, readSubs } from './drftSubs.js';
11
+ export { buildSdfv, readSdfv, SDFV_ENTRY_BYTES, SDFV_MAX_SAMPLES } from './sdfv.js';
12
+ export { NNET_ENTRY_BYTES, NNET_MAX_HIDDEN, NNET_MAX_WIDTH, buildNnet, nnetWeightCount, readNnet, } from './nnet.js';
13
+ export { buildNgrf, readNgrf } from './ngrf.js';
11
14
  export { buildClip, buildMorph, buildNodes, buildSkin, readClip, readMorph, readNodes, readSkin, } from './drftSkin.js';
12
15
  /*
13
16
  * The `.drft` asset container. See docs/FORMAT.md for the format and its compatibility rules.
@@ -17,6 +20,24 @@ export { buildClip, buildMorph, buildNodes, buildSkin, readClip, readMorph, read
17
20
  * of the format unable to disagree about a byte.
18
21
  */
19
22
  export { MAX_COLLIDER_HULLS, MAX_COLLIDER_POINTS, buildColliders, readColliders, } from './drftColliders.js';
23
+ /*
24
+ * **`DTEX`: a material as a decode program over a latent, rather than as pictures.** The chunk
25
+ * carries no semantics — this package is the container and does not know what a channel means —
26
+ * so a channel crosses as `(semanticIndex << 4) | component` and `@driftengine/texture` owns the
27
+ * numbering.
28
+ */
29
+ export { buildDtex, readDtex, DTEX_ADDRESS_MODES, DTEX_MAX_TILES, DTEX_REGISTERS } from './dtex.js';
30
+ /*
31
+ * **`NAVM`: a way across the scene, with the placement it was built at.** The numbers in a polygon
32
+ * mesh are cell indices, so the origin and the cell size travel inside the chunk — a mesh that
33
+ * arrives without them is in the wrong units at the wrong place, and looks loaded.
34
+ *
35
+ * **`ENTS`: the things in the scene, as the entity model wrote them.** Text, in a binary container,
36
+ * because the values are a consumer's own component fields and this package does not know their
37
+ * types — see `ents.ts` for why encoding them would be worse than carrying them.
38
+ */
39
+ export { buildNavm, readNavm } from './navm.js';
40
+ export { buildEnts, readEnts } from './ents.js';
20
41
  export { readDrft } from './drftRead.js';
21
42
  /* Reading one while it is still arriving, for a scene that would rather show a model build. */
22
43
  export { DrftStream, streamDrft } from './drftStream.js';
@@ -27,3 +48,4 @@ export { CODEC_PNG, CODEC_JPEG, CODEC_WEBP, CODEC_RAW, codecName } from './drftF
27
48
  /* The order a capture is written in, so a baker outside this package can lay one out the same
28
49
  way — it is the whole reason a capture streams into a recognisable place. */
29
50
  export { coarseFirstOrder } from './coarseFirst.js';
51
+ export { MAX_CLUSTERS, buildMeshlets, readMeshlets } from './drftMeshlets.js';
package/dist/navm.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `NAVM`: a polygon mesh a character can walk, with the placement it was built at.
3
+ *
4
+ * ```
5
+ * u32 vertexCount
6
+ * u32 polyCount
7
+ * u32 maxVertsPerPoly slots per polygon; unused ones hold -1
8
+ * f32 originX
9
+ * f32 originZ
10
+ * f32 cellSize
11
+ * i32 vertices[vertexCount * 2] x, z in cell units
12
+ * i32 polys[polyCount * maxVertsPerPoly]
13
+ * i32 neighbours[polyCount * maxVertsPerPoly] the polygon across edge i, or -1
14
+ * i32 polyRegion[polyCount]
15
+ * ```
16
+ *
17
+ * **The placement is in the chunk, and that is the whole reason this is not just two tables.** The
18
+ * vertices are cell indices; the origin and the cell size are what turn them into metres. A mesh
19
+ * that travels without them arrives in the wrong units at the wrong place, and every test built at
20
+ * the origin with one-unit cells passes anyway, because there the two are the same number.
21
+ *
22
+ * **Containment is checked here; walkability is not.** Whether these polygons describe a surface
23
+ * anybody can cross is `@driftengine/nav`'s question, in the package that knows what a region is.
24
+ * Whether an index points inside the tables is this one's, because an index past the end is a
25
+ * consumer reading whatever followed.
26
+ *
27
+ * The types are plain arrays that `@driftengine/nav`'s `PolyMesh` satisfies structurally — the same
28
+ * arrangement `DTEX` has with `@driftengine/texture`, and for the same reason: this package has no
29
+ * dependencies and cannot name a type it does not import.
30
+ *
31
+ * Additive on the same terms as `DTEX`: a reader that does not know this code skips it by its
32
+ * length and loses only the navigation, which it had nothing to walk with anyway.
33
+ */
34
+ /** A polygon mesh and where it stands. `@driftengine/nav`'s `PolyMesh` is one of these. */
35
+ export interface NavPolyMesh {
36
+ /** x, z per vertex, in cell units. */
37
+ readonly vertices: Int32Array;
38
+ readonly vertexCount: number;
39
+ /** `maxVertsPerPoly` slots per polygon, padded with −1. */
40
+ readonly polys: Int32Array;
41
+ /** The polygon across each edge, or −1. Same layout as `polys`. */
42
+ readonly neighbours: Int32Array;
43
+ readonly polyCount: number;
44
+ readonly maxVertsPerPoly: number;
45
+ /** Which region each polygon came from. */
46
+ readonly polyRegion: Int32Array;
47
+ readonly originX: number;
48
+ readonly originZ: number;
49
+ readonly cellSize: number;
50
+ }
51
+ export declare function buildNavm(mesh: NavPolyMesh): Uint8Array;
52
+ /**
53
+ * The mesh a `NAVM` chunk carries, as views over the fetched buffer.
54
+ *
55
+ * Views rather than copies, like every other chunk here, so a load allocates nothing beyond the
56
+ * object holding them.
57
+ */
58
+ export declare function readNavm(buffer: ArrayBuffer, offset: number, byteLength: number): NavPolyMesh;
package/dist/navm.js ADDED
@@ -0,0 +1,105 @@
1
+ import { DrftError, align } from './drftFormat.js';
2
+ /** Six: the three counts and the three numbers that place the mesh in the world. */
3
+ const HEADER_WORDS = 6;
4
+ const HEADER = HEADER_WORDS * 4;
5
+ /** Where each table starts, and the size of the whole. */
6
+ function layoutOf(vertexCount, polyCount, slots) {
7
+ let at = HEADER;
8
+ const vertices = at;
9
+ at += vertexCount * 2 * 4;
10
+ const polys = at;
11
+ at += polyCount * slots * 4;
12
+ const neighbours = at;
13
+ at += polyCount * slots * 4;
14
+ const region = at;
15
+ at += polyCount * 4;
16
+ return [vertices, polys, neighbours, region, at];
17
+ }
18
+ export function buildNavm(mesh) {
19
+ const { vertexCount, polyCount, maxVertsPerPoly: slots } = mesh;
20
+ if (!Number.isInteger(vertexCount) || vertexCount < 0) {
21
+ throw new DrftError(`NAVM declares ${vertexCount} vertices, which is not a count`);
22
+ }
23
+ if (!Number.isInteger(polyCount) || polyCount < 0) {
24
+ throw new DrftError(`NAVM declares ${polyCount} polygons, which is not a count`);
25
+ }
26
+ if (!Number.isInteger(slots) || slots < 3) {
27
+ throw new DrftError(`NAVM gives each polygon ${slots} slots, and a polygon has three corners`);
28
+ }
29
+ if (mesh.vertices.length !== vertexCount * 2) {
30
+ throw new DrftError(`NAVM has ${mesh.vertices.length} vertex words for ${vertexCount} vertices of two`);
31
+ }
32
+ if (mesh.polys.length !== polyCount * slots || mesh.neighbours.length !== polyCount * slots) {
33
+ throw new DrftError(`NAVM's polygon and neighbour tables are not ${polyCount} × ${slots}`);
34
+ }
35
+ if (mesh.polyRegion.length !== polyCount) {
36
+ throw new DrftError(`NAVM has ${mesh.polyRegion.length} regions for ${polyCount} polygons`);
37
+ }
38
+ refuseStrayIndices(mesh.polys, vertexCount, 'polygon', 'vertex');
39
+ refuseStrayIndices(mesh.neighbours, polyCount, 'neighbour', 'polygon');
40
+ const [vertices, polys, neighbours, region, size] = layoutOf(vertexCount, polyCount, slots);
41
+ const bytes = new Uint8Array(align(size));
42
+ const view = new DataView(bytes.buffer);
43
+ view.setUint32(0, vertexCount, true);
44
+ view.setUint32(4, polyCount, true);
45
+ view.setUint32(8, slots, true);
46
+ view.setFloat32(12, mesh.originX, true);
47
+ view.setFloat32(16, mesh.originZ, true);
48
+ view.setFloat32(20, mesh.cellSize, true);
49
+ new Int32Array(bytes.buffer, vertices, vertexCount * 2).set(mesh.vertices);
50
+ new Int32Array(bytes.buffer, polys, polyCount * slots).set(mesh.polys);
51
+ new Int32Array(bytes.buffer, neighbours, polyCount * slots).set(mesh.neighbours);
52
+ new Int32Array(bytes.buffer, region, polyCount).set(mesh.polyRegion);
53
+ return bytes;
54
+ }
55
+ /**
56
+ * The mesh a `NAVM` chunk carries, as views over the fetched buffer.
57
+ *
58
+ * Views rather than copies, like every other chunk here, so a load allocates nothing beyond the
59
+ * object holding them.
60
+ */
61
+ export function readNavm(buffer, offset, byteLength) {
62
+ if (byteLength < HEADER)
63
+ throw new DrftError('NAVM is too short to hold its header');
64
+ const view = new DataView(buffer, offset, byteLength);
65
+ const vertexCount = view.getUint32(0, true);
66
+ const polyCount = view.getUint32(4, true);
67
+ const slots = view.getUint32(8, true);
68
+ if (slots < 3) {
69
+ throw new DrftError(`NAVM gives each polygon ${slots} slots, and a polygon has three corners`);
70
+ }
71
+ const [vertices, polys, neighbours, region, size] = layoutOf(vertexCount, polyCount, slots);
72
+ if (size > byteLength) {
73
+ throw new DrftError(`NAVM declares ${String(size)} bytes of tables in a ${String(byteLength)}-byte chunk`);
74
+ }
75
+ const mesh = {
76
+ vertices: new Int32Array(buffer, offset + vertices, vertexCount * 2),
77
+ vertexCount,
78
+ polys: new Int32Array(buffer, offset + polys, polyCount * slots),
79
+ neighbours: new Int32Array(buffer, offset + neighbours, polyCount * slots),
80
+ polyCount,
81
+ maxVertsPerPoly: slots,
82
+ polyRegion: new Int32Array(buffer, offset + region, polyCount),
83
+ originX: view.getFloat32(12, true),
84
+ originZ: view.getFloat32(16, true),
85
+ cellSize: view.getFloat32(20, true),
86
+ };
87
+ /*
88
+ * **Checked again on the way in**, and not because the writer is untrusted: a file arrives from
89
+ * a network, a disk and a bake this reader did not run. An index past the end of a table is the
90
+ * one defect that costs a consumer rather than the file — it walks off the mesh.
91
+ */
92
+ refuseStrayIndices(mesh.polys, vertexCount, 'polygon', 'vertex');
93
+ refuseStrayIndices(mesh.neighbours, polyCount, 'neighbour', 'polygon');
94
+ return mesh;
95
+ }
96
+ function refuseStrayIndices(table, count, what, of) {
97
+ for (let at = 0; at < table.length; at += 1) {
98
+ const index = table[at];
99
+ if (index === -1)
100
+ continue;
101
+ if (index < 0 || index >= count) {
102
+ throw new DrftError(`NAVM's ${what} table names ${of} ${index}, and there are ${count}`);
103
+ }
104
+ }
105
+ }
package/dist/ngrf.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ export type DrftAttribute = number | string | boolean | readonly number[];
2
+ export interface DrftGraphValue {
3
+ readonly name: string;
4
+ readonly shape: readonly number[];
5
+ }
6
+ export interface DrftGraphNode {
7
+ readonly op: string;
8
+ readonly inputs: readonly string[];
9
+ readonly output: string;
10
+ readonly attributes: Readonly<Record<string, DrftAttribute>>;
11
+ }
12
+ export interface DrftGraphTensor {
13
+ readonly name: string;
14
+ readonly shape: readonly number[];
15
+ /** Single-precision values, or half-precision bits as a device uploads them. */
16
+ readonly data: Float32Array | Uint16Array;
17
+ }
18
+ export interface DrftGraph {
19
+ /** Four printable characters naming what the graph is for. */
20
+ readonly role: string;
21
+ readonly inputs: readonly DrftGraphValue[];
22
+ readonly outputs: readonly string[];
23
+ readonly nodes: readonly DrftGraphNode[];
24
+ readonly tensors: readonly DrftGraphTensor[];
25
+ }
26
+ /** Write the chunk. Refuses anything a reader would have to guess about. */
27
+ export declare function buildNgrf(graphs: readonly DrftGraph[]): Uint8Array;
28
+ /** Read the chunk, refusing every structure and offset a reader would otherwise have to trust. */
29
+ export declare function readNgrf(buffer: ArrayBuffer, offset: number, byteLength: number): DrftGraph[];
package/dist/ngrf.js ADDED
@@ -0,0 +1,214 @@
1
+ /**
2
+ * `NGRF` — a network as a graph of operators, and its tensors, in either precision.
3
+ *
4
+ * **`NNET` describes a perceptron and cannot describe anything else**: its entry table has room for
5
+ * inputs, eight hidden widths and outputs, and a transformer is attention, normalisation,
6
+ * convolution and resampling between named values. So a graph is its own chunk, beside `NNET`
7
+ * rather than in place of it — `NNET` stays the compact form for the small networks the texture
8
+ * decode and reconstruction carry.
9
+ *
10
+ * **The chunk carries no semantics.** This package has no dependencies and knows nothing of what
11
+ * `linear` or `attention` compute; the structure is plain data that `@driftengine/texture`'s graph
12
+ * satisfies, and texture validates a graph it is handed, naming any operator it lacks — the
13
+ * arrangement `DTEX` already has.
14
+ *
15
+ * **The structure is UTF-8 JSON and the tensors are bytes.** A graph's structure is small and read
16
+ * once; JSON keeps it legible and lets an attribute be added without a layout change. It is checked
17
+ * field by field on the way in, and every tensor's offset against the payload before one is viewed.
18
+ * The tensors are stored as a device uploads them — `Float32Array` values or half-precision bits —
19
+ * each block aligned to four bytes so it can be viewed rather than copied.
20
+ *
21
+ * ### Layout
22
+ *
23
+ * ```text
24
+ * u32 count
25
+ * count x:
26
+ * u32 role a FourCC naming what the graph is for; unique in the chunk
27
+ * u32 structureBytes
28
+ * u32 payloadBytes
29
+ * structure UTF-8 JSON: inputs, outputs, nodes, and a tensor directory of name,
30
+ * shape, precision (16 or 32) and byte offset into the payload;
31
+ * padded to four bytes
32
+ * payload the tensors, each aligned to four bytes
33
+ * ```
34
+ *
35
+ * **No per-chunk version and additive**, as every chunk here: a reader that does not know the code
36
+ * skips it by its length and loses only the graphs. Added at 1.15.
37
+ */
38
+ import { DrftError, align, fourCC, fourCCName } from './drftFormat.js';
39
+ /** A dimension past this is a corrupt file rather than a model: a product of four of them is 2^64. */
40
+ const MAX_DIMENSION = 1 << 24;
41
+ const encoder = new TextEncoder();
42
+ const decoder = new TextDecoder();
43
+ const count = (shape) => shape.reduce((total, d) => total * d, 1);
44
+ function printableRole(role) {
45
+ return (role.length === 4 && [...role].every((c) => c.charCodeAt(0) >= 0x20 && c.charCodeAt(0) <= 0x7e));
46
+ }
47
+ /** Write the chunk. Refuses anything a reader would have to guess about. */
48
+ export function buildNgrf(graphs) {
49
+ const roles = new Set();
50
+ const parts = [];
51
+ for (const graph of graphs) {
52
+ if (!printableRole(graph.role)) {
53
+ throw new DrftError(`NGRF: the role ${JSON.stringify(graph.role)} is not four printable characters`);
54
+ }
55
+ if (roles.has(graph.role)) {
56
+ throw new DrftError(`NGRF: two graphs have the role ${graph.role}; a reader finds a graph by it`);
57
+ }
58
+ roles.add(graph.role);
59
+ const directory = [];
60
+ let payloadBytes = 0;
61
+ for (const tensor of graph.tensors) {
62
+ if (tensor.data.length !== count(tensor.shape)) {
63
+ throw new DrftError(`NGRF ${graph.role}: tensor "${tensor.name}" holds ${tensor.data.length} values and its ` +
64
+ `shape [${tensor.shape.join(', ')}] needs ${count(tensor.shape)}`);
65
+ }
66
+ const precision = tensor.data instanceof Uint16Array ? 16 : 32;
67
+ directory.push({ name: tensor.name, shape: tensor.shape, precision, offset: payloadBytes });
68
+ payloadBytes += align(tensor.data.byteLength);
69
+ }
70
+ const structure = encoder.encode(JSON.stringify({
71
+ inputs: graph.inputs,
72
+ outputs: graph.outputs,
73
+ nodes: graph.nodes,
74
+ tensors: directory,
75
+ }));
76
+ const payload = new Uint8Array(payloadBytes);
77
+ for (const [i, tensor] of graph.tensors.entries()) {
78
+ payload.set(new Uint8Array(tensor.data.buffer, tensor.data.byteOffset, tensor.data.byteLength), directory[i].offset);
79
+ }
80
+ parts.push({ role: fourCC(graph.role), structure, payload });
81
+ }
82
+ let total = 4;
83
+ for (const part of parts)
84
+ total += 12 + align(part.structure.byteLength) + part.payload.byteLength;
85
+ const bytes = new Uint8Array(total);
86
+ const view = new DataView(bytes.buffer);
87
+ view.setUint32(0, parts.length, true);
88
+ let at = 4;
89
+ for (const part of parts) {
90
+ view.setUint32(at, part.role, true);
91
+ view.setUint32(at + 4, part.structure.byteLength, true);
92
+ view.setUint32(at + 8, part.payload.byteLength, true);
93
+ bytes.set(part.structure, at + 12);
94
+ at += 12 + align(part.structure.byteLength);
95
+ bytes.set(part.payload, at);
96
+ at += part.payload.byteLength;
97
+ }
98
+ return bytes;
99
+ }
100
+ function isShape(value) {
101
+ return (Array.isArray(value) &&
102
+ value.every((d) => Number.isInteger(d) && d > 0 && d <= MAX_DIMENSION));
103
+ }
104
+ function isAttribute(value) {
105
+ return (typeof value === 'number' ||
106
+ typeof value === 'string' ||
107
+ typeof value === 'boolean' ||
108
+ (Array.isArray(value) && value.every((v) => typeof v === 'number')));
109
+ }
110
+ /* The structure, checked field by field: JSON from a file is whatever the file says. */
111
+ function readStructure(label, text) {
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(text);
115
+ }
116
+ catch {
117
+ throw new DrftError(`${label}: the structure is not JSON`);
118
+ }
119
+ const s = parsed;
120
+ const fail = (what) => {
121
+ throw new DrftError(`${label}: ${what}`);
122
+ };
123
+ if (typeof s !== 'object' || s === null)
124
+ fail('the structure is not an object');
125
+ const inputs = s.inputs;
126
+ const outputs = s.outputs;
127
+ const nodes = s.nodes;
128
+ const tensors = s.tensors;
129
+ if (!Array.isArray(inputs) ||
130
+ !inputs.every((i) => typeof i?.name === 'string' && isShape(i?.shape))) {
131
+ fail('inputs must each have a name and a shape');
132
+ }
133
+ if (!Array.isArray(outputs) || !outputs.every((o) => typeof o === 'string'))
134
+ fail('outputs must be names');
135
+ if (!Array.isArray(nodes) ||
136
+ !nodes.every((n) => typeof n?.op === 'string' &&
137
+ typeof n?.output === 'string' &&
138
+ Array.isArray(n?.inputs) &&
139
+ n.inputs.every((i) => typeof i === 'string') &&
140
+ typeof n?.attributes === 'object' &&
141
+ n.attributes !== null &&
142
+ Object.values(n.attributes).every(isAttribute))) {
143
+ fail('nodes must each have an operator, input names, an output name and attributes');
144
+ }
145
+ if (!Array.isArray(tensors) ||
146
+ !tensors.every((t) => typeof t?.name === 'string' &&
147
+ isShape(t?.shape) &&
148
+ (t?.precision === 16 || t?.precision === 32) &&
149
+ Number.isInteger(t?.offset) &&
150
+ t.offset >= 0 &&
151
+ t.offset % 4 === 0)) {
152
+ fail('tensors must each have a name, a shape, a precision of 16 or 32 and an aligned offset');
153
+ }
154
+ return {
155
+ inputs: inputs,
156
+ outputs: outputs,
157
+ nodes: nodes,
158
+ tensors: tensors,
159
+ };
160
+ }
161
+ /** Read the chunk, refusing every structure and offset a reader would otherwise have to trust. */
162
+ export function readNgrf(buffer, offset, byteLength) {
163
+ if (byteLength < 4)
164
+ throw new DrftError('NGRF is too short to hold its count');
165
+ const view = new DataView(buffer, offset, byteLength);
166
+ const graphCount = view.getUint32(0, true);
167
+ const graphs = [];
168
+ const roles = new Set();
169
+ let at = 4;
170
+ for (let g = 0; g < graphCount; g += 1) {
171
+ if (at + 12 > byteLength)
172
+ throw new DrftError(`NGRF graph ${g} runs past the chunk`);
173
+ const role = fourCCName(view.getUint32(at, true));
174
+ const label = `NGRF ${role}`;
175
+ if (roles.has(role))
176
+ throw new DrftError(`${label} repeats a role; a reader finds a graph by it`);
177
+ roles.add(role);
178
+ const structureBytes = view.getUint32(at + 4, true);
179
+ const payloadBytes = view.getUint32(at + 8, true);
180
+ const structureAt = at + 12;
181
+ const payloadAt = structureAt + align(structureBytes);
182
+ if (payloadAt + payloadBytes > byteLength) {
183
+ throw new DrftError(`${label}: its structure and payload run past the chunk`);
184
+ }
185
+ const structure = readStructure(label, decoder.decode(new Uint8Array(buffer, offset + structureAt, structureBytes)));
186
+ const tensors = [];
187
+ for (const entry of structure.tensors) {
188
+ const bytes = count(entry.shape) * (entry.precision / 8);
189
+ if (entry.offset + bytes > payloadBytes) {
190
+ throw new DrftError(`${label}: tensor "${entry.name}" needs ${bytes} bytes at ${entry.offset} and the payload has ${payloadBytes}`);
191
+ }
192
+ const absolute = offset + payloadAt + entry.offset;
193
+ const length = count(entry.shape);
194
+ /* Viewed where the file's alignment allows, and copied where it does not. */
195
+ const data = entry.precision === 32
196
+ ? absolute % 4 === 0
197
+ ? new Float32Array(buffer, absolute, length)
198
+ : new Float32Array(buffer.slice(absolute, absolute + bytes))
199
+ : absolute % 2 === 0
200
+ ? new Uint16Array(buffer, absolute, length)
201
+ : new Uint16Array(buffer.slice(absolute, absolute + bytes));
202
+ tensors.push({ name: entry.name, shape: entry.shape, data });
203
+ }
204
+ graphs.push({
205
+ role,
206
+ inputs: structure.inputs,
207
+ outputs: structure.outputs,
208
+ nodes: structure.nodes,
209
+ tensors,
210
+ });
211
+ at = payloadAt + payloadBytes;
212
+ }
213
+ return graphs;
214
+ }
package/dist/nnet.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `NNET` — the weights of the small networks the engine evaluates, in either precision.
3
+ *
4
+ * **One chunk for every network a file carries, each found by its role.** A role is a FourCC the
5
+ * consumer chooses — reconstruction's refinement tier, a capture stage's prior — so a file can carry
6
+ * several and a reader asks for the one it wants rather than counting. Two networks with one role
7
+ * would make that question ambiguous, so the writer and the reader both refuse them.
8
+ *
9
+ * **The shape is carried, and the weight count has to agree with it.** A network whose weights are
10
+ * all present and whose shape says they are laid out differently is the corruption that reads as a
11
+ * working file: every weight lands in some layer, the evaluator runs, and the output is wrong
12
+ * everywhere. The layout is `@driftengine/texture`'s `evalNetwork`: for each layer, the weight
13
+ * matrix row by row by output, then the biases. `nnetWeightCount` is that formula, and
14
+ * `@driftengine/assets` holds it equal to `networkWeightCount` — the two packages cannot import one
15
+ * another, so a test that can import both is what keeps them one convention.
16
+ *
17
+ * **Sixteen or thirty-two bits, per network.** Half precision is what a device with `shader-f16`
18
+ * runs, and the bits are stored as they will be uploaded rather than converted here — `halfWeights`
19
+ * in `@driftengine/texture` is the one place a float becomes half-precision bits.
20
+ *
21
+ * **No per-chunk version field**, matching `SDFV`, `MSHL`, `DTEX` and every other chunk here, and
22
+ * **additive**: a reader that does not know the code skips it by its length and loses only the
23
+ * networks, which it had nothing to evaluate them with anyway.
24
+ *
25
+ * ### Layout
26
+ *
27
+ * ```text
28
+ * u32 count
29
+ * count x NNET_ENTRY_BYTES:
30
+ * u32 role a FourCC naming what the network is for
31
+ * u32 inputs
32
+ * u32 outputs
33
+ * u32 hiddenCount at most NNET_MAX_HIDDEN
34
+ * u32 hidden[8] widths; the slots past hiddenCount are zero
35
+ * u32 precision 16 or 32
36
+ * u32 weightCount
37
+ * weights every network's weights back to back, in entry order: four bytes each at
38
+ * 32 bits, two at 16, each block padded to a four-byte boundary
39
+ * ```
40
+ *
41
+ * **The padding is what lets every block be viewed rather than copied.** An odd number of
42
+ * sixteen-bit weights ends two bytes short of a boundary, and a `Float32Array` cannot be taken at an
43
+ * offset that is not a multiple of four.
44
+ */
45
+ /** Role, three counts, eight widths, precision and weight count. */
46
+ export declare const NNET_ENTRY_BYTES = 56;
47
+ /** Hidden layers the entry table has room for. */
48
+ export declare const NNET_MAX_HIDDEN = 8;
49
+ /**
50
+ * The widest layer a network may declare.
51
+ *
52
+ * **A bound on the arithmetic, not on ambition.** A width is a `u32` read out of a file and two of
53
+ * them multiply into a weight count, so a corrupt pair asks for an allocation the size of the
54
+ * address space. 4,096 is far past anything the device evaluates — its widest is sixteen — and
55
+ * keeps every product of two widths comfortably inside the integers a double holds exactly.
56
+ */
57
+ export declare const NNET_MAX_WIDTH = 4096;
58
+ export interface DrftNetwork {
59
+ /** Four printable characters naming what the network is for. */
60
+ readonly role: string;
61
+ readonly inputs: number;
62
+ /** Hidden layer widths, input side first. At most `NNET_MAX_HIDDEN`. */
63
+ readonly hidden: readonly number[];
64
+ readonly outputs: number;
65
+ /** Single-precision weights, or half-precision bits as the device uploads them. */
66
+ readonly weights: Float32Array | Uint16Array;
67
+ }
68
+ export interface DrftNnet {
69
+ readonly networks: readonly DrftNetwork[];
70
+ }
71
+ /** How many weights and biases a shape needs, in `evalNetwork`'s layout. */
72
+ export declare function nnetWeightCount(inputs: number, hidden: readonly number[], outputs: number): number;
73
+ /** Write the chunk. Refuses anything a reader would have to guess about. */
74
+ export declare function buildNnet(nnet: DrftNnet): Uint8Array;
75
+ /** Read the chunk, refusing every shape a reader would otherwise have to trust. */
76
+ export declare function readNnet(buffer: ArrayBuffer, offset: number, byteLength: number): DrftNnet;