-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Test texture+shader combo usage. (#4063)
Note: this test would have surfaced a compat issue if it already existed. The issue gpuweb/gpuweb#4989 was raised and this test tests in both compat and core that an implemation can use all the texture and sample combinations it is supposed to.
- Loading branch information
Showing
1 changed file
with
270 additions
and
0 deletions.
There are no files selected for viewing
270 changes: 270 additions & 0 deletions
270
src/webgpu/api/operation/sampling/sampler_texture.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
export const description = ` | ||
Tests samplers with textures. | ||
- test that you can use the maximum number of textures | ||
with the maximum number of samplers. | ||
`; | ||
|
||
import { makeTestGroup } from '../../../../common/framework/test_group.js'; | ||
import { assert, range } from '../../../../common/util/util.js'; | ||
import { MaxLimitsTest, TextureTestMixin } from '../../../gpu_test.js'; | ||
import { TexelView } from '../../../util/texture/texel_view.js'; | ||
|
||
export const g = makeTestGroup(TextureTestMixin(MaxLimitsTest)); | ||
|
||
g.test('sample_texture_combos') | ||
.desc( | ||
` | ||
Test that you can use the maximum number of textures with the maximum number of samplers. | ||
The test works by making the maximum number of texture+sampler combos. | ||
Each texture is [maxSamplersPerShaderStage, 1] in size where each texel is [textureId, samplerId] | ||
A function "useCombo<StageNum>(comboId)" is made that returns stage[stageNum].combo[comboId].texel[id, 0] | ||
or to put it another way, it returns the nth texel from the nth combo for that stage. | ||
These are read in both the vertex shader and fragment shader and written to a [maxSamplerPerShaderStage, 2] | ||
texture where the top row is the values from the vertex shader and the bottom row from the fragment shader. | ||
The result should be a texture that has a value in each texel unique to a particular combo | ||
` | ||
) | ||
.fn(t => { | ||
const { device } = t; | ||
const { maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage, maxBindingsPerBindGroup } = | ||
device.limits; | ||
|
||
assert(maxSampledTexturesPerShaderStage < 0xfffe); | ||
assert(maxSamplersPerShaderStage < 0xfffe); | ||
|
||
const maxTestableCombosPerStage = t.isCompatibility | ||
? Math.min(maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage) | ||
: maxSampledTexturesPerShaderStage * maxSamplersPerShaderStage; | ||
|
||
const textures: GPUTexture[] = []; | ||
const declarationLines: string[] = []; | ||
const groups: GPUBindGroupEntry[][] = [[]]; | ||
const layouts: GPUBindGroupLayoutEntry[][] = [[]]; | ||
const textureIds = new Set<string>(); | ||
const samplerIds = new Set<string>(); | ||
// per stage, per texel, each texel has 2 numbers, the texture id, and sampler id | ||
const expected: number[][][] = [[], []]; | ||
|
||
function addResource(stage: number, resourceId: string, resource: GPUTextureView | GPUSampler) { | ||
let bindGroupEntries = groups[groups.length - 1]; | ||
let bindGroupLayoutEntries = layouts[groups.length - 1]; | ||
if (bindGroupEntries.length === maxBindingsPerBindGroup) { | ||
bindGroupEntries = []; | ||
bindGroupLayoutEntries = []; | ||
groups.push(bindGroupEntries); | ||
layouts.push(bindGroupLayoutEntries); | ||
} | ||
const resourceType = resource instanceof GPUSampler ? 'sampler' : 'texture_2d<f32>'; | ||
const binding = bindGroupEntries.length; | ||
declarationLines.push( | ||
` @group(${groups.length - 1}) @binding(${binding}) var ${resourceId}: ${resourceType};` | ||
); | ||
bindGroupEntries.push({ | ||
binding, | ||
resource, | ||
}); | ||
bindGroupLayoutEntries.push({ | ||
binding, | ||
visibility: stage === 0 ? GPUShaderStage.VERTEX : GPUShaderStage.FRAGMENT, | ||
...(resource instanceof GPUSampler | ||
? { | ||
sampler: {}, | ||
} | ||
: { | ||
texture: {}, | ||
}), | ||
}); | ||
} | ||
|
||
function addTexture(stage: number, textureNum: number) { | ||
const textureId = `tex${stage}_${textureNum}`; | ||
if (!textureIds.has(textureId)) { | ||
textureIds.add(textureId); | ||
const texture = t.createTextureTracked({ | ||
format: 'rgba8unorm', | ||
size: [maxSamplersPerShaderStage, 1], | ||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, | ||
}); | ||
textures.push(texture); | ||
// Encode an rgba8unorm texture with rg16uint data where each texel is | ||
// [(textureId + 1) | (stage << 15), {samplerId + 1}] | ||
// The +1 is to avoid 0. | ||
const data = new Uint16Array(maxSamplersPerShaderStage * 2); | ||
const rg = (textureNum + 1) | (stage << 15); | ||
for (let x = 0; x < maxSamplersPerShaderStage; ++x) { | ||
const offset = x * 2; | ||
const samplerNum = x + 1; | ||
data[offset + 0] = rg; | ||
data[offset + 1] = samplerNum; | ||
} | ||
device.queue.writeTexture({ texture }, data, {}, [maxSamplersPerShaderStage]); | ||
addResource(stage, textureId, texture.createView()); | ||
} | ||
return textureId; | ||
} | ||
|
||
const kAddressModes = ['repeat', 'clamp-to-edge', 'mirror-repeat'] as const; | ||
const getAddressMode = (hash: number, depth: number) => { | ||
return kAddressModes[ | ||
((hash / Math.pow(kAddressModes.length, depth)) | 0) % kAddressModes.length | ||
]; | ||
}; | ||
|
||
function addSampler(stage: number, samplerNum: number) { | ||
const samplerId = `smp${stage}_${samplerNum}`; | ||
if (!samplerIds.has(samplerId)) { | ||
const samplerNum = samplerIds.size; | ||
samplerIds.add(samplerId); | ||
// try to make each sampler unique. This is because some backends | ||
// coalesce samplers with the same settings. | ||
const addressHash = samplerNum >> 3; | ||
const sampler = device.createSampler({ | ||
minFilter: samplerNum & 1 ? 'linear' : 'nearest', | ||
magFilter: samplerNum & 2 ? 'linear' : 'nearest', | ||
mipmapFilter: samplerNum & 4 ? 'linear' : 'nearest', | ||
addressModeU: getAddressMode(addressHash, 0), | ||
addressModeV: getAddressMode(addressHash, 1), | ||
addressModeW: getAddressMode(addressHash, 2), | ||
}); | ||
addResource(stage, samplerId, sampler); | ||
} | ||
return samplerId; | ||
} | ||
|
||
// Note: We are storing textureId, samplerId in the texture. That suggests we could use rgba32uint | ||
// texture but we can't do that because we want to be able to set the samplers to linear. | ||
// Similarly we can't use rgba32float since they're not filterable by default. | ||
// So, we encode via rgba8unorm where rg is a 16bit textureId and ba is a 16bit samplerId | ||
const code = ` | ||
// maxTestableCombosPerStage: ${maxTestableCombosPerStage} | ||
fn sample(t: texture_2d<f32>, s: sampler, validId: u32, currentId: u32, c: vec4f) -> vec4f { | ||
let size = textureDimensions(t, 0); | ||
let uv = vec2f((f32(currentId) + 0.5) / f32(size.x), 0.5); | ||
let v = textureSampleLevel(t, s, uv, 0); | ||
return select(c, v, currentId == validId); | ||
} | ||
${range( | ||
2, | ||
stage => ` | ||
fn useCombos${stage}(id: u32) -> vec4f { | ||
var c: vec4f; | ||
${range(maxTestableCombosPerStage, i => { | ||
const texNum = (i / maxSamplersPerShaderStage) | 0; | ||
const textureId = addTexture(stage, texNum); | ||
const smpNum = i % maxSamplersPerShaderStage; | ||
const samplerId = addSampler(stage, smpNum); | ||
expected[stage].push([(texNum + 1) | (stage << 15), smpNum + 1]); | ||
return ` c = sample(${textureId}, ${samplerId}, ${i}, id, c);`; | ||
}).join('\n')} | ||
return c; | ||
} | ||
` | ||
).join('\n\n')} | ||
${declarationLines.join('\n')} | ||
struct VOut { | ||
@builtin(position) pos: vec4f, | ||
@location(0) value: vec4f, | ||
}; | ||
@vertex fn vs(@builtin(instance_index) iNdx: u32) -> VOut { | ||
return VOut( | ||
vec4f(0, 0, 0, 1), | ||
useCombos0(iNdx), | ||
); | ||
} | ||
@fragment fn fs(vin: VOut) -> @location(0) vec4u { | ||
let ndx = u32(vin.pos.x); | ||
let f = select(vin.value, useCombos1(ndx), vin.pos.y > 1.0); | ||
// We're putting two u16 values in the source data but as rgba8unorm. | ||
// Convert them back to u32 then split them back into two u16s | ||
let bytes = pack4x8unorm(f); | ||
return vec4u(bytes & 0xffff, bytes >> 16, 0, 0); | ||
} | ||
`; | ||
|
||
const module = device.createShaderModule({ code }); | ||
const bindGroupLayouts = layouts.map(entries => device.createBindGroupLayout({ entries })); | ||
|
||
const pipeline = device.createRenderPipeline({ | ||
layout: device.createPipelineLayout({ bindGroupLayouts }), | ||
vertex: { | ||
module, | ||
}, | ||
fragment: { | ||
module, | ||
targets: [{ format: 'rg16uint' }], | ||
}, | ||
primitive: { topology: 'point-list' }, | ||
}); | ||
|
||
const bindGroups = groups.map((entries, i) => | ||
device.createBindGroup({ | ||
layout: pipeline.getBindGroupLayout(i), | ||
entries, | ||
}) | ||
); | ||
|
||
const renderTarget = t.createTextureTracked({ | ||
format: 'rg16uint', | ||
size: [maxTestableCombosPerStage, 2], | ||
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, | ||
}); | ||
textures.push(renderTarget); | ||
|
||
const encoder = device.createCommandEncoder(); | ||
const pass = encoder.beginRenderPass({ | ||
colorAttachments: [ | ||
{ | ||
view: renderTarget.createView(), | ||
loadOp: 'clear', | ||
storeOp: 'store', | ||
}, | ||
], | ||
}); | ||
pass.setPipeline(pipeline); | ||
bindGroups.forEach((bindGroup, i) => pass.setBindGroup(i, bindGroup)); | ||
for (let y = 0; y < 2; ++y) { | ||
for (let x = 0; x < maxTestableCombosPerStage; ++x) { | ||
pass.setViewport(x, y, 1, 1, 0, 1); | ||
pass.draw(1, 1, 0, x); | ||
} | ||
} | ||
pass.end(); | ||
|
||
device.queue.submit([encoder.finish()]); | ||
|
||
const expectedData = new Uint16Array(maxTestableCombosPerStage * 2 * 2); | ||
for (let stage = 0; stage < 2; ++stage) { | ||
expected[stage].forEach(([tid, sid], i) => { | ||
const offset = (maxTestableCombosPerStage * stage + i) * 2; | ||
expectedData[offset + 0] = tid; | ||
expectedData[offset + 1] = sid; | ||
}); | ||
} | ||
|
||
const expTexelView = TexelView.fromTextureDataByReference( | ||
'rg16uint', | ||
new Uint8Array(expectedData.buffer), | ||
{ | ||
bytesPerRow: maxTestableCombosPerStage * 4, | ||
rowsPerImage: 2, | ||
subrectOrigin: [0, 0, 0], | ||
subrectSize: [maxTestableCombosPerStage, 2], | ||
} | ||
); | ||
|
||
const size = [maxSamplersPerShaderStage, 2]; | ||
t.expectTexelViewComparisonIsOkInTexture({ texture: renderTarget }, expTexelView, size); | ||
|
||
textures.forEach(texture => texture.destroy()); | ||
}); |