@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,93 @@
1
+ /**
2
+ * Assembles one frame's worth of profile data, and holds the join that nothing else can supply.
3
+ *
4
+ * **The join.** A GPU pass is timed by slot, and a slot knows only the label the pass was opened
5
+ * with. Labels are not keys: one frame graph node routinely opens several GPU passes under labels
6
+ * of their own choosing, so which node produced a given span cannot be worked out after the fact.
7
+ * What can be known is *when* — a pass opened while a node was executing belongs to that node. This
8
+ * class listens to {@link FrameGraph#onPassBegin} for the node, takes slot ids from the command
9
+ * context as passes open, and pairs them. Attribution is recorded, never inferred.
10
+ *
11
+ * One recorder per frame. It accumulates across every command context in that frame, because a
12
+ * frame has several and their spans belong to the same picture.
13
+ *
14
+ * @author Alex Goldring
15
+ * @copyright Company Named Limited (c) 2026
16
+ */
17
+ export class GPUFrameRecorder {
18
+ /**
19
+ * @param {number} frame_index the renderer's own frame counter
20
+ * @param {number} cpu_begin_ms
21
+ */
22
+ constructor(frame_index: number, cpu_begin_ms: number);
23
+ /**
24
+ * Record dispatch and draw counts as well as timings.
25
+ *
26
+ * @returns {void}
27
+ */
28
+ enable_work_capture(): void;
29
+ /**
30
+ * Record structure as well as timings when this recorder closes.
31
+ *
32
+ * @returns {void}
33
+ */
34
+ enable_structure_capture(): void;
35
+ /**
36
+ * @returns {GPUProfileFrame}
37
+ */
38
+ get frame(): GPUProfileFrame;
39
+ /**
40
+ * Follow a graph's pass boundaries for as long as it is executing.
41
+ *
42
+ * @param {FrameGraph} graph
43
+ * @returns {void}
44
+ */
45
+ attach_graph(graph: FrameGraph): void;
46
+ /**
47
+ * Stop following every graph attached to this recorder.
48
+ *
49
+ * A frame graph is single-use and discarded at the end of the frame, so this is belt and
50
+ * braces rather than a leak fix — but a recorder outliving its frame while still holding
51
+ * handlers would attribute the *next* frame's passes to this one's nodes.
52
+ *
53
+ * @returns {void}
54
+ */
55
+ detach_graphs(): void;
56
+ /**
57
+ * A pass has just claimed a timer slot. Called by {@link ShadeGPUCommandContext} at the moment
58
+ * the pass is opened, which is the only moment at which the owning graph pass is known.
59
+ *
60
+ * @param {number} query_set_id
61
+ * @param {number} slot_id
62
+ * @returns {GPUProfileWork|null} to accumulate this pass's encoded work into, or null when the
63
+ * capture level does not ask for it
64
+ */
65
+ note_pass_slot(query_set_id: number, slot_id: number): GPUProfileWork | null;
66
+ /**
67
+ * Take the timings from one command context's timer array.
68
+ *
69
+ * Called after the results have been downloaded, so once per context per frame and always
70
+ * asynchronously with respect to the frame that produced them.
71
+ *
72
+ * @param {GPUTimerArray} timers
73
+ * @param {number} query_set_id which context this was, within the frame
74
+ * @returns {void}
75
+ */
76
+ absorb(timers: GPUTimerArray, query_set_id: number): void;
77
+ /**
78
+ * Finish the frame: rebase every span onto the frame's own epoch and hand it over.
79
+ *
80
+ * Rebasing happens here rather than per span because the epoch is the earliest timestamp in
81
+ * the frame, and that is not known until the last command context has reported. Absolute
82
+ * timestamps carry a large arbitrary origin; frame-relative ones fit a u32 with three orders
83
+ * of magnitude to spare.
84
+ *
85
+ * @param {number} cpu_submit_ms
86
+ * @returns {GPUProfileFrame}
87
+ */
88
+ close(cpu_submit_ms: number): GPUProfileFrame;
89
+ #private;
90
+ }
91
+ import { GPUProfileFrame } from "./GPUProfileFrame.js";
92
+ import { GPUProfileWork } from "./GPUProfileWork.js";
93
+ //# sourceMappingURL=GPUFrameRecorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GPUFrameRecorder.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/device/timing/profile/GPUFrameRecorder.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;GAeG;AACH;IAkFI;;;OAGG;IACH,yBAHW,MAAM,gBACN,MAAM,EAQhB;IAtDD;;;;OAIG;IACH,uBAFa,IAAI,CAIhB;IAqBD;;;;OAIG;IACH,4BAFa,IAAI,CAIhB;IAED;;OAEG;IACH,6BAEC;IAcD;;;;;OAKG;IACH,iCAFa,IAAI,CAiBhB;IAED;;;;;;;;OAQG;IACH,iBAFa,IAAI,CAShB;IAWD;;;;;;;;OAQG;IACH,6BALW,MAAM,WACN,MAAM,GACJ,cAAc,GAAC,IAAI,CAiB/B;IAED;;;;;;;;;OASG;IACH,4CAHW,MAAM,GACJ,IAAI,CAqChB;IAED;;;;;;;;;;OAUG;IACH,qBAHW,MAAM,GACJ,eAAe,CAgD3B;;CACJ;gCAvS+B,sBAAsB;+BAEvB,qBAAqB"}
@@ -0,0 +1,297 @@
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { GPUProfileFrame } from "./GPUProfileFrame.js";
3
+ import { GPUProfileSpan } from "./GPUProfileSpan.js";
4
+ import { GPUProfileWork } from "./GPUProfileWork.js";
5
+ import { frame_graph_extract_topology } from "./frame_graph_extract_topology.js";
6
+
7
+ /**
8
+ * Assembles one frame's worth of profile data, and holds the join that nothing else can supply.
9
+ *
10
+ * **The join.** A GPU pass is timed by slot, and a slot knows only the label the pass was opened
11
+ * with. Labels are not keys: one frame graph node routinely opens several GPU passes under labels
12
+ * of their own choosing, so which node produced a given span cannot be worked out after the fact.
13
+ * What can be known is *when* — a pass opened while a node was executing belongs to that node. This
14
+ * class listens to {@link FrameGraph#onPassBegin} for the node, takes slot ids from the command
15
+ * context as passes open, and pairs them. Attribution is recorded, never inferred.
16
+ *
17
+ * One recorder per frame. It accumulates across every command context in that frame, because a
18
+ * frame has several and their spans belong to the same picture.
19
+ *
20
+ * @author Alex Goldring
21
+ * @copyright Company Named Limited (c) 2026
22
+ */
23
+ export class GPUFrameRecorder {
24
+
25
+ /**
26
+ * @type {GPUProfileFrame}
27
+ */
28
+ #frame = new GPUProfileFrame();
29
+
30
+ /**
31
+ * The graph pass currently executing, or -1 outside of one. Work encoded outside any graph pass
32
+ * is real and worth recording; it simply has no node to blame.
33
+ *
34
+ * @type {number}
35
+ */
36
+ #current_graph_pass_id = -1;
37
+
38
+ /**
39
+ * Which graph pass owned each timer slot, indexed by `query_set_id * SLOT_SPACE + slot_id`.
40
+ *
41
+ * A map rather than an array because slot ids restart per command context, and a frame has
42
+ * several.
43
+ *
44
+ * @type {Map<string, number>}
45
+ */
46
+ #slot_owner = new Map();
47
+
48
+ /**
49
+ * What each timer slot's pass was asked to do, keyed the same way. Populated only while
50
+ * recording at {@link GPUProfileLevel.WORKLOAD} or above.
51
+ *
52
+ * @type {Map<string, GPUProfileWork>}
53
+ */
54
+ #slot_work = new Map();
55
+
56
+ /**
57
+ * @type {boolean}
58
+ */
59
+ #capture_work = false;
60
+
61
+ /**
62
+ * Record dispatch and draw counts as well as timings.
63
+ *
64
+ * @returns {void}
65
+ */
66
+ enable_work_capture() {
67
+ this.#capture_work = true;
68
+ }
69
+
70
+ /**
71
+ * Graphs this recorder is subscribed to, so it can unsubscribe.
72
+ *
73
+ * @type {{graph: FrameGraph, begin: function, end: function}[]}
74
+ */
75
+ #subscriptions = [];
76
+
77
+ /**
78
+ * @type {boolean}
79
+ */
80
+ #closed = false;
81
+
82
+ /**
83
+ * Whether to record the frame graph's structure alongside its timings.
84
+ *
85
+ * @type {boolean}
86
+ */
87
+ #capture_structure = false;
88
+
89
+ /**
90
+ * Record structure as well as timings when this recorder closes.
91
+ *
92
+ * @returns {void}
93
+ */
94
+ enable_structure_capture() {
95
+ this.#capture_structure = true;
96
+ }
97
+
98
+ /**
99
+ * @returns {GPUProfileFrame}
100
+ */
101
+ get frame() {
102
+ return this.#frame;
103
+ }
104
+
105
+ /**
106
+ * @param {number} frame_index the renderer's own frame counter
107
+ * @param {number} cpu_begin_ms
108
+ */
109
+ constructor(frame_index, cpu_begin_ms) {
110
+ assert.isNonNegativeInteger(frame_index, 'frame_index');
111
+ assert.isNumber(cpu_begin_ms, 'cpu_begin_ms');
112
+
113
+ this.#frame.frame_index = frame_index;
114
+ this.#frame.cpu_begin_ms = cpu_begin_ms;
115
+ }
116
+
117
+ /**
118
+ * Follow a graph's pass boundaries for as long as it is executing.
119
+ *
120
+ * @param {FrameGraph} graph
121
+ * @returns {void}
122
+ */
123
+ attach_graph(graph) {
124
+ assert.defined(graph, 'graph');
125
+
126
+ const begin = (node) => {
127
+ this.#current_graph_pass_id = node.id;
128
+ };
129
+
130
+ const end = () => {
131
+ this.#current_graph_pass_id = -1;
132
+ };
133
+
134
+ graph.onPassBegin.add(begin);
135
+ graph.onPassEnd.add(end);
136
+
137
+ this.#subscriptions.push({ graph, begin, end });
138
+ }
139
+
140
+ /**
141
+ * Stop following every graph attached to this recorder.
142
+ *
143
+ * A frame graph is single-use and discarded at the end of the frame, so this is belt and
144
+ * braces rather than a leak fix — but a recorder outliving its frame while still holding
145
+ * handlers would attribute the *next* frame's passes to this one's nodes.
146
+ *
147
+ * @returns {void}
148
+ */
149
+ detach_graphs() {
150
+ for (const { graph, begin, end } of this.#subscriptions) {
151
+ graph.onPassBegin.remove(begin);
152
+ graph.onPassEnd.remove(end);
153
+ }
154
+
155
+ this.#subscriptions.length = 0;
156
+ }
157
+
158
+ /**
159
+ * @param {number} query_set_id
160
+ * @param {number} slot_id
161
+ * @returns {string}
162
+ */
163
+ #slot_key(query_set_id, slot_id) {
164
+ return `${query_set_id}:${slot_id}`;
165
+ }
166
+
167
+ /**
168
+ * A pass has just claimed a timer slot. Called by {@link ShadeGPUCommandContext} at the moment
169
+ * the pass is opened, which is the only moment at which the owning graph pass is known.
170
+ *
171
+ * @param {number} query_set_id
172
+ * @param {number} slot_id
173
+ * @returns {GPUProfileWork|null} to accumulate this pass's encoded work into, or null when the
174
+ * capture level does not ask for it
175
+ */
176
+ note_pass_slot(query_set_id, slot_id) {
177
+ const key = this.#slot_key(query_set_id, slot_id);
178
+
179
+ this.#slot_owner.set(key, this.#current_graph_pass_id);
180
+
181
+ if (!this.#capture_work) {
182
+ return null;
183
+ }
184
+
185
+ const work = new GPUProfileWork();
186
+
187
+ this.#slot_work.set(key, work);
188
+
189
+ return work;
190
+ }
191
+
192
+ /**
193
+ * Take the timings from one command context's timer array.
194
+ *
195
+ * Called after the results have been downloaded, so once per context per frame and always
196
+ * asynchronously with respect to the frame that produced them.
197
+ *
198
+ * @param {GPUTimerArray} timers
199
+ * @param {number} query_set_id which context this was, within the frame
200
+ * @returns {void}
201
+ */
202
+ absorb(timers, query_set_id) {
203
+ assert.defined(timers, 'timers');
204
+ assert.isNonNegativeInteger(query_set_id, 'query_set_id');
205
+
206
+ if (this.#closed) {
207
+ // The frame was committed before this context's readback landed. Dropping it is
208
+ // correct — the alternative is mutating a frame already written to the stream.
209
+ return;
210
+ }
211
+
212
+ const frame = this.#frame;
213
+
214
+ timers.traverse_results((slot_id, label, type, t_begin, t_end) => {
215
+ const span = new GPUProfileSpan();
216
+
217
+ span.label = label;
218
+ span.kind = type;
219
+ span.query_set_id = query_set_id;
220
+
221
+ const key = this.#slot_key(query_set_id, slot_id);
222
+
223
+ const owner = this.#slot_owner.get(key);
224
+
225
+ span.graph_pass_id = owner === undefined ? -1 : owner;
226
+
227
+ span.work = this.#slot_work.get(key) ?? null;
228
+
229
+ // stored absolutely for now; `close` rebases the whole frame once every context is in
230
+ span.t_begin_ns = t_begin;
231
+ span.duration_ns = t_end > t_begin ? Number(t_end - t_begin) : 0;
232
+
233
+ frame.spans.push(span);
234
+ });
235
+
236
+ frame.dropped_pass_count += timers.dropped_count;
237
+ }
238
+
239
+ /**
240
+ * Finish the frame: rebase every span onto the frame's own epoch and hand it over.
241
+ *
242
+ * Rebasing happens here rather than per span because the epoch is the earliest timestamp in
243
+ * the frame, and that is not known until the last command context has reported. Absolute
244
+ * timestamps carry a large arbitrary origin; frame-relative ones fit a u32 with three orders
245
+ * of magnitude to spare.
246
+ *
247
+ * @param {number} cpu_submit_ms
248
+ * @returns {GPUProfileFrame}
249
+ */
250
+ close(cpu_submit_ms) {
251
+ assert.notOk(this.#closed, 'frame recorder already closed');
252
+
253
+ this.#closed = true;
254
+
255
+ const frame = this.#frame;
256
+
257
+ /*
258
+ Extracted here rather than at `attach_graph`, because reference counts and cull decisions
259
+ are what `compile` derives and the graph has not been compiled yet when it is attached.
260
+ Reading it before then records a frame in which every pass was culled.
261
+
262
+ Only the first graph is taken. A frame runs one — the shading graph — and anything else
263
+ attached is a subgraph whose passes are already in it.
264
+ */
265
+ if (this.#capture_structure && this.#subscriptions.length > 0) {
266
+ frame.topology = frame_graph_extract_topology(this.#subscriptions[0].graph);
267
+ }
268
+
269
+ this.detach_graphs();
270
+
271
+ frame.cpu_submit_ms = cpu_submit_ms;
272
+
273
+ const spans = frame.spans;
274
+
275
+ if (spans.length === 0) {
276
+ return frame;
277
+ }
278
+
279
+ let epoch = spans[0].t_begin_ns;
280
+
281
+ for (const span of spans) {
282
+ if (span.t_begin_ns < epoch) {
283
+ epoch = span.t_begin_ns;
284
+ }
285
+ }
286
+
287
+ frame.gpu_epoch_ns = epoch;
288
+
289
+ for (const span of spans) {
290
+ span.t_begin_ns = Number(span.t_begin_ns - epoch);
291
+ }
292
+
293
+ spans.sort((a, b) => a.t_begin_ns - b.t_begin_ns);
294
+
295
+ return frame;
296
+ }
297
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * A decoded `.sgpt` capture.
3
+ *
4
+ * Returned whole even when damaged. {@link defects} is not an error channel to check before
5
+ * trusting the rest — a capture of a GPU falling over ends mid-record by definition, and the
6
+ * records before the cut are the evidence. The inspector's job is to show what is here and say
7
+ * plainly what is not.
8
+ *
9
+ * @author Alex Goldring
10
+ * @copyright Company Named Limited (c) 2026
11
+ */
12
+ export class GPUProfileCapture {
13
+ /**
14
+ * @type {SGPTHeader|null}
15
+ */
16
+ header: SGPTHeader | null;
17
+ /**
18
+ * @type {GPUProfileMeta}
19
+ */
20
+ meta: GPUProfileMeta;
21
+ /**
22
+ * @type {GPUProfileFrame[]}
23
+ */
24
+ frames: GPUProfileFrame[];
25
+ /**
26
+ * Frame graph structures, indexed by `GPUProfileFrame.topology_id`. Empty for a capture
27
+ * recorded below {@link GPUProfileLevel.STRUCTURE}.
28
+ *
29
+ * Far fewer than there are frames: the graph is nearly identical frame to frame, so a long
30
+ * capture typically holds a handful — the steady state, plus whatever varies with it.
31
+ *
32
+ * @type {GPUProfileTopology[]}
33
+ */
34
+ topologies: GPUProfileTopology[];
35
+ /**
36
+ * Record types present in the stream that this build does not understand, and how many of each.
37
+ *
38
+ * Skipping them is correct — that is what the framing is for — but staying quiet about it is
39
+ * not. A newer recorder produced something here, and the reader should be able to say so.
40
+ *
41
+ * @type {Map<number, number>}
42
+ */
43
+ unknown_record_counts: Map<number, number>;
44
+ /**
45
+ * @type {SGPTDefect[]}
46
+ */
47
+ defects: SGPTDefect[];
48
+ /**
49
+ * The structure of `frame`, or null when the capture carries none.
50
+ *
51
+ * @param {GPUProfileFrame} frame
52
+ * @returns {GPUProfileTopology|null}
53
+ */
54
+ topology_of(frame: GPUProfileFrame): GPUProfileTopology | null;
55
+ /**
56
+ * Whether the writer reached `stop()`.
57
+ * @returns {boolean}
58
+ */
59
+ get is_complete(): boolean;
60
+ }
61
+ import { GPUProfileMeta } from "./GPUProfileMeta.js";
62
+ //# sourceMappingURL=GPUProfileCapture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GPUProfileCapture.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/device/timing/profile/GPUProfileCapture.js"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH;IACI;;OAEG;IACH,QAFU,aAAW,IAAI,CAEX;IAEd;;OAEG;IACH,MAFU,cAAc,CAEI;IAE5B;;OAEG;IACH,QAFU,iBAAiB,CAEf;IAEZ;;;;;;;;OAQG;IACH,YAFU,oBAAoB,CAEd;IAEhB;;;;;;;OAOG;IACH,uBAFU,IAAI,MAAM,EAAE,MAAM,CAAC,CAEK;IAElC;;OAEG;IACH,SAFU,YAAY,CAET;IAEb;;;;;OAKG;IACH,qCAFa,qBAAmB,IAAI,CAQnC;IAED;;;OAGG;IACH,2BAEC;CACJ;+BA5E8B,qBAAqB"}
@@ -0,0 +1,77 @@
1
+ import { GPUProfileMeta } from "./GPUProfileMeta.js";
2
+
3
+ /**
4
+ * A decoded `.sgpt` capture.
5
+ *
6
+ * Returned whole even when damaged. {@link defects} is not an error channel to check before
7
+ * trusting the rest — a capture of a GPU falling over ends mid-record by definition, and the
8
+ * records before the cut are the evidence. The inspector's job is to show what is here and say
9
+ * plainly what is not.
10
+ *
11
+ * @author Alex Goldring
12
+ * @copyright Company Named Limited (c) 2026
13
+ */
14
+ export class GPUProfileCapture {
15
+ /**
16
+ * @type {SGPTHeader|null}
17
+ */
18
+ header = null;
19
+
20
+ /**
21
+ * @type {GPUProfileMeta}
22
+ */
23
+ meta = new GPUProfileMeta();
24
+
25
+ /**
26
+ * @type {GPUProfileFrame[]}
27
+ */
28
+ frames = [];
29
+
30
+ /**
31
+ * Frame graph structures, indexed by `GPUProfileFrame.topology_id`. Empty for a capture
32
+ * recorded below {@link GPUProfileLevel.STRUCTURE}.
33
+ *
34
+ * Far fewer than there are frames: the graph is nearly identical frame to frame, so a long
35
+ * capture typically holds a handful — the steady state, plus whatever varies with it.
36
+ *
37
+ * @type {GPUProfileTopology[]}
38
+ */
39
+ topologies = [];
40
+
41
+ /**
42
+ * Record types present in the stream that this build does not understand, and how many of each.
43
+ *
44
+ * Skipping them is correct — that is what the framing is for — but staying quiet about it is
45
+ * not. A newer recorder produced something here, and the reader should be able to say so.
46
+ *
47
+ * @type {Map<number, number>}
48
+ */
49
+ unknown_record_counts = new Map();
50
+
51
+ /**
52
+ * @type {SGPTDefect[]}
53
+ */
54
+ defects = [];
55
+
56
+ /**
57
+ * The structure of `frame`, or null when the capture carries none.
58
+ *
59
+ * @param {GPUProfileFrame} frame
60
+ * @returns {GPUProfileTopology|null}
61
+ */
62
+ topology_of(frame) {
63
+ if (frame.topology_id < 0) {
64
+ return null;
65
+ }
66
+
67
+ return this.topologies[frame.topology_id] ?? null;
68
+ }
69
+
70
+ /**
71
+ * Whether the writer reached `stop()`.
72
+ * @returns {boolean}
73
+ */
74
+ get is_complete() {
75
+ return this.header !== null && this.header.is_closed;
76
+ }
77
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * One recorded frame.
3
+ *
4
+ * @author Alex Goldring
5
+ * @copyright Company Named Limited (c) 2026
6
+ */
7
+ export class GPUProfileFrame {
8
+ /**
9
+ * The renderer's own frame counter, not this frame's position in the capture. A capture started
10
+ * mid-session does not begin at zero, and a frame that dropped its records leaves a gap — both
11
+ * of which are worth seeing.
12
+ * @type {number}
13
+ */
14
+ frame_index: number;
15
+ /**
16
+ * `performance.now()` when the frame began encoding.
17
+ * @type {number}
18
+ */
19
+ cpu_begin_ms: number;
20
+ /**
21
+ * `performance.now()` when the command buffer was submitted.
22
+ * @type {number}
23
+ */
24
+ cpu_submit_ms: number;
25
+ /**
26
+ * The GPU timestamp all of this frame's spans are relative to — the earliest of them.
27
+ *
28
+ * There is no calibration between this clock and `performance.now()`; WebGPU exposes none. The
29
+ * CPU and GPU tracks are anchored per frame at submission and that anchoring is nominal, which
30
+ * is a thing to say out loud rather than paper over.
31
+ *
32
+ * @type {bigint}
33
+ */
34
+ gpu_epoch_ns: bigint;
35
+ /**
36
+ * Index into the capture's topology list, or -1 when the capture was recorded below
37
+ * {@link GPUProfileLevel.STRUCTURE} and has no structure to point at.
38
+ *
39
+ * Many frames share one: the graph is nearly identical frame to frame, so the structure is
40
+ * stored once and referenced.
41
+ *
42
+ * @type {number}
43
+ */
44
+ topology_id: number;
45
+ /**
46
+ * The structure this frame ran, before the session has deduplicated it.
47
+ *
48
+ * Transient: the session hashes it, writes it if the shape is new, sets {@link topology_id},
49
+ * and never looks at this again. A frame decoded from a capture has `null` here and the id
50
+ * instead — the structure lives once, in the capture, rather than once per frame.
51
+ *
52
+ * @type {GPUProfileTopology|null}
53
+ */
54
+ topology: GPUProfileTopology | null;
55
+ /**
56
+ * @type {GPUProfileSpan[]}
57
+ */
58
+ spans: GPUProfileSpan[];
59
+ /**
60
+ * Passes that went untimed because the frame had more of them than the timer array had slots.
61
+ * @see GPUTimerArray#dropped_count
62
+ * @type {number}
63
+ */
64
+ dropped_pass_count: number;
65
+ /**
66
+ * Total GPU time across this frame's spans. Spans can overlap on some hardware, so this is a
67
+ * sum of durations rather than a wall-clock span, and the two are not the same number.
68
+ *
69
+ * @returns {number} nanoseconds
70
+ */
71
+ get gpu_duration_ns(): number;
72
+ }
73
+ //# sourceMappingURL=GPUProfileFrame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GPUProfileFrame.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/device/timing/profile/GPUProfileFrame.js"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH;IACI;;;;;OAKG;IACH,aAFU,MAAM,CAEA;IAEhB;;;OAGG;IACH,cAFU,MAAM,CAEC;IAEjB;;;OAGG;IACH,eAFU,MAAM,CAEE;IAElB;;;;;;;;OAQG;IACH,cAFU,MAAM,CAEE;IAElB;;;;;;;;OAQG;IACH,aAFU,MAAM,CAEC;IAEjB;;;;;;;;OAQG;IACH,UAFU,qBAAmB,IAAI,CAEjB;IAEhB;;OAEG;IACH,OAFU,gBAAgB,CAEf;IAEX;;;;OAIG;IACH,oBAFU,MAAM,CAEO;IAEvB;;;;;OAKG;IACH,8BAQC;CACJ"}