blob: 03d7addde1423620aff9bc0901f648e451d3206b [file] [edit]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL2 readBuffer(NONE) on the default framebuffer</title>
<script src="../resources/js-test.js"></script>
</head>
<body>
<div id="description"></div>
<div id="console"></div>
<script>
description("Tests that WebGL2 readBuffer(NONE) on the default framebuffer is tracked as READ_BUFFER state and makes reads of the default framebuffer fail, but does not affect the contents any of the ways of reading the canvas produce.");
jsTestIsAsync = true;
var gl;
var canvas;
var pixel;
// The canvas contents are sampled through a 2D canvas for the read types that produce an image.
var readbackCanvas = document.createElement("canvas");
readbackCanvas.width = 2;
readbackCanvas.height = 2;
var readbackContext = readbackCanvas.getContext("2d", { willReadFrequently: true });
function sampleReadbackCanvas()
{
return readbackContext.getImageData(0, 0, 1, 1).data.toString();
}
function drawAndSample(source)
{
readbackContext.clearRect(0, 0, readbackCanvas.width, readbackCanvas.height);
readbackContext.drawImage(source, 0, 0);
return sampleReadbackCanvas();
}
function readPixel(gl)
{
var pixel = new Uint8Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
return pixel.toString();
}
function loadImage(url)
{
return new Promise(function(resolve, reject) {
var image = new Image();
image.onload = function() { resolve(image); };
image.onerror = function() { reject(new Error("failed to decode the image")); };
image.src = url;
});
}
// Uploads the canvas to a texture of another WebGL context and reads the texture back.
function readViaTexImage2D(canvas)
{
var uploadCanvas = document.createElement("canvas");
uploadCanvas.width = canvas.width;
uploadCanvas.height = canvas.height;
var uploadGL = uploadCanvas.getContext("webgl2");
if (!uploadGL)
return "unable to fetch the upload WebGL 2 context";
var texture = uploadGL.createTexture();
uploadGL.bindTexture(uploadGL.TEXTURE_2D, texture);
uploadGL.texImage2D(uploadGL.TEXTURE_2D, 0, uploadGL.RGBA, uploadGL.RGBA, uploadGL.UNSIGNED_BYTE, canvas);
uploadGL.bindFramebuffer(uploadGL.FRAMEBUFFER, uploadGL.createFramebuffer());
uploadGL.framebufferTexture2D(uploadGL.FRAMEBUFFER, uploadGL.COLOR_ATTACHMENT0, uploadGL.TEXTURE_2D, texture, 0);
var result = readPixel(uploadGL);
var error = uploadGL.getError();
// Drop the references to the upload context, so that the caller can collect it.
texture = null;
uploadGL = null;
uploadCanvas = null;
if (error)
return "the upload WebGL 2 context failed with error " + error;
return result;
}
// Captures the canvas into a MediaStream and samples the first frame the stream produces. The
// capture is triggered by re-drawing the canvas, so that the capture happens with the READ_BUFFER
// the caller has set.
async function readViaCaptureStream(canvas, gl, redraw)
{
var video = document.createElement("video");
video.width = canvas.width;
video.height = canvas.height;
video.autoplay = true;
video.muted = true;
video.playsInline = true;
document.body.appendChild(video);
var stream = canvas.captureStream();
video.srcObject = stream;
await new Promise(function(resolve) { requestAnimationFrame(function() { setTimeout(resolve, 0); }); });
var frame = new Promise(function(resolve, reject) {
var identifier = video.requestVideoFrameCallback(function() { resolve(); });
setTimeout(function() {
video.cancelVideoFrameCallback(identifier);
reject(new Error("timed out waiting for a captured video frame"));
}, 10000);
});
redraw();
await frame;
var result = drawAndSample(video);
// Release the stream and the video element, so that the caller can collect the context.
stream.getTracks().forEach(function(track) { track.stop(); });
video.srcObject = null;
video.remove();
video = null;
stream = null;
return result;
}
// Reads the canvas contents through readType and returns the top left pixel as "r,g,b,a".
async function readCanvas(readType, canvas, gl, redraw)
{
if (readType == "drawImage")
return drawAndSample(canvas);
if (readType == "toDataURL")
return drawAndSample(await loadImage(canvas.toDataURL()));
if (readType == "toBlob") {
var blob = await new Promise(function(resolve) { canvas.toBlob(resolve); });
if (!blob)
return "toBlob() produced no blob";
var blobBitmap = await createImageBitmap(blob);
var blobResult = drawAndSample(blobBitmap);
blobBitmap.close();
return blobResult;
}
if (readType == "createImageBitmap") {
var bitmap = await createImageBitmap(canvas);
var bitmapResult = drawAndSample(bitmap);
bitmap.close();
return bitmapResult;
}
if (readType == "createPattern") {
readbackContext.clearRect(0, 0, readbackCanvas.width, readbackCanvas.height);
readbackContext.fillStyle = readbackContext.createPattern(canvas, "no-repeat");
readbackContext.fillRect(0, 0, readbackCanvas.width, readbackCanvas.height);
return sampleReadbackCanvas();
}
if (readType == "texImage2D")
return readViaTexImage2D(canvas);
if (readType == "videoFrame") {
var videoFrame = new VideoFrame(canvas, { timestamp: 0 });
var videoFrameResult = drawAndSample(videoFrame);
videoFrame.close();
return videoFrameResult;
}
if (readType == "captureStream")
return await readViaCaptureStream(canvas, gl, redraw);
return "unknown read type " + readType;
}
// Read types that are not available on all the ports are skipped with a visible reason.
function readTypeUnavailableReason(readType)
{
if (readType == "videoFrame" && !window.VideoFrame)
return "VideoFrame is not available";
if (readType == "captureStream") {
if (!HTMLCanvasElement.prototype.captureStream)
return "HTMLCanvasElement.captureStream() is not available";
if (!HTMLVideoElement.prototype.requestVideoFrameCallback)
return "HTMLVideoElement.requestVideoFrameCallback() is not available";
}
return null;
}
async function runTest(parameters)
{
debug("");
debug("Parameters: " + JSON.stringify(parameters));
var unavailableReason = readTypeUnavailableReason(parameters.readType);
if (unavailableReason) {
debug("Skipped: " + unavailableReason + ".");
return;
}
// Drop the canvas, the context and the assertion globals of the previous case and collect them.
// The context of a collected canvas is destroyed, which keeps the number of the live contexts
// from growing to the limit after which the implementation recycles the oldest contexts.
canvas = null;
gl = null;
pixel = null;
gc();
canvas = document.createElement("canvas");
canvas.width = 2;
canvas.height = 2;
gl = canvas.getContext("webgl2", {
antialias: parameters.antialias,
preserveDrawingBuffer: parameters.preserveDrawingBuffer,
premultipliedAlpha: parameters.premultipliedAlpha,
});
if (!gl) {
testFailed("Unable to fetch WebGL 2 rendering context for canvas.");
return;
}
shouldBe("gl.getParameter(gl.READ_BUFFER)", "gl.BACK");
// The clear color is opaque, so that the expected value is the same for both
// premultipliedAlpha values.
var clearGreen = function() {
gl.clearColor(0, 1, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
};
clearGreen();
gl.readBuffer(gl.NONE);
shouldBe("gl.getError()", "gl.NO_ERROR");
shouldBe("gl.getParameter(gl.READ_BUFFER)", "gl.NONE");
debug("Reading the default framebuffer is invalid while its READ_BUFFER is NONE.");
shouldBeEqualToString("readPixel(gl)", "0,0,0,0");
shouldBe("gl.getError()", "gl.INVALID_OPERATION");
debug("READ_BUFFER of the default framebuffer is unaffected by readBuffer() on a bound read framebuffer.");
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, gl.createFramebuffer());
gl.readBuffer(gl.COLOR_ATTACHMENT0);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
shouldBe("gl.getError()", "gl.NO_ERROR");
shouldBe("gl.getParameter(gl.READ_BUFFER)", "gl.NONE");
debug("Reading the canvas contents is internal to the implementation, which READ_BUFFER must not affect.");
pixel = await readCanvas(parameters.readType, canvas, gl, clearGreen);
shouldBeEqualToString("pixel", "0,255,0,255");
shouldBe("gl.getError()", "gl.NO_ERROR");
debug("Reading the canvas contents restores READ_BUFFER, so reading the default framebuffer is still invalid.");
shouldBeEqualToString("readPixel(gl)", "0,0,0,0");
shouldBe("gl.getError()", "gl.INVALID_OPERATION");
debug("Restoring READ_BUFFER to BACK makes the default framebuffer readable again.");
var clearBlue = function() {
gl.clearColor(0, 0, 1, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
};
clearBlue();
gl.readBuffer(gl.BACK);
shouldBe("gl.getParameter(gl.READ_BUFFER)", "gl.BACK");
shouldBeEqualToString("readPixel(gl)", "0,0,255,255");
shouldBe("gl.getError()", "gl.NO_ERROR");
pixel = await readCanvas(parameters.readType, canvas, gl, clearBlue);
shouldBeEqualToString("pixel", "0,0,255,255");
shouldBe("gl.getError()", "gl.NO_ERROR");
// The errors of the failing reads above are logged to the console asynchronously, as the
// messages come from the process that runs the GL commands. Return only after they have been
// delivered, as the messages of a destroyed context are dropped with the context, and the next
// case destroys the context of this one.
await new Promise(function(resolve) { requestAnimationFrame(function() { setTimeout(resolve, 0); }); });
}
// The ways a script can read the canvas contents. They cover the different internal reads of the
// drawing buffer: drawImage, createImageBitmap, createPattern and toDataURL and toBlob of a
// premultiplied context read it as an image, toDataURL, toBlob and texImage2D of an unpremultiplied
// context read it as a pixel buffer, and videoFrame and captureStream read it as a video frame.
var readTypes = [
"drawImage",
"toDataURL",
"toBlob",
"createImageBitmap",
"createPattern",
"texImage2D",
"videoFrame",
"captureStream",
];
var testCases = [];
for (const antialias of [true, false]) {
for (const preserveDrawingBuffer of [true, false]) {
for (const premultipliedAlpha of [true, false]) {
for (const readType of readTypes)
testCases.push({ antialias, preserveDrawingBuffer, premultipliedAlpha, readType });
}
}
}
(async function() {
for (const testCase of testCases) {
try {
await runTest(testCase);
} catch (exception) {
testFailed("Threw: " + exception);
}
}
finishJSTest();
})();
</script>
</body>
</html>