@driftengine/drft 3.63.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/NOTICE +20 -0
- package/README.md +1 -1
- package/dist/drftFormat.d.ts +52 -1
- package/dist/drftFormat.js +59 -1
- package/dist/drftMeshlets.d.ts +52 -0
- package/dist/drftMeshlets.js +140 -0
- package/dist/drftRead.d.ts +47 -0
- package/dist/drftRead.js +37 -3
- package/dist/drftWrite.d.ts +48 -0
- package/dist/drftWrite.js +59 -3
- package/dist/dtex.d.ts +127 -0
- package/dist/dtex.js +324 -0
- package/dist/ents.d.ts +47 -0
- package/dist/ents.js +60 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +22 -0
- package/dist/navm.d.ts +58 -0
- package/dist/navm.js +105 -0
- package/dist/ngrf.d.ts +29 -0
- package/dist/ngrf.js +214 -0
- package/dist/nnet.d.ts +76 -0
- package/dist/nnet.js +194 -0
- package/dist/sdfv.d.ts +71 -0
- package/dist/sdfv.js +142 -0
- package/package.json +4 -2
- package/src/drftFormat.ts +66 -1
- package/src/drftMeshlets.ts +185 -0
- package/src/drftRead.ts +82 -2
- package/src/drftWrite.ts +112 -2
- package/src/dtex.ts +404 -0
- package/src/ents.ts +105 -0
- package/src/index.ts +43 -0
- package/src/navm.ts +171 -0
- package/src/ngrf.ts +299 -0
- package/src/nnet.ts +250 -0
- package/src/sdfv.ts +178 -0
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;
|