blob: aaa1188de0d08145841db6e865bb7027e3a02798 [file] [edit]
<!-- webkit-test-runner [ enableMetalShaderValidation=true ] -->
<body>
<script>
globalThis.testRunner?.dumpAsText();
globalThis.testRunner?.waitUntilDone();
// GPUDevice.createBindGroup reuses the last external texture bind group by rewriting its Metal
// argument buffer in place instead of building a new one. A GPURenderBundle bakes that argument
// buffer into an indirect command buffer and declares residency for its contents from a snapshot
// taken while recording, so once the bind group has been recorded into a render bundle the rewrite
// has to be refused - otherwise the bundle executes against textures it never declared resident,
// which Metal Shader Validation reports as a texture usage fault and which on hardware is an out of
// bounds read.
//
// Prints two lines: reuse must still happen while nothing consumes the argument buffer, and must
// not happen once a render bundle has recorded the bind group. The bundle is then executed and
// submitted so that a regression faults under Metal Shader Validation rather than only changing the
// printed text.
const log = [];
function out(text) { log.push(text); }
async function test() {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const module = device.createShaderModule({ code: `
@group(0) @binding(0) var t : texture_external;
@vertex fn vs(@builtin(vertex_index) i : u32) -> @builtin(position) vec4f {
var pos = array<vec2f, 3>(vec2f(-1, -3), vec2f(3, 1), vec2f(-1, 1));
return vec4f(pos[i], 0, 1);
}
@fragment fn fs() -> @location(0) vec4f {
return textureLoad(t, vec2u(0, 0));
}
` });
const bindGroupLayout = device.createBindGroupLayout({
entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, externalTexture: { } }],
});
const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format: 'rgba8unorm' }] },
});
const videoFrame = new VideoFrame(new ArrayBuffer(16), { codedWidth: 2, codedHeight: 2, format: 'BGRA', timestamp: 0 });
const externalTexture = device.importExternalTexture({ source: videoFrame, colorSpace: 'srgb' });
const descriptor = { layout: bindGroupLayout, entries: [{ binding: 0, resource: externalTexture }] };
const first = device.createBindGroup(descriptor);
// Nothing references the argument buffer yet, so the in place update is still allowed. This arm
// fails if the render bundle guard is too strict.
const reused = device.createBindGroup(descriptor);
out('reused with no render bundle recorded: ' + (reused === first));
const bundleEncoder = device.createRenderBundleEncoder({ colorFormats: ['rgba8unorm'] });
bundleEncoder.setPipeline(pipeline);
bundleEncoder.setBindGroup(0, reused);
bundleEncoder.draw(3);
const bundle = bundleEncoder.finish();
// The bundle's indirect command buffer now references the argument buffer of `reused`, so the
// in place update has to be refused and a fresh bind group returned instead.
const afterBundle = device.createBindGroup(descriptor);
out('reused after the bind group was recorded into a render bundle: ' + (afterBundle === reused));
const target = device.createTexture({
size: [2, 2],
format: 'rgba8unorm',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
const commandEncoder = device.createCommandEncoder();
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [{ view: target.createView(), loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 1] }],
});
renderPass.executeBundles([bundle]);
renderPass.end();
device.queue.submit([commandEncoder.finish()]);
await device.queue.onSubmittedWorkDone();
videoFrame.close();
}
test().catch(error => out('threw: ' + error)).finally(() => {
const pre = document.createElement('pre');
pre.textContent = log.join('\n');
document.body.appendChild(pre);
globalThis.testRunner?.notifyDone();
});
</script>
</body>