Skip to content

Instantly share code, notes, and snippets.

@greggman
Last active September 26, 2025 18:35
Show Gist options
  • Save greggman/0afbaac055980b6ce82cf47fca9d7bf7 to your computer and use it in GitHub Desktop.
Save greggman/0afbaac055980b6ce82cf47fca9d7bf7 to your computer and use it in GitHub Desktop.
WebGPU: Compat - test if having unused textures in the pipeline layout counts against the combo limit

WebGPU: Compat - test if having unused textures in the pipeline layout counts against the combo limit

view on jsgist

/*bug-in-github-api-content-can-not-be-empty*/
/*bug-in-github-api-content-can-not-be-empty*/
async function main() {
const adapter = await navigator.gpu.requestAdapter({featureLevel: 'compatibility'});
const device = await adapter.requestDevice();
const features = [...device.features];
if (features.length) {
console.error('There should be no features but these were found:', [...device.features].join('\n'));
return;
}
device.addEventListener('uncapturederror', e => console.error(e.error.message));
/*
maxCombinationsPerStage = min(maxSampledTexturesPerShaderStage, maxSamplersPerShaderStage)
for each stage of the pipeline:
sum = 0
for each texture binding in the pipeline layout which is visible to that stage:
sum += max(1, number of texture sampler combos for that texture binding)
for each external texture binding in the pipeline layout which is visible to that stage:
sum += 1 // for LUT texture + LUT sampler
sum += 3 * max(1, number of external_texture sampler combos) // for Y+U+V
if sum > maxCombinationsPerStage
generate a validation error.
*/
const maxCombos = Math.min(device.limits.maxSampledTexturesPerShaderStage, device.limits.maxSamplersPerShaderStage);
const numExternalCombos = (maxCombos - 1) / 3;
const numUsedByExternal = 1 + 3 * numExternalCombos;
const numExtraCombos = maxCombos - numUsedByExternal;
console.log('maxCombos:', maxCombos);
console.log('numExternalCombos:', numExternalCombos);
console.log('numCombosRemaining:', numExtraCombos);
const range = (num, fn) => new Array(num).fill(0).map((_, i) => fn(i));
const bgls = [
{
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
externalTexture: {},
},
...range(numExternalCombos, i => ({
binding: i + 1,
visibility: GPUShaderStage.COMPUTE,
sampler: {
type: "filtering",
},
})),
...range(3, i => ({
binding: numExternalCombos + i + 1,
visibility: GPUShaderStage.COMPUTE,
externalTexture: {},
})),
],
},
];
const code = `
@group(0) @binding(0) var tex: texture_external;
${range(numExternalCombos, i => `@group(0) @binding(${i + 1}) var smp${i + 1}: sampler;`).join('\n')}
@compute @workgroup_size(1) fn cs() {
${range(numExternalCombos, i => ` _ = textureSampleBaseClampToEdge(tex, smp${i + 1}, vec2f(0));`).join('\n')}
}
`;
console.log(code);
const module = device.createShaderModule({code});
const layout = device.createPipelineLayout({
bindGroupLayouts: bgls.map(bgl => { console.log(JSON.stringify(bgl, null, 2)); return device.createBindGroupLayout(bgl)}),
});
const pipeline = device.createComputePipeline({
layout,
compute: { module },
});
device.queue.submit([]);
}
main();
{"name":"WebGPU: Compat - test if having unused textures in the pipeline layout counts against the combo limit","settings":{},"filenames":["index.html","index.css","index.js"]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment