@zephyr3d/scene 0.9.14 → 0.9.15

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.
@@ -1,29 +1,36 @@
1
1
  import { getDevice } from '../../app/api.js';
2
2
 
3
- /**
4
- * Bridges the render graph's {@link RGTextureAllocator} interface to the
5
- * engine's device resource pool (`device.pool`).
6
- *
7
- * Transient textures are fetched from the pool on `allocate()` and
8
- * returned to the pool on `release()`, enabling automatic reuse
9
- * across frames without manual lifecycle management.
10
- *
11
- * Usage:
12
- * ```ts
13
- * const allocator = new DevicePoolAllocator();
14
- * const executor = new RenderGraphExecutor(allocator, width, height);
15
- * ```
16
- *
17
- * @public
3
+ /**
4
+ * Bridges the render graph's {@link RGTextureAllocator} interface to the
5
+ * engine's device resource pool (`device.pool`).
6
+ *
7
+ * Transient textures are fetched from the pool on `allocate()` and
8
+ * returned to the pool on `release()`, enabling automatic reuse
9
+ * across frames without manual lifecycle management.
10
+ *
11
+ * Usage:
12
+ * ```ts
13
+ * const allocator = new DevicePoolAllocator();
14
+ * const executor = new RenderGraphExecutor(allocator, width, height);
15
+ * ```
16
+ *
17
+ * @public
18
18
  */ class DevicePoolAllocator {
19
- /**
20
- * Allocate a transient texture from the device pool.
21
- *
22
- * @param desc - Texture descriptor from the render graph pass.
23
- * @param size - Resolved pixel dimensions.
24
- * @returns A pooled Texture2D instance.
19
+ _device;
20
+ /**
21
+ * Creates a new instance of the DevicePoolAllocator.
22
+ * @param device - Optional device instance to use for resource allocation. If not provided, the global device will be used.
23
+ */ constructor(device){
24
+ this._device = device ?? null;
25
+ }
26
+ /**
27
+ * Allocate a transient texture from the device pool.
28
+ *
29
+ * @param desc - Texture descriptor from the render graph pass.
30
+ * @param size - Resolved pixel dimensions.
31
+ * @returns A pooled Texture2D instance.
25
32
  */ allocate(desc, size) {
26
- const device = getDevice();
33
+ const device = this._device ?? getDevice();
27
34
  const mipmapping = (desc.mipLevels ?? 1) > 1;
28
35
  const texture = device.pool.fetchTemporalTexture2D(false, desc.format, size.width, size.height, mipmapping);
29
36
  if (desc.mipLevels && texture.mipLevelCount < desc.mipLevels) {
@@ -32,41 +39,41 @@ import { getDevice } from '../../app/api.js';
32
39
  }
33
40
  return texture;
34
41
  }
35
- /**
36
- * Release a transient texture back to the device pool.
37
- *
38
- * @param texture - The texture to release.
42
+ /**
43
+ * Release a transient texture back to the device pool.
44
+ *
45
+ * @param texture - The texture to release.
39
46
  */ release(texture) {
40
- const device = getDevice();
47
+ const device = this._device ?? getDevice();
41
48
  device.pool.releaseTexture(texture);
42
49
  }
43
- /**
44
- * Retain a pooled texture so it can be owned outside the graph lifetime.
45
- *
46
- * @param texture - The texture to retain.
50
+ /**
51
+ * Retain a pooled texture so it can be owned outside the graph lifetime.
52
+ *
53
+ * @param texture - The texture to retain.
47
54
  */ retain(texture) {
48
- const device = getDevice();
55
+ const device = this._device ?? getDevice();
49
56
  device.pool.retainTexture(texture);
50
57
  }
51
- /**
52
- * Allocate a temporary framebuffer from the device pool.
53
- *
54
- * @param desc - Framebuffer descriptor from the render graph pass.
55
- * @returns A pooled FrameBuffer instance.
58
+ /**
59
+ * Allocate a temporary framebuffer from the device pool.
60
+ *
61
+ * @param desc - Framebuffer descriptor from the render graph pass.
62
+ * @returns A pooled FrameBuffer instance.
56
63
  */ allocateFramebuffer(desc) {
57
- const device = getDevice();
64
+ const device = this._device ?? getDevice();
58
65
  const colors = Array.isArray(desc.colorAttachments) ? desc.colorAttachments : desc.colorAttachments ? [
59
66
  desc.colorAttachments
60
67
  ] : [];
61
68
  const depthAttachment = desc.depthAttachment ?? null;
62
69
  return device.pool.fetchTemporalFramebuffer(false, desc.width ?? 0, desc.height ?? 0, colors, depthAttachment, desc.mipmapping ?? false, desc.sampleCount ?? 1, desc.ignoreDepthStencil ?? true, desc.attachmentMipLevel ?? 0, desc.attachmentCubeface ?? 0, desc.attachmentLayer ?? 0);
63
70
  }
64
- /**
65
- * Release a temporary framebuffer back to the device pool.
66
- *
67
- * @param framebuffer - The framebuffer to release.
71
+ /**
72
+ * Release a temporary framebuffer back to the device pool.
73
+ *
74
+ * @param framebuffer - The framebuffer to release.
68
75
  */ releaseFramebuffer(framebuffer) {
69
- const device = getDevice();
76
+ const device = this._device ?? getDevice();
70
77
  device.pool.releaseFrameBuffer(framebuffer);
71
78
  }
72
79
  }
@@ -1 +1 @@
1
- {"version":3,"file":"device_pool_allocator.js","sources":["../../../src/render/rendergraph/device_pool_allocator.ts"],"sourcesContent":["import type { FrameBuffer, Texture2D, TextureFormat } from '@zephyr3d/device';\r\nimport type { RGFramebufferDesc, RGTextureAllocator, RGTextureDesc, RGResolvedSize } from './types';\r\nimport { getDevice } from '../../app/api';\r\n\r\n/**\r\n * Bridges the render graph's {@link RGTextureAllocator} interface to the\r\n * engine's device resource pool (`device.pool`).\r\n *\r\n * Transient textures are fetched from the pool on `allocate()` and\r\n * returned to the pool on `release()`, enabling automatic reuse\r\n * across frames without manual lifecycle management.\r\n *\r\n * Usage:\r\n * ```ts\r\n * const allocator = new DevicePoolAllocator();\r\n * const executor = new RenderGraphExecutor(allocator, width, height);\r\n * ```\r\n *\r\n * @public\r\n */\r\nexport class DevicePoolAllocator implements RGTextureAllocator<Texture2D, FrameBuffer> {\r\n /**\r\n * Allocate a transient texture from the device pool.\r\n *\r\n * @param desc - Texture descriptor from the render graph pass.\r\n * @param size - Resolved pixel dimensions.\r\n * @returns A pooled Texture2D instance.\r\n */\r\n allocate(desc: RGTextureDesc, size: RGResolvedSize): Texture2D {\r\n const device = getDevice();\r\n const mipmapping = (desc.mipLevels ?? 1) > 1;\r\n const texture = device.pool.fetchTemporalTexture2D(\r\n false,\r\n desc.format,\r\n size.width,\r\n size.height,\r\n mipmapping\r\n );\r\n if (desc.mipLevels && texture.mipLevelCount < desc.mipLevels) {\r\n device.pool.releaseTexture(texture);\r\n throw new Error(\r\n `DevicePoolAllocator: texture \"${desc.label ?? '<unnamed>'}\" requested ${desc.mipLevels} ` +\r\n `mip levels, but only ${texture.mipLevelCount} were allocated.`\r\n );\r\n }\r\n return texture;\r\n }\r\n\r\n /**\r\n * Release a transient texture back to the device pool.\r\n *\r\n * @param texture - The texture to release.\r\n */\r\n release(texture: Texture2D): void {\r\n const device = getDevice();\r\n device.pool.releaseTexture(texture);\r\n }\r\n\r\n /**\r\n * Retain a pooled texture so it can be owned outside the graph lifetime.\r\n *\r\n * @param texture - The texture to retain.\r\n */\r\n retain(texture: Texture2D): void {\r\n const device = getDevice();\r\n device.pool.retainTexture(texture);\r\n }\r\n\r\n /**\r\n * Allocate a temporary framebuffer from the device pool.\r\n *\r\n * @param desc - Framebuffer descriptor from the render graph pass.\r\n * @returns A pooled FrameBuffer instance.\r\n */\r\n allocateFramebuffer(desc: RGFramebufferDesc): FrameBuffer {\r\n const device = getDevice();\r\n const colors = Array.isArray(desc.colorAttachments)\r\n ? (desc.colorAttachments as Array<Texture2D | TextureFormat>)\r\n : desc.colorAttachments\r\n ? [desc.colorAttachments as Texture2D | TextureFormat]\r\n : [];\r\n const depthAttachment = (desc.depthAttachment ?? null) as Texture2D | TextureFormat | null;\r\n return device.pool.fetchTemporalFramebuffer(\r\n false,\r\n desc.width ?? 0,\r\n desc.height ?? 0,\r\n colors,\r\n depthAttachment,\r\n desc.mipmapping ?? false,\r\n desc.sampleCount ?? 1,\r\n desc.ignoreDepthStencil ?? true,\r\n desc.attachmentMipLevel ?? 0,\r\n desc.attachmentCubeface ?? 0,\r\n desc.attachmentLayer ?? 0\r\n );\r\n }\r\n\r\n /**\r\n * Release a temporary framebuffer back to the device pool.\r\n *\r\n * @param framebuffer - The framebuffer to release.\r\n */\r\n releaseFramebuffer(framebuffer: FrameBuffer): void {\r\n const device = getDevice();\r\n device.pool.releaseFrameBuffer(framebuffer);\r\n }\r\n}\r\n"],"names":["DevicePoolAllocator","allocate","desc","size","device","getDevice","mipmapping","mipLevels","texture","pool","fetchTemporalTexture2D","format","width","height","mipLevelCount","releaseTexture","Error","label","release","retain","retainTexture","allocateFramebuffer","colors","Array","isArray","colorAttachments","depthAttachment","fetchTemporalFramebuffer","sampleCount","ignoreDepthStencil","attachmentMipLevel","attachmentCubeface","attachmentLayer","releaseFramebuffer","framebuffer","releaseFrameBuffer"],"mappings":";;AAIA;;;;;;;;;;;;;;;AAeC,IACM,MAAMA,mBAAAA,CAAAA;AACX;;;;;;AAMC,MACDC,QAASC,CAAAA,IAAmB,EAAEC,IAAoB,EAAa;AAC7D,QAAA,MAAMC,MAASC,GAAAA,SAAAA,EAAAA;AACf,QAAA,MAAMC,aAAa,CAACJ,KAAKK,SAAS,IAAI,CAAA,IAAK,CAAA;AAC3C,QAAA,MAAMC,OAAUJ,GAAAA,MAAAA,CAAOK,IAAI,CAACC,sBAAsB,CAChD,KAAA,EACAR,IAAKS,CAAAA,MAAM,EACXR,IAAKS,CAAAA,KAAK,EACVT,IAAAA,CAAKU,MAAM,EACXP,UAAAA,CAAAA;QAEF,IAAIJ,IAAAA,CAAKK,SAAS,IAAIC,OAAAA,CAAQM,aAAa,GAAGZ,IAAAA,CAAKK,SAAS,EAAE;YAC5DH,MAAOK,CAAAA,IAAI,CAACM,cAAc,CAACP,OAAAA,CAAAA;YAC3B,MAAM,IAAIQ,KACR,CAAA,CAAC,8BAA8B,EAAEd,KAAKe,KAAK,IAAI,WAAY,CAAA,YAAY,EAAEf,IAAAA,CAAKK,SAAS,CAAC,CAAC,CAAC,GACxF,CAAC,qBAAqB,EAAEC,OAAQM,CAAAA,aAAa,CAAC,gBAAgB,CAAC,CAAA;AAErE;QACA,OAAON,OAAAA;AACT;AAEA;;;;MAKAU,OAAAA,CAAQV,OAAkB,EAAQ;AAChC,QAAA,MAAMJ,MAASC,GAAAA,SAAAA,EAAAA;QACfD,MAAOK,CAAAA,IAAI,CAACM,cAAc,CAACP,OAAAA,CAAAA;AAC7B;AAEA;;;;MAKAW,MAAAA,CAAOX,OAAkB,EAAQ;AAC/B,QAAA,MAAMJ,MAASC,GAAAA,SAAAA,EAAAA;QACfD,MAAOK,CAAAA,IAAI,CAACW,aAAa,CAACZ,OAAAA,CAAAA;AAC5B;AAEA;;;;;MAMAa,mBAAAA,CAAoBnB,IAAuB,EAAe;AACxD,QAAA,MAAME,MAASC,GAAAA,SAAAA,EAAAA;AACf,QAAA,MAAMiB,MAASC,GAAAA,KAAAA,CAAMC,OAAO,CAACtB,IAAKuB,CAAAA,gBAAgB,CAC7CvB,GAAAA,IAAAA,CAAKuB,gBAAgB,GACtBvB,IAAKuB,CAAAA,gBAAgB,GACnB;AAACvB,YAAAA,IAAAA,CAAKuB;AAA8C,SAAA,GACpD,EAAE;QACR,MAAMC,eAAAA,GAAmBxB,IAAKwB,CAAAA,eAAe,IAAI,IAAA;AACjD,QAAA,OAAOtB,OAAOK,IAAI,CAACkB,wBAAwB,CACzC,OACAzB,IAAKU,CAAAA,KAAK,IAAI,CAAA,EACdV,KAAKW,MAAM,IAAI,GACfS,MACAI,EAAAA,eAAAA,EACAxB,KAAKI,UAAU,IAAI,KACnBJ,EAAAA,IAAAA,CAAK0B,WAAW,IAAI,CAAA,EACpB1B,IAAK2B,CAAAA,kBAAkB,IAAI,IAC3B3B,EAAAA,IAAAA,CAAK4B,kBAAkB,IAAI,GAC3B5B,IAAK6B,CAAAA,kBAAkB,IAAI,CAC3B7B,EAAAA,IAAAA,CAAK8B,eAAe,IAAI,CAAA,CAAA;AAE5B;AAEA;;;;MAKAC,kBAAAA,CAAmBC,WAAwB,EAAQ;AACjD,QAAA,MAAM9B,MAASC,GAAAA,SAAAA,EAAAA;QACfD,MAAOK,CAAAA,IAAI,CAAC0B,kBAAkB,CAACD,WAAAA,CAAAA;AACjC;AACF;;;;"}
1
+ {"version":3,"file":"device_pool_allocator.js","sources":["../../../src/render/rendergraph/device_pool_allocator.ts"],"sourcesContent":["import type { FrameBuffer, Texture2D, TextureFormat, AbstractDevice } from '@zephyr3d/device';\nimport type { RGFramebufferDesc, RGTextureAllocator, RGTextureDesc, RGResolvedSize } from './types';\nimport { getDevice } from '../../app/api';\nimport type { Nullable } from '@zephyr3d/base';\n\n/**\n * Bridges the render graph's {@link RGTextureAllocator} interface to the\n * engine's device resource pool (`device.pool`).\n *\n * Transient textures are fetched from the pool on `allocate()` and\n * returned to the pool on `release()`, enabling automatic reuse\n * across frames without manual lifecycle management.\n *\n * Usage:\n * ```ts\n * const allocator = new DevicePoolAllocator();\n * const executor = new RenderGraphExecutor(allocator, width, height);\n * ```\n *\n * @public\n */\nexport class DevicePoolAllocator implements RGTextureAllocator<Texture2D, FrameBuffer> {\n private _device: Nullable<AbstractDevice>;\n /**\n * Creates a new instance of the DevicePoolAllocator.\n * @param device - Optional device instance to use for resource allocation. If not provided, the global device will be used.\n */\n constructor(device?: AbstractDevice) {\n this._device = device ?? null;\n }\n /**\n * Allocate a transient texture from the device pool.\n *\n * @param desc - Texture descriptor from the render graph pass.\n * @param size - Resolved pixel dimensions.\n * @returns A pooled Texture2D instance.\n */\n allocate(desc: RGTextureDesc, size: RGResolvedSize): Texture2D {\n const device = this._device ?? getDevice();\n const mipmapping = (desc.mipLevels ?? 1) > 1;\n const texture = device.pool.fetchTemporalTexture2D(\n false,\n desc.format,\n size.width,\n size.height,\n mipmapping\n );\n if (desc.mipLevels && texture.mipLevelCount < desc.mipLevels) {\n device.pool.releaseTexture(texture);\n throw new Error(\n `DevicePoolAllocator: texture \"${desc.label ?? '<unnamed>'}\" requested ${desc.mipLevels} ` +\n `mip levels, but only ${texture.mipLevelCount} were allocated.`\n );\n }\n return texture;\n }\n\n /**\n * Release a transient texture back to the device pool.\n *\n * @param texture - The texture to release.\n */\n release(texture: Texture2D): void {\n const device = this._device ?? getDevice();\n device.pool.releaseTexture(texture);\n }\n\n /**\n * Retain a pooled texture so it can be owned outside the graph lifetime.\n *\n * @param texture - The texture to retain.\n */\n retain(texture: Texture2D): void {\n const device = this._device ?? getDevice();\n device.pool.retainTexture(texture);\n }\n\n /**\n * Allocate a temporary framebuffer from the device pool.\n *\n * @param desc - Framebuffer descriptor from the render graph pass.\n * @returns A pooled FrameBuffer instance.\n */\n allocateFramebuffer(desc: RGFramebufferDesc): FrameBuffer {\n const device = this._device ?? getDevice();\n const colors = Array.isArray(desc.colorAttachments)\n ? (desc.colorAttachments as Array<Texture2D | TextureFormat>)\n : desc.colorAttachments\n ? [desc.colorAttachments as Texture2D | TextureFormat]\n : [];\n const depthAttachment = (desc.depthAttachment ?? null) as Texture2D | TextureFormat | null;\n return device.pool.fetchTemporalFramebuffer(\n false,\n desc.width ?? 0,\n desc.height ?? 0,\n colors,\n depthAttachment,\n desc.mipmapping ?? false,\n desc.sampleCount ?? 1,\n desc.ignoreDepthStencil ?? true,\n desc.attachmentMipLevel ?? 0,\n desc.attachmentCubeface ?? 0,\n desc.attachmentLayer ?? 0\n );\n }\n\n /**\n * Release a temporary framebuffer back to the device pool.\n *\n * @param framebuffer - The framebuffer to release.\n */\n releaseFramebuffer(framebuffer: FrameBuffer): void {\n const device = this._device ?? getDevice();\n device.pool.releaseFrameBuffer(framebuffer);\n }\n}\n"],"names":["DevicePoolAllocator","_device","device","allocate","desc","size","getDevice","mipmapping","mipLevels","texture","pool","fetchTemporalTexture2D","format","width","height","mipLevelCount","releaseTexture","Error","label","release","retain","retainTexture","allocateFramebuffer","colors","Array","isArray","colorAttachments","depthAttachment","fetchTemporalFramebuffer","sampleCount","ignoreDepthStencil","attachmentMipLevel","attachmentCubeface","attachmentLayer","releaseFramebuffer","framebuffer","releaseFrameBuffer"],"mappings":";;AAKA;;;;;;;;;;;;;;;AAeC,IACM,MAAMA,mBAAAA,CAAAA;IACHC,OAAkC;AAC1C;;;MAIA,WAAA,CAAYC,MAAuB,CAAE;QACnC,IAAI,CAACD,OAAO,GAAGC,MAAU,IAAA,IAAA;AAC3B;AACA;;;;;;AAMC,MACDC,QAASC,CAAAA,IAAmB,EAAEC,IAAoB,EAAa;AAC7D,QAAA,MAAMH,MAAS,GAAA,IAAI,CAACD,OAAO,IAAIK,SAAAA,EAAAA;AAC/B,QAAA,MAAMC,aAAa,CAACH,KAAKI,SAAS,IAAI,CAAA,IAAK,CAAA;AAC3C,QAAA,MAAMC,OAAUP,GAAAA,MAAAA,CAAOQ,IAAI,CAACC,sBAAsB,CAChD,KAAA,EACAP,IAAKQ,CAAAA,MAAM,EACXP,IAAKQ,CAAAA,KAAK,EACVR,IAAAA,CAAKS,MAAM,EACXP,UAAAA,CAAAA;QAEF,IAAIH,IAAAA,CAAKI,SAAS,IAAIC,OAAAA,CAAQM,aAAa,GAAGX,IAAAA,CAAKI,SAAS,EAAE;YAC5DN,MAAOQ,CAAAA,IAAI,CAACM,cAAc,CAACP,OAAAA,CAAAA;YAC3B,MAAM,IAAIQ,KACR,CAAA,CAAC,8BAA8B,EAAEb,KAAKc,KAAK,IAAI,WAAY,CAAA,YAAY,EAAEd,IAAAA,CAAKI,SAAS,CAAC,CAAC,CAAC,GACxF,CAAC,qBAAqB,EAAEC,OAAQM,CAAAA,aAAa,CAAC,gBAAgB,CAAC,CAAA;AAErE;QACA,OAAON,OAAAA;AACT;AAEA;;;;MAKAU,OAAAA,CAAQV,OAAkB,EAAQ;AAChC,QAAA,MAAMP,MAAS,GAAA,IAAI,CAACD,OAAO,IAAIK,SAAAA,EAAAA;QAC/BJ,MAAOQ,CAAAA,IAAI,CAACM,cAAc,CAACP,OAAAA,CAAAA;AAC7B;AAEA;;;;MAKAW,MAAAA,CAAOX,OAAkB,EAAQ;AAC/B,QAAA,MAAMP,MAAS,GAAA,IAAI,CAACD,OAAO,IAAIK,SAAAA,EAAAA;QAC/BJ,MAAOQ,CAAAA,IAAI,CAACW,aAAa,CAACZ,OAAAA,CAAAA;AAC5B;AAEA;;;;;MAMAa,mBAAAA,CAAoBlB,IAAuB,EAAe;AACxD,QAAA,MAAMF,MAAS,GAAA,IAAI,CAACD,OAAO,IAAIK,SAAAA,EAAAA;AAC/B,QAAA,MAAMiB,MAASC,GAAAA,KAAAA,CAAMC,OAAO,CAACrB,IAAKsB,CAAAA,gBAAgB,CAC7CtB,GAAAA,IAAAA,CAAKsB,gBAAgB,GACtBtB,IAAKsB,CAAAA,gBAAgB,GACnB;AAACtB,YAAAA,IAAAA,CAAKsB;AAA8C,SAAA,GACpD,EAAE;QACR,MAAMC,eAAAA,GAAmBvB,IAAKuB,CAAAA,eAAe,IAAI,IAAA;AACjD,QAAA,OAAOzB,OAAOQ,IAAI,CAACkB,wBAAwB,CACzC,OACAxB,IAAKS,CAAAA,KAAK,IAAI,CAAA,EACdT,KAAKU,MAAM,IAAI,GACfS,MACAI,EAAAA,eAAAA,EACAvB,KAAKG,UAAU,IAAI,KACnBH,EAAAA,IAAAA,CAAKyB,WAAW,IAAI,CAAA,EACpBzB,IAAK0B,CAAAA,kBAAkB,IAAI,IAC3B1B,EAAAA,IAAAA,CAAK2B,kBAAkB,IAAI,GAC3B3B,IAAK4B,CAAAA,kBAAkB,IAAI,CAC3B5B,EAAAA,IAAAA,CAAK6B,eAAe,IAAI,CAAA,CAAA;AAE5B;AAEA;;;;MAKAC,kBAAAA,CAAmBC,WAAwB,EAAQ;AACjD,QAAA,MAAMjC,MAAS,GAAA,IAAI,CAACD,OAAO,IAAIK,SAAAA,EAAAA;QAC/BJ,MAAOQ,CAAAA,IAAI,CAAC0B,kBAAkB,CAACD,WAAAA,CAAAA;AACjC;AACF;;;;"}
@@ -1,5 +1,16 @@
1
1
  import { RGHandle } from './types.js';
2
+ import { getDevice } from '../../app/api.js';
2
3
 
4
+ const DEFAULT_PROFILING_OPTIONS = {
5
+ enabled: false,
6
+ graph: true,
7
+ pass: true,
8
+ subpass: true,
9
+ includePendingUploads: true,
10
+ allowCrossFrame: false,
11
+ maxPendingFrames: 3,
12
+ label: 'RenderGraph'
13
+ };
3
14
  /**
4
15
  * Executes a compiled render graph with automatic resource lifecycle management.
5
16
  *
@@ -16,6 +27,11 @@ import { RGHandle } from './types.js';
16
27
  * @typeParam TTexture - The concrete texture type (e.g. `Texture2D`).
17
28
  * @public
18
29
  */ class RenderGraphExecutor {
30
+ static _defaultProfilingOptions = false;
31
+ static _latestProfileResult = null;
32
+ static _latestPendingProfileFrame = null;
33
+ static _latestResolvedProfileSerial = 0;
34
+ static _nextProfileSerial = 0;
19
35
  /** @internal */ _allocator;
20
36
  /** @internal */ _backbufferWidth;
21
37
  /** @internal */ _backbufferHeight;
@@ -25,10 +41,45 @@ import { RGHandle } from './types.js';
25
41
  /** @internal */ _importedTextureAliases = new Map();
26
42
  /** @internal */ _resolvedImportedTextures = new Map();
27
43
  /** @internal */ _cleanupCallbacks = [];
28
- constructor(allocator, backbufferWidth, backbufferHeight){
44
+ /** @internal */ _profilingOptions;
45
+ /** @internal */ _pendingProfileFrames = [];
46
+ /** @internal */ _latestProfileResult = null;
47
+ /** @internal */ _latestResolvedProfileSerial = 0;
48
+ constructor(allocator, backbufferWidth, backbufferHeight, options){
29
49
  this._allocator = allocator;
30
50
  this._backbufferWidth = backbufferWidth;
31
51
  this._backbufferHeight = backbufferHeight;
52
+ this._profilingOptions = this._normalizeProfilingOptions(options?.profiling ?? RenderGraphExecutor._defaultProfilingOptions, options?.device);
53
+ }
54
+ /**
55
+ * Set the default profiling options used by newly constructed render graph executors.
56
+ */ static setDefaultProfilingOptions(options) {
57
+ RenderGraphExecutor._defaultProfilingOptions = options;
58
+ }
59
+ /**
60
+ * Get the latest resolved profile result from any render graph executor.
61
+ */ static getLatestProfileResult() {
62
+ return RenderGraphExecutor._latestProfileResult;
63
+ }
64
+ /**
65
+ * Resolve the latest pending render graph profile result from any executor.
66
+ */ static resolveProfileResult() {
67
+ return RenderGraphExecutor._latestPendingProfileFrame?.resolvePromise ?? Promise.resolve(RenderGraphExecutor._latestProfileResult);
68
+ }
69
+ /**
70
+ * Enable, disable, or update timestamp profiling for this executor.
71
+ */ setProfilingOptions(options) {
72
+ this._profilingOptions = this._normalizeProfilingOptions(options, this._profilingOptions.device);
73
+ }
74
+ /**
75
+ * Get the latest resolved profile result produced by this executor.
76
+ */ getLatestProfileResult() {
77
+ return this._latestProfileResult;
78
+ }
79
+ /**
80
+ * Resolve the latest pending profile result produced by this executor.
81
+ */ resolveProfileResult() {
82
+ return this._pendingProfileFrames[this._pendingProfileFrames.length - 1]?.resolvePromise ?? Promise.resolve(this._latestProfileResult);
32
83
  }
33
84
  /**
34
85
  * Update the backbuffer dimensions used for 'backbuffer-relative' sizing.
@@ -56,6 +107,7 @@ import { RGHandle } from './types.js';
56
107
  */ execute(compiled) {
57
108
  this._cleanupCallbacks.length = 0;
58
109
  this._resolveImportedTextureAliases(compiled);
110
+ const profileFrame = this._beginProfileFrame();
59
111
  // Build per-pass allocation and release schedules
60
112
  const allocateAt = new Map(); // passIndex -> transient texture resourceIds to allocate
61
113
  const releaseAt = new Map(); // passIndex -> transient texture resourceIds to release
@@ -111,15 +163,19 @@ import { RGHandle } from './types.js';
111
163
  // Execute the pass with exception safety for resource cleanup.
112
164
  // Release errors must not hide the original pass execution error.
113
165
  let passError = null;
166
+ const passProfile = this._beginPassProfileScope(profileFrame, pass);
114
167
  try {
115
168
  if (pass.subpasses.length > 0) {
116
169
  const accessScope = this._createAccessScope(pass);
117
170
  const ctx = this._createContext(accessScope);
118
171
  for (const subpass of pass.subpasses){
172
+ const subpassProfile = this._beginSubpassProfileScope(profileFrame, passProfile, pass, subpass.name);
119
173
  try {
120
174
  subpass.executeFn(ctx, pass.data);
121
175
  } catch (e) {
122
176
  throw this._wrapSubpassError(pass.name, subpass.name, e);
177
+ } finally{
178
+ this._endProfileScope(profileFrame, subpassProfile);
123
179
  }
124
180
  }
125
181
  } else if (pass.executeFn) {
@@ -129,6 +185,8 @@ import { RGHandle } from './types.js';
129
185
  }
130
186
  } catch (e) {
131
187
  passError = e;
188
+ } finally{
189
+ this._endProfileScope(profileFrame, passProfile);
132
190
  }
133
191
  let releaseError = null;
134
192
  const framebuffersToRelease = releaseFramebufferAt.get(i);
@@ -171,6 +229,7 @@ import { RGHandle } from './types.js';
171
229
  } catch (e) {
172
230
  executionError = e;
173
231
  } finally{
232
+ this._finishProfileFrame(profileFrame);
174
233
  let cleanupError = null;
175
234
  try {
176
235
  this._runCleanupCallbacks();
@@ -212,6 +271,216 @@ import { RGHandle } from './types.js';
212
271
  this._resolvedImportedTextures.clear();
213
272
  }
214
273
  // ─── Private ────────────────────────────────────────────────────────
274
+ /** @internal */ _normalizeProfilingOptions(options, fallbackDevice) {
275
+ const source = typeof options === 'object' ? options : {};
276
+ const enabled = typeof options === 'boolean' ? options : source.enabled ?? true;
277
+ return {
278
+ enabled,
279
+ graph: source.graph ?? DEFAULT_PROFILING_OPTIONS.graph,
280
+ pass: source.pass ?? DEFAULT_PROFILING_OPTIONS.pass,
281
+ subpass: source.subpass ?? DEFAULT_PROFILING_OPTIONS.subpass,
282
+ includePendingUploads: source.includePendingUploads ?? DEFAULT_PROFILING_OPTIONS.includePendingUploads,
283
+ allowCrossFrame: source.allowCrossFrame ?? DEFAULT_PROFILING_OPTIONS.allowCrossFrame,
284
+ maxPendingFrames: Math.max(1, source.maxPendingFrames ?? DEFAULT_PROFILING_OPTIONS.maxPendingFrames),
285
+ label: source.label ?? DEFAULT_PROFILING_OPTIONS.label,
286
+ device: source.device ?? fallbackDevice
287
+ };
288
+ }
289
+ /** @internal */ _getProfilingDevice() {
290
+ if (this._profilingOptions.device) {
291
+ return this._profilingOptions.device;
292
+ }
293
+ try {
294
+ return getDevice();
295
+ } catch {
296
+ return null;
297
+ }
298
+ }
299
+ /** @internal */ _beginProfileFrame() {
300
+ if (!this._profilingOptions.enabled) {
301
+ return null;
302
+ }
303
+ const device = this._getProfilingDevice();
304
+ const supported = !!device?.getDeviceCaps().miscCaps.supportTimestampQuery;
305
+ const rootResult = {
306
+ name: this._profilingOptions.label,
307
+ type: 'graph',
308
+ queryId: 0,
309
+ durationMs: 0,
310
+ status: supported ? 'resolved' : 'unsupported',
311
+ children: [],
312
+ message: supported ? undefined : 'GPU timestamp queries are not supported'
313
+ };
314
+ const result = {
315
+ frameId: device?.frameInfo.frameCounter ?? -1,
316
+ status: rootResult.status,
317
+ graph: rootResult,
318
+ passes: rootResult.children
319
+ };
320
+ const root = {
321
+ result: rootResult,
322
+ queryId: 0,
323
+ ended: false
324
+ };
325
+ const frame = {
326
+ serial: ++RenderGraphExecutor._nextProfileSerial,
327
+ device,
328
+ supported,
329
+ result,
330
+ root,
331
+ scopes: [],
332
+ resolvePromise: Promise.resolve(result)
333
+ };
334
+ if (this._profilingOptions.graph) {
335
+ this._beginTimestampScope(frame, root, this._profilingOptions.label);
336
+ }
337
+ return frame;
338
+ }
339
+ /** @internal */ _beginPassProfileScope(frame, pass) {
340
+ if (!frame || !this._profilingOptions.pass && !this._profilingOptions.subpass) {
341
+ return null;
342
+ }
343
+ return this._beginProfileScope(frame, frame.root, 'pass', pass.name, this._profilingOptions.pass);
344
+ }
345
+ /** @internal */ _beginSubpassProfileScope(frame, passScope, pass, subpassName) {
346
+ if (!frame || !this._profilingOptions.subpass) {
347
+ return null;
348
+ }
349
+ const parent = passScope ?? frame.root;
350
+ return this._beginProfileScope(frame, parent, 'subpass', subpassName, true, `${pass.name}/${subpassName}`);
351
+ }
352
+ /** @internal */ _beginProfileScope(frame, parent, type, name, queryEnabled, queryLabel) {
353
+ const status = frame.supported && !queryEnabled ? 'resolved' : frame.supported ? 'pending' : 'unsupported';
354
+ const result = {
355
+ name,
356
+ type,
357
+ queryId: 0,
358
+ durationMs: 0,
359
+ status,
360
+ children: [],
361
+ message: frame.supported ? undefined : 'GPU timestamp queries are not supported'
362
+ };
363
+ parent.result.children.push(result);
364
+ const scope = {
365
+ result,
366
+ queryId: 0,
367
+ ended: false
368
+ };
369
+ if (queryEnabled) {
370
+ this._beginTimestampScope(frame, scope, queryLabel ?? name);
371
+ }
372
+ return scope;
373
+ }
374
+ /** @internal */ _beginTimestampScope(frame, scope, label) {
375
+ if (!frame.supported || !frame.device) {
376
+ scope.result.status = 'unsupported';
377
+ scope.result.message = 'GPU timestamp queries are not supported';
378
+ return;
379
+ }
380
+ const queryId = frame.device.beginTimestampQuery(label, {
381
+ includePendingUploads: this._profilingOptions.includePendingUploads,
382
+ allowCrossFrame: this._profilingOptions.allowCrossFrame
383
+ });
384
+ scope.queryId = queryId;
385
+ scope.result.queryId = queryId;
386
+ if (queryId > 0) {
387
+ scope.result.status = 'pending';
388
+ frame.scopes.push(scope);
389
+ } else {
390
+ scope.result.status = 'unsupported';
391
+ scope.result.message = 'GPU timestamp query was not started';
392
+ }
393
+ }
394
+ /** @internal */ _endProfileScope(frame, scope) {
395
+ if (!frame || !frame.device || !scope || scope.ended) {
396
+ return;
397
+ }
398
+ scope.ended = true;
399
+ if (scope.queryId > 0) {
400
+ frame.device.endTimestampQuery(scope.queryId);
401
+ }
402
+ }
403
+ /** @internal */ _finishProfileFrame(frame) {
404
+ if (!frame) {
405
+ return;
406
+ }
407
+ this._endProfileScope(frame, frame.root);
408
+ if (frame.scopes.length === 0 || !frame.device) {
409
+ frame.result.status = this._aggregateProfileStatus(frame.result.graph);
410
+ this._publishProfileResult(frame);
411
+ return;
412
+ }
413
+ frame.resolvePromise = Promise.all(frame.scopes.map((scope)=>frame.device.resolveTimestampQuery(scope.queryId))).then((results)=>{
414
+ for(let i = 0; i < results.length; i++){
415
+ this._applyTimestampResult(frame.scopes[i], results[i]);
416
+ }
417
+ frame.result.status = this._aggregateProfileStatus(frame.result.graph);
418
+ this._publishProfileResult(frame);
419
+ return frame.result;
420
+ }).catch((err)=>{
421
+ const message = err instanceof Error ? err.message : String(err);
422
+ for (const scope of frame.scopes){
423
+ scope.result.status = 'failed';
424
+ scope.result.message = message;
425
+ }
426
+ frame.result.status = 'failed';
427
+ this._publishProfileResult(frame);
428
+ return frame.result;
429
+ });
430
+ this._trackPendingProfileFrame(frame);
431
+ }
432
+ /** @internal */ _trackPendingProfileFrame(frame) {
433
+ this._pendingProfileFrames.push(frame);
434
+ while(this._pendingProfileFrames.length > this._profilingOptions.maxPendingFrames){
435
+ this._pendingProfileFrames.shift();
436
+ }
437
+ RenderGraphExecutor._latestPendingProfileFrame = frame;
438
+ }
439
+ /** @internal */ _publishProfileResult(frame) {
440
+ this._pendingProfileFrames = this._pendingProfileFrames.filter((pending)=>pending !== frame);
441
+ if (frame.serial >= this._latestResolvedProfileSerial) {
442
+ this._latestResolvedProfileSerial = frame.serial;
443
+ this._latestProfileResult = frame.result;
444
+ }
445
+ if (frame.serial >= RenderGraphExecutor._latestResolvedProfileSerial) {
446
+ RenderGraphExecutor._latestResolvedProfileSerial = frame.serial;
447
+ RenderGraphExecutor._latestProfileResult = frame.result;
448
+ }
449
+ if (RenderGraphExecutor._latestPendingProfileFrame === frame) {
450
+ RenderGraphExecutor._latestPendingProfileFrame = null;
451
+ }
452
+ }
453
+ /** @internal */ _applyTimestampResult(scope, result) {
454
+ scope.result.queryId = result.id;
455
+ scope.result.durationMs = result.durationMs;
456
+ scope.result.status = result.status;
457
+ scope.result.message = result.message;
458
+ }
459
+ /** @internal */ _aggregateProfileStatus(scope) {
460
+ const statuses = [];
461
+ const collect = (node)=>{
462
+ statuses.push(node.status);
463
+ for (const child of node.children){
464
+ collect(child);
465
+ }
466
+ };
467
+ collect(scope);
468
+ for (const status of [
469
+ 'failed',
470
+ 'invalid',
471
+ 'exhausted',
472
+ 'pending',
473
+ 'auto-closed'
474
+ ]){
475
+ if (statuses.includes(status)) {
476
+ return status;
477
+ }
478
+ }
479
+ if (statuses.includes('unsupported')) {
480
+ return 'unsupported';
481
+ }
482
+ return 'resolved';
483
+ }
215
484
  /** @internal */ _resolveSize(desc) {
216
485
  const mode = desc.sizeMode ?? 'backbuffer-relative';
217
486
  if (mode === 'absolute') {