| <!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> |