blob: e752d90f163a5dac8d1b2a26d522e18e1acf594e [file] [edit]
<script src="../../resources/js-test.js"></script>
<script>
jsTestIsAsync = true;
description("Tests the WebGPU 'dual-source-blending' feature and the WGSL dual_source_blending extension. " +
"Renders a full-screen triangle whose fragment shader writes @blend_src(0) and @blend_src(1) " +
"outputs, blended with srcFactor 'src1' and dstFactor 'one-minus-src1', and verifies the " +
"resulting pixel. Also checks the related validation rules.");
const SHADER = `
enable dual_source_blending;
@vertex
fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4f {
var positions = array<vec2f, 3>(
vec2f(-1.0, -1.0),
vec2f( 3.0, -1.0),
vec2f(-1.0, 3.0),
);
return vec4f(positions[vid], 0.0, 1.0);
}
struct FragOut {
@location(0) @blend_src(0) color: vec4f,
@location(0) @blend_src(1) blend: vec4f,
};
@fragment
fn fs_main() -> FragOut {
var out: FragOut;
out.color = vec4f(1.0, 1.0, 0.0, 1.0);
out.blend = vec4f(1.0, 0.0, 0.0, 0.0);
return out;
}
@fragment
fn fs_single() -> @location(0) vec4f {
return vec4f(1.0);
}
`;
const SINGLE_OUTPUT_SHADER = `
@vertex
fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4f {
return vec4f(0.0, 0.0, 0.0, 1.0);
}
@fragment
fn fs_single() -> @location(0) vec4f {
return vec4f(1.0);
}
`;
const dualSourceBlend = {
color: { srcFactor: "src1", dstFactor: "one-minus-src1", operation: "add" },
alpha: { srcFactor: "one", dstFactor: "zero", operation: "add" },
};
let pixel;
async function expectValidationError(device, shouldFail, message, fn)
{
device.pushErrorScope("validation");
fn();
const error = await device.popErrorScope();
if (!!error === shouldFail)
testPassed(message);
else
testFailed(message + (error ? " (unexpected error: " + error.message + ")" : " (expected a validation error)"));
}
async function main() {
const adapter = await navigator.gpu.requestAdapter({});
if (!adapter) {
testFailed("requestAdapter returned null");
return;
}
if (!adapter.features.has("dual-source-blending")) {
testFailed("Adapter does not advertise the 'dual-source-blending' feature");
return;
}
testPassed("Adapter advertises 'dual-source-blending' feature");
// Without the feature, both the WGSL extension and the src1 blend factors are rejected.
const deviceWithoutFeature = await adapter.requestDevice();
await expectValidationError(deviceWithoutFeature, true, "enable dual_source_blending fails without the feature", () => {
deviceWithoutFeature.createShaderModule({ code: SHADER });
});
await expectValidationError(deviceWithoutFeature, true, "src1 blend factors fail without the feature", () => {
const module = deviceWithoutFeature.createShaderModule({ code: SINGLE_OUTPUT_SHADER });
deviceWithoutFeature.createRenderPipeline({
layout: "auto",
vertex: { module, entryPoint: "vs_main" },
fragment: { module, entryPoint: "fs_single", targets: [{ format: "rgba8unorm", blend: dualSourceBlend }] },
});
});
const dualSourceAdapter = await navigator.gpu.requestAdapter({});
const device = await dualSourceAdapter.requestDevice({
requiredFeatures: ["dual-source-blending"],
});
let module;
await expectValidationError(device, false, "Shader using @blend_src compiles", () => {
module = device.createShaderModule({ code: SHADER });
});
await expectValidationError(device, true, "src1 blend factors require the fragment shader to use @blend_src", () => {
device.createRenderPipeline({
layout: "auto",
vertex: { module, entryPoint: "vs_main" },
fragment: { module, entryPoint: "fs_single", targets: [{ format: "rgba8unorm", blend: dualSourceBlend }] },
});
});
await expectValidationError(device, true, "src1 blend factors require exactly one color target", () => {
device.createRenderPipeline({
layout: "auto",
vertex: { module, entryPoint: "vs_main" },
fragment: { module, entryPoint: "fs_main", targets: [{ format: "rgba8unorm", blend: dualSourceBlend }, { format: "rgba8unorm", writeMask: 0 }] },
});
});
let pipeline;
await expectValidationError(device, false, "Pipeline using dual source blending is valid", () => {
pipeline = device.createRenderPipeline({
layout: "auto",
vertex: { module, entryPoint: "vs_main" },
fragment: { module, entryPoint: "fs_main", targets: [{ format: "rgba8unorm", blend: dualSourceBlend }] },
});
});
const colorTexture = device.createTexture({
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
format: "rgba8unorm",
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
});
const readBuffer = device.createBuffer({
size: 256,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: colorTexture.createView(),
clearValue: { r: 0, g: 0, b: 1, a: 1 },
loadOp: "clear",
storeOp: "store",
}],
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
encoder.copyTextureToBuffer({ texture: colorTexture }, { buffer: readBuffer, bytesPerRow: 256 }, { width: 1, height: 1 });
device.queue.submit([encoder.finish()]);
await readBuffer.mapAsync(GPUMapMode.READ);
pixel = new Uint8Array(readBuffer.getMappedRange(0, 4)).slice();
readBuffer.unmap();
// result.rgb = src0.rgb * src1.rgb + dst.rgb * (1 - src1.rgb)
// = (1, 1, 0) * (1, 0, 0) + (0, 0, 1) * (0, 1, 1) = (1, 0, 1)
// result.a = src0.a * 1 + dst.a * 0 = 1
shouldBe("pixel[0]", "255");
shouldBe("pixel[1]", "0");
shouldBe("pixel[2]", "255");
shouldBe("pixel[3]", "255");
}
globalThis.testRunner?.waitUntilDone();
main().catch(e => {
testFailed("Exception: " + e);
}).finally(() => {
finishJSTest();
});
</script>