@aura3d/rendering 1.3.3 → 1.4.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 (314) hide show
  1. package/dist/ForwardPass.js +7 -1
  2. package/dist/ForwardPass.js.map +1 -1
  3. package/dist/MorphTarget.d.ts +1 -0
  4. package/dist/MorphTarget.d.ts.map +1 -1
  5. package/dist/MorphTarget.js.map +1 -1
  6. package/dist/Renderer.js +7 -1
  7. package/dist/Renderer.js.map +1 -1
  8. package/package.json +38 -4
  9. package/src/ArchitecturalLightingFixtures.ts +0 -241
  10. package/src/ArchitecturalMaterialCatalog.ts +0 -130
  11. package/src/ArchitecturalMeasurementFixtures.ts +0 -155
  12. package/src/BRDFLut.ts +0 -47
  13. package/src/CameraFraming.ts +0 -200
  14. package/src/CanonicalSceneFixtures.ts +0 -95
  15. package/src/CascadedShadowMaps.ts +0 -449
  16. package/src/ColorManagement.ts +0 -127
  17. package/src/CullingFixtures.ts +0 -280
  18. package/src/DecalGeometry.ts +0 -26
  19. package/src/DepthPass.ts +0 -110
  20. package/src/EnvironmentBackgroundPass.ts +0 -194
  21. package/src/EnvironmentBackgroundResources.ts +0 -309
  22. package/src/EnvironmentLighting.ts +0 -96
  23. package/src/EnvironmentMapResources.ts +0 -1026
  24. package/src/EnvironmentPipeline.ts +0 -69
  25. package/src/EnvironmentPlatform.ts +0 -1306
  26. package/src/EnvironmentPreset.ts +0 -155
  27. package/src/Exposure.ts +0 -66
  28. package/src/ExternalParityRenderPreset.ts +0 -725
  29. package/src/ForwardPass.ts +0 -1663
  30. package/src/FrameVisualMetrics.ts +0 -301
  31. package/src/Geometry.ts +0 -461
  32. package/src/GeometryPrimitives.ts +0 -2
  33. package/src/HDRRenderPipeline.ts +0 -94
  34. package/src/IBL.ts +0 -86
  35. package/src/IndexBuffer.ts +0 -57
  36. package/src/InstancedPBRMaterial.ts +0 -186
  37. package/src/InstancedUnlitMaterial.ts +0 -60
  38. package/src/Instancing.ts +0 -19
  39. package/src/LightCollector.ts +0 -82
  40. package/src/LightUniforms.ts +0 -47
  41. package/src/LightingDebug.ts +0 -80
  42. package/src/LightingDefaults.ts +0 -79
  43. package/src/LightingRig.ts +0 -301
  44. package/src/LineGeometry.ts +0 -2
  45. package/src/Material.ts +0 -209
  46. package/src/MaterialBinding.ts +0 -131
  47. package/src/MaterialInstance.ts +0 -59
  48. package/src/MaterialPresets.ts +0 -272
  49. package/src/MorphTarget.ts +0 -123
  50. package/src/MorphTargetPlan.ts +0 -178
  51. package/src/MorphUnlitMaterial.ts +0 -60
  52. package/src/NormalMappedPBRMaterial.ts +0 -188
  53. package/src/OceanFixtures.ts +0 -318
  54. package/src/PBRLightingDefaults.ts +0 -12
  55. package/src/PBRMaterial.ts +0 -442
  56. package/src/PMREM.ts +0 -53
  57. package/src/PbrReference.ts +0 -681
  58. package/src/PostProcessPass.ts +0 -2507
  59. package/src/ProceduralTextureFixtures.ts +0 -393
  60. package/src/ProductTurntableFixtures.ts +0 -815
  61. package/src/Raycaster.ts +0 -21
  62. package/src/ReflectionProbe.ts +0 -13
  63. package/src/ReflectionSurfaces.ts +0 -219
  64. package/src/RenderBackend.ts +0 -43
  65. package/src/RenderDebugViews.ts +0 -138
  66. package/src/RenderDevice.ts +0 -836
  67. package/src/RenderGraph.ts +0 -194
  68. package/src/RenderPass.ts +0 -27
  69. package/src/RenderPipeline.ts +0 -101
  70. package/src/RenderQueue.ts +0 -12
  71. package/src/RenderState.ts +0 -12
  72. package/src/Renderer.ts +0 -2626
  73. package/src/RendererDebugOverlay.ts +0 -80
  74. package/src/RendererFeatureGates.ts +0 -150
  75. package/src/RendererPostprocessPlan.ts +0 -279
  76. package/src/RendererTiming.ts +0 -251
  77. package/src/RendererVisualPipelineReport.ts +0 -836
  78. package/src/ResourceLifecycle.ts +0 -21
  79. package/src/Sampler.ts +0 -37
  80. package/src/SceneOptimization.ts +0 -518
  81. package/src/ShaderChunks.ts +0 -393
  82. package/src/ShaderLibrary.ts +0 -2766
  83. package/src/ShaderModule.ts +0 -36
  84. package/src/ShaderPreprocessor.ts +0 -260
  85. package/src/ShaderReflection.ts +0 -201
  86. package/src/ShadowMap.ts +0 -237
  87. package/src/ShadowPass.ts +0 -198
  88. package/src/ShadowProjection.ts +0 -115
  89. package/src/SkinnedLitMaterial.ts +0 -397
  90. package/src/SkinnedUnlitMaterial.ts +0 -63
  91. package/src/SkinningBounds.ts +0 -131
  92. package/src/SpaceEnvironmentFixtures.ts +0 -200
  93. package/src/SpriteGeometry.ts +0 -21
  94. package/src/StereoCameraRig.ts +0 -127
  95. package/src/StereoEffects.ts +0 -274
  96. package/src/TerrainFixtures.ts +0 -180
  97. package/src/Texture.ts +0 -307
  98. package/src/TextureBinding.ts +0 -91
  99. package/src/TexturedPBRMaterial.ts +0 -1112
  100. package/src/TexturedUnlitMaterial.ts +0 -67
  101. package/src/ToneMapping.ts +0 -105
  102. package/src/UniformBinder.ts +0 -16
  103. package/src/UniformLayout.ts +0 -129
  104. package/src/UnlitMaterial.ts +0 -75
  105. package/src/VegetationFixtures.ts +0 -314
  106. package/src/VertexBuffer.ts +0 -120
  107. package/src/VertexFormat.ts +0 -171
  108. package/src/VoxelWorldFixtures.ts +0 -280
  109. package/src/WeatherFixtures.ts +0 -223
  110. package/src/WebGL2Device.ts +0 -2280
  111. package/src/WebGL2StateCache.ts +0 -330
  112. package/src/WebGPUDevice.ts +0 -2877
  113. package/src/WebGPURenderToTextureProof.ts +0 -139
  114. package/src/advanced-runtime/AdvancedRenderer.ts +0 -73
  115. package/src/advanced-runtime/index.ts +0 -9
  116. package/src/animation/AnimationMaterialStyle.ts +0 -32
  117. package/src/animation/AnimationRenderPreset.ts +0 -45
  118. package/src/animation/AnimationToonMaterial.ts +0 -387
  119. package/src/animation/AnimationVisualQuality.ts +0 -71
  120. package/src/animation/applyAnimationRenderPreset.ts +0 -180
  121. package/src/animation/index.ts +0 -5
  122. package/src/cinematic/BloomPass.ts +0 -26
  123. package/src/cinematic/CinematicDepthComposition.ts +0 -30
  124. package/src/cinematic/CinematicEvidence.ts +0 -123
  125. package/src/cinematic/CinematicLightingRig.ts +0 -144
  126. package/src/cinematic/CinematicMaterialPresets.ts +0 -130
  127. package/src/cinematic/CinematicPostProcess.ts +0 -83
  128. package/src/cinematic/DepthHazePass.ts +0 -30
  129. package/src/cinematic/EmissivePracticalLightSystem.ts +0 -45
  130. package/src/cinematic/FilmGrainPass.ts +0 -24
  131. package/src/cinematic/FogVolumeSystem.ts +0 -30
  132. package/src/cinematic/GlowCardSystem.ts +0 -44
  133. package/src/cinematic/RainParticleSystem.ts +0 -151
  134. package/src/cinematic/VignettePass.ts +0 -25
  135. package/src/cinematic/WetReflectionApproximation.ts +0 -27
  136. package/src/cinematic/index.ts +0 -14
  137. package/src/effects/CollisionModule.ts +0 -64
  138. package/src/effects/ColorModule.ts +0 -60
  139. package/src/effects/ForceModule.ts +0 -25
  140. package/src/effects/GPUParticleBackend.ts +0 -569
  141. package/src/effects/Particle.ts +0 -108
  142. package/src/effects/ParticleDiagnostics.ts +0 -224
  143. package/src/effects/ParticleEffectPresets.ts +0 -147
  144. package/src/effects/ParticleEmitter.ts +0 -326
  145. package/src/effects/ParticleModule.ts +0 -26
  146. package/src/effects/ParticleRenderPass.ts +0 -117
  147. package/src/effects/ParticleRenderer.ts +0 -132
  148. package/src/effects/ParticleSystem.ts +0 -333
  149. package/src/effects/SizeModule.ts +0 -56
  150. package/src/effects/TrailModule.ts +0 -72
  151. package/src/effects/VelocityModule.ts +0 -75
  152. package/src/effects/index.ts +0 -15
  153. package/src/index.ts +0 -925
  154. package/src/materials/AlphaSorting.ts +0 -30
  155. package/src/materials/MaterialExtensions.ts +0 -42
  156. package/src/materials/PhysicalMaterial.ts +0 -133
  157. package/src/materials/TransmissionPass.ts +0 -51
  158. package/src/performance/BVH.ts +0 -18
  159. package/src/performance/Batcher.ts +0 -2
  160. package/src/performance/FrustumCuller.ts +0 -9
  161. package/src/performance/LOD.ts +0 -20
  162. package/src/performance/Octree.ts +0 -10
  163. package/src/performance/RenderItemSorting.ts +0 -170
  164. package/src/performance/RendererStats.ts +0 -43
  165. package/src/performance/ResourceBudget.ts +0 -44
  166. package/src/postprocess/BloomPass.ts +0 -28
  167. package/src/postprocess/CinematicDiagnostics.ts +0 -642
  168. package/src/postprocess/ColorGradingPass.ts +0 -24
  169. package/src/postprocess/DepthOfFieldPass.ts +0 -12
  170. package/src/postprocess/EffectComposer.ts +0 -349
  171. package/src/postprocess/SSAOPass.ts +0 -20
  172. package/src/postprocess/index.ts +0 -34
  173. package/src/production-runtime/AnimationControlsPipeline.ts +0 -77
  174. package/src/production-runtime/PBRHDRPipeline.ts +0 -381
  175. package/src/production-runtime/ProductionEffectsPipeline.ts +0 -85
  176. package/src/production-runtime/ProductionRendererTypes.ts +0 -136
  177. package/src/production-runtime/ProductionRuntimeRenderer.ts +0 -172
  178. package/src/production-runtime/ProductionWebGL2Renderer.ts +0 -313
  179. package/src/production-runtime/ProductionWebGPURenderer.ts +0 -396
  180. package/src/production-runtime/TransmissionBackdropCapture.ts +0 -130
  181. package/src/production-runtime/animation/MorphTargetRenderer.ts +0 -2
  182. package/src/production-runtime/animation/SkinningRenderer.ts +0 -2
  183. package/src/production-runtime/backends/RendererBackend.ts +0 -17
  184. package/src/production-runtime/backends/WebGL2RendererBackend.ts +0 -30
  185. package/src/production-runtime/backends/WebGPURendererBackend.ts +0 -24
  186. package/src/production-runtime/backends/webgl2/WebGL2Buffer.ts +0 -2
  187. package/src/production-runtime/backends/webgl2/WebGL2Capabilities.ts +0 -2
  188. package/src/production-runtime/backends/webgl2/WebGL2RenderTarget.ts +0 -2
  189. package/src/production-runtime/backends/webgl2/WebGL2Shader.ts +0 -2
  190. package/src/production-runtime/backends/webgl2/WebGL2StateCache.ts +0 -1
  191. package/src/production-runtime/backends/webgl2/WebGL2Texture.ts +0 -2
  192. package/src/production-runtime/backends/webgpu/WebGPUBuffer.ts +0 -2
  193. package/src/production-runtime/backends/webgpu/WebGPUCapabilities.ts +0 -2
  194. package/src/production-runtime/backends/webgpu/WebGPURenderTarget.ts +0 -2
  195. package/src/production-runtime/backends/webgpu/WebGPUShader.ts +0 -2
  196. package/src/production-runtime/backends/webgpu/WebGPUTexture.ts +0 -2
  197. package/src/production-runtime/color/ColorManagement.ts +0 -3
  198. package/src/production-runtime/color/ToneMapping.ts +0 -7
  199. package/src/production-runtime/diagnostics/FrameCapture.ts +0 -2
  200. package/src/production-runtime/diagnostics/GPUCapabilities.ts +0 -2
  201. package/src/production-runtime/diagnostics/RendererStats.ts +0 -2
  202. package/src/production-runtime/environment/EnvironmentMap.ts +0 -1
  203. package/src/production-runtime/environment/HDRLoader.ts +0 -57
  204. package/src/production-runtime/environment/PMREMGenerator.ts +0 -713
  205. package/src/production-runtime/framegraph/FrameGraph.ts +0 -18
  206. package/src/production-runtime/framegraph/RenderPass.ts +0 -15
  207. package/src/production-runtime/geometry/ProjectedDecalGeometry.ts +0 -439
  208. package/src/production-runtime/index.ts +0 -127
  209. package/src/production-runtime/lights/LightManager.ts +0 -7
  210. package/src/production-runtime/lights/ShadowMapRenderer.ts +0 -5
  211. package/src/production-runtime/materials/GLTFMaterialAdapter.ts +0 -6
  212. package/src/production-runtime/materials/GLTFPBRMaterialAdapter.ts +0 -2
  213. package/src/production-runtime/materials/MaterialCompiler.ts +0 -7
  214. package/src/production-runtime/materials/MaterialTextureBindings.ts +0 -5
  215. package/src/production-runtime/materials/PBRMaterial.ts +0 -2
  216. package/src/production-runtime/materials/PBRShaderFeatures.ts +0 -2
  217. package/src/production-runtime/passes/ContactShadowPass.ts +0 -184
  218. package/src/production-runtime/passes/DepthPrepass.ts +0 -7
  219. package/src/production-runtime/passes/OpaquePass.ts +0 -7
  220. package/src/production-runtime/passes/ShadowPass.ts +0 -7
  221. package/src/production-runtime/passes/SkyboxPass.ts +0 -7
  222. package/src/production-runtime/passes/ToneMappingPass.ts +0 -7
  223. package/src/production-runtime/passes/TransparentPass.ts +0 -7
  224. package/src/production-runtime/postprocess/BloomPass.ts +0 -51
  225. package/src/production-runtime/postprocess/ColorGradingPass.ts +0 -49
  226. package/src/production-runtime/postprocess/DOFPass.ts +0 -47
  227. package/src/production-runtime/postprocess/FXAAPass.ts +0 -53
  228. package/src/production-runtime/postprocess/ProductionEffectComposer.ts +0 -78
  229. package/src/production-runtime/postprocess/ProductionPostProcessTypes.ts +0 -174
  230. package/src/production-runtime/postprocess/SSAOPass.ts +0 -45
  231. package/src/production-runtime/resources/GPUBuffer.ts +0 -2
  232. package/src/production-runtime/resources/GPUTexture.ts +0 -2
  233. package/src/production-runtime/resources/RenderTarget.ts +0 -3
  234. package/src/production-runtime/resources/ResourceCache.ts +0 -7
  235. package/src/production-runtime/scene/Camera.ts +0 -1
  236. package/src/production-runtime/scene/Lights.ts +0 -2
  237. package/src/production-runtime/scene/RenderableMesh.ts +0 -2
  238. package/src/production-runtime/scene/RenderablePrimitive.ts +0 -1
  239. package/src/production-runtime/scene/RenderableScene.ts +0 -4
  240. package/src/production-runtime/shaders/ShaderProgramLibrary.ts +0 -7
  241. package/src/production-runtime/shaders/chunks/brdf.glsl +0 -2
  242. package/src/production-runtime/shaders/chunks/ibl.glsl +0 -3
  243. package/src/production-runtime/shaders/chunks/pbr.frag.glsl +0 -47
  244. package/src/production-runtime/shaders/chunks/pbr.vert.glsl +0 -8
  245. package/src/production-runtime/shaders/chunks/shadows.glsl +0 -2
  246. package/src/production-runtime/shaders/wgsl/pbr.wgsl +0 -3
  247. package/src/production-runtime/shaders/wgsl/postprocess.wgsl +0 -2
  248. package/src/production-runtime/shaders/wgsl/skybox.wgsl +0 -2
  249. package/src/shaders/depth.frag.glsl +0 -5
  250. package/src/shaders/depth.vert.glsl +0 -8
  251. package/src/shaders/pbr-direct.frag.glsl +0 -687
  252. package/src/shaders/pbr-direct.vert.glsl +0 -18
  253. package/src/shaders/unlit.frag.glsl +0 -18
  254. package/src/shaders/unlit.vert.glsl +0 -13
  255. package/src/shadows/CascadedShadowPipeline.ts +0 -53
  256. package/src/shadows/ContactShadows.ts +0 -106
  257. package/src/shadows/ShadowDebugViews.ts +0 -46
  258. package/src/threejs-compatibility/InstancingSystem.ts +0 -17
  259. package/src/threejs-compatibility/LightingSystem.ts +0 -22
  260. package/src/threejs-compatibility/MaterialSystem.ts +0 -9
  261. package/src/threejs-compatibility/RenderTargetSystem.ts +0 -35
  262. package/src/threejs-compatibility/RendererDiagnostics.ts +0 -61
  263. package/src/threejs-compatibility/SceneRenderer.ts +0 -34
  264. package/src/threejs-compatibility/ShadowSystem.ts +0 -19
  265. package/src/threejs-compatibility/TextureSystem.ts +0 -19
  266. package/src/threejs-compatibility/ThreeCompatRenderer.ts +0 -90
  267. package/src/threejs-compatibility/TransparencySystem.ts +0 -19
  268. package/src/threejs-compatibility/index.ts +0 -29
  269. package/src/threejs-compatibility/performance/BVH.ts +0 -14
  270. package/src/threejs-compatibility/performance/FrustumCulling.ts +0 -10
  271. package/src/threejs-compatibility/performance/Instancing.ts +0 -4
  272. package/src/threejs-compatibility/performance/LODSystem.ts +0 -16
  273. package/src/threejs-compatibility/performance/OcclusionCulling.ts +0 -6
  274. package/src/threejs-compatibility/performance/RaycastAcceleration.ts +0 -12
  275. package/src/threejs-compatibility/performance/RendererProfiler.ts +0 -19
  276. package/src/threejs-compatibility/performance/TextureStreaming.ts +0 -4
  277. package/src/threejs-compatibility/performance/index.ts +0 -12
  278. package/src/threejs-compatibility/postprocess/BloomPass.ts +0 -16
  279. package/src/threejs-compatibility/postprocess/ColorGradingPass.ts +0 -16
  280. package/src/threejs-compatibility/postprocess/DepthOfFieldPass.ts +0 -21
  281. package/src/threejs-compatibility/postprocess/EffectComposer.ts +0 -14
  282. package/src/threejs-compatibility/postprocess/FXAAPass.ts +0 -15
  283. package/src/threejs-compatibility/postprocess/MotionBlurPass.ts +0 -20
  284. package/src/threejs-compatibility/postprocess/OutlinePass.ts +0 -21
  285. package/src/threejs-compatibility/postprocess/PostProcessTypes.ts +0 -169
  286. package/src/threejs-compatibility/postprocess/RenderPass.ts +0 -9
  287. package/src/threejs-compatibility/postprocess/SMAAPass.ts +0 -15
  288. package/src/threejs-compatibility/postprocess/SSAOPass.ts +0 -21
  289. package/src/threejs-compatibility/postprocess/ShaderPass.ts +0 -7
  290. package/src/threejs-compatibility/postprocess/TAAPass.ts +0 -15
  291. package/src/threejs-compatibility/postprocess/VignettePass.ts +0 -16
  292. package/src/threejs-compatibility/postprocess/index.ts +0 -15
  293. package/src/threejs-compatibility/shaders/NodeMaterial.ts +0 -21
  294. package/src/threejs-compatibility/shaders/RawShaderMaterial.ts +0 -6
  295. package/src/threejs-compatibility/shaders/ShaderDiagnostics.ts +0 -17
  296. package/src/threejs-compatibility/shaders/ShaderMaterial.ts +0 -18
  297. package/src/threejs-compatibility/shaders/ThreeCompatShaderChunks.ts +0 -6
  298. package/src/threejs-compatibility/shaders/Uniforms.ts +0 -18
  299. package/src/threejs-compatibility/shaders/index.ts +0 -9
  300. package/src/threejs-compatibility/vfx/GPUPointCloud.ts +0 -7
  301. package/src/threejs-compatibility/vfx/LineRenderer.ts +0 -13
  302. package/src/threejs-compatibility/vfx/ParticleSystem.ts +0 -23
  303. package/src/threejs-compatibility/vfx/SpriteSystem.ts +0 -14
  304. package/src/threejs-compatibility/vfx/TrailRenderer.ts +0 -8
  305. package/src/threejs-compatibility/vfx/VFXDiagnostics.ts +0 -23
  306. package/src/threejs-compatibility/vfx/index.ts +0 -9
  307. package/src/threejs-example-parity/index.ts +0 -205
  308. package/src/webgpu/WebGPUBuffer.ts +0 -1
  309. package/src/webgpu/WebGPUCompute.ts +0 -2
  310. package/src/webgpu/WebGPUPipelineCache.ts +0 -15
  311. package/src/webgpu/WebGPUPostProcess.ts +0 -2
  312. package/src/webgpu/WebGPUTexture.ts +0 -2
  313. package/tsconfig.json +0 -26
  314. package/vitest.config.ts +0 -20
@@ -1,2280 +0,0 @@
1
- import {
2
- type BufferUsage,
3
- type DrawCommand,
4
- type InstanceVertexAttribute,
5
- type LdrPostprocessPresentationOptions,
6
- type RenderBuffer,
7
- type RenderDevice,
8
- RenderDeviceError,
9
- type RenderDeviceDiagnostics,
10
- type RenderDeviceInfo,
11
- type RenderShaderProgram,
12
- type RenderTarget,
13
- type RenderTargetDescriptor,
14
- type ShaderReflection,
15
- type ShaderSources,
16
- } from "./RenderDevice";
17
- import { Texture, isCompressedTextureFormat, type TextureCompressedFormat, type TextureCubeFace, type TexturePixelData } from "./Texture";
18
- import { TextureBinding } from "./TextureBinding";
19
- import type { Sampler, TextureMagFilter, TextureMinFilter } from "./Sampler";
20
- import { type VertexAttribute, type VertexFormat } from "./VertexFormat";
21
- import { WebGL2StateCache } from "./WebGL2StateCache";
22
-
23
- interface TextureFilterAnisotropicExtension {
24
- readonly TEXTURE_MAX_ANISOTROPY_EXT: GLenum;
25
- readonly MAX_TEXTURE_MAX_ANISOTROPY_EXT: GLenum;
26
- }
27
-
28
- const WEBGL_CUBE_FACES: readonly TextureCubeFace[] = ["px", "nx", "py", "ny", "pz", "nz"];
29
-
30
- export interface WebGL2DeviceOptions {
31
- readonly canvas: HTMLCanvasElement | OffscreenCanvas;
32
- readonly antialias?: boolean;
33
- readonly alpha?: boolean;
34
- readonly preserveDrawingBuffer?: boolean;
35
- readonly errorCheckMode?: WebGL2ErrorCheckMode;
36
- }
37
-
38
- export type WebGL2ErrorCheckMode = "strict" | "frame";
39
-
40
- class WebGL2Buffer implements RenderBuffer {
41
- public disposed = false;
42
-
43
- constructor(
44
- public readonly id: number,
45
- public readonly usage: BufferUsage,
46
- public readonly byteLength: number,
47
- public readonly target: GLenum,
48
- public readonly handle: WebGLBuffer,
49
- private readonly gl: WebGL2RenderingContext
50
- ) {}
51
-
52
- dispose(): void {
53
- if (!this.disposed) {
54
- this.gl.deleteBuffer(this.handle);
55
- this.disposed = true;
56
- }
57
- }
58
- }
59
-
60
- class WebGL2ShaderProgram implements RenderShaderProgram {
61
- public disposed = false;
62
-
63
- constructor(
64
- public readonly id: number,
65
- public readonly label: string,
66
- public readonly marker: string,
67
- public readonly reflection: ShaderReflection,
68
- public readonly handle: WebGLProgram,
69
- private readonly gl: WebGL2RenderingContext
70
- ) {}
71
-
72
- dispose(): void {
73
- if (!this.disposed) {
74
- this.gl.deleteProgram(this.handle);
75
- this.disposed = true;
76
- }
77
- }
78
- }
79
-
80
- class WebGL2RenderTarget implements RenderTarget {
81
- public disposed = false;
82
-
83
- constructor(
84
- public readonly id: number,
85
- public readonly width: number,
86
- public readonly height: number,
87
- public readonly label: string,
88
- public readonly colorTexture: Texture,
89
- public readonly depthTexture: Texture | undefined,
90
- public readonly framebuffer: WebGLFramebuffer,
91
- public readonly colorHandle: WebGLTexture,
92
- public readonly depthHandle: WebGLRenderbuffer | null,
93
- public readonly depthTextureHandle: WebGLTexture | null,
94
- private readonly gl: WebGL2RenderingContext
95
- ) {}
96
-
97
- dispose(): void {
98
- if (!this.disposed) {
99
- this.gl.deleteFramebuffer(this.framebuffer);
100
- this.gl.deleteTexture(this.colorHandle);
101
- if (this.depthHandle) this.gl.deleteRenderbuffer(this.depthHandle);
102
- if (this.depthTextureHandle) this.gl.deleteTexture(this.depthTextureHandle);
103
- this.colorTexture.dispose();
104
- this.depthTexture?.dispose();
105
- this.disposed = true;
106
- }
107
- }
108
- }
109
-
110
- interface WebGL2VertexArrayCacheEntry {
111
- readonly key: string;
112
- readonly handle: WebGLVertexArrayObject;
113
- readonly boundLocations: ReadonlySet<number>;
114
- }
115
-
116
- interface WebGL2TextureUnit0Snapshot {
117
- readonly activeTexture: GLenum;
118
- readonly texture2d: WebGLTexture | null;
119
- readonly sampler: WebGLSampler | null;
120
- }
121
-
122
- interface WebGL2FullscreenPresentationStateSnapshot {
123
- readonly framebuffer: WebGLFramebuffer | null;
124
- readonly program: WebGLProgram | null;
125
- readonly textureUnit0: WebGL2TextureUnit0Snapshot;
126
- readonly vertexArray: WebGLVertexArrayObject | null;
127
- readonly viewport: Int32Array | readonly number[];
128
- readonly colorMask: readonly boolean[];
129
- readonly depthTestEnabled: boolean;
130
- readonly cullFaceEnabled: boolean;
131
- readonly blendEnabled: boolean;
132
- readonly scissorTestEnabled: boolean;
133
- readonly stencilTestEnabled: boolean;
134
- readonly polygonOffsetFillEnabled: boolean;
135
- readonly depthMask: boolean;
136
- }
137
-
138
- export class WebGL2Device implements RenderDevice {
139
- public readonly kind = "webgl2";
140
- public readonly info: RenderDeviceInfo;
141
- public disposed = false;
142
- public contextLost = false;
143
- private readonly gl: WebGL2RenderingContext;
144
- private nextId = 1;
145
- private drawCalls = 0;
146
- private buffers = new Set<WebGL2Buffer>();
147
- private shaders = new Set<WebGL2ShaderProgram>();
148
- private renderTargets = new Set<WebGL2RenderTarget>();
149
- private activeRenderTarget: WebGL2RenderTarget | null = null;
150
- private textures = new Map<Texture, WebGLTexture>();
151
- private textureUploadModes = new Map<Texture, "compressed" | "cube" | "depth-render-target" | "fallback" | "rgba8" | "rgba16f" | "rgba32f">();
152
- private fallbackTexture: WebGLTexture | null = null;
153
- private fallbackCubeTexture: WebGLTexture | null = null;
154
- private readonly samplerObjectCache = new Map<string, WebGLSampler>();
155
- private presentationProgram: WebGLProgram | null = null;
156
- private ldrPostprocessProgram: WebGLProgram | null = null;
157
- private depthReadbackProgram: WebGLProgram | null = null;
158
- private readonly uniformLocationCache = new WeakMap<WebGL2ShaderProgram, Map<string, WebGLUniformLocation | null>>();
159
- private readonly textureSamplerParameterCache = new WeakMap<WebGLTexture, Map<GLenum, GLenum | number>>();
160
- private readonly stateCache = new WebGL2StateCache({ label: "webgl2-device-state-cache" });
161
- private readonly textureUnitBindings = new Map<string, WebGLTexture>();
162
- private readonly vertexArrayCache = new Map<string, WebGL2VertexArrayCacheEntry>();
163
- private readonly vertexFormatIds = new WeakMap<VertexFormat, number>();
164
- private presentationVertexArray: WebGLVertexArrayObject | null = null;
165
- private activeTextureUnitIndex = -1;
166
- private shaderProgramCreateCount = 0;
167
- private uniformLocationLookupCount = 0;
168
- private bufferUpdateCount = 0;
169
- private textureBindCount = 0;
170
- private samplerParameterUploadCount = 0;
171
- private samplerAnisotropyUploadCount = 0;
172
- private vertexArrayCreateCount = 0;
173
- private nativeInstancedSubmissions = 0;
174
- private nextVertexFormatId = 1;
175
- private releasedTextureHandles = 0;
176
- private nativeEnvironmentBindings = 0;
177
- private nativeShadowMapBindings = 0;
178
- private lastError: string | null = null;
179
- private frameActive = false;
180
- private viewportWidth = 0;
181
- private viewportHeight = 0;
182
- private readonly maxVertexAttributes: number;
183
- private readonly anisotropicFilteringExtension: TextureFilterAnisotropicExtension | null;
184
- private readonly maxTextureAnisotropy: number;
185
- private readonly contextLostListener?: EventListener;
186
- private readonly contextRestoredListener?: EventListener;
187
- private readonly canvas?: HTMLCanvasElement | OffscreenCanvas;
188
-
189
- static create(options: WebGL2DeviceOptions): WebGL2Device {
190
- const gl = options.canvas.getContext("webgl2", {
191
- antialias: options.antialias ?? true,
192
- alpha: options.alpha ?? false,
193
- preserveDrawingBuffer: options.preserveDrawingBuffer ?? false
194
- });
195
- if (!gl) {
196
- throw new RenderDeviceError("WebGL2 is not available for the provided canvas", "WEBGL2_UNAVAILABLE");
197
- }
198
- return new WebGL2Device(gl as WebGL2RenderingContext, options.canvas, options.errorCheckMode ?? "strict");
199
- }
200
-
201
- private constructor(
202
- gl: WebGL2RenderingContext,
203
- canvas: HTMLCanvasElement | OffscreenCanvas,
204
- private readonly errorCheckMode: WebGL2ErrorCheckMode
205
- ) {
206
- this.gl = gl;
207
- this.maxVertexAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS) as number;
208
- this.anisotropicFilteringExtension = gl.getExtension("EXT_texture_filter_anisotropic") as TextureFilterAnisotropicExtension | null;
209
- this.maxTextureAnisotropy = this.anisotropicFilteringExtension
210
- ? Math.max(1, gl.getParameter(this.anisotropicFilteringExtension.MAX_TEXTURE_MAX_ANISOTROPY_EXT) as number)
211
- : 1;
212
- this.info = {
213
- backend: "webgl2",
214
- vendor: gl.getParameter(gl.VENDOR) as string,
215
- renderer: gl.getParameter(gl.RENDERER) as string,
216
- capabilities: [
217
- "buffers",
218
- "buffer-readback",
219
- "shader-validation",
220
- "render-targets",
221
- "pixel-readback",
222
- "postprocess-presentation",
223
- "draw-validation",
224
- "rasterization",
225
- "depth-render-targets",
226
- "depth-textures",
227
- "spot-shadow-maps",
228
- "point-shadow-maps",
229
- "hdr-image-based-lighting",
230
- ...(this.anisotropicFilteringExtension ? ["anisotropic-texture-filtering" as const] : []),
231
- ...(gl.getExtension("EXT_color_buffer_float") ? ["hdr-render-targets" as const, "float-readback" as const] : [])
232
- ]
233
- };
234
-
235
- this.canvas = canvas;
236
- if ("addEventListener" in canvas) {
237
- this.contextLostListener = ((event: Event) => {
238
- event.preventDefault();
239
- this.contextLost = true;
240
- this.lastError = "CONTEXT_LOST";
241
- this.frameActive = false;
242
- }) as EventListener;
243
- this.contextRestoredListener = (() => {
244
- this.contextLost = false;
245
- this.lastError = null;
246
- }) as EventListener;
247
- canvas.addEventListener("webglcontextlost", this.contextLostListener);
248
- canvas.addEventListener("webglcontextrestored", this.contextRestoredListener);
249
- }
250
- }
251
-
252
- createBuffer(usage: BufferUsage, byteLength: number, initialData?: ArrayBufferView): RenderBuffer {
253
- this.assertAlive();
254
- if (byteLength <= 0 || !Number.isInteger(byteLength)) {
255
- throw new RenderDeviceError("Buffer byteLength must be a positive integer", "INVALID_BUFFER_SIZE", { byteLength });
256
- }
257
-
258
- const handle = this.gl.createBuffer();
259
- if (!handle) {
260
- throw new RenderDeviceError("Failed to allocate WebGL buffer", "WEBGL_ALLOCATION_FAILED");
261
- }
262
-
263
- const target = usage === "index" ? this.gl.ELEMENT_ARRAY_BUFFER : this.gl.ARRAY_BUFFER;
264
- this.bindNoVertexArray();
265
- this.stateCache.bindBuffer(target, handle, () => this.gl.bindBuffer(target, handle));
266
- this.gl.bufferData(target, byteLength, this.gl.DYNAMIC_DRAW);
267
- if (initialData) {
268
- if (initialData.byteLength > byteLength) {
269
- throw new RenderDeviceError("Initial data exceeds buffer size", "BUFFER_OVERFLOW", {
270
- byteLength,
271
- dataByteLength: initialData.byteLength
272
- });
273
- }
274
- this.gl.bufferSubData(target, 0, initialData);
275
- }
276
-
277
- const buffer = new WebGL2Buffer(this.nextId++, usage, byteLength, target, handle, this.gl);
278
- this.buffers.add(buffer);
279
- return buffer;
280
- }
281
-
282
- updateBuffer(buffer: RenderBuffer, byteOffset: number, data: ArrayBufferView): void {
283
- this.assertAlive();
284
- const webglBuffer = this.requireBuffer(buffer);
285
- if (byteOffset < 0 || byteOffset + data.byteLength > webglBuffer.byteLength) {
286
- throw new RenderDeviceError("Buffer update range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
287
- byteOffset,
288
- dataByteLength: data.byteLength,
289
- byteLength: webglBuffer.byteLength
290
- });
291
- }
292
- this.bindNoVertexArray();
293
- this.stateCache.bindBuffer(webglBuffer.target, webglBuffer.handle, () => this.gl.bindBuffer(webglBuffer.target, webglBuffer.handle));
294
- this.gl.bufferSubData(webglBuffer.target, byteOffset, data);
295
- this.bufferUpdateCount += 1;
296
- }
297
-
298
- readBuffer(buffer: RenderBuffer, byteOffset = 0, byteLength = buffer.byteLength - byteOffset): Uint8Array {
299
- this.assertAlive();
300
- const webglBuffer = this.requireBuffer(buffer);
301
- if (byteOffset < 0 || byteLength < 0 || byteOffset + byteLength > webglBuffer.byteLength) {
302
- throw new RenderDeviceError("Buffer read range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
303
- byteOffset,
304
- byteLength,
305
- bufferByteLength: webglBuffer.byteLength
306
- });
307
- }
308
- const output = new Uint8Array(byteLength);
309
- this.bindNoVertexArray();
310
- this.stateCache.bindBuffer(webglBuffer.target, webglBuffer.handle, () => this.gl.bindBuffer(webglBuffer.target, webglBuffer.handle));
311
- this.gl.getBufferSubData(webglBuffer.target, byteOffset, output);
312
- return output;
313
- }
314
-
315
- createShaderProgram(sources: ShaderSources): RenderShaderProgram {
316
- this.assertAlive();
317
- if (!sources.vertex.includes(sources.marker) || !sources.fragment.includes(sources.marker)) {
318
- throw new RenderDeviceError("Shader source marker is missing from compiled sources", "SHADER_MARKER_MISSING", {
319
- label: sources.label,
320
- marker: sources.marker
321
- });
322
- }
323
- const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, sources.vertex, sources.label);
324
- const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, sources.fragment, sources.label);
325
- const program = this.gl.createProgram();
326
- if (!program) {
327
- throw new RenderDeviceError("Failed to allocate WebGL shader program", "WEBGL_ALLOCATION_FAILED");
328
- }
329
- this.shaderProgramCreateCount += 1;
330
- this.gl.attachShader(program, vertexShader);
331
- this.gl.attachShader(program, fragmentShader);
332
- this.gl.linkProgram(program);
333
- this.gl.deleteShader(vertexShader);
334
- this.gl.deleteShader(fragmentShader);
335
-
336
- if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
337
- const log = this.gl.getProgramInfoLog(program) ?? "Unknown shader link error";
338
- this.gl.deleteProgram(program);
339
- throw new RenderDeviceError("WebGL shader link failed", "SHADER_LINK_FAILED", { label: sources.label, log });
340
- }
341
-
342
- const shader = new WebGL2ShaderProgram(
343
- this.nextId++,
344
- sources.label,
345
- sources.marker,
346
- this.reflectProgram(program),
347
- program,
348
- this.gl
349
- );
350
- this.shaders.add(shader);
351
- return shader;
352
- }
353
-
354
- createRenderTarget(descriptor: RenderTargetDescriptor): RenderTarget {
355
- this.assertAlive();
356
- if (
357
- !Number.isInteger(descriptor.width) ||
358
- descriptor.width <= 0 ||
359
- !Number.isInteger(descriptor.height) ||
360
- descriptor.height <= 0
361
- ) {
362
- throw new RenderDeviceError("Render target dimensions must be positive integers", "INVALID_RENDER_TARGET_SIZE", {
363
- width: descriptor.width,
364
- height: descriptor.height,
365
- label: descriptor.label
366
- });
367
- }
368
- const colorHandle = this.gl.createTexture();
369
- const framebuffer = this.gl.createFramebuffer();
370
- const depthMode = descriptor.depth === "texture" ? "texture" : descriptor.depth === false ? "none" : "renderbuffer";
371
- const depthHandle = depthMode === "renderbuffer" ? this.gl.createRenderbuffer() : null;
372
- const depthTextureHandle = depthMode === "texture" ? this.gl.createTexture() : null;
373
- if (!colorHandle || !framebuffer || (depthMode === "renderbuffer" && !depthHandle) || (depthMode === "texture" && !depthTextureHandle)) {
374
- if (colorHandle) this.gl.deleteTexture(colorHandle);
375
- if (framebuffer) this.gl.deleteFramebuffer(framebuffer);
376
- if (depthHandle) this.gl.deleteRenderbuffer(depthHandle);
377
- if (depthTextureHandle) this.gl.deleteTexture(depthTextureHandle);
378
- throw new RenderDeviceError("Failed to allocate WebGL render target", "WEBGL_ALLOCATION_FAILED", {
379
- width: descriptor.width,
380
- height: descriptor.height,
381
- label: descriptor.label
382
- });
383
- }
384
- const previousActiveTexture = this.gl.getParameter(this.gl.ACTIVE_TEXTURE) as GLenum;
385
- this.gl.activeTexture(this.gl.TEXTURE0);
386
- const previousTexture = this.gl.getParameter(this.gl.TEXTURE_BINDING_2D) as WebGLTexture | null;
387
- const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
388
- const previousRenderbuffer = this.gl.getParameter(this.gl.RENDERBUFFER_BINDING) as WebGLRenderbuffer | null;
389
- this.gl.bindTexture(this.gl.TEXTURE_2D, colorHandle);
390
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
391
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
392
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
393
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
394
- const format = descriptor.format ?? "rgba8";
395
- const textureFormat = this.resolveRenderTargetFormat(format);
396
- this.gl.texImage2D(
397
- this.gl.TEXTURE_2D,
398
- 0,
399
- textureFormat.internalFormat,
400
- descriptor.width,
401
- descriptor.height,
402
- 0,
403
- this.gl.RGBA,
404
- textureFormat.type,
405
- null
406
- );
407
-
408
- this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
409
- this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, colorHandle, 0);
410
- if (depthHandle) {
411
- this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, depthHandle);
412
- this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_COMPONENT16, descriptor.width, descriptor.height);
413
- this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, depthHandle);
414
- }
415
- if (depthTextureHandle) {
416
- this.gl.bindTexture(this.gl.TEXTURE_2D, depthTextureHandle);
417
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
418
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
419
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
420
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
421
- this.gl.texImage2D(
422
- this.gl.TEXTURE_2D,
423
- 0,
424
- this.gl.DEPTH_COMPONENT24,
425
- descriptor.width,
426
- descriptor.height,
427
- 0,
428
- this.gl.DEPTH_COMPONENT,
429
- this.gl.UNSIGNED_INT,
430
- null
431
- );
432
- this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.TEXTURE_2D, depthTextureHandle, 0);
433
- }
434
- const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
435
- this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, previousRenderbuffer);
436
- this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
437
- this.gl.bindTexture(this.gl.TEXTURE_2D, previousTexture);
438
- this.gl.activeTexture(previousActiveTexture);
439
- this.stateCache.invalidate();
440
- if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
441
- this.gl.deleteTexture(colorHandle);
442
- this.gl.deleteFramebuffer(framebuffer);
443
- this.gl.deleteRenderbuffer(depthHandle);
444
- if (depthTextureHandle) this.gl.deleteTexture(depthTextureHandle);
445
- throw new RenderDeviceError("WebGL render target framebuffer status is invalid", "FRAMEBUFFER_INVALID", { status });
446
- }
447
-
448
- const depthTexture = depthTextureHandle
449
- ? new Texture({ width: descriptor.width, height: descriptor.height, format: "depth24", label: `${descriptor.label ?? "render-target"}-depth` })
450
- : undefined;
451
- const target = new WebGL2RenderTarget(
452
- this.nextId++,
453
- descriptor.width,
454
- descriptor.height,
455
- descriptor.label ?? "render-target",
456
- new Texture({ width: descriptor.width, height: descriptor.height, format, label: descriptor.label ?? "render-target-color" }),
457
- depthTexture,
458
- framebuffer,
459
- colorHandle,
460
- depthHandle,
461
- depthTextureHandle,
462
- this.gl
463
- );
464
- this.renderTargets.add(target);
465
- this.textures.set(target.colorTexture, colorHandle);
466
- this.textureUploadModes.set(target.colorTexture, "rgba8");
467
- if (depthTexture && depthTextureHandle) {
468
- this.textures.set(depthTexture, depthTextureHandle);
469
- this.textureUploadModes.set(depthTexture, "depth-render-target");
470
- }
471
- return target;
472
- }
473
-
474
- setRenderTarget(target: RenderTarget | null): void {
475
- this.assertAlive();
476
- if (target === null) {
477
- this.activeRenderTarget = null;
478
- this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, null, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null));
479
- this.stateCache.viewport(
480
- 0,
481
- 0,
482
- this.viewportWidth || this.gl.drawingBufferWidth,
483
- this.viewportHeight || this.gl.drawingBufferHeight,
484
- () => this.gl.viewport(0, 0, this.viewportWidth || this.gl.drawingBufferWidth, this.viewportHeight || this.gl.drawingBufferHeight)
485
- );
486
- return;
487
- }
488
- if (!(target instanceof WebGL2RenderTarget) || !this.renderTargets.has(target) || target.disposed) {
489
- throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
490
- targetId: target.id
491
- });
492
- }
493
- this.activeRenderTarget = target;
494
- this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, target.framebuffer, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, target.framebuffer));
495
- this.stateCache.viewport(0, 0, target.width, target.height, () => this.gl.viewport(0, 0, target.width, target.height));
496
- }
497
-
498
- writeRenderTargetPixels(target: RenderTarget, pixels: Uint8Array): void {
499
- this.assertAlive();
500
- if (!(target instanceof WebGL2RenderTarget) || !this.renderTargets.has(target) || target.disposed) {
501
- throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
502
- targetId: target.id
503
- });
504
- }
505
- if (target.colorTexture.format !== "rgba8") {
506
- throw new RenderDeviceError("Byte pixel uploads are only supported for rgba8 WebGL2 render targets", "INVALID_PIXEL_UPLOAD_FORMAT", {
507
- targetId: target.id,
508
- format: target.colorTexture.format
509
- });
510
- }
511
- if (pixels.length !== target.width * target.height * 4) {
512
- throw new RenderDeviceError("Render-target pixel upload must contain width * height * 4 bytes", "INVALID_PIXEL_UPLOAD_SIZE", {
513
- targetId: target.id,
514
- byteLength: pixels.length,
515
- expectedByteLength: target.width * target.height * 4
516
- });
517
- }
518
- const textureUnit0 = this.captureTextureUnit0();
519
- try {
520
- this.gl.bindTexture(this.gl.TEXTURE_2D, target.colorHandle);
521
- this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, target.width, target.height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
522
- } finally {
523
- this.restoreTextureUnit0(textureUnit0);
524
- }
525
- this.lastError = this.readError();
526
- if (this.lastError) {
527
- throw new RenderDeviceError("WebGL2 render-target pixel upload failed", "WEBGL_PIXEL_UPLOAD_FAILED", {
528
- targetId: target.id,
529
- error: this.lastError
530
- });
531
- }
532
- }
533
-
534
- presentRenderTarget(source: RenderTarget): void {
535
- this.assertAlive();
536
- if (!(source instanceof WebGL2RenderTarget) || !this.renderTargets.has(source) || source.disposed) {
537
- throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
538
- targetId: source.id
539
- });
540
- }
541
- const outputWidth = this.viewportWidth || this.gl.drawingBufferWidth;
542
- const outputHeight = this.viewportHeight || this.gl.drawingBufferHeight;
543
- this.drawRenderTargetToBackbuffer(source, outputWidth, outputHeight);
544
- this.activeRenderTarget = null;
545
- this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
546
- this.lastError = this.readError();
547
- if (this.lastError) {
548
- throw new RenderDeviceError(`WebGL2 render-target presentation failed: ${this.lastError}`, "WEBGL_PRESENT_FAILED", {
549
- targetId: source.id,
550
- error: this.lastError
551
- });
552
- }
553
- }
554
-
555
- presentLdrPostprocess(source: RenderTarget, options: LdrPostprocessPresentationOptions): void {
556
- this.assertAlive();
557
- if (!(source instanceof WebGL2RenderTarget) || !this.renderTargets.has(source) || source.disposed) {
558
- throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
559
- targetId: source.id
560
- });
561
- }
562
- const tonePass = options.passes.find((pass) => pass.name === "tone-mapping");
563
- if (source.colorTexture.format !== "rgba8" && !tonePass) {
564
- throw new RenderDeviceError("WebGL2 HDR postprocess presentation requires a tone-mapping pass before LDR output.", "WEBGL_LDR_POSTPROCESS_FORMAT_UNSUPPORTED", {
565
- targetId: source.id,
566
- format: source.colorTexture.format
567
- });
568
- }
569
- const outputTarget = options.outputTarget;
570
- if (outputTarget && (!(outputTarget instanceof WebGL2RenderTarget) || !this.renderTargets.has(outputTarget) || outputTarget.disposed)) {
571
- throw new RenderDeviceError("Output render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
572
- targetId: outputTarget.id
573
- });
574
- }
575
- if (outputTarget && outputTarget.colorTexture.format !== "rgba8") {
576
- throw new RenderDeviceError("WebGL2 LDR postprocess output target must be rgba8.", "WEBGL_LDR_POSTPROCESS_OUTPUT_FORMAT_UNSUPPORTED", {
577
- targetId: outputTarget.id,
578
- format: outputTarget.colorTexture.format
579
- });
580
- }
581
- if (outputTarget && (outputTarget.width !== source.width || outputTarget.height !== source.height)) {
582
- throw new RenderDeviceError("WebGL2 LDR postprocess source and output dimensions must match.", "WEBGL_LDR_POSTPROCESS_SIZE_MISMATCH", {
583
- sourceWidth: source.width,
584
- sourceHeight: source.height,
585
- targetWidth: outputTarget.width,
586
- targetHeight: outputTarget.height
587
- });
588
- }
589
- const webglOutputTarget = outputTarget as WebGL2RenderTarget | undefined;
590
-
591
- const colorPass = options.passes.find((pass) => pass.name === "color-grade");
592
- const fxaaPass = options.passes.find((pass) => pass.name === "fxaa");
593
- const toneOptions = { ...(options.toneMappingDefaults ?? {}), ...(tonePass?.options ?? {}) };
594
- const colorOptions = colorPass?.options ?? {};
595
- const fxaaOptions = fxaaPass?.options ?? {};
596
- const program = this.ensureLdrPostprocessProgram();
597
- const vertexArray = this.ensurePresentationVertexArray();
598
- const outputWidth = webglOutputTarget?.width ?? (this.viewportWidth || this.gl.drawingBufferWidth);
599
- const outputHeight = webglOutputTarget?.height ?? (this.viewportHeight || this.gl.drawingBufferHeight);
600
- const previousState = this.prepareFullscreenPresentation(webglOutputTarget?.framebuffer ?? null, outputWidth, outputHeight);
601
- try {
602
- this.gl.useProgram(program);
603
- this.gl.bindVertexArray(vertexArray);
604
- this.gl.activeTexture(this.gl.TEXTURE0);
605
- this.gl.bindTexture(this.gl.TEXTURE_2D, source.colorHandle);
606
- this.gl.bindSampler(0, null);
607
- this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
608
- this.gl.uniform2f(this.gl.getUniformLocation(program, "u_texelSize"), 1 / source.width, 1 / source.height);
609
- this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasToneMapping"), tonePass ? 1 : 0);
610
- this.gl.uniform1i(this.gl.getUniformLocation(program, "u_toneOperator"), toneMappingOperatorId(stringOption(toneOptions, "operator", "reinhard")));
611
- this.gl.uniform1i(this.gl.getUniformLocation(program, "u_inputColorSpace"), colorSpaceId(stringOption(toneOptions, "inputColorSpace", "linear")));
612
- this.gl.uniform1i(this.gl.getUniformLocation(program, "u_outputColorSpace"), colorSpaceId(stringOption(toneOptions, "outputColorSpace", "linear")));
613
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_exposure"), numberOption(toneOptions, "exposure", 1));
614
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_whitePoint"), Math.max(0.0001, numberOption(toneOptions, "whitePoint", 1)));
615
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_gamma"), Math.max(0.0001, numberOption(toneOptions, "gamma", 2.2)));
616
- this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasColorGrade"), colorPass ? 1 : 0);
617
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_contrast"), numberOption(colorOptions, "contrast", 1));
618
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_temperature"), numberOption(colorOptions, "temperature", 0));
619
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_tint"), numberOption(colorOptions, "tint", 0));
620
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_saturation"), numberOption(colorOptions, "saturation", 1));
621
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_vibrance"), numberOption(colorOptions, "vibrance", 0));
622
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_vignette"), numberOption(colorOptions, "vignette", 0));
623
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_sharpening"), numberOption(colorOptions, "sharpening", 0));
624
- this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasFxaa"), fxaaPass ? 1 : 0);
625
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_edgeThreshold"), numberOption(fxaaOptions, "edgeThreshold", 0.125));
626
- this.gl.uniform1f(this.gl.getUniformLocation(program, "u_subpixelBlend"), numberOption(fxaaOptions, "subpixelBlend", 0.75));
627
- this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
628
- this.gl.flush();
629
- } finally {
630
- this.restoreFullscreenPresentationState(previousState, outputWidth, outputHeight);
631
- }
632
- this.lastError = this.readError();
633
- if (this.lastError) {
634
- throw new RenderDeviceError(`WebGL2 LDR postprocess presentation failed: ${this.lastError}`, "WEBGL_LDR_POSTPROCESS_FAILED", {
635
- targetId: source.id,
636
- error: this.lastError
637
- });
638
- }
639
- this.setRenderTarget(webglOutputTarget ?? null);
640
- }
641
-
642
- readPixels(x: number, y: number, width: number, height: number): Uint8Array {
643
- this.assertAlive();
644
- if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
645
- throw new RenderDeviceError("Readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
646
- }
647
- const boundsWidth = this.activeRenderTarget?.width ?? this.gl.drawingBufferWidth;
648
- const boundsHeight = this.activeRenderTarget?.height ?? this.gl.drawingBufferHeight;
649
- if (x + width > boundsWidth || y + height > boundsHeight) {
650
- throw new RenderDeviceError("Readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
651
- x,
652
- y,
653
- width,
654
- height,
655
- boundsWidth,
656
- boundsHeight
657
- });
658
- }
659
- const pixels = new Uint8Array(width * height * 4);
660
- this.gl.readPixels(x, y, width, height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
661
- return pixels;
662
- }
663
-
664
- readFloatPixels(x: number, y: number, width: number, height: number): Float32Array {
665
- this.assertAlive();
666
- if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
667
- throw new RenderDeviceError("Float readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
668
- }
669
- const boundsWidth = this.activeRenderTarget?.width ?? this.gl.drawingBufferWidth;
670
- const boundsHeight = this.activeRenderTarget?.height ?? this.gl.drawingBufferHeight;
671
- if (x + width > boundsWidth || y + height > boundsHeight) {
672
- throw new RenderDeviceError("Float readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
673
- x,
674
- y,
675
- width,
676
- height,
677
- boundsWidth,
678
- boundsHeight
679
- });
680
- }
681
- const pixels = new Float32Array(width * height * 4);
682
- this.gl.readPixels(x, y, width, height, this.gl.RGBA, this.gl.FLOAT, pixels);
683
- return pixels;
684
- }
685
-
686
- readDepthPixels(x: number, y: number, width: number, height: number): Float32Array {
687
- this.assertAlive();
688
- if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
689
- throw new RenderDeviceError("Depth readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
690
- }
691
- const target = this.activeRenderTarget;
692
- if (!target?.depthTextureHandle) {
693
- throw new RenderDeviceError("Depth readback requires an active WebGL2 render target with a sampleable depth texture.", "DEPTH_READBACK_UNAVAILABLE", {
694
- renderTarget: target?.label ?? null
695
- });
696
- }
697
- if (x + width > target.width || y + height > target.height) {
698
- throw new RenderDeviceError("Depth readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
699
- x,
700
- y,
701
- width,
702
- height,
703
- boundsWidth: target.width,
704
- boundsHeight: target.height
705
- });
706
- }
707
- const encoded = this.copyDepthTextureToBytes(target, x, y, width, height);
708
- const pixels = new Float32Array(width * height);
709
- for (let index = 0; index < pixels.length; index += 1) {
710
- const byteIndex = index * 4;
711
- const r = encoded[byteIndex] ?? 255;
712
- const g = encoded[byteIndex + 1] ?? 255;
713
- const b = encoded[byteIndex + 2] ?? 255;
714
- pixels[index] = Math.max(0, Math.min(1, r / 255 + g / 65025 + b / 16581375));
715
- }
716
- return pixels;
717
- }
718
-
719
- beginFrame(width: number, height: number): void {
720
- this.assertAlive();
721
- if (this.frameActive) {
722
- throw new RenderDeviceError("Frame is already active", "FRAME_ALREADY_ACTIVE");
723
- }
724
- if (width <= 0 || height <= 0) {
725
- throw new RenderDeviceError("Frame dimensions must be positive", "INVALID_FRAME_SIZE", { width, height });
726
- }
727
- this.frameActive = true;
728
- this.drawCalls = 0;
729
- this.nativeEnvironmentBindings = 0;
730
- this.nativeShadowMapBindings = 0;
731
- this.activeTextureUnitIndex = -1;
732
- this.textureUnitBindings.clear();
733
- this.stateCache.invalidate();
734
- this.viewportWidth = width;
735
- this.viewportHeight = height;
736
- this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, this.activeRenderTarget?.framebuffer ?? null, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.activeRenderTarget?.framebuffer ?? null));
737
- this.stateCache.viewport(0, 0, width, height, () => this.gl.viewport(0, 0, width, height));
738
- this.stateCache.setEnabled(this.gl.DEPTH_TEST, true, () => this.gl.enable(this.gl.DEPTH_TEST));
739
- this.stateCache.depthFunc(this.gl.LEQUAL, () => this.gl.depthFunc(this.gl.LEQUAL));
740
- }
741
-
742
- clear(color: readonly [number, number, number, number]): void {
743
- this.assertFrame();
744
- this.gl.clearColor(color[0], color[1], color[2], color[3]);
745
- this.stateCache.depthMask(true, () => this.gl.depthMask(true));
746
- this.gl.clearDepth(1);
747
- this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
748
- }
749
-
750
- clearRenderTarget(color: readonly [number, number, number, number]): void {
751
- this.assertFrame();
752
- this.gl.clearColor(color[0], color[1], color[2], color[3]);
753
- this.stateCache.depthMask(true, () => this.gl.depthMask(true));
754
- this.gl.clearDepth(1);
755
- this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
756
- }
757
-
758
- draw(command: DrawCommand): void {
759
- this.assertFrame();
760
- const vertexBuffer = this.requireBuffer(command.vertexBuffer);
761
- this.applyRenderState(command.renderState);
762
- let shader: WebGL2ShaderProgram | undefined;
763
- if (command.shader) {
764
- const activeShader = this.requireShader(command.shader);
765
- shader = activeShader;
766
- this.stateCache.useProgram(activeShader.handle, () => this.gl.useProgram(activeShader.handle));
767
- if (command.uniforms) {
768
- this.uploadUniforms(activeShader, command.uniforms);
769
- if (this.errorCheckMode === "strict") {
770
- const uniformError = this.readError();
771
- if (uniformError) {
772
- throw new RenderDeviceError(`WebGL2 uniform upload failed for draw ${command.label ?? "unnamed"}: ${uniformError}`, "WEBGL_DRAW_FAILED", {
773
- label: command.label,
774
- stage: "uniforms",
775
- error: uniformError
776
- });
777
- }
778
- }
779
- }
780
- }
781
-
782
- if (shader && command.vertexFormat) {
783
- this.bindVertexArrayForCommand(command, shader, vertexBuffer);
784
- if (this.errorCheckMode === "strict") {
785
- const vertexFormatError = this.readError();
786
- if (vertexFormatError) {
787
- throw new RenderDeviceError(`WebGL2 vertex format binding failed for draw ${command.label ?? "unnamed"}: ${vertexFormatError}`, "WEBGL_DRAW_FAILED", {
788
- label: command.label,
789
- stage: "vertex-format",
790
- error: vertexFormatError
791
- });
792
- }
793
- }
794
- } else {
795
- this.bindNoVertexArray();
796
- this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle));
797
- }
798
- if (command.indexBuffer) {
799
- const indexBuffer = this.requireBuffer(command.indexBuffer);
800
- if (!shader || !command.vertexFormat) {
801
- this.stateCache.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle, () => this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle));
802
- }
803
- const type = command.indexType === "uint32" ? this.gl.UNSIGNED_INT : this.gl.UNSIGNED_SHORT;
804
- const indexByteSize = command.indexType === "uint32" ? 4 : 2;
805
- const indexOffset = (command.firstIndex ?? 0) * indexByteSize;
806
- if ((command.instanceCount ?? 1) > 1) {
807
- this.gl.drawElementsInstanced(this.primitive(command.topology), command.indexCount ?? 0, type, indexOffset, command.instanceCount ?? 1);
808
- this.nativeInstancedSubmissions += 1;
809
- } else {
810
- this.gl.drawElements(this.primitive(command.topology), command.indexCount ?? 0, type, indexOffset);
811
- }
812
- } else {
813
- if ((command.instanceCount ?? 1) > 1) {
814
- this.gl.drawArraysInstanced(this.primitive(command.topology), command.firstVertex ?? 0, command.vertexCount, command.instanceCount ?? 1);
815
- this.nativeInstancedSubmissions += 1;
816
- } else {
817
- this.gl.drawArrays(this.primitive(command.topology), command.firstVertex ?? 0, command.vertexCount);
818
- }
819
- }
820
- this.drawCalls += 1;
821
- if (this.errorCheckMode === "strict") {
822
- const drawError = this.readError();
823
- if (drawError) {
824
- throw new RenderDeviceError(`WebGL2 draw failed for ${command.label ?? "unnamed"}: ${drawError}`, "WEBGL_DRAW_FAILED", {
825
- label: command.label,
826
- stage: "draw",
827
- error: drawError
828
- });
829
- }
830
- }
831
- }
832
-
833
- endFrame(): void {
834
- this.assertFrame();
835
- this.frameActive = false;
836
- this.lastError = this.readError();
837
- }
838
-
839
- captureState(): ReadonlyMap<string, string | number | boolean | null> {
840
- const viewport = this.gl.getParameter(this.gl.VIEWPORT) as Int32Array | readonly number[];
841
- return new Map<string, string | number | boolean | null>([
842
- ["backend", this.kind],
843
- ["disposed", this.disposed],
844
- ["contextLost", this.contextLost],
845
- ["frameActive", this.frameActive],
846
- ["depthTest", this.gl.isEnabled(this.gl.DEPTH_TEST)],
847
- ["blend", this.gl.isEnabled(this.gl.BLEND)],
848
- ["cullFace", this.gl.isEnabled(this.gl.CULL_FACE)],
849
- ["program", this.gl.getParameter(this.gl.CURRENT_PROGRAM) ? "bound" : null],
850
- ["arrayBuffer", this.gl.getParameter(this.gl.ARRAY_BUFFER_BINDING) ? "bound" : null],
851
- ["elementArrayBuffer", this.gl.getParameter(this.gl.ELEMENT_ARRAY_BUFFER_BINDING) ? "bound" : null],
852
- ["viewportWidth", this.viewportWidth],
853
- ["viewportHeight", this.viewportHeight],
854
- ["actualViewportWidth", viewport[2] ?? 0],
855
- ["actualViewportHeight", viewport[3] ?? 0],
856
- ["renderTarget", this.activeRenderTarget?.label ?? null],
857
- ["drawCalls", this.drawCalls],
858
- ["shaderProgramCreates", this.shaderProgramCreateCount],
859
- ["uniformLocationLookups", this.uniformLocationLookupCount],
860
- ["bufferUpdates", this.bufferUpdateCount],
861
- ["textureBinds", this.textureBindCount],
862
- ["samplerParameterUploads", this.samplerParameterUploadCount],
863
- ["vertexArrayCreates", this.vertexArrayCreateCount],
864
- ["samplerObjects", this.samplerObjectCache.size],
865
- ["samplerAnisotropyUploads", this.samplerAnisotropyUploadCount],
866
- ["maxTextureAnisotropy", this.maxTextureAnisotropy],
867
- ["nativeEnvironmentBindings", this.nativeEnvironmentBindings],
868
- ["nativeShadowMapBindings", this.nativeShadowMapBindings],
869
- ["nativeInstancedSubmissions", this.nativeInstancedSubmissions],
870
- ["stateCacheIssued", this.stateCache.stats().issued],
871
- ["stateCacheSkipped", this.stateCache.stats().skipped]
872
- ]);
873
- }
874
-
875
- getDiagnostics(): RenderDeviceDiagnostics {
876
- this.releaseDisposedTextureHandles();
877
- const liveRenderTargets = [...this.renderTargets].filter((target) => !target.disposed);
878
- const liveTextures = new Set<Texture>([
879
- ...liveRenderTargets.map((target) => target.colorTexture),
880
- ...[...this.textures.keys()].filter((texture) => !texture.disposed)
881
- ]);
882
- const liveTextureList = [...liveTextures];
883
- const compressedTextures = liveTextureList.filter((texture) => isCompressedTextureFormat(texture.format));
884
- const fallbackTextures = liveTextureList.filter((texture) => this.textureUploadModes.get(texture) === "fallback");
885
- const stateCacheStats = this.stateCache.stats();
886
- const bufferBytes = [...this.buffers].filter((buffer) => !buffer.disposed).reduce((total, buffer) => total + buffer.byteLength, 0);
887
- const textureBytes = liveTextureList.reduce((total, texture) => total + texture.byteLength, 0);
888
- return {
889
- drawCalls: this.drawCalls,
890
- buffers: [...this.buffers].filter((buffer) => !buffer.disposed).length,
891
- shaders: [...this.shaders].filter((shader) => !shader.disposed).length,
892
- renderTargets: liveRenderTargets.length,
893
- textures: liveTextures.size,
894
- bufferBytes,
895
- textureBytes,
896
- approximateGpuMemoryBytes: bufferBytes + textureBytes,
897
- compressedTextures: compressedTextures.length,
898
- compressedTextureBytes: compressedTextures.reduce((total, texture) => total + texture.byteLength, 0),
899
- textureFallbacks: fallbackTextures.length,
900
- textureFallbackBytes: fallbackTextures.reduce((total, texture) => total + texture.fallbackByteLength, 0),
901
- nativeEnvironmentBindings: this.nativeEnvironmentBindings,
902
- nativeShadowMapBindings: this.nativeShadowMapBindings,
903
- nativeInstancedSubmissions: this.nativeInstancedSubmissions,
904
- samplerAnisotropyUploads: this.samplerAnisotropyUploadCount,
905
- maxTextureAnisotropy: this.maxTextureAnisotropy,
906
- stateCacheIssued: stateCacheStats.issued,
907
- stateCacheSkipped: stateCacheStats.skipped,
908
- stateCacheProgramSwitches: stateCacheStats.byOperation.useProgram?.issued ?? 0,
909
- stateCacheTextureBinds: stateCacheStats.byOperation.bindTexture?.issued ?? 0,
910
- stateCacheBufferBinds: stateCacheStats.byOperation.bindBuffer?.issued ?? 0,
911
- stateCacheVertexArrayBinds: stateCacheStats.byOperation.bindVertexArray?.issued ?? 0,
912
- stateCacheSamplerBinds: stateCacheStats.byOperation.bindSampler?.issued ?? 0,
913
- disposedBuffers: [...this.buffers].filter((buffer) => buffer.disposed).length,
914
- disposedShaders: [...this.shaders].filter((shader) => shader.disposed).length,
915
- disposedRenderTargets: [...this.renderTargets].filter((target) => target.disposed).length,
916
- disposedTextures: [...this.renderTargets].filter((target) => target.colorTexture.disposed).length + this.releasedTextureHandles,
917
- lastError: this.lastError,
918
- contextLost: this.contextLost
919
- };
920
- }
921
-
922
- dispose(): void {
923
- if (this.canvas && "removeEventListener" in this.canvas) {
924
- if (this.contextLostListener) this.canvas.removeEventListener("webglcontextlost", this.contextLostListener);
925
- if (this.contextRestoredListener) this.canvas.removeEventListener("webglcontextrestored", this.contextRestoredListener);
926
- }
927
- for (const buffer of this.buffers) {
928
- buffer.dispose();
929
- }
930
- for (const shader of this.shaders) {
931
- shader.dispose();
932
- }
933
- for (const target of this.renderTargets) {
934
- target.dispose();
935
- }
936
- for (const entry of this.vertexArrayCache.values()) {
937
- this.gl.deleteVertexArray(entry.handle);
938
- }
939
- this.vertexArrayCache.clear();
940
- for (const sampler of this.samplerObjectCache.values()) {
941
- this.gl.deleteSampler(sampler);
942
- }
943
- this.samplerObjectCache.clear();
944
- for (const texture of this.textures.values()) {
945
- this.gl.deleteTexture(texture);
946
- }
947
- this.textureUploadModes.clear();
948
- if (this.fallbackTexture) {
949
- this.gl.deleteTexture(this.fallbackTexture);
950
- this.fallbackTexture = null;
951
- }
952
- if (this.fallbackCubeTexture) {
953
- this.gl.deleteTexture(this.fallbackCubeTexture);
954
- this.fallbackCubeTexture = null;
955
- }
956
- if (this.presentationProgram) {
957
- this.gl.deleteProgram(this.presentationProgram);
958
- this.presentationProgram = null;
959
- }
960
- if (this.ldrPostprocessProgram) {
961
- this.gl.deleteProgram(this.ldrPostprocessProgram);
962
- this.ldrPostprocessProgram = null;
963
- }
964
- if (this.presentationVertexArray) {
965
- this.gl.deleteVertexArray(this.presentationVertexArray);
966
- this.presentationVertexArray = null;
967
- }
968
- if (this.depthReadbackProgram) {
969
- this.gl.deleteProgram(this.depthReadbackProgram);
970
- this.depthReadbackProgram = null;
971
- }
972
- this.textures.clear();
973
- this.disposed = true;
974
- }
975
-
976
- private copyDepthTextureToBytes(source: WebGL2RenderTarget, x: number, y: number, width: number, height: number): Uint8Array {
977
- const colorHandle = this.gl.createTexture();
978
- const framebuffer = this.gl.createFramebuffer();
979
- if (!colorHandle || !framebuffer) {
980
- if (colorHandle) this.gl.deleteTexture(colorHandle);
981
- if (framebuffer) this.gl.deleteFramebuffer(framebuffer);
982
- throw new RenderDeviceError("Failed to allocate WebGL2 depth readback target", "WEBGL_ALLOCATION_FAILED", {
983
- renderTarget: source.label
984
- });
985
- }
986
- const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
987
- const previousProgram = this.gl.getParameter(this.gl.CURRENT_PROGRAM) as WebGLProgram | null;
988
- const textureUnit0 = this.captureTextureUnit0();
989
- const viewport = this.gl.getParameter(this.gl.VIEWPORT) as Int32Array | readonly number[];
990
- const depthTestEnabled = this.gl.isEnabled(this.gl.DEPTH_TEST);
991
- const depthMask = this.gl.getParameter(this.gl.DEPTH_WRITEMASK) as boolean;
992
- const cullFaceEnabled = this.gl.isEnabled(this.gl.CULL_FACE);
993
- const blendEnabled = this.gl.isEnabled(this.gl.BLEND);
994
- const pixels = new Uint8Array(width * height * 4);
995
- try {
996
- this.gl.activeTexture(this.gl.TEXTURE0);
997
- this.gl.bindTexture(this.gl.TEXTURE_2D, colorHandle);
998
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
999
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
1000
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
1001
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
1002
- this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, width, height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null);
1003
- this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
1004
- this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, colorHandle, 0);
1005
- const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
1006
- if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
1007
- throw new RenderDeviceError("WebGL2 depth readback framebuffer status is invalid", "FRAMEBUFFER_INVALID", {
1008
- renderTarget: source.label,
1009
- status
1010
- });
1011
- }
1012
- this.gl.viewport(0, 0, width, height);
1013
- this.gl.disable(this.gl.DEPTH_TEST);
1014
- this.gl.disable(this.gl.CULL_FACE);
1015
- this.gl.disable(this.gl.BLEND);
1016
- this.gl.depthMask(false);
1017
- this.gl.useProgram(this.ensureDepthReadbackProgram());
1018
- this.gl.activeTexture(this.gl.TEXTURE0);
1019
- this.gl.bindTexture(this.gl.TEXTURE_2D, source.depthTextureHandle);
1020
- this.gl.bindSampler(0, null);
1021
- this.gl.uniform1i(this.gl.getUniformLocation(this.ensureDepthReadbackProgram(), "u_depth"), 0);
1022
- this.gl.uniform4f(this.gl.getUniformLocation(this.ensureDepthReadbackProgram(), "u_sourceRect"), x / source.width, y / source.height, width / source.width, height / source.height);
1023
- this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1024
- this.gl.readPixels(0, 0, width, height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
1025
- const error = this.readError();
1026
- if (error) {
1027
- throw new RenderDeviceError("WebGL2 depth texture readback failed", "WEBGL_DEPTH_READBACK_FAILED", {
1028
- renderTarget: source.label,
1029
- error
1030
- });
1031
- }
1032
- return pixels;
1033
- } finally {
1034
- this.restoreTextureUnit0(textureUnit0);
1035
- this.gl.useProgram(previousProgram);
1036
- if (depthTestEnabled) this.gl.enable(this.gl.DEPTH_TEST);
1037
- else this.gl.disable(this.gl.DEPTH_TEST);
1038
- if (cullFaceEnabled) this.gl.enable(this.gl.CULL_FACE);
1039
- else this.gl.disable(this.gl.CULL_FACE);
1040
- if (blendEnabled) this.gl.enable(this.gl.BLEND);
1041
- else this.gl.disable(this.gl.BLEND);
1042
- this.gl.depthMask(depthMask);
1043
- this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
1044
- this.gl.viewport(viewport[0] ?? 0, viewport[1] ?? 0, viewport[2] ?? this.viewportWidth, viewport[3] ?? this.viewportHeight);
1045
- this.gl.deleteTexture(colorHandle);
1046
- this.gl.deleteFramebuffer(framebuffer);
1047
- this.stateCache.invalidate();
1048
- }
1049
- }
1050
-
1051
- private drawRenderTargetToBackbuffer(source: WebGL2RenderTarget, outputWidth: number, outputHeight: number): void {
1052
- const program = this.ensurePresentationProgram();
1053
- const vertexArray = this.ensurePresentationVertexArray();
1054
- const previousState = this.prepareFullscreenPresentation(null, outputWidth, outputHeight);
1055
- this.gl.useProgram(program);
1056
- this.gl.bindVertexArray(vertexArray);
1057
- this.gl.activeTexture(this.gl.TEXTURE0);
1058
- this.gl.bindTexture(this.gl.TEXTURE_2D, source.colorHandle);
1059
- this.gl.bindSampler(0, null);
1060
- try {
1061
- const uniformLocation = this.gl.getUniformLocation(program, "u_source");
1062
- this.gl.uniform1i(uniformLocation, 0);
1063
- this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
1064
- this.gl.flush();
1065
- } finally {
1066
- this.restoreFullscreenPresentationState(previousState, outputWidth, outputHeight);
1067
- }
1068
- }
1069
-
1070
- private captureTextureUnit0(): WebGL2TextureUnit0Snapshot {
1071
- const activeTexture = this.gl.getParameter(this.gl.ACTIVE_TEXTURE) as GLenum;
1072
- this.gl.activeTexture(this.gl.TEXTURE0);
1073
- return {
1074
- activeTexture,
1075
- texture2d: this.gl.getParameter(this.gl.TEXTURE_BINDING_2D) as WebGLTexture | null,
1076
- sampler: this.gl.getParameter(this.gl.SAMPLER_BINDING) as WebGLSampler | null
1077
- };
1078
- }
1079
-
1080
- private restoreTextureUnit0(snapshot: WebGL2TextureUnit0Snapshot): void {
1081
- this.gl.activeTexture(this.gl.TEXTURE0);
1082
- this.gl.bindTexture(this.gl.TEXTURE_2D, snapshot.texture2d);
1083
- this.gl.bindSampler(0, snapshot.sampler);
1084
- this.gl.activeTexture(snapshot.activeTexture);
1085
- }
1086
-
1087
- private prepareFullscreenPresentation(framebuffer: WebGLFramebuffer | null, outputWidth: number, outputHeight: number): WebGL2FullscreenPresentationStateSnapshot {
1088
- const snapshot: WebGL2FullscreenPresentationStateSnapshot = {
1089
- framebuffer: this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING) as WebGLFramebuffer | null,
1090
- program: this.gl.getParameter(this.gl.CURRENT_PROGRAM) as WebGLProgram | null,
1091
- textureUnit0: this.captureTextureUnit0(),
1092
- vertexArray: this.gl.getParameter(this.gl.VERTEX_ARRAY_BINDING) as WebGLVertexArrayObject | null,
1093
- viewport: this.gl.getParameter(this.gl.VIEWPORT) as Int32Array | readonly number[],
1094
- colorMask: this.gl.getParameter(this.gl.COLOR_WRITEMASK) as readonly boolean[],
1095
- depthTestEnabled: this.gl.isEnabled(this.gl.DEPTH_TEST),
1096
- cullFaceEnabled: this.gl.isEnabled(this.gl.CULL_FACE),
1097
- blendEnabled: this.gl.isEnabled(this.gl.BLEND),
1098
- scissorTestEnabled: this.gl.isEnabled(this.gl.SCISSOR_TEST),
1099
- stencilTestEnabled: this.gl.isEnabled(this.gl.STENCIL_TEST),
1100
- polygonOffsetFillEnabled: this.gl.isEnabled(this.gl.POLYGON_OFFSET_FILL),
1101
- depthMask: this.gl.getParameter(this.gl.DEPTH_WRITEMASK) as boolean
1102
- };
1103
- this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
1104
- this.gl.viewport(0, 0, outputWidth, outputHeight);
1105
- this.gl.colorMask(true, true, true, true);
1106
- this.gl.disable(this.gl.SCISSOR_TEST);
1107
- this.gl.disable(this.gl.STENCIL_TEST);
1108
- this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
1109
- this.gl.disable(this.gl.DEPTH_TEST);
1110
- this.gl.disable(this.gl.CULL_FACE);
1111
- this.gl.disable(this.gl.BLEND);
1112
- this.gl.depthMask(false);
1113
- return snapshot;
1114
- }
1115
-
1116
- private restoreFullscreenPresentationState(snapshot: WebGL2FullscreenPresentationStateSnapshot, fallbackWidth: number, fallbackHeight: number): void {
1117
- this.gl.bindVertexArray(snapshot.vertexArray);
1118
- this.restoreTextureUnit0(snapshot.textureUnit0);
1119
- this.gl.useProgram(snapshot.program);
1120
- if (snapshot.depthTestEnabled) this.gl.enable(this.gl.DEPTH_TEST);
1121
- else this.gl.disable(this.gl.DEPTH_TEST);
1122
- if (snapshot.cullFaceEnabled) this.gl.enable(this.gl.CULL_FACE);
1123
- else this.gl.disable(this.gl.CULL_FACE);
1124
- if (snapshot.blendEnabled) this.gl.enable(this.gl.BLEND);
1125
- else this.gl.disable(this.gl.BLEND);
1126
- if (snapshot.scissorTestEnabled) this.gl.enable(this.gl.SCISSOR_TEST);
1127
- else this.gl.disable(this.gl.SCISSOR_TEST);
1128
- if (snapshot.stencilTestEnabled) this.gl.enable(this.gl.STENCIL_TEST);
1129
- else this.gl.disable(this.gl.STENCIL_TEST);
1130
- if (snapshot.polygonOffsetFillEnabled) this.gl.enable(this.gl.POLYGON_OFFSET_FILL);
1131
- else this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
1132
- this.gl.colorMask(snapshot.colorMask[0] ?? true, snapshot.colorMask[1] ?? true, snapshot.colorMask[2] ?? true, snapshot.colorMask[3] ?? true);
1133
- this.gl.depthMask(snapshot.depthMask);
1134
- this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, snapshot.framebuffer);
1135
- this.gl.viewport(snapshot.viewport[0] ?? 0, snapshot.viewport[1] ?? 0, snapshot.viewport[2] ?? fallbackWidth, snapshot.viewport[3] ?? fallbackHeight);
1136
- this.stateCache.invalidate();
1137
- }
1138
-
1139
- private ensurePresentationVertexArray(): WebGLVertexArrayObject {
1140
- if (this.presentationVertexArray) return this.presentationVertexArray;
1141
- const vertexArray = this.gl.createVertexArray();
1142
- if (!vertexArray) {
1143
- throw new RenderDeviceError("Failed to allocate WebGL presentation vertex array", "WEBGL_ALLOCATION_FAILED");
1144
- }
1145
- this.presentationVertexArray = vertexArray;
1146
- return vertexArray;
1147
- }
1148
-
1149
- private ensureDepthReadbackProgram(): WebGLProgram {
1150
- if (this.depthReadbackProgram) {
1151
- return this.depthReadbackProgram;
1152
- }
1153
- const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
1154
- precision highp float;
1155
- out vec2 v_uv;
1156
- void main() {
1157
- vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
1158
- v_uv = position;
1159
- gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
1160
- }
1161
- `, "webgl2-depth-readback");
1162
- const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
1163
- precision highp float;
1164
- uniform sampler2D u_depth;
1165
- uniform vec4 u_sourceRect;
1166
- in vec2 v_uv;
1167
- out vec4 outColor;
1168
- vec3 packDepth24(float value) {
1169
- value = clamp(value, 0.0, 1.0);
1170
- vec3 encoded = fract(value * vec3(1.0, 255.0, 65025.0));
1171
- encoded -= encoded.yzz * vec3(1.0 / 255.0, 1.0 / 255.0, 0.0);
1172
- return encoded;
1173
- }
1174
- void main() {
1175
- vec2 uv = u_sourceRect.xy + v_uv * u_sourceRect.zw;
1176
- outColor = vec4(packDepth24(texture(u_depth, uv).r), 1.0);
1177
- }
1178
- `, "webgl2-depth-readback");
1179
- const program = this.gl.createProgram();
1180
- if (!program) {
1181
- this.gl.deleteShader(vertex);
1182
- this.gl.deleteShader(fragment);
1183
- throw new RenderDeviceError("Failed to allocate WebGL2 depth readback shader", "WEBGL_ALLOCATION_FAILED");
1184
- }
1185
- this.gl.attachShader(program, vertex);
1186
- this.gl.attachShader(program, fragment);
1187
- this.gl.linkProgram(program);
1188
- this.gl.deleteShader(vertex);
1189
- this.gl.deleteShader(fragment);
1190
- if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
1191
- const log = this.gl.getProgramInfoLog(program) ?? "Unknown depth readback shader link error";
1192
- this.gl.deleteProgram(program);
1193
- throw new RenderDeviceError("WebGL2 depth readback shader link failed", "SHADER_LINK_FAILED", { log });
1194
- }
1195
- this.depthReadbackProgram = program;
1196
- return program;
1197
- }
1198
-
1199
- private ensurePresentationProgram(): WebGLProgram {
1200
- if (this.presentationProgram) {
1201
- return this.presentationProgram;
1202
- }
1203
- const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
1204
- precision highp float;
1205
- out vec2 v_uv;
1206
- void main() {
1207
- vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
1208
- v_uv = position;
1209
- gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
1210
- }
1211
- `, "webgl2-present-render-target");
1212
- const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
1213
- precision highp float;
1214
- uniform sampler2D u_source;
1215
- in vec2 v_uv;
1216
- out vec4 outColor;
1217
- void main() {
1218
- outColor = texture(u_source, v_uv);
1219
- }
1220
- `, "webgl2-present-render-target");
1221
- const program = this.gl.createProgram();
1222
- if (!program) {
1223
- this.gl.deleteShader(vertex);
1224
- this.gl.deleteShader(fragment);
1225
- throw new RenderDeviceError("Failed to allocate WebGL presentation shader", "WEBGL_ALLOCATION_FAILED");
1226
- }
1227
- this.gl.attachShader(program, vertex);
1228
- this.gl.attachShader(program, fragment);
1229
- this.gl.linkProgram(program);
1230
- this.gl.deleteShader(vertex);
1231
- this.gl.deleteShader(fragment);
1232
- if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
1233
- const log = this.gl.getProgramInfoLog(program) ?? "Unknown presentation shader link error";
1234
- this.gl.deleteProgram(program);
1235
- throw new RenderDeviceError("WebGL presentation shader link failed", "SHADER_LINK_FAILED", { log });
1236
- }
1237
- this.presentationProgram = program;
1238
- return program;
1239
- }
1240
-
1241
- private ensureLdrPostprocessProgram(): WebGLProgram {
1242
- if (this.ldrPostprocessProgram) {
1243
- return this.ldrPostprocessProgram;
1244
- }
1245
- const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
1246
- precision highp float;
1247
- out vec2 v_uv;
1248
- void main() {
1249
- vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
1250
- v_uv = position;
1251
- gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
1252
- }
1253
- `, "webgl2-ldr-postprocess");
1254
- const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
1255
- precision highp float;
1256
- uniform sampler2D u_source;
1257
- uniform vec2 u_texelSize;
1258
- uniform int u_hasToneMapping;
1259
- uniform int u_toneOperator;
1260
- uniform int u_inputColorSpace;
1261
- uniform int u_outputColorSpace;
1262
- uniform float u_exposure;
1263
- uniform float u_whitePoint;
1264
- uniform float u_gamma;
1265
- uniform int u_hasColorGrade;
1266
- uniform float u_contrast;
1267
- uniform float u_temperature;
1268
- uniform float u_tint;
1269
- uniform float u_saturation;
1270
- uniform float u_vibrance;
1271
- uniform float u_vignette;
1272
- uniform float u_sharpening;
1273
- uniform int u_hasFxaa;
1274
- uniform float u_edgeThreshold;
1275
- uniform float u_subpixelBlend;
1276
- in vec2 v_uv;
1277
- out vec4 outColor;
1278
-
1279
- float srgbToLinear(float value) {
1280
- return value <= 0.04045 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4);
1281
- }
1282
-
1283
- float linearToSrgb(float value) {
1284
- return value <= 0.0031308 ? value * 12.92 : 1.055 * pow(value, 1.0 / 2.4) - 0.055;
1285
- }
1286
-
1287
- vec3 decodeColor(vec3 color) {
1288
- return u_inputColorSpace == 1 ? vec3(srgbToLinear(color.r), srgbToLinear(color.g), srgbToLinear(color.b)) : color;
1289
- }
1290
-
1291
- vec3 encodeColor(vec3 color) {
1292
- vec3 linear = clamp(color, 0.0, 1.0);
1293
- return u_outputColorSpace == 1 ? vec3(linearToSrgb(linear.r), linearToSrgb(linear.g), linearToSrgb(linear.b)) : linear;
1294
- }
1295
-
1296
- float aces(float value) {
1297
- return (value * (2.51 * value + 0.03)) / (value * (2.43 * value + 0.59) + 0.14);
1298
- }
1299
-
1300
- float filmic(float value) {
1301
- float x = max(0.0, value);
1302
- float toe = max(0.0, x - 0.004);
1303
- float curve = (toe * (6.2 * toe + 0.5)) / (toe * (6.2 * toe + 1.7) + 0.06);
1304
- return min(curve, x * 1.08);
1305
- }
1306
-
1307
- float uncharted2(float value) {
1308
- float a = 0.15;
1309
- float b = 0.5;
1310
- float c = 0.1;
1311
- float d = 0.2;
1312
- float e = 0.02;
1313
- float f = 0.3;
1314
- float w = 11.2;
1315
- float x = value * 2.0;
1316
- float curve = ((x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f)) - e / f;
1317
- float wx = w;
1318
- float white = ((wx * (a * wx + c * b) + d * e) / (wx * (a * wx + b) + d * f)) - e / f;
1319
- return clamp(curve / white, 0.0, 1.0);
1320
- }
1321
-
1322
- float agx(float value) {
1323
- float x = max(0.0, value);
1324
- float encoded = log2(1.0 + x) / log2(17.0);
1325
- return clamp(encoded * encoded * (3.0 - 2.0 * encoded), 0.0, 1.0);
1326
- }
1327
-
1328
- float neutral(float value) {
1329
- float x = max(0.0, value);
1330
- return min(1.0, (x * (1.0 + x / 7.5)) / (1.0 + x));
1331
- }
1332
-
1333
- float toneMapChannel(float value) {
1334
- float exposed = max(0.0, value * u_exposure) / max(0.0001, u_whitePoint);
1335
- if (u_toneOperator == 0) return min(1.0, exposed);
1336
- if (u_toneOperator == 1) return exposed / (1.0 + exposed);
1337
- if (u_toneOperator == 2) return aces(exposed);
1338
- if (u_toneOperator == 3) return filmic(exposed);
1339
- if (u_toneOperator == 4) return uncharted2(exposed);
1340
- if (u_toneOperator == 5) return agx(exposed);
1341
- return neutral(exposed);
1342
- }
1343
-
1344
- vec3 applyToneMapping(vec3 color) {
1345
- if (u_hasToneMapping == 0) return color;
1346
- vec3 decoded = decodeColor(color);
1347
- vec3 mapped = vec3(toneMapChannel(decoded.r), toneMapChannel(decoded.g), toneMapChannel(decoded.b));
1348
- return encodeColor(mapped);
1349
- }
1350
-
1351
- vec3 applyColorGrade(vec3 color, vec2 uv) {
1352
- if (u_hasColorGrade == 0) return color;
1353
- float contrastOffset = 0.5 - 0.5 * u_contrast;
1354
- float redShift = u_temperature * 0.08 - u_tint * 0.02;
1355
- float greenShift = u_tint * 0.06;
1356
- float blueShift = -u_temperature * 0.08 - u_tint * 0.02;
1357
- float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
1358
- vec3 graded = color * u_contrast + vec3(contrastOffset + redShift, contrastOffset + greenShift, contrastOffset + blueShift);
1359
- float distanceFromLuma = min(1.0, abs(graded.r - luma) + abs(graded.g - luma) + abs(graded.b - luma));
1360
- float vibranceBoost = u_vibrance == 0.0 ? 0.0 : u_vibrance * (1.0 - distanceFromLuma);
1361
- float saturation = u_saturation + vibranceBoost;
1362
- graded = vec3(luma) + (graded - vec3(luma)) * saturation;
1363
- vec2 centered = uv * 2.0 - 1.0;
1364
- float vignette = 1.0 - u_vignette * clamp((length(centered) - 0.28) / 1.12, 0.0, 1.0);
1365
- return clamp(graded * vignette, 0.0, 1.0);
1366
- }
1367
-
1368
- vec3 baseColorAt(vec2 uv) {
1369
- vec2 clampedUv = clamp(uv, vec2(0.0), vec2(1.0));
1370
- vec3 color = texture(u_source, clampedUv).rgb;
1371
- return applyColorGrade(applyToneMapping(color), clampedUv);
1372
- }
1373
-
1374
- vec3 finalColorAt(vec2 uv) {
1375
- vec3 center = baseColorAt(uv);
1376
- if (u_hasColorGrade == 0 || u_sharpening <= 0.0) return center;
1377
- vec3 blur = (
1378
- baseColorAt(uv + vec2(-u_texelSize.x, 0.0)) +
1379
- baseColorAt(uv + vec2(u_texelSize.x, 0.0)) +
1380
- baseColorAt(uv + vec2(0.0, -u_texelSize.y)) +
1381
- baseColorAt(uv + vec2(0.0, u_texelSize.y))
1382
- ) * 0.25;
1383
- return clamp(center + (center - blur) * u_sharpening, 0.0, 1.0);
1384
- }
1385
-
1386
- float luma(vec3 color) {
1387
- return dot(color, vec3(0.2126, 0.7152, 0.0722));
1388
- }
1389
-
1390
- void main() {
1391
- vec3 center = finalColorAt(v_uv);
1392
- float alpha = texture(u_source, clamp(v_uv, vec2(0.0), vec2(1.0))).a;
1393
- if (u_hasFxaa == 0) {
1394
- outColor = vec4(center, alpha);
1395
- return;
1396
- }
1397
- vec3 north = finalColorAt(v_uv + vec2(0.0, -u_texelSize.y));
1398
- vec3 south = finalColorAt(v_uv + vec2(0.0, u_texelSize.y));
1399
- vec3 west = finalColorAt(v_uv + vec2(-u_texelSize.x, 0.0));
1400
- vec3 east = finalColorAt(v_uv + vec2(u_texelSize.x, 0.0));
1401
- float centerLuma = luma(center);
1402
- float minLuma = min(centerLuma, min(min(luma(north), luma(south)), min(luma(west), luma(east))));
1403
- float maxLuma = max(centerLuma, max(max(luma(north), luma(south)), max(luma(west), luma(east))));
1404
- if (maxLuma - minLuma < u_edgeThreshold) {
1405
- outColor = vec4(center, alpha);
1406
- return;
1407
- }
1408
- vec3 average = (north + south + west + east) * 0.25;
1409
- outColor = vec4(mix(center, average, clamp(u_subpixelBlend, 0.0, 1.0)), alpha);
1410
- }
1411
- `, "webgl2-ldr-postprocess");
1412
- const program = this.gl.createProgram();
1413
- if (!program) {
1414
- this.gl.deleteShader(vertex);
1415
- this.gl.deleteShader(fragment);
1416
- throw new RenderDeviceError("Failed to allocate WebGL2 LDR postprocess shader", "WEBGL_ALLOCATION_FAILED");
1417
- }
1418
- this.gl.attachShader(program, vertex);
1419
- this.gl.attachShader(program, fragment);
1420
- this.gl.linkProgram(program);
1421
- this.gl.deleteShader(vertex);
1422
- this.gl.deleteShader(fragment);
1423
- if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
1424
- const log = this.gl.getProgramInfoLog(program) ?? "Unknown LDR postprocess shader link error";
1425
- this.gl.deleteProgram(program);
1426
- throw new RenderDeviceError("WebGL2 LDR postprocess shader link failed", "SHADER_LINK_FAILED", { log });
1427
- }
1428
- this.ldrPostprocessProgram = program;
1429
- return program;
1430
- }
1431
-
1432
- private compileShader(type: GLenum, source: string, label: string): WebGLShader {
1433
- const shader = this.gl.createShader(type);
1434
- if (!shader) {
1435
- throw new RenderDeviceError("Failed to allocate WebGL shader", "WEBGL_ALLOCATION_FAILED", { label });
1436
- }
1437
- this.gl.shaderSource(shader, source);
1438
- this.gl.compileShader(shader);
1439
- if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
1440
- const log = this.gl.getShaderInfoLog(shader) ?? "Unknown shader compile error";
1441
- this.gl.deleteShader(shader);
1442
- throw new RenderDeviceError("WebGL shader compile failed", "SHADER_COMPILE_FAILED", { label, log });
1443
- }
1444
- return shader;
1445
- }
1446
-
1447
- private reflectProgram(program: WebGLProgram): ShaderReflection {
1448
- const attributes = new Map<string, number>();
1449
- const uniforms = new Set<string>();
1450
- const attributeDetails = new Map<string, ShaderReflection["attributeDetails"] extends ReadonlyMap<string, infer T> ? T : never>();
1451
- const uniformDetails = new Map<string, ShaderReflection["uniformDetails"] extends ReadonlyMap<string, infer T> ? T : never>();
1452
- const activeAttributes = this.gl.getProgramParameter(program, this.gl.ACTIVE_ATTRIBUTES) as number;
1453
- const activeUniforms = this.gl.getProgramParameter(program, this.gl.ACTIVE_UNIFORMS) as number;
1454
-
1455
- for (let i = 0; i < activeAttributes; i += 1) {
1456
- const info = this.gl.getActiveAttrib(program, i);
1457
- if (info) {
1458
- const location = this.gl.getAttribLocation(program, info.name);
1459
- attributes.set(info.name, location);
1460
- attributeDetails.set(info.name, { name: info.name, type: String(info.type), location, source: "vertex", line: 0 });
1461
- }
1462
- }
1463
- for (let i = 0; i < activeUniforms; i += 1) {
1464
- const info = this.gl.getActiveUniform(program, i);
1465
- if (info) {
1466
- const name = info.name.replace(/\[0\]$/, "");
1467
- uniforms.add(name);
1468
- uniformDetails.set(name, { name, type: String(info.type), arraySize: info.size > 1 ? info.size : null, source: "fragment", line: 0 });
1469
- }
1470
- }
1471
- return { attributes, uniforms, attributeDetails, uniformDetails };
1472
- }
1473
-
1474
- private uploadUniforms(shader: WebGL2ShaderProgram, uniforms: ReadonlyMap<string, unknown>): void {
1475
- let textureUnit = 0;
1476
- for (const [name, value] of uniforms) {
1477
- if (!shader.reflection.uniforms.has(name)) {
1478
- continue;
1479
- }
1480
- const location = this.getUniformLocation(shader, name);
1481
- if (location === null) {
1482
- throw new RenderDeviceError("Material tried to bind a missing shader uniform", "MISSING_UNIFORM", { name });
1483
- }
1484
- if (value instanceof TextureBinding) {
1485
- this.uploadTextureUniform(location, value, textureUnit);
1486
- textureUnit += 1;
1487
- } else if (typeof value === "number") {
1488
- this.gl.uniform1f(location, value);
1489
- } else if (Array.isArray(value) || ArrayBuffer.isView(value)) {
1490
- const length = (value as ArrayLike<number>).length;
1491
- const floatData = value as Float32List;
1492
- if (length === 16 || (length > 16 && length % 16 === 0 && /(?:Matrix|Matrices)$/.test(name))) {
1493
- this.gl.uniformMatrix4fv(location, false, floatData);
1494
- } else if (length > 16 && length % 4 === 0) {
1495
- this.gl.uniform4fv(location, floatData);
1496
- } else if (length === 4) {
1497
- this.gl.uniform4fv(location, floatData);
1498
- } else if (length === 3) {
1499
- this.gl.uniform3fv(location, floatData);
1500
- } else if (length === 2) {
1501
- this.gl.uniform2fv(location, floatData);
1502
- } else {
1503
- throw new RenderDeviceError("Unsupported uniform array length", "UNSUPPORTED_UNIFORM", { name, length });
1504
- }
1505
- } else {
1506
- throw new RenderDeviceError("Unsupported uniform value", "UNSUPPORTED_UNIFORM", { name, valueType: typeof value });
1507
- }
1508
- }
1509
- }
1510
-
1511
- private getUniformLocation(shader: WebGL2ShaderProgram, name: string): WebGLUniformLocation | null {
1512
- let cache = this.uniformLocationCache.get(shader);
1513
- if (!cache) {
1514
- cache = new Map();
1515
- this.uniformLocationCache.set(shader, cache);
1516
- }
1517
- if (!cache.has(name)) {
1518
- this.uniformLocationLookupCount += 1;
1519
- cache.set(name, this.gl.getUniformLocation(shader.handle, name) ?? this.gl.getUniformLocation(shader.handle, `${name}[0]`));
1520
- }
1521
- return cache.get(name) ?? null;
1522
- }
1523
-
1524
- private uploadTextureUniform(location: WebGLUniformLocation, binding: TextureBinding, textureUnit: number): void {
1525
- const validation = binding.validate();
1526
- if (!validation.ok) {
1527
- throw new RenderDeviceError("Texture binding validation failed", "INVALID_TEXTURE_BINDING", {
1528
- diagnostics: validation.diagnostics,
1529
- name: binding.name
1530
- });
1531
- }
1532
- this.activateTextureUnit(textureUnit);
1533
- const dimension = binding.texture?.dimension ?? (binding.name.toLowerCase().includes("cubemap") || binding.name.toLowerCase().includes("cube") ? "cube" : "2d");
1534
- const target = dimension === "cube" ? this.gl.TEXTURE_CUBE_MAP : this.gl.TEXTURE_2D;
1535
- const handle = binding.texture ? this.getTextureHandle(binding.texture) : dimension === "cube" ? this.getFallbackCubeTextureHandle() : this.getFallbackTextureHandle();
1536
- if (binding.texture) {
1537
- const lowerName = binding.name.toLowerCase();
1538
- if (lowerName.includes("environment")) this.nativeEnvironmentBindings += 1;
1539
- if (lowerName.includes("shadow")) this.nativeShadowMapBindings += 1;
1540
- }
1541
- this.bindTextureForUnit(textureUnit, target, handle);
1542
- const samplerHandle = this.getSamplerHandle(binding.sampler, target);
1543
- this.stateCache.bindSampler(textureUnit, samplerHandle, () => this.gl.bindSampler(textureUnit, samplerHandle));
1544
- this.gl.uniform1i(location, textureUnit);
1545
- }
1546
-
1547
- private activateTextureUnit(textureUnit: number): void {
1548
- if (this.activeTextureUnitIndex === textureUnit) return;
1549
- this.stateCache.activeTexture(textureUnit, () => this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit));
1550
- this.activeTextureUnitIndex = textureUnit;
1551
- }
1552
-
1553
- private bindTextureForUnit(textureUnit: number, target: GLenum, handle: WebGLTexture): void {
1554
- const key = `${textureUnit}:${target}`;
1555
- if (this.textureUnitBindings.get(key) === handle) return;
1556
- this.stateCache.bindTexture(target, handle, () => this.gl.bindTexture(target, handle));
1557
- this.textureUnitBindings.set(key, handle);
1558
- this.textureBindCount += 1;
1559
- }
1560
-
1561
- private setTextureParameterIfNeeded(handle: WebGLTexture, target: GLenum, parameter: GLenum, value: GLenum | number): void {
1562
- let parameters = this.textureSamplerParameterCache.get(handle);
1563
- if (!parameters) {
1564
- parameters = new Map();
1565
- this.textureSamplerParameterCache.set(handle, parameters);
1566
- }
1567
- if (parameters.get(parameter) === value) return;
1568
- this.gl.texParameteri(target, parameter, value);
1569
- parameters.set(parameter, value);
1570
- this.samplerParameterUploadCount += 1;
1571
- }
1572
-
1573
- private applySamplerAnisotropy(maxAnisotropy: number, target: GLenum = this.gl.TEXTURE_2D): void {
1574
- if (maxAnisotropy <= 1) return;
1575
- if (!this.anisotropicFilteringExtension) return;
1576
- this.gl.texParameterf(
1577
- target,
1578
- this.anisotropicFilteringExtension.TEXTURE_MAX_ANISOTROPY_EXT,
1579
- Math.min(Math.max(1, maxAnisotropy), this.maxTextureAnisotropy)
1580
- );
1581
- this.samplerAnisotropyUploadCount += 1;
1582
- }
1583
-
1584
- private getSamplerHandle(sampler: Sampler, target: GLenum): WebGLSampler {
1585
- const key = this.samplerKey(sampler, target);
1586
- const cached = this.samplerObjectCache.get(key);
1587
- if (cached) return cached;
1588
- const handle = this.gl.createSampler();
1589
- if (!handle) {
1590
- throw new RenderDeviceError("Failed to allocate WebGL sampler", "WEBGL_ALLOCATION_FAILED");
1591
- }
1592
- this.gl.samplerParameteri(handle, this.gl.TEXTURE_MIN_FILTER, this.minFilter(sampler.minFilter));
1593
- this.gl.samplerParameteri(handle, this.gl.TEXTURE_MAG_FILTER, this.magFilter(sampler.magFilter));
1594
- this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_S, this.addressMode(sampler.addressU));
1595
- this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_T, this.addressMode(sampler.addressV));
1596
- this.samplerParameterUploadCount += 4;
1597
- if (target === this.gl.TEXTURE_CUBE_MAP) {
1598
- this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_R, this.addressMode(sampler.addressV));
1599
- this.samplerParameterUploadCount += 1;
1600
- }
1601
- if (sampler.maxAnisotropy > 1 && this.anisotropicFilteringExtension) {
1602
- this.gl.samplerParameterf(
1603
- handle,
1604
- this.anisotropicFilteringExtension.TEXTURE_MAX_ANISOTROPY_EXT,
1605
- Math.min(Math.max(1, sampler.maxAnisotropy), this.maxTextureAnisotropy)
1606
- );
1607
- this.samplerAnisotropyUploadCount += 1;
1608
- }
1609
- this.samplerObjectCache.set(key, handle);
1610
- return handle;
1611
- }
1612
-
1613
- private samplerKey(sampler: Sampler, target: GLenum): string {
1614
- return [
1615
- sampler.minFilter,
1616
- sampler.magFilter,
1617
- sampler.addressU,
1618
- sampler.addressV,
1619
- sampler.maxAnisotropy,
1620
- target === this.gl.TEXTURE_CUBE_MAP ? "cube" : "2d"
1621
- ].join("|");
1622
- }
1623
-
1624
- private getFallbackTextureHandle(): WebGLTexture {
1625
- if (this.fallbackTexture) {
1626
- return this.fallbackTexture;
1627
- }
1628
- const handle = this.gl.createTexture();
1629
- if (!handle) {
1630
- throw new RenderDeviceError("Failed to allocate WebGL fallback texture", "WEBGL_ALLOCATION_FAILED");
1631
- }
1632
- this.stateCache.invalidate();
1633
- this.gl.bindTexture(this.gl.TEXTURE_2D, handle);
1634
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
1635
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
1636
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
1637
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
1638
- 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]));
1639
- this.fallbackTexture = handle;
1640
- return handle;
1641
- }
1642
-
1643
- private getFallbackCubeTextureHandle(): WebGLTexture {
1644
- if (this.fallbackCubeTexture) {
1645
- return this.fallbackCubeTexture;
1646
- }
1647
- const handle = this.gl.createTexture();
1648
- if (!handle) {
1649
- throw new RenderDeviceError("Failed to allocate WebGL fallback cube texture", "WEBGL_ALLOCATION_FAILED");
1650
- }
1651
- this.stateCache.invalidate();
1652
- this.gl.bindTexture(this.gl.TEXTURE_CUBE_MAP, handle);
1653
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
1654
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
1655
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
1656
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
1657
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_R, this.gl.CLAMP_TO_EDGE);
1658
- const pixel = new Uint8Array([255, 255, 255, 255]);
1659
- for (const face of WEBGL_CUBE_FACES) {
1660
- this.gl.texImage2D(this.cubeFaceTarget(face), 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixel);
1661
- }
1662
- this.fallbackCubeTexture = handle;
1663
- return handle;
1664
- }
1665
-
1666
- private getTextureHandle(texture: Texture): WebGLTexture {
1667
- this.releaseDisposedTextureHandles();
1668
- if (texture.disposed) {
1669
- throw new RenderDeviceError("Texture is disposed", "DISPOSED_RESOURCE", { label: texture.label });
1670
- }
1671
- const cached = this.textures.get(texture);
1672
- if (cached) {
1673
- return cached;
1674
- }
1675
- const handle = this.gl.createTexture();
1676
- if (!handle) {
1677
- throw new RenderDeviceError("Failed to allocate WebGL texture", "WEBGL_ALLOCATION_FAILED", { label: texture.label });
1678
- }
1679
- this.stateCache.invalidate();
1680
- this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false);
1681
- this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
1682
- this.gl.pixelStorei(this.gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, this.gl.NONE);
1683
- if (texture.dimension === "cube") {
1684
- this.uploadCubeTexture(texture, handle);
1685
- this.textureUploadModes.set(texture, "cube");
1686
- this.textures.set(texture, handle);
1687
- return handle;
1688
- }
1689
- this.gl.bindTexture(this.gl.TEXTURE_2D, handle);
1690
- if (isCompressedTextureFormat(texture.format)) {
1691
- const compressed = this.resolveCompressedTextureFormat(texture.format);
1692
- if (compressed) {
1693
- const uploadLevels = completeUploadLevels(texture.textureLevels);
1694
- for (const [levelIndex, level] of uploadLevels.entries()) {
1695
- this.gl.compressedTexImage2D(this.gl.TEXTURE_2D, levelIndex, compressed.internalFormat, level.width, level.height, 0, level.data);
1696
- }
1697
- const compressedUploadError = this.readError();
1698
- if (!compressedUploadError) {
1699
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
1700
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, uploadLevels.length - 1);
1701
- this.textureUploadModes.set(texture, "compressed");
1702
- this.textures.set(texture, handle);
1703
- return handle;
1704
- }
1705
- }
1706
- const fallbackLevels = completeUploadLevels(texture.fallbackTextureLevels);
1707
- if (fallbackLevels.length === 0) {
1708
- this.gl.deleteTexture(handle);
1709
- throw new RenderDeviceError("Compressed texture format is not supported and no RGBA8 fallback data was provided", "COMPRESSED_TEXTURE_UNSUPPORTED", {
1710
- label: texture.label,
1711
- format: texture.format
1712
- });
1713
- }
1714
- this.uploadRgba8FallbackTexture(texture, fallbackLevels);
1715
- this.textureUploadModes.set(texture, "fallback");
1716
- this.textures.set(texture, handle);
1717
- return handle;
1718
- }
1719
- if (texture.format === "depth24") {
1720
- throw new RenderDeviceError("Depth textures cannot be uploaded to WebGL2 color samplers", "UNSUPPORTED_TEXTURE_FORMAT", {
1721
- label: texture.label,
1722
- format: texture.format
1723
- });
1724
- }
1725
- if (texture.source) {
1726
- this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.rgba8TextureInternalFormat(texture), this.gl.RGBA, this.gl.UNSIGNED_BYTE, texture.source);
1727
- this.gl.generateMipmap(this.gl.TEXTURE_2D);
1728
- } else if (texture.mipLevels.length > 0) {
1729
- const uploadFormat = this.textureUploadFormat(texture);
1730
- const uploadLevels = completeUploadLevels(texture.textureLevels);
1731
- for (const [levelIndex, level] of uploadLevels.entries()) {
1732
- this.gl.texImage2D(
1733
- this.gl.TEXTURE_2D,
1734
- levelIndex,
1735
- uploadFormat.internalFormat,
1736
- level.width,
1737
- level.height,
1738
- 0,
1739
- uploadFormat.format,
1740
- uploadFormat.type,
1741
- texturePixelUploadData(level.data, texture.format)
1742
- );
1743
- }
1744
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
1745
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, uploadLevels.length - 1);
1746
- } else {
1747
- const uploadFormat = this.textureUploadFormat(texture);
1748
- this.gl.texImage2D(
1749
- this.gl.TEXTURE_2D,
1750
- 0,
1751
- uploadFormat.internalFormat,
1752
- texture.width,
1753
- texture.height,
1754
- 0,
1755
- uploadFormat.format,
1756
- uploadFormat.type,
1757
- texture.data ? texturePixelUploadData(texture.data, texture.format) : null
1758
- );
1759
- this.gl.generateMipmap(this.gl.TEXTURE_2D);
1760
- }
1761
- this.textureUploadModes.set(texture, texture.format);
1762
- this.textures.set(texture, handle);
1763
- return handle;
1764
- }
1765
-
1766
- private uploadCubeTexture(texture: Texture, handle: WebGLTexture): void {
1767
- this.gl.bindTexture(this.gl.TEXTURE_CUBE_MAP, handle);
1768
- const uploadFormat = this.textureUploadFormat(texture);
1769
- for (const face of texture.cubeFaces) {
1770
- const uploadLevels = completeUploadLevels(face.mipLevels);
1771
- for (const [levelIndex, level] of uploadLevels.entries()) {
1772
- this.gl.texImage2D(
1773
- this.cubeFaceTarget(face.face),
1774
- levelIndex,
1775
- uploadFormat.internalFormat,
1776
- level.width,
1777
- level.height,
1778
- 0,
1779
- uploadFormat.format,
1780
- uploadFormat.type,
1781
- texturePixelUploadData(level.data, texture.format)
1782
- );
1783
- }
1784
- }
1785
- const firstFaceLevels = completeUploadLevels(texture.cubeFaces[0]?.mipLevels ?? []);
1786
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_BASE_LEVEL, 0);
1787
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MAX_LEVEL, Math.max(0, firstFaceLevels.length - 1));
1788
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
1789
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
1790
- this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_R, this.gl.CLAMP_TO_EDGE);
1791
- }
1792
-
1793
- private uploadRgba8FallbackTexture(texture: Texture, fallbackLevels: readonly { readonly width: number; readonly height: number; readonly data: TexturePixelData }[]): void {
1794
- const internalFormat = this.rgba8TextureInternalFormat(texture);
1795
- for (const [levelIndex, level] of fallbackLevels.entries()) {
1796
- this.gl.texImage2D(
1797
- this.gl.TEXTURE_2D,
1798
- levelIndex,
1799
- internalFormat,
1800
- level.width,
1801
- level.height,
1802
- 0,
1803
- this.gl.RGBA,
1804
- this.gl.UNSIGNED_BYTE,
1805
- texturePixelUploadData(level.data, "rgba8")
1806
- );
1807
- }
1808
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
1809
- this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, fallbackLevels.length - 1);
1810
- }
1811
-
1812
- private cubeFaceTarget(face: TextureCubeFace): GLenum {
1813
- switch (face) {
1814
- case "px": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_X;
1815
- case "nx": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_X;
1816
- case "py": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_Y;
1817
- case "ny": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_Y;
1818
- case "pz": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_Z;
1819
- case "nz": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_Z;
1820
- }
1821
- }
1822
-
1823
- private rgba8TextureInternalFormat(texture: Texture): GLenum {
1824
- return texture.colorSpace === "srgb" ? this.gl.SRGB8_ALPHA8 : this.gl.RGBA;
1825
- }
1826
-
1827
- private textureUploadFormat(texture: Texture): { readonly internalFormat: GLenum; readonly format: GLenum; readonly type: GLenum } {
1828
- if (texture.format === "rgba8") {
1829
- return { internalFormat: this.rgba8TextureInternalFormat(texture), format: this.gl.RGBA, type: this.gl.UNSIGNED_BYTE };
1830
- }
1831
- if (texture.colorSpace === "srgb") {
1832
- throw new RenderDeviceError("Floating-point texture uploads must use linear colorSpace", "UNSUPPORTED_TEXTURE_FORMAT", {
1833
- label: texture.label,
1834
- format: texture.format,
1835
- colorSpace: texture.colorSpace
1836
- });
1837
- }
1838
- if (texture.format === "rgba16f") {
1839
- return { internalFormat: this.gl.RGBA16F, format: this.gl.RGBA, type: this.gl.HALF_FLOAT };
1840
- }
1841
- if (texture.format === "rgba32f") {
1842
- return { internalFormat: this.gl.RGBA32F, format: this.gl.RGBA, type: this.gl.FLOAT };
1843
- }
1844
- throw new RenderDeviceError("Unsupported texture upload format", "UNSUPPORTED_TEXTURE_FORMAT", {
1845
- label: texture.label,
1846
- format: texture.format
1847
- });
1848
- }
1849
-
1850
- private resolveRenderTargetFormat(format: "rgba8" | "rgba16f" | "rgba32f"): { readonly internalFormat: GLenum; readonly type: GLenum } {
1851
- if (format === "rgba8") {
1852
- return { internalFormat: this.gl.RGBA, type: this.gl.UNSIGNED_BYTE };
1853
- }
1854
- if (!this.gl.getExtension("EXT_color_buffer_float")) {
1855
- throw new RenderDeviceError("Floating-point color render targets require EXT_color_buffer_float", "HDR_RENDER_TARGET_UNSUPPORTED", { format });
1856
- }
1857
- if (format === "rgba16f") {
1858
- return { internalFormat: this.gl.RGBA16F, type: this.gl.HALF_FLOAT };
1859
- }
1860
- return { internalFormat: this.gl.RGBA32F, type: this.gl.FLOAT };
1861
- }
1862
-
1863
- private releaseDisposedTextureHandles(): void {
1864
- for (const [texture, handle] of [...this.textures]) {
1865
- if (texture.disposed) {
1866
- this.gl.deleteTexture(handle);
1867
- this.textures.delete(texture);
1868
- this.textureUploadModes.delete(texture);
1869
- this.releasedTextureHandles += 1;
1870
- }
1871
- }
1872
- }
1873
-
1874
- private resolveCompressedTextureFormat(format: TextureCompressedFormat): { readonly internalFormat: GLenum } | null {
1875
- switch (format) {
1876
- case "bc1-rgba-unorm": {
1877
- const extension = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
1878
- return extension ? { internalFormat: extension.COMPRESSED_RGBA_S3TC_DXT1_EXT } : null;
1879
- }
1880
- case "bc3-rgba-unorm": {
1881
- const extension = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
1882
- return extension ? { internalFormat: extension.COMPRESSED_RGBA_S3TC_DXT5_EXT } : null;
1883
- }
1884
- case "etc2-rgba8unorm":
1885
- return { internalFormat: 0x9278 };
1886
- case "astc-4x4-rgba-unorm": {
1887
- const extension = this.gl.getExtension("WEBGL_compressed_texture_astc");
1888
- return extension ? { internalFormat: extension.COMPRESSED_RGBA_ASTC_4x4_KHR } : null;
1889
- }
1890
- }
1891
- }
1892
-
1893
- private magFilter(filter: TextureMagFilter): GLenum {
1894
- return filter === "nearest" ? this.gl.NEAREST : this.gl.LINEAR;
1895
- }
1896
-
1897
- private minFilter(filter: TextureMinFilter): GLenum {
1898
- switch (filter) {
1899
- case "nearest":
1900
- return this.gl.NEAREST;
1901
- case "linear":
1902
- return this.gl.LINEAR;
1903
- case "nearest-mipmap-nearest":
1904
- return this.gl.NEAREST_MIPMAP_NEAREST;
1905
- case "linear-mipmap-nearest":
1906
- return this.gl.LINEAR_MIPMAP_NEAREST;
1907
- case "nearest-mipmap-linear":
1908
- return this.gl.NEAREST_MIPMAP_LINEAR;
1909
- case "linear-mipmap-linear":
1910
- return this.gl.LINEAR_MIPMAP_LINEAR;
1911
- }
1912
- }
1913
-
1914
- private addressMode(mode: "clamp-to-edge" | "repeat" | "mirror-repeat"): GLenum {
1915
- if (mode === "repeat") return this.gl.REPEAT;
1916
- if (mode === "mirror-repeat") return this.gl.MIRRORED_REPEAT;
1917
- return this.gl.CLAMP_TO_EDGE;
1918
- }
1919
-
1920
- private bindVertexFormat(shader: RenderShaderProgram, format: VertexFormat): Set<number> {
1921
- const boundLocations = new Set<number>();
1922
- for (const attribute of format.attributes) {
1923
- const location = this.resolveAttributeLocation(shader, attribute);
1924
- if (location < 0) {
1925
- continue;
1926
- }
1927
- if (attribute.type !== "float32") {
1928
- throw new RenderDeviceError("Unsupported vertex attribute type", "UNSUPPORTED_VERTEX_ATTRIBUTE", {
1929
- attribute: attribute.shaderName,
1930
- type: attribute.type
1931
- });
1932
- }
1933
- boundLocations.add(location);
1934
- this.gl.enableVertexAttribArray(location);
1935
- this.gl.vertexAttribDivisor(location, 0);
1936
- this.gl.vertexAttribPointer(
1937
- location,
1938
- attribute.components,
1939
- this.gl.FLOAT,
1940
- attribute.normalized,
1941
- format.stride,
1942
- attribute.offset
1943
- );
1944
- }
1945
- return boundLocations;
1946
- }
1947
-
1948
- private bindVertexArrayForCommand(command: DrawCommand, shader: WebGL2ShaderProgram, vertexBuffer: WebGL2Buffer): WebGL2VertexArrayCacheEntry {
1949
- const indexBuffer = command.indexBuffer ? this.requireBuffer(command.indexBuffer) : undefined;
1950
- const key = this.vertexArrayCacheKey(command, shader, vertexBuffer, indexBuffer);
1951
- const cached = this.vertexArrayCache.get(key);
1952
- if (cached) {
1953
- this.stateCache.bindVertexArray(cached.handle, () => this.gl.bindVertexArray(cached.handle));
1954
- return cached;
1955
- }
1956
-
1957
- const handle = this.gl.createVertexArray();
1958
- if (!handle) {
1959
- throw new RenderDeviceError("Failed to allocate WebGL vertex array", "WEBGL_ALLOCATION_FAILED");
1960
- }
1961
- this.vertexArrayCreateCount += 1;
1962
- this.stateCache.bindVertexArray(handle, () => this.gl.bindVertexArray(handle));
1963
- this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle));
1964
- const boundLocations = this.bindVertexFormat(shader, command.vertexFormat!);
1965
- if (command.instanceAttributes && command.instanceAttributes.length > 0) {
1966
- this.bindInstanceAttributes(shader, command.instanceAttributes, boundLocations);
1967
- }
1968
- if (indexBuffer) {
1969
- this.stateCache.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle, () => this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle));
1970
- }
1971
- this.disableUnboundVertexAttributes(boundLocations);
1972
- this.applyDefaultAttributes(shader, boundLocations);
1973
- const entry: WebGL2VertexArrayCacheEntry = { key, handle, boundLocations };
1974
- this.vertexArrayCache.set(key, entry);
1975
- return entry;
1976
- }
1977
-
1978
- private bindNoVertexArray(): void {
1979
- this.stateCache.bindVertexArray(null, () => this.gl.bindVertexArray(null));
1980
- }
1981
-
1982
- private vertexArrayCacheKey(command: DrawCommand, shader: WebGL2ShaderProgram, vertexBuffer: WebGL2Buffer, indexBuffer: WebGL2Buffer | undefined): string {
1983
- const instanceKey = (command.instanceAttributes ?? []).map((attribute) => {
1984
- const buffer = this.requireBuffer(attribute.buffer);
1985
- return `${attribute.shaderName}:${buffer.id}:${attribute.components}:${attribute.offset}:${attribute.stride}:${attribute.normalized === true ? 1 : 0}:${attribute.divisor ?? 1}`;
1986
- }).join(",");
1987
- return [
1988
- shader.id,
1989
- vertexBuffer.id,
1990
- this.vertexFormatId(command.vertexFormat!),
1991
- indexBuffer?.id ?? 0,
1992
- instanceKey
1993
- ].join("|");
1994
- }
1995
-
1996
- private vertexFormatId(format: VertexFormat): number {
1997
- const existing = this.vertexFormatIds.get(format);
1998
- if (existing !== undefined) return existing;
1999
- const next = this.nextVertexFormatId;
2000
- this.nextVertexFormatId += 1;
2001
- this.vertexFormatIds.set(format, next);
2002
- return next;
2003
- }
2004
-
2005
- private bindInstanceAttributes(shader: RenderShaderProgram, attributes: readonly InstanceVertexAttribute[], boundLocations: Set<number>): void {
2006
- for (const attribute of attributes) {
2007
- const location = shader.reflection.attributes.get(attribute.shaderName);
2008
- if (location === undefined || location < 0) {
2009
- continue;
2010
- }
2011
- if (![1, 2, 3, 4].includes(attribute.components)) {
2012
- throw new RenderDeviceError("Instance vertex attribute components must be 1, 2, 3, or 4", "INVALID_DRAW_COMMAND", {
2013
- attribute: attribute.shaderName,
2014
- components: attribute.components
2015
- });
2016
- }
2017
- if (!Number.isInteger(attribute.offset) || attribute.offset < 0 || attribute.offset % 4 !== 0) {
2018
- throw new RenderDeviceError("Instance vertex attribute offset must be non-negative and 4-byte aligned", "INVALID_DRAW_COMMAND", {
2019
- attribute: attribute.shaderName,
2020
- offset: attribute.offset
2021
- });
2022
- }
2023
- if (!Number.isInteger(attribute.stride) || attribute.stride <= 0 || attribute.stride % 4 !== 0) {
2024
- throw new RenderDeviceError("Instance vertex attribute stride must be positive and 4-byte aligned", "INVALID_DRAW_COMMAND", {
2025
- attribute: attribute.shaderName,
2026
- stride: attribute.stride
2027
- });
2028
- }
2029
- if (attribute.offset + attribute.components * 4 > attribute.stride) {
2030
- throw new RenderDeviceError("Instance vertex attribute range must fit inside its stride", "INVALID_DRAW_COMMAND", {
2031
- attribute: attribute.shaderName,
2032
- offset: attribute.offset,
2033
- components: attribute.components,
2034
- stride: attribute.stride
2035
- });
2036
- }
2037
- const buffer = this.requireBuffer(attribute.buffer);
2038
- boundLocations.add(location);
2039
- this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, buffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer.handle));
2040
- this.gl.enableVertexAttribArray(location);
2041
- this.gl.vertexAttribPointer(
2042
- location,
2043
- attribute.components,
2044
- this.gl.FLOAT,
2045
- attribute.normalized ?? false,
2046
- attribute.stride,
2047
- attribute.offset
2048
- );
2049
- this.gl.vertexAttribDivisor(location, attribute.divisor ?? 1);
2050
- }
2051
- }
2052
-
2053
- private resolveAttributeLocation(shader: RenderShaderProgram, attribute: VertexAttribute): number {
2054
- const reflected = shader.reflection.attributes.get(attribute.shaderName);
2055
- if (reflected !== undefined) {
2056
- return reflected;
2057
- }
2058
- return shader.reflection.attributes.get(attribute.semantic) ?? -1;
2059
- }
2060
-
2061
- private applyDefaultAttributes(shader: RenderShaderProgram, boundLocations: ReadonlySet<number>): void {
2062
- const colorLocation = shader.reflection.attributes.get("a_color") ?? shader.reflection.attributes.get("color");
2063
- if (colorLocation !== undefined && colorLocation >= 0 && !boundLocations.has(colorLocation)) {
2064
- this.gl.disableVertexAttribArray(colorLocation);
2065
- this.gl.vertexAttribDivisor(colorLocation, 0);
2066
- this.gl.vertexAttrib4f(colorLocation, 1, 1, 1, 1);
2067
- }
2068
- const tangentLocation = shader.reflection.attributes.get("a_tangent") ?? shader.reflection.attributes.get("tangent");
2069
- if (tangentLocation !== undefined && tangentLocation >= 0 && !boundLocations.has(tangentLocation)) {
2070
- this.gl.disableVertexAttribArray(tangentLocation);
2071
- this.gl.vertexAttribDivisor(tangentLocation, 0);
2072
- this.gl.vertexAttrib4f(tangentLocation, 1, 0, 0, 1);
2073
- }
2074
- const uv1Location = shader.reflection.attributes.get("a_uv1") ?? shader.reflection.attributes.get("uv1");
2075
- if (uv1Location !== undefined && uv1Location >= 0 && !boundLocations.has(uv1Location)) {
2076
- this.gl.disableVertexAttribArray(uv1Location);
2077
- this.gl.vertexAttribDivisor(uv1Location, 0);
2078
- this.gl.vertexAttrib2f(uv1Location, 0, 0);
2079
- }
2080
- const instanceColorLocation = shader.reflection.attributes.get("a_instanceColor") ?? shader.reflection.attributes.get("instanceColor");
2081
- if (instanceColorLocation !== undefined && instanceColorLocation >= 0 && !boundLocations.has(instanceColorLocation)) {
2082
- this.gl.disableVertexAttribArray(instanceColorLocation);
2083
- this.gl.vertexAttribDivisor(instanceColorLocation, 0);
2084
- this.gl.vertexAttrib4f(instanceColorLocation, 1, 1, 1, 1);
2085
- }
2086
- }
2087
-
2088
- private disableUnboundVertexAttributes(boundLocations: ReadonlySet<number>): void {
2089
- for (let location = 0; location < this.maxVertexAttributes; location += 1) {
2090
- if (!boundLocations.has(location)) {
2091
- this.gl.disableVertexAttribArray(location);
2092
- this.gl.vertexAttribDivisor(location, 0);
2093
- }
2094
- }
2095
- }
2096
-
2097
- private primitive(topology: DrawCommand["topology"]): GLenum {
2098
- if (topology === "lines") return this.gl.LINES;
2099
- if (topology === "points") return this.gl.POINTS;
2100
- return this.gl.TRIANGLES;
2101
- }
2102
-
2103
- private applyRenderState(state: DrawCommand["renderState"]): void {
2104
- const renderState = state ?? {
2105
- depthTest: true,
2106
- depthWrite: true,
2107
- cullMode: "back" as const,
2108
- blend: false,
2109
- depthCompare: "less-equal" as const,
2110
- colorWrite: [true, true, true, true] as const,
2111
- scissor: null,
2112
- polygonOffset: null,
2113
- stencil: null
2114
- };
2115
- this.stateCache.setEnabled(this.gl.DEPTH_TEST, renderState.depthTest, () => {
2116
- if (renderState.depthTest) this.gl.enable(this.gl.DEPTH_TEST);
2117
- else this.gl.disable(this.gl.DEPTH_TEST);
2118
- });
2119
- this.stateCache.depthMask(renderState.depthWrite, () => this.gl.depthMask(renderState.depthWrite));
2120
- this.stateCache.depthFunc(renderState.depthCompare === "always" ? this.gl.ALWAYS : this.gl.LEQUAL, () => this.gl.depthFunc(renderState.depthCompare === "always" ? this.gl.ALWAYS : this.gl.LEQUAL));
2121
- const colorWrite = renderState.colorWrite ?? [true, true, true, true] as const;
2122
- this.stateCache.colorMask(colorWrite[0], colorWrite[1], colorWrite[2], colorWrite[3], () => this.gl.colorMask(colorWrite[0], colorWrite[1], colorWrite[2], colorWrite[3]));
2123
- if (renderState.scissor) {
2124
- this.stateCache.setEnabled(this.gl.SCISSOR_TEST, true, () => this.gl.enable(this.gl.SCISSOR_TEST));
2125
- this.stateCache.scissor(renderState.scissor.x, renderState.scissor.y, renderState.scissor.width, renderState.scissor.height, () => {
2126
- this.gl.scissor(renderState.scissor!.x, renderState.scissor!.y, renderState.scissor!.width, renderState.scissor!.height);
2127
- });
2128
- } else {
2129
- this.stateCache.setEnabled(this.gl.SCISSOR_TEST, false, () => this.gl.disable(this.gl.SCISSOR_TEST));
2130
- }
2131
- if (renderState.polygonOffset) {
2132
- this.stateCache.setEnabled(this.gl.POLYGON_OFFSET_FILL, true, () => this.gl.enable(this.gl.POLYGON_OFFSET_FILL));
2133
- this.stateCache.polygonOffset(renderState.polygonOffset.factor, renderState.polygonOffset.units, () => this.gl.polygonOffset(renderState.polygonOffset!.factor, renderState.polygonOffset!.units));
2134
- } else {
2135
- this.stateCache.setEnabled(this.gl.POLYGON_OFFSET_FILL, false, () => this.gl.disable(this.gl.POLYGON_OFFSET_FILL));
2136
- }
2137
- if (renderState.stencil) {
2138
- const stencil = renderState.stencil;
2139
- const compare = this.stencilCompare(stencil.compare ?? "always");
2140
- const reference = stencil.reference ?? 0;
2141
- const readMask = stencil.readMask ?? 0xff;
2142
- const writeMask = stencil.writeMask ?? 0xff;
2143
- const fail = this.stencilOperation(stencil.fail ?? "keep");
2144
- const depthFail = this.stencilOperation(stencil.depthFail ?? "keep");
2145
- const depthPass = this.stencilOperation(stencil.depthPass ?? "keep");
2146
- this.stateCache.setEnabled(this.gl.STENCIL_TEST, true, () => this.gl.enable(this.gl.STENCIL_TEST));
2147
- this.stateCache.stencilFunc(compare, reference, readMask, () => this.gl.stencilFunc(compare, reference, readMask));
2148
- this.stateCache.stencilMask(writeMask, () => this.gl.stencilMask(writeMask));
2149
- this.stateCache.stencilOp(fail, depthFail, depthPass, () => this.gl.stencilOp(fail, depthFail, depthPass));
2150
- } else {
2151
- this.stateCache.setEnabled(this.gl.STENCIL_TEST, false, () => this.gl.disable(this.gl.STENCIL_TEST));
2152
- }
2153
- if (renderState.cullMode === "none") {
2154
- this.stateCache.setEnabled(this.gl.CULL_FACE, false, () => this.gl.disable(this.gl.CULL_FACE));
2155
- } else {
2156
- this.stateCache.setEnabled(this.gl.CULL_FACE, true, () => this.gl.enable(this.gl.CULL_FACE));
2157
- this.stateCache.cullFace(renderState.cullMode === "front" ? this.gl.FRONT : this.gl.BACK, () => this.gl.cullFace(renderState.cullMode === "front" ? this.gl.FRONT : this.gl.BACK));
2158
- }
2159
- if (renderState.blend) {
2160
- this.stateCache.setEnabled(this.gl.BLEND, true, () => this.gl.enable(this.gl.BLEND));
2161
- 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));
2162
- } else {
2163
- this.stateCache.setEnabled(this.gl.BLEND, false, () => this.gl.disable(this.gl.BLEND));
2164
- }
2165
- }
2166
-
2167
- private requireBuffer(buffer: RenderBuffer): WebGL2Buffer {
2168
- if (!(buffer instanceof WebGL2Buffer) || !this.buffers.has(buffer) || buffer.disposed) {
2169
- throw new RenderDeviceError("Buffer is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
2170
- bufferId: buffer.id
2171
- });
2172
- }
2173
- return buffer;
2174
- }
2175
-
2176
- private stencilCompare(compare: NonNullable<DrawCommand["renderState"]>["stencil"] extends infer Stencil ? Stencil extends { readonly compare?: infer Compare } ? NonNullable<Compare> : never : never): GLenum {
2177
- switch (compare) {
2178
- case "never": return this.gl.NEVER;
2179
- case "less": return this.gl.LESS;
2180
- case "less-equal": return this.gl.LEQUAL;
2181
- case "greater": return this.gl.GREATER;
2182
- case "greater-equal": return this.gl.GEQUAL;
2183
- case "equal": return this.gl.EQUAL;
2184
- case "not-equal": return this.gl.NOTEQUAL;
2185
- case "always": return this.gl.ALWAYS;
2186
- }
2187
- }
2188
-
2189
- private stencilOperation(operation: NonNullable<DrawCommand["renderState"]>["stencil"] extends infer Stencil ? Stencil extends { readonly fail?: infer Operation } ? NonNullable<Operation> : never : never): GLenum {
2190
- switch (operation) {
2191
- case "keep": return this.gl.KEEP;
2192
- case "zero": return this.gl.ZERO;
2193
- case "replace": return this.gl.REPLACE;
2194
- case "increment": return this.gl.INCR;
2195
- case "decrement": return this.gl.DECR;
2196
- case "invert": return this.gl.INVERT;
2197
- case "increment-wrap": return this.gl.INCR_WRAP;
2198
- case "decrement-wrap": return this.gl.DECR_WRAP;
2199
- }
2200
- }
2201
-
2202
- private requireShader(shader: RenderShaderProgram): WebGL2ShaderProgram {
2203
- if (!(shader instanceof WebGL2ShaderProgram) || !this.shaders.has(shader) || shader.disposed) {
2204
- throw new RenderDeviceError("Shader is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
2205
- shaderId: shader.id
2206
- });
2207
- }
2208
- return shader;
2209
- }
2210
-
2211
- private assertAlive(): void {
2212
- if (this.disposed) {
2213
- throw new RenderDeviceError("Render device is disposed", "DISPOSED_DEVICE");
2214
- }
2215
- if (this.contextLost) {
2216
- throw new RenderDeviceError("Render context is lost", "CONTEXT_LOST");
2217
- }
2218
- }
2219
-
2220
- private assertFrame(): void {
2221
- this.assertAlive();
2222
- if (!this.frameActive) {
2223
- throw new RenderDeviceError("No active frame", "NO_ACTIVE_FRAME");
2224
- }
2225
- }
2226
-
2227
- private readError(): string | null {
2228
- const error = this.gl.getError();
2229
- return error === this.gl.NO_ERROR ? null : `0x${error.toString(16)}`;
2230
- }
2231
- }
2232
-
2233
- function numberOption(options: Readonly<Record<string, unknown>>, key: string, fallback: number): number {
2234
- const value = options[key];
2235
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
2236
- }
2237
-
2238
- function stringOption(options: Readonly<Record<string, unknown>>, key: string, fallback: string): string {
2239
- const value = options[key];
2240
- return typeof value === "string" ? value : fallback;
2241
- }
2242
-
2243
- function colorSpaceId(value: string): number {
2244
- return value === "srgb" ? 1 : 0;
2245
- }
2246
-
2247
- function toneMappingOperatorId(value: string): number {
2248
- if (value === "linear") return 0;
2249
- if (value === "reinhard") return 1;
2250
- if (value === "aces") return 2;
2251
- if (value === "filmic") return 3;
2252
- if (value === "uncharted2") return 4;
2253
- if (value === "agx") return 5;
2254
- return 6;
2255
- }
2256
-
2257
- function completeUploadLevels<T extends { readonly width: number; readonly height: number }>(levels: readonly T[]): readonly T[] {
2258
- const uploadLevels: T[] = [];
2259
- for (const level of levels) {
2260
- if (uploadLevels.length === 0) {
2261
- uploadLevels.push(level);
2262
- continue;
2263
- }
2264
- const previous = uploadLevels[uploadLevels.length - 1]!;
2265
- const expectedWidth = Math.max(1, Math.floor(previous.width / 2));
2266
- const expectedHeight = Math.max(1, Math.floor(previous.height / 2));
2267
- if (level.width !== expectedWidth || level.height !== expectedHeight) {
2268
- break;
2269
- }
2270
- uploadLevels.push(level);
2271
- }
2272
- return uploadLevels;
2273
- }
2274
-
2275
- function texturePixelUploadData(data: TexturePixelData, format: Texture["format"]): ArrayBufferView {
2276
- if (format === "rgba16f" && data instanceof Uint16Array) return data;
2277
- if (format === "rgba32f" && data instanceof Float32Array) return data;
2278
- if (format === "rgba8" && (data instanceof Uint8Array || data instanceof Uint8ClampedArray)) return data;
2279
- throw new RenderDeviceError("Texture pixel data type does not match texture format", "UNSUPPORTED_TEXTURE_FORMAT", { format });
2280
- }