@woosh/meep-engine 3.10.0 → 3.11.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 (142) hide show
  1. package/README.md +168 -152
  2. package/package.json +1 -1
  3. package/src/REVIEW_2026_08_06.md +610 -610
  4. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  5. package/src/core/geom/3d/shape/ConvexHullShape3D.js +28 -4
  6. package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts +26 -0
  7. package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts.map +1 -1
  8. package/src/engine/graphics/render/frame_graph/FrameGraph.js +47 -0
  9. package/src/engine/physics/ecs/PhysicsSystem.d.ts +0 -3
  10. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  11. package/src/engine/physics/ecs/PhysicsSystem.js +0 -3
  12. package/src/shade/device/ShadeGPUCommandContext.d.ts +21 -0
  13. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  14. package/src/shade/device/ShadeGPUCommandContext.js +940 -749
  15. package/src/shade/device/timing/GPUTimerArray.d.ts +22 -4
  16. package/src/shade/device/timing/GPUTimerArray.d.ts.map +1 -1
  17. package/src/shade/device/timing/GPUTimerArray.js +88 -14
  18. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +349 -98
  19. package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts +93 -0
  20. package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts.map +1 -0
  21. package/src/shade/device/timing/profile/GPUFrameRecorder.js +297 -0
  22. package/src/shade/device/timing/profile/GPUProfileCapture.d.ts +62 -0
  23. package/src/shade/device/timing/profile/GPUProfileCapture.d.ts.map +1 -0
  24. package/src/shade/device/timing/profile/GPUProfileCapture.js +77 -0
  25. package/src/shade/device/timing/profile/GPUProfileFrame.d.ts +73 -0
  26. package/src/shade/device/timing/profile/GPUProfileFrame.d.ts.map +1 -0
  27. package/src/shade/device/timing/profile/GPUProfileFrame.js +88 -0
  28. package/src/shade/device/timing/profile/GPUProfileLevel.d.ts +20 -0
  29. package/src/shade/device/timing/profile/GPUProfileLevel.d.ts.map +1 -0
  30. package/src/shade/device/timing/profile/GPUProfileLevel.js +50 -0
  31. package/src/shade/device/timing/profile/GPUProfileMeta.d.ts +63 -0
  32. package/src/shade/device/timing/profile/GPUProfileMeta.d.ts.map +1 -0
  33. package/src/shade/device/timing/profile/GPUProfileMeta.js +73 -0
  34. package/src/shade/device/timing/profile/GPUProfileSession.d.ts +173 -0
  35. package/src/shade/device/timing/profile/GPUProfileSession.d.ts.map +1 -0
  36. package/src/shade/device/timing/profile/GPUProfileSession.js +470 -0
  37. package/src/shade/device/timing/profile/GPUProfileSpan.d.ts +60 -0
  38. package/src/shade/device/timing/profile/GPUProfileSpan.d.ts.map +1 -0
  39. package/src/shade/device/timing/profile/GPUProfileSpan.js +65 -0
  40. package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts +19 -0
  41. package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts.map +1 -0
  42. package/src/shade/device/timing/profile/GPUProfileSpanKind.js +25 -0
  43. package/src/shade/device/timing/profile/GPUProfileTopology.d.ts +159 -0
  44. package/src/shade/device/timing/profile/GPUProfileTopology.d.ts.map +1 -0
  45. package/src/shade/device/timing/profile/GPUProfileTopology.js +202 -0
  46. package/src/shade/device/timing/profile/GPUProfileWork.d.ts +90 -0
  47. package/src/shade/device/timing/profile/GPUProfileWork.d.ts.map +1 -0
  48. package/src/shade/device/timing/profile/GPUProfileWork.js +115 -0
  49. package/src/shade/device/timing/profile/SGPTDefect.d.ts +43 -0
  50. package/src/shade/device/timing/profile/SGPTDefect.d.ts.map +1 -0
  51. package/src/shade/device/timing/profile/SGPTDefect.js +65 -0
  52. package/src/shade/device/timing/profile/SGPTHeader.d.ts +38 -0
  53. package/src/shade/device/timing/profile/SGPTHeader.d.ts.map +1 -0
  54. package/src/shade/device/timing/profile/SGPTHeader.js +46 -0
  55. package/src/shade/device/timing/profile/SGPTRecordRef.d.ts +37 -0
  56. package/src/shade/device/timing/profile/SGPTRecordRef.d.ts.map +1 -0
  57. package/src/shade/device/timing/profile/SGPTRecordRef.js +45 -0
  58. package/src/shade/device/timing/profile/SGPTWriteStream.d.ts +103 -0
  59. package/src/shade/device/timing/profile/SGPTWriteStream.d.ts.map +1 -0
  60. package/src/shade/device/timing/profile/SGPTWriteStream.js +287 -0
  61. package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts +19 -0
  62. package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts.map +1 -0
  63. package/src/shade/device/timing/profile/SGPT_DEFECT.js +26 -0
  64. package/src/shade/device/timing/profile/SGPT_FLAG.d.ts +8 -0
  65. package/src/shade/device/timing/profile/SGPT_FLAG.d.ts.map +1 -0
  66. package/src/shade/device/timing/profile/SGPT_FLAG.js +15 -0
  67. package/src/shade/device/timing/profile/SGPT_FORMAT.md +345 -0
  68. package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts +10 -0
  69. package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts.map +1 -0
  70. package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.js +9 -0
  71. package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts +17 -0
  72. package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts.map +1 -0
  73. package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.js +16 -0
  74. package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts +16 -0
  75. package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts.map +1 -0
  76. package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.js +15 -0
  77. package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts +15 -0
  78. package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts.map +1 -0
  79. package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.js +29 -0
  80. package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts +7 -0
  81. package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts.map +1 -0
  82. package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.js +6 -0
  83. package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts +7 -0
  84. package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts.map +1 -0
  85. package/src/shade/device/timing/profile/SGPT_MAGIC.js +6 -0
  86. package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts +16 -0
  87. package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts.map +1 -0
  88. package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.js +15 -0
  89. package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts +7 -0
  90. package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts.map +1 -0
  91. package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.js +6 -0
  92. package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts +13 -0
  93. package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts.map +1 -0
  94. package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.js +23 -0
  95. package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts +12 -0
  96. package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts.map +1 -0
  97. package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.js +11 -0
  98. package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts +18 -0
  99. package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts.map +1 -0
  100. package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.js +46 -0
  101. package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts +20 -0
  102. package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts.map +1 -0
  103. package/src/shade/device/timing/profile/frame_graph_extract_topology.js +174 -0
  104. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts +21 -0
  105. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts.map +1 -0
  106. package/src/shade/device/timing/profile/make_profiling_pass_encoder.js +114 -0
  107. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts +20 -0
  108. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts.map +1 -0
  109. package/src/shade/device/timing/profile/parse_workgroup_size.js +43 -0
  110. package/src/shade/device/timing/profile/sgpt_fourcc.d.ts +18 -0
  111. package/src/shade/device/timing/profile/sgpt_fourcc.d.ts.map +1 -0
  112. package/src/shade/device/timing/profile/sgpt_fourcc.js +34 -0
  113. package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts +42 -0
  114. package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts.map +1 -0
  115. package/src/shade/device/timing/profile/sgpt_frame_codec.js +172 -0
  116. package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts +20 -0
  117. package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts.map +1 -0
  118. package/src/shade/device/timing/profile/sgpt_meta_codec.js +65 -0
  119. package/src/shade/device/timing/profile/sgpt_read_capture.d.ts +19 -0
  120. package/src/shade/device/timing/profile/sgpt_read_capture.d.ts.map +1 -0
  121. package/src/shade/device/timing/profile/sgpt_read_capture.js +134 -0
  122. package/src/shade/device/timing/profile/sgpt_read_header.d.ts +22 -0
  123. package/src/shade/device/timing/profile/sgpt_read_header.d.ts.map +1 -0
  124. package/src/shade/device/timing/profile/sgpt_read_header.js +94 -0
  125. package/src/shade/device/timing/profile/sgpt_read_records.d.ts +27 -0
  126. package/src/shade/device/timing/profile/sgpt_read_records.d.ts.map +1 -0
  127. package/src/shade/device/timing/profile/sgpt_read_records.js +153 -0
  128. package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts +54 -0
  129. package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts.map +1 -0
  130. package/src/shade/device/timing/profile/sgpt_topology_codec.js +329 -0
  131. package/src/shade/device/timing/profile/sgpt_write_header.d.ts +22 -0
  132. package/src/shade/device/timing/profile/sgpt_write_header.d.ts.map +1 -0
  133. package/src/shade/device/timing/profile/sgpt_write_header.js +47 -0
  134. package/src/shade/device/timing/profile/sgpt_write_record.d.ts +23 -0
  135. package/src/shade/device/timing/profile/sgpt_write_record.d.ts.map +1 -0
  136. package/src/shade/device/timing/profile/sgpt_write_record.js +62 -0
  137. package/src/shade/renderer/Renderer.d.ts +21 -0
  138. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  139. package/src/shade/renderer/Renderer.js +2513 -2455
  140. package/src/engine/save/GameStateLoader.d.ts +0 -49
  141. package/src/engine/save/GameStateLoader.d.ts.map +0 -1
  142. package/src/engine/save/GameStateLoader.js +0 -168
@@ -0,0 +1,153 @@
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { crc32 } from "../../../../core/binary/hash/crc32.js";
3
+ import { SGPT_DEFECT } from "./SGPT_DEFECT.js";
4
+ import { SGPTDefect } from "./SGPTDefect.js";
5
+ import { SGPT_HEADER_SIZE } from "./SGPT_HEADER_SIZE.js";
6
+ import { SGPT_RECORD_HEADER_SIZE } from "./SGPT_RECORD_HEADER_SIZE.js";
7
+ import { SGPT_RECORD_SYNC } from "./SGPT_RECORD_SYNC.js";
8
+ import { SGPTRecordRef } from "./SGPTRecordRef.js";
9
+
10
+ /**
11
+ * Sync marker as four bytes, in the order they appear on the wire. Compared byte-wise during a
12
+ * scan so that resynchronisation does not depend on alignment — corruption does not politely
13
+ * truncate to a multiple of four.
14
+ *
15
+ * @type {number[]}
16
+ */
17
+ const SYNC_BYTES = [
18
+ SGPT_RECORD_SYNC & 0xff,
19
+ (SGPT_RECORD_SYNC >>> 8) & 0xff,
20
+ (SGPT_RECORD_SYNC >>> 16) & 0xff,
21
+ (SGPT_RECORD_SYNC >>> 24) & 0xff
22
+ ];
23
+
24
+ /**
25
+ * Whether a well-formed, checksum-clean record starts at `offset`.
26
+ *
27
+ * All four conditions are checked together on purpose. The sync marker alone is a four-byte
28
+ * coincidence away from a false positive — a `SREC` in a pass label would do it — and accepting one
29
+ * mid-scan would resynchronise onto garbage and desynchronise everything after. The length has to
30
+ * fit and the payload has to checksum before a candidate counts.
31
+ *
32
+ * @param {Uint8Array} bytes
33
+ * @param {DataView} view over the same bytes, hoisted because the resync scan calls this per byte
34
+ * @param {number} offset
35
+ * @param {number} end
36
+ * @returns {SGPTRecordRef|null}
37
+ */
38
+ function try_read_record_at(bytes, view, offset, end) {
39
+ if (offset + SGPT_RECORD_HEADER_SIZE > end) {
40
+ return null;
41
+ }
42
+
43
+ for (let i = 0; i < 4; i++) {
44
+ if (bytes[offset + i] !== SYNC_BYTES[i]) {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ const type = view.getUint32(offset + 4, true);
50
+ const payload_byte_length = view.getUint32(offset + 8, true);
51
+ const stored_checksum = view.getUint32(offset + 12, true);
52
+
53
+ const payload_offset = offset + SGPT_RECORD_HEADER_SIZE;
54
+
55
+ if (payload_offset + payload_byte_length > end) {
56
+ return null;
57
+ }
58
+
59
+ if (crc32(bytes, payload_offset, payload_byte_length) !== stored_checksum) {
60
+ return null;
61
+ }
62
+
63
+ return new SGPTRecordRef(type, offset, payload_offset, payload_byte_length);
64
+ }
65
+
66
+ /**
67
+ * Walk the record stream, resynchronising past anything that is not a record.
68
+ *
69
+ * The stream is designed to be read this way rather than through the directory, and the directory
70
+ * is an index over the result rather than a prerequisite for it. A capture that ended when the tab
71
+ * did has no directory at all, and it is exactly the capture worth reading.
72
+ *
73
+ * Two recoverable outcomes are reported and kept going:
74
+ *
75
+ * - **Resync.** Bytes that are not a record are skipped and a {@link SGPT_DEFECT.RESYNC} names how
76
+ * many. Everything after the damage is still decoded.
77
+ * - **Truncation.** The stream ends inside a record. A {@link SGPT_DEFECT.TRUNCATED} says so, and
78
+ * the records before it stand. This is the ordinary end state of an interrupted session, not a
79
+ * sign that anything went wrong with the writing.
80
+ *
81
+ * @param {BinaryBuffer} buffer
82
+ * @param {number} end offset one past the last readable byte
83
+ * @param {SGPTDefect[]} defects appended to
84
+ * @returns {SGPTRecordRef[]} in stream order
85
+ *
86
+ * @author Alex Goldring
87
+ * @copyright Company Named Limited (c) 2026
88
+ */
89
+ export function sgpt_read_records(buffer, end, defects) {
90
+ assert.defined(buffer, 'buffer');
91
+ assert.isNonNegativeInteger(end, 'end');
92
+ assert.isArray(defects, 'defects');
93
+
94
+ const bytes = buffer.raw_bytes;
95
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
96
+
97
+ const result = [];
98
+
99
+ let offset = SGPT_HEADER_SIZE;
100
+
101
+ while (offset < end) {
102
+
103
+ const record = try_read_record_at(bytes, view, offset, end);
104
+
105
+ if (record !== null) {
106
+ result.push(record);
107
+
108
+ offset = record.payload_offset + record.payload_byte_length;
109
+
110
+ continue;
111
+ }
112
+
113
+ // Nothing valid here. Scan for somewhere that is.
114
+ const damaged_from = offset;
115
+
116
+ let scan = offset + 1;
117
+ let recovered = null;
118
+
119
+ while (scan < end) {
120
+ recovered = try_read_record_at(bytes, view, scan, end);
121
+
122
+ if (recovered !== null) {
123
+ break;
124
+ }
125
+
126
+ scan++;
127
+ }
128
+
129
+ if (recovered === null) {
130
+ // Ran out without finding another record. Everything from here on is either a partial
131
+ // final record or trailing damage; either way there is nothing more to decode.
132
+ defects.push(new SGPTDefect(
133
+ SGPT_DEFECT.TRUNCATED,
134
+ `stream ends inside a record; ${end - damaged_from} unreadable bytes at the tail`,
135
+ damaged_from
136
+ ));
137
+
138
+ break;
139
+ }
140
+
141
+ defects.push(new SGPTDefect(
142
+ SGPT_DEFECT.RESYNC,
143
+ `skipped ${scan - damaged_from} bytes that are not a record`,
144
+ damaged_from
145
+ ));
146
+
147
+ result.push(recovered);
148
+
149
+ offset = recovered.payload_offset + recovered.payload_byte_length;
150
+ }
151
+
152
+ return result;
153
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Encode a `TOPO` payload.
3
+ *
4
+ * @param {BinaryBuffer} buffer
5
+ * @param {GPUProfileTopology} topology
6
+ * @param {function(string): number} intern
7
+ * @returns {void}
8
+ *
9
+ * @author Alex Goldring
10
+ * @copyright Company Named Limited (c) 2026
11
+ */
12
+ export function sgpt_write_topology(buffer: BinaryBuffer, topology: GPUProfileTopology, intern: (arg0: string) => number): void;
13
+ /**
14
+ * Every string a topology will need interned, in the order {@link sgpt_write_topology} asks for
15
+ * them.
16
+ *
17
+ * Exists because symbols must be flushed into a `SYMS` record *before* the record referencing them
18
+ * — interning from inside the payload writer puts them in the next flush, behind the record that
19
+ * needs them, and a capture truncated in between resolves none of them.
20
+ *
21
+ * @param {GPUProfileTopology} topology
22
+ * @param {function(string): number} intern
23
+ * @returns {void}
24
+ */
25
+ export function sgpt_intern_topology_symbols(topology: GPUProfileTopology, intern: (arg0: string) => number): void;
26
+ /**
27
+ * Decode a `TOPO` payload.
28
+ *
29
+ * @param {BinaryBuffer} buffer positioned at the payload
30
+ * @param {string[]} symbols
31
+ * @returns {GPUProfileTopology}
32
+ */
33
+ export function sgpt_read_topology(buffer: BinaryBuffer, symbols: string[]): GPUProfileTopology;
34
+ /**
35
+ * A content hash of everything that makes one topology different from another.
36
+ *
37
+ * This is what deduplication turns on. A Shade frame graph is nearly identical frame to frame, so
38
+ * the steady state hashes to one value for hundreds of frames and the structure is stored once —
39
+ * the difference between a capture of tens of megabytes and one of a couple.
40
+ *
41
+ * **Cull state is part of the hash.** A frame where a different set of passes was culled genuinely
42
+ * is a different shape, and folding the two together would report passes as having run in frames
43
+ * where they did not.
44
+ *
45
+ * FNV-1a over a canonical string. Not cryptographic and does not need to be: a collision costs a
46
+ * mislabelled structure in a debug tool, and the inputs are pass names and integers rather than
47
+ * anything adversarial.
48
+ *
49
+ * @param {GPUProfileTopology} topology
50
+ * @returns {number} unsigned 32-bit
51
+ */
52
+ export function sgpt_topology_hash(topology: GPUProfileTopology): number;
53
+ import { GPUProfileTopology } from "./GPUProfileTopology.js";
54
+ //# sourceMappingURL=sgpt_topology_codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sgpt_topology_codec.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/device/timing/profile/sgpt_topology_codec.js"],"names":[],"mappings":"AA6DA;;;;;;;;;;GAUG;AACH,oEAPW,kBAAkB,iBACT,MAAM,KAAG,MAAM,GACtB,IAAI,CAiFhB;AAED;;;;;;;;;;;GAWG;AACH,uDAJW,kBAAkB,iBACT,MAAM,KAAG,MAAM,GACtB,IAAI,CAqBhB;AAED;;;;;;GAMG;AACH,kEAHW,MAAM,EAAE,GACN,kBAAkB,CA2F9B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,6CAHW,kBAAkB,GAChB,MAAM,CA+BlB;mCAjUM,yBAAyB"}
@@ -0,0 +1,329 @@
1
+ import { assert } from "../../../../core/assert.js";
2
+ import {
3
+ GPUProfilePass,
4
+ GPUProfileResource,
5
+ GPUProfileResourceKind,
6
+ GPUProfileScope,
7
+ GPUProfileTopology
8
+ } from "./GPUProfileTopology.js";
9
+
10
+ const FLAG_CULLED = 1 << 0;
11
+ const FLAG_SIDE_EFFECTS = 1 << 1;
12
+ const FLAG_TRANSIENT = 1 << 2;
13
+ const FLAG_IMPORTED = 1 << 3;
14
+
15
+ /**
16
+ * `-1` means "none" throughout the topology — no scope, no producer. Shifting by one lets those
17
+ * ride in a `uintVar`, which has no negative encoding.
18
+ *
19
+ * @param {BinaryBuffer} buffer
20
+ * @param {number} value
21
+ */
22
+ function write_optional(buffer, value) {
23
+ buffer.writeUintVar(value + 1);
24
+ }
25
+
26
+ /**
27
+ * @param {BinaryBuffer} buffer
28
+ * @returns {number}
29
+ */
30
+ function read_optional(buffer) {
31
+ return buffer.readUintVar() - 1;
32
+ }
33
+
34
+ /**
35
+ * @param {BinaryBuffer} buffer
36
+ * @param {number[]} ids
37
+ */
38
+ function write_ids(buffer, ids) {
39
+ buffer.writeUintVar(ids.length);
40
+
41
+ for (const id of ids) {
42
+ buffer.writeUintVar(id);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * @param {BinaryBuffer} buffer
48
+ * @returns {number[]}
49
+ */
50
+ function read_ids(buffer) {
51
+ const count = buffer.readUintVar();
52
+
53
+ const ids = [];
54
+
55
+ for (let i = 0; i < count; i++) {
56
+ ids.push(buffer.readUintVar());
57
+ }
58
+
59
+ return ids;
60
+ }
61
+
62
+ /**
63
+ * Encode a `TOPO` payload.
64
+ *
65
+ * @param {BinaryBuffer} buffer
66
+ * @param {GPUProfileTopology} topology
67
+ * @param {function(string): number} intern
68
+ * @returns {void}
69
+ *
70
+ * @author Alex Goldring
71
+ * @copyright Company Named Limited (c) 2026
72
+ */
73
+ export function sgpt_write_topology(buffer, topology, intern) {
74
+ assert.defined(buffer, 'buffer');
75
+ assert.defined(topology, 'topology');
76
+ assert.isFunction(intern, 'intern');
77
+
78
+ buffer.writeUintVar(topology.id);
79
+ buffer.writeUintVar(intern(topology.name));
80
+
81
+ buffer.writeUintVar(topology.scopes.length);
82
+
83
+ for (const scope of topology.scopes) {
84
+ buffer.writeUintVar(intern(scope.name));
85
+ write_optional(buffer, scope.parent);
86
+ }
87
+
88
+ buffer.writeUintVar(topology.passes.length);
89
+
90
+ for (const pass of topology.passes) {
91
+ buffer.writeUintVar(pass.id);
92
+ buffer.writeUintVar(intern(pass.name));
93
+ write_optional(buffer, pass.scope);
94
+
95
+ let flags = 0;
96
+
97
+ if (pass.culled) {
98
+ flags |= FLAG_CULLED;
99
+ }
100
+
101
+ if (pass.has_side_effects) {
102
+ flags |= FLAG_SIDE_EFFECTS;
103
+ }
104
+
105
+ buffer.writeUint8(flags);
106
+ buffer.writeUintVar(pass.ref_count);
107
+
108
+ write_ids(buffer, pass.reads);
109
+ write_ids(buffer, pass.writes);
110
+ write_ids(buffer, pass.creates);
111
+ }
112
+
113
+ buffer.writeUintVar(topology.resources.length);
114
+
115
+ for (const resource of topology.resources) {
116
+ buffer.writeUintVar(resource.id);
117
+ buffer.writeUintVar(intern(resource.name));
118
+ buffer.writeUintVar(resource.version);
119
+
120
+ let flags = 0;
121
+
122
+ if (resource.transient) {
123
+ flags |= FLAG_TRANSIENT;
124
+ }
125
+
126
+ if (resource.imported) {
127
+ flags |= FLAG_IMPORTED;
128
+ }
129
+
130
+ buffer.writeUint8(flags);
131
+ write_optional(buffer, resource.producer);
132
+
133
+ buffer.writeUint8(resource.kind);
134
+ buffer.writeUintVar(resource.usage);
135
+ buffer.writeUintVar(resource.byte_size);
136
+
137
+ if (resource.kind === GPUProfileResourceKind.BUFFER) {
138
+ buffer.writeUintVar(resource.size);
139
+ } else if (resource.kind === GPUProfileResourceKind.TEXTURE) {
140
+ buffer.writeUintVar(resource.resolution[0]);
141
+ buffer.writeUintVar(resource.resolution[1]);
142
+ buffer.writeUintVar(resource.resolution[2]);
143
+ buffer.writeUintVar(resource.mip_level_count);
144
+ buffer.writeUintVar(resource.sample_count);
145
+ buffer.writeUintVar(intern(resource.format));
146
+ buffer.writeUintVar(intern(resource.dimension));
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Every string a topology will need interned, in the order {@link sgpt_write_topology} asks for
153
+ * them.
154
+ *
155
+ * Exists because symbols must be flushed into a `SYMS` record *before* the record referencing them
156
+ * — interning from inside the payload writer puts them in the next flush, behind the record that
157
+ * needs them, and a capture truncated in between resolves none of them.
158
+ *
159
+ * @param {GPUProfileTopology} topology
160
+ * @param {function(string): number} intern
161
+ * @returns {void}
162
+ */
163
+ export function sgpt_intern_topology_symbols(topology, intern) {
164
+ intern(topology.name);
165
+
166
+ for (const scope of topology.scopes) {
167
+ intern(scope.name);
168
+ }
169
+
170
+ for (const pass of topology.passes) {
171
+ intern(pass.name);
172
+ }
173
+
174
+ for (const resource of topology.resources) {
175
+ intern(resource.name);
176
+
177
+ if (resource.kind === GPUProfileResourceKind.TEXTURE) {
178
+ intern(resource.format);
179
+ intern(resource.dimension);
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Decode a `TOPO` payload.
186
+ *
187
+ * @param {BinaryBuffer} buffer positioned at the payload
188
+ * @param {string[]} symbols
189
+ * @returns {GPUProfileTopology}
190
+ */
191
+ export function sgpt_read_topology(buffer, symbols) {
192
+ assert.defined(buffer, 'buffer');
193
+ assert.isArray(symbols, 'symbols');
194
+
195
+ /**
196
+ * @param {number} index
197
+ * @returns {string}
198
+ */
199
+ const resolve = (index) => symbols[index] !== undefined
200
+ ? symbols[index]
201
+ : `<unresolved symbol ${index}>`;
202
+
203
+ const topology = new GPUProfileTopology();
204
+
205
+ topology.id = buffer.readUintVar();
206
+ topology.name = resolve(buffer.readUintVar());
207
+
208
+ const scope_count = buffer.readUintVar();
209
+
210
+ for (let i = 0; i < scope_count; i++) {
211
+ const scope = new GPUProfileScope();
212
+
213
+ scope.name = resolve(buffer.readUintVar());
214
+ scope.parent = read_optional(buffer);
215
+
216
+ topology.scopes.push(scope);
217
+ }
218
+
219
+ const pass_count = buffer.readUintVar();
220
+
221
+ for (let i = 0; i < pass_count; i++) {
222
+ const pass = new GPUProfilePass();
223
+
224
+ pass.id = buffer.readUintVar();
225
+ pass.name = resolve(buffer.readUintVar());
226
+ pass.scope = read_optional(buffer);
227
+
228
+ const flags = buffer.readUint8();
229
+
230
+ pass.culled = (flags & FLAG_CULLED) !== 0;
231
+ pass.has_side_effects = (flags & FLAG_SIDE_EFFECTS) !== 0;
232
+
233
+ pass.ref_count = buffer.readUintVar();
234
+
235
+ pass.reads = read_ids(buffer);
236
+ pass.writes = read_ids(buffer);
237
+ pass.creates = read_ids(buffer);
238
+
239
+ topology.passes.push(pass);
240
+ }
241
+
242
+ const resource_count = buffer.readUintVar();
243
+
244
+ for (let i = 0; i < resource_count; i++) {
245
+ const resource = new GPUProfileResource();
246
+
247
+ resource.id = buffer.readUintVar();
248
+ resource.name = resolve(buffer.readUintVar());
249
+ resource.version = buffer.readUintVar();
250
+
251
+ const flags = buffer.readUint8();
252
+
253
+ resource.transient = (flags & FLAG_TRANSIENT) !== 0;
254
+ resource.imported = (flags & FLAG_IMPORTED) !== 0;
255
+
256
+ resource.producer = read_optional(buffer);
257
+
258
+ resource.kind = buffer.readUint8();
259
+ resource.usage = buffer.readUintVar();
260
+ resource.byte_size = buffer.readUintVar();
261
+
262
+ if (resource.kind === GPUProfileResourceKind.BUFFER) {
263
+ resource.size = buffer.readUintVar();
264
+ } else if (resource.kind === GPUProfileResourceKind.TEXTURE) {
265
+ resource.resolution = [
266
+ buffer.readUintVar(),
267
+ buffer.readUintVar(),
268
+ buffer.readUintVar()
269
+ ];
270
+ resource.mip_level_count = buffer.readUintVar();
271
+ resource.sample_count = buffer.readUintVar();
272
+ resource.format = resolve(buffer.readUintVar());
273
+ resource.dimension = resolve(buffer.readUintVar());
274
+ }
275
+
276
+ topology.resources.push(resource);
277
+ }
278
+
279
+ return topology;
280
+ }
281
+
282
+ /**
283
+ * A content hash of everything that makes one topology different from another.
284
+ *
285
+ * This is what deduplication turns on. A Shade frame graph is nearly identical frame to frame, so
286
+ * the steady state hashes to one value for hundreds of frames and the structure is stored once —
287
+ * the difference between a capture of tens of megabytes and one of a couple.
288
+ *
289
+ * **Cull state is part of the hash.** A frame where a different set of passes was culled genuinely
290
+ * is a different shape, and folding the two together would report passes as having run in frames
291
+ * where they did not.
292
+ *
293
+ * FNV-1a over a canonical string. Not cryptographic and does not need to be: a collision costs a
294
+ * mislabelled structure in a debug tool, and the inputs are pass names and integers rather than
295
+ * anything adversarial.
296
+ *
297
+ * @param {GPUProfileTopology} topology
298
+ * @returns {number} unsigned 32-bit
299
+ */
300
+ export function sgpt_topology_hash(topology) {
301
+ let hash = 2166136261;
302
+
303
+ /**
304
+ * @param {string} text
305
+ */
306
+ function feed(text) {
307
+ for (let i = 0; i < text.length; i++) {
308
+ hash ^= text.charCodeAt(i);
309
+ hash = Math.imul(hash, 16777619);
310
+ }
311
+ }
312
+
313
+ feed(topology.name);
314
+
315
+ for (const scope of topology.scopes) {
316
+ feed(`S${scope.name}|${scope.parent}`);
317
+ }
318
+
319
+ for (const pass of topology.passes) {
320
+ feed(`P${pass.id}|${pass.name}|${pass.scope}|${pass.culled ? 1 : 0}|${pass.has_side_effects ? 1 : 0}`);
321
+ feed(`r${pass.reads.join(',')}w${pass.writes.join(',')}c${pass.creates.join(',')}`);
322
+ }
323
+
324
+ for (const resource of topology.resources) {
325
+ feed(`R${resource.id}|${resource.name}|${resource.version}|${resource.kind}|${resource.byte_size}|${resource.usage}|${resource.producer}`);
326
+ }
327
+
328
+ return hash >>> 0;
329
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Write the fixed 32-byte header at the front of an `.sgpt` stream.
3
+ *
4
+ * Called once, when the session opens, with the directory fields left at zero — they are not known
5
+ * until the session stops, and a capture that never stops keeps them at zero permanently. That is a
6
+ * supported end state, not a damaged file: see {@link SGPT_HEADER_OFFSET.DIRECTORY_OFFSET}.
7
+ *
8
+ * The checksum covers bytes `[0, 12)` only, deliberately stopping short of the fields that get
9
+ * patched afterwards. A checksum over the whole header would have to be recomputed at stop, which
10
+ * means a capture cut short would carry one that fails — turning the recoverable case into an
11
+ * apparently corrupt file.
12
+ *
13
+ * Leaves `position` at {@link SGPT_HEADER_SIZE}, where the first record goes.
14
+ *
15
+ * @param {BinaryBuffer} buffer positioned at 0
16
+ * @returns {void}
17
+ *
18
+ * @author Alex Goldring
19
+ * @copyright Company Named Limited (c) 2026
20
+ */
21
+ export function sgpt_write_header(buffer: BinaryBuffer): void;
22
+ //# sourceMappingURL=sgpt_write_header.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sgpt_write_header.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/device/timing/profile/sgpt_write_header.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,yDALa,IAAI,CAsBhB"}
@@ -0,0 +1,47 @@
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { crc32 } from "../../../../core/binary/hash/crc32.js";
3
+ import { SGPT_FORMAT_VERSION } from "./SGPT_FORMAT_VERSION.js";
4
+ import { SGPT_HEADER_CHECKSUM_COVERAGE } from "./SGPT_HEADER_CHECKSUM_COVERAGE.js";
5
+ import { SGPT_HEADER_OFFSET } from "./SGPT_HEADER_OFFSET.js";
6
+ import { SGPT_HEADER_SIZE } from "./SGPT_HEADER_SIZE.js";
7
+ import { SGPT_MAGIC } from "./SGPT_MAGIC.js";
8
+ import { SGPT_MIN_READER_VERSION } from "./SGPT_MIN_READER_VERSION.js";
9
+
10
+ /**
11
+ * Write the fixed 32-byte header at the front of an `.sgpt` stream.
12
+ *
13
+ * Called once, when the session opens, with the directory fields left at zero — they are not known
14
+ * until the session stops, and a capture that never stops keeps them at zero permanently. That is a
15
+ * supported end state, not a damaged file: see {@link SGPT_HEADER_OFFSET.DIRECTORY_OFFSET}.
16
+ *
17
+ * The checksum covers bytes `[0, 12)` only, deliberately stopping short of the fields that get
18
+ * patched afterwards. A checksum over the whole header would have to be recomputed at stop, which
19
+ * means a capture cut short would carry one that fails — turning the recoverable case into an
20
+ * apparently corrupt file.
21
+ *
22
+ * Leaves `position` at {@link SGPT_HEADER_SIZE}, where the first record goes.
23
+ *
24
+ * @param {BinaryBuffer} buffer positioned at 0
25
+ * @returns {void}
26
+ *
27
+ * @author Alex Goldring
28
+ * @copyright Company Named Limited (c) 2026
29
+ */
30
+ export function sgpt_write_header(buffer) {
31
+ assert.defined(buffer, 'buffer');
32
+ assert.equal(buffer.position, 0, 'header must be written at offset 0');
33
+
34
+ buffer.writeUint32(SGPT_MAGIC);
35
+ buffer.writeUint16(SGPT_FORMAT_VERSION);
36
+ buffer.writeUint16(SGPT_MIN_READER_VERSION);
37
+ buffer.writeUint32(0); // flags; CLOSED is set at stop
38
+
39
+ const checksum = crc32(buffer.raw_bytes, 0, SGPT_HEADER_CHECKSUM_COVERAGE);
40
+
41
+ buffer.writeUint32(checksum);
42
+
43
+ buffer.writeUint64(0n); // directory_offset, patched at stop
44
+ buffer.writeUint64(0n); // directory_byte_length, patched at stop
45
+
46
+ assert.equal(buffer.position, SGPT_HEADER_SIZE, 'header size mismatch');
47
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Append one framed record, encoding its payload through `write_payload`.
3
+ *
4
+ * The payload is written first, into the same buffer, and the framing is filled in behind it once
5
+ * its length and checksum are known. That ordering is what keeps a record a single pass: the
6
+ * alternative — encode to a scratch buffer, measure, copy — costs an allocation and a copy per
7
+ * frame, every frame, for nothing.
8
+ *
9
+ * A record is complete or it is absent. If `write_payload` throws, `position` is restored to where
10
+ * the record would have begun, so a failed encode leaves no half-record for a reader to trip over.
11
+ *
12
+ * @template T
13
+ * @param {BinaryBuffer} buffer positioned where the record should start
14
+ * @param {number} type one of {@link SGPT_RECORD_TYPE}
15
+ * @param {T} data handed to `write_payload`
16
+ * @param {function(BinaryBuffer, T): void} write_payload
17
+ * @returns {number} bytes written, framing included
18
+ *
19
+ * @author Alex Goldring
20
+ * @copyright Company Named Limited (c) 2026
21
+ */
22
+ export function sgpt_write_record<T>(buffer: BinaryBuffer, type: number, data: T, write_payload: (arg0: BinaryBuffer, arg1: T) => void): number;
23
+ //# sourceMappingURL=sgpt_write_record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sgpt_write_record.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/device/timing/profile/sgpt_write_record.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,iEARW,MAAM,2DAEqB,IAAI,GAC7B,MAAM,CAwClB"}