|
| 1 | +export const description = ` |
| 2 | +Tests for capability checking for the 'texture-component-swizzle' feature. |
| 3 | +
|
| 4 | +Test that when the feature is off, swizzling is not allowed, even the identity swizzle. |
| 5 | +When the feature is on, swizzling is applied correctly. |
| 6 | +`; |
| 7 | + |
| 8 | +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; |
| 9 | +import { GPUConst } from '../../../../constants.js'; |
| 10 | +import { EncodableTextureFormat, isSintOrUintFormat } from '../../../../format_info.js'; |
| 11 | +import { UniqueFeaturesOrLimitsGPUTest } from '../../../../gpu_test.js'; |
| 12 | +import { convertPerTexelComponentToResultFormat } from '../../../../shader/execution/expression/call/builtin/texture_utils.js'; |
| 13 | +import * as ttu from '../../../../texture_test_utils.js'; |
| 14 | +import { PerTexelComponent } from '../../../../util/texture/texel_data.js'; |
| 15 | +import { TexelView } from '../../../../util/texture/texel_view.js'; |
| 16 | +import { createTextureFromTexelViews } from '../../../../util/texture.js'; |
| 17 | + |
| 18 | +export const g = makeTestGroup(UniqueFeaturesOrLimitsGPUTest); |
| 19 | + |
| 20 | +// MAINTENANCE_TODO: Remove these types once texture-component-swizzle is added to @webgpu/types |
| 21 | +/* prettier-ignore */ |
| 22 | +type GPUComponentSwizzle = |
| 23 | + | 'zero' // Force its value to 0. |
| 24 | + | 'one' // Force its value to 1. |
| 25 | + | 'r' // Take its value from the red channel of the texture. |
| 26 | + | 'g' // Take its value from the green channel of the texture. |
| 27 | + | 'b' // Take its value from the blue channel of the texture. |
| 28 | + | 'a' // Take its value from the alpha channel of the texture. |
| 29 | + ; |
| 30 | + |
| 31 | +type GPUTextureComponentSwizzle = { |
| 32 | + r?: GPUComponentSwizzle; |
| 33 | + g?: GPUComponentSwizzle; |
| 34 | + b?: GPUComponentSwizzle; |
| 35 | + a?: GPUComponentSwizzle; |
| 36 | +}; |
| 37 | + |
| 38 | +declare global { |
| 39 | + interface GPUTextureViewDescriptor { |
| 40 | + swizzle?: GPUTextureComponentSwizzle | undefined; |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +// Note: There are 4 settings with 7 options each including undefined |
| 45 | +// which is 2401 combinations. So we don't check them all. Just a few below. |
| 46 | +const kSwizzleTests = [ |
| 47 | + 'uuuu', |
| 48 | + 'rgba', |
| 49 | + '0000', |
| 50 | + '1111', |
| 51 | + 'rrrr', |
| 52 | + 'gggg', |
| 53 | + 'bbbb', |
| 54 | + 'aaaa', |
| 55 | + 'abgr', |
| 56 | + 'gbar', |
| 57 | + 'barg', |
| 58 | + 'argb', |
| 59 | + '0gba', |
| 60 | + 'r0ba', |
| 61 | + 'rg0a', |
| 62 | + 'rgb0', |
| 63 | + '1gba', |
| 64 | + 'r1ba', |
| 65 | + 'rg1a', |
| 66 | + 'rgb1', |
| 67 | + 'ubga', |
| 68 | + 'ruga', |
| 69 | + 'rbua', |
| 70 | + 'rbgu', |
| 71 | +] as const; |
| 72 | + |
| 73 | +const kSwizzleLetterToComponent: Record<string, GPUComponentSwizzle | undefined> = { |
| 74 | + u: undefined, |
| 75 | + r: 'r', |
| 76 | + g: 'g', |
| 77 | + b: 'b', |
| 78 | + a: 'a', |
| 79 | + '0': 'zero', |
| 80 | + '1': 'one', |
| 81 | +} as const; |
| 82 | + |
| 83 | +const kComponents = ['r', 'g', 'b', 'a'] as const; |
| 84 | + |
| 85 | +function swizzleSpecToGPUTextureComponentSwizzle(spec: string): GPUTextureComponentSwizzle { |
| 86 | + const swizzle: Record<string, string> = {}; |
| 87 | + kComponents.forEach((component, i) => { |
| 88 | + const v = kSwizzleLetterToComponent[spec[i]]; |
| 89 | + if (v) { |
| 90 | + swizzle[component] = v; |
| 91 | + } |
| 92 | + }); |
| 93 | + return swizzle as GPUTextureComponentSwizzle; |
| 94 | +} |
| 95 | + |
| 96 | +function swizzleComponentToTexelComponent( |
| 97 | + src: PerTexelComponent<number>, |
| 98 | + component: GPUComponentSwizzle |
| 99 | +): number { |
| 100 | + switch (component) { |
| 101 | + case 'zero': |
| 102 | + return 0; |
| 103 | + case 'one': |
| 104 | + return 1; |
| 105 | + case 'r': |
| 106 | + return src.R!; |
| 107 | + case 'g': |
| 108 | + return src.G!; |
| 109 | + case 'b': |
| 110 | + return src.B!; |
| 111 | + case 'a': |
| 112 | + return src.A!; |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +function swizzleTexel( |
| 117 | + src: PerTexelComponent<number>, |
| 118 | + swizzle: GPUTextureComponentSwizzle |
| 119 | +): PerTexelComponent<number> { |
| 120 | + return { |
| 121 | + R: swizzle.r ? swizzleComponentToTexelComponent(src, swizzle.r) : src.R, |
| 122 | + G: swizzle.g ? swizzleComponentToTexelComponent(src, swizzle.g) : src.G, |
| 123 | + B: swizzle.b ? swizzleComponentToTexelComponent(src, swizzle.b) : src.B, |
| 124 | + A: swizzle.a ? swizzleComponentToTexelComponent(src, swizzle.a) : src.A, |
| 125 | + }; |
| 126 | +} |
| 127 | + |
| 128 | +function isIdentitySwizzle(swizzle: GPUTextureComponentSwizzle): boolean { |
| 129 | + return ( |
| 130 | + (swizzle.r === undefined || swizzle.r === 'r') && |
| 131 | + (swizzle.g === undefined || swizzle.g === 'g') && |
| 132 | + (swizzle.b === undefined || swizzle.b === 'b') && |
| 133 | + (swizzle.a === undefined || swizzle.a === 'a') |
| 134 | + ); |
| 135 | +} |
| 136 | + |
| 137 | +function normalizeSwizzle(swizzle: GPUTextureComponentSwizzle): GPUTextureComponentSwizzle { |
| 138 | + return { |
| 139 | + r: swizzle.r ?? 'r', |
| 140 | + g: swizzle.g ?? 'g', |
| 141 | + b: swizzle.b ?? 'b', |
| 142 | + a: swizzle.a ?? 'a', |
| 143 | + }; |
| 144 | +} |
| 145 | + |
| 146 | +function swizzlesAreTheSame(a: GPUTextureComponentSwizzle, b: GPUTextureComponentSwizzle): boolean { |
| 147 | + a = normalizeSwizzle(a); |
| 148 | + b = normalizeSwizzle(b); |
| 149 | + return a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a; |
| 150 | +} |
| 151 | + |
| 152 | +g.test('no_swizzle') |
| 153 | + .desc( |
| 154 | + ` |
| 155 | + Test that if texture-component-swizzle is not enabled, having a swizzle property generates a validation error. |
| 156 | + ` |
| 157 | + ) |
| 158 | + .fn(t => { |
| 159 | + const texture = t.createTextureTracked({ |
| 160 | + format: 'rgba8unorm', |
| 161 | + size: [1], |
| 162 | + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, |
| 163 | + }); |
| 164 | + t.expectValidationError(() => { |
| 165 | + texture.createView({ swizzle: {} }); |
| 166 | + }); |
| 167 | + }); |
| 168 | + |
| 169 | +g.test('no_render_nor_storage') |
| 170 | + .desc( |
| 171 | + ` |
| 172 | + Test that setting the swizzle on the texture with RENDER_ATTACHMENT or STORAGE_BINDING usage works |
| 173 | + if the swizzle is the identity but generates a validation error otherwise. |
| 174 | + ` |
| 175 | + ) |
| 176 | + .params(u => |
| 177 | + u |
| 178 | + .combine('usage', [ |
| 179 | + GPUConst.TextureUsage.COPY_SRC, |
| 180 | + GPUConst.TextureUsage.COPY_DST, |
| 181 | + GPUConst.TextureUsage.TEXTURE_BINDING, |
| 182 | + GPUConst.TextureUsage.RENDER_ATTACHMENT, |
| 183 | + GPUConst.TextureUsage.STORAGE_BINDING, |
| 184 | + ] as const) |
| 185 | + .beginSubcases() |
| 186 | + .combine('swizzleSpec', kSwizzleTests) |
| 187 | + ) |
| 188 | + .beforeAllSubcases(t => { |
| 189 | + // MAINTENANCE_TODO: Remove this cast once texture-component-swizzle is added to @webgpu/types |
| 190 | + t.selectDeviceOrSkipTestCase('texture-component-swizzle' as GPUFeatureName); |
| 191 | + }) |
| 192 | + .fn(t => { |
| 193 | + const { swizzleSpec, usage } = t.params; |
| 194 | + const swizzle = swizzleSpecToGPUTextureComponentSwizzle(swizzleSpec); |
| 195 | + const texture = t.createTextureTracked({ |
| 196 | + format: 'rgba8unorm', |
| 197 | + size: [1], |
| 198 | + usage, |
| 199 | + }); |
| 200 | + const badUsage = |
| 201 | + (usage & |
| 202 | + (GPUConst.TextureUsage.RENDER_ATTACHMENT | GPUConst.TextureUsage.STORAGE_BINDING)) !== |
| 203 | + 0; |
| 204 | + const shouldError = badUsage && !isIdentitySwizzle(swizzle); |
| 205 | + t.expectValidationError(() => { |
| 206 | + texture.createView({ swizzle }); |
| 207 | + }, shouldError); |
| 208 | + }); |
| 209 | + |
| 210 | +g.test('read_swizzle') |
| 211 | + .desc( |
| 212 | + ` |
| 213 | + Test reading textures with swizzles. |
| 214 | + * Test that multiple swizzles of the same texture work. |
| 215 | + * Test that multiple swizzles of the same fails in compat if the swizzles are different. |
| 216 | + ` |
| 217 | + ) |
| 218 | + .params(u => |
| 219 | + u |
| 220 | + .combine('format', [ |
| 221 | + 'rgba8unorm', |
| 222 | + 'bgra8unorm', |
| 223 | + 'r8unorm', |
| 224 | + 'rg8unorm', |
| 225 | + 'r8uint', |
| 226 | + 'rgba8uint', |
| 227 | + ] as const) |
| 228 | + .beginSubcases() |
| 229 | + .combine('sampled', [false, true] as const) |
| 230 | + .combine('swizzleSpec', kSwizzleTests) |
| 231 | + .combine('otherSwizzleIndexOffset', [0, 1, 5]) // used to choose a different 2nd swizzle. 0 = same swizzle as 1st |
| 232 | + .unless(t => isSintOrUintFormat(t.format) && t.sampled) |
| 233 | + ) |
| 234 | + .beforeAllSubcases(t => { |
| 235 | + // MAINTENANCE_TODO: Remove this cast once texture-component-swizzle is added to @webgpu/types |
| 236 | + t.selectDeviceOrSkipTestCase('texture-component-swizzle' as GPUFeatureName); |
| 237 | + }) |
| 238 | + .fn(t => { |
| 239 | + const { format, sampled, swizzleSpec, otherSwizzleIndexOffset } = t.params; |
| 240 | + |
| 241 | + const isIntFormat = isSintOrUintFormat(format); |
| 242 | + const srcColor = isIntFormat |
| 243 | + ? { R: 20, G: 40, B: 60, A: 80 } |
| 244 | + : { R: 0.2, G: 0.4, B: 0.6, A: 0.8 }; |
| 245 | + const srcTexelView = TexelView.fromTexelsAsColors(format, _coords => srcColor); |
| 246 | + |
| 247 | + const texture = createTextureFromTexelViews(t, [srcTexelView], { |
| 248 | + format, |
| 249 | + size: [1], |
| 250 | + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, |
| 251 | + }); |
| 252 | + |
| 253 | + const otherSwizzleSpec = |
| 254 | + kSwizzleTests[ |
| 255 | + (kSwizzleTests.indexOf(swizzleSpec) + otherSwizzleIndexOffset) % kSwizzleTests.length |
| 256 | + ]; |
| 257 | + |
| 258 | + const expFormat: EncodableTextureFormat = isIntFormat ? 'rgba32uint' : 'rgba32float'; |
| 259 | + const data = [swizzleSpec, otherSwizzleSpec].map(swizzleSpec => { |
| 260 | + const swizzle = swizzleSpecToGPUTextureComponentSwizzle(swizzleSpec); |
| 261 | + const expColor = swizzleTexel( |
| 262 | + convertPerTexelComponentToResultFormat(srcColor, format), |
| 263 | + swizzle |
| 264 | + ); |
| 265 | + const expTexelView = TexelView.fromTexelsAsColors(expFormat, _coords => expColor); |
| 266 | + const textureView = texture.createView({ swizzle }); |
| 267 | + return { swizzle, expFormat, expTexelView, textureView }; |
| 268 | + }); |
| 269 | + |
| 270 | + const loadWGSL = sampled |
| 271 | + ? (v: number) => `textureSampleLevel(tex${v}, smp, vec2f(0), 0)` |
| 272 | + : (v: number) => `textureLoad(tex${v}, vec2u(0), 0)`; |
| 273 | + const module = t.device.createShaderModule({ |
| 274 | + code: ` |
| 275 | + // These are intentionally in different bindGroups to test in compat that different swizzles |
| 276 | + // of the same texture are not allowed. |
| 277 | + @group(0) @binding(0) var tex0: texture_2d<${isIntFormat ? 'u32' : 'f32'}>; |
| 278 | + @group(1) @binding(0) var tex1: texture_2d<${isIntFormat ? 'u32' : 'f32'}>; |
| 279 | + @group(0) @binding(1) var smp: sampler; |
| 280 | + @group(0) @binding(2) var result: texture_storage_2d<${expFormat}, write>; |
| 281 | +
|
| 282 | + @compute @workgroup_size(1) fn cs() { |
| 283 | + _ = smp; |
| 284 | + let c0 = ${loadWGSL(0)}; |
| 285 | + let c1 = ${loadWGSL(1)}; |
| 286 | + textureStore(result, vec2u(0, 0), c0); |
| 287 | + textureStore(result, vec2u(1, 0), c1); |
| 288 | + } |
| 289 | + `, |
| 290 | + }); |
| 291 | + |
| 292 | + const pipeline = t.device.createComputePipeline({ |
| 293 | + layout: 'auto', |
| 294 | + compute: { module }, |
| 295 | + }); |
| 296 | + |
| 297 | + const outputTexture = t.createTextureTracked({ |
| 298 | + format: expFormat, |
| 299 | + size: [2], |
| 300 | + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.STORAGE_BINDING, |
| 301 | + }); |
| 302 | + |
| 303 | + const sampler = t.device.createSampler(); |
| 304 | + |
| 305 | + const bindGroup0 = t.device.createBindGroup({ |
| 306 | + layout: pipeline.getBindGroupLayout(0), |
| 307 | + entries: [ |
| 308 | + { binding: 0, resource: data[0].textureView }, |
| 309 | + { binding: 1, resource: sampler }, |
| 310 | + { binding: 2, resource: outputTexture }, |
| 311 | + ], |
| 312 | + }); |
| 313 | + |
| 314 | + const bindGroup1 = t.device.createBindGroup({ |
| 315 | + layout: pipeline.getBindGroupLayout(1), |
| 316 | + entries: [{ binding: 0, resource: data[1].textureView }], |
| 317 | + }); |
| 318 | + |
| 319 | + const encoder = t.device.createCommandEncoder(); |
| 320 | + const pass = encoder.beginComputePass(); |
| 321 | + pass.setPipeline(pipeline); |
| 322 | + pass.setBindGroup(0, bindGroup0); |
| 323 | + pass.setBindGroup(1, bindGroup1); |
| 324 | + pass.dispatchWorkgroups(1); |
| 325 | + pass.end(); |
| 326 | + |
| 327 | + if (t.isCompatibility && !swizzlesAreTheSame(data[0].swizzle, data[1].swizzle)) { |
| 328 | + // Swizzles can not be different in compatibility mode |
| 329 | + t.expectValidationError(() => { |
| 330 | + t.device.queue.submit([encoder.finish()]); |
| 331 | + }); |
| 332 | + } else { |
| 333 | + t.device.queue.submit([encoder.finish()]); |
| 334 | + |
| 335 | + data.forEach(({ expTexelView }, i) => { |
| 336 | + if (i === 0) return; |
| 337 | + ttu.expectTexelViewComparisonIsOkInTexture( |
| 338 | + t, |
| 339 | + { texture: outputTexture, origin: [i, 0, 0] }, |
| 340 | + expTexelView, |
| 341 | + [1, 1, 1] |
| 342 | + ); |
| 343 | + }); |
| 344 | + } |
| 345 | + }); |
0 commit comments