@woosh/meep-engine 3.10.0 → 3.11.1
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/README.md +168 -152
- package/package.json +1 -1
- package/src/REVIEW_2026_08_06.md +610 -610
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +28 -4
- package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts +26 -0
- package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts.map +1 -1
- package/src/engine/graphics/render/frame_graph/FrameGraph.js +47 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +0 -3
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +0 -3
- package/src/shade/device/ShadeGPUCommandContext.d.ts +21 -0
- package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
- package/src/shade/device/ShadeGPUCommandContext.js +940 -749
- package/src/shade/device/timing/GPUTimerArray.d.ts +22 -4
- package/src/shade/device/timing/GPUTimerArray.d.ts.map +1 -1
- package/src/shade/device/timing/GPUTimerArray.js +88 -14
- package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +349 -98
- package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts +93 -0
- package/src/shade/device/timing/profile/GPUFrameRecorder.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUFrameRecorder.js +297 -0
- package/src/shade/device/timing/profile/GPUProfileCapture.d.ts +62 -0
- package/src/shade/device/timing/profile/GPUProfileCapture.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileCapture.js +77 -0
- package/src/shade/device/timing/profile/GPUProfileFrame.d.ts +73 -0
- package/src/shade/device/timing/profile/GPUProfileFrame.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileFrame.js +88 -0
- package/src/shade/device/timing/profile/GPUProfileLevel.d.ts +20 -0
- package/src/shade/device/timing/profile/GPUProfileLevel.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileLevel.js +50 -0
- package/src/shade/device/timing/profile/GPUProfileMeta.d.ts +63 -0
- package/src/shade/device/timing/profile/GPUProfileMeta.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileMeta.js +73 -0
- package/src/shade/device/timing/profile/GPUProfileSession.d.ts +173 -0
- package/src/shade/device/timing/profile/GPUProfileSession.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileSession.js +470 -0
- package/src/shade/device/timing/profile/GPUProfileSpan.d.ts +60 -0
- package/src/shade/device/timing/profile/GPUProfileSpan.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileSpan.js +65 -0
- package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts +19 -0
- package/src/shade/device/timing/profile/GPUProfileSpanKind.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileSpanKind.js +25 -0
- package/src/shade/device/timing/profile/GPUProfileTopology.d.ts +159 -0
- package/src/shade/device/timing/profile/GPUProfileTopology.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileTopology.js +202 -0
- package/src/shade/device/timing/profile/GPUProfileWork.d.ts +90 -0
- package/src/shade/device/timing/profile/GPUProfileWork.d.ts.map +1 -0
- package/src/shade/device/timing/profile/GPUProfileWork.js +115 -0
- package/src/shade/device/timing/profile/SGPTDefect.d.ts +43 -0
- package/src/shade/device/timing/profile/SGPTDefect.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPTDefect.js +65 -0
- package/src/shade/device/timing/profile/SGPTHeader.d.ts +38 -0
- package/src/shade/device/timing/profile/SGPTHeader.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPTHeader.js +46 -0
- package/src/shade/device/timing/profile/SGPTRecordRef.d.ts +37 -0
- package/src/shade/device/timing/profile/SGPTRecordRef.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPTRecordRef.js +45 -0
- package/src/shade/device/timing/profile/SGPTWriteStream.d.ts +103 -0
- package/src/shade/device/timing/profile/SGPTWriteStream.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPTWriteStream.js +287 -0
- package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts +19 -0
- package/src/shade/device/timing/profile/SGPT_DEFECT.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_DEFECT.js +26 -0
- package/src/shade/device/timing/profile/SGPT_FLAG.d.ts +8 -0
- package/src/shade/device/timing/profile/SGPT_FLAG.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_FLAG.js +15 -0
- package/src/shade/device/timing/profile/SGPT_FORMAT.md +345 -0
- package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts +10 -0
- package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_FORMAT_VERSION.js +9 -0
- package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts +17 -0
- package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_FRAME_SLACK.js +16 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts +16 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_CHECKSUM_COVERAGE.js +15 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts +15 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_OFFSET.js +29 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts +7 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_HEADER_SIZE.js +6 -0
- package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts +7 -0
- package/src/shade/device/timing/profile/SGPT_MAGIC.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_MAGIC.js +6 -0
- package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts +16 -0
- package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_MIN_READER_VERSION.js +15 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts +7 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_HEADER_SIZE.js +6 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts +13 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_OFFSET.js +23 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts +12 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_SYNC.js +11 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts +18 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.d.ts.map +1 -0
- package/src/shade/device/timing/profile/SGPT_RECORD_TYPE.js +46 -0
- package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts +20 -0
- package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts.map +1 -0
- package/src/shade/device/timing/profile/frame_graph_extract_topology.js +174 -0
- package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts +21 -0
- package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts.map +1 -0
- package/src/shade/device/timing/profile/make_profiling_pass_encoder.js +114 -0
- package/src/shade/device/timing/profile/parse_workgroup_size.d.ts +20 -0
- package/src/shade/device/timing/profile/parse_workgroup_size.d.ts.map +1 -0
- package/src/shade/device/timing/profile/parse_workgroup_size.js +43 -0
- package/src/shade/device/timing/profile/sgpt_fourcc.d.ts +18 -0
- package/src/shade/device/timing/profile/sgpt_fourcc.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_fourcc.js +34 -0
- package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts +42 -0
- package/src/shade/device/timing/profile/sgpt_frame_codec.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_frame_codec.js +172 -0
- package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts +20 -0
- package/src/shade/device/timing/profile/sgpt_meta_codec.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_meta_codec.js +65 -0
- package/src/shade/device/timing/profile/sgpt_read_capture.d.ts +19 -0
- package/src/shade/device/timing/profile/sgpt_read_capture.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_read_capture.js +134 -0
- package/src/shade/device/timing/profile/sgpt_read_header.d.ts +22 -0
- package/src/shade/device/timing/profile/sgpt_read_header.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_read_header.js +94 -0
- package/src/shade/device/timing/profile/sgpt_read_records.d.ts +27 -0
- package/src/shade/device/timing/profile/sgpt_read_records.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_read_records.js +153 -0
- package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts +54 -0
- package/src/shade/device/timing/profile/sgpt_topology_codec.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_topology_codec.js +329 -0
- package/src/shade/device/timing/profile/sgpt_write_header.d.ts +22 -0
- package/src/shade/device/timing/profile/sgpt_write_header.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_write_header.js +47 -0
- package/src/shade/device/timing/profile/sgpt_write_record.d.ts +23 -0
- package/src/shade/device/timing/profile/sgpt_write_record.d.ts.map +1 -0
- package/src/shade/device/timing/profile/sgpt_write_record.js +62 -0
- package/src/shade/playground/volumetrics_froxel/README.md +88 -0
- package/src/shade/playground/volumetrics_froxel/index.html +251 -0
- package/src/shade/playground/volumetrics_froxel/main.d.ts +2 -0
- package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -0
- package/src/shade/playground/volumetrics_froxel/main.js +619 -0
- package/src/shade/renderer/Renderer.d.ts +34 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +2536 -2455
- package/src/shade/renderer/volumetrics/NOTES.md +42 -1
- package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.d.ts +21 -0
- package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_camera_ray_planar_depth_scale.js +47 -0
- package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +19 -8
- package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.d.ts +37 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_content_offset.js +56 -0
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +6 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +367 -324
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.js +14 -3
- package/src/shade/renderer/volumetrics/volumetrics_position_world_to_froxel_uvw.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/volumetrics_position_world_to_froxel_uvw.js +48 -36
- package/src/engine/save/GameStateLoader.d.ts +0 -49
- package/src/engine/save/GameStateLoader.d.ts.map +0 -1
- package/src/engine/save/GameStateLoader.js +0 -168
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { assert } from "../../../../core/assert.js";
|
|
2
|
+
import { Signal } from "../../../../core/events/signal/Signal.js";
|
|
3
|
+
import { GPUFrameRecorder } from "./GPUFrameRecorder.js";
|
|
4
|
+
import { GPUProfileLevel, gpu_profile_level_name } from "./GPUProfileLevel.js";
|
|
5
|
+
import { GPUProfileMeta } from "./GPUProfileMeta.js";
|
|
6
|
+
import { SGPT_RECORD_TYPE } from "./SGPT_RECORD_TYPE.js";
|
|
7
|
+
import { SGPTWriteStream } from "./SGPTWriteStream.js";
|
|
8
|
+
import { sgpt_write_frame } from "./sgpt_frame_codec.js";
|
|
9
|
+
import { sgpt_write_meta } from "./sgpt_meta_codec.js";
|
|
10
|
+
import {
|
|
11
|
+
sgpt_intern_topology_symbols,
|
|
12
|
+
sgpt_topology_hash,
|
|
13
|
+
sgpt_write_topology
|
|
14
|
+
} from "./sgpt_topology_codec.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default point at which an uncapped session starts saying how much it has accumulated. Does not
|
|
18
|
+
* stop anything — see {@link GPUProfileSession#byte_budget}.
|
|
19
|
+
*
|
|
20
|
+
* @type {number}
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_BYTE_BUDGET = 512 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A GPU profile recording, from `start` to `stop`.
|
|
26
|
+
*
|
|
27
|
+
* **The engine never constructs one of these.** The caller builds it and hands it over:
|
|
28
|
+
*
|
|
29
|
+
* ```js
|
|
30
|
+
* const session = new GPUProfileSession({ level: GPUProfileLevel.TIMING });
|
|
31
|
+
*
|
|
32
|
+
* renderer.profile_session = session;
|
|
33
|
+
*
|
|
34
|
+
* await session.start(graphics);
|
|
35
|
+
* // ... frames run, for as long as you like ...
|
|
36
|
+
* const bytes = await session.stop();
|
|
37
|
+
*
|
|
38
|
+
* renderer.profile_session = null;
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* That ownership is the point rather than a style choice. **Nothing in the engine imports this file
|
|
42
|
+
* or anything it reaches** — the write stream, the format codecs, the topology extractor — so an
|
|
43
|
+
* application that never mentions the profiler does not carry any of it. The feature ships without
|
|
44
|
+
* a build flag, a strip-plugin interaction, or a dead branch anyone has to keep honest.
|
|
45
|
+
*
|
|
46
|
+
* Two leaf helpers under this directory *are* linked unconditionally, because
|
|
47
|
+
* {@link ShadeGPUCommandContext} needs them while encoding: `make_profiling_pass_encoder` and
|
|
48
|
+
* `parse_workgroup_size`. They import `assert` and nothing else, and neither knows what a capture
|
|
49
|
+
* is. What remains in the engine beyond those is a nullable field and the null checks around it.
|
|
50
|
+
*
|
|
51
|
+
* Recording is an **open-ended forward stream**: `frame_limit` defaults to `Infinity` and
|
|
52
|
+
* everything captured is kept. There is no ring buffer. Uncapped means a per-frame cost is also a
|
|
53
|
+
* per-second one, so the session reports what it is accumulating rather than letting an
|
|
54
|
+
* application find out when the tab dies.
|
|
55
|
+
*
|
|
56
|
+
* @author Alex Goldring
|
|
57
|
+
* @copyright Company Named Limited (c) 2026
|
|
58
|
+
*/
|
|
59
|
+
export class GPUProfileSession {
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @type {number}
|
|
63
|
+
*/
|
|
64
|
+
#level = GPUProfileLevel.TIMING;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @type {number}
|
|
68
|
+
*/
|
|
69
|
+
#frame_limit = Infinity;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @type {string}
|
|
73
|
+
*/
|
|
74
|
+
#note = "";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @type {SGPTWriteStream|null}
|
|
78
|
+
*/
|
|
79
|
+
#stream = null;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @type {GPUProfileMeta}
|
|
83
|
+
*/
|
|
84
|
+
#meta = new GPUProfileMeta();
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @type {number}
|
|
88
|
+
*/
|
|
89
|
+
#frames_recorded = 0;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @type {boolean}
|
|
93
|
+
*/
|
|
94
|
+
#running = false;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @type {boolean}
|
|
98
|
+
*/
|
|
99
|
+
#stopped = false;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @type {boolean}
|
|
103
|
+
*/
|
|
104
|
+
#budget_warned = false;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Content hash of every topology already written, to the id it was given.
|
|
108
|
+
*
|
|
109
|
+
* The whole of the deduplication. A Shade frame graph is nearly identical frame to frame, so
|
|
110
|
+
* this map stays tiny and hundreds of frames share one entry — the difference between a capture
|
|
111
|
+
* of tens of megabytes and one of a couple.
|
|
112
|
+
*
|
|
113
|
+
* @type {Map<number, number>}
|
|
114
|
+
*/
|
|
115
|
+
#topology_ids = new Map();
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @type {number}
|
|
119
|
+
*/
|
|
120
|
+
#topology_count = 0;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* How many distinct frame graph structures this capture has needed.
|
|
124
|
+
*
|
|
125
|
+
* Worth watching: if this climbs with the frame count, the graph is changing shape every frame
|
|
126
|
+
* and the size estimates for a long capture do not hold.
|
|
127
|
+
*
|
|
128
|
+
* @returns {number}
|
|
129
|
+
*/
|
|
130
|
+
get topology_count() {
|
|
131
|
+
return this.#topology_count;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Bytes at which the session warns — once — that it is getting large. It does **not** stop:
|
|
136
|
+
* full uncapped history is the behaviour that was asked for, and silently truncating a capture
|
|
137
|
+
* is worse than a large one.
|
|
138
|
+
*
|
|
139
|
+
* @type {number}
|
|
140
|
+
*/
|
|
141
|
+
byte_budget = DEFAULT_BYTE_BUDGET;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fires per frame with the running byte total.
|
|
145
|
+
*
|
|
146
|
+
* @readonly
|
|
147
|
+
* @type {Signal<number>}
|
|
148
|
+
*/
|
|
149
|
+
onBytesWritten = new Signal();
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Fires once, with the finished capture, however the session ended — `stop()` or the frame
|
|
153
|
+
* limit running out. Both paths land here so a caller can await one thing.
|
|
154
|
+
*
|
|
155
|
+
* @readonly
|
|
156
|
+
* @type {Signal<ArrayBuffer>}
|
|
157
|
+
*/
|
|
158
|
+
onComplete = new Signal();
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {object} [options]
|
|
162
|
+
* @param {number} [options.level] one of {@link GPUProfileLevel}
|
|
163
|
+
* @param {number} [options.frame_limit] frames to record before stopping on its own. `Infinity`
|
|
164
|
+
* by default — a session runs until `stop()`.
|
|
165
|
+
* @param {string} [options.note] whatever you want the capture to say about itself
|
|
166
|
+
*/
|
|
167
|
+
constructor({
|
|
168
|
+
level = GPUProfileLevel.TIMING,
|
|
169
|
+
frame_limit = Infinity,
|
|
170
|
+
note = ""
|
|
171
|
+
} = {}) {
|
|
172
|
+
|
|
173
|
+
assert.isInteger(level, 'level');
|
|
174
|
+
assert.isString(note, 'note');
|
|
175
|
+
assert.ok(
|
|
176
|
+
frame_limit === Infinity || (Number.isInteger(frame_limit) && frame_limit > 0),
|
|
177
|
+
`frame_limit must be a positive integer or Infinity, was ${frame_limit}`
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
this.#level = level;
|
|
181
|
+
this.#frame_limit = frame_limit;
|
|
182
|
+
this.#note = note;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @returns {number} one of {@link GPUProfileLevel}
|
|
187
|
+
*/
|
|
188
|
+
get level() {
|
|
189
|
+
return this.#level;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
get is_running() {
|
|
196
|
+
return this.#running;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @returns {number}
|
|
201
|
+
*/
|
|
202
|
+
get frames_recorded() {
|
|
203
|
+
return this.#frames_recorded;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @returns {number}
|
|
208
|
+
*/
|
|
209
|
+
get bytes_written() {
|
|
210
|
+
return this.#stream === null ? 0 : this.#stream.bytes_written;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The metadata this capture will carry. Populate before {@link start}; it is written into the
|
|
215
|
+
* stream at that point and not read again.
|
|
216
|
+
*
|
|
217
|
+
* @returns {GPUProfileMeta}
|
|
218
|
+
*/
|
|
219
|
+
get meta() {
|
|
220
|
+
return this.#meta;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Begin recording.
|
|
225
|
+
*
|
|
226
|
+
* {@link meta} is written into the stream here and not read again, so populate it first.
|
|
227
|
+
*
|
|
228
|
+
* @returns {void}
|
|
229
|
+
*/
|
|
230
|
+
start() {
|
|
231
|
+
assert.notOk(this.#running, 'session already started');
|
|
232
|
+
assert.notOk(this.#stopped, 'a stopped session cannot be restarted; construct a new one');
|
|
233
|
+
|
|
234
|
+
const meta = this.#meta;
|
|
235
|
+
|
|
236
|
+
meta.level = this.#level;
|
|
237
|
+
meta.note = this.#note;
|
|
238
|
+
|
|
239
|
+
if (meta.wall_clock_start_ms === 0) {
|
|
240
|
+
meta.wall_clock_start_ms = Date.now();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const stream = new SGPTWriteStream();
|
|
244
|
+
|
|
245
|
+
stream.write_record(SGPT_RECORD_TYPE.META, sgpt_write_meta, meta);
|
|
246
|
+
|
|
247
|
+
this.#stream = stream;
|
|
248
|
+
this.#running = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Open a recorder for a frame about to be encoded, or null when nothing should be recorded.
|
|
253
|
+
*
|
|
254
|
+
* **This is why the renderer imports nothing from the profiler.** It holds a nullable session
|
|
255
|
+
* and calls methods on it; the recorder type never appears in its module graph, so the whole
|
|
256
|
+
* profiler stays unreachable from any engine entry point and drops out of a bundle that does
|
|
257
|
+
* not ask for it. A `new GPUFrameRecorder(...)` in the renderer would have quietly undone that.
|
|
258
|
+
*
|
|
259
|
+
* @param {number} frame_index the renderer's own frame counter
|
|
260
|
+
* @param {number} cpu_begin_ms
|
|
261
|
+
* @returns {GPUFrameRecorder|null}
|
|
262
|
+
*/
|
|
263
|
+
begin_frame(frame_index, cpu_begin_ms) {
|
|
264
|
+
if (!this.#running) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const recorder = new GPUFrameRecorder(frame_index, cpu_begin_ms);
|
|
269
|
+
|
|
270
|
+
if (this.#level >= GPUProfileLevel.STRUCTURE) {
|
|
271
|
+
recorder.enable_structure_capture();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (this.#level >= GPUProfileLevel.WORKLOAD) {
|
|
275
|
+
recorder.enable_work_capture();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return recorder;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Close a recorder opened by {@link begin_frame} and commit what it collected.
|
|
283
|
+
*
|
|
284
|
+
* @param {GPUFrameRecorder} recorder
|
|
285
|
+
* @param {number} cpu_submit_ms
|
|
286
|
+
* @returns {void}
|
|
287
|
+
*/
|
|
288
|
+
end_frame(recorder, cpu_submit_ms) {
|
|
289
|
+
assert.defined(recorder, 'recorder');
|
|
290
|
+
|
|
291
|
+
this.record_frame(recorder.close(cpu_submit_ms));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Write a topology if this shape has not been seen, and return the id either way.
|
|
296
|
+
*
|
|
297
|
+
* @param {GPUProfileTopology} topology
|
|
298
|
+
* @returns {number}
|
|
299
|
+
*/
|
|
300
|
+
#write_topology(topology) {
|
|
301
|
+
const hash = sgpt_topology_hash(topology);
|
|
302
|
+
|
|
303
|
+
const existing = this.#topology_ids.get(hash);
|
|
304
|
+
|
|
305
|
+
if (existing !== undefined) {
|
|
306
|
+
return existing;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const id = this.#topology_count++;
|
|
310
|
+
|
|
311
|
+
topology.id = id;
|
|
312
|
+
|
|
313
|
+
this.#topology_ids.set(hash, id);
|
|
314
|
+
|
|
315
|
+
const stream = this.#stream;
|
|
316
|
+
|
|
317
|
+
// Ahead of the record, for the same reason frame span labels are — a `SYMS` block must
|
|
318
|
+
// precede whatever references it, or a capture truncated in between resolves no names.
|
|
319
|
+
sgpt_intern_topology_symbols(topology, value => stream.intern(value));
|
|
320
|
+
|
|
321
|
+
stream.write_record(
|
|
322
|
+
SGPT_RECORD_TYPE.TOPO,
|
|
323
|
+
(buffer, t) => sgpt_write_topology(buffer, t, value => stream.intern(value)),
|
|
324
|
+
topology
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return id;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Commit one frame.
|
|
332
|
+
*
|
|
333
|
+
* Ignored when the session is not running, so a frame that lands after `stop()` — the readback
|
|
334
|
+
* for a frame already submitted, which is the ordinary case — is dropped rather than throwing
|
|
335
|
+
* inside somebody's render loop.
|
|
336
|
+
*
|
|
337
|
+
* @param {GPUProfileFrame} frame
|
|
338
|
+
* @returns {void}
|
|
339
|
+
*/
|
|
340
|
+
record_frame(frame) {
|
|
341
|
+
assert.defined(frame, 'frame');
|
|
342
|
+
|
|
343
|
+
if (!this.#running) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const stream = this.#stream;
|
|
348
|
+
|
|
349
|
+
// Reserve before writing, so the buffer's growth — an allocation and a copy — happens here
|
|
350
|
+
// at the boundary rather than part-way through encoding a frame.
|
|
351
|
+
stream.reserve_frame();
|
|
352
|
+
|
|
353
|
+
if (frame.topology !== null && frame.topology !== undefined) {
|
|
354
|
+
frame.topology_id = this.#write_topology(frame.topology);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/*
|
|
358
|
+
Intern every name this frame uses *before* the record is opened.
|
|
359
|
+
|
|
360
|
+
`write_record` flushes pending symbols ahead of the record it is about to write, and
|
|
361
|
+
interning from inside the payload writer is too late for that — the symbols would land in
|
|
362
|
+
the *next* flush, behind the record referencing them. A reader of a capture truncated in
|
|
363
|
+
between then resolves none of them, which is the failure this format exists to avoid.
|
|
364
|
+
|
|
365
|
+
Doing it here rather than inside the stream keeps the stream ignorant of the frame model.
|
|
366
|
+
*/
|
|
367
|
+
for (const span of frame.spans) {
|
|
368
|
+
stream.intern(span.label);
|
|
369
|
+
|
|
370
|
+
if (span.work !== null && span.work !== undefined) {
|
|
371
|
+
stream.intern(span.work.pipeline);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
stream.write_record(
|
|
376
|
+
SGPT_RECORD_TYPE.FRAM,
|
|
377
|
+
(buffer, f) => sgpt_write_frame(buffer, f, value => stream.intern(value)),
|
|
378
|
+
frame
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
this.#frames_recorded++;
|
|
382
|
+
|
|
383
|
+
const bytes = stream.bytes_written;
|
|
384
|
+
|
|
385
|
+
if (this.onBytesWritten.hasHandlers()) {
|
|
386
|
+
this.onBytesWritten.send1(bytes);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!this.#budget_warned && bytes >= this.byte_budget) {
|
|
390
|
+
this.#budget_warned = true;
|
|
391
|
+
|
|
392
|
+
const per_frame = Math.round(bytes / this.#frames_recorded);
|
|
393
|
+
|
|
394
|
+
console.warn(
|
|
395
|
+
`GPUProfileSession: ${(bytes / (1024 * 1024)).toFixed(0)} MiB after `
|
|
396
|
+
+ `${this.#frames_recorded} frames at level ${gpu_profile_level_name(this.#level)} `
|
|
397
|
+
+ `(~${per_frame} bytes/frame). Recording continues — call stop() when you have enough.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (this.#frames_recorded >= this.#frame_limit) {
|
|
402
|
+
this.stop();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The capture as it stands, without ending the session.
|
|
408
|
+
*
|
|
409
|
+
* Reads back as a capture that was cut short — no directory, not marked closed — because that
|
|
410
|
+
* is precisely what it is. Recording continues afterwards.
|
|
411
|
+
*
|
|
412
|
+
* @returns {ArrayBuffer}
|
|
413
|
+
*/
|
|
414
|
+
snapshot() {
|
|
415
|
+
assert.ok(this.#running, 'session is not running');
|
|
416
|
+
|
|
417
|
+
return this.#stream.snapshot();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* End the recording and hand back the capture.
|
|
422
|
+
*
|
|
423
|
+
* Calling it twice returns the same bytes rather than complaining; a session that hit its
|
|
424
|
+
* frame limit has already stopped itself, and the caller awaiting `stop()` should still get
|
|
425
|
+
* the capture.
|
|
426
|
+
*
|
|
427
|
+
* @returns {ArrayBuffer}
|
|
428
|
+
*/
|
|
429
|
+
stop() {
|
|
430
|
+
if (this.#stopped) {
|
|
431
|
+
return this.#result;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
assert.ok(this.#running, 'session was never started');
|
|
435
|
+
|
|
436
|
+
this.#running = false;
|
|
437
|
+
this.#stopped = true;
|
|
438
|
+
|
|
439
|
+
this.#result = this.#stream.finish();
|
|
440
|
+
|
|
441
|
+
this.onComplete.send1(this.#result);
|
|
442
|
+
|
|
443
|
+
return this.#result;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* @type {ArrayBuffer|null}
|
|
448
|
+
*/
|
|
449
|
+
#result = null;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Resolves with the capture when the session ends, whichever way it ends.
|
|
453
|
+
*
|
|
454
|
+
* @returns {Promise<ArrayBuffer>}
|
|
455
|
+
*/
|
|
456
|
+
get done() {
|
|
457
|
+
if (this.#stopped) {
|
|
458
|
+
return Promise.resolve(this.#result);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return new Promise(resolve => {
|
|
462
|
+
this.onComplete.addOne(resolve);
|
|
463
|
+
|
|
464
|
+
if (this.#stopped) {
|
|
465
|
+
// guard against the session ending between the check above and the subscription
|
|
466
|
+
resolve(this.#result);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One GPU pass, as the device timed it.
|
|
3
|
+
*
|
|
4
|
+
* @author Alex Goldring
|
|
5
|
+
* @copyright Company Named Limited (c) 2026
|
|
6
|
+
*/
|
|
7
|
+
export class GPUProfileSpan {
|
|
8
|
+
/**
|
|
9
|
+
* The pass label, as it was given to `beginComputePass` / `beginRenderPass`.
|
|
10
|
+
*
|
|
11
|
+
* **Not a key.** Labels are neither unique nor stable, and one frame graph pass routinely opens
|
|
12
|
+
* several GPU passes with labels of their own — see {@link FrameGraph#onPassBegin}. At
|
|
13
|
+
* {@link GPUProfileLevel.STRUCTURE} and above, {@link graph_pass_id} is what identifies who
|
|
14
|
+
* recorded this.
|
|
15
|
+
*
|
|
16
|
+
* @type {string}
|
|
17
|
+
*/
|
|
18
|
+
label: string;
|
|
19
|
+
/**
|
|
20
|
+
* 'compute' or 'render'.
|
|
21
|
+
* @type {string}
|
|
22
|
+
*/
|
|
23
|
+
kind: string;
|
|
24
|
+
/**
|
|
25
|
+
* Nanoseconds from the frame's GPU epoch.
|
|
26
|
+
* @type {number}
|
|
27
|
+
*/
|
|
28
|
+
t_begin_ns: number;
|
|
29
|
+
/**
|
|
30
|
+
* Nanoseconds. Never negative — see {@link sgpt_write_frame} for what a device reporting an end
|
|
31
|
+
* before its begin is taken to mean.
|
|
32
|
+
* @type {number}
|
|
33
|
+
*/
|
|
34
|
+
duration_ns: number;
|
|
35
|
+
/**
|
|
36
|
+
* Which query set produced this. Timestamps from two different sets are not specified to share
|
|
37
|
+
* a clock domain, and a frame already spans several — one per command context. Carried so an
|
|
38
|
+
* anomaly that follows a set boundary is visible rather than unfalsifiable.
|
|
39
|
+
* @type {number}
|
|
40
|
+
*/
|
|
41
|
+
query_set_id: number;
|
|
42
|
+
/**
|
|
43
|
+
* Id of the frame graph pass that was executing when this pass was opened, or -1 when nothing
|
|
44
|
+
* was — work encoded outside the graph, or a capture below
|
|
45
|
+
* {@link GPUProfileLevel.STRUCTURE}.
|
|
46
|
+
* @type {number}
|
|
47
|
+
*/
|
|
48
|
+
graph_pass_id: number;
|
|
49
|
+
/**
|
|
50
|
+
* What this pass was asked to do — dispatch and draw counts, the pipeline, the workgroup size.
|
|
51
|
+
*
|
|
52
|
+
* Null below {@link GPUProfileLevel.WORKLOAD}, which is not the same as a pass that did
|
|
53
|
+
* nothing. A duration on its own says a pass is slow; this is what says whether it is slow
|
|
54
|
+
* because it is doing too much or because it is doing it badly.
|
|
55
|
+
*
|
|
56
|
+
* @type {GPUProfileWork|null}
|
|
57
|
+
*/
|
|
58
|
+
work: GPUProfileWork | null;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=GPUProfileSpan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GPUProfileSpan.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/device/timing/profile/GPUProfileSpan.js"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH;IACI;;;;;;;;;OASG;IACH,OAFU,MAAM,CAEL;IAEX;;;OAGG;IACH,MAFU,MAAM,CAEC;IAEjB;;;OAGG;IACH,YAFU,MAAM,CAED;IAEf;;;;OAIG;IACH,aAFU,MAAM,CAEA;IAEhB;;;;;OAKG;IACH,cAFU,MAAM,CAEC;IAEjB;;;;;OAKG;IACH,eAFU,MAAM,CAEG;IAEnB;;;;;;;;OAQG;IACH,MAFU,iBAAe,IAAI,CAEjB;CACf"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One GPU pass, as the device timed it.
|
|
3
|
+
*
|
|
4
|
+
* @author Alex Goldring
|
|
5
|
+
* @copyright Company Named Limited (c) 2026
|
|
6
|
+
*/
|
|
7
|
+
export class GPUProfileSpan {
|
|
8
|
+
/**
|
|
9
|
+
* The pass label, as it was given to `beginComputePass` / `beginRenderPass`.
|
|
10
|
+
*
|
|
11
|
+
* **Not a key.** Labels are neither unique nor stable, and one frame graph pass routinely opens
|
|
12
|
+
* several GPU passes with labels of their own — see {@link FrameGraph#onPassBegin}. At
|
|
13
|
+
* {@link GPUProfileLevel.STRUCTURE} and above, {@link graph_pass_id} is what identifies who
|
|
14
|
+
* recorded this.
|
|
15
|
+
*
|
|
16
|
+
* @type {string}
|
|
17
|
+
*/
|
|
18
|
+
label = "";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 'compute' or 'render'.
|
|
22
|
+
* @type {string}
|
|
23
|
+
*/
|
|
24
|
+
kind = "compute";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Nanoseconds from the frame's GPU epoch.
|
|
28
|
+
* @type {number}
|
|
29
|
+
*/
|
|
30
|
+
t_begin_ns = 0;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Nanoseconds. Never negative — see {@link sgpt_write_frame} for what a device reporting an end
|
|
34
|
+
* before its begin is taken to mean.
|
|
35
|
+
* @type {number}
|
|
36
|
+
*/
|
|
37
|
+
duration_ns = 0;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Which query set produced this. Timestamps from two different sets are not specified to share
|
|
41
|
+
* a clock domain, and a frame already spans several — one per command context. Carried so an
|
|
42
|
+
* anomaly that follows a set boundary is visible rather than unfalsifiable.
|
|
43
|
+
* @type {number}
|
|
44
|
+
*/
|
|
45
|
+
query_set_id = 0;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Id of the frame graph pass that was executing when this pass was opened, or -1 when nothing
|
|
49
|
+
* was — work encoded outside the graph, or a capture below
|
|
50
|
+
* {@link GPUProfileLevel.STRUCTURE}.
|
|
51
|
+
* @type {number}
|
|
52
|
+
*/
|
|
53
|
+
graph_pass_id = -1;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What this pass was asked to do — dispatch and draw counts, the pipeline, the workgroup size.
|
|
57
|
+
*
|
|
58
|
+
* Null below {@link GPUProfileLevel.WORKLOAD}, which is not the same as a pass that did
|
|
59
|
+
* nothing. A duration on its own says a pass is slow; this is what says whether it is slow
|
|
60
|
+
* because it is doing too much or because it is doing it badly.
|
|
61
|
+
*
|
|
62
|
+
* @type {GPUProfileWork|null}
|
|
63
|
+
*/
|
|
64
|
+
work = null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} name 'compute' or 'render'
|
|
3
|
+
* @returns {number} one of {@link GPUProfileSpanKind}
|
|
4
|
+
*/
|
|
5
|
+
export function gpu_profile_span_kind_from_name(name: string): number;
|
|
6
|
+
/**
|
|
7
|
+
* @param {number} kind one of {@link GPUProfileSpanKind}
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function gpu_profile_span_kind_name(kind: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* How a pass kind is written on the wire. One byte; the strings live in the model.
|
|
13
|
+
*/
|
|
14
|
+
export type GPUProfileSpanKind = number;
|
|
15
|
+
export namespace GPUProfileSpanKind {
|
|
16
|
+
let COMPUTE: number;
|
|
17
|
+
let RENDER: number;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=GPUProfileSpanKind.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GPUProfileSpanKind.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/device/timing/profile/GPUProfileSpanKind.js"],"names":[],"mappings":"AAUA;;;GAGG;AACH,sDAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;GAGG;AACH,iDAHW,MAAM,GACJ,MAAM,CAIlB;;;;iCArBS,MAAM"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a pass kind is written on the wire. One byte; the strings live in the model.
|
|
3
|
+
*
|
|
4
|
+
* @enum {number}
|
|
5
|
+
*/
|
|
6
|
+
export const GPUProfileSpanKind = {
|
|
7
|
+
COMPUTE: 0,
|
|
8
|
+
RENDER: 1
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} name 'compute' or 'render'
|
|
13
|
+
* @returns {number} one of {@link GPUProfileSpanKind}
|
|
14
|
+
*/
|
|
15
|
+
export function gpu_profile_span_kind_from_name(name) {
|
|
16
|
+
return name === 'render' ? GPUProfileSpanKind.RENDER : GPUProfileSpanKind.COMPUTE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {number} kind one of {@link GPUProfileSpanKind}
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function gpu_profile_span_kind_name(kind) {
|
|
24
|
+
return kind === GPUProfileSpanKind.RENDER ? 'render' : 'compute';
|
|
25
|
+
}
|