@driftengine/ui2d 4.8.1 → 4.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The 2D layer: quads with a texture on them, batched, in the order you drew them.
4
4
 
5
- **Cost: 8.8 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it
5
+ **Cost: 10.1 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it
6
6
  drifts more than 3% — the number is derived from the same floors that gate asserts, so a README
7
7
  quoting a stale one is a red suite rather than a thing somebody notices.
8
8
 
@@ -1,4 +1,4 @@
1
- export declare const SPRITE_FRAG_WGSL = "struct Uniforms {\n uOutputTransform: i32,\n uOutputExposure: f32,\n}\n\n@group(0) @binding(1) \nvar<uniform> unnamed: Uniforms;\n@group(0) @binding(32) \nvar uSpriteTexture_t: texture_2d<f32>;\n@group(0) @binding(33) \nvar uSpriteTexture_s: sampler;\nvar<private> vUv_1: vec2<f32>;\nvar<private> vTint_1: vec4<f32>;\nvar<private> fragColor: vec4<f32>;\n\nfn linearToSrgb_u0028_vf3_u003b(c: ptr<function, vec3<f32>>) -> vec3<f32> {\n var low: vec3<f32>;\n var high: vec3<f32>;\n\n let _e55 = (*c);\n low = (_e55 * 12.92f);\n let _e57 = (*c);\n high = ((pow(max(_e57, vec3<f32>(0f, 0f, 0f)), vec3<f32>(0.41666666f, 0.41666666f, 0.41666666f)) * 1.055f) - vec3(0.055f));\n let _e63 = high;\n let _e64 = low;\n let _e65 = (*c);\n return mix(_e63, _e64, step(_e65, vec3<f32>(0.0031308f, 0.0031308f, 0.0031308f)));\n}\n\nfn rrtAndOdtFit_u0028_vf3_u003b(v: ptr<function, vec3<f32>>) -> vec3<f32> {\n var a: vec3<f32>;\n var b: vec3<f32>;\n\n let _e55 = (*v);\n let _e56 = (*v);\n a = ((_e55 * (_e56 + vec3(0.0245786f))) - vec3(0.000090537f));\n let _e62 = (*v);\n let _e63 = (*v);\n b = ((_e62 * ((_e63 * 0.983729f) + vec3(0.432951f))) + vec3(0.238081f));\n let _e70 = a;\n let _e71 = b;\n return (_e70 / _e71);\n}\n\nfn acesFilmic_u0028_vf3_u003b(x: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param: vec3<f32>;\n\n let _e55 = unnamed.uOutputExposure;\n let _e56 = (*x);\n (*x) = (_e56 * _e55);\n let _e58 = (*x);\n param = (mat3x3<f32>(vec3<f32>(0.59719f, 0.076f, 0.0284f), vec3<f32>(0.35458f, 0.90834f, 0.13383f), vec3<f32>(0.04823f, 0.01566f, 0.83777f)) * _e58);\n let _e60 = rrtAndOdtFit_u0028_vf3_u003b((&param));\n return clamp((mat3x3<f32>(vec3<f32>(1.60475f, -0.10208f, -0.00327f), vec3<f32>(-0.53108f, 1.10813f, -0.07276f), vec3<f32>(-0.07367f, -0.00605f, 1.07602f)) * _e60), vec3(0f), vec3(1f));\n}\n\nfn applyOutputTransform_u0028_vf3_u003b(c_1: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param_1: vec3<f32>;\n var param_2: vec3<f32>;\n\n let _e56 = unnamed.uOutputTransform;\n if (_e56 == 0i) {\n let _e58 = (*c_1);\n return _e58;\n }\n let _e60 = unnamed.uOutputTransform;\n if (_e60 == 2i) {\n let _e62 = (*c_1);\n param_1 = _e62;\n let _e63 = acesFilmic_u0028_vf3_u003b((&param_1));\n (*c_1) = _e63;\n }\n let _e64 = (*c_1);\n param_2 = _e64;\n let _e65 = linearToSrgb_u0028_vf3_u003b((&param_2));\n return _e65;\n}\n\nfn main_1() {\n var texel: vec4<f32>;\n var colour: vec4<f32>;\n var param_3: vec3<f32>;\n\n let _e55 = vUv_1;\n let _e56 = textureSample(uSpriteTexture_t, uSpriteTexture_s, _e55);\n texel = _e56;\n let _e57 = texel;\n let _e58 = vTint_1;\n colour = (_e57 * _e58);\n let _e61 = colour[3u];\n if (_e61 < 0.00392157f) {\n discard;\n }\n let _e63 = colour;\n param_3 = _e63.xyz;\n let _e65 = applyOutputTransform_u0028_vf3_u003b((&param_3));\n let _e66 = colour;\n colour = vec4<f32>(_e65.x, _e65.y, _e65.z, _e66.w);\n let _e72 = colour;\n let _e75 = colour[3u];\n let _e76 = (_e72.xyz * _e75);\n let _e78 = colour[3u];\n fragColor = vec4<f32>(_e76.x, _e76.y, _e76.z, _e78);\n return;\n}\n\n@fragment \nfn main(@location(0) vUv: vec2<f32>, @location(1) vTint: vec4<f32>) -> @location(0) vec4<f32> {\n vUv_1 = vUv;\n vTint_1 = vTint;\n main_1();\n let _e5 = fragColor;\n return _e5;\n}\n";
1
+ export declare const SPRITE_FRAG_WGSL = "struct Uniforms {\n uOutputTransform: i32,\n uOutputExposure: f32,\n}\n\n@group(0) @binding(1) \nvar<uniform> unnamed: Uniforms;\n@group(0) @binding(32) \nvar uSpriteTexture_t: texture_2d<f32>;\n@group(0) @binding(33) \nvar uSpriteTexture_s: sampler;\nvar<private> vUv_1: vec2<f32>;\nvar<private> vTint_1: vec4<f32>;\nvar<private> fragColor: vec4<f32>;\n\nfn linearToSrgb_u0028_vf3_u003b(c: ptr<function, vec3<f32>>) -> vec3<f32> {\n var low: vec3<f32>;\n var high: vec3<f32>;\n\n let _e61 = (*c);\n low = (_e61 * 12.92f);\n let _e63 = (*c);\n high = ((pow(max(_e63, vec3<f32>(0f, 0f, 0f)), vec3<f32>(0.41666666f, 0.41666666f, 0.41666666f)) * 1.055f) - vec3(0.055f));\n let _e69 = high;\n let _e70 = low;\n let _e71 = (*c);\n return mix(_e69, _e70, step(_e71, vec3<f32>(0.0031308f, 0.0031308f, 0.0031308f)));\n}\n\nfn highlightShoulder_u0028_vf3_u003b(c_1: ptr<function, vec3<f32>>) -> vec3<f32> {\n var m: f32;\n var e: f32;\n\n let _e62 = (*c_1)[0u];\n let _e64 = (*c_1)[1u];\n let _e66 = (*c_1)[2u];\n m = max(_e62, max(_e64, _e66));\n let _e69 = m;\n if (_e69 <= 0.8f) {\n let _e71 = (*c_1);\n return _e71;\n }\n let _e72 = m;\n e = (_e72 - 0.8f);\n let _e74 = (*c_1);\n let _e75 = e;\n let _e77 = e;\n let _e81 = m;\n return (_e74 * ((0.8f + ((0.2f * _e75) / (_e77 + 0.2f))) / _e81));\n}\n\nfn rrtAndOdtFit_u0028_vf3_u003b(v: ptr<function, vec3<f32>>) -> vec3<f32> {\n var a: vec3<f32>;\n var b: vec3<f32>;\n\n let _e61 = (*v);\n let _e62 = (*v);\n a = ((_e61 * (_e62 + vec3(0.0245786f))) - vec3(0.000090537f));\n let _e68 = (*v);\n let _e69 = (*v);\n b = ((_e68 * ((_e69 * 0.983729f) + vec3(0.432951f))) + vec3(0.238081f));\n let _e76 = a;\n let _e77 = b;\n return (_e76 / _e77);\n}\n\nfn acesFilmic_u0028_vf3_u003b(x: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param: vec3<f32>;\n\n let _e61 = unnamed.uOutputExposure;\n let _e62 = (*x);\n (*x) = (_e62 * _e61);\n let _e64 = (*x);\n param = (mat3x3<f32>(vec3<f32>(0.59719f, 0.076f, 0.0284f), vec3<f32>(0.35458f, 0.90834f, 0.13383f), vec3<f32>(0.04823f, 0.01566f, 0.83777f)) * _e64);\n let _e66 = rrtAndOdtFit_u0028_vf3_u003b((&param));\n return clamp((mat3x3<f32>(vec3<f32>(1.60475f, -0.10208f, -0.00327f), vec3<f32>(-0.53108f, 1.10813f, -0.07276f), vec3<f32>(-0.07367f, -0.00605f, 1.07602f)) * _e66), vec3(0f), vec3(1f));\n}\n\nfn applyOutputTransform_u0028_vf3_u003b(c_2: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param_1: vec3<f32>;\n var param_2: vec3<f32>;\n var param_3: vec3<f32>;\n\n let _e63 = unnamed.uOutputTransform;\n if (_e63 == 0i) {\n let _e65 = (*c_2);\n return _e65;\n }\n let _e67 = unnamed.uOutputTransform;\n if (_e67 == 2i) {\n let _e69 = (*c_2);\n param_1 = _e69;\n let _e70 = acesFilmic_u0028_vf3_u003b((&param_1));\n (*c_2) = _e70;\n }\n let _e72 = unnamed.uOutputTransform;\n if (_e72 == 3i) {\n let _e74 = (*c_2);\n let _e76 = unnamed.uOutputExposure;\n param_2 = (_e74 * _e76);\n let _e78 = highlightShoulder_u0028_vf3_u003b((&param_2));\n (*c_2) = _e78;\n }\n let _e79 = (*c_2);\n param_3 = _e79;\n let _e80 = linearToSrgb_u0028_vf3_u003b((&param_3));\n return _e80;\n}\n\nfn main_1() {\n var texel: vec4<f32>;\n var colour: vec4<f32>;\n var param_4: vec3<f32>;\n\n let _e61 = vUv_1;\n let _e62 = textureSample(uSpriteTexture_t, uSpriteTexture_s, _e61);\n texel = _e62;\n let _e63 = texel;\n let _e64 = vTint_1;\n colour = (_e63 * _e64);\n let _e67 = colour[3u];\n if (_e67 < 0.00392157f) {\n discard;\n }\n let _e69 = colour;\n param_4 = _e69.xyz;\n let _e71 = applyOutputTransform_u0028_vf3_u003b((&param_4));\n let _e72 = colour;\n colour = vec4<f32>(_e71.x, _e71.y, _e71.z, _e72.w);\n let _e78 = colour;\n let _e81 = colour[3u];\n let _e82 = (_e78.xyz * _e81);\n let _e84 = colour[3u];\n fragColor = vec4<f32>(_e82.x, _e82.y, _e82.z, _e84);\n return;\n}\n\n@fragment \nfn main(@location(0) vUv: vec2<f32>, @location(1) vTint: vec4<f32>) -> @location(0) vec4<f32> {\n vUv_1 = vUv;\n vTint_1 = vTint;\n main_1();\n let _e5 = fragColor;\n return _e5;\n}\n";
2
2
  export declare const SPRITE_VERT_WGSL = "struct Uniforms {\n uToNdc0_: vec4<f32>,\n uToNdc1_: vec4<f32>,\n uClipCorrection: mat4x4<f32>,\n}\n\nstruct gl_PerVertex {\n @builtin(position) gl_Position: vec4<f32>,\n gl_PointSize: f32,\n}\n\nstruct VertexOutput {\n @location(0) member: vec2<f32>,\n @location(1) member_1: vec4<f32>,\n @builtin(position) gl_Position: vec4<f32>,\n}\n\nvar<private> gl_VertexIndex_1: i32;\nvar<private> aOrigin_1: vec2<f32>;\nvar<private> aEdges_1: vec4<f32>;\n@group(0) @binding(0) \nvar<uniform> unnamed: Uniforms;\nvar<private> vUv: vec2<f32>;\nvar<private> aUv_1: vec4<f32>;\nvar<private> vTint: vec4<f32>;\nvar<private> aTint_1: vec4<f32>;\nvar<private> unnamed_1: gl_PerVertex = gl_PerVertex(vec4<f32>(0f, 0f, 0f, 1f), 1f);\n\nfn main_1() {\n var corner: vec2<f32>;\n var indexable: array<vec2<f32>, 6>;\n var p: vec2<f32>;\n var ndc: vec2<f32>;\n var orientation: f32;\n var along: vec2<f32>;\n var local: f32;\n\n let _e31 = gl_VertexIndex_1;\n indexable = array<vec2<f32>, 6>(vec2<f32>(0f, 0f), vec2<f32>(1f, 0f), vec2<f32>(0f, 1f), vec2<f32>(0f, 1f), vec2<f32>(1f, 0f), vec2<f32>(1f, 1f));\n let _e33 = indexable[_e31];\n corner = _e33;\n let _e34 = aOrigin_1;\n let _e35 = aEdges_1;\n let _e38 = corner[0u];\n let _e41 = aEdges_1;\n let _e44 = corner[1u];\n p = ((_e34 + (_e35.xy * _e38)) + (_e41.zw * _e44));\n let _e49 = unnamed.uToNdc0_[0u];\n let _e51 = p[0u];\n let _e55 = unnamed.uToNdc0_[2u];\n let _e57 = p[1u];\n let _e62 = unnamed.uToNdc1_[0u];\n let _e66 = unnamed.uToNdc0_[1u];\n let _e68 = p[0u];\n let _e72 = unnamed.uToNdc0_[3u];\n let _e74 = p[1u];\n let _e79 = unnamed.uToNdc1_[1u];\n ndc = vec2<f32>((((_e49 * _e51) + (_e55 * _e57)) + _e62), (((_e66 * _e68) + (_e72 * _e74)) + _e79));\n let _e84 = unnamed.uToNdc0_[0u];\n let _e87 = unnamed.uToNdc0_[3u];\n let _e91 = unnamed.uToNdc0_[1u];\n let _e94 = unnamed.uToNdc0_[2u];\n orientation = ((_e84 * _e87) - (_e91 * _e94));\n let _e98 = corner[0u];\n let _e99 = orientation;\n if (_e99 > 0f) {\n let _e102 = corner[1u];\n local = (1f - _e102);\n } else {\n let _e105 = corner[1u];\n local = _e105;\n }\n let _e106 = local;\n along = vec2<f32>(_e98, _e106);\n let _e108 = aUv_1;\n let _e110 = aUv_1;\n let _e112 = along;\n vUv = mix(_e108.xy, _e110.zw, _e112);\n let _e114 = aTint_1;\n vTint = _e114;\n let _e116 = unnamed.uClipCorrection;\n let _e117 = ndc;\n unnamed_1.gl_Position = (_e116 * vec4<f32>(_e117.x, _e117.y, 0f, 1f));\n return;\n}\n\n@vertex \nfn main(@builtin(vertex_index) gl_VertexIndex: u32, @location(3) aOrigin: vec2<f32>, @location(0) aEdges: vec4<f32>, @location(1) aUv: vec4<f32>, @location(2) aTint: vec4<f32>) -> VertexOutput {\n gl_VertexIndex_1 = i32(gl_VertexIndex);\n aOrigin_1 = aOrigin;\n aEdges_1 = aEdges;\n aUv_1 = aUv;\n aTint_1 = aTint;\n main_1();\n let _e16 = unnamed_1.gl_Position.y;\n unnamed_1.gl_Position.y = -(_e16);\n let _e18 = vUv;\n let _e19 = vTint;\n let _e20 = unnamed_1.gl_Position;\n return VertexOutput(_e18, _e19, _e20);\n}\n";
3
3
  /**
4
4
  * What the transform assigned, so the renderer binds the same numbers.
@@ -4,7 +4,7 @@
4
4
  * The GLSL beside this file is the source of truth. A hand edit here is discarded by
5
5
  * the next generation, and `npm run wgsl:check` fails the build when this is stale.
6
6
  */
7
- export const SPRITE_FRAG_WGSL = "struct Uniforms {\n uOutputTransform: i32,\n uOutputExposure: f32,\n}\n\n@group(0) @binding(1) \nvar<uniform> unnamed: Uniforms;\n@group(0) @binding(32) \nvar uSpriteTexture_t: texture_2d<f32>;\n@group(0) @binding(33) \nvar uSpriteTexture_s: sampler;\nvar<private> vUv_1: vec2<f32>;\nvar<private> vTint_1: vec4<f32>;\nvar<private> fragColor: vec4<f32>;\n\nfn linearToSrgb_u0028_vf3_u003b(c: ptr<function, vec3<f32>>) -> vec3<f32> {\n var low: vec3<f32>;\n var high: vec3<f32>;\n\n let _e55 = (*c);\n low = (_e55 * 12.92f);\n let _e57 = (*c);\n high = ((pow(max(_e57, vec3<f32>(0f, 0f, 0f)), vec3<f32>(0.41666666f, 0.41666666f, 0.41666666f)) * 1.055f) - vec3(0.055f));\n let _e63 = high;\n let _e64 = low;\n let _e65 = (*c);\n return mix(_e63, _e64, step(_e65, vec3<f32>(0.0031308f, 0.0031308f, 0.0031308f)));\n}\n\nfn rrtAndOdtFit_u0028_vf3_u003b(v: ptr<function, vec3<f32>>) -> vec3<f32> {\n var a: vec3<f32>;\n var b: vec3<f32>;\n\n let _e55 = (*v);\n let _e56 = (*v);\n a = ((_e55 * (_e56 + vec3(0.0245786f))) - vec3(0.000090537f));\n let _e62 = (*v);\n let _e63 = (*v);\n b = ((_e62 * ((_e63 * 0.983729f) + vec3(0.432951f))) + vec3(0.238081f));\n let _e70 = a;\n let _e71 = b;\n return (_e70 / _e71);\n}\n\nfn acesFilmic_u0028_vf3_u003b(x: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param: vec3<f32>;\n\n let _e55 = unnamed.uOutputExposure;\n let _e56 = (*x);\n (*x) = (_e56 * _e55);\n let _e58 = (*x);\n param = (mat3x3<f32>(vec3<f32>(0.59719f, 0.076f, 0.0284f), vec3<f32>(0.35458f, 0.90834f, 0.13383f), vec3<f32>(0.04823f, 0.01566f, 0.83777f)) * _e58);\n let _e60 = rrtAndOdtFit_u0028_vf3_u003b((&param));\n return clamp((mat3x3<f32>(vec3<f32>(1.60475f, -0.10208f, -0.00327f), vec3<f32>(-0.53108f, 1.10813f, -0.07276f), vec3<f32>(-0.07367f, -0.00605f, 1.07602f)) * _e60), vec3(0f), vec3(1f));\n}\n\nfn applyOutputTransform_u0028_vf3_u003b(c_1: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param_1: vec3<f32>;\n var param_2: vec3<f32>;\n\n let _e56 = unnamed.uOutputTransform;\n if (_e56 == 0i) {\n let _e58 = (*c_1);\n return _e58;\n }\n let _e60 = unnamed.uOutputTransform;\n if (_e60 == 2i) {\n let _e62 = (*c_1);\n param_1 = _e62;\n let _e63 = acesFilmic_u0028_vf3_u003b((&param_1));\n (*c_1) = _e63;\n }\n let _e64 = (*c_1);\n param_2 = _e64;\n let _e65 = linearToSrgb_u0028_vf3_u003b((&param_2));\n return _e65;\n}\n\nfn main_1() {\n var texel: vec4<f32>;\n var colour: vec4<f32>;\n var param_3: vec3<f32>;\n\n let _e55 = vUv_1;\n let _e56 = textureSample(uSpriteTexture_t, uSpriteTexture_s, _e55);\n texel = _e56;\n let _e57 = texel;\n let _e58 = vTint_1;\n colour = (_e57 * _e58);\n let _e61 = colour[3u];\n if (_e61 < 0.00392157f) {\n discard;\n }\n let _e63 = colour;\n param_3 = _e63.xyz;\n let _e65 = applyOutputTransform_u0028_vf3_u003b((&param_3));\n let _e66 = colour;\n colour = vec4<f32>(_e65.x, _e65.y, _e65.z, _e66.w);\n let _e72 = colour;\n let _e75 = colour[3u];\n let _e76 = (_e72.xyz * _e75);\n let _e78 = colour[3u];\n fragColor = vec4<f32>(_e76.x, _e76.y, _e76.z, _e78);\n return;\n}\n\n@fragment \nfn main(@location(0) vUv: vec2<f32>, @location(1) vTint: vec4<f32>) -> @location(0) vec4<f32> {\n vUv_1 = vUv;\n vTint_1 = vTint;\n main_1();\n let _e5 = fragColor;\n return _e5;\n}\n";
7
+ export const SPRITE_FRAG_WGSL = "struct Uniforms {\n uOutputTransform: i32,\n uOutputExposure: f32,\n}\n\n@group(0) @binding(1) \nvar<uniform> unnamed: Uniforms;\n@group(0) @binding(32) \nvar uSpriteTexture_t: texture_2d<f32>;\n@group(0) @binding(33) \nvar uSpriteTexture_s: sampler;\nvar<private> vUv_1: vec2<f32>;\nvar<private> vTint_1: vec4<f32>;\nvar<private> fragColor: vec4<f32>;\n\nfn linearToSrgb_u0028_vf3_u003b(c: ptr<function, vec3<f32>>) -> vec3<f32> {\n var low: vec3<f32>;\n var high: vec3<f32>;\n\n let _e61 = (*c);\n low = (_e61 * 12.92f);\n let _e63 = (*c);\n high = ((pow(max(_e63, vec3<f32>(0f, 0f, 0f)), vec3<f32>(0.41666666f, 0.41666666f, 0.41666666f)) * 1.055f) - vec3(0.055f));\n let _e69 = high;\n let _e70 = low;\n let _e71 = (*c);\n return mix(_e69, _e70, step(_e71, vec3<f32>(0.0031308f, 0.0031308f, 0.0031308f)));\n}\n\nfn highlightShoulder_u0028_vf3_u003b(c_1: ptr<function, vec3<f32>>) -> vec3<f32> {\n var m: f32;\n var e: f32;\n\n let _e62 = (*c_1)[0u];\n let _e64 = (*c_1)[1u];\n let _e66 = (*c_1)[2u];\n m = max(_e62, max(_e64, _e66));\n let _e69 = m;\n if (_e69 <= 0.8f) {\n let _e71 = (*c_1);\n return _e71;\n }\n let _e72 = m;\n e = (_e72 - 0.8f);\n let _e74 = (*c_1);\n let _e75 = e;\n let _e77 = e;\n let _e81 = m;\n return (_e74 * ((0.8f + ((0.2f * _e75) / (_e77 + 0.2f))) / _e81));\n}\n\nfn rrtAndOdtFit_u0028_vf3_u003b(v: ptr<function, vec3<f32>>) -> vec3<f32> {\n var a: vec3<f32>;\n var b: vec3<f32>;\n\n let _e61 = (*v);\n let _e62 = (*v);\n a = ((_e61 * (_e62 + vec3(0.0245786f))) - vec3(0.000090537f));\n let _e68 = (*v);\n let _e69 = (*v);\n b = ((_e68 * ((_e69 * 0.983729f) + vec3(0.432951f))) + vec3(0.238081f));\n let _e76 = a;\n let _e77 = b;\n return (_e76 / _e77);\n}\n\nfn acesFilmic_u0028_vf3_u003b(x: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param: vec3<f32>;\n\n let _e61 = unnamed.uOutputExposure;\n let _e62 = (*x);\n (*x) = (_e62 * _e61);\n let _e64 = (*x);\n param = (mat3x3<f32>(vec3<f32>(0.59719f, 0.076f, 0.0284f), vec3<f32>(0.35458f, 0.90834f, 0.13383f), vec3<f32>(0.04823f, 0.01566f, 0.83777f)) * _e64);\n let _e66 = rrtAndOdtFit_u0028_vf3_u003b((&param));\n return clamp((mat3x3<f32>(vec3<f32>(1.60475f, -0.10208f, -0.00327f), vec3<f32>(-0.53108f, 1.10813f, -0.07276f), vec3<f32>(-0.07367f, -0.00605f, 1.07602f)) * _e66), vec3(0f), vec3(1f));\n}\n\nfn applyOutputTransform_u0028_vf3_u003b(c_2: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param_1: vec3<f32>;\n var param_2: vec3<f32>;\n var param_3: vec3<f32>;\n\n let _e63 = unnamed.uOutputTransform;\n if (_e63 == 0i) {\n let _e65 = (*c_2);\n return _e65;\n }\n let _e67 = unnamed.uOutputTransform;\n if (_e67 == 2i) {\n let _e69 = (*c_2);\n param_1 = _e69;\n let _e70 = acesFilmic_u0028_vf3_u003b((&param_1));\n (*c_2) = _e70;\n }\n let _e72 = unnamed.uOutputTransform;\n if (_e72 == 3i) {\n let _e74 = (*c_2);\n let _e76 = unnamed.uOutputExposure;\n param_2 = (_e74 * _e76);\n let _e78 = highlightShoulder_u0028_vf3_u003b((&param_2));\n (*c_2) = _e78;\n }\n let _e79 = (*c_2);\n param_3 = _e79;\n let _e80 = linearToSrgb_u0028_vf3_u003b((&param_3));\n return _e80;\n}\n\nfn main_1() {\n var texel: vec4<f32>;\n var colour: vec4<f32>;\n var param_4: vec3<f32>;\n\n let _e61 = vUv_1;\n let _e62 = textureSample(uSpriteTexture_t, uSpriteTexture_s, _e61);\n texel = _e62;\n let _e63 = texel;\n let _e64 = vTint_1;\n colour = (_e63 * _e64);\n let _e67 = colour[3u];\n if (_e67 < 0.00392157f) {\n discard;\n }\n let _e69 = colour;\n param_4 = _e69.xyz;\n let _e71 = applyOutputTransform_u0028_vf3_u003b((&param_4));\n let _e72 = colour;\n colour = vec4<f32>(_e71.x, _e71.y, _e71.z, _e72.w);\n let _e78 = colour;\n let _e81 = colour[3u];\n let _e82 = (_e78.xyz * _e81);\n let _e84 = colour[3u];\n fragColor = vec4<f32>(_e82.x, _e82.y, _e82.z, _e84);\n return;\n}\n\n@fragment \nfn main(@location(0) vUv: vec2<f32>, @location(1) vTint: vec4<f32>) -> @location(0) vec4<f32> {\n vUv_1 = vUv;\n vTint_1 = vTint;\n main_1();\n let _e5 = fragColor;\n return _e5;\n}\n";
8
8
  export const SPRITE_VERT_WGSL = "struct Uniforms {\n uToNdc0_: vec4<f32>,\n uToNdc1_: vec4<f32>,\n uClipCorrection: mat4x4<f32>,\n}\n\nstruct gl_PerVertex {\n @builtin(position) gl_Position: vec4<f32>,\n gl_PointSize: f32,\n}\n\nstruct VertexOutput {\n @location(0) member: vec2<f32>,\n @location(1) member_1: vec4<f32>,\n @builtin(position) gl_Position: vec4<f32>,\n}\n\nvar<private> gl_VertexIndex_1: i32;\nvar<private> aOrigin_1: vec2<f32>;\nvar<private> aEdges_1: vec4<f32>;\n@group(0) @binding(0) \nvar<uniform> unnamed: Uniforms;\nvar<private> vUv: vec2<f32>;\nvar<private> aUv_1: vec4<f32>;\nvar<private> vTint: vec4<f32>;\nvar<private> aTint_1: vec4<f32>;\nvar<private> unnamed_1: gl_PerVertex = gl_PerVertex(vec4<f32>(0f, 0f, 0f, 1f), 1f);\n\nfn main_1() {\n var corner: vec2<f32>;\n var indexable: array<vec2<f32>, 6>;\n var p: vec2<f32>;\n var ndc: vec2<f32>;\n var orientation: f32;\n var along: vec2<f32>;\n var local: f32;\n\n let _e31 = gl_VertexIndex_1;\n indexable = array<vec2<f32>, 6>(vec2<f32>(0f, 0f), vec2<f32>(1f, 0f), vec2<f32>(0f, 1f), vec2<f32>(0f, 1f), vec2<f32>(1f, 0f), vec2<f32>(1f, 1f));\n let _e33 = indexable[_e31];\n corner = _e33;\n let _e34 = aOrigin_1;\n let _e35 = aEdges_1;\n let _e38 = corner[0u];\n let _e41 = aEdges_1;\n let _e44 = corner[1u];\n p = ((_e34 + (_e35.xy * _e38)) + (_e41.zw * _e44));\n let _e49 = unnamed.uToNdc0_[0u];\n let _e51 = p[0u];\n let _e55 = unnamed.uToNdc0_[2u];\n let _e57 = p[1u];\n let _e62 = unnamed.uToNdc1_[0u];\n let _e66 = unnamed.uToNdc0_[1u];\n let _e68 = p[0u];\n let _e72 = unnamed.uToNdc0_[3u];\n let _e74 = p[1u];\n let _e79 = unnamed.uToNdc1_[1u];\n ndc = vec2<f32>((((_e49 * _e51) + (_e55 * _e57)) + _e62), (((_e66 * _e68) + (_e72 * _e74)) + _e79));\n let _e84 = unnamed.uToNdc0_[0u];\n let _e87 = unnamed.uToNdc0_[3u];\n let _e91 = unnamed.uToNdc0_[1u];\n let _e94 = unnamed.uToNdc0_[2u];\n orientation = ((_e84 * _e87) - (_e91 * _e94));\n let _e98 = corner[0u];\n let _e99 = orientation;\n if (_e99 > 0f) {\n let _e102 = corner[1u];\n local = (1f - _e102);\n } else {\n let _e105 = corner[1u];\n local = _e105;\n }\n let _e106 = local;\n along = vec2<f32>(_e98, _e106);\n let _e108 = aUv_1;\n let _e110 = aUv_1;\n let _e112 = along;\n vUv = mix(_e108.xy, _e110.zw, _e112);\n let _e114 = aTint_1;\n vTint = _e114;\n let _e116 = unnamed.uClipCorrection;\n let _e117 = ndc;\n unnamed_1.gl_Position = (_e116 * vec4<f32>(_e117.x, _e117.y, 0f, 1f));\n return;\n}\n\n@vertex \nfn main(@builtin(vertex_index) gl_VertexIndex: u32, @location(3) aOrigin: vec2<f32>, @location(0) aEdges: vec4<f32>, @location(1) aUv: vec4<f32>, @location(2) aTint: vec4<f32>) -> VertexOutput {\n gl_VertexIndex_1 = i32(gl_VertexIndex);\n aOrigin_1 = aOrigin;\n aEdges_1 = aEdges;\n aUv_1 = aUv;\n aTint_1 = aTint;\n main_1();\n let _e16 = unnamed_1.gl_Position.y;\n unnamed_1.gl_Position.y = -(_e16);\n let _e18 = vUv;\n let _e19 = vTint;\n let _e20 = unnamed_1.gl_Position;\n return VertexOutput(_e18, _e19, _e20);\n}\n";
9
9
  /**
10
10
  * What the transform assigned, so the renderer binds the same numbers.
package/dist/spriteGl.js CHANGED
@@ -1,4 +1,5 @@
1
1
  /** The WebGL2 half of the sprite pass: one program, one instance buffer, one texture slot table. */
2
+ import { blendCovering } from '@driftengine/core';
2
3
  import { SPRITE_FRAG, SPRITE_VERT } from './shaders/sprite.js';
3
4
  import { SPRITE_FLOATS } from './spriteBatch.js';
4
5
  /**
@@ -191,8 +192,9 @@ export function drawWebgl2Sprites(gl, sprites, batch, toNdc, clipCorrection, out
191
192
  */
192
193
  gl.disable(gl.CULL_FACE);
193
194
  gl.enable(gl.BLEND);
194
- /* Premultiplied `over`: the fragment stage folds alpha in, so this composes onto opaque pixels. */
195
- gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
195
+ /* Premultiplied `over`: the fragment stage folds alpha in, so this composes onto opaque pixels,
196
+ and covers the scene's surface by its alpha so occlusion behind it does not darken it. */
197
+ blendCovering(gl, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
196
198
  gl.disable(gl.DEPTH_TEST);
197
199
  gl.depthMask(false);
198
200
  gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
@@ -6,8 +6,20 @@ export interface GpuSpriteSlot {
6
6
  readonly texture: GPUTexture;
7
7
  readonly bindGroup: GPUBindGroup;
8
8
  }
9
+ /** Pipelines by colour format, then depth format, then sample count: a lookup that allocates nothing. */
10
+ type PipelinesByTarget = Map<GPUTextureFormat, Map<GPUTextureFormat, Map<number, GPURenderPipeline>>>;
9
11
  export interface GpuSprites {
10
- readonly pipeline: GPURenderPipeline;
12
+ /**
13
+ * One pipeline per pass the sprites have been drawn into, chosen at the draw by
14
+ * `spritePipelineFor`. A pipeline must match its pass's attachments exactly, and a 2D layer is
15
+ * drawn in the frame and over it, which are two different passes: the scene's target at the
16
+ * scene's samples, and the canvas at one.
17
+ */
18
+ readonly pipelines: PipelinesByTarget;
19
+ readonly pipelineLayout: GPUPipelineLayout;
20
+ readonly vertexModule: GPUShaderModule;
21
+ readonly fragmentModule: GPUShaderModule;
22
+ readonly label: string;
11
23
  readonly layout: GPUBindGroupLayout;
12
24
  readonly vertexUniforms: GPUBuffer;
13
25
  readonly fragmentUniforms: GPUBuffer;
@@ -28,9 +40,20 @@ export interface GpuSprites {
28
40
  readonly fragmentInts: Int32Array;
29
41
  }
30
42
  export declare function createGpuSprites(device: GPUDevice, format: GPUTextureFormat, depthFormat: GPUTextureFormat, samples: number, capacity: number, slots: number, label: string): GpuSprites;
43
+ /**
44
+ * The pipeline for a pass with these attachments, built the first time it is asked for.
45
+ *
46
+ * Chosen at the draw from what the pass is (`PassContext.format`, `depthFormat`, `samples`), so a
47
+ * 2D layer drawn over the finished frame, on the canvas at one sample, is not refused for having
48
+ * been built for the scene target. A lookup through three maps on every later draw, no allocation.
49
+ */
50
+ export declare function spritePipelineFor(device: GPUDevice, sprites: GpuSprites, format: GPUTextureFormat, depthFormat: GPUTextureFormat, samples: number): GPURenderPipeline;
31
51
  export declare function setGpuSpriteTexture(device: GPUDevice, sprites: GpuSprites, slot: number, source: SpriteImage, options: SpriteTextureOptions): void;
32
52
  /** The same one white texel, written straight into a texture. See the WebGL2 half for why. */
33
53
  export declare function setGpuWhiteTexture(device: GPUDevice, sprites: GpuSprites, slot: number): void;
34
54
  export declare function uploadGpuInstances(device: GPUDevice, sprites: GpuSprites, batch: SpriteBatch): void;
35
- export declare function drawGpuSprites(pass: GPURenderPassEncoder, sprites: GpuSprites, batch: SpriteBatch): void;
55
+ export declare function drawGpuSprites(pass: GPURenderPassEncoder, sprites: GpuSprites, batch: SpriteBatch,
56
+ /** The pipeline for this pass's attachments: `spritePipelineFor`. */
57
+ pipeline: GPURenderPipeline): void;
36
58
  export declare function disposeGpuSprites(sprites: GpuSprites): void;
59
+ export {};
package/dist/spriteGpu.js CHANGED
@@ -10,7 +10,7 @@ import { SPRITE_FLOATS } from './spriteBatch.js';
10
10
  * and nothing else. The alternative was a second copy of the mip blit living here, and the second
11
11
  * copy is the one that ends up disagreeing about the colour space.
12
12
  */
13
- import { generateMipChain, mipLevelCount } from '@driftengine/core';
13
+ import { SCENE_ALPHA_COVERS, generateMipChain, mipLevelCount, } from '@driftengine/core';
14
14
  const VERT = SPRITE_BINDINGS.SPRITE_VERT;
15
15
  const FRAG = SPRITE_BINDINGS.SPRITE_FRAG;
16
16
  const STRIDE = SPRITE_FLOATS * 4;
@@ -37,52 +37,14 @@ export function createGpuSprites(device, format, depthFormat, samples, capacity,
37
37
  },
38
38
  ],
39
39
  });
40
- const pipeline = device.createRenderPipeline({
41
- label,
42
- layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
43
- vertex: {
44
- module: device.createShaderModule({ label: `${label}.vert`, code: SPRITE_VERT_WGSL }),
45
- entryPoint: 'main',
46
- buffers: [
47
- {
48
- arrayStride: STRIDE,
49
- stepMode: 'instance',
50
- attributes: [
51
- { shaderLocation: 0, offset: 0, format: 'float32x4' },
52
- { shaderLocation: 1, offset: 16, format: 'float32x4' },
53
- { shaderLocation: 2, offset: 32, format: 'float32x4' },
54
- { shaderLocation: 3, offset: 48, format: 'float32x2' },
55
- ],
56
- },
57
- ],
58
- },
59
- fragment: {
60
- module: device.createShaderModule({ label: `${label}.frag`, code: SPRITE_FRAG_WGSL }),
61
- entryPoint: 'main',
62
- targets: [
63
- {
64
- format,
65
- /* Premultiplied `over`, matching the WebGL2 half exactly. */
66
- blend: {
67
- color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
68
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
69
- },
70
- },
71
- ],
72
- },
73
- /*
74
- * Nothing is culled: a sprite is mirrored by giving it a negative width, which reverses its
75
- * winding, and a character facing left is exactly that.
76
- */
77
- primitive: { topology: 'triangle-list', cullMode: 'none' },
78
- multisample: { count: samples },
79
- /*
80
- * Neither tested nor written. The 2D layer's order *is* its layering — there is no z to sort
81
- * by — and a pass that depth-tested would let whatever 3D geometry is in the frame punch holes
82
- * in an overlay drawn over it. Declared all the same, because a pipeline in a render pass that
83
- * has a depth attachment must name its format.
84
- */
85
- depthStencil: { format: depthFormat, depthWriteEnabled: false, depthCompare: 'always' },
40
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
41
+ const vertexModule = device.createShaderModule({
42
+ label: `${label}.vert`,
43
+ code: SPRITE_VERT_WGSL,
44
+ });
45
+ const fragmentModule = device.createShaderModule({
46
+ label: `${label}.frag`,
47
+ code: SPRITE_FRAG_WGSL,
86
48
  });
87
49
  const vertexUniforms = device.createBuffer({
88
50
  label: `${label}.vertexUniforms`,
@@ -101,8 +63,12 @@ export function createGpuSprites(device, format, depthFormat, samples, capacity,
101
63
  });
102
64
  const vertexScratch = new ArrayBuffer(VERT.uniformSize);
103
65
  const fragmentScratch = new ArrayBuffer(FRAG.uniformSize);
104
- return {
105
- pipeline,
66
+ const sprites = {
67
+ pipelines: new Map(),
68
+ pipelineLayout,
69
+ vertexModule,
70
+ fragmentModule,
71
+ label,
106
72
  layout,
107
73
  vertexUniforms,
108
74
  fragmentUniforms,
@@ -138,6 +104,81 @@ export function createGpuSprites(device, format, depthFormat, samples, capacity,
138
104
  fragmentFloats: new Float32Array(fragmentScratch),
139
105
  fragmentInts: new Int32Array(fragmentScratch),
140
106
  };
107
+ /* The frame's own target, built now, so the common case compiles before its first draw. */
108
+ spritePipelineFor(device, sprites, format, depthFormat, samples);
109
+ return sprites;
110
+ }
111
+ /**
112
+ * The pipeline for a pass with these attachments, built the first time it is asked for.
113
+ *
114
+ * Chosen at the draw from what the pass is (`PassContext.format`, `depthFormat`, `samples`), so a
115
+ * 2D layer drawn over the finished frame, on the canvas at one sample, is not refused for having
116
+ * been built for the scene target. A lookup through three maps on every later draw, no allocation.
117
+ */
118
+ export function spritePipelineFor(device, sprites, format, depthFormat, samples) {
119
+ let byDepth = sprites.pipelines.get(format);
120
+ if (byDepth === undefined) {
121
+ byDepth = new Map();
122
+ sprites.pipelines.set(format, byDepth);
123
+ }
124
+ let bySamples = byDepth.get(depthFormat);
125
+ if (bySamples === undefined) {
126
+ bySamples = new Map();
127
+ byDepth.set(depthFormat, bySamples);
128
+ }
129
+ const found = bySamples.get(samples);
130
+ if (found !== undefined)
131
+ return found;
132
+ const pipeline = device.createRenderPipeline({
133
+ label: `${sprites.label}:${format}x${samples}`,
134
+ layout: sprites.pipelineLayout,
135
+ vertex: {
136
+ module: sprites.vertexModule,
137
+ entryPoint: 'main',
138
+ buffers: [
139
+ {
140
+ arrayStride: STRIDE,
141
+ stepMode: 'instance',
142
+ attributes: [
143
+ { shaderLocation: 0, offset: 0, format: 'float32x4' },
144
+ { shaderLocation: 1, offset: 16, format: 'float32x4' },
145
+ { shaderLocation: 2, offset: 32, format: 'float32x4' },
146
+ { shaderLocation: 3, offset: 48, format: 'float32x2' },
147
+ ],
148
+ },
149
+ ],
150
+ },
151
+ fragment: {
152
+ module: sprites.fragmentModule,
153
+ entryPoint: 'main',
154
+ targets: [
155
+ {
156
+ format,
157
+ /* Premultiplied `over`, matching the WebGL2 half exactly; a sprite covers the scene's
158
+ surface by its alpha, so occlusion behind it does not darken it. */
159
+ blend: {
160
+ color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
161
+ alpha: SCENE_ALPHA_COVERS,
162
+ },
163
+ },
164
+ ],
165
+ },
166
+ /*
167
+ * Nothing is culled: a sprite is mirrored by giving it a negative width, which reverses its
168
+ * winding, and a character facing left is exactly that.
169
+ */
170
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
171
+ multisample: { count: samples },
172
+ /*
173
+ * Neither tested nor written. The 2D layer's order *is* its layering — there is no z to sort
174
+ * by — and a pass that depth-tested would let whatever 3D geometry is in the frame punch holes
175
+ * in an overlay drawn over it. Declared all the same, because a pipeline in a render pass that
176
+ * has a depth attachment must name its format.
177
+ */
178
+ depthStencil: { format: depthFormat, depthWriteEnabled: false, depthCompare: 'always' },
179
+ });
180
+ bySamples.set(samples, pipeline);
181
+ return pipeline;
141
182
  }
142
183
  /**
143
184
  * One pipeline per format, kept on the sprite pass rather than in the renderer's cache.
@@ -231,10 +272,12 @@ export function uploadGpuInstances(device, sprites, batch) {
231
272
  return;
232
273
  device.queue.writeBuffer(sprites.instances, 0, batch.instances.buffer, batch.instances.byteOffset, batch.count * STRIDE);
233
274
  }
234
- export function drawGpuSprites(pass, sprites, batch) {
275
+ export function drawGpuSprites(pass, sprites, batch,
276
+ /** The pipeline for this pass's attachments: `spritePipelineFor`. */
277
+ pipeline) {
235
278
  if (batch.count === 0)
236
279
  return;
237
- pass.setPipeline(sprites.pipeline);
280
+ pass.setPipeline(pipeline);
238
281
  for (let run = 0; run < batch.runCount; run += 1) {
239
282
  const at = run * 3;
240
283
  const slot = sprites.slots[batch.runs[at]];
@@ -3,7 +3,7 @@ import { SPRITE_BINDINGS } from './shaders/generated/sprite.wgsl.js';
3
3
  import { createAffine2D } from './camera2d.js';
4
4
  import { createSpriteBatch, resetSpriteBatch } from './spriteBatch.js';
5
5
  import { createWebgl2Sprites, disposeWebgl2Sprites, drawWebgl2Sprites, setWebgl2SpriteTexture, setWebgl2WhiteTexture, uploadWebgl2Instances, } from './spriteGl.js';
6
- import { createGpuSprites, disposeGpuSprites, drawGpuSprites, setGpuSpriteTexture, setGpuWhiteTexture, uploadGpuInstances, } from './spriteGpu.js';
6
+ import { createGpuSprites, disposeGpuSprites, drawGpuSprites, spritePipelineFor, setGpuSpriteTexture, setGpuWhiteTexture, uploadGpuInstances, } from './spriteGpu.js';
7
7
  import { DEFAULT_SPRITE_TEXTURE_OPTIONS } from './spriteTexture.js';
8
8
  const VERT = SPRITE_BINDINGS.SPRITE_VERT;
9
9
  const FRAG = SPRITE_BINDINGS.SPRITE_FRAG;
@@ -126,7 +126,8 @@ export function createSpritePass(options = {}) {
126
126
  device.queue.writeBuffer(gpu.vertexUniforms, 0, gpu.vertexScratch);
127
127
  device.queue.writeBuffer(gpu.fragmentUniforms, 0, gpu.fragmentScratch);
128
128
  uploadGpuInstances(device, gpu, batch);
129
- drawGpuSprites(ctx.pass, gpu, batch);
129
+ /* For the pass this lands in, which after `endFrame` is the canvas: see `spritePipelineFor`. */
130
+ drawGpuSprites(ctx.pass, gpu, batch, spritePipelineFor(device, gpu, ctx.format, ctx.depthFormat, ctx.samples));
130
131
  },
131
132
  dispose() {
132
133
  if (gl !== null && context !== null)
package/dist/uiDraw.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /** Turn a laid-out interface tree into quads, parents first so children land on top. */
2
2
  import { drawSprite } from './spriteBatch.js';
3
3
  import { createSpriteFrame } from './spriteSheet.js';
4
+ import { layersPresent } from './uiLayer.js';
4
5
  /* One placement, refilled per node. This is a per-frame path and these are the only objects in it. */
5
6
  const PLACEMENT = {
6
7
  x: 0,
@@ -30,11 +31,26 @@ const CUT = createSpriteFrame();
30
31
  * left at.
31
32
  */
32
33
  export function drawUiTree(batch, root, white, sink) {
33
- return drawWithin(batch, root, white, sink, -Infinity, -Infinity, Infinity, Infinity);
34
+ /*
35
+ * **Layer by layer, lowest first, and tree order within one.** A raised node — a dropdown that
36
+ * must cover the panel its control sits in — was drawn in tree order with everything else, so a
37
+ * later sibling covered it. Each layer is a walk of the whole tree with the clips it would have
38
+ * had anyway, so a raised node is still cut by a clipping ancestor; a tree with no raised node is
39
+ * one walk, as before.
40
+ */
41
+ const count = layersPresent(root, LAYERS);
42
+ let drawn = 0;
43
+ for (let at = 0; at < count; at += 1) {
44
+ drawn += drawWithin(batch, root, white, sink, -Infinity, -Infinity, Infinity, Infinity, 0, LAYERS[at] ?? 0);
45
+ }
46
+ return drawn;
34
47
  }
35
- function drawWithin(batch, node, white, sink, left, top, right, bottom) {
48
+ /** The layers of the tree being drawn, reused: this runs per frame. */
49
+ const LAYERS = [];
50
+ function drawWithin(batch, node, white, sink, left, top, right, bottom, inherited, only) {
36
51
  if (node.hidden)
37
52
  return 0;
53
+ const layer = node.layer === 0 ? inherited : node.layer;
38
54
  const rect = node.rect;
39
55
  const clipped = left !== -Infinity;
40
56
  const x0 = Math.max(rect.x, left);
@@ -47,7 +63,7 @@ function drawWithin(batch, node, white, sink, left, top, right, bottom) {
47
63
  * a node is drawn as it always was, whatever its size. Its children are cut one by one either way,
48
64
  * since a child can lie outside its parent's box and still inside the clip.
49
65
  */
50
- if (!clipped || (x1 > x0 && y1 > y0)) {
66
+ if (layer === only && (!clipped || (x1 > x0 && y1 > y0))) {
51
67
  PLACEMENT.x = clipped ? x0 : rect.x;
52
68
  PLACEMENT.y = clipped ? y0 : rect.y;
53
69
  PLACEMENT.w = clipped ? x1 - x0 : rect.w;
@@ -75,8 +91,9 @@ function drawWithin(batch, node, white, sink, left, top, right, bottom) {
75
91
  const ct = keep ? Math.max(top, rect.y) : top;
76
92
  const cr = keep ? Math.min(right, rect.x + rect.w) : right;
77
93
  const cb = keep ? Math.min(bottom, rect.y + rect.h) : bottom;
78
- for (const child of node.children)
79
- drawn += drawWithin(batch, child, white, sink, cl, ct, cr, cb);
94
+ for (const child of node.children) {
95
+ drawn += drawWithin(batch, child, white, sink, cl, ct, cr, cb, layer, only);
96
+ }
80
97
  return drawn;
81
98
  }
82
99
  /** The part of a node's frame its visible box shows, or the whole frame when nothing was cut. */
package/dist/uiFocus.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Which node a point is on, and which node a keyboard should be talking to. */
2
2
  import { uiRectHolds } from './uiNode.js';
3
+ import { layersPresent } from './uiLayer.js';
3
4
  /**
4
5
  * The interactive node under a point, or `null`.
5
6
  *
@@ -14,18 +15,32 @@ import { uiRectHolds } from './uiNode.js';
14
15
  * Allocates nothing.
15
16
  */
16
17
  export function uiHitTest(root, x, y) {
17
- if (root.hidden)
18
+ /* The highest layer first, as it is drawn last: a raised popup takes the pointer over a later
19
+ sibling it covers. See `drawUiTree`. */
20
+ const count = layersPresent(root, HIT_LAYERS);
21
+ for (let at = count - 1; at >= 0; at -= 1) {
22
+ const hit = hitOn(root, x, y, 0, HIT_LAYERS[at] ?? 0);
23
+ if (hit !== null)
24
+ return hit;
25
+ }
26
+ return null;
27
+ }
28
+ /** The layers of the tree being pointed at, reused: a hit test runs per pointer event. */
29
+ const HIT_LAYERS = [];
30
+ function hitOn(node, x, y, inherited, only) {
31
+ if (node.hidden)
18
32
  return null;
19
33
  /* A clipping node bounds its descendants here as it does in the picture: a row scrolled out of a
20
34
  list is not under the pointer, whatever is drawn where it would have been. */
21
- if (root.clip && !uiRectHolds(root, x, y))
35
+ if (node.clip && !uiRectHolds(node, x, y))
22
36
  return null;
23
- for (let i = root.children.length - 1; i >= 0; i -= 1) {
24
- const hit = uiHitTest(root.children[i], x, y);
37
+ const layer = node.layer === 0 ? inherited : node.layer;
38
+ for (let i = node.children.length - 1; i >= 0; i -= 1) {
39
+ const hit = hitOn(node.children[i], x, y, layer, only);
25
40
  if (hit !== null)
26
41
  return hit;
27
42
  }
28
- return root.interactive && uiRectHolds(root, x, y) ? root : null;
43
+ return layer === only && node.interactive && uiRectHolds(node, x, y) ? node : null;
29
44
  }
30
45
  /**
31
46
  * Every focusable node, in tree order, appended to `out`.
package/dist/uiLayer.d.ts CHANGED
@@ -16,3 +16,10 @@
16
16
  import type { UiNode } from './uiNode.ts';
17
17
  /** Fill `out` with the tree in draw order and return how many entries were written. */
18
18
  export declare function layerOrder(root: UiNode, out: UiNode[]): number;
19
+ /**
20
+ * The layers a tree draws on, ascending, into `out`; how many there are.
21
+ *
22
+ * What `drawUiTree` and `uiHitTest` walk a tree once per: a tree with no raised node answers one
23
+ * layer and is walked once, exactly as it was before either read a layer.
24
+ */
25
+ export declare function layersPresent(root: UiNode, out: number[]): number;
package/dist/uiLayer.js CHANGED
@@ -28,3 +28,27 @@ export function layerOrder(root, out) {
28
28
  out.length = count;
29
29
  return count;
30
30
  }
31
+ /** Ascending, without allocating a comparator per call. */
32
+ const ASCENDING = (a, b) => a - b;
33
+ /**
34
+ * The layers a tree draws on, ascending, into `out`; how many there are.
35
+ *
36
+ * What `drawUiTree` and `uiHitTest` walk a tree once per: a tree with no raised node answers one
37
+ * layer and is walked once, exactly as it was before either read a layer.
38
+ */
39
+ export function layersPresent(root, out) {
40
+ out.length = 0;
41
+ collect(root, 0, out);
42
+ if (out.length > 1)
43
+ out.sort(ASCENDING);
44
+ return out.length;
45
+ }
46
+ function collect(node, inherited, out) {
47
+ if (node.hidden)
48
+ return;
49
+ const layer = node.layer === 0 ? inherited : node.layer;
50
+ if (!out.includes(layer))
51
+ out.push(layer);
52
+ for (const child of node.children)
53
+ collect(child, layer, out);
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/ui2d",
3
- "version": "4.8.1",
3
+ "version": "4.8.3",
4
4
  "description": "The 2D layer: batched sprites, sheets, tilemaps, and a retained interface tree over them",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "sideEffects": false,
29
29
  "peerDependencies": {
30
- "@driftengine/core": "4.8.1"
30
+ "@driftengine/core": "4.8.3"
31
31
  },
32
32
  "author": "Drift Technologies",
33
33
  "repository": {
@@ -5,7 +5,7 @@
5
5
  * the next generation, and `npm run wgsl:check` fails the build when this is stale.
6
6
  */
7
7
 
8
- export const SPRITE_FRAG_WGSL = "struct Uniforms {\n uOutputTransform: i32,\n uOutputExposure: f32,\n}\n\n@group(0) @binding(1) \nvar<uniform> unnamed: Uniforms;\n@group(0) @binding(32) \nvar uSpriteTexture_t: texture_2d<f32>;\n@group(0) @binding(33) \nvar uSpriteTexture_s: sampler;\nvar<private> vUv_1: vec2<f32>;\nvar<private> vTint_1: vec4<f32>;\nvar<private> fragColor: vec4<f32>;\n\nfn linearToSrgb_u0028_vf3_u003b(c: ptr<function, vec3<f32>>) -> vec3<f32> {\n var low: vec3<f32>;\n var high: vec3<f32>;\n\n let _e55 = (*c);\n low = (_e55 * 12.92f);\n let _e57 = (*c);\n high = ((pow(max(_e57, vec3<f32>(0f, 0f, 0f)), vec3<f32>(0.41666666f, 0.41666666f, 0.41666666f)) * 1.055f) - vec3(0.055f));\n let _e63 = high;\n let _e64 = low;\n let _e65 = (*c);\n return mix(_e63, _e64, step(_e65, vec3<f32>(0.0031308f, 0.0031308f, 0.0031308f)));\n}\n\nfn rrtAndOdtFit_u0028_vf3_u003b(v: ptr<function, vec3<f32>>) -> vec3<f32> {\n var a: vec3<f32>;\n var b: vec3<f32>;\n\n let _e55 = (*v);\n let _e56 = (*v);\n a = ((_e55 * (_e56 + vec3(0.0245786f))) - vec3(0.000090537f));\n let _e62 = (*v);\n let _e63 = (*v);\n b = ((_e62 * ((_e63 * 0.983729f) + vec3(0.432951f))) + vec3(0.238081f));\n let _e70 = a;\n let _e71 = b;\n return (_e70 / _e71);\n}\n\nfn acesFilmic_u0028_vf3_u003b(x: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param: vec3<f32>;\n\n let _e55 = unnamed.uOutputExposure;\n let _e56 = (*x);\n (*x) = (_e56 * _e55);\n let _e58 = (*x);\n param = (mat3x3<f32>(vec3<f32>(0.59719f, 0.076f, 0.0284f), vec3<f32>(0.35458f, 0.90834f, 0.13383f), vec3<f32>(0.04823f, 0.01566f, 0.83777f)) * _e58);\n let _e60 = rrtAndOdtFit_u0028_vf3_u003b((&param));\n return clamp((mat3x3<f32>(vec3<f32>(1.60475f, -0.10208f, -0.00327f), vec3<f32>(-0.53108f, 1.10813f, -0.07276f), vec3<f32>(-0.07367f, -0.00605f, 1.07602f)) * _e60), vec3(0f), vec3(1f));\n}\n\nfn applyOutputTransform_u0028_vf3_u003b(c_1: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param_1: vec3<f32>;\n var param_2: vec3<f32>;\n\n let _e56 = unnamed.uOutputTransform;\n if (_e56 == 0i) {\n let _e58 = (*c_1);\n return _e58;\n }\n let _e60 = unnamed.uOutputTransform;\n if (_e60 == 2i) {\n let _e62 = (*c_1);\n param_1 = _e62;\n let _e63 = acesFilmic_u0028_vf3_u003b((&param_1));\n (*c_1) = _e63;\n }\n let _e64 = (*c_1);\n param_2 = _e64;\n let _e65 = linearToSrgb_u0028_vf3_u003b((&param_2));\n return _e65;\n}\n\nfn main_1() {\n var texel: vec4<f32>;\n var colour: vec4<f32>;\n var param_3: vec3<f32>;\n\n let _e55 = vUv_1;\n let _e56 = textureSample(uSpriteTexture_t, uSpriteTexture_s, _e55);\n texel = _e56;\n let _e57 = texel;\n let _e58 = vTint_1;\n colour = (_e57 * _e58);\n let _e61 = colour[3u];\n if (_e61 < 0.00392157f) {\n discard;\n }\n let _e63 = colour;\n param_3 = _e63.xyz;\n let _e65 = applyOutputTransform_u0028_vf3_u003b((&param_3));\n let _e66 = colour;\n colour = vec4<f32>(_e65.x, _e65.y, _e65.z, _e66.w);\n let _e72 = colour;\n let _e75 = colour[3u];\n let _e76 = (_e72.xyz * _e75);\n let _e78 = colour[3u];\n fragColor = vec4<f32>(_e76.x, _e76.y, _e76.z, _e78);\n return;\n}\n\n@fragment \nfn main(@location(0) vUv: vec2<f32>, @location(1) vTint: vec4<f32>) -> @location(0) vec4<f32> {\n vUv_1 = vUv;\n vTint_1 = vTint;\n main_1();\n let _e5 = fragColor;\n return _e5;\n}\n";
8
+ export const SPRITE_FRAG_WGSL = "struct Uniforms {\n uOutputTransform: i32,\n uOutputExposure: f32,\n}\n\n@group(0) @binding(1) \nvar<uniform> unnamed: Uniforms;\n@group(0) @binding(32) \nvar uSpriteTexture_t: texture_2d<f32>;\n@group(0) @binding(33) \nvar uSpriteTexture_s: sampler;\nvar<private> vUv_1: vec2<f32>;\nvar<private> vTint_1: vec4<f32>;\nvar<private> fragColor: vec4<f32>;\n\nfn linearToSrgb_u0028_vf3_u003b(c: ptr<function, vec3<f32>>) -> vec3<f32> {\n var low: vec3<f32>;\n var high: vec3<f32>;\n\n let _e61 = (*c);\n low = (_e61 * 12.92f);\n let _e63 = (*c);\n high = ((pow(max(_e63, vec3<f32>(0f, 0f, 0f)), vec3<f32>(0.41666666f, 0.41666666f, 0.41666666f)) * 1.055f) - vec3(0.055f));\n let _e69 = high;\n let _e70 = low;\n let _e71 = (*c);\n return mix(_e69, _e70, step(_e71, vec3<f32>(0.0031308f, 0.0031308f, 0.0031308f)));\n}\n\nfn highlightShoulder_u0028_vf3_u003b(c_1: ptr<function, vec3<f32>>) -> vec3<f32> {\n var m: f32;\n var e: f32;\n\n let _e62 = (*c_1)[0u];\n let _e64 = (*c_1)[1u];\n let _e66 = (*c_1)[2u];\n m = max(_e62, max(_e64, _e66));\n let _e69 = m;\n if (_e69 <= 0.8f) {\n let _e71 = (*c_1);\n return _e71;\n }\n let _e72 = m;\n e = (_e72 - 0.8f);\n let _e74 = (*c_1);\n let _e75 = e;\n let _e77 = e;\n let _e81 = m;\n return (_e74 * ((0.8f + ((0.2f * _e75) / (_e77 + 0.2f))) / _e81));\n}\n\nfn rrtAndOdtFit_u0028_vf3_u003b(v: ptr<function, vec3<f32>>) -> vec3<f32> {\n var a: vec3<f32>;\n var b: vec3<f32>;\n\n let _e61 = (*v);\n let _e62 = (*v);\n a = ((_e61 * (_e62 + vec3(0.0245786f))) - vec3(0.000090537f));\n let _e68 = (*v);\n let _e69 = (*v);\n b = ((_e68 * ((_e69 * 0.983729f) + vec3(0.432951f))) + vec3(0.238081f));\n let _e76 = a;\n let _e77 = b;\n return (_e76 / _e77);\n}\n\nfn acesFilmic_u0028_vf3_u003b(x: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param: vec3<f32>;\n\n let _e61 = unnamed.uOutputExposure;\n let _e62 = (*x);\n (*x) = (_e62 * _e61);\n let _e64 = (*x);\n param = (mat3x3<f32>(vec3<f32>(0.59719f, 0.076f, 0.0284f), vec3<f32>(0.35458f, 0.90834f, 0.13383f), vec3<f32>(0.04823f, 0.01566f, 0.83777f)) * _e64);\n let _e66 = rrtAndOdtFit_u0028_vf3_u003b((&param));\n return clamp((mat3x3<f32>(vec3<f32>(1.60475f, -0.10208f, -0.00327f), vec3<f32>(-0.53108f, 1.10813f, -0.07276f), vec3<f32>(-0.07367f, -0.00605f, 1.07602f)) * _e66), vec3(0f), vec3(1f));\n}\n\nfn applyOutputTransform_u0028_vf3_u003b(c_2: ptr<function, vec3<f32>>) -> vec3<f32> {\n var param_1: vec3<f32>;\n var param_2: vec3<f32>;\n var param_3: vec3<f32>;\n\n let _e63 = unnamed.uOutputTransform;\n if (_e63 == 0i) {\n let _e65 = (*c_2);\n return _e65;\n }\n let _e67 = unnamed.uOutputTransform;\n if (_e67 == 2i) {\n let _e69 = (*c_2);\n param_1 = _e69;\n let _e70 = acesFilmic_u0028_vf3_u003b((&param_1));\n (*c_2) = _e70;\n }\n let _e72 = unnamed.uOutputTransform;\n if (_e72 == 3i) {\n let _e74 = (*c_2);\n let _e76 = unnamed.uOutputExposure;\n param_2 = (_e74 * _e76);\n let _e78 = highlightShoulder_u0028_vf3_u003b((&param_2));\n (*c_2) = _e78;\n }\n let _e79 = (*c_2);\n param_3 = _e79;\n let _e80 = linearToSrgb_u0028_vf3_u003b((&param_3));\n return _e80;\n}\n\nfn main_1() {\n var texel: vec4<f32>;\n var colour: vec4<f32>;\n var param_4: vec3<f32>;\n\n let _e61 = vUv_1;\n let _e62 = textureSample(uSpriteTexture_t, uSpriteTexture_s, _e61);\n texel = _e62;\n let _e63 = texel;\n let _e64 = vTint_1;\n colour = (_e63 * _e64);\n let _e67 = colour[3u];\n if (_e67 < 0.00392157f) {\n discard;\n }\n let _e69 = colour;\n param_4 = _e69.xyz;\n let _e71 = applyOutputTransform_u0028_vf3_u003b((&param_4));\n let _e72 = colour;\n colour = vec4<f32>(_e71.x, _e71.y, _e71.z, _e72.w);\n let _e78 = colour;\n let _e81 = colour[3u];\n let _e82 = (_e78.xyz * _e81);\n let _e84 = colour[3u];\n fragColor = vec4<f32>(_e82.x, _e82.y, _e82.z, _e84);\n return;\n}\n\n@fragment \nfn main(@location(0) vUv: vec2<f32>, @location(1) vTint: vec4<f32>) -> @location(0) vec4<f32> {\n vUv_1 = vUv;\n vTint_1 = vTint;\n main_1();\n let _e5 = fragColor;\n return _e5;\n}\n";
9
9
 
10
10
  export const SPRITE_VERT_WGSL = "struct Uniforms {\n uToNdc0_: vec4<f32>,\n uToNdc1_: vec4<f32>,\n uClipCorrection: mat4x4<f32>,\n}\n\nstruct gl_PerVertex {\n @builtin(position) gl_Position: vec4<f32>,\n gl_PointSize: f32,\n}\n\nstruct VertexOutput {\n @location(0) member: vec2<f32>,\n @location(1) member_1: vec4<f32>,\n @builtin(position) gl_Position: vec4<f32>,\n}\n\nvar<private> gl_VertexIndex_1: i32;\nvar<private> aOrigin_1: vec2<f32>;\nvar<private> aEdges_1: vec4<f32>;\n@group(0) @binding(0) \nvar<uniform> unnamed: Uniforms;\nvar<private> vUv: vec2<f32>;\nvar<private> aUv_1: vec4<f32>;\nvar<private> vTint: vec4<f32>;\nvar<private> aTint_1: vec4<f32>;\nvar<private> unnamed_1: gl_PerVertex = gl_PerVertex(vec4<f32>(0f, 0f, 0f, 1f), 1f);\n\nfn main_1() {\n var corner: vec2<f32>;\n var indexable: array<vec2<f32>, 6>;\n var p: vec2<f32>;\n var ndc: vec2<f32>;\n var orientation: f32;\n var along: vec2<f32>;\n var local: f32;\n\n let _e31 = gl_VertexIndex_1;\n indexable = array<vec2<f32>, 6>(vec2<f32>(0f, 0f), vec2<f32>(1f, 0f), vec2<f32>(0f, 1f), vec2<f32>(0f, 1f), vec2<f32>(1f, 0f), vec2<f32>(1f, 1f));\n let _e33 = indexable[_e31];\n corner = _e33;\n let _e34 = aOrigin_1;\n let _e35 = aEdges_1;\n let _e38 = corner[0u];\n let _e41 = aEdges_1;\n let _e44 = corner[1u];\n p = ((_e34 + (_e35.xy * _e38)) + (_e41.zw * _e44));\n let _e49 = unnamed.uToNdc0_[0u];\n let _e51 = p[0u];\n let _e55 = unnamed.uToNdc0_[2u];\n let _e57 = p[1u];\n let _e62 = unnamed.uToNdc1_[0u];\n let _e66 = unnamed.uToNdc0_[1u];\n let _e68 = p[0u];\n let _e72 = unnamed.uToNdc0_[3u];\n let _e74 = p[1u];\n let _e79 = unnamed.uToNdc1_[1u];\n ndc = vec2<f32>((((_e49 * _e51) + (_e55 * _e57)) + _e62), (((_e66 * _e68) + (_e72 * _e74)) + _e79));\n let _e84 = unnamed.uToNdc0_[0u];\n let _e87 = unnamed.uToNdc0_[3u];\n let _e91 = unnamed.uToNdc0_[1u];\n let _e94 = unnamed.uToNdc0_[2u];\n orientation = ((_e84 * _e87) - (_e91 * _e94));\n let _e98 = corner[0u];\n let _e99 = orientation;\n if (_e99 > 0f) {\n let _e102 = corner[1u];\n local = (1f - _e102);\n } else {\n let _e105 = corner[1u];\n local = _e105;\n }\n let _e106 = local;\n along = vec2<f32>(_e98, _e106);\n let _e108 = aUv_1;\n let _e110 = aUv_1;\n let _e112 = along;\n vUv = mix(_e108.xy, _e110.zw, _e112);\n let _e114 = aTint_1;\n vTint = _e114;\n let _e116 = unnamed.uClipCorrection;\n let _e117 = ndc;\n unnamed_1.gl_Position = (_e116 * vec4<f32>(_e117.x, _e117.y, 0f, 1f));\n return;\n}\n\n@vertex \nfn main(@builtin(vertex_index) gl_VertexIndex: u32, @location(3) aOrigin: vec2<f32>, @location(0) aEdges: vec4<f32>, @location(1) aUv: vec4<f32>, @location(2) aTint: vec4<f32>) -> VertexOutput {\n gl_VertexIndex_1 = i32(gl_VertexIndex);\n aOrigin_1 = aOrigin;\n aEdges_1 = aEdges;\n aUv_1 = aUv;\n aTint_1 = aTint;\n main_1();\n let _e16 = unnamed_1.gl_Position.y;\n unnamed_1.gl_Position.y = -(_e16);\n let _e18 = vUv;\n let _e19 = vTint;\n let _e20 = unnamed_1.gl_Position;\n return VertexOutput(_e18, _e19, _e20);\n}\n";
11
11
 
package/src/spriteGl.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  /** The WebGL2 half of the sprite pass: one program, one instance buffer, one texture slot table. */
2
2
 
3
+ import { blendCovering } from '@driftengine/core';
4
+
3
5
  import { SPRITE_FRAG, SPRITE_VERT } from './shaders/sprite.ts';
4
6
  import { SPRITE_FLOATS } from './spriteBatch.ts';
5
7
  import type { SpriteBatch } from './spriteBatch.ts';
@@ -260,8 +262,9 @@ export function drawWebgl2Sprites(
260
262
  */
261
263
  gl.disable(gl.CULL_FACE);
262
264
  gl.enable(gl.BLEND);
263
- /* Premultiplied `over`: the fragment stage folds alpha in, so this composes onto opaque pixels. */
264
- gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
265
+ /* Premultiplied `over`: the fragment stage folds alpha in, so this composes onto opaque pixels,
266
+ and covers the scene's surface by its alpha so occlusion behind it does not darken it. */
267
+ blendCovering(gl, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
265
268
  gl.disable(gl.DEPTH_TEST);
266
269
  gl.depthMask(false);
267
270
  gl.activeTexture(gl.TEXTURE0 + SPRITE_UNIT);
package/src/spriteGpu.ts CHANGED
@@ -16,7 +16,12 @@ import type { SpriteBatch } from './spriteBatch.ts';
16
16
  * and nothing else. The alternative was a second copy of the mip blit living here, and the second
17
17
  * copy is the one that ends up disagreeing about the colour space.
18
18
  */
19
- import { generateMipChain, mipLevelCount, type MipPipelines } from '@driftengine/core';
19
+ import {
20
+ SCENE_ALPHA_COVERS,
21
+ generateMipChain,
22
+ mipLevelCount,
23
+ type MipPipelines,
24
+ } from '@driftengine/core';
20
25
  import type { SpriteImage, SpriteTextureOptions } from './spriteTexture.ts';
21
26
 
22
27
  const VERT = SPRITE_BINDINGS.SPRITE_VERT;
@@ -34,8 +39,24 @@ export interface GpuSpriteSlot {
34
39
  readonly bindGroup: GPUBindGroup;
35
40
  }
36
41
 
42
+ /** Pipelines by colour format, then depth format, then sample count: a lookup that allocates nothing. */
43
+ type PipelinesByTarget = Map<
44
+ GPUTextureFormat,
45
+ Map<GPUTextureFormat, Map<number, GPURenderPipeline>>
46
+ >;
47
+
37
48
  export interface GpuSprites {
38
- readonly pipeline: GPURenderPipeline;
49
+ /**
50
+ * One pipeline per pass the sprites have been drawn into, chosen at the draw by
51
+ * `spritePipelineFor`. A pipeline must match its pass's attachments exactly, and a 2D layer is
52
+ * drawn in the frame and over it, which are two different passes: the scene's target at the
53
+ * scene's samples, and the canvas at one.
54
+ */
55
+ readonly pipelines: PipelinesByTarget;
56
+ readonly pipelineLayout: GPUPipelineLayout;
57
+ readonly vertexModule: GPUShaderModule;
58
+ readonly fragmentModule: GPUShaderModule;
59
+ readonly label: string;
39
60
  readonly layout: GPUBindGroupLayout;
40
61
  readonly vertexUniforms: GPUBuffer;
41
62
  readonly fragmentUniforms: GPUBuffer;
@@ -83,52 +104,14 @@ export function createGpuSprites(
83
104
  ],
84
105
  });
85
106
 
86
- const pipeline = device.createRenderPipeline({
87
- label,
88
- layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
89
- vertex: {
90
- module: device.createShaderModule({ label: `${label}.vert`, code: SPRITE_VERT_WGSL }),
91
- entryPoint: 'main',
92
- buffers: [
93
- {
94
- arrayStride: STRIDE,
95
- stepMode: 'instance',
96
- attributes: [
97
- { shaderLocation: 0, offset: 0, format: 'float32x4' },
98
- { shaderLocation: 1, offset: 16, format: 'float32x4' },
99
- { shaderLocation: 2, offset: 32, format: 'float32x4' },
100
- { shaderLocation: 3, offset: 48, format: 'float32x2' },
101
- ],
102
- },
103
- ],
104
- },
105
- fragment: {
106
- module: device.createShaderModule({ label: `${label}.frag`, code: SPRITE_FRAG_WGSL }),
107
- entryPoint: 'main',
108
- targets: [
109
- {
110
- format,
111
- /* Premultiplied `over`, matching the WebGL2 half exactly. */
112
- blend: {
113
- color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
114
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
115
- },
116
- },
117
- ],
118
- },
119
- /*
120
- * Nothing is culled: a sprite is mirrored by giving it a negative width, which reverses its
121
- * winding, and a character facing left is exactly that.
122
- */
123
- primitive: { topology: 'triangle-list', cullMode: 'none' },
124
- multisample: { count: samples },
125
- /*
126
- * Neither tested nor written. The 2D layer's order *is* its layering — there is no z to sort
127
- * by — and a pass that depth-tested would let whatever 3D geometry is in the frame punch holes
128
- * in an overlay drawn over it. Declared all the same, because a pipeline in a render pass that
129
- * has a depth attachment must name its format.
130
- */
131
- depthStencil: { format: depthFormat, depthWriteEnabled: false, depthCompare: 'always' },
107
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
108
+ const vertexModule = device.createShaderModule({
109
+ label: `${label}.vert`,
110
+ code: SPRITE_VERT_WGSL,
111
+ });
112
+ const fragmentModule = device.createShaderModule({
113
+ label: `${label}.frag`,
114
+ code: SPRITE_FRAG_WGSL,
132
115
  });
133
116
 
134
117
  const vertexUniforms = device.createBuffer({
@@ -150,8 +133,12 @@ export function createGpuSprites(
150
133
  const vertexScratch = new ArrayBuffer(VERT.uniformSize);
151
134
  const fragmentScratch = new ArrayBuffer(FRAG.uniformSize);
152
135
 
153
- return {
154
- pipeline,
136
+ const sprites: GpuSprites = {
137
+ pipelines: new Map(),
138
+ pipelineLayout,
139
+ vertexModule,
140
+ fragmentModule,
141
+ label,
155
142
  layout,
156
143
  vertexUniforms,
157
144
  fragmentUniforms,
@@ -187,6 +174,87 @@ export function createGpuSprites(
187
174
  fragmentFloats: new Float32Array(fragmentScratch),
188
175
  fragmentInts: new Int32Array(fragmentScratch),
189
176
  };
177
+ /* The frame's own target, built now, so the common case compiles before its first draw. */
178
+ spritePipelineFor(device, sprites, format, depthFormat, samples);
179
+ return sprites;
180
+ }
181
+
182
+ /**
183
+ * The pipeline for a pass with these attachments, built the first time it is asked for.
184
+ *
185
+ * Chosen at the draw from what the pass is (`PassContext.format`, `depthFormat`, `samples`), so a
186
+ * 2D layer drawn over the finished frame, on the canvas at one sample, is not refused for having
187
+ * been built for the scene target. A lookup through three maps on every later draw, no allocation.
188
+ */
189
+ export function spritePipelineFor(
190
+ device: GPUDevice,
191
+ sprites: GpuSprites,
192
+ format: GPUTextureFormat,
193
+ depthFormat: GPUTextureFormat,
194
+ samples: number,
195
+ ): GPURenderPipeline {
196
+ let byDepth = sprites.pipelines.get(format);
197
+ if (byDepth === undefined) {
198
+ byDepth = new Map();
199
+ sprites.pipelines.set(format, byDepth);
200
+ }
201
+ let bySamples = byDepth.get(depthFormat);
202
+ if (bySamples === undefined) {
203
+ bySamples = new Map();
204
+ byDepth.set(depthFormat, bySamples);
205
+ }
206
+ const found = bySamples.get(samples);
207
+ if (found !== undefined) return found;
208
+ const pipeline = device.createRenderPipeline({
209
+ label: `${sprites.label}:${format}x${samples}`,
210
+ layout: sprites.pipelineLayout,
211
+ vertex: {
212
+ module: sprites.vertexModule,
213
+ entryPoint: 'main',
214
+ buffers: [
215
+ {
216
+ arrayStride: STRIDE,
217
+ stepMode: 'instance',
218
+ attributes: [
219
+ { shaderLocation: 0, offset: 0, format: 'float32x4' },
220
+ { shaderLocation: 1, offset: 16, format: 'float32x4' },
221
+ { shaderLocation: 2, offset: 32, format: 'float32x4' },
222
+ { shaderLocation: 3, offset: 48, format: 'float32x2' },
223
+ ],
224
+ },
225
+ ],
226
+ },
227
+ fragment: {
228
+ module: sprites.fragmentModule,
229
+ entryPoint: 'main',
230
+ targets: [
231
+ {
232
+ format,
233
+ /* Premultiplied `over`, matching the WebGL2 half exactly; a sprite covers the scene's
234
+ surface by its alpha, so occlusion behind it does not darken it. */
235
+ blend: {
236
+ color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
237
+ alpha: SCENE_ALPHA_COVERS,
238
+ },
239
+ },
240
+ ],
241
+ },
242
+ /*
243
+ * Nothing is culled: a sprite is mirrored by giving it a negative width, which reverses its
244
+ * winding, and a character facing left is exactly that.
245
+ */
246
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
247
+ multisample: { count: samples },
248
+ /*
249
+ * Neither tested nor written. The 2D layer's order *is* its layering — there is no z to sort
250
+ * by — and a pass that depth-tested would let whatever 3D geometry is in the frame punch holes
251
+ * in an overlay drawn over it. Declared all the same, because a pipeline in a render pass that
252
+ * has a depth attachment must name its format.
253
+ */
254
+ depthStencil: { format: depthFormat, depthWriteEnabled: false, depthCompare: 'always' },
255
+ });
256
+ bySamples.set(samples, pipeline);
257
+ return pipeline;
190
258
  }
191
259
 
192
260
  /**
@@ -313,9 +381,11 @@ export function drawGpuSprites(
313
381
  pass: GPURenderPassEncoder,
314
382
  sprites: GpuSprites,
315
383
  batch: SpriteBatch,
384
+ /** The pipeline for this pass's attachments: `spritePipelineFor`. */
385
+ pipeline: GPURenderPipeline,
316
386
  ): void {
317
387
  if (batch.count === 0) return;
318
- pass.setPipeline(sprites.pipeline);
388
+ pass.setPipeline(pipeline);
319
389
  for (let run = 0; run < batch.runCount; run += 1) {
320
390
  const at = run * 3;
321
391
  const slot = sprites.slots[batch.runs[at] as number];
package/src/spritePass.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  createGpuSprites,
21
21
  disposeGpuSprites,
22
22
  drawGpuSprites,
23
+ spritePipelineFor,
23
24
  setGpuSpriteTexture,
24
25
  setGpuWhiteTexture,
25
26
  uploadGpuInstances,
@@ -216,7 +217,13 @@ export function createSpritePass(options: SpritePassOptions = {}): SpritePass {
216
217
  device.queue.writeBuffer(gpu.vertexUniforms, 0, gpu.vertexScratch);
217
218
  device.queue.writeBuffer(gpu.fragmentUniforms, 0, gpu.fragmentScratch);
218
219
  uploadGpuInstances(device, gpu, batch);
219
- drawGpuSprites(ctx.pass, gpu, batch);
220
+ /* For the pass this lands in, which after `endFrame` is the canvas: see `spritePipelineFor`. */
221
+ drawGpuSprites(
222
+ ctx.pass,
223
+ gpu,
224
+ batch,
225
+ spritePipelineFor(device, gpu, ctx.format, ctx.depthFormat, ctx.samples),
226
+ );
220
227
  },
221
228
 
222
229
  dispose(): void {
package/src/uiDraw.ts CHANGED
@@ -5,6 +5,7 @@ import type { SpriteBatch, SpritePlacement } from './spriteBatch.ts';
5
5
  import { createSpriteFrame } from './spriteSheet.ts';
6
6
  import type { SpriteFrame } from './spriteSheet.ts';
7
7
  import type { UiNode, UiRect } from './uiNode.ts';
8
+ import { layersPresent } from './uiLayer.ts';
8
9
 
9
10
  /**
10
11
  * Where a caller draws what a quad cannot be.
@@ -61,9 +62,35 @@ export function drawUiTree(
61
62
  white: number,
62
63
  sink: UiContentSink | null,
63
64
  ): number {
64
- return drawWithin(batch, root, white, sink, -Infinity, -Infinity, Infinity, Infinity);
65
+ /*
66
+ * **Layer by layer, lowest first, and tree order within one.** A raised node — a dropdown that
67
+ * must cover the panel its control sits in — was drawn in tree order with everything else, so a
68
+ * later sibling covered it. Each layer is a walk of the whole tree with the clips it would have
69
+ * had anyway, so a raised node is still cut by a clipping ancestor; a tree with no raised node is
70
+ * one walk, as before.
71
+ */
72
+ const count = layersPresent(root, LAYERS);
73
+ let drawn = 0;
74
+ for (let at = 0; at < count; at += 1) {
75
+ drawn += drawWithin(
76
+ batch,
77
+ root,
78
+ white,
79
+ sink,
80
+ -Infinity,
81
+ -Infinity,
82
+ Infinity,
83
+ Infinity,
84
+ 0,
85
+ LAYERS[at] ?? 0,
86
+ );
87
+ }
88
+ return drawn;
65
89
  }
66
90
 
91
+ /** The layers of the tree being drawn, reused: this runs per frame. */
92
+ const LAYERS: number[] = [];
93
+
67
94
  function drawWithin(
68
95
  batch: SpriteBatch,
69
96
  node: UiNode,
@@ -73,8 +100,11 @@ function drawWithin(
73
100
  top: number,
74
101
  right: number,
75
102
  bottom: number,
103
+ inherited: number,
104
+ only: number,
76
105
  ): number {
77
106
  if (node.hidden) return 0;
107
+ const layer = node.layer === 0 ? inherited : node.layer;
78
108
  const rect = node.rect;
79
109
  const clipped = left !== -Infinity;
80
110
  const x0 = Math.max(rect.x, left);
@@ -87,7 +117,7 @@ function drawWithin(
87
117
  * a node is drawn as it always was, whatever its size. Its children are cut one by one either way,
88
118
  * since a child can lie outside its parent's box and still inside the clip.
89
119
  */
90
- if (!clipped || (x1 > x0 && y1 > y0)) {
120
+ if (layer === only && (!clipped || (x1 > x0 && y1 > y0))) {
91
121
  PLACEMENT.x = clipped ? x0 : rect.x;
92
122
  PLACEMENT.y = clipped ? y0 : rect.y;
93
123
  PLACEMENT.w = clipped ? x1 - x0 : rect.w;
@@ -115,7 +145,9 @@ function drawWithin(
115
145
  const ct = keep ? Math.max(top, rect.y) : top;
116
146
  const cr = keep ? Math.min(right, rect.x + rect.w) : right;
117
147
  const cb = keep ? Math.min(bottom, rect.y + rect.h) : bottom;
118
- for (const child of node.children) drawn += drawWithin(batch, child, white, sink, cl, ct, cr, cb);
148
+ for (const child of node.children) {
149
+ drawn += drawWithin(batch, child, white, sink, cl, ct, cr, cb, layer, only);
150
+ }
119
151
  return drawn;
120
152
  }
121
153
 
package/src/uiFocus.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { uiRectHolds } from './uiNode.ts';
4
4
  import type { UiNode } from './uiNode.ts';
5
+ import { layersPresent } from './uiLayer.ts';
5
6
 
6
7
  /**
7
8
  * The interactive node under a point, or `null`.
@@ -17,15 +18,30 @@ import type { UiNode } from './uiNode.ts';
17
18
  * Allocates nothing.
18
19
  */
19
20
  export function uiHitTest(root: UiNode, x: number, y: number): UiNode | null {
20
- if (root.hidden) return null;
21
+ /* The highest layer first, as it is drawn last: a raised popup takes the pointer over a later
22
+ sibling it covers. See `drawUiTree`. */
23
+ const count = layersPresent(root, HIT_LAYERS);
24
+ for (let at = count - 1; at >= 0; at -= 1) {
25
+ const hit = hitOn(root, x, y, 0, HIT_LAYERS[at] ?? 0);
26
+ if (hit !== null) return hit;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /** The layers of the tree being pointed at, reused: a hit test runs per pointer event. */
32
+ const HIT_LAYERS: number[] = [];
33
+
34
+ function hitOn(node: UiNode, x: number, y: number, inherited: number, only: number): UiNode | null {
35
+ if (node.hidden) return null;
21
36
  /* A clipping node bounds its descendants here as it does in the picture: a row scrolled out of a
22
37
  list is not under the pointer, whatever is drawn where it would have been. */
23
- if (root.clip && !uiRectHolds(root, x, y)) return null;
24
- for (let i = root.children.length - 1; i >= 0; i -= 1) {
25
- const hit = uiHitTest(root.children[i] as UiNode, x, y);
38
+ if (node.clip && !uiRectHolds(node, x, y)) return null;
39
+ const layer = node.layer === 0 ? inherited : node.layer;
40
+ for (let i = node.children.length - 1; i >= 0; i -= 1) {
41
+ const hit = hitOn(node.children[i] as UiNode, x, y, layer, only);
26
42
  if (hit !== null) return hit;
27
43
  }
28
- return root.interactive && uiRectHolds(root, x, y) ? root : null;
44
+ return layer === only && node.interactive && uiRectHolds(node, x, y) ? node : null;
29
45
  }
30
46
 
31
47
  /**
package/src/uiLayer.ts CHANGED
@@ -45,3 +45,26 @@ export function layerOrder(root: UiNode, out: UiNode[]): number {
45
45
  out.length = count;
46
46
  return count;
47
47
  }
48
+
49
+ /** Ascending, without allocating a comparator per call. */
50
+ const ASCENDING = (a: number, b: number): number => a - b;
51
+
52
+ /**
53
+ * The layers a tree draws on, ascending, into `out`; how many there are.
54
+ *
55
+ * What `drawUiTree` and `uiHitTest` walk a tree once per: a tree with no raised node answers one
56
+ * layer and is walked once, exactly as it was before either read a layer.
57
+ */
58
+ export function layersPresent(root: UiNode, out: number[]): number {
59
+ out.length = 0;
60
+ collect(root, 0, out);
61
+ if (out.length > 1) out.sort(ASCENDING);
62
+ return out.length;
63
+ }
64
+
65
+ function collect(node: UiNode, inherited: number, out: number[]): void {
66
+ if (node.hidden) return;
67
+ const layer = node.layer === 0 ? inherited : node.layer;
68
+ if (!out.includes(layer)) out.push(layer);
69
+ for (const child of node.children) collect(child, layer, out);
70
+ }