@aura3d/rendering 2.0.4 → 3.0.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.
Files changed (374) hide show
  1. package/dist/AtmosphereWetness.d.ts +61 -0
  2. package/dist/AtmosphereWetness.d.ts.map +1 -0
  3. package/dist/AtmosphereWetness.js +99 -0
  4. package/dist/AtmosphereWetness.js.map +1 -0
  5. package/dist/CascadedShadowMaps.d.ts.map +1 -1
  6. package/dist/CascadedShadowMaps.js +3 -1
  7. package/dist/CascadedShadowMaps.js.map +1 -1
  8. package/dist/ClusteredForwardLighting.d.ts +25 -1
  9. package/dist/ClusteredForwardLighting.d.ts.map +1 -1
  10. package/dist/ClusteredForwardLighting.js +80 -3
  11. package/dist/ClusteredForwardLighting.js.map +1 -1
  12. package/dist/DayNightSky.d.ts +63 -0
  13. package/dist/DayNightSky.d.ts.map +1 -0
  14. package/dist/DayNightSky.js +156 -0
  15. package/dist/DayNightSky.js.map +1 -0
  16. package/dist/EnvironmentPresetPack.d.ts +30 -0
  17. package/dist/EnvironmentPresetPack.d.ts.map +1 -0
  18. package/dist/EnvironmentPresetPack.js +49 -0
  19. package/dist/EnvironmentPresetPack.js.map +1 -0
  20. package/dist/ExtensionScalarAtlas.d.ts +18 -0
  21. package/dist/ExtensionScalarAtlas.d.ts.map +1 -0
  22. package/dist/ExtensionScalarAtlas.js +107 -0
  23. package/dist/ExtensionScalarAtlas.js.map +1 -0
  24. package/dist/ForwardPass.d.ts +65 -0
  25. package/dist/ForwardPass.d.ts.map +1 -1
  26. package/dist/ForwardPass.js +196 -2
  27. package/dist/ForwardPass.js.map +1 -1
  28. package/dist/InstancedPBRMaterial.d.ts +1 -0
  29. package/dist/InstancedPBRMaterial.d.ts.map +1 -1
  30. package/dist/InstancedPBRMaterial.js +3 -0
  31. package/dist/InstancedPBRMaterial.js.map +1 -1
  32. package/dist/InstancingDiagnostics.d.ts +69 -0
  33. package/dist/InstancingDiagnostics.d.ts.map +1 -0
  34. package/dist/InstancingDiagnostics.js +98 -0
  35. package/dist/InstancingDiagnostics.js.map +1 -0
  36. package/dist/LeanWebGL2Device.d.ts +265 -0
  37. package/dist/LeanWebGL2Device.d.ts.map +1 -0
  38. package/dist/LeanWebGL2Device.js +4001 -0
  39. package/dist/LeanWebGL2Device.js.map +1 -0
  40. package/dist/LightingRig.d.ts +19 -1
  41. package/dist/LightingRig.d.ts.map +1 -1
  42. package/dist/LightingRig.js +61 -1
  43. package/dist/LightingRig.js.map +1 -1
  44. package/dist/MaterialBinding.js +1 -1
  45. package/dist/MaterialBinding.js.map +1 -1
  46. package/dist/MeshConsolidation.d.ts +12 -0
  47. package/dist/MeshConsolidation.d.ts.map +1 -1
  48. package/dist/MeshConsolidation.js +28 -0
  49. package/dist/MeshConsolidation.js.map +1 -1
  50. package/dist/MorphTargetPlan.d.ts +29 -2
  51. package/dist/MorphTargetPlan.d.ts.map +1 -1
  52. package/dist/MorphTargetPlan.js +23 -4
  53. package/dist/MorphTargetPlan.js.map +1 -1
  54. package/dist/NormalMappedPBRMaterial.d.ts.map +1 -1
  55. package/dist/NormalMappedPBRMaterial.js +22 -2
  56. package/dist/NormalMappedPBRMaterial.js.map +1 -1
  57. package/dist/OceanSurface.d.ts +75 -0
  58. package/dist/OceanSurface.d.ts.map +1 -1
  59. package/dist/OceanSurface.js +211 -0
  60. package/dist/OceanSurface.js.map +1 -1
  61. package/dist/PBRMaterial.d.ts +1 -0
  62. package/dist/PBRMaterial.d.ts.map +1 -1
  63. package/dist/PBRMaterial.js +23 -2
  64. package/dist/PBRMaterial.js.map +1 -1
  65. package/dist/PbrReference.d.ts +48 -0
  66. package/dist/PbrReference.d.ts.map +1 -1
  67. package/dist/PbrReference.js +126 -0
  68. package/dist/PbrReference.js.map +1 -1
  69. package/dist/PlanarReflection.d.ts +221 -0
  70. package/dist/PlanarReflection.d.ts.map +1 -0
  71. package/dist/PlanarReflection.js +541 -0
  72. package/dist/PlanarReflection.js.map +1 -0
  73. package/dist/PostProcessPass.d.ts +32 -0
  74. package/dist/PostProcessPass.d.ts.map +1 -1
  75. package/dist/PostProcessPass.js +79 -36
  76. package/dist/PostProcessPass.js.map +1 -1
  77. package/dist/ProjectionMath.d.ts +3 -0
  78. package/dist/ProjectionMath.d.ts.map +1 -0
  79. package/dist/ProjectionMath.js +26 -0
  80. package/dist/ProjectionMath.js.map +1 -0
  81. package/dist/ReflectionSurfaces.d.ts +17 -0
  82. package/dist/ReflectionSurfaces.d.ts.map +1 -1
  83. package/dist/ReflectionSurfaces.js +103 -1
  84. package/dist/ReflectionSurfaces.js.map +1 -1
  85. package/dist/RenderDevice.d.ts +92 -0
  86. package/dist/RenderDevice.d.ts.map +1 -1
  87. package/dist/RenderDevice.js +85 -0
  88. package/dist/RenderDevice.js.map +1 -1
  89. package/dist/Renderer.d.ts +9 -0
  90. package/dist/Renderer.d.ts.map +1 -1
  91. package/dist/Renderer.js +97 -9
  92. package/dist/Renderer.js.map +1 -1
  93. package/dist/RendererPostprocessPlan.d.ts +64 -3
  94. package/dist/RendererPostprocessPlan.d.ts.map +1 -1
  95. package/dist/RendererPostprocessPlan.js +116 -12
  96. package/dist/RendererPostprocessPlan.js.map +1 -1
  97. package/dist/RendererVisualPipelineReport.d.ts.map +1 -1
  98. package/dist/RendererVisualPipelineReport.js +3 -1
  99. package/dist/RendererVisualPipelineReport.js.map +1 -1
  100. package/dist/Sampler.d.ts +29 -0
  101. package/dist/Sampler.d.ts.map +1 -1
  102. package/dist/Sampler.js +47 -0
  103. package/dist/Sampler.js.map +1 -1
  104. package/dist/ScreenSpaceReflectionPass.d.ts +43 -0
  105. package/dist/ScreenSpaceReflectionPass.d.ts.map +1 -0
  106. package/dist/ScreenSpaceReflectionPass.js +58 -0
  107. package/dist/ScreenSpaceReflectionPass.js.map +1 -0
  108. package/dist/SdfText.d.ts +250 -0
  109. package/dist/SdfText.d.ts.map +1 -0
  110. package/dist/SdfText.js +551 -0
  111. package/dist/SdfText.js.map +1 -0
  112. package/dist/ShaderChunks.d.ts.map +1 -1
  113. package/dist/ShaderChunks.js +30 -12
  114. package/dist/ShaderChunks.js.map +1 -1
  115. package/dist/ShaderLibrary.d.ts.map +1 -1
  116. package/dist/ShaderLibrary.js +601 -186
  117. package/dist/ShaderLibrary.js.map +1 -1
  118. package/dist/ShaderLibraryCore.d.ts +3 -0
  119. package/dist/ShaderLibraryCore.d.ts.map +1 -1
  120. package/dist/ShaderLibraryCore.js +73 -12
  121. package/dist/ShaderLibraryCore.js.map +1 -1
  122. package/dist/ShadowMap.d.ts +24 -0
  123. package/dist/ShadowMap.d.ts.map +1 -1
  124. package/dist/ShadowMap.js +43 -0
  125. package/dist/ShadowMap.js.map +1 -1
  126. package/dist/SkinnedLitMaterial.d.ts.map +1 -1
  127. package/dist/SkinnedLitMaterial.js +2 -0
  128. package/dist/SkinnedLitMaterial.js.map +1 -1
  129. package/dist/SkinningBounds.d.ts +10 -0
  130. package/dist/SkinningBounds.d.ts.map +1 -1
  131. package/dist/SkinningBounds.js +27 -0
  132. package/dist/SkinningBounds.js.map +1 -1
  133. package/dist/SpriteFlipbook.d.ts +60 -0
  134. package/dist/SpriteFlipbook.d.ts.map +1 -0
  135. package/dist/SpriteFlipbook.js +128 -0
  136. package/dist/SpriteFlipbook.js.map +1 -0
  137. package/dist/TemporalHistory.d.ts +37 -0
  138. package/dist/TemporalHistory.d.ts.map +1 -0
  139. package/dist/TemporalHistory.js +135 -0
  140. package/dist/TemporalHistory.js.map +1 -0
  141. package/dist/TemporalMath.d.ts +3 -0
  142. package/dist/TemporalMath.d.ts.map +1 -0
  143. package/dist/TemporalMath.js +9 -0
  144. package/dist/TemporalMath.js.map +1 -0
  145. package/dist/TerrainTiles.d.ts +120 -0
  146. package/dist/TerrainTiles.d.ts.map +1 -0
  147. package/dist/TerrainTiles.js +221 -0
  148. package/dist/TerrainTiles.js.map +1 -0
  149. package/dist/TexturedPBRMaterial.d.ts +4 -0
  150. package/dist/TexturedPBRMaterial.d.ts.map +1 -1
  151. package/dist/TexturedPBRMaterial.js +88 -5
  152. package/dist/TexturedPBRMaterial.js.map +1 -1
  153. package/dist/TexturedUnlitMaterial.d.ts.map +1 -1
  154. package/dist/TexturedUnlitMaterial.js +20 -2
  155. package/dist/TexturedUnlitMaterial.js.map +1 -1
  156. package/dist/UnlitMaterial.d.ts.map +1 -1
  157. package/dist/UnlitMaterial.js +20 -2
  158. package/dist/UnlitMaterial.js.map +1 -1
  159. package/dist/VertexFormat.d.ts +6 -0
  160. package/dist/VertexFormat.d.ts.map +1 -1
  161. package/dist/VertexFormat.js +12 -0
  162. package/dist/VertexFormat.js.map +1 -1
  163. package/dist/VolumetricFog.d.ts +55 -0
  164. package/dist/VolumetricFog.d.ts.map +1 -0
  165. package/dist/VolumetricFog.js +91 -0
  166. package/dist/VolumetricFog.js.map +1 -0
  167. package/dist/WaterSurface.d.ts +77 -0
  168. package/dist/WaterSurface.d.ts.map +1 -0
  169. package/dist/WaterSurface.js +133 -0
  170. package/dist/WaterSurface.js.map +1 -0
  171. package/dist/WebGL2Device.d.ts +73 -0
  172. package/dist/WebGL2Device.d.ts.map +1 -1
  173. package/dist/WebGL2Device.js +836 -79
  174. package/dist/WebGL2Device.js.map +1 -1
  175. package/dist/WebGPUDevice.d.ts +62 -1
  176. package/dist/WebGPUDevice.d.ts.map +1 -1
  177. package/dist/WebGPUDevice.js +844 -51
  178. package/dist/WebGPUDevice.js.map +1 -1
  179. package/dist/WebGPUExtensionAtlas.d.ts +6 -0
  180. package/dist/WebGPUExtensionAtlas.d.ts.map +1 -0
  181. package/dist/WebGPUExtensionAtlas.js +225 -0
  182. package/dist/WebGPUExtensionAtlas.js.map +1 -0
  183. package/dist/WebGPUSkinningLimits.d.ts +40 -0
  184. package/dist/WebGPUSkinningLimits.d.ts.map +1 -1
  185. package/dist/WebGPUSkinningLimits.js +32 -0
  186. package/dist/WebGPUSkinningLimits.js.map +1 -1
  187. package/dist/effects/CollisionModule.d.ts +9 -0
  188. package/dist/effects/CollisionModule.d.ts.map +1 -1
  189. package/dist/effects/CollisionModule.js +10 -0
  190. package/dist/effects/CollisionModule.js.map +1 -1
  191. package/dist/effects/ColorModule.d.ts +7 -0
  192. package/dist/effects/ColorModule.d.ts.map +1 -1
  193. package/dist/effects/ColorModule.js +23 -0
  194. package/dist/effects/ColorModule.js.map +1 -1
  195. package/dist/effects/ForceModule.d.ts +35 -0
  196. package/dist/effects/ForceModule.d.ts.map +1 -1
  197. package/dist/effects/ForceModule.js +49 -0
  198. package/dist/effects/ForceModule.js.map +1 -1
  199. package/dist/effects/GPUParticleBackend.d.ts +188 -0
  200. package/dist/effects/GPUParticleBackend.d.ts.map +1 -1
  201. package/dist/effects/GPUParticleBackend.js +710 -1
  202. package/dist/effects/GPUParticleBackend.js.map +1 -1
  203. package/dist/effects/HeightfieldModule.d.ts +58 -0
  204. package/dist/effects/HeightfieldModule.d.ts.map +1 -0
  205. package/dist/effects/HeightfieldModule.js +129 -0
  206. package/dist/effects/HeightfieldModule.js.map +1 -0
  207. package/dist/effects/LightingModule.d.ts +34 -0
  208. package/dist/effects/LightingModule.d.ts.map +1 -0
  209. package/dist/effects/LightingModule.js +54 -0
  210. package/dist/effects/LightingModule.js.map +1 -0
  211. package/dist/effects/ParticleDiagnostics.d.ts +8 -0
  212. package/dist/effects/ParticleDiagnostics.d.ts.map +1 -1
  213. package/dist/effects/ParticleDiagnostics.js +3 -0
  214. package/dist/effects/ParticleDiagnostics.js.map +1 -1
  215. package/dist/effects/ParticleModule.d.ts +8 -0
  216. package/dist/effects/ParticleModule.d.ts.map +1 -1
  217. package/dist/effects/ParticleModule.js.map +1 -1
  218. package/dist/effects/ParticleRenderer.d.ts +20 -0
  219. package/dist/effects/ParticleRenderer.d.ts.map +1 -1
  220. package/dist/effects/ParticleRenderer.js +57 -7
  221. package/dist/effects/ParticleRenderer.js.map +1 -1
  222. package/dist/effects/ParticleSystem.d.ts +45 -1
  223. package/dist/effects/ParticleSystem.d.ts.map +1 -1
  224. package/dist/effects/ParticleSystem.js +373 -4
  225. package/dist/effects/ParticleSystem.js.map +1 -1
  226. package/dist/effects/ResidentGPUParticleRenderer.d.ts +158 -0
  227. package/dist/effects/ResidentGPUParticleRenderer.d.ts.map +1 -0
  228. package/dist/effects/ResidentGPUParticleRenderer.js +576 -0
  229. package/dist/effects/ResidentGPUParticleRenderer.js.map +1 -0
  230. package/dist/effects/SizeModule.d.ts +4 -0
  231. package/dist/effects/SizeModule.d.ts.map +1 -1
  232. package/dist/effects/SizeModule.js +16 -0
  233. package/dist/effects/SizeModule.js.map +1 -1
  234. package/dist/effects/SubEmitterModule.d.ts +61 -0
  235. package/dist/effects/SubEmitterModule.d.ts.map +1 -0
  236. package/dist/effects/SubEmitterModule.js +119 -0
  237. package/dist/effects/SubEmitterModule.js.map +1 -0
  238. package/dist/effects/TrailModule.d.ts +38 -0
  239. package/dist/effects/TrailModule.d.ts.map +1 -1
  240. package/dist/effects/TrailModule.js +95 -2
  241. package/dist/effects/TrailModule.js.map +1 -1
  242. package/dist/effects/TurbulenceModule.d.ts +43 -0
  243. package/dist/effects/TurbulenceModule.d.ts.map +1 -0
  244. package/dist/effects/TurbulenceModule.js +130 -0
  245. package/dist/effects/TurbulenceModule.js.map +1 -0
  246. package/dist/effects/index.d.ts +4 -0
  247. package/dist/effects/index.d.ts.map +1 -1
  248. package/dist/effects/index.js +4 -0
  249. package/dist/effects/index.js.map +1 -1
  250. package/dist/extension-scalar-atlas.d.ts +3 -0
  251. package/dist/extension-scalar-atlas.d.ts.map +1 -0
  252. package/dist/extension-scalar-atlas.js +2 -0
  253. package/dist/extension-scalar-atlas.js.map +1 -0
  254. package/dist/index.d.ts +58 -19
  255. package/dist/index.d.ts.map +1 -1
  256. package/dist/index.js +32 -10
  257. package/dist/index.js.map +1 -1
  258. package/dist/lean/LeanProductionRenderer.d.ts.map +1 -1
  259. package/dist/lean/LeanProductionRenderer.js +2 -2
  260. package/dist/lean/LeanProductionRenderer.js.map +1 -1
  261. package/dist/lean-core-runtime.d.ts +13 -0
  262. package/dist/lean-core-runtime.d.ts.map +1 -0
  263. package/dist/lean-core-runtime.js +10 -0
  264. package/dist/lean-core-runtime.js.map +1 -0
  265. package/dist/materials/MaterialExtensions.js +3 -3
  266. package/dist/materials/MaterialExtensions.js.map +1 -1
  267. package/dist/postprocess/NativeBloomPyramid.d.ts +44 -0
  268. package/dist/postprocess/NativeBloomPyramid.d.ts.map +1 -0
  269. package/dist/postprocess/NativeBloomPyramid.js +84 -0
  270. package/dist/postprocess/NativeBloomPyramid.js.map +1 -0
  271. package/dist/postprocess/NativeLdrEffectLuts.d.ts +10 -1
  272. package/dist/postprocess/NativeLdrEffectLuts.d.ts.map +1 -1
  273. package/dist/postprocess/NativeLdrEffectLuts.js +19 -2
  274. package/dist/postprocess/NativeLdrEffectLuts.js.map +1 -1
  275. package/dist/postprocess/index.d.ts +2 -0
  276. package/dist/postprocess/index.d.ts.map +1 -1
  277. package/dist/postprocess/index.js +1 -0
  278. package/dist/postprocess/index.js.map +1 -1
  279. package/dist/production-runtime/PBRHDRPipeline.d.ts +22 -0
  280. package/dist/production-runtime/PBRHDRPipeline.d.ts.map +1 -1
  281. package/dist/production-runtime/PBRHDRPipeline.js +125 -0
  282. package/dist/production-runtime/PBRHDRPipeline.js.map +1 -1
  283. package/dist/production-runtime/ProductionRendererTypes.d.ts +1 -0
  284. package/dist/production-runtime/ProductionRendererTypes.d.ts.map +1 -1
  285. package/dist/production-runtime/ProductionRendererTypes.js.map +1 -1
  286. package/dist/production-runtime/ProductionRuntimeRenderer.d.ts +3 -0
  287. package/dist/production-runtime/ProductionRuntimeRenderer.d.ts.map +1 -1
  288. package/dist/production-runtime/ProductionRuntimeRenderer.js +8 -1
  289. package/dist/production-runtime/ProductionRuntimeRenderer.js.map +1 -1
  290. package/dist/production-runtime/ProductionWebGL2Renderer.d.ts +4 -0
  291. package/dist/production-runtime/ProductionWebGL2Renderer.d.ts.map +1 -1
  292. package/dist/production-runtime/ProductionWebGL2Renderer.js +15 -0
  293. package/dist/production-runtime/ProductionWebGL2Renderer.js.map +1 -1
  294. package/dist/production-runtime/ProductionWebGPURenderer.d.ts +58 -0
  295. package/dist/production-runtime/ProductionWebGPURenderer.d.ts.map +1 -1
  296. package/dist/production-runtime/ProductionWebGPURenderer.js +127 -0
  297. package/dist/production-runtime/ProductionWebGPURenderer.js.map +1 -1
  298. package/dist/production-runtime/framegraph/FrameGraph.d.ts +4 -2
  299. package/dist/production-runtime/framegraph/FrameGraph.d.ts.map +1 -1
  300. package/dist/production-runtime/framegraph/FrameGraph.js +62 -4
  301. package/dist/production-runtime/framegraph/FrameGraph.js.map +1 -1
  302. package/dist/production-runtime/framegraph/NativeFrameGraphBindings.d.ts +32 -0
  303. package/dist/production-runtime/framegraph/NativeFrameGraphBindings.d.ts.map +1 -0
  304. package/dist/production-runtime/framegraph/NativeFrameGraphBindings.js +111 -0
  305. package/dist/production-runtime/framegraph/NativeFrameGraphBindings.js.map +1 -0
  306. package/dist/production-runtime/framegraph/RenderPass.d.ts +23 -0
  307. package/dist/production-runtime/framegraph/RenderPass.d.ts.map +1 -1
  308. package/dist/production-runtime/framegraph/RenderPass.js +87 -1
  309. package/dist/production-runtime/framegraph/RenderPass.js.map +1 -1
  310. package/dist/production-runtime/index.d.ts +7 -4
  311. package/dist/production-runtime/index.d.ts.map +1 -1
  312. package/dist/production-runtime/index.js +4 -2
  313. package/dist/production-runtime/index.js.map +1 -1
  314. package/dist/production-runtime/passes/DepthPrepass.d.ts +18 -1
  315. package/dist/production-runtime/passes/DepthPrepass.d.ts.map +1 -1
  316. package/dist/production-runtime/passes/DepthPrepass.js +41 -2
  317. package/dist/production-runtime/passes/DepthPrepass.js.map +1 -1
  318. package/dist/production-runtime/passes/FramegraphTopology.d.ts +13 -0
  319. package/dist/production-runtime/passes/FramegraphTopology.d.ts.map +1 -0
  320. package/dist/production-runtime/passes/FramegraphTopology.js +59 -0
  321. package/dist/production-runtime/passes/FramegraphTopology.js.map +1 -0
  322. package/dist/production-runtime/passes/OpaquePass.d.ts +19 -1
  323. package/dist/production-runtime/passes/OpaquePass.d.ts.map +1 -1
  324. package/dist/production-runtime/passes/OpaquePass.js +43 -2
  325. package/dist/production-runtime/passes/OpaquePass.js.map +1 -1
  326. package/dist/production-runtime/passes/ShadowPass.d.ts +19 -1
  327. package/dist/production-runtime/passes/ShadowPass.d.ts.map +1 -1
  328. package/dist/production-runtime/passes/ShadowPass.js +45 -2
  329. package/dist/production-runtime/passes/ShadowPass.js.map +1 -1
  330. package/dist/production-runtime/passes/SkyboxPass.d.ts +17 -1
  331. package/dist/production-runtime/passes/SkyboxPass.d.ts.map +1 -1
  332. package/dist/production-runtime/passes/SkyboxPass.js +41 -2
  333. package/dist/production-runtime/passes/SkyboxPass.js.map +1 -1
  334. package/dist/production-runtime/passes/ToneMappingPass.d.ts +26 -1
  335. package/dist/production-runtime/passes/ToneMappingPass.d.ts.map +1 -1
  336. package/dist/production-runtime/passes/ToneMappingPass.js +62 -2
  337. package/dist/production-runtime/passes/ToneMappingPass.js.map +1 -1
  338. package/dist/production-runtime/passes/TransparentPass.d.ts +19 -1
  339. package/dist/production-runtime/passes/TransparentPass.d.ts.map +1 -1
  340. package/dist/production-runtime/passes/TransparentPass.js +47 -2
  341. package/dist/production-runtime/passes/TransparentPass.js.map +1 -1
  342. package/dist/reflection-surfaces.d.ts +5 -0
  343. package/dist/reflection-surfaces.d.ts.map +1 -0
  344. package/dist/reflection-surfaces.js +3 -0
  345. package/dist/reflection-surfaces.js.map +1 -0
  346. package/dist/shadows/CascadeHysteresis.d.ts +67 -0
  347. package/dist/shadows/CascadeHysteresis.d.ts.map +1 -0
  348. package/dist/shadows/CascadeHysteresis.js +104 -0
  349. package/dist/shadows/CascadeHysteresis.js.map +1 -0
  350. package/dist/shadows/ContactShadows.d.ts +52 -0
  351. package/dist/shadows/ContactShadows.d.ts.map +1 -1
  352. package/dist/shadows/ContactShadows.js +122 -0
  353. package/dist/shadows/ContactShadows.js.map +1 -1
  354. package/dist/shadows/SpotShadowMaps.d.ts +77 -0
  355. package/dist/shadows/SpotShadowMaps.d.ts.map +1 -0
  356. package/dist/shadows/SpotShadowMaps.js +106 -0
  357. package/dist/shadows/SpotShadowMaps.js.map +1 -0
  358. package/dist/webgpu/WebGPUPostProcess.d.ts +2 -0
  359. package/dist/webgpu/WebGPUPostProcess.d.ts.map +1 -1
  360. package/dist/webgpu/WebGPUPostProcess.js +1 -0
  361. package/dist/webgpu/WebGPUPostProcess.js.map +1 -1
  362. package/dist/webgpu/WebGPUPostShaders.d.ts +77 -0
  363. package/dist/webgpu/WebGPUPostShaders.d.ts.map +1 -0
  364. package/dist/webgpu/WebGPUPostShaders.js +261 -0
  365. package/dist/webgpu/WebGPUPostShaders.js.map +1 -0
  366. package/dist/webgpu/WebGPUTemporal.d.ts +17 -0
  367. package/dist/webgpu/WebGPUTemporal.d.ts.map +1 -0
  368. package/dist/webgpu/WebGPUTemporal.js +64 -0
  369. package/dist/webgpu/WebGPUTemporal.js.map +1 -0
  370. package/dist/webgpu.d.ts +6 -0
  371. package/dist/webgpu.d.ts.map +1 -1
  372. package/dist/webgpu.js +4 -0
  373. package/dist/webgpu.js.map +1 -1
  374. package/package.json +15 -3
@@ -0,0 +1,4001 @@
1
+ import { RenderDeviceError, resolveGpuTargetOwner, spreadGpuTargetInventory, } from "./RenderDevice.js";
2
+ import { Texture, isCompressedTextureFormat, isFloatColorTextureFormat } from "./Texture.js";
3
+ import { isTextureBinding, TextureBinding } from "./TextureBinding.js";
4
+ import {} from "./VertexFormat.js";
5
+ import { WebGL2StateCache } from "./WebGL2StateCache.js";
6
+ import { BLOOM_BRIGHT_LUT_HEIGHT, BLOOM_BRIGHT_LUT_WIDTH, BLOOM_COMPOSITE_LUT_SIZE, OUTLINE_BLEND_LUT_WIDTH, OUTLINE_LIMB_RADIX, createOutlineBlendLut, createOutlineGradientBound, createBloomBrightThresholdLut, createBloomCompositeLut } from "./postprocess/NativeLdrEffectLuts.js";
7
+ import { normalizeBloomQualityPreset, resolveBloomPyramidBlurRadii, resolveBloomPyramidPlan, resolveBloomPyramidResponseGain, } from "./postprocess/NativeBloomPyramid.js";
8
+ const WEBGL_CUBE_FACES = ["px", "nx", "py", "ny", "pz", "nz"];
9
+ class WebGL2Buffer {
10
+ id;
11
+ usage;
12
+ byteLength;
13
+ target;
14
+ handle;
15
+ gl;
16
+ disposed = false;
17
+ constructor(id, usage, byteLength, target, handle, gl) {
18
+ this.id = id;
19
+ this.usage = usage;
20
+ this.byteLength = byteLength;
21
+ this.target = target;
22
+ this.handle = handle;
23
+ this.gl = gl;
24
+ }
25
+ dispose() {
26
+ if (!this.disposed) {
27
+ this.gl.deleteBuffer(this.handle);
28
+ this.disposed = true;
29
+ }
30
+ }
31
+ }
32
+ class WebGL2ShaderProgram {
33
+ id;
34
+ label;
35
+ marker;
36
+ reflection;
37
+ handle;
38
+ gl;
39
+ disposed = false;
40
+ constructor(id, label, marker, reflection, handle, gl) {
41
+ this.id = id;
42
+ this.label = label;
43
+ this.marker = marker;
44
+ this.reflection = reflection;
45
+ this.handle = handle;
46
+ this.gl = gl;
47
+ }
48
+ dispose() {
49
+ if (!this.disposed) {
50
+ this.gl.deleteProgram(this.handle);
51
+ this.disposed = true;
52
+ }
53
+ }
54
+ }
55
+ class WebGL2RenderTarget {
56
+ id;
57
+ width;
58
+ height;
59
+ label;
60
+ colorTexture;
61
+ depthTexture;
62
+ framebuffer;
63
+ colorHandle;
64
+ depthHandle;
65
+ depthTextureHandle;
66
+ sampleCount;
67
+ drawFramebuffer;
68
+ multisampleColorHandle;
69
+ gl;
70
+ disposed = false;
71
+ needsResolve = false;
72
+ constructor(id, width, height, label, colorTexture, depthTexture, framebuffer, colorHandle, depthHandle, depthTextureHandle, sampleCount, drawFramebuffer, multisampleColorHandle, gl) {
73
+ this.id = id;
74
+ this.width = width;
75
+ this.height = height;
76
+ this.label = label;
77
+ this.colorTexture = colorTexture;
78
+ this.depthTexture = depthTexture;
79
+ this.framebuffer = framebuffer;
80
+ this.colorHandle = colorHandle;
81
+ this.depthHandle = depthHandle;
82
+ this.depthTextureHandle = depthTextureHandle;
83
+ this.sampleCount = sampleCount;
84
+ this.drawFramebuffer = drawFramebuffer;
85
+ this.multisampleColorHandle = multisampleColorHandle;
86
+ this.gl = gl;
87
+ }
88
+ dispose() {
89
+ if (!this.disposed) {
90
+ this.gl.deleteFramebuffer(this.framebuffer);
91
+ if (this.drawFramebuffer !== this.framebuffer)
92
+ this.gl.deleteFramebuffer(this.drawFramebuffer);
93
+ this.gl.deleteTexture(this.colorHandle);
94
+ if (this.depthHandle)
95
+ this.gl.deleteRenderbuffer(this.depthHandle);
96
+ if (this.multisampleColorHandle)
97
+ this.gl.deleteRenderbuffer(this.multisampleColorHandle);
98
+ if (this.depthTextureHandle)
99
+ this.gl.deleteTexture(this.depthTextureHandle);
100
+ this.colorTexture.dispose();
101
+ this.depthTexture?.dispose();
102
+ this.disposed = true;
103
+ }
104
+ }
105
+ }
106
+ export class LeanWebGL2Device {
107
+ errorCheckMode;
108
+ kind = "webgl2";
109
+ info;
110
+ disposed = false;
111
+ contextLost = false;
112
+ gl;
113
+ nextId = 1;
114
+ drawCalls = 0;
115
+ buffers = new Set();
116
+ shaders = new Set();
117
+ renderTargets = new Set();
118
+ activeRenderTarget = null;
119
+ textures = new Map();
120
+ textureUploadModes = new Map();
121
+ fallbackTexture = null;
122
+ fallbackCubeTexture = null;
123
+ samplerObjectCache = new Map();
124
+ presentationProgram = null;
125
+ ldrPostprocessProgram = null;
126
+ bloomBrightExtractProgram = null;
127
+ bloomBlurProgram = null;
128
+ bloomCompositeProgram = null;
129
+ bloomDownsampleProgram = null;
130
+ bloomAccumulateProgram = null;
131
+ bloomPyramidResources = null;
132
+ outlineProgram = null;
133
+ ssaoProgram = null;
134
+ ssrProgram = null;
135
+ depthOfFieldProgram = null;
136
+ motionBlurProgram = null;
137
+ motionBlurVelocityTexture = null;
138
+ motionBlurVelocitySize = null;
139
+ taaProgram = null;
140
+ taaHistoryTexture = null;
141
+ taaHistorySize = null;
142
+ bloomPingPongResources = null;
143
+ bloomBrightLutTexture = null;
144
+ bloomBrightLutThreshold = null;
145
+ bloomCompositeLutTexture = null;
146
+ bloomCompositeLutIntensity = null;
147
+ bloomCompositeLutShoulder = null;
148
+ lastBloomDiagnostics = null;
149
+ outlineBlendLutTexture = null;
150
+ outlineBlendLutKey = null;
151
+ depthReadbackProgram = null;
152
+ uniformLocationCache = new WeakMap();
153
+ textureSamplerParameterCache = new WeakMap();
154
+ stateCache = new WebGL2StateCache({ label: "webgl2-device-state-cache" });
155
+ textureUnitBindings = new Map();
156
+ vertexArrayCache = new Map();
157
+ vertexFormatIds = new WeakMap();
158
+ presentationVertexArray = null;
159
+ activeTextureUnitIndex = -1;
160
+ shaderProgramCreateCount = 0;
161
+ uniformLocationLookupCount = 0;
162
+ bufferUpdateCount = 0;
163
+ textureBindCount = 0;
164
+ samplerParameterUploadCount = 0;
165
+ samplerAnisotropyUploadCount = 0;
166
+ vertexArrayCreateCount = 0;
167
+ nativeInstancedSubmissions = 0;
168
+ nextVertexFormatId = 1;
169
+ releasedTextureHandles = 0;
170
+ nativeEnvironmentBindings = 0;
171
+ nativeShadowMapBindings = 0;
172
+ shadowRenderTargetsAllocated = 0;
173
+ lastError = null;
174
+ frameActive = false;
175
+ viewportWidth = 0;
176
+ viewportHeight = 0;
177
+ maxVertexAttributes;
178
+ anisotropicFilteringExtension;
179
+ maxTextureAnisotropy;
180
+ /**
181
+ * WS-2.6 — subscribers for context loss and restoration.
182
+ *
183
+ * The device has tracked `contextLost` since before 1.6 and acted on it internally, but nothing
184
+ * could *observe* it: there was no callback, so a consumer could only poll `diagnostics()`. That is
185
+ * why the parity table listed context-loss recovery as a gap while the listeners existed — the
186
+ * device layer was never the gap, the absence of a public signal was.
187
+ */
188
+ deviceLostListeners = new Set();
189
+ deviceRestoredListeners = new Set();
190
+ contextLostListener;
191
+ contextRestoredListener;
192
+ canvas;
193
+ static create(options) {
194
+ const gl = options.canvas.getContext("webgl2", {
195
+ antialias: options.antialias ?? true,
196
+ alpha: options.alpha ?? false,
197
+ preserveDrawingBuffer: options.preserveDrawingBuffer ?? false
198
+ });
199
+ if (!gl) {
200
+ throw new RenderDeviceError("WebGL2 is not available for the provided canvas", "WEBGL2_UNAVAILABLE");
201
+ }
202
+ // Default to frame-level error checking.
203
+ //
204
+ // `strict` calls `gl.getError()` after every uniform upload, vertex-format
205
+ // bind, and draw. `gl.getError()` forces a synchronous CPU/GPU sync, so on a
206
+ // scene with a few dozen draws and many uniforms per draw it dominates frame
207
+ // time: profiling Aura Clash attributed ~93% of frame time to `getError`,
208
+ // holding the route at ~11 FPS on an Apple M4 Max. Frame-level checking still
209
+ // reports real WebGL errors (read once in `endFrame` and surfaced through
210
+ // `lastError`); callers that need per-operation attribution can still opt into
211
+ // `strict` explicitly.
212
+ return new LeanWebGL2Device(gl, options.canvas, options.errorCheckMode ?? "frame");
213
+ }
214
+ constructor(gl, canvas, errorCheckMode) {
215
+ this.errorCheckMode = errorCheckMode;
216
+ this.gl = gl;
217
+ // A browser returns the same WebGL2 context object after `webglcontextrestored`.
218
+ // The controller that owned it before the loss is disposed before a replacement
219
+ // device mounts, and Chromium can leave an INVALID_OPERATION from that stale
220
+ // resource teardown in the context's error queue. A fresh device must establish
221
+ // its own error boundary; otherwise its first valid uniform upload or presentation
222
+ // is blamed for an error produced by the previous owner.
223
+ for (let index = 0; index < 16 && gl.getError() !== gl.NO_ERROR; index += 1) {
224
+ // Drain only errors that predate this device. Normal frame/strict checks still
225
+ // report every error generated after construction.
226
+ }
227
+ this.maxVertexAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
228
+ this.anisotropicFilteringExtension = gl.getExtension("EXT_texture_filter_anisotropic");
229
+ this.maxTextureAnisotropy = this.anisotropicFilteringExtension
230
+ ? Math.max(1, gl.getParameter(this.anisotropicFilteringExtension.MAX_TEXTURE_MAX_ANISOTROPY_EXT))
231
+ : 1;
232
+ this.info = {
233
+ backend: "webgl2",
234
+ vendor: gl.getParameter(gl.VENDOR),
235
+ renderer: gl.getParameter(gl.RENDERER),
236
+ capabilities: [
237
+ "buffers",
238
+ "buffer-readback",
239
+ "shader-validation",
240
+ "render-targets",
241
+ "pixel-readback",
242
+ "postprocess-presentation",
243
+ "draw-validation",
244
+ "rasterization",
245
+ "depth-render-targets",
246
+ "depth-textures",
247
+ "spot-shadow-maps",
248
+ "point-shadow-maps",
249
+ "hdr-image-based-lighting",
250
+ ...(this.anisotropicFilteringExtension ? ["anisotropic-texture-filtering"] : []),
251
+ ...(gl.getExtension("EXT_color_buffer_float") ? ["hdr-render-targets", "float-readback"] : [])
252
+ ]
253
+ };
254
+ this.canvas = canvas;
255
+ if ("addEventListener" in canvas) {
256
+ this.contextLostListener = ((event) => {
257
+ event.preventDefault();
258
+ this.contextLost = true;
259
+ this.lastError = "CONTEXT_LOST";
260
+ this.frameActive = false;
261
+ // WS-2.6: notify subscribers. Errors in a listener must not prevent the others from running,
262
+ // nor leave the device in a half-notified state during an already-degraded condition.
263
+ for (const listener of [...this.deviceLostListeners]) {
264
+ try {
265
+ listener();
266
+ }
267
+ catch {
268
+ // A listener that throws is the listener's problem; the device stays consistent.
269
+ }
270
+ }
271
+ });
272
+ this.contextRestoredListener = (() => {
273
+ this.contextLost = false;
274
+ this.lastError = null;
275
+ for (const listener of [...this.deviceRestoredListeners]) {
276
+ try {
277
+ listener();
278
+ }
279
+ catch {
280
+ // As above.
281
+ }
282
+ }
283
+ });
284
+ canvas.addEventListener("webglcontextlost", this.contextLostListener);
285
+ canvas.addEventListener("webglcontextrestored", this.contextRestoredListener);
286
+ }
287
+ }
288
+ /**
289
+ * Subscribe to WebGL context loss. Returns an unsubscribe function.
290
+ *
291
+ * A lost context is not an error a caller can prevent — the browser reclaims GPU resources under
292
+ * memory pressure, on driver reset, or when a tab is backgrounded too long. What a caller can do is
293
+ * be told, so it can recreate resources rather than render nothing.
294
+ */
295
+ onDeviceLost(listener) {
296
+ this.deviceLostListeners.add(listener);
297
+ return () => this.deviceLostListeners.delete(listener);
298
+ }
299
+ /** Subscribe to context restoration. Returns an unsubscribe function. */
300
+ onDeviceRestored(listener) {
301
+ this.deviceRestoredListeners.add(listener);
302
+ return () => this.deviceRestoredListeners.delete(listener);
303
+ }
304
+ /** True while the WebGL context is lost. Polling alternative to {@link onDeviceLost}. */
305
+ isDeviceLost() {
306
+ return this.contextLost;
307
+ }
308
+ createBuffer(usage, byteLength, initialData) {
309
+ this.assertAlive();
310
+ if (byteLength <= 0 || !Number.isInteger(byteLength)) {
311
+ throw new RenderDeviceError("Buffer byteLength must be a positive integer", "INVALID_BUFFER_SIZE", { byteLength });
312
+ }
313
+ const handle = this.gl.createBuffer();
314
+ if (!handle) {
315
+ throw new RenderDeviceError("Failed to allocate WebGL buffer", "WEBGL_ALLOCATION_FAILED");
316
+ }
317
+ const target = usage === "index" ? this.gl.ELEMENT_ARRAY_BUFFER : this.gl.ARRAY_BUFFER;
318
+ this.bindNoVertexArray();
319
+ this.stateCache.bindBuffer(target, handle, () => this.gl.bindBuffer(target, handle));
320
+ this.gl.bufferData(target, byteLength, this.gl.DYNAMIC_DRAW);
321
+ if (initialData) {
322
+ if (initialData.byteLength > byteLength) {
323
+ throw new RenderDeviceError("Initial data exceeds buffer size", "BUFFER_OVERFLOW", {
324
+ byteLength,
325
+ dataByteLength: initialData.byteLength
326
+ });
327
+ }
328
+ this.gl.bufferSubData(target, 0, initialData);
329
+ }
330
+ const buffer = new WebGL2Buffer(this.nextId++, usage, byteLength, target, handle, this.gl);
331
+ this.buffers.add(buffer);
332
+ return buffer;
333
+ }
334
+ updateBuffer(buffer, byteOffset, data) {
335
+ this.assertAlive();
336
+ const webglBuffer = this.requireBuffer(buffer);
337
+ if (byteOffset < 0 || byteOffset + data.byteLength > webglBuffer.byteLength) {
338
+ throw new RenderDeviceError("Buffer update range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
339
+ byteOffset,
340
+ dataByteLength: data.byteLength,
341
+ byteLength: webglBuffer.byteLength
342
+ });
343
+ }
344
+ this.bindNoVertexArray();
345
+ this.stateCache.bindBuffer(webglBuffer.target, webglBuffer.handle, () => this.gl.bindBuffer(webglBuffer.target, webglBuffer.handle));
346
+ this.gl.bufferSubData(webglBuffer.target, byteOffset, data);
347
+ this.bufferUpdateCount += 1;
348
+ }
349
+ readBuffer(buffer, byteOffset = 0, byteLength = buffer.byteLength - byteOffset) {
350
+ this.assertAlive();
351
+ const webglBuffer = this.requireBuffer(buffer);
352
+ if (byteOffset < 0 || byteLength < 0 || byteOffset + byteLength > webglBuffer.byteLength) {
353
+ throw new RenderDeviceError("Buffer read range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
354
+ byteOffset,
355
+ byteLength,
356
+ bufferByteLength: webglBuffer.byteLength
357
+ });
358
+ }
359
+ const output = new Uint8Array(byteLength);
360
+ this.bindNoVertexArray();
361
+ this.stateCache.bindBuffer(webglBuffer.target, webglBuffer.handle, () => this.gl.bindBuffer(webglBuffer.target, webglBuffer.handle));
362
+ this.gl.getBufferSubData(webglBuffer.target, byteOffset, output);
363
+ return output;
364
+ }
365
+ createShaderProgram(sources) {
366
+ this.assertAlive();
367
+ if (!sources.vertex.includes(sources.marker) || !sources.fragment.includes(sources.marker)) {
368
+ throw new RenderDeviceError("Shader source marker is missing from compiled sources", "SHADER_MARKER_MISSING", {
369
+ label: sources.label,
370
+ marker: sources.marker
371
+ });
372
+ }
373
+ const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, sources.vertex, sources.label);
374
+ const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, sources.fragment, sources.label);
375
+ const program = this.gl.createProgram();
376
+ if (!program) {
377
+ throw new RenderDeviceError("Failed to allocate WebGL shader program", "WEBGL_ALLOCATION_FAILED");
378
+ }
379
+ this.shaderProgramCreateCount += 1;
380
+ this.gl.attachShader(program, vertexShader);
381
+ this.gl.attachShader(program, fragmentShader);
382
+ this.gl.linkProgram(program);
383
+ this.gl.deleteShader(vertexShader);
384
+ this.gl.deleteShader(fragmentShader);
385
+ if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
386
+ const log = this.gl.getProgramInfoLog(program) ?? "Unknown shader link error";
387
+ this.gl.deleteProgram(program);
388
+ throw new RenderDeviceError("WebGL shader link failed", "SHADER_LINK_FAILED", { label: sources.label, log });
389
+ }
390
+ const shader = new WebGL2ShaderProgram(this.nextId++, sources.label, sources.marker, this.reflectProgram(program), program, this.gl);
391
+ this.shaders.add(shader);
392
+ return shader;
393
+ }
394
+ createRenderTarget(descriptor) {
395
+ this.assertAlive();
396
+ if (!Number.isInteger(descriptor.width) ||
397
+ descriptor.width <= 0 ||
398
+ !Number.isInteger(descriptor.height) ||
399
+ descriptor.height <= 0) {
400
+ throw new RenderDeviceError("Render target dimensions must be positive integers", "INVALID_RENDER_TARGET_SIZE", {
401
+ width: descriptor.width,
402
+ height: descriptor.height,
403
+ label: descriptor.label
404
+ });
405
+ }
406
+ const requestedSampleCount = descriptor.sampleCount ?? 1;
407
+ const maxSamples = Number(this.gl.getParameter(this.gl.MAX_SAMPLES) ?? 1);
408
+ if (!Number.isInteger(requestedSampleCount) || requestedSampleCount < 1 || requestedSampleCount > maxSamples) {
409
+ throw new RenderDeviceError("WebGL2 render-target sampleCount must be an integer supported by MAX_SAMPLES.", "INVALID_RENDER_TARGET_SAMPLE_COUNT", {
410
+ sampleCount: requestedSampleCount,
411
+ maxSamples,
412
+ label: descriptor.label
413
+ });
414
+ }
415
+ const sampleCount = requestedSampleCount;
416
+ const colorHandle = this.gl.createTexture();
417
+ const framebuffer = this.gl.createFramebuffer();
418
+ const drawFramebuffer = sampleCount > 1 ? this.gl.createFramebuffer() : framebuffer;
419
+ const multisampleColorHandle = sampleCount > 1 ? this.gl.createRenderbuffer() : null;
420
+ const depthMode = descriptor.depth === "texture" ? "texture" : descriptor.depth === false ? "none" : "renderbuffer";
421
+ const depthHandle = depthMode === "renderbuffer" ? this.gl.createRenderbuffer() : null;
422
+ const multisampleDepthHandle = sampleCount > 1 && depthMode !== "none"
423
+ ? (depthHandle ?? this.gl.createRenderbuffer())
424
+ : depthHandle;
425
+ const depthTextureHandle = depthMode === "texture" ? this.gl.createTexture() : null;
426
+ if (!colorHandle
427
+ || !framebuffer
428
+ || !drawFramebuffer
429
+ || (sampleCount > 1 && !multisampleColorHandle)
430
+ || ((depthMode === "renderbuffer" || (sampleCount > 1 && depthMode !== "none")) && !multisampleDepthHandle)
431
+ || (depthMode === "texture" && !depthTextureHandle)) {
432
+ if (colorHandle)
433
+ this.gl.deleteTexture(colorHandle);
434
+ if (framebuffer)
435
+ this.gl.deleteFramebuffer(framebuffer);
436
+ if (drawFramebuffer && drawFramebuffer !== framebuffer)
437
+ this.gl.deleteFramebuffer(drawFramebuffer);
438
+ if (multisampleColorHandle)
439
+ this.gl.deleteRenderbuffer(multisampleColorHandle);
440
+ if (multisampleDepthHandle)
441
+ this.gl.deleteRenderbuffer(multisampleDepthHandle);
442
+ if (depthTextureHandle)
443
+ this.gl.deleteTexture(depthTextureHandle);
444
+ throw new RenderDeviceError("Failed to allocate WebGL render target", "WEBGL_ALLOCATION_FAILED", {
445
+ width: descriptor.width,
446
+ height: descriptor.height,
447
+ label: descriptor.label
448
+ });
449
+ }
450
+ const previousActiveTexture = this.gl.getParameter(this.gl.ACTIVE_TEXTURE);
451
+ this.gl.activeTexture(this.gl.TEXTURE0);
452
+ const previousTexture = this.gl.getParameter(this.gl.TEXTURE_BINDING_2D);
453
+ const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
454
+ const previousRenderbuffer = this.gl.getParameter(this.gl.RENDERBUFFER_BINDING);
455
+ this.gl.bindTexture(this.gl.TEXTURE_2D, colorHandle);
456
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
457
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
458
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
459
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
460
+ const format = descriptor.format ?? "rgba8";
461
+ const textureFormat = this.resolveRenderTargetFormat(format);
462
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, textureFormat.internalFormat, descriptor.width, descriptor.height, 0, this.gl.RGBA, textureFormat.type, null);
463
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
464
+ this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, colorHandle, 0);
465
+ if (depthHandle && sampleCount === 1) {
466
+ this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, depthHandle);
467
+ this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_COMPONENT16, descriptor.width, descriptor.height);
468
+ this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, depthHandle);
469
+ }
470
+ if (depthTextureHandle) {
471
+ this.gl.bindTexture(this.gl.TEXTURE_2D, depthTextureHandle);
472
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
473
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
474
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
475
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
476
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.DEPTH_COMPONENT24, descriptor.width, descriptor.height, 0, this.gl.DEPTH_COMPONENT, this.gl.UNSIGNED_INT, null);
477
+ this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.TEXTURE_2D, depthTextureHandle, 0);
478
+ }
479
+ const resolveStatus = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
480
+ let drawStatus = resolveStatus;
481
+ if (sampleCount > 1 && multisampleColorHandle) {
482
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, drawFramebuffer);
483
+ this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, multisampleColorHandle);
484
+ const multisampleColorFormat = format === "rgba8" ? this.gl.RGBA8 : textureFormat.internalFormat;
485
+ this.gl.renderbufferStorageMultisample(this.gl.RENDERBUFFER, sampleCount, multisampleColorFormat, descriptor.width, descriptor.height);
486
+ this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.RENDERBUFFER, multisampleColorHandle);
487
+ if (multisampleDepthHandle) {
488
+ this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, multisampleDepthHandle);
489
+ this.gl.renderbufferStorageMultisample(this.gl.RENDERBUFFER, sampleCount, this.gl.DEPTH_COMPONENT24, descriptor.width, descriptor.height);
490
+ this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, multisampleDepthHandle);
491
+ }
492
+ drawStatus = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
493
+ }
494
+ this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, previousRenderbuffer);
495
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
496
+ this.gl.bindTexture(this.gl.TEXTURE_2D, previousTexture);
497
+ this.gl.activeTexture(previousActiveTexture);
498
+ this.stateCache.invalidate();
499
+ if (resolveStatus !== this.gl.FRAMEBUFFER_COMPLETE || drawStatus !== this.gl.FRAMEBUFFER_COMPLETE) {
500
+ this.gl.deleteTexture(colorHandle);
501
+ this.gl.deleteFramebuffer(framebuffer);
502
+ if (drawFramebuffer !== framebuffer)
503
+ this.gl.deleteFramebuffer(drawFramebuffer);
504
+ if (multisampleColorHandle)
505
+ this.gl.deleteRenderbuffer(multisampleColorHandle);
506
+ if (multisampleDepthHandle)
507
+ this.gl.deleteRenderbuffer(multisampleDepthHandle);
508
+ if (depthTextureHandle)
509
+ this.gl.deleteTexture(depthTextureHandle);
510
+ throw new RenderDeviceError("WebGL render target framebuffer status is invalid", "FRAMEBUFFER_INVALID", { resolveStatus, drawStatus });
511
+ }
512
+ const depthTexture = depthTextureHandle
513
+ ? new Texture({ width: descriptor.width, height: descriptor.height, format: "depth24", label: `${descriptor.label ?? "render-target"}-depth` })
514
+ : undefined;
515
+ const target = new WebGL2RenderTarget(this.nextId++, descriptor.width, descriptor.height, descriptor.label ?? "render-target", new Texture({ width: descriptor.width, height: descriptor.height, format, label: descriptor.label ?? "render-target-color" }), depthTexture, framebuffer, colorHandle, multisampleDepthHandle, depthTextureHandle, sampleCount, drawFramebuffer, multisampleColorHandle, this.gl);
516
+ this.renderTargets.add(target);
517
+ // Shadow passes label their depth target after the shadow map texture, so the
518
+ // label is the device-side signal that a shadow depth target was allocated.
519
+ if ((descriptor.label ?? "").toLowerCase().includes("shadow"))
520
+ this.shadowRenderTargetsAllocated += 1;
521
+ this.textures.set(target.colorTexture, colorHandle);
522
+ this.textureUploadModes.set(target.colorTexture, "rgba8");
523
+ if (depthTexture && depthTextureHandle) {
524
+ this.textures.set(depthTexture, depthTextureHandle);
525
+ this.textureUploadModes.set(depthTexture, "depth-render-target");
526
+ }
527
+ return target;
528
+ }
529
+ setRenderTarget(target) {
530
+ this.assertAlive();
531
+ if (this.activeRenderTarget && this.activeRenderTarget !== target) {
532
+ this.resolveMultisampleTarget(this.activeRenderTarget);
533
+ }
534
+ if (target === null) {
535
+ this.activeRenderTarget = null;
536
+ this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, null, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null));
537
+ this.stateCache.viewport(0, 0, this.viewportWidth || this.gl.drawingBufferWidth, this.viewportHeight || this.gl.drawingBufferHeight, () => this.gl.viewport(0, 0, this.viewportWidth || this.gl.drawingBufferWidth, this.viewportHeight || this.gl.drawingBufferHeight));
538
+ return;
539
+ }
540
+ if (!(target instanceof WebGL2RenderTarget) || !this.renderTargets.has(target) || target.disposed) {
541
+ throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
542
+ targetId: target.id
543
+ });
544
+ }
545
+ this.activeRenderTarget = target;
546
+ this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, target.drawFramebuffer, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, target.drawFramebuffer));
547
+ this.stateCache.viewport(0, 0, target.width, target.height, () => this.gl.viewport(0, 0, target.width, target.height));
548
+ }
549
+ resolveMultisampleTarget(target) {
550
+ if (target.sampleCount <= 1 || !target.needsResolve)
551
+ return;
552
+ const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
553
+ this.gl.bindFramebuffer(this.gl.READ_FRAMEBUFFER, target.drawFramebuffer);
554
+ this.gl.bindFramebuffer(this.gl.DRAW_FRAMEBUFFER, target.framebuffer);
555
+ let mask = this.gl.COLOR_BUFFER_BIT;
556
+ if (target.depthTextureHandle)
557
+ mask |= this.gl.DEPTH_BUFFER_BIT;
558
+ this.gl.blitFramebuffer(0, 0, target.width, target.height, 0, 0, target.width, target.height, mask, this.gl.NEAREST);
559
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
560
+ target.needsResolve = false;
561
+ this.stateCache.invalidate();
562
+ }
563
+ writeRenderTargetPixels(target, pixels) {
564
+ this.assertAlive();
565
+ if (!(target instanceof WebGL2RenderTarget) || !this.renderTargets.has(target) || target.disposed) {
566
+ throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
567
+ targetId: target.id
568
+ });
569
+ }
570
+ if (target.colorTexture.format !== "rgba8") {
571
+ throw new RenderDeviceError("Byte pixel uploads are only supported for rgba8 WebGL2 render targets", "INVALID_PIXEL_UPLOAD_FORMAT", {
572
+ targetId: target.id,
573
+ format: target.colorTexture.format
574
+ });
575
+ }
576
+ if (pixels.length !== target.width * target.height * 4) {
577
+ throw new RenderDeviceError("Render-target pixel upload must contain width * height * 4 bytes", "INVALID_PIXEL_UPLOAD_SIZE", {
578
+ targetId: target.id,
579
+ byteLength: pixels.length,
580
+ expectedByteLength: target.width * target.height * 4
581
+ });
582
+ }
583
+ const textureUnit0 = this.captureTextureUnit0();
584
+ try {
585
+ this.gl.bindTexture(this.gl.TEXTURE_2D, target.colorHandle);
586
+ this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, target.width, target.height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
587
+ target.needsResolve = false;
588
+ }
589
+ finally {
590
+ this.restoreTextureUnit0(textureUnit0);
591
+ }
592
+ this.lastError = this.readError();
593
+ if (this.lastError) {
594
+ throw new RenderDeviceError("WebGL2 render-target pixel upload failed", "WEBGL_PIXEL_UPLOAD_FAILED", {
595
+ targetId: target.id,
596
+ error: this.lastError
597
+ });
598
+ }
599
+ }
600
+ presentRenderTarget(source) {
601
+ this.assertAlive();
602
+ if (!(source instanceof WebGL2RenderTarget) || !this.renderTargets.has(source) || source.disposed) {
603
+ throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
604
+ targetId: source.id
605
+ });
606
+ }
607
+ this.resolveMultisampleTarget(source);
608
+ const outputWidth = this.viewportWidth || this.gl.drawingBufferWidth;
609
+ const outputHeight = this.viewportHeight || this.gl.drawingBufferHeight;
610
+ this.drawRenderTargetToBackbuffer(source, outputWidth, outputHeight);
611
+ this.activeRenderTarget = null;
612
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
613
+ this.lastError = this.readError();
614
+ if (this.lastError) {
615
+ throw new RenderDeviceError(`WebGL2 render-target presentation failed: ${this.lastError}`, "WEBGL_PRESENT_FAILED", {
616
+ targetId: source.id,
617
+ error: this.lastError
618
+ });
619
+ }
620
+ }
621
+ presentLdrPostprocess(source, options) {
622
+ this.assertAlive();
623
+ if (!(source instanceof WebGL2RenderTarget) || !this.renderTargets.has(source) || source.disposed) {
624
+ throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
625
+ targetId: source.id
626
+ });
627
+ }
628
+ this.resolveMultisampleTarget(source);
629
+ const tonePass = options.passes.find((pass) => pass.name === "tone-mapping");
630
+ const bloomPass = options.passes.find((pass) => pass.name === "bloom");
631
+ const depthOfFieldPass = options.passes.find((pass) => pass.name === "depth-of-field");
632
+ const motionBlurPass = options.passes.find((pass) => pass.name === "motion-blur");
633
+ const ssaoPass = options.passes.find((pass) => pass.name === "ssao");
634
+ const ssrPass = options.passes.find((pass) => pass.name === "ssr");
635
+ const taaPass = options.passes.find((pass) => pass.name === "taa");
636
+ const outlinePass = options.passes.find((pass) => pass.name === "outline");
637
+ if (source.colorTexture.format !== "rgba8" && !tonePass) {
638
+ throw new RenderDeviceError("WebGL2 HDR postprocess presentation requires a tone-mapping pass before LDR output.", "WEBGL_LDR_POSTPROCESS_FORMAT_UNSUPPORTED", {
639
+ targetId: source.id,
640
+ format: source.colorTexture.format
641
+ });
642
+ }
643
+ if (source.colorTexture.format !== "rgba8" && (depthOfFieldPass || motionBlurPass || ssaoPass || ssrPass || taaPass || outlinePass) && !tonePass) {
644
+ throw new RenderDeviceError("WebGL2 byte-kernel postprocess requires rgba8 input or a preceding tone-mapping pass.", "WEBGL_LDR_POSTPROCESS_FORMAT_UNSUPPORTED", {
645
+ targetId: source.id,
646
+ format: source.colorTexture.format,
647
+ pass: depthOfFieldPass ? "depth-of-field" : motionBlurPass ? "motion-blur" : ssaoPass ? "ssao" : ssrPass ? "ssr" : taaPass ? "taa" : "outline"
648
+ });
649
+ }
650
+ if ((depthOfFieldPass || ssaoPass || ssrPass) && !source.depthTextureHandle) {
651
+ throw new RenderDeviceError("WebGL2 native depth postprocess requires a renderer-owned sampleable depth texture.", "WEBGL_LDR_POSTPROCESS_DEPTH_REQUIRED", {
652
+ targetId: source.id,
653
+ pass: depthOfFieldPass ? "depth-of-field" : ssaoPass ? "ssao" : "ssr"
654
+ });
655
+ }
656
+ // Raw GL depth is nonlinear (0.1/1000 defaults park the play area past
657
+ // 0.97), which blinded every depth-gated native pass. SSR and DOF compare
658
+ // linearized depth; SSAO keeps its legacy response (documented partial).
659
+ const depthRange = normalizeLdrDepthRange(options.depthRange);
660
+ const outputTarget = options.outputTarget;
661
+ if (outputTarget && (!(outputTarget instanceof WebGL2RenderTarget) || !this.renderTargets.has(outputTarget) || outputTarget.disposed)) {
662
+ throw new RenderDeviceError("Output render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
663
+ targetId: outputTarget.id
664
+ });
665
+ }
666
+ if (outputTarget && outputTarget.colorTexture.format !== "rgba8") {
667
+ throw new RenderDeviceError("WebGL2 LDR postprocess output target must be rgba8.", "WEBGL_LDR_POSTPROCESS_OUTPUT_FORMAT_UNSUPPORTED", {
668
+ targetId: outputTarget.id,
669
+ format: outputTarget.colorTexture.format
670
+ });
671
+ }
672
+ if (outputTarget && (outputTarget.width !== source.width || outputTarget.height !== source.height)) {
673
+ throw new RenderDeviceError("WebGL2 LDR postprocess source and output dimensions must match.", "WEBGL_LDR_POSTPROCESS_SIZE_MISMATCH", {
674
+ sourceWidth: source.width,
675
+ sourceHeight: source.height,
676
+ targetWidth: outputTarget.width,
677
+ targetHeight: outputTarget.height
678
+ });
679
+ }
680
+ const webglOutputTarget = outputTarget;
681
+ const colorPass = options.passes.find((pass) => pass.name === "color-grade");
682
+ const fxaaPass = options.passes.find((pass) => pass.name === "fxaa");
683
+ const bloomOptions = bloomPass ? normalizeNativeBloomOptions(bloomPass.options) : undefined;
684
+ const depthOfFieldOptions = depthOfFieldPass ? normalizeNativeDepthOfFieldOptions(depthOfFieldPass.options) : undefined;
685
+ const motionBlurOptions = motionBlurPass
686
+ ? normalizeNativeMotionBlurOptions(motionBlurPass.options, source.width, source.height)
687
+ : undefined;
688
+ const ssaoOptions = ssaoPass ? normalizeNativeSsaoOptions(ssaoPass.options) : undefined;
689
+ const ssrOptions = ssrPass ? normalizeNativeSsrOptions(ssrPass.options) : undefined;
690
+ const taaOptions = taaPass ? normalizeNativeTaaOptions(taaPass.options, source.width, source.height) : undefined;
691
+ const outlineOptions = outlinePass ? normalizeNativeOutlineOptions(outlinePass.options) : undefined;
692
+ const program = this.ensureLdrPostprocessProgram();
693
+ const vertexArray = this.ensurePresentationVertexArray();
694
+ const sourceIsHdr = source.colorTexture.format !== "rgba8";
695
+ const bloomResources = bloomOptions || depthOfFieldOptions || motionBlurOptions || ssaoOptions || ssrOptions || taaOptions || outlineOptions
696
+ ? this.ensureBloomPingPongResources(source.width, source.height, sourceIsHdr && Boolean(bloomOptions))
697
+ : undefined;
698
+ if (bloomOptions && !sourceIsHdr) {
699
+ this.ensureBloomLutTextures(bloomOptions);
700
+ }
701
+ if (outlineOptions) {
702
+ this.ensureOutlineBlendLutTexture(outlineOptions);
703
+ }
704
+ if (motionBlurOptions) {
705
+ this.ensureMotionBlurVelocityTexture(source.width, source.height, motionBlurOptions.velocity);
706
+ }
707
+ if (taaOptions) {
708
+ this.ensureTaaHistoryTexture(source.width, source.height, taaOptions.history);
709
+ }
710
+ const outputWidth = webglOutputTarget?.width ?? (this.viewportWidth || this.gl.drawingBufferWidth);
711
+ const outputHeight = webglOutputTarget?.height ?? (this.viewportHeight || this.gl.drawingBufferHeight);
712
+ const previousState = this.prepareFullscreenPresentation(webglOutputTarget?.framebuffer ?? null, outputWidth, outputHeight);
713
+ try {
714
+ let ldrSourceHandle = source.colorHandle;
715
+ let temporarySourceIndex = -1;
716
+ if (bloomOptions && bloomResources) {
717
+ if (bloomOptions.quality === "performance") {
718
+ ldrSourceHandle = this.executeNativeBloomPasses(ldrSourceHandle, bloomResources, bloomOptions, vertexArray, sourceIsHdr);
719
+ temporarySourceIndex = 1;
720
+ this.recordBloomDiagnostics(bloomOptions, source.width, source.height, sourceIsHdr, 1);
721
+ }
722
+ else {
723
+ const plan = resolveBloomPyramidPlan(source.width, source.height, bloomOptions.quality, sourceIsHdr);
724
+ const pyramid = this.ensureBloomPyramidResources(plan, sourceIsHdr);
725
+ const pyramidResult = this.executeNativeBloomPyramidPasses(ldrSourceHandle, source.width, source.height, pyramid, {
726
+ textureA: bloomResources.textures[0],
727
+ framebufferA: bloomResources.framebuffers[0],
728
+ textureB: bloomResources.textures[1],
729
+ framebufferB: bloomResources.framebuffers[1]
730
+ }, bloomOptions, vertexArray, sourceIsHdr);
731
+ // The pyramid sum lands in ping-pong texture A; composite it with the
732
+ // source through the proven single-scale composite program.
733
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, bloomResources.framebuffers[1]);
734
+ this.gl.viewport(0, 0, source.width, source.height);
735
+ const pyramidComposite = this.ensureBloomCompositeProgram();
736
+ this.gl.useProgram(pyramidComposite);
737
+ this.gl.bindVertexArray(vertexArray);
738
+ this.bindFullscreenTexture(0, ldrSourceHandle);
739
+ this.bindFullscreenTexture(1, pyramidResult);
740
+ if (this.bloomCompositeLutTexture)
741
+ this.bindFullscreenTexture(2, this.bloomCompositeLutTexture);
742
+ this.gl.uniform1i(this.gl.getUniformLocation(pyramidComposite, "u_source"), 0);
743
+ this.gl.uniform1i(this.gl.getUniformLocation(pyramidComposite, "u_blurred"), 1);
744
+ this.gl.uniform1i(this.gl.getUniformLocation(pyramidComposite, "u_compositeLut"), 2);
745
+ this.gl.uniform1i(this.gl.getUniformLocation(pyramidComposite, "u_hdr"), sourceIsHdr ? 1 : 0);
746
+ this.gl.uniform1f(this.gl.getUniformLocation(pyramidComposite, "u_intensity"), bloomOptions.intensity * resolveBloomPyramidResponseGain(plan));
747
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
748
+ ldrSourceHandle = bloomResources.textures[1];
749
+ // The composited result lives in texture B, so subsequent passes
750
+ // must write texture A (index 0) to avoid sampling a bound target.
751
+ temporarySourceIndex = 0;
752
+ this.recordBloomDiagnostics(bloomOptions, source.width, source.height, sourceIsHdr, plan.mipCount);
753
+ }
754
+ }
755
+ if ((depthOfFieldOptions || motionBlurOptions || ssaoOptions || ssrOptions || taaOptions || outlineOptions) && bloomResources) {
756
+ if (tonePass || colorPass) {
757
+ const baseTargetIndex = 0;
758
+ this.drawNativeLdrStage(ldrSourceHandle, bloomResources.framebuffers[baseTargetIndex], source.width, source.height, program, vertexArray, tonePass, colorPass, undefined, options.toneMappingDefaults);
759
+ ldrSourceHandle = bloomResources.textures[baseTargetIndex];
760
+ temporarySourceIndex = baseTargetIndex;
761
+ }
762
+ if (depthOfFieldOptions && source.depthTextureHandle) {
763
+ const depthOfFieldTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
764
+ ldrSourceHandle = this.executeNativeDepthOfFieldPass(ldrSourceHandle, source.depthTextureHandle, bloomResources, depthOfFieldOptions, vertexArray, depthOfFieldTargetIndex, depthRange);
765
+ temporarySourceIndex = depthOfFieldTargetIndex;
766
+ }
767
+ if (motionBlurOptions) {
768
+ const motionBlurTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
769
+ ldrSourceHandle = this.executeNativeMotionBlurPass(ldrSourceHandle, bloomResources, motionBlurOptions, vertexArray, motionBlurTargetIndex);
770
+ temporarySourceIndex = motionBlurTargetIndex;
771
+ }
772
+ if (ssaoOptions && source.depthTextureHandle) {
773
+ const ssaoTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
774
+ ldrSourceHandle = this.executeNativeSsaoPass(ldrSourceHandle, source.depthTextureHandle, bloomResources, ssaoOptions, vertexArray, ssaoTargetIndex);
775
+ temporarySourceIndex = ssaoTargetIndex;
776
+ }
777
+ if (ssrOptions && source.depthTextureHandle) {
778
+ const ssrTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
779
+ ldrSourceHandle = this.executeNativeSsrPass(ldrSourceHandle, source.depthTextureHandle, bloomResources, ssrOptions, vertexArray, ssrTargetIndex, depthRange);
780
+ temporarySourceIndex = ssrTargetIndex;
781
+ }
782
+ if (taaOptions) {
783
+ const taaTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
784
+ ldrSourceHandle = this.executeNativeTaaPass(ldrSourceHandle, bloomResources, taaOptions, vertexArray, taaTargetIndex);
785
+ temporarySourceIndex = taaTargetIndex;
786
+ }
787
+ }
788
+ if (outlineOptions && bloomResources) {
789
+ const outlineTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
790
+ ldrSourceHandle = this.executeNativeOutlinePasses(ldrSourceHandle, bloomResources, outlineOptions, vertexArray, outlineTargetIndex);
791
+ temporarySourceIndex = outlineTargetIndex;
792
+ }
793
+ this.drawNativeLdrStage(ldrSourceHandle, webglOutputTarget?.framebuffer ?? null, outputWidth, outputHeight, program, vertexArray, depthOfFieldOptions || motionBlurOptions || ssaoOptions || ssrOptions || taaOptions || outlineOptions ? undefined : tonePass, depthOfFieldOptions || motionBlurOptions || ssaoOptions || ssrOptions || taaOptions || outlineOptions ? undefined : colorPass, fxaaPass, options.toneMappingDefaults);
794
+ this.gl.flush();
795
+ }
796
+ finally {
797
+ this.restoreFullscreenPresentationState(previousState, outputWidth, outputHeight);
798
+ }
799
+ this.lastError = this.readError();
800
+ if (this.lastError) {
801
+ throw new RenderDeviceError(`WebGL2 LDR postprocess presentation failed: ${this.lastError}`, "WEBGL_LDR_POSTPROCESS_FAILED", {
802
+ targetId: source.id,
803
+ error: this.lastError
804
+ });
805
+ }
806
+ this.setRenderTarget(webglOutputTarget ?? null);
807
+ }
808
+ readPixels(x, y, width, height) {
809
+ this.assertAlive();
810
+ if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
811
+ throw new RenderDeviceError("Readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
812
+ }
813
+ const boundsWidth = this.activeRenderTarget?.width ?? this.gl.drawingBufferWidth;
814
+ const boundsHeight = this.activeRenderTarget?.height ?? this.gl.drawingBufferHeight;
815
+ if (x + width > boundsWidth || y + height > boundsHeight) {
816
+ throw new RenderDeviceError("Readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
817
+ x,
818
+ y,
819
+ width,
820
+ height,
821
+ boundsWidth,
822
+ boundsHeight
823
+ });
824
+ }
825
+ const readTarget = this.activeRenderTarget;
826
+ if (readTarget) {
827
+ this.resolveMultisampleTarget(readTarget);
828
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, readTarget.framebuffer);
829
+ }
830
+ const pixels = new Uint8Array(width * height * 4);
831
+ this.gl.readPixels(x, y, width, height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
832
+ if (readTarget && readTarget.sampleCount > 1) {
833
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, readTarget.drawFramebuffer);
834
+ this.stateCache.invalidate();
835
+ }
836
+ return pixels;
837
+ }
838
+ readFloatPixels(x, y, width, height) {
839
+ this.assertAlive();
840
+ if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
841
+ throw new RenderDeviceError("Float readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
842
+ }
843
+ const boundsWidth = this.activeRenderTarget?.width ?? this.gl.drawingBufferWidth;
844
+ const boundsHeight = this.activeRenderTarget?.height ?? this.gl.drawingBufferHeight;
845
+ if (x + width > boundsWidth || y + height > boundsHeight) {
846
+ throw new RenderDeviceError("Float readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
847
+ x,
848
+ y,
849
+ width,
850
+ height,
851
+ boundsWidth,
852
+ boundsHeight
853
+ });
854
+ }
855
+ const readTarget = this.activeRenderTarget;
856
+ if (readTarget) {
857
+ this.resolveMultisampleTarget(readTarget);
858
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, readTarget.framebuffer);
859
+ }
860
+ const pixels = new Float32Array(width * height * 4);
861
+ this.gl.readPixels(x, y, width, height, this.gl.RGBA, this.gl.FLOAT, pixels);
862
+ if (readTarget && readTarget.sampleCount > 1) {
863
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, readTarget.drawFramebuffer);
864
+ this.stateCache.invalidate();
865
+ }
866
+ return pixels;
867
+ }
868
+ readDepthPixels(x, y, width, height) {
869
+ this.assertAlive();
870
+ if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
871
+ throw new RenderDeviceError("Depth readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
872
+ }
873
+ const target = this.activeRenderTarget;
874
+ if (!target?.depthTextureHandle) {
875
+ throw new RenderDeviceError("Depth readback requires an active WebGL2 render target with a sampleable depth texture.", "DEPTH_READBACK_UNAVAILABLE", {
876
+ renderTarget: target?.label ?? null
877
+ });
878
+ }
879
+ if (x + width > target.width || y + height > target.height) {
880
+ throw new RenderDeviceError("Depth readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
881
+ x,
882
+ y,
883
+ width,
884
+ height,
885
+ boundsWidth: target.width,
886
+ boundsHeight: target.height
887
+ });
888
+ }
889
+ this.resolveMultisampleTarget(target);
890
+ const encoded = this.copyDepthTextureToBytes(target, x, y, width, height);
891
+ const pixels = new Float32Array(width * height);
892
+ for (let index = 0; index < pixels.length; index += 1) {
893
+ const byteIndex = index * 4;
894
+ const r = encoded[byteIndex] ?? 255;
895
+ const g = encoded[byteIndex + 1] ?? 255;
896
+ const b = encoded[byteIndex + 2] ?? 255;
897
+ pixels[index] = Math.max(0, Math.min(1, r / 255 + g / 65025 + b / 16581375));
898
+ }
899
+ return pixels;
900
+ }
901
+ beginFrame(width, height) {
902
+ this.assertAlive();
903
+ if (this.frameActive) {
904
+ throw new RenderDeviceError("Frame is already active", "FRAME_ALREADY_ACTIVE");
905
+ }
906
+ if (width <= 0 || height <= 0) {
907
+ throw new RenderDeviceError("Frame dimensions must be positive", "INVALID_FRAME_SIZE", { width, height });
908
+ }
909
+ this.frameActive = true;
910
+ this.drawCalls = 0;
911
+ this.nativeEnvironmentBindings = 0;
912
+ this.nativeShadowMapBindings = 0;
913
+ this.activeTextureUnitIndex = -1;
914
+ this.textureUnitBindings.clear();
915
+ this.stateCache.invalidate();
916
+ this.viewportWidth = width;
917
+ this.viewportHeight = height;
918
+ this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, this.activeRenderTarget?.framebuffer ?? null, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.activeRenderTarget?.framebuffer ?? null));
919
+ this.stateCache.viewport(0, 0, width, height, () => this.gl.viewport(0, 0, width, height));
920
+ this.stateCache.setEnabled(this.gl.DEPTH_TEST, true, () => this.gl.enable(this.gl.DEPTH_TEST));
921
+ this.stateCache.depthFunc(this.gl.LEQUAL, () => this.gl.depthFunc(this.gl.LEQUAL));
922
+ }
923
+ clear(color) {
924
+ this.assertFrame();
925
+ this.gl.clearColor(color[0], color[1], color[2], color[3]);
926
+ this.stateCache.depthMask(true, () => this.gl.depthMask(true));
927
+ this.gl.clearDepth(1);
928
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
929
+ if (this.activeRenderTarget?.sampleCount && this.activeRenderTarget.sampleCount > 1)
930
+ this.activeRenderTarget.needsResolve = true;
931
+ }
932
+ clearRenderTarget(color) {
933
+ this.assertFrame();
934
+ this.gl.clearColor(color[0], color[1], color[2], color[3]);
935
+ this.stateCache.depthMask(true, () => this.gl.depthMask(true));
936
+ this.gl.clearDepth(1);
937
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
938
+ if (this.activeRenderTarget?.sampleCount && this.activeRenderTarget.sampleCount > 1)
939
+ this.activeRenderTarget.needsResolve = true;
940
+ }
941
+ draw(command) {
942
+ this.assertFrame();
943
+ const vertexBuffer = this.requireBuffer(command.vertexBuffer);
944
+ this.applyRenderState(command.renderState);
945
+ let shader;
946
+ if (command.shader) {
947
+ const activeShader = this.requireShader(command.shader);
948
+ shader = activeShader;
949
+ this.stateCache.useProgram(activeShader.handle, () => this.gl.useProgram(activeShader.handle));
950
+ if (command.uniforms) {
951
+ this.uploadUniforms(activeShader, command.uniforms);
952
+ if (this.errorCheckMode === "strict") {
953
+ const uniformError = this.readError();
954
+ if (uniformError) {
955
+ throw new RenderDeviceError(`WebGL2 uniform upload failed for draw ${command.label ?? "unnamed"}: ${uniformError}`, "WEBGL_DRAW_FAILED", {
956
+ label: command.label,
957
+ stage: "uniforms",
958
+ error: uniformError
959
+ });
960
+ }
961
+ }
962
+ }
963
+ }
964
+ if (shader && command.vertexFormat) {
965
+ this.bindVertexArrayForCommand(command, shader, vertexBuffer);
966
+ if (this.errorCheckMode === "strict") {
967
+ const vertexFormatError = this.readError();
968
+ if (vertexFormatError) {
969
+ throw new RenderDeviceError(`WebGL2 vertex format binding failed for draw ${command.label ?? "unnamed"}: ${vertexFormatError}`, "WEBGL_DRAW_FAILED", {
970
+ label: command.label,
971
+ stage: "vertex-format",
972
+ error: vertexFormatError
973
+ });
974
+ }
975
+ }
976
+ }
977
+ else {
978
+ this.bindNoVertexArray();
979
+ this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle));
980
+ }
981
+ if (command.indexBuffer) {
982
+ const indexBuffer = this.requireBuffer(command.indexBuffer);
983
+ if (!shader || !command.vertexFormat) {
984
+ this.stateCache.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle, () => this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle));
985
+ }
986
+ const type = command.indexType === "uint32" ? this.gl.UNSIGNED_INT : this.gl.UNSIGNED_SHORT;
987
+ const indexByteSize = command.indexType === "uint32" ? 4 : 2;
988
+ const indexOffset = (command.firstIndex ?? 0) * indexByteSize;
989
+ if ((command.instanceCount ?? 1) > 1) {
990
+ this.gl.drawElementsInstanced(this.primitive(command.topology), command.indexCount ?? 0, type, indexOffset, command.instanceCount ?? 1);
991
+ this.nativeInstancedSubmissions += 1;
992
+ }
993
+ else {
994
+ this.gl.drawElements(this.primitive(command.topology), command.indexCount ?? 0, type, indexOffset);
995
+ }
996
+ }
997
+ else {
998
+ if ((command.instanceCount ?? 1) > 1) {
999
+ this.gl.drawArraysInstanced(this.primitive(command.topology), command.firstVertex ?? 0, command.vertexCount, command.instanceCount ?? 1);
1000
+ this.nativeInstancedSubmissions += 1;
1001
+ }
1002
+ else {
1003
+ this.gl.drawArrays(this.primitive(command.topology), command.firstVertex ?? 0, command.vertexCount);
1004
+ }
1005
+ }
1006
+ this.drawCalls += 1;
1007
+ if (this.activeRenderTarget?.sampleCount && this.activeRenderTarget.sampleCount > 1)
1008
+ this.activeRenderTarget.needsResolve = true;
1009
+ if (this.errorCheckMode === "strict") {
1010
+ const drawError = this.readError();
1011
+ if (drawError) {
1012
+ throw new RenderDeviceError(`WebGL2 draw failed for ${command.label ?? "unnamed"}: ${drawError}`, "WEBGL_DRAW_FAILED", {
1013
+ label: command.label,
1014
+ stage: "draw",
1015
+ error: drawError
1016
+ });
1017
+ }
1018
+ }
1019
+ }
1020
+ endFrame() {
1021
+ this.assertFrame();
1022
+ this.frameActive = false;
1023
+ this.lastError = this.readError();
1024
+ }
1025
+ captureState() {
1026
+ const viewport = this.gl.getParameter(this.gl.VIEWPORT);
1027
+ return new Map([
1028
+ ["backend", this.kind],
1029
+ ["disposed", this.disposed],
1030
+ ["contextLost", this.contextLost],
1031
+ ["frameActive", this.frameActive],
1032
+ ["depthTest", this.gl.isEnabled(this.gl.DEPTH_TEST)],
1033
+ ["blend", this.gl.isEnabled(this.gl.BLEND)],
1034
+ ["cullFace", this.gl.isEnabled(this.gl.CULL_FACE)],
1035
+ ["program", this.gl.getParameter(this.gl.CURRENT_PROGRAM) ? "bound" : null],
1036
+ ["arrayBuffer", this.gl.getParameter(this.gl.ARRAY_BUFFER_BINDING) ? "bound" : null],
1037
+ ["elementArrayBuffer", this.gl.getParameter(this.gl.ELEMENT_ARRAY_BUFFER_BINDING) ? "bound" : null],
1038
+ ["viewportWidth", this.viewportWidth],
1039
+ ["viewportHeight", this.viewportHeight],
1040
+ ["actualViewportWidth", viewport[2] ?? 0],
1041
+ ["actualViewportHeight", viewport[3] ?? 0],
1042
+ ["renderTarget", this.activeRenderTarget?.label ?? null],
1043
+ ["drawCalls", this.drawCalls],
1044
+ ["shaderProgramCreates", this.shaderProgramCreateCount],
1045
+ ["uniformLocationLookups", this.uniformLocationLookupCount],
1046
+ ["bufferUpdates", this.bufferUpdateCount],
1047
+ ["textureBinds", this.textureBindCount],
1048
+ ["samplerParameterUploads", this.samplerParameterUploadCount],
1049
+ ["vertexArrayCreates", this.vertexArrayCreateCount],
1050
+ ["samplerObjects", this.samplerObjectCache.size],
1051
+ ["samplerAnisotropyUploads", this.samplerAnisotropyUploadCount],
1052
+ ["maxTextureAnisotropy", this.maxTextureAnisotropy],
1053
+ ["nativeEnvironmentBindings", this.nativeEnvironmentBindings],
1054
+ ["nativeShadowMapBindings", this.nativeShadowMapBindings],
1055
+ ["nativeInstancedSubmissions", this.nativeInstancedSubmissions],
1056
+ ["stateCacheIssued", this.stateCache.stats().issued],
1057
+ ["stateCacheSkipped", this.stateCache.stats().skipped]
1058
+ ]);
1059
+ }
1060
+ getDiagnostics() {
1061
+ this.releaseDisposedTextureHandles();
1062
+ const liveRenderTargets = [...this.renderTargets].filter((target) => !target.disposed);
1063
+ const liveTextures = new Set([
1064
+ ...liveRenderTargets.map((target) => target.colorTexture),
1065
+ ...[...this.textures.keys()].filter((texture) => !texture.disposed)
1066
+ ]);
1067
+ const liveTextureList = [...liveTextures];
1068
+ const compressedTextures = liveTextureList.filter((texture) => isCompressedTextureFormat(texture.format));
1069
+ const fallbackTextures = liveTextureList.filter((texture) => this.textureUploadModes.get(texture) === "fallback");
1070
+ const stateCacheStats = this.stateCache.stats();
1071
+ const bufferBytes = [...this.buffers].filter((buffer) => !buffer.disposed).reduce((total, buffer) => total + buffer.byteLength, 0);
1072
+ const textureBytes = liveTextureList.reduce((total, texture) => total + texture.byteLength, 0);
1073
+ const gpuTargetEntries = this.describeGpuTargetInventory(liveRenderTargets);
1074
+ return {
1075
+ drawCalls: this.drawCalls,
1076
+ buffers: [...this.buffers].filter((buffer) => !buffer.disposed).length,
1077
+ shaders: [...this.shaders].filter((shader) => !shader.disposed).length,
1078
+ renderTargets: liveRenderTargets.length,
1079
+ maxRenderTargetSampleCount: Math.max(1, ...liveRenderTargets.map((target) => target.sampleCount)),
1080
+ textures: liveTextures.size,
1081
+ bufferBytes,
1082
+ textureBytes,
1083
+ approximateGpuMemoryBytes: bufferBytes + textureBytes,
1084
+ compressedTextures: compressedTextures.length,
1085
+ compressedTextureBytes: compressedTextures.reduce((total, texture) => total + texture.byteLength, 0),
1086
+ textureFallbacks: fallbackTextures.length,
1087
+ textureFallbackBytes: fallbackTextures.reduce((total, texture) => total + texture.fallbackByteLength, 0),
1088
+ nativeEnvironmentBindings: this.nativeEnvironmentBindings,
1089
+ nativeShadowMapBindings: this.nativeShadowMapBindings,
1090
+ shadowRenderTargetsAllocated: this.shadowRenderTargetsAllocated,
1091
+ nativeInstancedSubmissions: this.nativeInstancedSubmissions,
1092
+ bloom: this.lastBloomDiagnostics,
1093
+ samplerAnisotropyUploads: this.samplerAnisotropyUploadCount,
1094
+ maxTextureAnisotropy: this.maxTextureAnisotropy,
1095
+ stateCacheIssued: stateCacheStats.issued,
1096
+ stateCacheSkipped: stateCacheStats.skipped,
1097
+ stateCacheProgramSwitches: stateCacheStats.byOperation.useProgram?.issued ?? 0,
1098
+ stateCacheTextureBinds: stateCacheStats.byOperation.bindTexture?.issued ?? 0,
1099
+ stateCacheBufferBinds: stateCacheStats.byOperation.bindBuffer?.issued ?? 0,
1100
+ stateCacheVertexArrayBinds: stateCacheStats.byOperation.bindVertexArray?.issued ?? 0,
1101
+ stateCacheSamplerBinds: stateCacheStats.byOperation.bindSampler?.issued ?? 0,
1102
+ disposedBuffers: [...this.buffers].filter((buffer) => buffer.disposed).length,
1103
+ disposedShaders: [...this.shaders].filter((shader) => shader.disposed).length,
1104
+ disposedRenderTargets: [...this.renderTargets].filter((target) => target.disposed).length,
1105
+ disposedTextures: [...this.renderTargets].filter((target) => target.colorTexture.disposed).length + this.releasedTextureHandles,
1106
+ ...spreadGpuTargetInventory(gpuTargetEntries),
1107
+ lastError: this.lastError,
1108
+ contextLost: this.contextLost
1109
+ };
1110
+ }
1111
+ dispose() {
1112
+ if (this.canvas && "removeEventListener" in this.canvas) {
1113
+ this.deviceLostListeners.clear();
1114
+ this.deviceRestoredListeners.clear();
1115
+ if (this.contextLostListener)
1116
+ this.canvas.removeEventListener("webglcontextlost", this.contextLostListener);
1117
+ if (this.contextRestoredListener)
1118
+ this.canvas.removeEventListener("webglcontextrestored", this.contextRestoredListener);
1119
+ }
1120
+ for (const buffer of this.buffers) {
1121
+ buffer.dispose();
1122
+ }
1123
+ for (const shader of this.shaders) {
1124
+ shader.dispose();
1125
+ }
1126
+ for (const target of this.renderTargets) {
1127
+ target.dispose();
1128
+ }
1129
+ for (const entry of this.vertexArrayCache.values()) {
1130
+ this.gl.deleteVertexArray(entry.handle);
1131
+ }
1132
+ this.vertexArrayCache.clear();
1133
+ for (const sampler of this.samplerObjectCache.values()) {
1134
+ this.gl.deleteSampler(sampler);
1135
+ }
1136
+ this.samplerObjectCache.clear();
1137
+ for (const texture of this.textures.values()) {
1138
+ this.gl.deleteTexture(texture);
1139
+ }
1140
+ this.textureUploadModes.clear();
1141
+ if (this.fallbackTexture) {
1142
+ this.gl.deleteTexture(this.fallbackTexture);
1143
+ this.fallbackTexture = null;
1144
+ }
1145
+ if (this.fallbackCubeTexture) {
1146
+ this.gl.deleteTexture(this.fallbackCubeTexture);
1147
+ this.fallbackCubeTexture = null;
1148
+ }
1149
+ if (this.presentationProgram) {
1150
+ this.gl.deleteProgram(this.presentationProgram);
1151
+ this.presentationProgram = null;
1152
+ }
1153
+ if (this.ldrPostprocessProgram) {
1154
+ this.gl.deleteProgram(this.ldrPostprocessProgram);
1155
+ this.ldrPostprocessProgram = null;
1156
+ }
1157
+ if (this.bloomBrightExtractProgram) {
1158
+ this.gl.deleteProgram(this.bloomBrightExtractProgram);
1159
+ this.bloomBrightExtractProgram = null;
1160
+ }
1161
+ if (this.bloomBlurProgram) {
1162
+ this.gl.deleteProgram(this.bloomBlurProgram);
1163
+ this.bloomBlurProgram = null;
1164
+ }
1165
+ if (this.bloomCompositeProgram) {
1166
+ this.gl.deleteProgram(this.bloomCompositeProgram);
1167
+ this.bloomCompositeProgram = null;
1168
+ }
1169
+ if (this.bloomDownsampleProgram) {
1170
+ this.gl.deleteProgram(this.bloomDownsampleProgram);
1171
+ this.bloomDownsampleProgram = null;
1172
+ }
1173
+ if (this.bloomAccumulateProgram) {
1174
+ this.gl.deleteProgram(this.bloomAccumulateProgram);
1175
+ this.bloomAccumulateProgram = null;
1176
+ }
1177
+ if (this.outlineProgram) {
1178
+ this.gl.deleteProgram(this.outlineProgram);
1179
+ this.outlineProgram = null;
1180
+ }
1181
+ if (this.ssaoProgram) {
1182
+ this.gl.deleteProgram(this.ssaoProgram);
1183
+ this.ssaoProgram = null;
1184
+ }
1185
+ if (this.ssrProgram) {
1186
+ this.gl.deleteProgram(this.ssrProgram);
1187
+ this.ssrProgram = null;
1188
+ }
1189
+ if (this.depthOfFieldProgram) {
1190
+ this.gl.deleteProgram(this.depthOfFieldProgram);
1191
+ this.depthOfFieldProgram = null;
1192
+ }
1193
+ if (this.motionBlurProgram) {
1194
+ this.gl.deleteProgram(this.motionBlurProgram);
1195
+ this.motionBlurProgram = null;
1196
+ }
1197
+ if (this.motionBlurVelocityTexture) {
1198
+ this.gl.deleteTexture(this.motionBlurVelocityTexture);
1199
+ this.motionBlurVelocityTexture = null;
1200
+ this.motionBlurVelocitySize = null;
1201
+ }
1202
+ if (this.taaProgram) {
1203
+ this.gl.deleteProgram(this.taaProgram);
1204
+ this.taaProgram = null;
1205
+ }
1206
+ if (this.taaHistoryTexture) {
1207
+ this.gl.deleteTexture(this.taaHistoryTexture);
1208
+ this.taaHistoryTexture = null;
1209
+ this.taaHistorySize = null;
1210
+ }
1211
+ this.disposeBloomPingPongResources();
1212
+ this.disposeBloomPyramidResources();
1213
+ if (this.bloomBrightLutTexture) {
1214
+ this.gl.deleteTexture(this.bloomBrightLutTexture);
1215
+ this.bloomBrightLutTexture = null;
1216
+ this.bloomBrightLutThreshold = null;
1217
+ }
1218
+ if (this.bloomCompositeLutTexture) {
1219
+ this.gl.deleteTexture(this.bloomCompositeLutTexture);
1220
+ this.bloomCompositeLutTexture = null;
1221
+ this.bloomCompositeLutIntensity = null;
1222
+ this.bloomCompositeLutShoulder = null;
1223
+ }
1224
+ if (this.outlineBlendLutTexture) {
1225
+ this.gl.deleteTexture(this.outlineBlendLutTexture);
1226
+ this.outlineBlendLutTexture = null;
1227
+ this.outlineBlendLutKey = null;
1228
+ }
1229
+ if (this.presentationVertexArray) {
1230
+ this.gl.deleteVertexArray(this.presentationVertexArray);
1231
+ this.presentationVertexArray = null;
1232
+ }
1233
+ if (this.depthReadbackProgram) {
1234
+ this.gl.deleteProgram(this.depthReadbackProgram);
1235
+ this.depthReadbackProgram = null;
1236
+ }
1237
+ this.textures.clear();
1238
+ this.disposed = true;
1239
+ }
1240
+ copyDepthTextureToBytes(source, x, y, width, height) {
1241
+ const colorHandle = this.gl.createTexture();
1242
+ const framebuffer = this.gl.createFramebuffer();
1243
+ if (!colorHandle || !framebuffer) {
1244
+ if (colorHandle)
1245
+ this.gl.deleteTexture(colorHandle);
1246
+ if (framebuffer)
1247
+ this.gl.deleteFramebuffer(framebuffer);
1248
+ throw new RenderDeviceError("Failed to allocate WebGL2 depth readback target", "WEBGL_ALLOCATION_FAILED", {
1249
+ renderTarget: source.label
1250
+ });
1251
+ }
1252
+ const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
1253
+ const previousProgram = this.gl.getParameter(this.gl.CURRENT_PROGRAM);
1254
+ const textureUnit0 = this.captureTextureUnit0();
1255
+ const viewport = this.gl.getParameter(this.gl.VIEWPORT);
1256
+ const depthTestEnabled = this.gl.isEnabled(this.gl.DEPTH_TEST);
1257
+ const depthMask = this.gl.getParameter(this.gl.DEPTH_WRITEMASK);
1258
+ const cullFaceEnabled = this.gl.isEnabled(this.gl.CULL_FACE);
1259
+ const blendEnabled = this.gl.isEnabled(this.gl.BLEND);
1260
+ const pixels = new Uint8Array(width * height * 4);
1261
+ try {
1262
+ this.gl.activeTexture(this.gl.TEXTURE0);
1263
+ this.gl.bindTexture(this.gl.TEXTURE_2D, colorHandle);
1264
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
1265
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
1266
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
1267
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
1268
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, width, height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null);
1269
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
1270
+ this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, colorHandle, 0);
1271
+ const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
1272
+ if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
1273
+ throw new RenderDeviceError("WebGL2 depth readback framebuffer status is invalid", "FRAMEBUFFER_INVALID", {
1274
+ renderTarget: source.label,
1275
+ status
1276
+ });
1277
+ }
1278
+ this.gl.viewport(0, 0, width, height);
1279
+ this.gl.disable(this.gl.DEPTH_TEST);
1280
+ this.gl.disable(this.gl.CULL_FACE);
1281
+ this.gl.disable(this.gl.BLEND);
1282
+ this.gl.depthMask(false);
1283
+ this.gl.useProgram(this.ensureDepthReadbackProgram());
1284
+ this.gl.activeTexture(this.gl.TEXTURE0);
1285
+ this.gl.bindTexture(this.gl.TEXTURE_2D, source.depthTextureHandle);
1286
+ this.gl.bindSampler(0, null);
1287
+ this.gl.uniform1i(this.gl.getUniformLocation(this.ensureDepthReadbackProgram(), "u_depth"), 0);
1288
+ this.gl.uniform4f(this.gl.getUniformLocation(this.ensureDepthReadbackProgram(), "u_sourceRect"), x / source.width, y / source.height, width / source.width, height / source.height);
1289
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1290
+ this.gl.readPixels(0, 0, width, height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
1291
+ const error = this.readError();
1292
+ if (error) {
1293
+ throw new RenderDeviceError("WebGL2 depth texture readback failed", "WEBGL_DEPTH_READBACK_FAILED", {
1294
+ renderTarget: source.label,
1295
+ error
1296
+ });
1297
+ }
1298
+ return pixels;
1299
+ }
1300
+ finally {
1301
+ this.restoreTextureUnit0(textureUnit0);
1302
+ this.gl.useProgram(previousProgram);
1303
+ if (depthTestEnabled)
1304
+ this.gl.enable(this.gl.DEPTH_TEST);
1305
+ else
1306
+ this.gl.disable(this.gl.DEPTH_TEST);
1307
+ if (cullFaceEnabled)
1308
+ this.gl.enable(this.gl.CULL_FACE);
1309
+ else
1310
+ this.gl.disable(this.gl.CULL_FACE);
1311
+ if (blendEnabled)
1312
+ this.gl.enable(this.gl.BLEND);
1313
+ else
1314
+ this.gl.disable(this.gl.BLEND);
1315
+ this.gl.depthMask(depthMask);
1316
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
1317
+ this.gl.viewport(viewport[0] ?? 0, viewport[1] ?? 0, viewport[2] ?? this.viewportWidth, viewport[3] ?? this.viewportHeight);
1318
+ this.gl.deleteTexture(colorHandle);
1319
+ this.gl.deleteFramebuffer(framebuffer);
1320
+ this.stateCache.invalidate();
1321
+ }
1322
+ }
1323
+ drawRenderTargetToBackbuffer(source, outputWidth, outputHeight) {
1324
+ const program = this.ensurePresentationProgram();
1325
+ const vertexArray = this.ensurePresentationVertexArray();
1326
+ const previousState = this.prepareFullscreenPresentation(null, outputWidth, outputHeight);
1327
+ this.gl.useProgram(program);
1328
+ this.gl.bindVertexArray(vertexArray);
1329
+ this.gl.activeTexture(this.gl.TEXTURE0);
1330
+ this.gl.bindTexture(this.gl.TEXTURE_2D, source.colorHandle);
1331
+ this.gl.bindSampler(0, null);
1332
+ try {
1333
+ const uniformLocation = this.gl.getUniformLocation(program, "u_source");
1334
+ this.gl.uniform1i(uniformLocation, 0);
1335
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1336
+ this.gl.flush();
1337
+ }
1338
+ finally {
1339
+ this.restoreFullscreenPresentationState(previousState, outputWidth, outputHeight);
1340
+ }
1341
+ }
1342
+ ensureBloomPingPongResources(width, height, hdr = false) {
1343
+ const current = this.bloomPingPongResources;
1344
+ if (current && current.width === width && current.height === height && current.hdr === hdr) {
1345
+ return current;
1346
+ }
1347
+ this.disposeBloomPingPongResources();
1348
+ this.disposeBloomPyramidResources();
1349
+ const textureUnit0 = this.captureTextureUnit0();
1350
+ const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
1351
+ const textures = [];
1352
+ const framebuffers = [];
1353
+ try {
1354
+ for (let index = 0; index < 2; index += 1) {
1355
+ const texture = this.gl.createTexture();
1356
+ const framebuffer = this.gl.createFramebuffer();
1357
+ if (!texture || !framebuffer) {
1358
+ if (texture)
1359
+ this.gl.deleteTexture(texture);
1360
+ if (framebuffer)
1361
+ this.gl.deleteFramebuffer(framebuffer);
1362
+ throw new RenderDeviceError("Failed to allocate WebGL2 bloom ping-pong resources", "WEBGL_ALLOCATION_FAILED", {
1363
+ width,
1364
+ height,
1365
+ index
1366
+ });
1367
+ }
1368
+ textures.push(texture);
1369
+ framebuffers.push(framebuffer);
1370
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
1371
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
1372
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
1373
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
1374
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
1375
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, hdr ? this.gl.RGBA16F : this.gl.RGBA8, width, height, 0, this.gl.RGBA, hdr ? this.gl.HALF_FLOAT : this.gl.UNSIGNED_BYTE, null);
1376
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
1377
+ this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
1378
+ const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
1379
+ if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
1380
+ throw new RenderDeviceError("WebGL2 bloom ping-pong framebuffer status is invalid", "FRAMEBUFFER_INVALID", {
1381
+ width,
1382
+ height,
1383
+ index,
1384
+ status
1385
+ });
1386
+ }
1387
+ }
1388
+ const resources = {
1389
+ width,
1390
+ height,
1391
+ hdr,
1392
+ textures: [textures[0], textures[1]],
1393
+ framebuffers: [framebuffers[0], framebuffers[1]]
1394
+ };
1395
+ this.bloomPingPongResources = resources;
1396
+ return resources;
1397
+ }
1398
+ catch (error) {
1399
+ for (const texture of textures)
1400
+ this.gl.deleteTexture(texture);
1401
+ for (const framebuffer of framebuffers)
1402
+ this.gl.deleteFramebuffer(framebuffer);
1403
+ throw error;
1404
+ }
1405
+ finally {
1406
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
1407
+ this.restoreTextureUnit0(textureUnit0);
1408
+ this.stateCache.invalidate();
1409
+ }
1410
+ }
1411
+ ensureBloomLutTextures(options) {
1412
+ const updateBright = this.bloomBrightLutThreshold !== options.threshold;
1413
+ const updateComposite = this.bloomCompositeLutIntensity !== options.intensity ||
1414
+ this.bloomCompositeLutShoulder !== options.shoulder;
1415
+ if (this.bloomBrightLutTexture && this.bloomCompositeLutTexture && !updateBright && !updateComposite) {
1416
+ return;
1417
+ }
1418
+ const textureUnit0 = this.captureTextureUnit0();
1419
+ try {
1420
+ if (!this.bloomBrightLutTexture) {
1421
+ this.bloomBrightLutTexture = this.createNativeBloomDataTexture();
1422
+ }
1423
+ if (updateBright) {
1424
+ const pixels = createBloomBrightThresholdLut(options.threshold);
1425
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.bloomBrightLutTexture);
1426
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, BLOOM_BRIGHT_LUT_WIDTH, BLOOM_BRIGHT_LUT_HEIGHT, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
1427
+ this.bloomBrightLutThreshold = options.threshold;
1428
+ }
1429
+ if (!this.bloomCompositeLutTexture) {
1430
+ this.bloomCompositeLutTexture = this.createNativeBloomDataTexture();
1431
+ }
1432
+ if (updateComposite) {
1433
+ const pixels = createBloomCompositeLut(options.intensity, { shoulder: options.shoulder });
1434
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.bloomCompositeLutTexture);
1435
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, BLOOM_COMPOSITE_LUT_SIZE, BLOOM_COMPOSITE_LUT_SIZE, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
1436
+ this.bloomCompositeLutIntensity = options.intensity;
1437
+ this.bloomCompositeLutShoulder = options.shoulder;
1438
+ }
1439
+ }
1440
+ finally {
1441
+ this.restoreTextureUnit0(textureUnit0);
1442
+ this.stateCache.invalidate();
1443
+ }
1444
+ }
1445
+ createNativeBloomDataTexture() {
1446
+ const texture = this.gl.createTexture();
1447
+ if (!texture) {
1448
+ throw new RenderDeviceError("Failed to allocate WebGL2 bloom lookup texture", "WEBGL_ALLOCATION_FAILED");
1449
+ }
1450
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
1451
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
1452
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
1453
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
1454
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
1455
+ return texture;
1456
+ }
1457
+ ensureOutlineBlendLutTexture(options) {
1458
+ const combinedAlpha = options.opacity * (options.color[3] / 255);
1459
+ const key = `${options.color.join(",")}|${combinedAlpha}`;
1460
+ if (this.outlineBlendLutTexture && this.outlineBlendLutKey === key)
1461
+ return;
1462
+ const textureUnit0 = this.captureTextureUnit0();
1463
+ try {
1464
+ if (!this.outlineBlendLutTexture) {
1465
+ this.outlineBlendLutTexture = this.createNativeBloomDataTexture();
1466
+ }
1467
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.outlineBlendLutTexture);
1468
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, OUTLINE_BLEND_LUT_WIDTH, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, createOutlineBlendLut(options.color, combinedAlpha));
1469
+ this.outlineBlendLutKey = key;
1470
+ }
1471
+ finally {
1472
+ this.restoreTextureUnit0(textureUnit0);
1473
+ this.stateCache.invalidate();
1474
+ }
1475
+ }
1476
+ ensureMotionBlurVelocityTexture(width, height, velocity) {
1477
+ const textureUnit0 = this.captureTextureUnit0();
1478
+ try {
1479
+ if (!this.motionBlurVelocityTexture) {
1480
+ this.motionBlurVelocityTexture = this.createNativeBloomDataTexture();
1481
+ }
1482
+ this.motionBlurVelocitySize = { width, height };
1483
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.motionBlurVelocityTexture);
1484
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RG32F, width, height, 0, this.gl.RG, this.gl.FLOAT, velocity);
1485
+ }
1486
+ finally {
1487
+ this.restoreTextureUnit0(textureUnit0);
1488
+ this.stateCache.invalidate();
1489
+ }
1490
+ }
1491
+ ensureTaaHistoryTexture(width, height, history) {
1492
+ const textureUnit0 = this.captureTextureUnit0();
1493
+ try {
1494
+ if (!this.taaHistoryTexture) {
1495
+ this.taaHistoryTexture = this.createNativeBloomDataTexture();
1496
+ }
1497
+ this.taaHistorySize = { width, height };
1498
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.taaHistoryTexture);
1499
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, width, height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, history);
1500
+ }
1501
+ finally {
1502
+ this.restoreTextureUnit0(textureUnit0);
1503
+ this.stateCache.invalidate();
1504
+ }
1505
+ }
1506
+ drawNativeLdrStage(sourceTexture, framebuffer, width, height, program, vertexArray, tonePass, colorPass, fxaaPass, toneMappingDefaults) {
1507
+ const toneOptions = { ...(toneMappingDefaults ?? {}), ...(tonePass?.options ?? {}) };
1508
+ const colorOptions = colorPass?.options ?? {};
1509
+ const fxaaOptions = fxaaPass?.options ?? {};
1510
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
1511
+ this.gl.viewport(0, 0, width, height);
1512
+ this.gl.useProgram(program);
1513
+ this.gl.bindVertexArray(vertexArray);
1514
+ this.bindFullscreenTexture(0, sourceTexture);
1515
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
1516
+ this.gl.uniform2f(this.gl.getUniformLocation(program, "u_texelSize"), 1 / width, 1 / height);
1517
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasToneMapping"), tonePass ? 1 : 0);
1518
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_toneOperator"), toneMappingOperatorId(stringOption(toneOptions, "operator", "reinhard")));
1519
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_inputColorSpace"), colorSpaceId(stringOption(toneOptions, "inputColorSpace", "linear")));
1520
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_outputColorSpace"), colorSpaceId(stringOption(toneOptions, "outputColorSpace", "linear")));
1521
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_exposure"), numberOption(toneOptions, "exposure", 1));
1522
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_whitePoint"), Math.max(0.0001, numberOption(toneOptions, "whitePoint", 1)));
1523
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_gamma"), Math.max(0.0001, numberOption(toneOptions, "gamma", 2.2)));
1524
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasColorGrade"), colorPass ? 1 : 0);
1525
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_contrast"), numberOption(colorOptions, "contrast", 1));
1526
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_temperature"), numberOption(colorOptions, "temperature", 0));
1527
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_tint"), numberOption(colorOptions, "tint", 0));
1528
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_saturation"), numberOption(colorOptions, "saturation", 1));
1529
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_vibrance"), numberOption(colorOptions, "vibrance", 0));
1530
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_vignette"), numberOption(colorOptions, "vignette", 0));
1531
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_sharpening"), numberOption(colorOptions, "sharpening", 0));
1532
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasFxaa"), fxaaPass ? 1 : 0);
1533
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_edgeThreshold"), numberOption(fxaaOptions, "edgeThreshold", 0.125));
1534
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_subpixelBlend"), numberOption(fxaaOptions, "subpixelBlend", 0.75));
1535
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1536
+ }
1537
+ executeNativeBloomPasses(sourceTexture, resources, options, vertexArray, hdr) {
1538
+ const brightLut = this.bloomBrightLutTexture;
1539
+ const compositeLut = this.bloomCompositeLutTexture;
1540
+ if (!hdr && (!brightLut || !compositeLut)) {
1541
+ throw new RenderDeviceError("WebGL2 bloom lookup textures were not initialized", "WEBGL_ALLOCATION_FAILED");
1542
+ }
1543
+ const [textureA, textureB] = resources.textures;
1544
+ const [framebufferA, framebufferB] = resources.framebuffers;
1545
+ const brightProgram = this.ensureBloomBrightExtractProgram();
1546
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebufferA);
1547
+ this.gl.viewport(0, 0, resources.width, resources.height);
1548
+ this.gl.useProgram(brightProgram);
1549
+ this.gl.bindVertexArray(vertexArray);
1550
+ this.bindFullscreenTexture(0, sourceTexture);
1551
+ if (brightLut)
1552
+ this.bindFullscreenTexture(1, brightLut);
1553
+ this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_source"), 0);
1554
+ this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_brightLut"), 1);
1555
+ this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_hdr"), hdr ? 1 : 0);
1556
+ this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_threshold"), options.threshold);
1557
+ this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_softKnee"), options.softKnee);
1558
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1559
+ const blurProgram = this.ensureBloomBlurProgram();
1560
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebufferB);
1561
+ this.gl.useProgram(blurProgram);
1562
+ this.bindFullscreenTexture(0, textureA);
1563
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_source"), 0);
1564
+ this.gl.uniform2i(this.gl.getUniformLocation(blurProgram, "u_size"), resources.width, resources.height);
1565
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_radius"), options.radius);
1566
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_horizontal"), 1);
1567
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_hdr"), hdr ? 1 : 0);
1568
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1569
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebufferA);
1570
+ this.bindFullscreenTexture(0, textureB);
1571
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_horizontal"), 0);
1572
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1573
+ const compositeProgram = this.ensureBloomCompositeProgram();
1574
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebufferB);
1575
+ this.gl.useProgram(compositeProgram);
1576
+ this.bindFullscreenTexture(0, sourceTexture);
1577
+ this.bindFullscreenTexture(1, textureA);
1578
+ if (compositeLut)
1579
+ this.bindFullscreenTexture(2, compositeLut);
1580
+ this.gl.uniform1i(this.gl.getUniformLocation(compositeProgram, "u_source"), 0);
1581
+ this.gl.uniform1i(this.gl.getUniformLocation(compositeProgram, "u_blurred"), 1);
1582
+ this.gl.uniform1i(this.gl.getUniformLocation(compositeProgram, "u_compositeLut"), 2);
1583
+ this.gl.uniform1i(this.gl.getUniformLocation(compositeProgram, "u_hdr"), hdr ? 1 : 0);
1584
+ this.gl.uniform1f(this.gl.getUniformLocation(compositeProgram, "u_intensity"), options.intensity);
1585
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1586
+ return textureB;
1587
+ }
1588
+ /**
1589
+ * Balanced/cinematic bloom: bright-extract once, then a half-resolution mip
1590
+ * chain where every level is downsampled, separably blurred, and accumulated
1591
+ * with its energy-preserving weight. Returns a full-resolution bloom texture
1592
+ * holding the weighted sum, ready for the existing composite program.
1593
+ */
1594
+ executeNativeBloomPyramidPasses(sourceTexture, sourceWidth, sourceHeight, resources, fullResStaging, options, vertexArray, hdr) {
1595
+ const plan = resources.plan;
1596
+ const mipCount = plan.mips.length;
1597
+ const blurRadii = resolveBloomPyramidBlurRadii(plan, options.radius);
1598
+ const downsampleProgram = this.ensureBloomDownsampleProgram();
1599
+ const brightProgram = this.ensureBloomBrightExtractProgram();
1600
+ const blurProgram = this.ensureBloomBlurProgram();
1601
+ const accumulateProgram = this.ensureBloomAccumulateProgram();
1602
+ const brightLut = this.bloomBrightLutTexture;
1603
+ const mipTexture = (level, slot) => resources.textures[level * 2 + slot];
1604
+ const mipFramebuffer = (level, slot) => resources.framebuffers[level * 2 + slot];
1605
+ const mipSize = (level) => plan.mips[level];
1606
+ // Every level settles in slot 0 after the vertical blur.
1607
+ const blurredSlot = (_level) => 0;
1608
+ const drawDownsample = (fromTexture, fromWidth, fromHeight, toFramebuffer, toWidth, toHeight) => {
1609
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, toFramebuffer);
1610
+ this.gl.viewport(0, 0, toWidth, toHeight);
1611
+ this.gl.useProgram(downsampleProgram);
1612
+ this.gl.bindVertexArray(vertexArray);
1613
+ this.bindFullscreenTexture(0, fromTexture);
1614
+ this.gl.uniform1i(this.gl.getUniformLocation(downsampleProgram, "u_source"), 0);
1615
+ this.gl.uniform2f(this.gl.getUniformLocation(downsampleProgram, "u_texelSize"), 1 / fromWidth, 1 / fromHeight);
1616
+ this.gl.uniform2f(this.gl.getUniformLocation(downsampleProgram, "u_targetSize"), toWidth, toHeight);
1617
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1618
+ };
1619
+ const drawBrightExtract = (fromTexture, toFramebuffer, size) => {
1620
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, toFramebuffer);
1621
+ this.gl.viewport(0, 0, size.width, size.height);
1622
+ this.gl.useProgram(brightProgram);
1623
+ this.gl.bindVertexArray(vertexArray);
1624
+ this.bindFullscreenTexture(0, fromTexture);
1625
+ if (brightLut)
1626
+ this.bindFullscreenTexture(1, brightLut);
1627
+ this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_source"), 0);
1628
+ this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_brightLut"), 1);
1629
+ this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_hdr"), hdr ? 1 : 0);
1630
+ this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_threshold"), options.threshold);
1631
+ this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_softKnee"), options.softKnee);
1632
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1633
+ };
1634
+ const drawSeparableBlur = (readTexture, tmpTexture, tmpFramebuffer, outFramebuffer, size, radius) => {
1635
+ // Horizontal into the temp target, vertical back into the output target.
1636
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, tmpFramebuffer);
1637
+ this.gl.viewport(0, 0, size.width, size.height);
1638
+ this.gl.useProgram(blurProgram);
1639
+ this.gl.bindVertexArray(vertexArray);
1640
+ this.bindFullscreenTexture(0, readTexture);
1641
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_source"), 0);
1642
+ this.gl.uniform2i(this.gl.getUniformLocation(blurProgram, "u_size"), size.width, size.height);
1643
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_radius"), radius);
1644
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_horizontal"), 1);
1645
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_hdr"), hdr ? 1 : 0);
1646
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1647
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, outFramebuffer);
1648
+ this.bindFullscreenTexture(0, tmpTexture);
1649
+ this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_horizontal"), 0);
1650
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1651
+ };
1652
+ // Extract highlights at the source resolution before the first downsample.
1653
+ // Downsampling the complete scene first averaged narrow emissive sources
1654
+ // below the authored threshold, so balanced/cinematic bloom could execute
1655
+ // all native passes yet remain pixel-identical to disabled output.
1656
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fullResStaging.framebufferA);
1657
+ this.gl.viewport(0, 0, sourceWidth, sourceHeight);
1658
+ this.gl.useProgram(brightProgram);
1659
+ this.gl.bindVertexArray(vertexArray);
1660
+ this.bindFullscreenTexture(0, sourceTexture);
1661
+ if (brightLut)
1662
+ this.bindFullscreenTexture(1, brightLut);
1663
+ this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_source"), 0);
1664
+ this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_brightLut"), 1);
1665
+ this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_hdr"), hdr ? 1 : 0);
1666
+ this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_threshold"), options.threshold);
1667
+ this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_softKnee"), options.softKnee);
1668
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1669
+ // Level 0 downsamples the extracted highlight field, then blurs H into
1670
+ // slot 1 and V back into slot 0. The result therefore settles in slot 0.
1671
+ const level0 = mipSize(0);
1672
+ drawDownsample(fullResStaging.textureA, sourceWidth, sourceHeight, mipFramebuffer(0, 0), level0.width, level0.height);
1673
+ drawSeparableBlur(mipTexture(0, 0), mipTexture(0, 1), mipFramebuffer(0, 1), mipFramebuffer(0, 0), level0, blurRadii[0]);
1674
+ for (let level = 1; level < mipCount; level += 1) {
1675
+ const size = mipSize(level);
1676
+ const previous = mipSize(level - 1);
1677
+ drawDownsample(mipTexture(level - 1, blurredSlot(level - 1)), previous.width, previous.height, mipFramebuffer(level, 0), size.width, size.height);
1678
+ // H into slot 1, V back into slot 0: blurred result settles in slot 0.
1679
+ drawSeparableBlur(mipTexture(level, 0), mipTexture(level, 1), mipFramebuffer(level, 1), mipFramebuffer(level, 0), size, blurRadii[level]);
1680
+ }
1681
+ // Accumulate smallest-to-largest, ping-ponging between the two
1682
+ // accumulators (a pass must never sample its own bound target).
1683
+ const weights = plan.weights;
1684
+ let readAccumulator = { texture: resources.accumulatorTextureA, framebuffer: resources.accumulatorFramebufferA };
1685
+ let writeAccumulator = { texture: resources.accumulatorTextureB, framebuffer: resources.accumulatorFramebufferB };
1686
+ // Clear both accumulators; the seed reads the cleared sibling, never the
1687
+ // bound target (feedback loop).
1688
+ for (const acc of [readAccumulator, writeAccumulator]) {
1689
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, acc.framebuffer);
1690
+ this.gl.viewport(0, 0, level0.width, level0.height);
1691
+ this.gl.clearColor(0, 0, 0, 0);
1692
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
1693
+ }
1694
+ // Seed: smallest mip onto the cleared sibling accumulator.
1695
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, writeAccumulator.framebuffer);
1696
+ this.gl.viewport(0, 0, level0.width, level0.height);
1697
+ this.gl.useProgram(accumulateProgram);
1698
+ this.gl.bindVertexArray(vertexArray);
1699
+ this.bindFullscreenTexture(0, readAccumulator.texture);
1700
+ this.bindFullscreenTexture(1, mipTexture(mipCount - 1, blurredSlot(mipCount - 1)));
1701
+ this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_base"), 0);
1702
+ this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_bloom"), 1);
1703
+ this.gl.uniform1f(this.gl.getUniformLocation(accumulateProgram, "u_weight"), weights[mipCount - 1]);
1704
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1705
+ for (let level = mipCount - 2; level >= 0; level -= 1) {
1706
+ const nextRead = writeAccumulator;
1707
+ const nextWrite = readAccumulator;
1708
+ readAccumulator = nextRead;
1709
+ writeAccumulator = nextWrite;
1710
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, writeAccumulator.framebuffer);
1711
+ this.gl.viewport(0, 0, level0.width, level0.height);
1712
+ this.gl.useProgram(accumulateProgram);
1713
+ this.gl.bindVertexArray(vertexArray);
1714
+ this.bindFullscreenTexture(0, readAccumulator.texture);
1715
+ this.bindFullscreenTexture(1, mipTexture(level, blurredSlot(level)));
1716
+ this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_base"), 0);
1717
+ this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_bloom"), 1);
1718
+ this.gl.uniform1f(this.gl.getUniformLocation(accumulateProgram, "u_weight"), weights[level]);
1719
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1720
+ }
1721
+ // Upsample the weighted sum onto full-resolution staging target A, using
1722
+ // the cleared staging target B as the zero base (never sample A while
1723
+ // rendering into it).
1724
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fullResStaging.framebufferB);
1725
+ this.gl.viewport(0, 0, sourceWidth, sourceHeight);
1726
+ this.gl.clearColor(0, 0, 0, 0);
1727
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
1728
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fullResStaging.framebufferA);
1729
+ this.gl.viewport(0, 0, sourceWidth, sourceHeight);
1730
+ this.gl.useProgram(accumulateProgram);
1731
+ this.gl.bindVertexArray(vertexArray);
1732
+ this.bindFullscreenTexture(0, fullResStaging.textureB);
1733
+ this.bindFullscreenTexture(1, writeAccumulator.texture);
1734
+ this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_base"), 0);
1735
+ this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_bloom"), 1);
1736
+ this.gl.uniform1f(this.gl.getUniformLocation(accumulateProgram, "u_weight"), 1);
1737
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1738
+ return fullResStaging.textureA;
1739
+ }
1740
+ executeNativeOutlinePasses(sourceTexture, resources, options, vertexArray, targetIndex) {
1741
+ const blendLut = this.outlineBlendLutTexture;
1742
+ if (!blendLut) {
1743
+ throw new RenderDeviceError("WebGL2 outline lookup texture was not initialized", "WEBGL_ALLOCATION_FAILED");
1744
+ }
1745
+ const program = this.ensureOutlineProgram();
1746
+ const targetTexture = resources.textures[targetIndex];
1747
+ const targetFramebuffer = resources.framebuffers[targetIndex];
1748
+ const bound = createOutlineGradientBound(options.threshold);
1749
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, targetFramebuffer);
1750
+ this.gl.viewport(0, 0, resources.width, resources.height);
1751
+ this.gl.useProgram(program);
1752
+ this.gl.bindVertexArray(vertexArray);
1753
+ this.bindFullscreenTexture(0, sourceTexture);
1754
+ this.bindFullscreenTexture(1, blendLut);
1755
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
1756
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_blendLut"), 1);
1757
+ this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
1758
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_width"), options.width);
1759
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_boundHigh"), bound.highWord);
1760
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_boundLow"), bound.lowWord);
1761
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1762
+ return targetTexture;
1763
+ }
1764
+ executeNativeSsaoPass(sourceTexture, depthTexture, resources, options, vertexArray, targetIndex) {
1765
+ const program = this.ensureSsaoProgram();
1766
+ const targetTexture = resources.textures[targetIndex];
1767
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
1768
+ this.gl.viewport(0, 0, resources.width, resources.height);
1769
+ this.gl.useProgram(program);
1770
+ this.gl.bindVertexArray(vertexArray);
1771
+ this.bindFullscreenTexture(0, sourceTexture);
1772
+ this.bindFullscreenTexture(1, depthTexture);
1773
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
1774
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_depth"), 1);
1775
+ this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
1776
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_radius"), options.radius);
1777
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_intensity"), options.intensity);
1778
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_bias"), options.bias);
1779
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1780
+ return targetTexture;
1781
+ }
1782
+ executeNativeSsrPass(sourceTexture, depthTexture, resources, options, vertexArray, targetIndex, depthRange) {
1783
+ const program = this.ensureSsrProgram();
1784
+ const targetTexture = resources.textures[targetIndex];
1785
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
1786
+ this.gl.viewport(0, 0, resources.width, resources.height);
1787
+ this.gl.useProgram(program);
1788
+ this.gl.bindVertexArray(vertexArray);
1789
+ this.bindFullscreenTexture(0, sourceTexture);
1790
+ this.bindFullscreenTexture(1, depthTexture);
1791
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
1792
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_depth"), 1);
1793
+ this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
1794
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_intensity"), options.intensity);
1795
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_maxDistance"), options.maxDistance);
1796
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_depthNear"), depthRange.near);
1797
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_depthFar"), depthRange.far);
1798
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1799
+ return targetTexture;
1800
+ }
1801
+ executeNativeDepthOfFieldPass(sourceTexture, depthTexture, resources, options, vertexArray, targetIndex, depthRange) {
1802
+ const program = this.ensureDepthOfFieldProgram();
1803
+ const targetTexture = resources.textures[targetIndex];
1804
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
1805
+ this.gl.viewport(0, 0, resources.width, resources.height);
1806
+ this.gl.useProgram(program);
1807
+ this.gl.bindVertexArray(vertexArray);
1808
+ this.bindFullscreenTexture(0, sourceTexture);
1809
+ this.bindFullscreenTexture(1, depthTexture);
1810
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
1811
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_depth"), 1);
1812
+ this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
1813
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_focusDepth"), options.focusDepth);
1814
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_focusRange"), options.focusRange);
1815
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_maxRadius"), options.maxRadius);
1816
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_depthNear"), depthRange.near);
1817
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_depthFar"), depthRange.far);
1818
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1819
+ return targetTexture;
1820
+ }
1821
+ executeNativeMotionBlurPass(sourceTexture, resources, options, vertexArray, targetIndex) {
1822
+ const velocityTexture = this.motionBlurVelocityTexture;
1823
+ if (!velocityTexture) {
1824
+ throw new RenderDeviceError("WebGL2 motion-blur velocity texture was not initialized", "WEBGL_ALLOCATION_FAILED");
1825
+ }
1826
+ const program = this.ensureMotionBlurProgram();
1827
+ const targetTexture = resources.textures[targetIndex];
1828
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
1829
+ this.gl.viewport(0, 0, resources.width, resources.height);
1830
+ this.gl.useProgram(program);
1831
+ this.gl.bindVertexArray(vertexArray);
1832
+ this.bindFullscreenTexture(0, sourceTexture);
1833
+ this.bindFullscreenTexture(1, velocityTexture);
1834
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
1835
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_velocity"), 1);
1836
+ this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
1837
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_samples"), options.samples);
1838
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_scale"), options.scale);
1839
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1840
+ return targetTexture;
1841
+ }
1842
+ executeNativeTaaPass(sourceTexture, resources, options, vertexArray, targetIndex) {
1843
+ const historyTexture = this.taaHistoryTexture;
1844
+ if (!historyTexture) {
1845
+ throw new RenderDeviceError("WebGL2 TAA history texture was not initialized", "WEBGL_ALLOCATION_FAILED");
1846
+ }
1847
+ const program = this.ensureTaaProgram();
1848
+ const targetTexture = resources.textures[targetIndex];
1849
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
1850
+ this.gl.viewport(0, 0, resources.width, resources.height);
1851
+ this.gl.useProgram(program);
1852
+ this.gl.bindVertexArray(vertexArray);
1853
+ this.bindFullscreenTexture(0, sourceTexture);
1854
+ this.bindFullscreenTexture(1, historyTexture);
1855
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
1856
+ this.gl.uniform1i(this.gl.getUniformLocation(program, "u_history"), 1);
1857
+ this.gl.uniform1f(this.gl.getUniformLocation(program, "u_blend"), options.blend);
1858
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1859
+ return targetTexture;
1860
+ }
1861
+ bindFullscreenTexture(unit, texture) {
1862
+ this.gl.activeTexture(this.gl.TEXTURE0 + unit);
1863
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
1864
+ this.gl.bindSampler(unit, null);
1865
+ }
1866
+ disposeBloomPingPongResources() {
1867
+ if (!this.bloomPingPongResources)
1868
+ return;
1869
+ for (const texture of this.bloomPingPongResources.textures)
1870
+ this.gl.deleteTexture(texture);
1871
+ for (const framebuffer of this.bloomPingPongResources.framebuffers)
1872
+ this.gl.deleteFramebuffer(framebuffer);
1873
+ this.bloomPingPongResources = null;
1874
+ }
1875
+ /**
1876
+ * Diagnostics for the most recent native bloom execution (muse3jsparity-PRD
1877
+ * A1). Null when no native bloom pass has run yet on this device.
1878
+ */
1879
+ getBloomDiagnostics() {
1880
+ return this.lastBloomDiagnostics;
1881
+ }
1882
+ recordBloomDiagnostics(options, sourceWidth, sourceHeight, hdr, mipCount) {
1883
+ const plan = resolveBloomPyramidPlan(sourceWidth, sourceHeight, options.quality, hdr);
1884
+ const pingPongBytes = sourceWidth * sourceHeight * 4 * (hdr ? 2 : 1) * 2;
1885
+ const bytesPerChannel = plan.halfFloat ? 2 : 1;
1886
+ const level0 = plan.mips[0];
1887
+ const accumulatorBytes = level0.width * level0.height * 4 * bytesPerChannel;
1888
+ this.lastBloomDiagnostics = {
1889
+ quality: options.quality,
1890
+ mipCount,
1891
+ targetCount: options.quality === "performance" ? 2 : plan.mips.length * 2 + 1,
1892
+ targetBytes: (options.quality === "performance" ? pingPongBytes : plan.targetBytes + accumulatorBytes + pingPongBytes),
1893
+ compositeGain: options.intensity * resolveBloomPyramidResponseGain(plan),
1894
+ threshold: options.threshold,
1895
+ intensity: options.intensity,
1896
+ softKnee: options.softKnee,
1897
+ shoulder: options.shoulder,
1898
+ halfFloat: hdr || plan.halfFloat,
1899
+ };
1900
+ }
1901
+ /**
1902
+ * PART U: every live GPU allocation with bytes + lane owner — registry
1903
+ * render targets (B1 shadow maps arrive labeled, B4 captures arrive as
1904
+ * `-mirror-target`/`-scene-color-target`) plus the raw-handle residents the
1905
+ * registry set cannot see: the A1 bloom pyramid (two targets per mip plus
1906
+ * two level-0 accumulators), the legacy bloom ping-pong pair, the A1/A3
1907
+ * lookup textures, and the motion-blur/taa history textures. The
1908
+ * depth-readback scratch pair is intentionally absent: it is allocated and
1909
+ * deleted inside one synchronous call, so it is never live at a diagnostics
1910
+ * boundary. A5 volumetric light, C4 decals, and the G1 SDF atlas own no GPU
1911
+ * target (forward uniforms + CPU kernel, CPU geometry, CPU atlas data), so
1912
+ * no entry is emitted for them; a target appearing under their owner would
1913
+ * be the signal that the lane grew one.
1914
+ */
1915
+ describeGpuTargetInventory(liveRenderTargets) {
1916
+ const entries = liveRenderTargets.map((target) => ({
1917
+ label: target.label,
1918
+ kind: "render-target",
1919
+ bytes: target.colorTexture.byteLength + (target.depthTexture?.byteLength ?? 0),
1920
+ owner: resolveGpuTargetOwner(target.label)
1921
+ }));
1922
+ const pyramid = this.bloomPyramidResources;
1923
+ if (pyramid) {
1924
+ const bytesPerPixel = pyramid.hdr ? 8 : 4;
1925
+ pyramid.plan.mips.forEach((mip, index) => {
1926
+ for (const side of ["a", "b"]) {
1927
+ entries.push({
1928
+ label: `bloom-pyramid-mip${index}${side}`,
1929
+ kind: "pyramid-mip",
1930
+ bytes: mip.width * mip.height * bytesPerPixel,
1931
+ owner: "a1-bloom"
1932
+ });
1933
+ }
1934
+ });
1935
+ const level0 = pyramid.plan.mips[0];
1936
+ if (level0) {
1937
+ for (const side of ["a", "b"]) {
1938
+ entries.push({
1939
+ label: `bloom-pyramid-accumulator-${side}`,
1940
+ kind: "pyramid-accumulator",
1941
+ bytes: level0.width * level0.height * bytesPerPixel,
1942
+ owner: "a1-bloom"
1943
+ });
1944
+ }
1945
+ }
1946
+ }
1947
+ const pingPong = this.bloomPingPongResources;
1948
+ if (pingPong) {
1949
+ const bytesPerPixel = pingPong.hdr ? 8 : 4;
1950
+ for (const side of ["a", "b"]) {
1951
+ entries.push({
1952
+ label: `bloom-ping-pong-${side}`,
1953
+ kind: "ping-pong",
1954
+ bytes: pingPong.width * pingPong.height * bytesPerPixel,
1955
+ owner: "a1-bloom"
1956
+ });
1957
+ }
1958
+ }
1959
+ if (this.bloomBrightLutTexture) {
1960
+ entries.push({
1961
+ label: "bloom-bright-lut",
1962
+ kind: "lut",
1963
+ bytes: BLOOM_BRIGHT_LUT_WIDTH * BLOOM_BRIGHT_LUT_HEIGHT * 4,
1964
+ owner: "a1-bloom"
1965
+ });
1966
+ }
1967
+ if (this.bloomCompositeLutTexture) {
1968
+ entries.push({
1969
+ label: "bloom-composite-lut",
1970
+ kind: "lut",
1971
+ bytes: BLOOM_COMPOSITE_LUT_SIZE * BLOOM_COMPOSITE_LUT_SIZE * 4,
1972
+ owner: "a1-bloom"
1973
+ });
1974
+ }
1975
+ if (this.outlineBlendLutTexture) {
1976
+ entries.push({ label: "outline-blend-lut", kind: "lut", bytes: OUTLINE_BLEND_LUT_WIDTH * 4, owner: "post" });
1977
+ }
1978
+ if (this.motionBlurVelocityTexture && this.motionBlurVelocitySize) {
1979
+ entries.push({
1980
+ label: "motion-blur-velocity",
1981
+ kind: "history",
1982
+ bytes: this.motionBlurVelocitySize.width * this.motionBlurVelocitySize.height * 8,
1983
+ owner: "post"
1984
+ });
1985
+ }
1986
+ if (this.taaHistoryTexture && this.taaHistorySize) {
1987
+ entries.push({
1988
+ label: "taa-history",
1989
+ kind: "history",
1990
+ bytes: this.taaHistorySize.width * this.taaHistorySize.height * 4,
1991
+ owner: "post"
1992
+ });
1993
+ }
1994
+ return entries;
1995
+ }
1996
+ disposeBloomPyramidResources() {
1997
+ if (!this.bloomPyramidResources)
1998
+ return;
1999
+ for (const texture of this.bloomPyramidResources.textures)
2000
+ this.gl.deleteTexture(texture);
2001
+ for (const framebuffer of this.bloomPyramidResources.framebuffers)
2002
+ this.gl.deleteFramebuffer(framebuffer);
2003
+ this.gl.deleteTexture(this.bloomPyramidResources.accumulatorTextureA);
2004
+ this.gl.deleteFramebuffer(this.bloomPyramidResources.accumulatorFramebufferA);
2005
+ this.gl.deleteTexture(this.bloomPyramidResources.accumulatorTextureB);
2006
+ this.gl.deleteFramebuffer(this.bloomPyramidResources.accumulatorFramebufferB);
2007
+ this.bloomPyramidResources = null;
2008
+ }
2009
+ /**
2010
+ * Mip-chain targets for the balanced/cinematic bloom pyramid. Each level is a
2011
+ * single texture+framebuffer at the planned size with linear filtering so the
2012
+ * downsample and accumulate passes can sample across levels. Half-float backs
2013
+ * the chain when the source is HDR or the cinematic preset requests it.
2014
+ */
2015
+ ensureBloomPyramidResources(plan, hdr) {
2016
+ const current = this.bloomPyramidResources;
2017
+ if (current && current.plan.quality === plan.quality && current.hdr === (hdr || plan.halfFloat) &&
2018
+ current.plan.mips.length === plan.mips.length &&
2019
+ current.plan.mips.every((mip, index) => mip.width === plan.mips[index].width && mip.height === plan.mips[index].height)) {
2020
+ return current;
2021
+ }
2022
+ this.disposeBloomPyramidResources();
2023
+ const textureUnit0 = this.captureTextureUnit0();
2024
+ const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
2025
+ const textures = [];
2026
+ const framebuffers = [];
2027
+ const useHalfFloat = hdr || plan.halfFloat;
2028
+ const allocateTarget = (width, height, label) => {
2029
+ const texture = this.gl.createTexture();
2030
+ const framebuffer = this.gl.createFramebuffer();
2031
+ if (!texture || !framebuffer) {
2032
+ if (texture)
2033
+ this.gl.deleteTexture(texture);
2034
+ if (framebuffer)
2035
+ this.gl.deleteFramebuffer(framebuffer);
2036
+ throw new RenderDeviceError(`Failed to allocate WebGL2 bloom pyramid ${label}`, "WEBGL_ALLOCATION_FAILED", { width, height });
2037
+ }
2038
+ textures.push(texture);
2039
+ framebuffers.push(framebuffer);
2040
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
2041
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
2042
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
2043
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
2044
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
2045
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, useHalfFloat ? this.gl.RGBA16F : this.gl.RGBA8, width, height, 0, this.gl.RGBA, useHalfFloat ? this.gl.HALF_FLOAT : this.gl.UNSIGNED_BYTE, null);
2046
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
2047
+ this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
2048
+ const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
2049
+ if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
2050
+ throw new RenderDeviceError("WebGL2 bloom pyramid framebuffer status is invalid", "FRAMEBUFFER_INVALID", { width, height, status });
2051
+ }
2052
+ return { texture, framebuffer };
2053
+ };
2054
+ try {
2055
+ // Two ping-pong targets per mip (separable blur needs distinct read/write
2056
+ // targets at the same size), plus two level-0-sized accumulators: the
2057
+ // weighted sum ping-pongs between them so no pass ever samples the
2058
+ // texture of its own bound framebuffer (feedback loop).
2059
+ for (const mip of plan.mips) {
2060
+ allocateTarget(mip.width, mip.height, "mip");
2061
+ allocateTarget(mip.width, mip.height, "mip");
2062
+ }
2063
+ const level0 = plan.mips[0];
2064
+ const accumulatorA = allocateTarget(level0.width, level0.height, "accumulator-a");
2065
+ const accumulatorB = allocateTarget(level0.width, level0.height, "accumulator-b");
2066
+ const finalResources = {
2067
+ plan,
2068
+ hdr: useHalfFloat,
2069
+ textures,
2070
+ framebuffers,
2071
+ accumulatorTextureA: accumulatorA.texture,
2072
+ accumulatorFramebufferA: accumulatorA.framebuffer,
2073
+ accumulatorTextureB: accumulatorB.texture,
2074
+ accumulatorFramebufferB: accumulatorB.framebuffer
2075
+ };
2076
+ this.bloomPyramidResources = finalResources;
2077
+ return finalResources;
2078
+ }
2079
+ catch (error) {
2080
+ for (const texture of textures)
2081
+ this.gl.deleteTexture(texture);
2082
+ for (const framebuffer of framebuffers)
2083
+ this.gl.deleteFramebuffer(framebuffer);
2084
+ throw error;
2085
+ }
2086
+ finally {
2087
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
2088
+ this.restoreTextureUnit0(textureUnit0);
2089
+ this.stateCache.invalidate();
2090
+ }
2091
+ }
2092
+ captureTextureUnit0() {
2093
+ const activeTexture = this.gl.getParameter(this.gl.ACTIVE_TEXTURE);
2094
+ this.gl.activeTexture(this.gl.TEXTURE0);
2095
+ return {
2096
+ activeTexture,
2097
+ texture2d: this.gl.getParameter(this.gl.TEXTURE_BINDING_2D),
2098
+ sampler: this.gl.getParameter(this.gl.SAMPLER_BINDING)
2099
+ };
2100
+ }
2101
+ restoreTextureUnit0(snapshot) {
2102
+ this.gl.activeTexture(this.gl.TEXTURE0);
2103
+ this.gl.bindTexture(this.gl.TEXTURE_2D, snapshot.texture2d);
2104
+ this.gl.bindSampler(0, snapshot.sampler);
2105
+ this.gl.activeTexture(snapshot.activeTexture);
2106
+ }
2107
+ captureTextureUnitBinding(unit) {
2108
+ this.gl.activeTexture(this.gl.TEXTURE0 + unit);
2109
+ return {
2110
+ texture2d: this.gl.getParameter(this.gl.TEXTURE_BINDING_2D),
2111
+ sampler: this.gl.getParameter(this.gl.SAMPLER_BINDING)
2112
+ };
2113
+ }
2114
+ restoreTextureUnitBinding(unit, snapshot) {
2115
+ this.gl.activeTexture(this.gl.TEXTURE0 + unit);
2116
+ this.gl.bindTexture(this.gl.TEXTURE_2D, snapshot.texture2d);
2117
+ this.gl.bindSampler(unit, snapshot.sampler);
2118
+ }
2119
+ prepareFullscreenPresentation(framebuffer, outputWidth, outputHeight) {
2120
+ const textureUnit0 = this.captureTextureUnit0();
2121
+ const textureUnit1 = this.captureTextureUnitBinding(1);
2122
+ const textureUnit2 = this.captureTextureUnitBinding(2);
2123
+ this.gl.activeTexture(textureUnit0.activeTexture);
2124
+ const snapshot = {
2125
+ framebuffer: this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING),
2126
+ program: this.gl.getParameter(this.gl.CURRENT_PROGRAM),
2127
+ textureUnit0,
2128
+ textureUnit1,
2129
+ textureUnit2,
2130
+ vertexArray: this.gl.getParameter(this.gl.VERTEX_ARRAY_BINDING),
2131
+ viewport: this.gl.getParameter(this.gl.VIEWPORT),
2132
+ colorMask: this.gl.getParameter(this.gl.COLOR_WRITEMASK),
2133
+ depthTestEnabled: this.gl.isEnabled(this.gl.DEPTH_TEST),
2134
+ cullFaceEnabled: this.gl.isEnabled(this.gl.CULL_FACE),
2135
+ blendEnabled: this.gl.isEnabled(this.gl.BLEND),
2136
+ scissorTestEnabled: this.gl.isEnabled(this.gl.SCISSOR_TEST),
2137
+ stencilTestEnabled: this.gl.isEnabled(this.gl.STENCIL_TEST),
2138
+ polygonOffsetFillEnabled: this.gl.isEnabled(this.gl.POLYGON_OFFSET_FILL),
2139
+ depthMask: this.gl.getParameter(this.gl.DEPTH_WRITEMASK)
2140
+ };
2141
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
2142
+ this.gl.viewport(0, 0, outputWidth, outputHeight);
2143
+ this.gl.colorMask(true, true, true, true);
2144
+ this.gl.disable(this.gl.SCISSOR_TEST);
2145
+ this.gl.disable(this.gl.STENCIL_TEST);
2146
+ this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
2147
+ this.gl.disable(this.gl.DEPTH_TEST);
2148
+ this.gl.disable(this.gl.CULL_FACE);
2149
+ this.gl.disable(this.gl.BLEND);
2150
+ this.gl.depthMask(false);
2151
+ return snapshot;
2152
+ }
2153
+ restoreFullscreenPresentationState(snapshot, fallbackWidth, fallbackHeight) {
2154
+ this.gl.bindVertexArray(snapshot.vertexArray);
2155
+ this.restoreTextureUnitBinding(2, snapshot.textureUnit2);
2156
+ this.restoreTextureUnitBinding(1, snapshot.textureUnit1);
2157
+ this.restoreTextureUnit0(snapshot.textureUnit0);
2158
+ this.gl.useProgram(snapshot.program);
2159
+ if (snapshot.depthTestEnabled)
2160
+ this.gl.enable(this.gl.DEPTH_TEST);
2161
+ else
2162
+ this.gl.disable(this.gl.DEPTH_TEST);
2163
+ if (snapshot.cullFaceEnabled)
2164
+ this.gl.enable(this.gl.CULL_FACE);
2165
+ else
2166
+ this.gl.disable(this.gl.CULL_FACE);
2167
+ if (snapshot.blendEnabled)
2168
+ this.gl.enable(this.gl.BLEND);
2169
+ else
2170
+ this.gl.disable(this.gl.BLEND);
2171
+ if (snapshot.scissorTestEnabled)
2172
+ this.gl.enable(this.gl.SCISSOR_TEST);
2173
+ else
2174
+ this.gl.disable(this.gl.SCISSOR_TEST);
2175
+ if (snapshot.stencilTestEnabled)
2176
+ this.gl.enable(this.gl.STENCIL_TEST);
2177
+ else
2178
+ this.gl.disable(this.gl.STENCIL_TEST);
2179
+ if (snapshot.polygonOffsetFillEnabled)
2180
+ this.gl.enable(this.gl.POLYGON_OFFSET_FILL);
2181
+ else
2182
+ this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
2183
+ this.gl.colorMask(snapshot.colorMask[0] ?? true, snapshot.colorMask[1] ?? true, snapshot.colorMask[2] ?? true, snapshot.colorMask[3] ?? true);
2184
+ this.gl.depthMask(snapshot.depthMask);
2185
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, snapshot.framebuffer);
2186
+ this.gl.viewport(snapshot.viewport[0] ?? 0, snapshot.viewport[1] ?? 0, snapshot.viewport[2] ?? fallbackWidth, snapshot.viewport[3] ?? fallbackHeight);
2187
+ this.stateCache.invalidate();
2188
+ }
2189
+ ensurePresentationVertexArray() {
2190
+ if (this.presentationVertexArray)
2191
+ return this.presentationVertexArray;
2192
+ const vertexArray = this.gl.createVertexArray();
2193
+ if (!vertexArray) {
2194
+ throw new RenderDeviceError("Failed to allocate WebGL presentation vertex array", "WEBGL_ALLOCATION_FAILED");
2195
+ }
2196
+ this.presentationVertexArray = vertexArray;
2197
+ return vertexArray;
2198
+ }
2199
+ ensureDepthReadbackProgram() {
2200
+ if (this.depthReadbackProgram) {
2201
+ return this.depthReadbackProgram;
2202
+ }
2203
+ const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
2204
+ precision highp float;
2205
+ out vec2 v_uv;
2206
+ void main() {
2207
+ vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
2208
+ v_uv = position;
2209
+ gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
2210
+ }
2211
+ `, "webgl2-depth-readback");
2212
+ const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
2213
+ precision highp float;
2214
+ uniform sampler2D u_depth;
2215
+ uniform vec4 u_sourceRect;
2216
+ in vec2 v_uv;
2217
+ out vec4 outColor;
2218
+ vec3 packDepth24(float value) {
2219
+ value = clamp(value, 0.0, 1.0);
2220
+ vec3 encoded = fract(value * vec3(1.0, 255.0, 65025.0));
2221
+ encoded -= encoded.yzz * vec3(1.0 / 255.0, 1.0 / 255.0, 0.0);
2222
+ return encoded;
2223
+ }
2224
+ void main() {
2225
+ vec2 uv = u_sourceRect.xy + v_uv * u_sourceRect.zw;
2226
+ outColor = vec4(packDepth24(texture(u_depth, uv).r), 1.0);
2227
+ }
2228
+ `, "webgl2-depth-readback");
2229
+ const program = this.gl.createProgram();
2230
+ if (!program) {
2231
+ this.gl.deleteShader(vertex);
2232
+ this.gl.deleteShader(fragment);
2233
+ throw new RenderDeviceError("Failed to allocate WebGL2 depth readback shader", "WEBGL_ALLOCATION_FAILED");
2234
+ }
2235
+ this.gl.attachShader(program, vertex);
2236
+ this.gl.attachShader(program, fragment);
2237
+ this.gl.linkProgram(program);
2238
+ this.gl.deleteShader(vertex);
2239
+ this.gl.deleteShader(fragment);
2240
+ if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
2241
+ const log = this.gl.getProgramInfoLog(program) ?? "Unknown depth readback shader link error";
2242
+ this.gl.deleteProgram(program);
2243
+ throw new RenderDeviceError("WebGL2 depth readback shader link failed", "SHADER_LINK_FAILED", { log });
2244
+ }
2245
+ this.depthReadbackProgram = program;
2246
+ return program;
2247
+ }
2248
+ ensurePresentationProgram() {
2249
+ if (this.presentationProgram) {
2250
+ return this.presentationProgram;
2251
+ }
2252
+ const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
2253
+ precision highp float;
2254
+ out vec2 v_uv;
2255
+ void main() {
2256
+ vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
2257
+ v_uv = position;
2258
+ gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
2259
+ }
2260
+ `, "webgl2-present-render-target");
2261
+ const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
2262
+ precision highp float;
2263
+ uniform sampler2D u_source;
2264
+ in vec2 v_uv;
2265
+ out vec4 outColor;
2266
+ void main() {
2267
+ outColor = texture(u_source, v_uv);
2268
+ }
2269
+ `, "webgl2-present-render-target");
2270
+ const program = this.gl.createProgram();
2271
+ if (!program) {
2272
+ this.gl.deleteShader(vertex);
2273
+ this.gl.deleteShader(fragment);
2274
+ throw new RenderDeviceError("Failed to allocate WebGL presentation shader", "WEBGL_ALLOCATION_FAILED");
2275
+ }
2276
+ this.gl.attachShader(program, vertex);
2277
+ this.gl.attachShader(program, fragment);
2278
+ this.gl.linkProgram(program);
2279
+ this.gl.deleteShader(vertex);
2280
+ this.gl.deleteShader(fragment);
2281
+ if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
2282
+ const log = this.gl.getProgramInfoLog(program) ?? "Unknown presentation shader link error";
2283
+ this.gl.deleteProgram(program);
2284
+ throw new RenderDeviceError("WebGL presentation shader link failed", "SHADER_LINK_FAILED", { log });
2285
+ }
2286
+ this.presentationProgram = program;
2287
+ return program;
2288
+ }
2289
+ ensureBloomBrightExtractProgram() {
2290
+ if (this.bloomBrightExtractProgram) {
2291
+ return this.bloomBrightExtractProgram;
2292
+ }
2293
+ this.bloomBrightExtractProgram = this.createFullscreenProgram(`#version 300 es
2294
+ precision highp float;
2295
+ precision highp int;
2296
+ uniform sampler2D u_source;
2297
+ uniform sampler2D u_brightLut;
2298
+ uniform int u_hdr;
2299
+ uniform float u_threshold;
2300
+ uniform float u_softKnee;
2301
+ out vec4 outColor;
2302
+
2303
+ uvec4 byteTexel(sampler2D source, ivec2 coordinate) {
2304
+ return uvec4(texelFetch(source, coordinate, 0) * 255.0 + 0.5);
2305
+ }
2306
+
2307
+ void main() {
2308
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2309
+ if (u_hdr == 1) {
2310
+ vec4 source = texelFetch(u_source, pixel, 0);
2311
+ float luma = dot(source.rgb, vec3(0.2126, 0.7152, 0.0722));
2312
+ float kneeLow = max(u_threshold - u_softKnee, 0.0);
2313
+ float kneeHigh = min(u_threshold + u_softKnee, 1.0);
2314
+ float weight = u_softKnee <= 0.0
2315
+ ? (luma >= u_threshold ? 1.0 : 0.0)
2316
+ : smoothstep(kneeLow, kneeHigh, luma);
2317
+ outColor = source * weight;
2318
+ return;
2319
+ }
2320
+ uvec4 source = byteTexel(u_source, pixel);
2321
+ uint colorIndex = (source.r << 16u) | (source.g << 8u) | source.b;
2322
+ uint byteIndex = colorIndex >> 3u;
2323
+ uint texelIndex = byteIndex >> 2u;
2324
+ ivec2 lutCoordinate = ivec2(
2325
+ int(texelIndex % ${BLOOM_BRIGHT_LUT_WIDTH}u),
2326
+ int(texelIndex / ${BLOOM_BRIGHT_LUT_WIDTH}u)
2327
+ );
2328
+ uvec4 packed = byteTexel(u_brightLut, lutCoordinate);
2329
+ uint component = byteIndex & 3u;
2330
+ uint packedByte = component == 0u
2331
+ ? packed.r
2332
+ : component == 1u
2333
+ ? packed.g
2334
+ : component == 2u
2335
+ ? packed.b
2336
+ : packed.a;
2337
+ bool bright = ((packedByte >> (colorIndex & 7u)) & 1u) == 1u;
2338
+ outColor = bright ? vec4(source) / 255.0 : vec4(0.0);
2339
+ }
2340
+ `, "webgl2-bloom-bright-extract");
2341
+ return this.bloomBrightExtractProgram;
2342
+ }
2343
+ ensureBloomBlurProgram() {
2344
+ if (this.bloomBlurProgram) {
2345
+ return this.bloomBlurProgram;
2346
+ }
2347
+ this.bloomBlurProgram = this.createFullscreenProgram(`#version 300 es
2348
+ precision highp float;
2349
+ precision highp int;
2350
+ uniform sampler2D u_source;
2351
+ uniform ivec2 u_size;
2352
+ uniform int u_radius;
2353
+ uniform int u_horizontal;
2354
+ uniform int u_hdr;
2355
+ out vec4 outColor;
2356
+
2357
+ vec4 sourcePixel(ivec2 coordinate) {
2358
+ return texelFetch(u_source, clamp(coordinate, ivec2(0), u_size - ivec2(1)), 0);
2359
+ }
2360
+
2361
+ void main() {
2362
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2363
+ float sigma = max(float(u_radius) / 3.0, 0.5);
2364
+ float coefficient = 0.39894 / sigma;
2365
+ vec4 sum = sourcePixel(pixel) * coefficient;
2366
+ float weightSum = coefficient;
2367
+ for (int offset = 1; offset <= 16; offset += 1) {
2368
+ if (offset > u_radius) continue;
2369
+ float distance = float(offset);
2370
+ float weight = coefficient * exp(-0.5 * distance * distance / (sigma * sigma));
2371
+ ivec2 delta = u_horizontal == 1 ? ivec2(offset, 0) : ivec2(0, offset);
2372
+ sum += (sourcePixel(pixel + delta) + sourcePixel(pixel - delta)) * weight;
2373
+ weightSum += 2.0 * weight;
2374
+ }
2375
+ outColor = sum / max(weightSum, 0.000001);
2376
+ }
2377
+ `, "webgl2-bloom-blur");
2378
+ return this.bloomBlurProgram;
2379
+ }
2380
+ ensureBloomCompositeProgram() {
2381
+ if (this.bloomCompositeProgram) {
2382
+ return this.bloomCompositeProgram;
2383
+ }
2384
+ this.bloomCompositeProgram = this.createFullscreenProgram(`#version 300 es
2385
+ precision highp float;
2386
+ precision highp int;
2387
+ uniform sampler2D u_source;
2388
+ uniform sampler2D u_blurred;
2389
+ uniform sampler2D u_compositeLut;
2390
+ uniform int u_hdr;
2391
+ uniform float u_intensity;
2392
+ out vec4 outColor;
2393
+
2394
+ uvec4 byteTexel(sampler2D source, ivec2 coordinate) {
2395
+ return uvec4(texelFetch(source, coordinate, 0) * 255.0 + 0.5);
2396
+ }
2397
+
2398
+ float compositeChannel(uint source, uint blurred) {
2399
+ return texelFetch(u_compositeLut, ivec2(int(source), int(blurred)), 0).r;
2400
+ }
2401
+
2402
+ void main() {
2403
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2404
+ if (u_hdr == 1) {
2405
+ vec4 source = texelFetch(u_source, pixel, 0);
2406
+ vec3 blurred = texelFetch(u_blurred, pixel, 0).rgb;
2407
+ outColor = vec4(source.rgb + blurred * u_intensity, source.a);
2408
+ return;
2409
+ }
2410
+ uvec4 source = byteTexel(u_source, pixel);
2411
+ uvec4 blurred = byteTexel(u_blurred, pixel);
2412
+ outColor = vec4(
2413
+ compositeChannel(source.r, blurred.r),
2414
+ compositeChannel(source.g, blurred.g),
2415
+ compositeChannel(source.b, blurred.b),
2416
+ float(source.a) / 255.0
2417
+ );
2418
+ }
2419
+ `, "webgl2-bloom-composite");
2420
+ return this.bloomCompositeProgram;
2421
+ }
2422
+ ensureBloomDownsampleProgram() {
2423
+ if (this.bloomDownsampleProgram)
2424
+ return this.bloomDownsampleProgram;
2425
+ this.bloomDownsampleProgram = this.createFullscreenProgram(`#version 300 es
2426
+ precision highp float;
2427
+ uniform sampler2D u_source;
2428
+ uniform vec2 u_texelSize;
2429
+ uniform vec2 u_targetSize;
2430
+ out vec4 outColor;
2431
+ void main() {
2432
+ // gl_FragCoord is expressed in destination pixels. Normalize by the
2433
+ // destination size so every downsample covers the complete source image;
2434
+ // multiplying by source texel size cropped a half-size target to the
2435
+ // lower-left quarter of the preceding level.
2436
+ vec2 uv = gl_FragCoord.xy / u_targetSize;
2437
+ vec2 e = u_texelSize * 0.5;
2438
+ vec4 sum = texture(u_source, uv - e) + texture(u_source, uv + vec2(e.x, -e.y))
2439
+ + texture(u_source, uv + vec2(-e.x, e.y)) + texture(u_source, uv + e);
2440
+ outColor = sum * 0.25;
2441
+ }
2442
+ `, "webgl2-bloom-downsample");
2443
+ return this.bloomDownsampleProgram;
2444
+ }
2445
+ ensureBloomAccumulateProgram() {
2446
+ if (this.bloomAccumulateProgram)
2447
+ return this.bloomAccumulateProgram;
2448
+ this.bloomAccumulateProgram = this.createFullscreenProgram(`#version 300 es
2449
+ precision highp float;
2450
+ uniform sampler2D u_base;
2451
+ uniform sampler2D u_bloom;
2452
+ uniform float u_weight;
2453
+ out vec4 outColor;
2454
+ void main() {
2455
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2456
+ vec4 base = texelFetch(u_base, pixel, 0);
2457
+ vec2 uv = (vec2(pixel) + 0.5) / vec2(textureSize(u_base, 0));
2458
+ vec3 bloom = texture(u_bloom, uv).rgb;
2459
+ outColor = vec4(base.rgb + bloom * u_weight, base.a);
2460
+ }
2461
+ `, "webgl2-bloom-accumulate");
2462
+ return this.bloomAccumulateProgram;
2463
+ }
2464
+ ensureOutlineProgram() {
2465
+ if (this.outlineProgram)
2466
+ return this.outlineProgram;
2467
+ this.outlineProgram = this.createFullscreenProgram(`#version 300 es
2468
+ precision highp float;
2469
+ precision highp int;
2470
+ uniform sampler2D u_source;
2471
+ uniform sampler2D u_blendLut;
2472
+ uniform ivec2 u_size;
2473
+ uniform int u_width;
2474
+ uniform float u_boundHigh;
2475
+ uniform float u_boundLow;
2476
+ out vec4 outColor;
2477
+
2478
+ uvec4 sourceByte(ivec2 coordinate) {
2479
+ ivec2 bounded = clamp(coordinate, ivec2(0), u_size - ivec2(1));
2480
+ return uvec4(texelFetch(u_source, bounded, 0) * 255.0 + 0.5);
2481
+ }
2482
+
2483
+ uint lumaNumerator(ivec2 coordinate) {
2484
+ uvec3 source = sourceByte(coordinate).rgb;
2485
+ return 2126u * source.r + 7152u * source.g + 722u * source.b;
2486
+ }
2487
+
2488
+ uvec2 squareWords(int signedValue) {
2489
+ uint value = uint(abs(signedValue));
2490
+ uint high = value >> 12u;
2491
+ uint low = value & 4095u;
2492
+ uint middle = 2u * high * low;
2493
+ uint lowWord = low * low + (middle & 4095u) * 4096u;
2494
+ uint highWord = high * high + (middle >> 12u);
2495
+ highWord += lowWord / ${OUTLINE_LIMB_RADIX}u;
2496
+ lowWord %= ${OUTLINE_LIMB_RADIX}u;
2497
+ return uvec2(highWord, lowWord);
2498
+ }
2499
+
2500
+ bool edgeAt(ivec2 pixel) {
2501
+ int nw = int(lumaNumerator(pixel + ivec2(-1, -1)));
2502
+ int n = int(lumaNumerator(pixel + ivec2(0, -1)));
2503
+ int ne = int(lumaNumerator(pixel + ivec2(1, -1)));
2504
+ int w = int(lumaNumerator(pixel + ivec2(-1, 0)));
2505
+ int e = int(lumaNumerator(pixel + ivec2(1, 0)));
2506
+ int sw = int(lumaNumerator(pixel + ivec2(-1, 1)));
2507
+ int s = int(lumaNumerator(pixel + ivec2(0, 1)));
2508
+ int se = int(lumaNumerator(pixel + ivec2(1, 1)));
2509
+ int gx = -nw - 2 * w - sw + ne + 2 * e + se;
2510
+ int gy = -nw - 2 * n - ne + sw + 2 * s + se;
2511
+ uvec2 x = squareWords(gx);
2512
+ uvec2 y = squareWords(gy);
2513
+ uint low = x.y + y.y;
2514
+ uint high = x.x + y.x;
2515
+ if (low >= ${OUTLINE_LIMB_RADIX}u) {
2516
+ low -= ${OUTLINE_LIMB_RADIX}u;
2517
+ high += 1u;
2518
+ }
2519
+ uint boundHigh = uint(u_boundHigh);
2520
+ uint boundLow = uint(u_boundLow);
2521
+ return high != boundHigh ? high > boundHigh : low >= boundLow;
2522
+ }
2523
+
2524
+ void main() {
2525
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2526
+ bool outlined = false;
2527
+ for (int offsetY = -6; offsetY <= 6 && !outlined; offsetY += 1) {
2528
+ for (int offsetX = -6; offsetX <= 6; offsetX += 1) {
2529
+ if (abs(offsetX) > u_width || abs(offsetY) > u_width) continue;
2530
+ if (offsetX * offsetX + offsetY * offsetY > u_width * u_width) continue;
2531
+ ivec2 candidate = clamp(pixel + ivec2(offsetX, offsetY), ivec2(0), u_size - ivec2(1));
2532
+ if (edgeAt(candidate)) {
2533
+ outlined = true;
2534
+ break;
2535
+ }
2536
+ }
2537
+ }
2538
+ uvec4 source = sourceByte(pixel);
2539
+ if (!outlined) {
2540
+ outColor = vec4(source) / 255.0;
2541
+ return;
2542
+ }
2543
+ outColor = vec4(
2544
+ texelFetch(u_blendLut, ivec2(int(source.r), 0), 0).r,
2545
+ texelFetch(u_blendLut, ivec2(int(source.g), 0), 0).g,
2546
+ texelFetch(u_blendLut, ivec2(int(source.b), 0), 0).b,
2547
+ float(source.a) / 255.0
2548
+ );
2549
+ }
2550
+ `, "webgl2-outline");
2551
+ return this.outlineProgram;
2552
+ }
2553
+ ensureSsaoProgram() {
2554
+ if (this.ssaoProgram)
2555
+ return this.ssaoProgram;
2556
+ this.ssaoProgram = this.createFullscreenProgram(`#version 300 es
2557
+ precision highp float;
2558
+ precision highp int;
2559
+ uniform sampler2D u_source;
2560
+ uniform sampler2D u_depth;
2561
+ uniform ivec2 u_size;
2562
+ uniform int u_radius;
2563
+ uniform float u_intensity;
2564
+ uniform float u_bias;
2565
+ out vec4 outColor;
2566
+
2567
+ float depthAt(ivec2 coordinate) {
2568
+ return texelFetch(u_depth, clamp(coordinate, ivec2(0), u_size - ivec2(1)), 0).r;
2569
+ }
2570
+
2571
+ void main() {
2572
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2573
+ float centerDepth = depthAt(pixel);
2574
+ float occlusion = 0.0;
2575
+ float samples = 0.0;
2576
+ for (int stepY = -1; stepY <= 1; stepY += 1) {
2577
+ for (int stepX = -1; stepX <= 1; stepX += 1) {
2578
+ if (stepX == 0 && stepY == 0) continue;
2579
+ float sampleDepth = depthAt(pixel + ivec2(stepX * u_radius, stepY * u_radius));
2580
+ occlusion += clamp(
2581
+ (centerDepth - sampleDepth - u_bias) / (0.14 + float(u_radius) * 0.04),
2582
+ 0.0,
2583
+ 1.0
2584
+ );
2585
+ samples += 1.0;
2586
+ }
2587
+ }
2588
+ float factor = max(0.18, 1.0 - (occlusion / max(1.0, samples)) * u_intensity * 0.72);
2589
+ vec4 source = texelFetch(u_source, pixel, 0);
2590
+ outColor = vec4(source.rgb * factor, source.a);
2591
+ }
2592
+ `, "webgl2-ssao");
2593
+ return this.ssaoProgram;
2594
+ }
2595
+ ensureSsrProgram() {
2596
+ if (this.ssrProgram)
2597
+ return this.ssrProgram;
2598
+ this.ssrProgram = this.createFullscreenProgram(`#version 300 es
2599
+ precision highp float;
2600
+ precision highp int;
2601
+ uniform sampler2D u_source;
2602
+ uniform sampler2D u_depth;
2603
+ uniform ivec2 u_size;
2604
+ uniform float u_intensity;
2605
+ uniform int u_maxDistance;
2606
+ uniform float u_depthNear;
2607
+ uniform float u_depthFar;
2608
+ out vec4 outColor;
2609
+
2610
+ // Raw GL depth is nonlinear (0.1/1000 defaults park the play area past 0.97),
2611
+ // so every depth-gated comparison below runs on linearized depth. The CPU
2612
+ // byte kernel keeps fixture-unit semantics; the divergence is documented at
2613
+ // normalizeLdrDepthRange.
2614
+ float a3dSsrLinearDepth(float depth) {
2615
+ float viewZ = u_depthNear * u_depthFar / max(u_depthFar - depth * (u_depthFar - u_depthNear), 0.0001);
2616
+ return clamp((viewZ - u_depthNear) / max(u_depthFar - u_depthNear, 0.0001), 0.0, 1.0);
2617
+ }
2618
+
2619
+ void main() {
2620
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2621
+ vec4 target = texelFetch(u_source, pixel, 0);
2622
+ float linearDepth = a3dSsrLinearDepth(texelFetch(u_depth, pixel, 0).r);
2623
+ int rayDistance = max(1, int(round((1.0 - linearDepth) * float(u_maxDistance))));
2624
+ int direction = pixel.x < u_size.x / 2 ? 1 : -1;
2625
+ ivec2 samplePixel = ivec2(
2626
+ clamp(pixel.x + direction * rayDistance, 0, u_size.x - 1),
2627
+ clamp(u_size.y - 1 - pixel.y, 0, u_size.y - 1)
2628
+ );
2629
+ vec3 reflected = texelFetch(u_source, samplePixel, 0).rgb;
2630
+ float sourceLuma = (reflected.r + reflected.g + reflected.b) / 3.0;
2631
+ if (sourceLuma < 0.18 || linearDepth > 0.995) {
2632
+ outColor = target;
2633
+ return;
2634
+ }
2635
+ float boost = sourceLuma * u_intensity * (1.0 - linearDepth);
2636
+ outColor = vec4(clamp(target.rgb + reflected * boost, 0.0, 1.0), target.a);
2637
+ }
2638
+ `, "webgl2-ssr");
2639
+ return this.ssrProgram;
2640
+ }
2641
+ ensureDepthOfFieldProgram() {
2642
+ if (this.depthOfFieldProgram)
2643
+ return this.depthOfFieldProgram;
2644
+ this.depthOfFieldProgram = this.createFullscreenProgram(`#version 300 es
2645
+ precision highp float;
2646
+ precision highp int;
2647
+ uniform sampler2D u_source;
2648
+ uniform sampler2D u_depth;
2649
+ uniform ivec2 u_size;
2650
+ uniform float u_focusDepth;
2651
+ uniform float u_focusRange;
2652
+ uniform int u_maxRadius;
2653
+ uniform float u_depthNear;
2654
+ uniform float u_depthFar;
2655
+ out vec4 outColor;
2656
+
2657
+ // Same linearization contract as the SSR program, except focus is authored
2658
+ // directly as a linear-distance fraction (0 = near plane, 1 = far plane):
2659
+ // buffer-unit focus values are unusable once depth is linearized, because the
2660
+ // projection curve parks the whole play area inside a sliver of buffer units.
2661
+ // The CPU byte kernel keeps buffer-unit semantics; the divergence is
2662
+ // documented at normalizeLdrDepthRange.
2663
+ float a3dDofLinearDepth(float depth) {
2664
+ float viewZ = u_depthNear * u_depthFar / max(u_depthFar - depth * (u_depthFar - u_depthNear), 0.0001);
2665
+ return clamp((viewZ - u_depthNear) / max(u_depthFar - u_depthNear, 0.0001), 0.0, 1.0);
2666
+ }
2667
+
2668
+ void main() {
2669
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2670
+ vec4 source = texelFetch(u_source, pixel, 0);
2671
+ float linearDepth = a3dDofLinearDepth(texelFetch(u_depth, pixel, 0).r);
2672
+ float normalizedBlur = max(0.0, abs(linearDepth - u_focusDepth) - u_focusRange)
2673
+ / max(u_focusRange, 0.001) * float(u_maxRadius);
2674
+ int radius = min(u_maxRadius, int(round(normalizedBlur)));
2675
+ if (radius == 0) {
2676
+ outColor = source;
2677
+ return;
2678
+ }
2679
+ vec3 sum = vec3(0.0);
2680
+ float samples = 0.0;
2681
+ for (int offsetY = -8; offsetY <= 8; offsetY += 1) {
2682
+ for (int offsetX = -8; offsetX <= 8; offsetX += 1) {
2683
+ if (abs(offsetX) > radius || abs(offsetY) > radius) continue;
2684
+ if (offsetX * offsetX + offsetY * offsetY > radius * radius) continue;
2685
+ ivec2 samplePixel = clamp(pixel + ivec2(offsetX, offsetY), ivec2(0), u_size - ivec2(1));
2686
+ sum += texelFetch(u_source, samplePixel, 0).rgb;
2687
+ samples += 1.0;
2688
+ }
2689
+ }
2690
+ outColor = vec4(sum / max(1.0, samples), source.a);
2691
+ }
2692
+ `, "webgl2-depth-of-field");
2693
+ return this.depthOfFieldProgram;
2694
+ }
2695
+ ensureMotionBlurProgram() {
2696
+ if (this.motionBlurProgram)
2697
+ return this.motionBlurProgram;
2698
+ this.motionBlurProgram = this.createFullscreenProgram(`#version 300 es
2699
+ precision highp float;
2700
+ precision highp int;
2701
+ uniform sampler2D u_source;
2702
+ uniform sampler2D u_velocity;
2703
+ uniform ivec2 u_size;
2704
+ uniform int u_samples;
2705
+ uniform float u_scale;
2706
+ out vec4 outColor;
2707
+
2708
+ void main() {
2709
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2710
+ vec4 source = texelFetch(u_source, pixel, 0);
2711
+ vec2 velocity = texelFetch(u_velocity, pixel, 0).rg * u_scale;
2712
+ if (length(velocity) < 0.01) {
2713
+ outColor = source;
2714
+ return;
2715
+ }
2716
+ vec3 sum = vec3(0.0);
2717
+ for (int sampleIndex = 0; sampleIndex < 16; sampleIndex += 1) {
2718
+ if (sampleIndex >= u_samples) continue;
2719
+ float t = float(sampleIndex) / float(u_samples - 1) - 0.5;
2720
+ ivec2 samplePixel = ivec2(floor(vec2(pixel) - velocity * t + vec2(0.5)));
2721
+ samplePixel = clamp(samplePixel, ivec2(0), u_size - ivec2(1));
2722
+ sum += texelFetch(u_source, samplePixel, 0).rgb;
2723
+ }
2724
+ outColor = vec4(sum / float(u_samples), source.a);
2725
+ }
2726
+ `, "webgl2-motion-blur");
2727
+ return this.motionBlurProgram;
2728
+ }
2729
+ ensureTaaProgram() {
2730
+ if (this.taaProgram)
2731
+ return this.taaProgram;
2732
+ this.taaProgram = this.createFullscreenProgram(`#version 300 es
2733
+ precision highp float;
2734
+ uniform sampler2D u_source;
2735
+ uniform sampler2D u_history;
2736
+ uniform float u_blend;
2737
+ out vec4 outColor;
2738
+
2739
+ void main() {
2740
+ ivec2 pixel = ivec2(gl_FragCoord.xy);
2741
+ vec4 source = texelFetch(u_source, pixel, 0);
2742
+ vec3 history = texelFetch(u_history, pixel, 0).rgb;
2743
+ outColor = vec4(mix(source.rgb, history, u_blend), source.a);
2744
+ }
2745
+ `, "webgl2-taa");
2746
+ return this.taaProgram;
2747
+ }
2748
+ createFullscreenProgram(fragmentSource, label) {
2749
+ const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
2750
+ precision highp float;
2751
+ void main() {
2752
+ vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
2753
+ gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
2754
+ }
2755
+ `, label);
2756
+ const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, fragmentSource, label);
2757
+ const program = this.gl.createProgram();
2758
+ if (!program) {
2759
+ this.gl.deleteShader(vertex);
2760
+ this.gl.deleteShader(fragment);
2761
+ throw new RenderDeviceError("Failed to allocate WebGL2 fullscreen shader program", "WEBGL_ALLOCATION_FAILED", { label });
2762
+ }
2763
+ this.gl.attachShader(program, vertex);
2764
+ this.gl.attachShader(program, fragment);
2765
+ this.gl.linkProgram(program);
2766
+ this.gl.deleteShader(vertex);
2767
+ this.gl.deleteShader(fragment);
2768
+ if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
2769
+ const log = this.gl.getProgramInfoLog(program) ?? "Unknown fullscreen shader link error";
2770
+ this.gl.deleteProgram(program);
2771
+ throw new RenderDeviceError("WebGL2 fullscreen shader link failed", "SHADER_LINK_FAILED", { label, log });
2772
+ }
2773
+ return program;
2774
+ }
2775
+ ensureLdrPostprocessProgram() {
2776
+ if (this.ldrPostprocessProgram) {
2777
+ return this.ldrPostprocessProgram;
2778
+ }
2779
+ const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
2780
+ precision highp float;
2781
+ out vec2 v_uv;
2782
+ void main() {
2783
+ vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
2784
+ v_uv = position;
2785
+ gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
2786
+ }
2787
+ `, "webgl2-ldr-postprocess");
2788
+ const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
2789
+ precision highp float;
2790
+ uniform sampler2D u_source;
2791
+ uniform vec2 u_texelSize;
2792
+ uniform int u_hasToneMapping;
2793
+ uniform int u_toneOperator;
2794
+ uniform int u_inputColorSpace;
2795
+ uniform int u_outputColorSpace;
2796
+ uniform float u_exposure;
2797
+ uniform float u_whitePoint;
2798
+ uniform float u_gamma;
2799
+ uniform int u_hasColorGrade;
2800
+ uniform float u_contrast;
2801
+ uniform float u_temperature;
2802
+ uniform float u_tint;
2803
+ uniform float u_saturation;
2804
+ uniform float u_vibrance;
2805
+ uniform float u_vignette;
2806
+ uniform float u_sharpening;
2807
+ uniform int u_hasFxaa;
2808
+ uniform float u_edgeThreshold;
2809
+ uniform float u_subpixelBlend;
2810
+ in vec2 v_uv;
2811
+ out vec4 outColor;
2812
+
2813
+ float srgbToLinear(float value) {
2814
+ return value <= 0.04045 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4);
2815
+ }
2816
+
2817
+ float linearToSrgb(float value) {
2818
+ return value <= 0.0031308 ? value * 12.92 : 1.055 * pow(value, 1.0 / 2.4) - 0.055;
2819
+ }
2820
+
2821
+ vec3 decodeColor(vec3 color) {
2822
+ return u_inputColorSpace == 1 ? vec3(srgbToLinear(color.r), srgbToLinear(color.g), srgbToLinear(color.b)) : color;
2823
+ }
2824
+
2825
+ vec3 encodeColor(vec3 color) {
2826
+ vec3 linear = clamp(color, 0.0, 1.0);
2827
+ return u_outputColorSpace == 1 ? vec3(linearToSrgb(linear.r), linearToSrgb(linear.g), linearToSrgb(linear.b)) : linear;
2828
+ }
2829
+
2830
+ vec3 acesRrtAndOdtFit(vec3 value) {
2831
+ vec3 numerator = value * (value + 0.0245786) - 0.000090537;
2832
+ vec3 denominator = value * (0.983729 * value + 0.4329510) + 0.238081;
2833
+ return numerator / denominator;
2834
+ }
2835
+
2836
+ vec3 acesFilmic(vec3 color) {
2837
+ // Match current Three.js ACESFilmicToneMapping. ACES is a coupled RGB
2838
+ // transform; a scalar approximation shifts hue and washes saturated colors.
2839
+ const mat3 acesInput = mat3(
2840
+ vec3(0.59719, 0.07600, 0.02840),
2841
+ vec3(0.35458, 0.90834, 0.13383),
2842
+ vec3(0.04823, 0.01566, 0.83777)
2843
+ );
2844
+ const mat3 acesOutput = mat3(
2845
+ vec3(1.60475, -0.10208, -0.00327),
2846
+ vec3(-0.53108, 1.10813, -0.07276),
2847
+ vec3(-0.07367, -0.00605, 1.07602)
2848
+ );
2849
+ vec3 acesColor = acesInput * (color / 0.6);
2850
+ acesColor = acesRrtAndOdtFit(acesColor);
2851
+ return clamp(acesOutput * acesColor, 0.0, 1.0);
2852
+ }
2853
+
2854
+ float filmic(float value) {
2855
+ float x = max(0.0, value);
2856
+ float toe = max(0.0, x - 0.004);
2857
+ float curve = (toe * (6.2 * toe + 0.5)) / (toe * (6.2 * toe + 1.7) + 0.06);
2858
+ return min(curve, x * 1.08);
2859
+ }
2860
+
2861
+ float uncharted2(float value) {
2862
+ float a = 0.15;
2863
+ float b = 0.5;
2864
+ float c = 0.1;
2865
+ float d = 0.2;
2866
+ float e = 0.02;
2867
+ float f = 0.3;
2868
+ float w = 11.2;
2869
+ float x = value * 2.0;
2870
+ float curve = ((x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f)) - e / f;
2871
+ float wx = w;
2872
+ float white = ((wx * (a * wx + c * b) + d * e) / (wx * (a * wx + b) + d * f)) - e / f;
2873
+ return clamp(curve / white, 0.0, 1.0);
2874
+ }
2875
+
2876
+ float agx(float value) {
2877
+ float x = max(0.0, value);
2878
+ float encoded = log2(1.0 + x) / log2(17.0);
2879
+ return clamp(encoded * encoded * (3.0 - 2.0 * encoded), 0.0, 1.0);
2880
+ }
2881
+
2882
+ float neutral(float value) {
2883
+ float x = max(0.0, value);
2884
+ return min(1.0, (x * (1.0 + x / 7.5)) / (1.0 + x));
2885
+ }
2886
+
2887
+ float toneMapChannel(float value) {
2888
+ float exposed = max(0.0, value * u_exposure) / max(0.0001, u_whitePoint);
2889
+ if (u_toneOperator == 0) return min(1.0, exposed);
2890
+ if (u_toneOperator == 1) return exposed / (1.0 + exposed);
2891
+ if (u_toneOperator == 2) return exposed;
2892
+ if (u_toneOperator == 3) return filmic(exposed);
2893
+ if (u_toneOperator == 4) return uncharted2(exposed);
2894
+ if (u_toneOperator == 5) return agx(exposed);
2895
+ return neutral(exposed);
2896
+ }
2897
+
2898
+ vec3 applyToneMapping(vec3 color) {
2899
+ if (u_hasToneMapping == 0) return color;
2900
+ vec3 decoded = decodeColor(color);
2901
+ vec3 mapped = u_toneOperator == 2
2902
+ ? acesFilmic(max(vec3(0.0), decoded * u_exposure) / max(0.0001, u_whitePoint))
2903
+ : vec3(toneMapChannel(decoded.r), toneMapChannel(decoded.g), toneMapChannel(decoded.b));
2904
+ return encodeColor(mapped);
2905
+ }
2906
+
2907
+ vec3 applyColorGrade(vec3 color, vec2 uv) {
2908
+ if (u_hasColorGrade == 0) return color;
2909
+ float contrastOffset = 0.5 - 0.5 * u_contrast;
2910
+ float redShift = u_temperature * 0.08 - u_tint * 0.02;
2911
+ float greenShift = u_tint * 0.06;
2912
+ float blueShift = -u_temperature * 0.08 - u_tint * 0.02;
2913
+ float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
2914
+ vec3 graded = color * u_contrast + vec3(contrastOffset + redShift, contrastOffset + greenShift, contrastOffset + blueShift);
2915
+ float distanceFromLuma = min(1.0, abs(graded.r - luma) + abs(graded.g - luma) + abs(graded.b - luma));
2916
+ float vibranceBoost = u_vibrance == 0.0 ? 0.0 : u_vibrance * (1.0 - distanceFromLuma);
2917
+ float saturation = u_saturation + vibranceBoost;
2918
+ graded = vec3(luma) + (graded - vec3(luma)) * saturation;
2919
+ vec2 centered = uv * 2.0 - 1.0;
2920
+ float vignette = 1.0 - u_vignette * clamp((length(centered) - 0.28) / 1.12, 0.0, 1.0);
2921
+ return clamp(graded * vignette, 0.0, 1.0);
2922
+ }
2923
+
2924
+ vec3 baseColorAt(vec2 uv) {
2925
+ vec2 clampedUv = clamp(uv, vec2(0.0), vec2(1.0));
2926
+ vec3 color = texture(u_source, clampedUv).rgb;
2927
+ return applyColorGrade(applyToneMapping(color), clampedUv);
2928
+ }
2929
+
2930
+ vec3 finalColorAt(vec2 uv) {
2931
+ vec3 center = baseColorAt(uv);
2932
+ if (u_hasColorGrade == 0 || u_sharpening <= 0.0) return center;
2933
+ vec3 blur = (
2934
+ baseColorAt(uv + vec2(-u_texelSize.x, 0.0)) +
2935
+ baseColorAt(uv + vec2(u_texelSize.x, 0.0)) +
2936
+ baseColorAt(uv + vec2(0.0, -u_texelSize.y)) +
2937
+ baseColorAt(uv + vec2(0.0, u_texelSize.y))
2938
+ ) * 0.25;
2939
+ return clamp(center + (center - blur) * u_sharpening, 0.0, 1.0);
2940
+ }
2941
+
2942
+ float luma(vec3 color) {
2943
+ return dot(color, vec3(0.2126, 0.7152, 0.0722));
2944
+ }
2945
+
2946
+ void main() {
2947
+ vec3 center = finalColorAt(v_uv);
2948
+ float alpha = texture(u_source, clamp(v_uv, vec2(0.0), vec2(1.0))).a;
2949
+ if (u_hasFxaa == 0) {
2950
+ outColor = vec4(center, alpha);
2951
+ return;
2952
+ }
2953
+ vec3 north = finalColorAt(v_uv + vec2(0.0, -u_texelSize.y));
2954
+ vec3 south = finalColorAt(v_uv + vec2(0.0, u_texelSize.y));
2955
+ vec3 west = finalColorAt(v_uv + vec2(-u_texelSize.x, 0.0));
2956
+ vec3 east = finalColorAt(v_uv + vec2(u_texelSize.x, 0.0));
2957
+ float centerLuma = luma(center);
2958
+ float minLuma = min(centerLuma, min(min(luma(north), luma(south)), min(luma(west), luma(east))));
2959
+ float maxLuma = max(centerLuma, max(max(luma(north), luma(south)), max(luma(west), luma(east))));
2960
+ if (maxLuma - minLuma < u_edgeThreshold) {
2961
+ outColor = vec4(center, alpha);
2962
+ return;
2963
+ }
2964
+ vec3 average = (north + south + west + east) * 0.25;
2965
+ outColor = vec4(mix(center, average, clamp(u_subpixelBlend, 0.0, 1.0)), alpha);
2966
+ }
2967
+ `, "webgl2-ldr-postprocess");
2968
+ const program = this.gl.createProgram();
2969
+ if (!program) {
2970
+ this.gl.deleteShader(vertex);
2971
+ this.gl.deleteShader(fragment);
2972
+ throw new RenderDeviceError("Failed to allocate WebGL2 LDR postprocess shader", "WEBGL_ALLOCATION_FAILED");
2973
+ }
2974
+ this.gl.attachShader(program, vertex);
2975
+ this.gl.attachShader(program, fragment);
2976
+ this.gl.linkProgram(program);
2977
+ this.gl.deleteShader(vertex);
2978
+ this.gl.deleteShader(fragment);
2979
+ if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
2980
+ const log = this.gl.getProgramInfoLog(program) ?? "Unknown LDR postprocess shader link error";
2981
+ this.gl.deleteProgram(program);
2982
+ throw new RenderDeviceError("WebGL2 LDR postprocess shader link failed", "SHADER_LINK_FAILED", { log });
2983
+ }
2984
+ this.ldrPostprocessProgram = program;
2985
+ return program;
2986
+ }
2987
+ compileShader(type, source, label) {
2988
+ const shader = this.gl.createShader(type);
2989
+ if (!shader) {
2990
+ throw new RenderDeviceError("Failed to allocate WebGL shader", "WEBGL_ALLOCATION_FAILED", { label });
2991
+ }
2992
+ this.gl.shaderSource(shader, source);
2993
+ this.gl.compileShader(shader);
2994
+ if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
2995
+ const log = this.gl.getShaderInfoLog(shader) ?? "Unknown shader compile error";
2996
+ this.gl.deleteShader(shader);
2997
+ throw new RenderDeviceError("WebGL shader compile failed", "SHADER_COMPILE_FAILED", { label, log });
2998
+ }
2999
+ return shader;
3000
+ }
3001
+ reflectProgram(program) {
3002
+ const attributes = new Map();
3003
+ const uniforms = new Set();
3004
+ const attributeDetails = new Map();
3005
+ const uniformDetails = new Map();
3006
+ const activeAttributes = this.gl.getProgramParameter(program, this.gl.ACTIVE_ATTRIBUTES);
3007
+ const activeUniforms = this.gl.getProgramParameter(program, this.gl.ACTIVE_UNIFORMS);
3008
+ for (let i = 0; i < activeAttributes; i += 1) {
3009
+ const info = this.gl.getActiveAttrib(program, i);
3010
+ if (info) {
3011
+ const location = this.gl.getAttribLocation(program, info.name);
3012
+ attributes.set(info.name, location);
3013
+ attributeDetails.set(info.name, { name: info.name, type: String(info.type), location, source: "vertex", line: 0 });
3014
+ }
3015
+ }
3016
+ for (let i = 0; i < activeUniforms; i += 1) {
3017
+ const info = this.gl.getActiveUniform(program, i);
3018
+ if (info) {
3019
+ const name = info.name.replace(/\[0\]$/, "");
3020
+ uniforms.add(name);
3021
+ uniformDetails.set(name, { name, type: String(info.type), arraySize: info.size > 1 ? info.size : null, source: "fragment", line: 0 });
3022
+ }
3023
+ }
3024
+ return { attributes, uniforms, attributeDetails, uniformDetails };
3025
+ }
3026
+ uploadUniforms(shader, uniforms) {
3027
+ let textureUnit = 0;
3028
+ for (const [name, value] of uniforms) {
3029
+ if (!shader.reflection.uniforms.has(name)) {
3030
+ continue;
3031
+ }
3032
+ const location = this.getUniformLocation(shader, name);
3033
+ if (location === null) {
3034
+ throw new RenderDeviceError("Material tried to bind a missing shader uniform", "MISSING_UNIFORM", { name });
3035
+ }
3036
+ if (isTextureBinding(value)) {
3037
+ this.uploadTextureUniform(location, value, textureUnit);
3038
+ textureUnit += 1;
3039
+ }
3040
+ else if (typeof value === "number") {
3041
+ this.gl.uniform1f(location, value);
3042
+ }
3043
+ else if (Array.isArray(value) || ArrayBuffer.isView(value)) {
3044
+ const length = value.length;
3045
+ const floatData = value;
3046
+ if (length === 16 || (length > 16 && length % 16 === 0 && /(?:Matrix|Matrices)$/.test(name))) {
3047
+ this.gl.uniformMatrix4fv(location, false, floatData);
3048
+ }
3049
+ else if (length > 16 && length % 4 === 0) {
3050
+ this.gl.uniform4fv(location, floatData);
3051
+ }
3052
+ else if (length === 4) {
3053
+ this.gl.uniform4fv(location, floatData);
3054
+ }
3055
+ else if (length === 3) {
3056
+ this.gl.uniform3fv(location, floatData);
3057
+ }
3058
+ else if (length === 2) {
3059
+ this.gl.uniform2fv(location, floatData);
3060
+ }
3061
+ else {
3062
+ throw new RenderDeviceError("Unsupported uniform array length", "UNSUPPORTED_UNIFORM", { name, length });
3063
+ }
3064
+ }
3065
+ else {
3066
+ throw new RenderDeviceError("Unsupported uniform value", "UNSUPPORTED_UNIFORM", { name, valueType: typeof value });
3067
+ }
3068
+ if (this.errorCheckMode === "strict") {
3069
+ const uniformError = this.readError();
3070
+ if (uniformError) {
3071
+ throw new RenderDeviceError(`WebGL2 uniform upload failed for ${name}: ${uniformError}`, "WEBGL_DRAW_FAILED", {
3072
+ name,
3073
+ stage: "uniform",
3074
+ error: uniformError
3075
+ });
3076
+ }
3077
+ }
3078
+ }
3079
+ }
3080
+ getUniformLocation(shader, name) {
3081
+ let cache = this.uniformLocationCache.get(shader);
3082
+ if (!cache) {
3083
+ cache = new Map();
3084
+ this.uniformLocationCache.set(shader, cache);
3085
+ }
3086
+ if (!cache.has(name)) {
3087
+ this.uniformLocationLookupCount += 1;
3088
+ cache.set(name, this.gl.getUniformLocation(shader.handle, name) ?? this.gl.getUniformLocation(shader.handle, `${name}[0]`));
3089
+ }
3090
+ return cache.get(name) ?? null;
3091
+ }
3092
+ uploadTextureUniform(location, binding, textureUnit) {
3093
+ const validation = binding.validate();
3094
+ if (!validation.ok) {
3095
+ throw new RenderDeviceError("Texture binding validation failed", "INVALID_TEXTURE_BINDING", {
3096
+ diagnostics: validation.diagnostics,
3097
+ name: binding.name
3098
+ });
3099
+ }
3100
+ this.activateTextureUnit(textureUnit);
3101
+ const dimension = binding.texture?.dimension ?? (binding.name.toLowerCase().includes("cubemap") || binding.name.toLowerCase().includes("cube") ? "cube" : "2d");
3102
+ const target = dimension === "cube" ? this.gl.TEXTURE_CUBE_MAP : this.gl.TEXTURE_2D;
3103
+ const handle = binding.texture ? this.getTextureHandle(binding.texture) : dimension === "cube" ? this.getFallbackCubeTextureHandle() : this.getFallbackTextureHandle();
3104
+ if (binding.texture) {
3105
+ const lowerName = binding.name.toLowerCase();
3106
+ if (lowerName.includes("environment"))
3107
+ this.nativeEnvironmentBindings += 1;
3108
+ if (lowerName.includes("shadow"))
3109
+ this.nativeShadowMapBindings += 1;
3110
+ }
3111
+ this.bindTextureForUnit(textureUnit, target, handle);
3112
+ const samplerHandle = this.getSamplerHandle(binding.sampler, target);
3113
+ this.stateCache.bindSampler(textureUnit, samplerHandle, () => this.gl.bindSampler(textureUnit, samplerHandle));
3114
+ this.gl.uniform1i(location, textureUnit);
3115
+ }
3116
+ activateTextureUnit(textureUnit) {
3117
+ if (this.activeTextureUnitIndex === textureUnit)
3118
+ return;
3119
+ this.stateCache.activeTexture(textureUnit, () => this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit));
3120
+ this.activeTextureUnitIndex = textureUnit;
3121
+ }
3122
+ bindTextureForUnit(textureUnit, target, handle) {
3123
+ const key = `${textureUnit}:${target}`;
3124
+ if (this.textureUnitBindings.get(key) === handle)
3125
+ return;
3126
+ this.stateCache.bindTexture(target, handle, () => this.gl.bindTexture(target, handle));
3127
+ this.textureUnitBindings.set(key, handle);
3128
+ this.textureBindCount += 1;
3129
+ }
3130
+ setTextureParameterIfNeeded(handle, target, parameter, value) {
3131
+ let parameters = this.textureSamplerParameterCache.get(handle);
3132
+ if (!parameters) {
3133
+ parameters = new Map();
3134
+ this.textureSamplerParameterCache.set(handle, parameters);
3135
+ }
3136
+ if (parameters.get(parameter) === value)
3137
+ return;
3138
+ this.gl.texParameteri(target, parameter, value);
3139
+ parameters.set(parameter, value);
3140
+ this.samplerParameterUploadCount += 1;
3141
+ }
3142
+ applySamplerAnisotropy(maxAnisotropy, target = this.gl.TEXTURE_2D) {
3143
+ if (maxAnisotropy <= 1)
3144
+ return;
3145
+ if (!this.anisotropicFilteringExtension)
3146
+ return;
3147
+ this.gl.texParameterf(target, this.anisotropicFilteringExtension.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(Math.max(1, maxAnisotropy), this.maxTextureAnisotropy));
3148
+ this.samplerAnisotropyUploadCount += 1;
3149
+ }
3150
+ getSamplerHandle(sampler, target) {
3151
+ const key = this.samplerKey(sampler, target);
3152
+ const cached = this.samplerObjectCache.get(key);
3153
+ if (cached)
3154
+ return cached;
3155
+ const handle = this.gl.createSampler();
3156
+ if (!handle) {
3157
+ throw new RenderDeviceError("Failed to allocate WebGL sampler", "WEBGL_ALLOCATION_FAILED");
3158
+ }
3159
+ this.gl.samplerParameteri(handle, this.gl.TEXTURE_MIN_FILTER, this.minFilter(sampler.minFilter));
3160
+ this.gl.samplerParameteri(handle, this.gl.TEXTURE_MAG_FILTER, this.magFilter(sampler.magFilter));
3161
+ this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_S, this.addressMode(sampler.addressU));
3162
+ this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_T, this.addressMode(sampler.addressV));
3163
+ this.samplerParameterUploadCount += 4;
3164
+ if (target === this.gl.TEXTURE_CUBE_MAP) {
3165
+ this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_R, this.addressMode(sampler.addressV));
3166
+ this.samplerParameterUploadCount += 1;
3167
+ }
3168
+ if (sampler.maxAnisotropy > 1 && this.anisotropicFilteringExtension) {
3169
+ this.gl.samplerParameterf(handle, this.anisotropicFilteringExtension.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(Math.max(1, sampler.maxAnisotropy), this.maxTextureAnisotropy));
3170
+ this.samplerAnisotropyUploadCount += 1;
3171
+ }
3172
+ this.samplerObjectCache.set(key, handle);
3173
+ return handle;
3174
+ }
3175
+ samplerKey(sampler, target) {
3176
+ return [
3177
+ sampler.minFilter,
3178
+ sampler.magFilter,
3179
+ sampler.addressU,
3180
+ sampler.addressV,
3181
+ sampler.maxAnisotropy,
3182
+ target === this.gl.TEXTURE_CUBE_MAP ? "cube" : "2d"
3183
+ ].join("|");
3184
+ }
3185
+ getFallbackTextureHandle() {
3186
+ if (this.fallbackTexture) {
3187
+ return this.fallbackTexture;
3188
+ }
3189
+ const handle = this.gl.createTexture();
3190
+ if (!handle) {
3191
+ throw new RenderDeviceError("Failed to allocate WebGL fallback texture", "WEBGL_ALLOCATION_FAILED");
3192
+ }
3193
+ this.stateCache.invalidate();
3194
+ this.gl.bindTexture(this.gl.TEXTURE_2D, handle);
3195
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
3196
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
3197
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
3198
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
3199
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
3200
+ this.fallbackTexture = handle;
3201
+ return handle;
3202
+ }
3203
+ getFallbackCubeTextureHandle() {
3204
+ if (this.fallbackCubeTexture) {
3205
+ return this.fallbackCubeTexture;
3206
+ }
3207
+ const handle = this.gl.createTexture();
3208
+ if (!handle) {
3209
+ throw new RenderDeviceError("Failed to allocate WebGL fallback cube texture", "WEBGL_ALLOCATION_FAILED");
3210
+ }
3211
+ this.stateCache.invalidate();
3212
+ this.gl.bindTexture(this.gl.TEXTURE_CUBE_MAP, handle);
3213
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
3214
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
3215
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
3216
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
3217
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_R, this.gl.CLAMP_TO_EDGE);
3218
+ const pixel = new Uint8Array([255, 255, 255, 255]);
3219
+ for (const face of WEBGL_CUBE_FACES) {
3220
+ this.gl.texImage2D(this.cubeFaceTarget(face), 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixel);
3221
+ }
3222
+ this.fallbackCubeTexture = handle;
3223
+ return handle;
3224
+ }
3225
+ getTextureHandle(texture) {
3226
+ this.releaseDisposedTextureHandles();
3227
+ if (texture.disposed) {
3228
+ throw new RenderDeviceError("Texture is disposed", "DISPOSED_RESOURCE", { label: texture.label });
3229
+ }
3230
+ const cached = this.textures.get(texture);
3231
+ if (cached) {
3232
+ return cached;
3233
+ }
3234
+ const handle = this.gl.createTexture();
3235
+ if (!handle) {
3236
+ throw new RenderDeviceError("Failed to allocate WebGL texture", "WEBGL_ALLOCATION_FAILED", { label: texture.label });
3237
+ }
3238
+ this.stateCache.invalidate();
3239
+ this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false);
3240
+ this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
3241
+ this.gl.pixelStorei(this.gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, this.gl.NONE);
3242
+ if (texture.dimension === "cube") {
3243
+ this.uploadCubeTexture(texture, handle);
3244
+ this.textureUploadModes.set(texture, "cube");
3245
+ this.textures.set(texture, handle);
3246
+ return handle;
3247
+ }
3248
+ this.gl.bindTexture(this.gl.TEXTURE_2D, handle);
3249
+ if (isCompressedTextureFormat(texture.format)) {
3250
+ const compressed = this.resolveCompressedTextureFormat(texture.format);
3251
+ if (compressed) {
3252
+ const uploadLevels = completeUploadLevels(texture.textureLevels);
3253
+ for (const [levelIndex, level] of uploadLevels.entries()) {
3254
+ this.gl.compressedTexImage2D(this.gl.TEXTURE_2D, levelIndex, compressed.internalFormat, level.width, level.height, 0, level.data);
3255
+ }
3256
+ const compressedUploadError = this.readError();
3257
+ if (!compressedUploadError) {
3258
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
3259
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, uploadLevels.length - 1);
3260
+ this.textureUploadModes.set(texture, "compressed");
3261
+ this.textures.set(texture, handle);
3262
+ return handle;
3263
+ }
3264
+ }
3265
+ const fallbackLevels = completeUploadLevels(texture.fallbackTextureLevels);
3266
+ if (fallbackLevels.length === 0) {
3267
+ this.gl.deleteTexture(handle);
3268
+ throw new RenderDeviceError("Compressed texture format is not supported and no RGBA8 fallback data was provided", "COMPRESSED_TEXTURE_UNSUPPORTED", {
3269
+ label: texture.label,
3270
+ format: texture.format
3271
+ });
3272
+ }
3273
+ this.uploadRgba8FallbackTexture(texture, fallbackLevels);
3274
+ this.textureUploadModes.set(texture, "fallback");
3275
+ this.textures.set(texture, handle);
3276
+ return handle;
3277
+ }
3278
+ if (texture.format === "depth24") {
3279
+ throw new RenderDeviceError("Depth textures cannot be uploaded to WebGL2 color samplers", "UNSUPPORTED_TEXTURE_FORMAT", {
3280
+ label: texture.label,
3281
+ format: texture.format
3282
+ });
3283
+ }
3284
+ if (texture.source) {
3285
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.rgba8TextureInternalFormat(texture), this.gl.RGBA, this.gl.UNSIGNED_BYTE, texture.source);
3286
+ this.gl.generateMipmap(this.gl.TEXTURE_2D);
3287
+ }
3288
+ else if (texture.mipLevels.length > 0) {
3289
+ const uploadFormat = this.textureUploadFormat(texture);
3290
+ const uploadLevels = completeUploadLevels(texture.textureLevels);
3291
+ for (const [levelIndex, level] of uploadLevels.entries()) {
3292
+ this.gl.texImage2D(this.gl.TEXTURE_2D, levelIndex, uploadFormat.internalFormat, level.width, level.height, 0, uploadFormat.format, uploadFormat.type, texturePixelUploadData(level.data, texture.format));
3293
+ }
3294
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
3295
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, uploadLevels.length - 1);
3296
+ }
3297
+ else {
3298
+ const uploadFormat = this.textureUploadFormat(texture);
3299
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, uploadFormat.internalFormat, texture.width, texture.height, 0, uploadFormat.format, uploadFormat.type, texture.data ? texturePixelUploadData(texture.data, texture.format) : null);
3300
+ // Float colour formats are not guaranteed mipmap-filterable in WebGL2:
3301
+ // RGBA32F/RGBA16F are only linear-filterable with OES_texture_float_linear, and
3302
+ // generateMipmap on a non-filterable format raises INVALID_OPERATION. Data
3303
+ // textures such as joint palettes are point-sampled with texelFetch and never
3304
+ // need mips, so skip generation and pin the level range instead.
3305
+ if (isFloatColorTextureFormat(texture.format)) {
3306
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
3307
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, 0);
3308
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
3309
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
3310
+ }
3311
+ else {
3312
+ this.gl.generateMipmap(this.gl.TEXTURE_2D);
3313
+ }
3314
+ }
3315
+ this.textureUploadModes.set(texture, texture.format);
3316
+ this.textures.set(texture, handle);
3317
+ return handle;
3318
+ }
3319
+ uploadCubeTexture(texture, handle) {
3320
+ this.gl.bindTexture(this.gl.TEXTURE_CUBE_MAP, handle);
3321
+ const uploadFormat = this.textureUploadFormat(texture);
3322
+ for (const face of texture.cubeFaces) {
3323
+ const uploadLevels = completeUploadLevels(face.mipLevels);
3324
+ for (const [levelIndex, level] of uploadLevels.entries()) {
3325
+ this.gl.texImage2D(this.cubeFaceTarget(face.face), levelIndex, uploadFormat.internalFormat, level.width, level.height, 0, uploadFormat.format, uploadFormat.type, texturePixelUploadData(level.data, texture.format));
3326
+ }
3327
+ }
3328
+ const firstFaceLevels = completeUploadLevels(texture.cubeFaces[0]?.mipLevels ?? []);
3329
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_BASE_LEVEL, 0);
3330
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MAX_LEVEL, Math.max(0, firstFaceLevels.length - 1));
3331
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
3332
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
3333
+ this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_R, this.gl.CLAMP_TO_EDGE);
3334
+ }
3335
+ uploadRgba8FallbackTexture(texture, fallbackLevels) {
3336
+ const internalFormat = this.rgba8TextureInternalFormat(texture);
3337
+ for (const [levelIndex, level] of fallbackLevels.entries()) {
3338
+ this.gl.texImage2D(this.gl.TEXTURE_2D, levelIndex, internalFormat, level.width, level.height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, texturePixelUploadData(level.data, "rgba8"));
3339
+ }
3340
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
3341
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, fallbackLevels.length - 1);
3342
+ }
3343
+ cubeFaceTarget(face) {
3344
+ switch (face) {
3345
+ case "px": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_X;
3346
+ case "nx": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_X;
3347
+ case "py": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_Y;
3348
+ case "ny": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_Y;
3349
+ case "pz": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_Z;
3350
+ case "nz": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_Z;
3351
+ }
3352
+ }
3353
+ rgba8TextureInternalFormat(texture) {
3354
+ return texture.colorSpace === "srgb" ? this.gl.SRGB8_ALPHA8 : this.gl.RGBA;
3355
+ }
3356
+ textureUploadFormat(texture) {
3357
+ if (texture.format === "rgba8") {
3358
+ return { internalFormat: this.rgba8TextureInternalFormat(texture), format: this.gl.RGBA, type: this.gl.UNSIGNED_BYTE };
3359
+ }
3360
+ if (texture.colorSpace === "srgb") {
3361
+ throw new RenderDeviceError("Floating-point texture uploads must use linear colorSpace", "UNSUPPORTED_TEXTURE_FORMAT", {
3362
+ label: texture.label,
3363
+ format: texture.format,
3364
+ colorSpace: texture.colorSpace
3365
+ });
3366
+ }
3367
+ if (texture.format === "rgba16f") {
3368
+ return { internalFormat: this.gl.RGBA16F, format: this.gl.RGBA, type: this.gl.HALF_FLOAT };
3369
+ }
3370
+ if (texture.format === "rgba32f") {
3371
+ return { internalFormat: this.gl.RGBA32F, format: this.gl.RGBA, type: this.gl.FLOAT };
3372
+ }
3373
+ throw new RenderDeviceError("Unsupported texture upload format", "UNSUPPORTED_TEXTURE_FORMAT", {
3374
+ label: texture.label,
3375
+ format: texture.format
3376
+ });
3377
+ }
3378
+ resolveRenderTargetFormat(format) {
3379
+ if (format === "rgba8") {
3380
+ return { internalFormat: this.gl.RGBA, type: this.gl.UNSIGNED_BYTE };
3381
+ }
3382
+ if (!this.gl.getExtension("EXT_color_buffer_float")) {
3383
+ throw new RenderDeviceError("Floating-point color render targets require EXT_color_buffer_float", "HDR_RENDER_TARGET_UNSUPPORTED", { format });
3384
+ }
3385
+ if (format === "rgba16f") {
3386
+ return { internalFormat: this.gl.RGBA16F, type: this.gl.HALF_FLOAT };
3387
+ }
3388
+ return { internalFormat: this.gl.RGBA32F, type: this.gl.FLOAT };
3389
+ }
3390
+ releaseDisposedTextureHandles() {
3391
+ for (const [texture, handle] of [...this.textures]) {
3392
+ if (texture.disposed) {
3393
+ this.gl.deleteTexture(handle);
3394
+ this.textures.delete(texture);
3395
+ this.textureUploadModes.delete(texture);
3396
+ this.releasedTextureHandles += 1;
3397
+ }
3398
+ }
3399
+ }
3400
+ resolveCompressedTextureFormat(format) {
3401
+ switch (format) {
3402
+ case "bc1-rgba-unorm": {
3403
+ const extension = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
3404
+ return extension ? { internalFormat: extension.COMPRESSED_RGBA_S3TC_DXT1_EXT } : null;
3405
+ }
3406
+ case "bc3-rgba-unorm": {
3407
+ const extension = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
3408
+ return extension ? { internalFormat: extension.COMPRESSED_RGBA_S3TC_DXT5_EXT } : null;
3409
+ }
3410
+ case "etc2-rgba8unorm":
3411
+ return { internalFormat: 0x9278 };
3412
+ case "astc-4x4-rgba-unorm": {
3413
+ const extension = this.gl.getExtension("WEBGL_compressed_texture_astc");
3414
+ return extension ? { internalFormat: extension.COMPRESSED_RGBA_ASTC_4x4_KHR } : null;
3415
+ }
3416
+ }
3417
+ }
3418
+ magFilter(filter) {
3419
+ return filter === "nearest" ? this.gl.NEAREST : this.gl.LINEAR;
3420
+ }
3421
+ minFilter(filter) {
3422
+ switch (filter) {
3423
+ case "nearest":
3424
+ return this.gl.NEAREST;
3425
+ case "linear":
3426
+ return this.gl.LINEAR;
3427
+ case "nearest-mipmap-nearest":
3428
+ return this.gl.NEAREST_MIPMAP_NEAREST;
3429
+ case "linear-mipmap-nearest":
3430
+ return this.gl.LINEAR_MIPMAP_NEAREST;
3431
+ case "nearest-mipmap-linear":
3432
+ return this.gl.NEAREST_MIPMAP_LINEAR;
3433
+ case "linear-mipmap-linear":
3434
+ return this.gl.LINEAR_MIPMAP_LINEAR;
3435
+ }
3436
+ }
3437
+ addressMode(mode) {
3438
+ if (mode === "repeat")
3439
+ return this.gl.REPEAT;
3440
+ if (mode === "mirror-repeat")
3441
+ return this.gl.MIRRORED_REPEAT;
3442
+ return this.gl.CLAMP_TO_EDGE;
3443
+ }
3444
+ bindVertexFormat(shader, format) {
3445
+ const boundLocations = new Set();
3446
+ for (const attribute of format.attributes) {
3447
+ const location = this.resolveAttributeLocation(shader, attribute);
3448
+ if (location < 0) {
3449
+ continue;
3450
+ }
3451
+ if (attribute.type !== "float32") {
3452
+ throw new RenderDeviceError("Unsupported vertex attribute type", "UNSUPPORTED_VERTEX_ATTRIBUTE", {
3453
+ attribute: attribute.shaderName,
3454
+ type: attribute.type
3455
+ });
3456
+ }
3457
+ boundLocations.add(location);
3458
+ this.gl.enableVertexAttribArray(location);
3459
+ this.gl.vertexAttribDivisor(location, 0);
3460
+ this.gl.vertexAttribPointer(location, attribute.components, this.gl.FLOAT, attribute.normalized, format.stride, attribute.offset);
3461
+ }
3462
+ return boundLocations;
3463
+ }
3464
+ bindVertexArrayForCommand(command, shader, vertexBuffer) {
3465
+ const indexBuffer = command.indexBuffer ? this.requireBuffer(command.indexBuffer) : undefined;
3466
+ const key = this.vertexArrayCacheKey(command, shader, vertexBuffer, indexBuffer);
3467
+ const cached = this.vertexArrayCache.get(key);
3468
+ if (cached) {
3469
+ this.stateCache.bindVertexArray(cached.handle, () => this.gl.bindVertexArray(cached.handle));
3470
+ return cached;
3471
+ }
3472
+ const handle = this.gl.createVertexArray();
3473
+ if (!handle) {
3474
+ throw new RenderDeviceError("Failed to allocate WebGL vertex array", "WEBGL_ALLOCATION_FAILED");
3475
+ }
3476
+ this.vertexArrayCreateCount += 1;
3477
+ this.stateCache.bindVertexArray(handle, () => this.gl.bindVertexArray(handle));
3478
+ this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle));
3479
+ const boundLocations = this.bindVertexFormat(shader, command.vertexFormat);
3480
+ if (command.instanceAttributes && command.instanceAttributes.length > 0) {
3481
+ this.bindInstanceAttributes(shader, command.instanceAttributes, boundLocations);
3482
+ }
3483
+ if (indexBuffer) {
3484
+ this.stateCache.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle, () => this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle));
3485
+ }
3486
+ this.disableUnboundVertexAttributes(boundLocations);
3487
+ this.applyDefaultAttributes(shader, boundLocations);
3488
+ const entry = { key, handle, boundLocations };
3489
+ this.vertexArrayCache.set(key, entry);
3490
+ return entry;
3491
+ }
3492
+ bindNoVertexArray() {
3493
+ this.stateCache.bindVertexArray(null, () => this.gl.bindVertexArray(null));
3494
+ }
3495
+ vertexArrayCacheKey(command, shader, vertexBuffer, indexBuffer) {
3496
+ const instanceKey = (command.instanceAttributes ?? []).map((attribute) => {
3497
+ const buffer = this.requireBuffer(attribute.buffer);
3498
+ return `${attribute.shaderName}:${buffer.id}:${attribute.components}:${attribute.offset}:${attribute.stride}:${attribute.normalized === true ? 1 : 0}:${attribute.divisor ?? 1}`;
3499
+ }).join(",");
3500
+ return [
3501
+ shader.id,
3502
+ vertexBuffer.id,
3503
+ this.vertexFormatId(command.vertexFormat),
3504
+ indexBuffer?.id ?? 0,
3505
+ instanceKey
3506
+ ].join("|");
3507
+ }
3508
+ vertexFormatId(format) {
3509
+ const existing = this.vertexFormatIds.get(format);
3510
+ if (existing !== undefined)
3511
+ return existing;
3512
+ const next = this.nextVertexFormatId;
3513
+ this.nextVertexFormatId += 1;
3514
+ this.vertexFormatIds.set(format, next);
3515
+ return next;
3516
+ }
3517
+ bindInstanceAttributes(shader, attributes, boundLocations) {
3518
+ for (const attribute of attributes) {
3519
+ const location = shader.reflection.attributes.get(attribute.shaderName);
3520
+ if (location === undefined || location < 0) {
3521
+ continue;
3522
+ }
3523
+ if (![1, 2, 3, 4].includes(attribute.components)) {
3524
+ throw new RenderDeviceError("Instance vertex attribute components must be 1, 2, 3, or 4", "INVALID_DRAW_COMMAND", {
3525
+ attribute: attribute.shaderName,
3526
+ components: attribute.components
3527
+ });
3528
+ }
3529
+ if (!Number.isInteger(attribute.offset) || attribute.offset < 0 || attribute.offset % 4 !== 0) {
3530
+ throw new RenderDeviceError("Instance vertex attribute offset must be non-negative and 4-byte aligned", "INVALID_DRAW_COMMAND", {
3531
+ attribute: attribute.shaderName,
3532
+ offset: attribute.offset
3533
+ });
3534
+ }
3535
+ if (!Number.isInteger(attribute.stride) || attribute.stride <= 0 || attribute.stride % 4 !== 0) {
3536
+ throw new RenderDeviceError("Instance vertex attribute stride must be positive and 4-byte aligned", "INVALID_DRAW_COMMAND", {
3537
+ attribute: attribute.shaderName,
3538
+ stride: attribute.stride
3539
+ });
3540
+ }
3541
+ if (attribute.offset + attribute.components * 4 > attribute.stride) {
3542
+ throw new RenderDeviceError("Instance vertex attribute range must fit inside its stride", "INVALID_DRAW_COMMAND", {
3543
+ attribute: attribute.shaderName,
3544
+ offset: attribute.offset,
3545
+ components: attribute.components,
3546
+ stride: attribute.stride
3547
+ });
3548
+ }
3549
+ const buffer = this.requireBuffer(attribute.buffer);
3550
+ boundLocations.add(location);
3551
+ this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, buffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer.handle));
3552
+ this.gl.enableVertexAttribArray(location);
3553
+ this.gl.vertexAttribPointer(location, attribute.components, this.gl.FLOAT, attribute.normalized ?? false, attribute.stride, attribute.offset);
3554
+ this.gl.vertexAttribDivisor(location, attribute.divisor ?? 1);
3555
+ }
3556
+ }
3557
+ resolveAttributeLocation(shader, attribute) {
3558
+ const reflected = shader.reflection.attributes.get(attribute.shaderName);
3559
+ if (reflected !== undefined) {
3560
+ return reflected;
3561
+ }
3562
+ return shader.reflection.attributes.get(attribute.semantic) ?? -1;
3563
+ }
3564
+ applyDefaultAttributes(shader, boundLocations) {
3565
+ const colorLocation = shader.reflection.attributes.get("a_color") ?? shader.reflection.attributes.get("color");
3566
+ if (colorLocation !== undefined && colorLocation >= 0 && !boundLocations.has(colorLocation)) {
3567
+ this.gl.disableVertexAttribArray(colorLocation);
3568
+ this.gl.vertexAttribDivisor(colorLocation, 0);
3569
+ this.gl.vertexAttrib4f(colorLocation, 1, 1, 1, 1);
3570
+ }
3571
+ const tangentLocation = shader.reflection.attributes.get("a_tangent") ?? shader.reflection.attributes.get("tangent");
3572
+ if (tangentLocation !== undefined && tangentLocation >= 0 && !boundLocations.has(tangentLocation)) {
3573
+ this.gl.disableVertexAttribArray(tangentLocation);
3574
+ this.gl.vertexAttribDivisor(tangentLocation, 0);
3575
+ this.gl.vertexAttrib4f(tangentLocation, 1, 0, 0, 1);
3576
+ }
3577
+ const uv1Location = shader.reflection.attributes.get("a_uv1") ?? shader.reflection.attributes.get("uv1");
3578
+ if (uv1Location !== undefined && uv1Location >= 0 && !boundLocations.has(uv1Location)) {
3579
+ this.gl.disableVertexAttribArray(uv1Location);
3580
+ this.gl.vertexAttribDivisor(uv1Location, 0);
3581
+ this.gl.vertexAttrib2f(uv1Location, 0, 0);
3582
+ }
3583
+ const instanceColorLocation = shader.reflection.attributes.get("a_instanceColor") ?? shader.reflection.attributes.get("instanceColor");
3584
+ if (instanceColorLocation !== undefined && instanceColorLocation >= 0 && !boundLocations.has(instanceColorLocation)) {
3585
+ this.gl.disableVertexAttribArray(instanceColorLocation);
3586
+ this.gl.vertexAttribDivisor(instanceColorLocation, 0);
3587
+ this.gl.vertexAttrib4f(instanceColorLocation, 1, 1, 1, 1);
3588
+ }
3589
+ }
3590
+ disableUnboundVertexAttributes(boundLocations) {
3591
+ for (let location = 0; location < this.maxVertexAttributes; location += 1) {
3592
+ if (!boundLocations.has(location)) {
3593
+ this.gl.disableVertexAttribArray(location);
3594
+ this.gl.vertexAttribDivisor(location, 0);
3595
+ }
3596
+ }
3597
+ }
3598
+ primitive(topology) {
3599
+ if (topology === "lines")
3600
+ return this.gl.LINES;
3601
+ if (topology === "points")
3602
+ return this.gl.POINTS;
3603
+ return this.gl.TRIANGLES;
3604
+ }
3605
+ applyRenderState(state) {
3606
+ const renderState = state ?? {
3607
+ depthTest: true,
3608
+ depthWrite: true,
3609
+ cullMode: "back",
3610
+ blend: false,
3611
+ depthCompare: "less-equal",
3612
+ colorWrite: [true, true, true, true],
3613
+ scissor: null,
3614
+ polygonOffset: null,
3615
+ stencil: null
3616
+ };
3617
+ this.stateCache.setEnabled(this.gl.DEPTH_TEST, renderState.depthTest, () => {
3618
+ if (renderState.depthTest)
3619
+ this.gl.enable(this.gl.DEPTH_TEST);
3620
+ else
3621
+ this.gl.disable(this.gl.DEPTH_TEST);
3622
+ });
3623
+ this.stateCache.depthMask(renderState.depthWrite, () => this.gl.depthMask(renderState.depthWrite));
3624
+ this.stateCache.depthFunc(renderState.depthCompare === "always" ? this.gl.ALWAYS : this.gl.LEQUAL, () => this.gl.depthFunc(renderState.depthCompare === "always" ? this.gl.ALWAYS : this.gl.LEQUAL));
3625
+ const colorWrite = renderState.colorWrite ?? [true, true, true, true];
3626
+ this.stateCache.colorMask(colorWrite[0], colorWrite[1], colorWrite[2], colorWrite[3], () => this.gl.colorMask(colorWrite[0], colorWrite[1], colorWrite[2], colorWrite[3]));
3627
+ if (renderState.scissor) {
3628
+ this.stateCache.setEnabled(this.gl.SCISSOR_TEST, true, () => this.gl.enable(this.gl.SCISSOR_TEST));
3629
+ this.stateCache.scissor(renderState.scissor.x, renderState.scissor.y, renderState.scissor.width, renderState.scissor.height, () => {
3630
+ this.gl.scissor(renderState.scissor.x, renderState.scissor.y, renderState.scissor.width, renderState.scissor.height);
3631
+ });
3632
+ }
3633
+ else {
3634
+ this.stateCache.setEnabled(this.gl.SCISSOR_TEST, false, () => this.gl.disable(this.gl.SCISSOR_TEST));
3635
+ }
3636
+ if (renderState.polygonOffset) {
3637
+ this.stateCache.setEnabled(this.gl.POLYGON_OFFSET_FILL, true, () => this.gl.enable(this.gl.POLYGON_OFFSET_FILL));
3638
+ this.stateCache.polygonOffset(renderState.polygonOffset.factor, renderState.polygonOffset.units, () => this.gl.polygonOffset(renderState.polygonOffset.factor, renderState.polygonOffset.units));
3639
+ }
3640
+ else {
3641
+ this.stateCache.setEnabled(this.gl.POLYGON_OFFSET_FILL, false, () => this.gl.disable(this.gl.POLYGON_OFFSET_FILL));
3642
+ }
3643
+ if (renderState.stencil) {
3644
+ const stencil = renderState.stencil;
3645
+ const compare = this.stencilCompare(stencil.compare ?? "always");
3646
+ const reference = stencil.reference ?? 0;
3647
+ const readMask = stencil.readMask ?? 0xff;
3648
+ const writeMask = stencil.writeMask ?? 0xff;
3649
+ const fail = this.stencilOperation(stencil.fail ?? "keep");
3650
+ const depthFail = this.stencilOperation(stencil.depthFail ?? "keep");
3651
+ const depthPass = this.stencilOperation(stencil.depthPass ?? "keep");
3652
+ this.stateCache.setEnabled(this.gl.STENCIL_TEST, true, () => this.gl.enable(this.gl.STENCIL_TEST));
3653
+ this.stateCache.stencilFunc(compare, reference, readMask, () => this.gl.stencilFunc(compare, reference, readMask));
3654
+ this.stateCache.stencilMask(writeMask, () => this.gl.stencilMask(writeMask));
3655
+ this.stateCache.stencilOp(fail, depthFail, depthPass, () => this.gl.stencilOp(fail, depthFail, depthPass));
3656
+ }
3657
+ else {
3658
+ this.stateCache.setEnabled(this.gl.STENCIL_TEST, false, () => this.gl.disable(this.gl.STENCIL_TEST));
3659
+ }
3660
+ if (renderState.cullMode === "none") {
3661
+ this.stateCache.setEnabled(this.gl.CULL_FACE, false, () => this.gl.disable(this.gl.CULL_FACE));
3662
+ }
3663
+ else {
3664
+ this.stateCache.setEnabled(this.gl.CULL_FACE, true, () => this.gl.enable(this.gl.CULL_FACE));
3665
+ this.stateCache.cullFace(renderState.cullMode === "front" ? this.gl.FRONT : this.gl.BACK, () => this.gl.cullFace(renderState.cullMode === "front" ? this.gl.FRONT : this.gl.BACK));
3666
+ }
3667
+ if (renderState.blend) {
3668
+ this.stateCache.setEnabled(this.gl.BLEND, true, () => this.gl.enable(this.gl.BLEND));
3669
+ this.stateCache.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, () => this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA));
3670
+ }
3671
+ else {
3672
+ this.stateCache.setEnabled(this.gl.BLEND, false, () => this.gl.disable(this.gl.BLEND));
3673
+ }
3674
+ }
3675
+ requireBuffer(buffer) {
3676
+ if (!(buffer instanceof WebGL2Buffer) || !this.buffers.has(buffer) || buffer.disposed) {
3677
+ throw new RenderDeviceError("Buffer is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
3678
+ bufferId: buffer.id
3679
+ });
3680
+ }
3681
+ return buffer;
3682
+ }
3683
+ stencilCompare(compare) {
3684
+ switch (compare) {
3685
+ case "never": return this.gl.NEVER;
3686
+ case "less": return this.gl.LESS;
3687
+ case "less-equal": return this.gl.LEQUAL;
3688
+ case "greater": return this.gl.GREATER;
3689
+ case "greater-equal": return this.gl.GEQUAL;
3690
+ case "equal": return this.gl.EQUAL;
3691
+ case "not-equal": return this.gl.NOTEQUAL;
3692
+ case "always": return this.gl.ALWAYS;
3693
+ }
3694
+ }
3695
+ stencilOperation(operation) {
3696
+ switch (operation) {
3697
+ case "keep": return this.gl.KEEP;
3698
+ case "zero": return this.gl.ZERO;
3699
+ case "replace": return this.gl.REPLACE;
3700
+ case "increment": return this.gl.INCR;
3701
+ case "decrement": return this.gl.DECR;
3702
+ case "invert": return this.gl.INVERT;
3703
+ case "increment-wrap": return this.gl.INCR_WRAP;
3704
+ case "decrement-wrap": return this.gl.DECR_WRAP;
3705
+ }
3706
+ }
3707
+ requireShader(shader) {
3708
+ if (!(shader instanceof WebGL2ShaderProgram) || !this.shaders.has(shader) || shader.disposed) {
3709
+ throw new RenderDeviceError("Shader is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
3710
+ shaderId: shader.id
3711
+ });
3712
+ }
3713
+ return shader;
3714
+ }
3715
+ assertAlive() {
3716
+ if (this.disposed) {
3717
+ throw new RenderDeviceError("Render device is disposed", "DISPOSED_DEVICE");
3718
+ }
3719
+ if (this.contextLost) {
3720
+ throw new RenderDeviceError("Render context is lost", "CONTEXT_LOST");
3721
+ }
3722
+ }
3723
+ assertFrame() {
3724
+ this.assertAlive();
3725
+ if (!this.frameActive) {
3726
+ throw new RenderDeviceError("No active frame", "NO_ACTIVE_FRAME");
3727
+ }
3728
+ }
3729
+ readError() {
3730
+ const error = this.gl.getError();
3731
+ return error === this.gl.NO_ERROR ? null : `0x${error.toString(16)}`;
3732
+ }
3733
+ }
3734
+ function numberOption(options, key, fallback) {
3735
+ const value = options[key];
3736
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
3737
+ }
3738
+ function normalizeNativeBloomOptions(options) {
3739
+ const threshold = bloomNumberOption(options, "threshold", 0.75);
3740
+ const intensity = bloomNumberOption(options, "intensity", 0.35);
3741
+ const radius = bloomNumberOption(options, "radius", 1);
3742
+ if (threshold < 0 || threshold > 1) {
3743
+ throw new RenderDeviceError("Bloom threshold must be finite and in [0, 1].", "INVALID_POSTPROCESS_OPTIONS", { threshold });
3744
+ }
3745
+ if (intensity < 0) {
3746
+ throw new RenderDeviceError("Bloom intensity must be finite and non-negative.", "INVALID_POSTPROCESS_OPTIONS", { intensity });
3747
+ }
3748
+ if (!Number.isInteger(radius) || radius < 0 || radius > 16) {
3749
+ throw new RenderDeviceError("Bloom radius must be an integer in [0, 16].", "INVALID_POSTPROCESS_OPTIONS", { radius });
3750
+ }
3751
+ const softKnee = bloomNumberOption(options, "softKnee", 0);
3752
+ if (!(softKnee >= 0) || softKnee > 0.5) {
3753
+ throw new RenderDeviceError("Bloom softKnee must be finite and in [0, 0.5].", "INVALID_POSTPROCESS_OPTIONS", { softKnee });
3754
+ }
3755
+ const shoulder = bloomNumberOption(options, "shoulder", 0);
3756
+ if (!(shoulder >= 0) || shoulder > 1) {
3757
+ throw new RenderDeviceError("Bloom shoulder must be finite and in [0, 1].", "INVALID_POSTPROCESS_OPTIONS", { shoulder });
3758
+ }
3759
+ let quality;
3760
+ try {
3761
+ quality = normalizeBloomQualityPreset(options.quality);
3762
+ }
3763
+ catch {
3764
+ throw new RenderDeviceError("Bloom quality must be one of performance|balanced|cinematic.", "INVALID_POSTPROCESS_OPTIONS", { quality: options.quality });
3765
+ }
3766
+ return { threshold, intensity, radius, quality, softKnee, shoulder };
3767
+ }
3768
+ function normalizeNativeOutlineOptions(options) {
3769
+ const width = outlineNumberOption(options, "width", 1);
3770
+ const threshold = outlineNumberOption(options, "threshold", 0.22);
3771
+ const opacity = outlineNumberOption(options, "opacity", 0.85);
3772
+ const value = options.color;
3773
+ const sourceColor = value === undefined ? [255, 188, 64, 255] : value;
3774
+ if (!Array.isArray(sourceColor)
3775
+ || (sourceColor.length !== 3 && sourceColor.length !== 4)
3776
+ || sourceColor.some((channel) => typeof channel !== "number" || !Number.isFinite(channel) || channel < 0 || channel > 255)) {
3777
+ throw new RenderDeviceError("Outline color must contain three or four finite channels in [0, 255].", "INVALID_POSTPROCESS_OPTIONS", { color: value });
3778
+ }
3779
+ if (!Number.isInteger(width) || width < 1 || width > 6) {
3780
+ throw new RenderDeviceError("Outline width must be an integer in [1, 6].", "INVALID_POSTPROCESS_OPTIONS", { width });
3781
+ }
3782
+ if (threshold < 0 || threshold > 4) {
3783
+ throw new RenderDeviceError("Outline threshold must be finite and in [0, 4].", "INVALID_POSTPROCESS_OPTIONS", { threshold });
3784
+ }
3785
+ if (opacity < 0 || opacity > 1) {
3786
+ throw new RenderDeviceError("Outline opacity must be finite and in [0, 1].", "INVALID_POSTPROCESS_OPTIONS", { opacity });
3787
+ }
3788
+ const color = [
3789
+ sourceColor[0],
3790
+ sourceColor[1],
3791
+ sourceColor[2],
3792
+ (sourceColor[3] ?? 255)
3793
+ ];
3794
+ return { width, threshold, opacity, color };
3795
+ }
3796
+ function normalizeNativeSsaoOptions(options) {
3797
+ const radius = ssaoNumberOption(options, "radius", 2);
3798
+ const intensity = ssaoNumberOption(options, "intensity", 0.38);
3799
+ const bias = ssaoNumberOption(options, "bias", 0.015);
3800
+ if (!Number.isInteger(radius) || radius < 1 || radius > 8) {
3801
+ throw new RenderDeviceError("SSAO radius must be an integer in [1, 8].", "INVALID_POSTPROCESS_OPTIONS", { radius });
3802
+ }
3803
+ if (intensity < 0 || intensity > 2) {
3804
+ throw new RenderDeviceError("SSAO intensity must be finite and in [0, 2].", "INVALID_POSTPROCESS_OPTIONS", { intensity });
3805
+ }
3806
+ if (bias < 0 || bias > 0.25) {
3807
+ throw new RenderDeviceError("SSAO bias must be finite and in [0, 0.25].", "INVALID_POSTPROCESS_OPTIONS", { bias });
3808
+ }
3809
+ return { radius, intensity, bias };
3810
+ }
3811
+ /**
3812
+ * Camera depth range for linearizing sampleable GL depth in the native
3813
+ * SSR/depth-of-field programs (muse3jsparity-PRD A3). Missing or invalid
3814
+ * ranges fall back to the engine perspective defaults (0.1/1000) rather than
3815
+ * failing the frame: the range is a calibration hint, and every engine camera
3816
+ * builder defaults to exactly 0.1/1000. The CPU byte kernels intentionally
3817
+ * keep fixture-unit depth semantics (deterministic reference); the native
3818
+ * programs document their GL-depth contract in-shader.
3819
+ */
3820
+ function normalizeLdrDepthRange(range) {
3821
+ const near = range && Number.isFinite(range.near) && range.near > 0 ? range.near : 0.1;
3822
+ const far = range && Number.isFinite(range.far) && range.far > near ? range.far : 1000;
3823
+ return { near, far };
3824
+ }
3825
+ function normalizeNativeSsrOptions(options) {
3826
+ const intensity = ssrNumberOption(options, "intensity", 0.32);
3827
+ const maxDistance = ssrNumberOption(options, "maxDistance", 16);
3828
+ if (intensity < 0 || intensity > 2) {
3829
+ throw new RenderDeviceError("SSR intensity must be finite and in [0, 2].", "INVALID_POSTPROCESS_OPTIONS", { intensity });
3830
+ }
3831
+ if (!Number.isInteger(maxDistance) || maxDistance < 1 || maxDistance > 64) {
3832
+ throw new RenderDeviceError("SSR maxDistance must be an integer in [1, 64].", "INVALID_POSTPROCESS_OPTIONS", { maxDistance });
3833
+ }
3834
+ return { intensity, maxDistance };
3835
+ }
3836
+ function normalizeNativeDepthOfFieldOptions(options) {
3837
+ const focusDepth = depthOfFieldNumberOption(options, "focusDepth", 0.5);
3838
+ const focusRange = depthOfFieldNumberOption(options, "focusRange", 0.12);
3839
+ const maxRadius = depthOfFieldNumberOption(options, "maxRadius", 2);
3840
+ if (focusDepth < 0 || focusDepth > 1) {
3841
+ throw new RenderDeviceError("Depth-of-field focusDepth must be finite and in [0, 1].", "INVALID_POSTPROCESS_OPTIONS", { focusDepth });
3842
+ }
3843
+ if (focusRange < 0.001 || focusRange > 1) {
3844
+ throw new RenderDeviceError("Depth-of-field focusRange must be finite and in [0.001, 1].", "INVALID_POSTPROCESS_OPTIONS", { focusRange });
3845
+ }
3846
+ if (!Number.isInteger(maxRadius) || maxRadius < 0 || maxRadius > 8) {
3847
+ throw new RenderDeviceError("Depth-of-field maxRadius must be an integer in [0, 8].", "INVALID_POSTPROCESS_OPTIONS", { maxRadius });
3848
+ }
3849
+ return { focusDepth, focusRange, maxRadius };
3850
+ }
3851
+ function normalizeNativeMotionBlurOptions(options, width, height) {
3852
+ const samples = motionBlurNumberOption(options, "samples", 5);
3853
+ const scale = motionBlurNumberOption(options, "scale", 1);
3854
+ const value = options.velocity;
3855
+ if (!(value instanceof Float32Array) && !Array.isArray(value)) {
3856
+ throw new RenderDeviceError("Motion blur requires a velocity Float32Array or number array.", "WEBGL_LDR_POSTPROCESS_VELOCITY_REQUIRED");
3857
+ }
3858
+ const velocity = value instanceof Float32Array ? value : new Float32Array(value);
3859
+ if (velocity.length !== width * height * 2) {
3860
+ throw new RenderDeviceError("Motion blur velocity must contain width * height * 2 float samples.", "INVALID_POSTPROCESS_OPTIONS", {
3861
+ expectedLength: width * height * 2,
3862
+ actualLength: velocity.length
3863
+ });
3864
+ }
3865
+ if (!Number.isInteger(samples) || samples < 2 || samples > 16) {
3866
+ throw new RenderDeviceError("Motion blur samples must be an integer in [2, 16].", "INVALID_POSTPROCESS_OPTIONS", { samples });
3867
+ }
3868
+ if (scale < 0 || scale > 8) {
3869
+ throw new RenderDeviceError("Motion blur scale must be finite and in [0, 8].", "INVALID_POSTPROCESS_OPTIONS", { scale });
3870
+ }
3871
+ return { samples, scale, velocity };
3872
+ }
3873
+ function normalizeNativeTaaOptions(options, width, height) {
3874
+ const blend = taaNumberOption(options, "blend", 0.18);
3875
+ const history = options.history;
3876
+ if (!(history instanceof Uint8Array)) {
3877
+ throw new RenderDeviceError("TAA requires a Uint8Array history buffer.", "WEBGL_LDR_POSTPROCESS_HISTORY_REQUIRED");
3878
+ }
3879
+ if (history.byteLength !== width * height * 4) {
3880
+ throw new RenderDeviceError("TAA history must contain width * height * 4 RGBA bytes.", "INVALID_POSTPROCESS_OPTIONS", {
3881
+ expectedLength: width * height * 4,
3882
+ actualLength: history.byteLength
3883
+ });
3884
+ }
3885
+ if (blend < 0 || blend > 0.95) {
3886
+ throw new RenderDeviceError("TAA blend must be finite and in [0, 0.95].", "INVALID_POSTPROCESS_OPTIONS", { blend });
3887
+ }
3888
+ return { blend, history };
3889
+ }
3890
+ function bloomNumberOption(options, key, fallback) {
3891
+ const value = options[key];
3892
+ if (value === undefined)
3893
+ return fallback;
3894
+ if (typeof value !== "number" || !Number.isFinite(value)) {
3895
+ throw new RenderDeviceError(`Bloom ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
3896
+ }
3897
+ return value;
3898
+ }
3899
+ function outlineNumberOption(options, key, fallback) {
3900
+ const value = options[key];
3901
+ if (value === undefined)
3902
+ return fallback;
3903
+ if (typeof value !== "number" || !Number.isFinite(value)) {
3904
+ throw new RenderDeviceError(`Outline ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
3905
+ }
3906
+ return value;
3907
+ }
3908
+ function ssaoNumberOption(options, key, fallback) {
3909
+ const value = options[key];
3910
+ if (value === undefined)
3911
+ return fallback;
3912
+ if (typeof value !== "number" || !Number.isFinite(value)) {
3913
+ throw new RenderDeviceError(`SSAO ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
3914
+ }
3915
+ return value;
3916
+ }
3917
+ function ssrNumberOption(options, key, fallback) {
3918
+ const value = options[key];
3919
+ if (value === undefined)
3920
+ return fallback;
3921
+ if (typeof value !== "number" || !Number.isFinite(value)) {
3922
+ throw new RenderDeviceError(`SSR ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
3923
+ }
3924
+ return value;
3925
+ }
3926
+ function depthOfFieldNumberOption(options, key, fallback) {
3927
+ const value = options[key];
3928
+ if (value === undefined)
3929
+ return fallback;
3930
+ if (typeof value !== "number" || !Number.isFinite(value)) {
3931
+ throw new RenderDeviceError(`Depth-of-field ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
3932
+ }
3933
+ return value;
3934
+ }
3935
+ function motionBlurNumberOption(options, key, fallback) {
3936
+ const value = options[key];
3937
+ if (value === undefined)
3938
+ return fallback;
3939
+ if (typeof value !== "number" || !Number.isFinite(value)) {
3940
+ throw new RenderDeviceError(`Motion blur ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
3941
+ }
3942
+ return value;
3943
+ }
3944
+ function taaNumberOption(options, key, fallback) {
3945
+ const value = options[key];
3946
+ if (value === undefined)
3947
+ return fallback;
3948
+ if (typeof value !== "number" || !Number.isFinite(value)) {
3949
+ throw new RenderDeviceError(`TAA ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
3950
+ }
3951
+ return value;
3952
+ }
3953
+ function stringOption(options, key, fallback) {
3954
+ const value = options[key];
3955
+ return typeof value === "string" ? value : fallback;
3956
+ }
3957
+ function colorSpaceId(value) {
3958
+ return value === "srgb" ? 1 : 0;
3959
+ }
3960
+ function toneMappingOperatorId(value) {
3961
+ if (value === "linear")
3962
+ return 0;
3963
+ if (value === "reinhard")
3964
+ return 1;
3965
+ if (value === "aces")
3966
+ return 2;
3967
+ if (value === "filmic")
3968
+ return 3;
3969
+ if (value === "uncharted2")
3970
+ return 4;
3971
+ if (value === "agx")
3972
+ return 5;
3973
+ return 6;
3974
+ }
3975
+ function completeUploadLevels(levels) {
3976
+ const uploadLevels = [];
3977
+ for (const level of levels) {
3978
+ if (uploadLevels.length === 0) {
3979
+ uploadLevels.push(level);
3980
+ continue;
3981
+ }
3982
+ const previous = uploadLevels[uploadLevels.length - 1];
3983
+ const expectedWidth = Math.max(1, Math.floor(previous.width / 2));
3984
+ const expectedHeight = Math.max(1, Math.floor(previous.height / 2));
3985
+ if (level.width !== expectedWidth || level.height !== expectedHeight) {
3986
+ break;
3987
+ }
3988
+ uploadLevels.push(level);
3989
+ }
3990
+ return uploadLevels;
3991
+ }
3992
+ function texturePixelUploadData(data, format) {
3993
+ if (format === "rgba16f" && data instanceof Uint16Array)
3994
+ return data;
3995
+ if (format === "rgba32f" && data instanceof Float32Array)
3996
+ return data;
3997
+ if (format === "rgba8" && (data instanceof Uint8Array || data instanceof Uint8ClampedArray))
3998
+ return data;
3999
+ throw new RenderDeviceError("Texture pixel data type does not match texture format", "UNSUPPORTED_TEXTURE_FORMAT", { format });
4000
+ }
4001
+ //# sourceMappingURL=LeanWebGL2Device.js.map