@aura3d/rendering 4.0.3 → 4.0.5

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 (240) hide show
  1. package/dist/EnvironmentPlatform.d.ts.map +1 -1
  2. package/dist/EnvironmentPlatform.js +6 -2
  3. package/dist/EnvironmentPlatform.js.map +1 -1
  4. package/dist/ForwardPass.d.ts +2 -0
  5. package/dist/ForwardPass.d.ts.map +1 -1
  6. package/dist/ForwardPass.js +70 -5
  7. package/dist/ForwardPass.js.map +1 -1
  8. package/dist/Geometry.d.ts +40 -0
  9. package/dist/Geometry.d.ts.map +1 -1
  10. package/dist/Geometry.js +39 -2
  11. package/dist/Geometry.js.map +1 -1
  12. package/dist/IndexBuffer.d.ts +16 -1
  13. package/dist/IndexBuffer.d.ts.map +1 -1
  14. package/dist/IndexBuffer.js +46 -1
  15. package/dist/IndexBuffer.js.map +1 -1
  16. package/dist/InstancedPBRMaterial.d.ts +2 -0
  17. package/dist/InstancedPBRMaterial.d.ts.map +1 -1
  18. package/dist/InstancedPBRMaterial.js +1 -0
  19. package/dist/InstancedPBRMaterial.js.map +1 -1
  20. package/dist/Material.d.ts +9 -0
  21. package/dist/Material.d.ts.map +1 -1
  22. package/dist/Material.js +3 -0
  23. package/dist/Material.js.map +1 -1
  24. package/dist/PBRMaterial.d.ts +2 -0
  25. package/dist/PBRMaterial.d.ts.map +1 -1
  26. package/dist/PBRMaterial.js +1 -0
  27. package/dist/PBRMaterial.js.map +1 -1
  28. package/dist/PortableShaderMaterial.d.ts +13 -3
  29. package/dist/PortableShaderMaterial.d.ts.map +1 -1
  30. package/dist/PortableShaderMaterial.js +24 -12
  31. package/dist/PortableShaderMaterial.js.map +1 -1
  32. package/dist/RenderDevice.d.ts +98 -0
  33. package/dist/RenderDevice.d.ts.map +1 -1
  34. package/dist/RenderDevice.js +129 -0
  35. package/dist/RenderDevice.js.map +1 -1
  36. package/dist/Renderer.d.ts +94 -0
  37. package/dist/Renderer.d.ts.map +1 -1
  38. package/dist/Renderer.js +321 -26
  39. package/dist/Renderer.js.map +1 -1
  40. package/dist/Sampler.d.ts +35 -0
  41. package/dist/Sampler.d.ts.map +1 -1
  42. package/dist/Sampler.js +96 -0
  43. package/dist/Sampler.js.map +1 -1
  44. package/dist/ShaderLibraryCore.d.ts +14 -0
  45. package/dist/ShaderLibraryCore.d.ts.map +1 -1
  46. package/dist/ShaderLibraryCore.js +94 -4
  47. package/dist/ShaderLibraryCore.js.map +1 -1
  48. package/dist/Texture.d.ts +53 -1
  49. package/dist/Texture.d.ts.map +1 -1
  50. package/dist/Texture.js +53 -2
  51. package/dist/Texture.js.map +1 -1
  52. package/dist/UnlitMaterial.d.ts +46 -0
  53. package/dist/UnlitMaterial.d.ts.map +1 -1
  54. package/dist/UnlitMaterial.js +32 -3
  55. package/dist/UnlitMaterial.js.map +1 -1
  56. package/dist/VertexFormat.d.ts +19 -1
  57. package/dist/VertexFormat.d.ts.map +1 -1
  58. package/dist/VertexFormat.js +40 -1
  59. package/dist/VertexFormat.js.map +1 -1
  60. package/dist/WebGL2Device.d.ts +16 -0
  61. package/dist/WebGL2Device.d.ts.map +1 -1
  62. package/dist/WebGL2Device.js +31 -6
  63. package/dist/WebGL2Device.js.map +1 -1
  64. package/dist/WebGPUDevice.d.ts +92 -1
  65. package/dist/WebGPUDevice.d.ts.map +1 -1
  66. package/dist/WebGPUDevice.js +417 -44
  67. package/dist/WebGPUDevice.js.map +1 -1
  68. package/dist/batching/BatchedGeometryPool.d.ts +22 -1
  69. package/dist/batching/BatchedGeometryPool.d.ts.map +1 -1
  70. package/dist/batching/BatchedGeometryPool.js +50 -2
  71. package/dist/batching/BatchedGeometryPool.js.map +1 -1
  72. package/dist/batching/DrawDataTexture.d.ts +21 -1
  73. package/dist/batching/DrawDataTexture.d.ts.map +1 -1
  74. package/dist/batching/DrawDataTexture.js +30 -4
  75. package/dist/batching/DrawDataTexture.js.map +1 -1
  76. package/dist/batching/MultiDrawBatch.d.ts +26 -0
  77. package/dist/batching/MultiDrawBatch.d.ts.map +1 -1
  78. package/dist/batching/MultiDrawBatch.js +64 -2
  79. package/dist/batching/MultiDrawBatch.js.map +1 -1
  80. package/dist/batching/index.d.ts +2 -2
  81. package/dist/batching/index.d.ts.map +1 -1
  82. package/dist/batching/index.js +2 -2
  83. package/dist/batching/index.js.map +1 -1
  84. package/dist/compute/ComputeContext.d.ts +84 -0
  85. package/dist/compute/ComputeContext.d.ts.map +1 -0
  86. package/dist/compute/ComputeContext.js +371 -0
  87. package/dist/compute/ComputeContext.js.map +1 -0
  88. package/dist/compute/index.d.ts +4 -0
  89. package/dist/compute/index.d.ts.map +1 -0
  90. package/dist/compute/index.js +3 -0
  91. package/dist/compute/index.js.map +1 -0
  92. package/dist/compute/types.d.ts +108 -0
  93. package/dist/compute/types.d.ts.map +1 -0
  94. package/dist/compute/types.js +25 -0
  95. package/dist/compute/types.js.map +1 -0
  96. package/dist/contracts/core.d.ts +1 -1
  97. package/dist/contracts/core.d.ts.map +1 -1
  98. package/dist/contracts/core.js.map +1 -1
  99. package/dist/contracts/flags.state.d.ts.map +1 -1
  100. package/dist/contracts/flags.state.js +20 -1
  101. package/dist/contracts/flags.state.js.map +1 -1
  102. package/dist/contracts/output.d.ts +1 -1
  103. package/dist/contracts/output.d.ts.map +1 -1
  104. package/dist/contracts/output.js.map +1 -1
  105. package/dist/contracts/post.d.ts +7 -0
  106. package/dist/contracts/post.d.ts.map +1 -1
  107. package/dist/contracts/post.js.map +1 -1
  108. package/dist/contracts/renderItem.d.ts +15 -0
  109. package/dist/contracts/renderItem.d.ts.map +1 -1
  110. package/dist/contracts/renderSource.d.ts +3 -1
  111. package/dist/contracts/renderSource.d.ts.map +1 -1
  112. package/dist/contracts/sampling.d.ts +23 -0
  113. package/dist/contracts/sampling.d.ts.map +1 -1
  114. package/dist/contracts/sampling.js.map +1 -1
  115. package/dist/environment/EnvironmentCache.d.ts.map +1 -1
  116. package/dist/environment/EnvironmentCache.js +7 -3
  117. package/dist/environment/EnvironmentCache.js.map +1 -1
  118. package/dist/environment/RoomEnvironmentScene.d.ts +2 -2
  119. package/dist/environment/RoomEnvironmentScene.d.ts.map +1 -1
  120. package/dist/environment/RoomEnvironmentScene.js.map +1 -1
  121. package/dist/geometry/Circle.d.ts +14 -0
  122. package/dist/geometry/Circle.d.ts.map +1 -0
  123. package/dist/geometry/Circle.js +31 -0
  124. package/dist/geometry/Circle.js.map +1 -0
  125. package/dist/geometry/Cone.d.ts +37 -0
  126. package/dist/geometry/Cone.d.ts.map +1 -0
  127. package/dist/geometry/Cone.js +129 -0
  128. package/dist/geometry/Cone.js.map +1 -0
  129. package/dist/geometry/Convex.d.ts +19 -0
  130. package/dist/geometry/Convex.d.ts.map +1 -0
  131. package/dist/geometry/Convex.js +27 -0
  132. package/dist/geometry/Convex.js.map +1 -0
  133. package/dist/geometry/Edges.d.ts +15 -0
  134. package/dist/geometry/Edges.d.ts.map +1 -0
  135. package/dist/geometry/Edges.js +98 -0
  136. package/dist/geometry/Edges.js.map +1 -0
  137. package/dist/geometry/Extrude.d.ts +29 -0
  138. package/dist/geometry/Extrude.d.ts.map +1 -0
  139. package/dist/geometry/Extrude.js +403 -0
  140. package/dist/geometry/Extrude.js.map +1 -0
  141. package/dist/geometry/Lathe.d.ts +20 -0
  142. package/dist/geometry/Lathe.d.ts.map +1 -0
  143. package/dist/geometry/Lathe.js +94 -0
  144. package/dist/geometry/Lathe.js.map +1 -0
  145. package/dist/geometry/Polyhedron.d.ts +21 -0
  146. package/dist/geometry/Polyhedron.d.ts.map +1 -0
  147. package/dist/geometry/Polyhedron.js +201 -0
  148. package/dist/geometry/Polyhedron.js.map +1 -0
  149. package/dist/geometry/Primitives.d.ts +38 -2
  150. package/dist/geometry/Primitives.d.ts.map +1 -1
  151. package/dist/geometry/Primitives.js +114 -0
  152. package/dist/geometry/Primitives.js.map +1 -1
  153. package/dist/geometry/Ring.d.ts +16 -0
  154. package/dist/geometry/Ring.d.ts.map +1 -0
  155. package/dist/geometry/Ring.js +44 -0
  156. package/dist/geometry/Ring.js.map +1 -0
  157. package/dist/geometry/RoundedBox.d.ts +22 -0
  158. package/dist/geometry/RoundedBox.d.ts.map +1 -0
  159. package/dist/geometry/RoundedBox.js +191 -0
  160. package/dist/geometry/RoundedBox.js.map +1 -0
  161. package/dist/geometry/Shape.d.ts +15 -0
  162. package/dist/geometry/Shape.d.ts.map +1 -0
  163. package/dist/geometry/Shape.js +44 -0
  164. package/dist/geometry/Shape.js.map +1 -0
  165. package/dist/geometry/Support.d.ts +137 -0
  166. package/dist/geometry/Support.d.ts.map +1 -0
  167. package/dist/geometry/Support.js +1066 -0
  168. package/dist/geometry/Support.js.map +1 -0
  169. package/dist/geometry/TorusKnot.d.ts +18 -0
  170. package/dist/geometry/TorusKnot.d.ts.map +1 -0
  171. package/dist/geometry/TorusKnot.js +83 -0
  172. package/dist/geometry/TorusKnot.js.map +1 -0
  173. package/dist/geometry/Tube.d.ts +19 -0
  174. package/dist/geometry/Tube.d.ts.map +1 -0
  175. package/dist/geometry/Tube.js +70 -0
  176. package/dist/geometry/Tube.js.map +1 -0
  177. package/dist/geometry/Wireframe.d.ts +19 -0
  178. package/dist/geometry/Wireframe.d.ts.map +1 -0
  179. package/dist/geometry/Wireframe.js +63 -0
  180. package/dist/geometry/Wireframe.js.map +1 -0
  181. package/dist/index.d.ts +38 -7
  182. package/dist/index.d.ts.map +1 -1
  183. package/dist/index.js +25 -3
  184. package/dist/index.js.map +1 -1
  185. package/dist/lanes/prd11.js +1 -1
  186. package/dist/lanes/prd11.js.map +1 -1
  187. package/dist/output/ToneMappingOperators.glsl.d.ts +1 -1
  188. package/dist/output/ToneMappingOperators.glsl.d.ts.map +1 -1
  189. package/dist/output/ToneMappingOperators.glsl.js +9 -1
  190. package/dist/output/ToneMappingOperators.glsl.js.map +1 -1
  191. package/dist/post/v2Entry.d.ts +1 -0
  192. package/dist/post/v2Entry.d.ts.map +1 -1
  193. package/dist/post/v2Entry.js.map +1 -1
  194. package/dist/post/v2Stages.d.ts +30 -1
  195. package/dist/post/v2Stages.d.ts.map +1 -1
  196. package/dist/post/v2Stages.js +87 -21
  197. package/dist/post/v2Stages.js.map +1 -1
  198. package/dist/program/ProgramCache.d.ts.map +1 -1
  199. package/dist/program/ProgramCache.js +12 -0
  200. package/dist/program/ProgramCache.js.map +1 -1
  201. package/dist/program/ProgramGenerator.d.ts.map +1 -1
  202. package/dist/program/ProgramGenerator.js +3 -0
  203. package/dist/program/ProgramGenerator.js.map +1 -1
  204. package/dist/reference/PostProcessPass.d.ts +1 -1
  205. package/dist/reference/PostProcessPass.d.ts.map +1 -1
  206. package/dist/reference/PostProcessPass.js +8 -1
  207. package/dist/reference/PostProcessPass.js.map +1 -1
  208. package/dist/renderer/CullingBatching.d.ts +44 -2
  209. package/dist/renderer/CullingBatching.d.ts.map +1 -1
  210. package/dist/renderer/CullingBatching.js +66 -11
  211. package/dist/renderer/CullingBatching.js.map +1 -1
  212. package/dist/shaders/chunks/lighting_mirrors.d.ts +2 -2
  213. package/dist/shaders/chunks/lighting_mirrors.d.ts.map +1 -1
  214. package/dist/shaders/chunks/lighting_mirrors.js.map +1 -1
  215. package/dist/shadows/DirectionalCascadeFitter.d.ts +8 -8
  216. package/dist/shadows/DirectionalCascadeFitter.d.ts.map +1 -1
  217. package/dist/shadows/DirectionalCascadeFitter.js.map +1 -1
  218. package/dist/shadows/ShadowAtlas.d.ts +5 -5
  219. package/dist/shadows/ShadowAtlas.d.ts.map +1 -1
  220. package/dist/shadows/ShadowAtlas.js.map +1 -1
  221. package/dist/vfx/ProceduralVolumeEmitter.d.ts +11 -0
  222. package/dist/vfx/ProceduralVolumeEmitter.d.ts.map +1 -1
  223. package/dist/vfx/ProceduralVolumeEmitter.js +20 -6
  224. package/dist/vfx/ProceduralVolumeEmitter.js.map +1 -1
  225. package/dist/webgl2/ContextLifecycle.d.ts +36 -0
  226. package/dist/webgl2/ContextLifecycle.d.ts.map +1 -1
  227. package/dist/webgl2/ContextLifecycle.js +110 -2
  228. package/dist/webgl2/ContextLifecycle.js.map +1 -1
  229. package/dist/webgl2/LegacyPost.d.ts +16 -1
  230. package/dist/webgl2/LegacyPost.d.ts.map +1 -1
  231. package/dist/webgl2/LegacyPost.js +49 -16
  232. package/dist/webgl2/LegacyPost.js.map +1 -1
  233. package/dist/webgl2/MultiDraw.d.ts.map +1 -1
  234. package/dist/webgl2/MultiDraw.js +42 -2
  235. package/dist/webgl2/MultiDraw.js.map +1 -1
  236. package/dist/webgl2/TextureUpload.d.ts +7 -0
  237. package/dist/webgl2/TextureUpload.d.ts.map +1 -1
  238. package/dist/webgl2/TextureUpload.js +54 -10
  239. package/dist/webgl2/TextureUpload.js.map +1 -1
  240. package/package.json +3 -3
@@ -5,6 +5,8 @@ import { normalizeWebGPUTemporalOptions, WEBGPU_TAA_FRAGMENT, WEBGPU_TAA_OUTPUT_
5
5
  import { defaultWebGPUBloomWeights, normalizeWebGPUColorGradeOptions, normalizeWebGPUBloomOptions, webgpuBlurFragment, webgpuBloomCompositeFragment, webgpuBrightExtractFragment, webgpuColorGradeFragment, webgpuFxaaFragment, WEBGPU_POST_VERTEX_WGSL } from "./webgpu/WebGPUPostShaders.js";
6
6
  import { DEFAULT_DEPTH_SHADER_MARKER } from "./ShaderLibraryCore.js";
7
7
  import { reflectShaderSources } from "./ShaderReflection.js";
8
+ import { recordPostSkipped, recordPostStage } from "./renderer/postSkipped.js";
9
+ import { registeredPostPasses } from "./contracts/post.js";
8
10
  import { Texture, bytesPerPixel, isCompressedTextureFormat } from "./Texture.js";
9
11
  import { isTextureBinding, TextureBinding } from "./TextureBinding.js";
10
12
  import {} from "./VertexFormat.js";
@@ -162,6 +164,13 @@ export class WebGPUDevice {
162
164
  drawCalls = 0;
163
165
  viewportWidth = 0;
164
166
  viewportHeight = 0;
167
+ /** SC-01: viewport/scissor override (origin bottom-left, three convention) and the target it was set on; applies only while that target is bound. */
168
+ viewStateTarget = null;
169
+ viewportOverride = null;
170
+ scissorOverride = null;
171
+ scissorTestOn = false;
172
+ /** XR/SC-01: viewport scoped to the default (canvas) target that survives `beginFrame`. */
173
+ persistentViewportRect = null;
165
174
  clearColor = [0, 0, 0, 1];
166
175
  lastError = null;
167
176
  nativeSubmissions = 0;
@@ -202,6 +211,14 @@ export class WebGPUDevice {
202
211
  shaders = new Set();
203
212
  renderTargets = new Set();
204
213
  nativeSampledTextures = new Map();
214
+ /**
215
+ * threejs-parity ST-03 (L4): `Texture.revision` recorded at the last native
216
+ * upload. A mismatch on a cached binding re-uploads the texture in place —
217
+ * the same revision gate WebGL2 has in `webgl2/TextureUpload.ts` — so
218
+ * canvas/video/data textures bound through `texture.*` handles refresh
219
+ * on `needsUpdate` instead of staying frozen at the first upload.
220
+ */
221
+ nativeSampledTextureRevisions = new Map();
205
222
  activeRenderTarget = null;
206
223
  canvasDepthTexture = null;
207
224
  canvasDepthView = null;
@@ -225,6 +242,10 @@ export class WebGPUDevice {
225
242
  }
226
243
  return new WebGPUDevice(adapter, device, canvasContext, presentationFormat);
227
244
  }
245
+ /** The native `GPUDevice` this render device owns; used by `createComputeContext` (SH-04). */
246
+ get nativeGpuDevice() {
247
+ return this.device;
248
+ }
228
249
  constructor(adapter, device, canvasContext, presentationFormat) {
229
250
  this.adapter = adapter;
230
251
  this.device = device;
@@ -661,9 +682,65 @@ export class WebGPUDevice {
661
682
  this.lastError = null;
662
683
  this.viewportWidth = width;
663
684
  this.viewportHeight = height;
685
+ this.viewportOverride = null;
686
+ this.scissorOverride = null;
687
+ this.scissorTestOn = false;
688
+ this.viewStateTarget = null;
664
689
  this.releaseDisposedNativeSampledTextures();
665
690
  this.ensureCanvasDepthAttachment(width, height);
666
691
  }
692
+ /** threejs-parity L7/SC-01: raster viewport in target pixels, origin bottom-left; converted to WebGPU's top-left origin per draw pass. */
693
+ setViewport(x, y, width, height, options) {
694
+ this.assertAlive();
695
+ if (!(width > 0) || !(height > 0)) {
696
+ throw new RenderDeviceError("Viewport dimensions must be positive", "INVALID_VIEWPORT", { width, height });
697
+ }
698
+ if (options?.persistent) {
699
+ this.persistentViewportRect = { x, y, width, height };
700
+ return;
701
+ }
702
+ this.viewportOverride = { x, y, width, height };
703
+ this.viewStateTarget = this.activeRenderTarget;
704
+ }
705
+ /** Clears the persistent viewport and the frame-scoped override. */
706
+ resetViewport() {
707
+ this.assertAlive();
708
+ this.persistentViewportRect = null;
709
+ this.viewportOverride = null;
710
+ this.viewStateTarget = null;
711
+ }
712
+ setScissor(x, y, width, height) {
713
+ this.assertAlive();
714
+ if (width < 0 || height < 0) {
715
+ throw new RenderDeviceError("Scissor dimensions must be non-negative", "INVALID_SCISSOR", { width, height });
716
+ }
717
+ this.scissorOverride = { x, y, width, height };
718
+ this.viewStateTarget = this.activeRenderTarget;
719
+ }
720
+ setScissorTest(enabled) {
721
+ this.assertAlive();
722
+ this.scissorTestOn = enabled;
723
+ this.viewStateTarget = this.activeRenderTarget;
724
+ }
725
+ /** Applies the live viewport/scissor override (if any) to a draw pass. Clears are full-attachment load ops and are never scissored. */
726
+ applyViewStateToPass(pass) {
727
+ const targetHeight = this.activeRenderTarget ? this.activeRenderTarget.height : this.viewportHeight;
728
+ if (this.viewStateTarget !== this.activeRenderTarget || !this.viewportOverride) {
729
+ const persistent = this.activeRenderTarget === null ? this.persistentViewportRect : null;
730
+ if (persistent)
731
+ pass.setViewport?.(persistent.x, targetHeight - persistent.y - persistent.height, persistent.width, persistent.height, 0, 1);
732
+ }
733
+ if (this.viewStateTarget !== this.activeRenderTarget)
734
+ return;
735
+ if (this.viewportOverride) {
736
+ const v = this.viewportOverride;
737
+ pass.setViewport?.(v.x, targetHeight - v.y - v.height, v.width, v.height, 0, 1);
738
+ }
739
+ if (this.scissorTestOn && this.scissorOverride) {
740
+ const r = this.scissorOverride;
741
+ pass.setScissorRect?.(r.x, targetHeight - r.y - r.height, r.width, r.height);
742
+ }
743
+ }
667
744
  clear(color) {
668
745
  this.assertFrame();
669
746
  this.clearColor = color;
@@ -1027,8 +1104,29 @@ export class WebGPUDevice {
1027
1104
  const owned = [];
1028
1105
  let current = source;
1029
1106
  const track = (target) => { owned.push(target); current = target; };
1107
+ /*
1108
+ * threejs-parity SH-03/SH-06: C-13 custom passes registered via
1109
+ * `registerPostPass` (`app.post.addPostPass` / `effects.shaderPass`) run on
1110
+ * the native WebGPU chain too. Anchor resolution mirrors the v2 graph's
1111
+ * named slots onto this chain's pass order; a registered pass missing its
1112
+ * WGSL fragment is recorded `WGSL_PROGRAM_MISSING:<id>` and skipped — never
1113
+ * silently, never as a GLSL fallback.
1114
+ */
1115
+ const customAt = this.webgpuCustomPassPositions(options.passes);
1116
+ const runCustomsAt = (position) => {
1117
+ const customs = customAt.get(position);
1118
+ if (!customs?.length)
1119
+ return;
1120
+ for (const custom of customs) {
1121
+ const out = this.executeWebGPUCustomPass(custom, current);
1122
+ if (out)
1123
+ track(out);
1124
+ }
1125
+ };
1030
1126
  try {
1031
- for (const pass of options.passes) {
1127
+ for (let passIndex = 0; passIndex < options.passes.length; passIndex += 1) {
1128
+ runCustomsAt(passIndex);
1129
+ const pass = options.passes[passIndex];
1032
1130
  if (pass.name === "bloom")
1033
1131
  track(this.executeWebGPUBloom(current, pass.options));
1034
1132
  else if (pass.name === "color-grade")
@@ -1061,6 +1159,7 @@ export class WebGPUDevice {
1061
1159
  current = target;
1062
1160
  }
1063
1161
  }
1162
+ runCustomsAt(options.passes.length);
1064
1163
  const target = options.outputTarget ?? null;
1065
1164
  if (target === current)
1066
1165
  throw new RenderDeviceError("Native WebGPU presentation source/output alias", "INVALID_RESOURCE");
@@ -1108,6 +1207,139 @@ export class WebGPUDevice {
1108
1207
  throw error;
1109
1208
  }
1110
1209
  }
1210
+ /**
1211
+ * SH-03: maps each registered custom pass's `insertAt` anchor onto a position
1212
+ * in this native chain — `position` runs BEFORE `options.passes[position]` and
1213
+ * `options.passes.length` runs after the last pass (before the present copy).
1214
+ * Deterministic fallbacks keep passes attached when their anchor stage is not
1215
+ * in the submitted chain. Passes sharing a position sort by `order` ascending
1216
+ * (stable — registration order preserved for omitted keys).
1217
+ */
1218
+ webgpuCustomPassPositions(passes) {
1219
+ const positions = new Map();
1220
+ const customs = registeredPostPasses();
1221
+ if (!customs.length)
1222
+ return positions;
1223
+ const indexOf = (...names) => passes.findIndex((pass) => names.includes(pass.name));
1224
+ const anchorPosition = (insertAt) => {
1225
+ switch (insertAt) {
1226
+ case "after-depth": return 0;
1227
+ case "before-taa": {
1228
+ const taa = indexOf("taa");
1229
+ if (taa >= 0)
1230
+ return taa;
1231
+ const bloom = indexOf("bloom");
1232
+ return bloom >= 0 ? bloom : 0;
1233
+ }
1234
+ case "after-taa": {
1235
+ const taa = indexOf("taa");
1236
+ if (taa >= 0)
1237
+ return taa + 1;
1238
+ // No TAA: the anchor still precedes bloom (v2 graph order is TAA → after-taa → bloom).
1239
+ const bloom = indexOf("bloom");
1240
+ return bloom >= 0 ? bloom : 0;
1241
+ }
1242
+ case "before-tonemap": {
1243
+ const tone = indexOf("tone-mapping");
1244
+ return tone >= 0 ? tone : passes.length;
1245
+ }
1246
+ case "after-tonemap": {
1247
+ const tone = indexOf("tone-mapping");
1248
+ return tone >= 0 ? tone + 1 : passes.length;
1249
+ }
1250
+ default: return passes.length;
1251
+ }
1252
+ };
1253
+ const orderKey = (pass) => typeof pass.order === "number" && Number.isFinite(pass.order) ? pass.order : Number.MAX_SAFE_INTEGER;
1254
+ for (const pass of [...customs].sort((a, b) => orderKey(a) - orderKey(b))) {
1255
+ const position = anchorPosition(pass.insertAt);
1256
+ const list = positions.get(position) ?? [];
1257
+ list.push(pass);
1258
+ positions.set(position, list);
1259
+ }
1260
+ return positions;
1261
+ }
1262
+ /**
1263
+ * SH-03/SH-06: one C-13 custom pass as a native WGSL fullscreen draw.
1264
+ *
1265
+ * Bind convention (fixed by runWebGPUFullscreenPass): binding 0 = uniform
1266
+ * buffer (`vec4f` array — `values[0]` is `vec4(texelSize, timeSeconds, 0)`,
1267
+ * declared uniforms follow in registration order, one vec4 each), binding 1 =
1268
+ * linear sampler, binding 2 = `u_color`, binding 3 = `u_depthLinear` (only
1269
+ * when `inputs` declares "depth"), binding 4 = `u_velocity` (only for
1270
+ * "velocity"). The authored `fragment.wgsl` is a complete fragment stage;
1271
+ * its `@fragment` entry point is detected or defaults to `fs_main`.
1272
+ *
1273
+ * Returns null when the pass is disabled or has no WGSL (recorded as
1274
+ * `WGSL_PROGRAM_MISSING:<id>` in the post skipped-reasons channel).
1275
+ */
1276
+ executeWebGPUCustomPass(pass, source) {
1277
+ const timeSeconds = typeof performance !== "undefined" ? performance.now() / 1000 : Date.now() / 1000;
1278
+ const frameContext = {
1279
+ device: this,
1280
+ width: source.width,
1281
+ height: source.height,
1282
+ timeSeconds,
1283
+ frameIndex: 0
1284
+ };
1285
+ if (pass.enabled && !pass.enabled(frameContext))
1286
+ return null;
1287
+ const wgsl = pass.fragment.wgsl;
1288
+ if (typeof wgsl !== "string" || !wgsl.trim()) {
1289
+ recordPostSkipped(`WGSL_PROGRAM_MISSING:${pass.id}`);
1290
+ return null;
1291
+ }
1292
+ const src = this.requireRenderTarget(source);
1293
+ const srcView = src.nativeView;
1294
+ if (!srcView) {
1295
+ throw new RenderDeviceError("WebGPU custom pass source target has no native view", "NATIVE_POST_UNSUPPORTED", { label: src.label });
1296
+ }
1297
+ recordPostStage(`custom:${pass.id}@${pass.insertAt}`);
1298
+ const wantsDepth = pass.inputs.includes("depth");
1299
+ const wantsVelocity = pass.inputs.includes("velocity");
1300
+ const target = this.createRenderTarget({
1301
+ width: src.width,
1302
+ height: src.height,
1303
+ format: pass.space === "display" ? "rgba8" : "rgba16f",
1304
+ label: `a3d-webgpu-custom-${pass.id}`
1305
+ });
1306
+ const views = [srcView];
1307
+ if (wantsDepth)
1308
+ views.push(this.requireRenderTarget(this.webgpuPostBlankTarget()).nativeView);
1309
+ if (wantsVelocity)
1310
+ views.push(this.requireRenderTarget(this.webgpuPostBlankTarget()).nativeView);
1311
+ const declared = pass.uniforms?.(frameContext) ?? {};
1312
+ const packed = [1 / src.width, 1 / src.height, timeSeconds, 0];
1313
+ for (const value of Object.values(declared)) {
1314
+ const scalars = Array.isArray(value) ? [...value] : [value];
1315
+ for (let offset = 0; offset < Math.max(1, scalars.length); offset += 4) {
1316
+ for (let lane = 0; lane < 4; lane += 1)
1317
+ packed.push(Number.isFinite(scalars[offset + lane]) ? scalars[offset + lane] : 0);
1318
+ }
1319
+ }
1320
+ const entryPoint = wgsl.match(/@fragment\s*fn\s+([A-Za-z_]\w*)/)?.[1] ?? "fs_main";
1321
+ try {
1322
+ this.runWebGPUFullscreenPass({
1323
+ label: `webgpu-custom:${pass.id}`,
1324
+ fragment: wgsl,
1325
+ entryPoint,
1326
+ uniforms: new Float32Array(packed),
1327
+ views,
1328
+ target
1329
+ });
1330
+ return target;
1331
+ }
1332
+ catch (error) {
1333
+ target.dispose();
1334
+ throw error;
1335
+ }
1336
+ }
1337
+ /** 1×1 stand-in for declared depth/velocity inputs this chain cannot supply (matches the WebGL2 blankTex fallback). */
1338
+ webgpuPostBlankTarget() {
1339
+ this.webgpuPostBlank ??= this.createRenderTarget({ width: 1, height: 1, format: "rgba8", label: "a3d-webgpu-custom-blank" });
1340
+ return this.webgpuPostBlank;
1341
+ }
1342
+ webgpuPostBlank = null;
1111
1343
  runWebGPUFullscreenPass(args) {
1112
1344
  // No assertFrame here on purpose: these are target-to-target passes with
1113
1345
  // their own encoders, so they run both inside a Renderer frame (scene ->
@@ -1229,6 +1461,7 @@ export class WebGPUDevice {
1229
1461
  for (const resource of this.nativeSampledTextures.values())
1230
1462
  resource.texture.destroy();
1231
1463
  this.nativeSampledTextures.clear();
1464
+ this.nativeSampledTextureRevisions.clear();
1232
1465
  this.canvasDepthTexture?.destroy();
1233
1466
  this.canvasDepthTexture = null;
1234
1467
  this.canvasDepthView = null;
@@ -1476,6 +1709,7 @@ export class WebGPUDevice {
1476
1709
  });
1477
1710
  this.nativeRenderPasses += 1;
1478
1711
  pass.setPipeline(pipeline);
1712
+ this.applyViewStateToPass(pass);
1479
1713
  pass.setVertexBuffer(0, vertexBuffer.handle);
1480
1714
  if (bindGroup)
1481
1715
  pass.setBindGroup?.(0, bindGroup);
@@ -1591,65 +1825,193 @@ export class WebGPUDevice {
1591
1825
  actual: true
1592
1826
  };
1593
1827
  }
1828
+ // ST-03 (L4): live sources (video) may mark a new frame before the
1829
+ // revision comparison runs.
1830
+ texture.pollContentRevision?.();
1594
1831
  let resource = this.nativeSampledTextures.get(texture);
1832
+ if (resource && this.nativeSampledTextureRevisions.get(texture) !== texture.revision) {
1833
+ // ST-03: revision-bumped content (Texture.update / handle needsUpdate /
1834
+ // a video frame) — re-upload into the same GPUTexture so every binding
1835
+ // built on it refreshes without re-allocating.
1836
+ if (this.uploadNativeSampledTextureRevision(texture, resource.texture)) {
1837
+ this.nativeSampledTextureRevisions.set(texture, texture.revision);
1838
+ }
1839
+ }
1595
1840
  if (!resource) {
1596
- if (texture.source) {
1597
- const sourceLevels = nativeTextureSourceLevels(texture);
1598
- if (sourceLevels.length === 0)
1599
- return null;
1600
- const nativeTexture = this.device.createTexture({
1601
- label: texture.label,
1602
- size: { width: texture.width, height: texture.height, depthOrArrayLayers: 1 },
1603
- format: webgpuSampledTextureFormat(texture, "rgba8"),
1604
- usage: TEXTURE_USAGE.TEXTURE_BINDING | TEXTURE_USAGE.COPY_DST,
1605
- mipLevelCount: sourceLevels.length
1606
- });
1841
+ const created = this.createNativeSampledTextureResource(texture);
1842
+ if (!created)
1843
+ return null;
1844
+ resource = created;
1845
+ this.nativeSampledTextures.set(texture, created);
1846
+ this.nativeSampledTextureRevisions.set(texture, texture.revision);
1847
+ }
1848
+ return {
1849
+ view: resource.view,
1850
+ sampler: this.device.createSampler(webgpuSamplerDescriptor(binding.sampler)),
1851
+ actual: true
1852
+ };
1853
+ }
1854
+ /**
1855
+ * threejs-parity ST-01/ST-02/ST-03 (L4): allocate and upload the native
1856
+ * texture for a `Texture`. Source-backed textures prefer
1857
+ * `queue.copyExternalImageToTexture` (the level-0 external-image copy
1858
+ * replaces the draw-to-canvas readback entirely); when a mip chain is
1859
+ * requested — or the queue predates the copy — the canvas readback path
1860
+ * still produces real downsized levels so mipmap samplers stay complete.
1861
+ * `2d-array` textures upload every layer in a single writeTexture and cube
1862
+ * textures write all six faces at `origin.z = face`.
1863
+ */
1864
+ createNativeSampledTextureResource(texture) {
1865
+ if (!this.device.createTexture)
1866
+ return null;
1867
+ if (texture.source) {
1868
+ const copyExternalImageToTexture = this.device.queue.copyExternalImageToTexture;
1869
+ const useExternalCopy = typeof copyExternalImageToTexture === "function" && !texture.generateMipmaps;
1870
+ const sourceLevels = useExternalCopy ? [] : nativeTextureSourceLevels(texture);
1871
+ if (!useExternalCopy && sourceLevels.length === 0)
1872
+ return null;
1873
+ const nativeTexture = this.device.createTexture({
1874
+ label: texture.label,
1875
+ size: { width: texture.width, height: texture.height, depthOrArrayLayers: 1 },
1876
+ format: webgpuSampledTextureFormat(texture, "rgba8"),
1877
+ // copyExternalImageToTexture additionally requires RENDER_ATTACHMENT.
1878
+ usage: TEXTURE_USAGE.TEXTURE_BINDING | TEXTURE_USAGE.COPY_DST | (useExternalCopy ? TEXTURE_USAGE.RENDER_ATTACHMENT : 0),
1879
+ mipLevelCount: useExternalCopy ? 1 : sourceLevels.length
1880
+ });
1881
+ if (useExternalCopy) {
1882
+ copyExternalImageToTexture.call(this.device.queue, { source: texture.source, flipY: texture.flipY }, { texture: nativeTexture }, { width: texture.width, height: texture.height, depthOrArrayLayers: 1 });
1883
+ this.nativeTextureUploads += 1;
1884
+ }
1885
+ else {
1607
1886
  const writeTexture = this.device.queue.writeTexture;
1608
- if (!writeTexture)
1887
+ if (!writeTexture) {
1888
+ nativeTexture.destroy();
1609
1889
  return null;
1890
+ }
1610
1891
  sourceLevels.forEach((level, mipLevel) => {
1611
1892
  writeTexture.call(this.device.queue, { texture: nativeTexture, mipLevel }, level.data, { bytesPerRow: level.bytesPerRow, rowsPerImage: level.height }, { width: level.width, height: level.height, depthOrArrayLayers: 1 });
1612
1893
  this.nativeTextureUploads += 1;
1613
1894
  });
1614
- resource = { texture: nativeTexture, view: nativeTexture.createView() };
1615
1895
  }
1616
- else {
1617
- const sampledTexture = nativeSampledTextureLevels(texture);
1618
- if (!sampledTexture)
1619
- return null;
1620
- const { levels, format } = sampledTexture;
1621
- const baseLevel = levels[0];
1622
- if (!baseLevel)
1623
- return null;
1624
- const levelBytesPerPixel = bytesPerPixel(format);
1625
- if (levels.some((level) => level.data.byteLength < level.width * level.height * levelBytesPerPixel))
1626
- return null;
1627
- const nativeTexture = this.device.createTexture({
1628
- label: texture.label,
1629
- size: { width: baseLevel.width, height: baseLevel.height, depthOrArrayLayers: 1 },
1630
- format: webgpuSampledTextureFormat(texture, format),
1631
- usage: TEXTURE_USAGE.TEXTURE_BINDING | TEXTURE_USAGE.COPY_DST,
1632
- mipLevelCount: levels.length
1633
- });
1634
- for (const [mipLevel, level] of levels.entries()) {
1635
- this.device.queue.writeTexture({ texture: nativeTexture, mipLevel }, level.data, { bytesPerRow: level.width * levelBytesPerPixel, rowsPerImage: level.height }, { width: level.width, height: level.height, depthOrArrayLayers: 1 });
1636
- this.nativeTextureUploads += 1;
1637
- }
1638
- resource = { texture: nativeTexture, view: nativeTexture.createView() };
1896
+ return { texture: nativeTexture, view: nativeTexture.createView() };
1897
+ }
1898
+ if (texture.dimension === "cube") {
1899
+ const faces = texture.cubeFaces;
1900
+ if (faces.length !== 6)
1901
+ return null;
1902
+ const mipLevelCount = faces[0]?.mipLevels.length ?? 0;
1903
+ if (mipLevelCount === 0)
1904
+ return null;
1905
+ const nativeTexture = this.device.createTexture({
1906
+ label: texture.label,
1907
+ size: { width: texture.width, height: texture.height, depthOrArrayLayers: 6 },
1908
+ format: webgpuSampledTextureFormat(texture, texture.format),
1909
+ usage: TEXTURE_USAGE.TEXTURE_BINDING | TEXTURE_USAGE.COPY_DST,
1910
+ mipLevelCount
1911
+ });
1912
+ if (!this.writeNativeCubeFaceLevels(texture, nativeTexture)) {
1913
+ nativeTexture.destroy();
1914
+ return null;
1639
1915
  }
1640
- this.nativeSampledTextures.set(texture, resource);
1916
+ return { texture: nativeTexture, view: nativeTexture.createView({ dimension: "cube" }) };
1641
1917
  }
1642
- return {
1643
- view: resource.view,
1644
- sampler: this.device.createSampler(webgpuSamplerDescriptor(binding.sampler)),
1645
- actual: true
1646
- };
1918
+ const sampledTexture = nativeSampledTextureLevels(texture);
1919
+ if (!sampledTexture)
1920
+ return null;
1921
+ const { levels, format } = sampledTexture;
1922
+ const baseLevel = levels[0];
1923
+ if (!baseLevel)
1924
+ return null;
1925
+ const levelBytesPerPixel = bytesPerPixel(format);
1926
+ if (levels.some((level) => level.data.byteLength < level.width * level.height * levelBytesPerPixel))
1927
+ return null;
1928
+ const depthOrArrayLayers = texture.dimension === "2d-array" ? texture.layers : 1;
1929
+ const nativeTexture = this.device.createTexture({
1930
+ label: texture.label,
1931
+ size: { width: baseLevel.width, height: baseLevel.height, depthOrArrayLayers },
1932
+ format: webgpuSampledTextureFormat(texture, format),
1933
+ usage: TEXTURE_USAGE.TEXTURE_BINDING | TEXTURE_USAGE.COPY_DST,
1934
+ mipLevelCount: levels.length
1935
+ });
1936
+ if (!this.writeNativeTextureLevels(texture, nativeTexture, levels, format)) {
1937
+ nativeTexture.destroy();
1938
+ return null;
1939
+ }
1940
+ return { texture: nativeTexture, view: nativeTexture.createView() };
1941
+ }
1942
+ /**
1943
+ * threejs-parity ST-01/ST-03 (L4): push every mip level of `levels` into
1944
+ * `nativeTexture`; for `2d-array` textures a single writeTexture covers all
1945
+ * `texture.layers` slices (the level data is stored layer-contiguous).
1946
+ */
1947
+ writeNativeTextureLevels(texture, nativeTexture, levels, format) {
1948
+ const writeTexture = this.device.queue.writeTexture;
1949
+ if (!writeTexture)
1950
+ return false;
1951
+ const levelBytesPerPixel = bytesPerPixel(format);
1952
+ for (const [mipLevel, level] of levels.entries()) {
1953
+ writeTexture.call(this.device.queue, { texture: nativeTexture, mipLevel }, level.data, { bytesPerRow: level.width * levelBytesPerPixel, rowsPerImage: level.height }, { width: level.width, height: level.height, depthOrArrayLayers: texture.dimension === "2d-array" ? texture.layers : 1 });
1954
+ this.nativeTextureUploads += 1;
1955
+ }
1956
+ return true;
1957
+ }
1958
+ /** threejs-parity ST-01 (L4): per-face per-level writeTexture for cube textures. */
1959
+ writeNativeCubeFaceLevels(texture, nativeTexture) {
1960
+ if (!this.device.queue.writeTexture)
1961
+ return false;
1962
+ const faceOrder = ["px", "nx", "py", "ny", "pz", "nz"];
1963
+ const levelBytesPerPixel = bytesPerPixel(texture.format);
1964
+ for (const [faceIndex, face] of faceOrder.entries()) {
1965
+ const faceData = texture.cubeFaces.find((candidate) => candidate.face === face);
1966
+ if (!faceData)
1967
+ return false;
1968
+ for (const [mipLevel, level] of faceData.mipLevels.entries()) {
1969
+ this.device.queue.writeTexture({ texture: nativeTexture, mipLevel, origin: { z: faceIndex } }, level.data, { bytesPerRow: level.width * levelBytesPerPixel, rowsPerImage: level.height }, { width: level.width, height: level.height, depthOrArrayLayers: 1 });
1970
+ this.nativeTextureUploads += 1;
1971
+ }
1972
+ }
1973
+ return true;
1974
+ }
1975
+ /**
1976
+ * threejs-parity ST-03 (L4): in-place re-upload for a revision-bumped
1977
+ * texture. Mirrors the create paths — external-image copy for source
1978
+ * textures (the video per-frame path), whole-chain writeTexture otherwise.
1979
+ * Returns false when the queue lacks the needed upload entry point so the
1980
+ * cached binding keeps its last-good content instead of corrupting it.
1981
+ */
1982
+ uploadNativeSampledTextureRevision(texture, nativeTexture) {
1983
+ texture.pendingUpdateRegion = null;
1984
+ texture.needsUpdate = false;
1985
+ if (texture.source) {
1986
+ const copyExternalImageToTexture = this.device.queue.copyExternalImageToTexture;
1987
+ if (typeof copyExternalImageToTexture === "function" && !texture.generateMipmaps) {
1988
+ copyExternalImageToTexture.call(this.device.queue, { source: texture.source, flipY: texture.flipY }, { texture: nativeTexture }, { width: texture.width, height: texture.height, depthOrArrayLayers: 1 });
1989
+ this.nativeTextureUploads += 1;
1990
+ return true;
1991
+ }
1992
+ const sourceLevels = nativeTextureSourceLevels(texture);
1993
+ if (sourceLevels.length === 0 || !this.device.queue.writeTexture)
1994
+ return false;
1995
+ sourceLevels.forEach((level, mipLevel) => {
1996
+ this.device.queue.writeTexture({ texture: nativeTexture, mipLevel }, level.data, { bytesPerRow: level.bytesPerRow, rowsPerImage: level.height }, { width: level.width, height: level.height, depthOrArrayLayers: 1 });
1997
+ this.nativeTextureUploads += 1;
1998
+ });
1999
+ return true;
2000
+ }
2001
+ if (texture.dimension === "cube") {
2002
+ return this.writeNativeCubeFaceLevels(texture, nativeTexture);
2003
+ }
2004
+ const sampledTexture = nativeSampledTextureLevels(texture);
2005
+ if (!sampledTexture)
2006
+ return false;
2007
+ return this.writeNativeTextureLevels(texture, nativeTexture, sampledTexture.levels, sampledTexture.format);
1647
2008
  }
1648
2009
  releaseDisposedNativeSampledTextures() {
1649
2010
  for (const [texture, resource] of this.nativeSampledTextures) {
1650
2011
  if (texture instanceof Texture && texture.disposed) {
1651
2012
  resource.texture.destroy();
1652
2013
  this.nativeSampledTextures.delete(texture);
2014
+ this.nativeSampledTextureRevisions.delete(texture);
1653
2015
  }
1654
2016
  }
1655
2017
  }
@@ -2076,7 +2438,18 @@ function nativeTextureSourceLevels(texture) {
2076
2438
  if (!context)
2077
2439
  return levels;
2078
2440
  context.clearRect(0, 0, levelWidth, levelHeight);
2079
- context.drawImage(source, 0, 0, levelWidth, levelHeight);
2441
+ if (texture.flipY) {
2442
+ // threejs-parity ST-02 (L4): flip the source at draw time so the CPU
2443
+ // readback matches UNPACK_FLIP_Y / copyExternalImageToTexture flipY.
2444
+ context.save();
2445
+ context.translate(0, levelHeight);
2446
+ context.scale(1, -1);
2447
+ context.drawImage(source, 0, 0, levelWidth, levelHeight);
2448
+ context.restore();
2449
+ }
2450
+ else {
2451
+ context.drawImage(source, 0, 0, levelWidth, levelHeight);
2452
+ }
2080
2453
  const pixels = context.getImageData(0, 0, levelWidth, levelHeight).data;
2081
2454
  const sourceBytesPerRow = levelWidth * 4;
2082
2455
  const bytesPerRow = alignTo(sourceBytesPerRow, 256);