Source code
Revision control
Copy as Markdown
Other Tools
<!--
Copyright (c) 2023 The Khronos Group Inc.
Use of this source code is governed by an MIT-style license that can be
found in the LICENSE.txt file.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL OES_sample_variables Conformance Tests</title>
<link rel="stylesheet" href="../../resources/js-test-style.css"/>
<script src="../../js/js-test-pre.js"></script>
<script src="../../js/webgl-test-utils.js"></script>
</head>
<body>
<canvas width="32" height="32" id="c"></canvas>
<div id="description"></div>
<div id="console"></div>
<script>
"use strict";
description("This test verifies the functionality of the OES_sample_variables extension, if it is available.");
debug("");
var wtu = WebGLTestUtils;
var gl = wtu.create3DContext("c", { antialias: false }, 2);
var ext;
function runShaderTests(extensionEnabled) {
debug("");
debug("Testing various shader compiles with extension " + (extensionEnabled ? "enabled" : "disabled"));
const macro = `#version 300 es
precision highp float;
out vec4 my_FragColor;
void main() {
#ifdef GL_OES_sample_variables
my_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
#else
#error no GL_OES_sample_variables;
#endif
}`;
// Expect the macro shader to succeed ONLY if enabled
if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, macro])) {
if (extensionEnabled) {
testPassed("Macro defined in shaders when extension is enabled");
} else {
testFailed("Macro defined in shaders when extension is disabled");
}
} else {
if (extensionEnabled) {
testFailed("Macro not defined in shaders when extension is enabled");
} else {
testPassed("Macro not defined in shaders when extension is disabled");
}
}
const missing = `#version 300 es
precision highp float;
out vec4 my_FragColor;
void main() {
gl_SampleMask[0] = gl_SampleMaskIn[0] & 0x55555555;
my_FragColor = vec4(gl_SamplePosition.yx, float(gl_SampleID), float(gl_MaxSamples + gl_NumSamples));
}`;
// Always expect the shader missing the #extension pragma to fail (whether enabled or not)
if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, missing])) {
testFailed("Sample variables allowed without #extension pragma");
} else {
testPassed("Sample variables disallowed without #extension pragma");
}
const valid = `#version 300 es
#extension GL_OES_sample_variables : enable
precision highp float;
out vec4 my_FragColor;
void main() {
gl_SampleMask[0] = gl_SampleMaskIn[0] & 0x55555555;
my_FragColor = vec4(gl_SamplePosition.yx, float(gl_SampleID), float(gl_MaxSamples + gl_NumSamples));
}`;
// Try to compile a shader using sample variables that should only succeed if enabled
if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, valid])) {
if (extensionEnabled) {
testPassed("Sample variables compiled successfully when extension enabled");
} else {
testFailed("Sample variables compiled successfully when extension disabled");
}
} else {
if (extensionEnabled) {
testFailed("Sample variables failed to compile when extension enabled");
} else {
testPassed("Sample variables failed to compile when extension disabled");
}
}
debug("");
}
function runMaxSamplesTest() {
debug("");
debug("Testing gl_MaxSamples");
const frag = `#version 300 es
#extension GL_OES_sample_variables : require
precision highp float;
out vec4 color;
void main() {
color = vec4(float(gl_MaxSamples * 4) / 255.0, 0.0, 0.0, 1.0);
}`;
gl.useProgram(wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]));
wtu.setupUnitQuad(gl);
wtu.drawUnitQuad(gl);
wtu.checkCanvas(gl, [gl.getParameter(gl.MAX_SAMPLES) * 4, 0, 0, 255], "should match MAX_SAMPLES", 1);
}
function runNumSamplesTest() {
debug("");
debug("Testing gl_NumSamples");
const rbo = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, rbo);
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 4, gl.RGBA8, 32, 32);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo);
wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE);
const frag = `#version 300 es
#extension GL_OES_sample_variables : require
precision highp float;
out vec4 color;
void main() {
if (gl_NumSamples == 4) {
color = vec4(0.0, 1.0, 0.0, 1.0);
} else {
color = vec4(1.0, 0.0, 0.0, 1.0);
}
}`;
gl.useProgram(wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]));
wtu.setupUnitQuad(gl);
wtu.drawUnitQuad(gl);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
wtu.checkCanvas(gl, [0, 255, 0, 255], "should be green");
}
function runSampleIDTest() {
debug("");
debug("Testing gl_SampleID");
const frag = `#version 300 es
#extension GL_OES_sample_variables : require
precision highp float;
out vec4 color;
uniform int id;
void main() {
// Special value when the selected sample is processed, 0.0 otherwise
float r = float(gl_SampleID == id ? (1 << gl_SampleID) : 0) * 32.0 / 255.0;
// Must always be 0.0
float g = float(gl_SampleID < 0 || gl_SampleID >= gl_NumSamples);
color = vec4(r, g, 0.0, 1.0);
}`;
const program = wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]);
gl.useProgram(program);
wtu.setupUnitQuad(gl);
const rbo = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, rbo);
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 4, gl.RGBA8, 32, 32);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo);
wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE);
for (let sample = 0; sample < 4; sample++) {
debug(`Sample ${sample} is selected`);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo);
gl.uniform1i(gl.getUniformLocation(program, "id"), sample);
wtu.drawUnitQuad(gl);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
wtu.checkCanvas(gl, [(1 << sample) * 8, 0, 0, 255], undefined, 1);
}
}
function runSampleMaskInTest() {
debug("");
debug("Testing gl_SampleMaskIn");
const frag = `#version 300 es
#extension GL_OES_sample_variables : require
precision highp float;
out vec4 color;
uint popcount(uint v) {
uint c = 0u;
for (; v != 0u; v >>= 1) c += v & 1u;
return c;
}
void main() {
float r = float(popcount(uint(gl_SampleMaskIn[0])));
color = vec4(r * 4.0 / 255.0, 0, 0, 1);
}`;
const program = wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]);
gl.useProgram(program);
// Use a triangle instead of the WTU's quad
// to avoid artifacts along the diagonal
const vertices = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertices);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1.0, 1.0,
1.0, -1.0,
-1.0, -1.0]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
function test(sampleCount, sampleCoverageEnabled, coverage) {
if (sampleCoverageEnabled) {
gl.enable(gl.SAMPLE_COVERAGE);
} else {
gl.disable(gl.SAMPLE_COVERAGE);
}
gl.sampleCoverage(coverage, false);
const rbo = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, rbo);
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, sampleCount, gl.RGBA8, 32, 32);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo);
wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST);
// Shader scales up the number of input samples to increase precision in unorm8 space.
let expected = Math.max(sampleCount, 1) * 4;
// Sample coverage must not affect single sampled buffers
if (sampleCoverageEnabled && sampleCount > 0) {
// The number of samples in gl_SampleMaskIn must be affected by the sample
// coverage GL state and then the resolved value must be scaled down again.
expected *= coverage * coverage;
}
// Check only the red channel
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
const pixel = new Uint8Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
const message = `Expected: ${expected}, Actual: ${pixel[0]}, ` +
`Samples: ${sampleCount}, Sample Coverage: ${sampleCoverageEnabled}, Coverage: ${coverage}`;
if (Math.abs(pixel[0] - expected) > 2) {
testFailed(message);
} else {
testPassed(message);
}
}
// Include all exposed sample counts and additionally test single-sampled rendering
const sampleCounts = [...gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA8, gl.SAMPLES), 0];
for (const sampleCount of sampleCounts) {
if (sampleCount > 32) {
// This test will not work with more than 32 samples.
continue;
}
for (const sampleCoverageEnabled of [false, true]) {
for (const coverage of [0.0, 0.5, 1.0]) {
if (sampleCount == 1 && coverage != 0.0 && coverage != 1.0) {
continue;
}
test(sampleCount, sampleCoverageEnabled, coverage);
}
}
}
}
function runSampleMaskInPerSampleTest() {
debug("");
debug("Testing gl_SampleMaskIn with per-sample shading");
const frag = `#version 300 es
#extension GL_OES_sample_variables : require
precision highp float;
out vec4 color;
void main() {
float r = float(gl_SampleMaskIn[0] == (1 << gl_SampleID));
color = vec4(r, 0, 0, 1);
}`;
const program = wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]);
gl.useProgram(program);
wtu.setupUnitQuad(gl);
// Include all exposed sample counts and additionally test single-sampled rendering
const sampleCounts = [...gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA8, gl.SAMPLES), 0];
for (const sampleCount of sampleCounts) {
if (sampleCount > 32) {
// This test will not work with more than 32 samples.
continue;
}
const rbo = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, rbo);
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, sampleCount, gl.RGBA8, 32, 32);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo);
wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo);
wtu.drawUnitQuad(gl);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
wtu.checkCanvas(gl, [255, 0, 0, 255], `Samples: ${sampleCount}`, 1);
}
}
function runSampleMaskTest() {
debug("");
debug("Testing gl_SampleMask");
const frag = `#version 300 es
#extension GL_OES_sample_variables : require
precision highp float;
uniform highp int sampleMask;
out vec4 color;
void main() {
gl_SampleMask[0] = sampleMask;
color = vec4(1, 0, 0, 1);
}`;
const program = wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]);
gl.useProgram(program);
// Use a triangle instead of the WTU's quad
// to avoid artifacts along the diagonal
const vertices = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertices);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1.0, 1.0,
1.0, -1.0,
-1.0, -1.0]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
function test(sampleCount, sampleMask) {
const rbo = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, rbo);
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, sampleCount, gl.RGBA8, 32, 32);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo);
wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform1i(gl.getUniformLocation(program, "sampleMask"), sampleMask);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST);
let expected = 1.0;
if (sampleCount > 0) {
let mask = sampleMask & ((1 << Math.max(sampleCount, 1)) - 1);
let bits = 0;
for (; mask != 0; mask >>= 1) bits += mask & 1;
expected = bits / Math.max(sampleCount, 1);
}
expected *= 255;
// Check only the red channel
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
const pixel = new Uint8Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
const message = `Samples: ${sampleCount}, `
+ `gl_SampleMask[0]: 0x${sampleMask.toString(16).padStart(8, "0").toUpperCase()}, `
+ `Actual: ${pixel[0]}, Expected: ${expected}`;
if (Math.abs(pixel[0] - expected) > 2) {
testFailed(message);
} else {
testPassed(message);
}
}
// Include all exposed sample counts and additionally test single-sampled rendering
const sampleCounts = [...gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA8, gl.SAMPLES), 0];
for (const sampleCount of sampleCounts) {
if (sampleCount > 31) {
// This test will not work with more than 31 samples.
continue;
}
for (const sampleMask of [0xFFFFFFFF, 0x55555555, 0xAAAAAAAA, 0x00000000]) {
test(sampleCount, sampleMask);
}
}
}
function runTest() {
if (!gl) {
testFailed("WebGL context does not exist");
return;
}
testPassed("WebGL context exists");
runShaderTests(false);
ext = gl.getExtension("OES_sample_variables");
wtu.runExtensionSupportedTest(gl, "OES_sample_variables", ext !== null);
if (!ext) {
testPassed("No OES_sample_variables support -- this is legal");
} else {
testPassed("Successfully enabled OES_sample_variables extension");
runShaderTests(true);
debug("Testing sample variables");
runMaxSamplesTest();
runNumSamplesTest();
runSampleIDTest();
runSampleMaskInTest();
runSampleMaskInPerSampleTest();
runSampleMaskTest();
}
}
runTest();
var successfullyParsed = true;
</script>
<script src="../../js/js-test-post.js"></script>
</body>
</html>