画布drawImage第一次将另一个画布用作源参数时速度较慢 [英] Canvas drawImage slow first time another canvas is used as the source argument

查看:55
本文介绍了画布drawImage第一次将另一个画布用作源参数时速度较慢的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我第一次使用其他画布作为绘图源时看到的画布绘制速度很慢.在我交换图像之前,随后的canvas到canvas的.drawImage调用是可以的(然后再次看到相同的问题).

下面的示例代码-加载图像,然后创建4个画布,从图像本身绘制第一个画布,从第一个画布绘制第二个画布,依此类推.创建画布之后,将交换源图像并代码再次运行.

  var sourceImage = new Image();//原始图片var myImages = [];//图片和画布引用的数组myImages [0] = sourceImage;//将第一个myImage设置为图像源//图片加载sourceImage.onload = function(){console.log("Imageload",新的Date()-t0);myImages [0] = sourceImage;//循环创建并在画布上绘制对于(var i = 1; i< = 4; i + = 1){//创建画布myImages [i] = document.createElement("canvas");//将画布尺寸设置为与原始图像相同myImages [i] .width = myImages [0] .width;myImages [i] .height = myImages [0] .height;//在此画布上绘制最后一个画布/图像t0 =新的Date();myImages [i] .getContext("2d").drawImage(myImages [i-1],0,0,myImages [i] .width,myImages [i].高度);console.log("drawImage",i,新的Date()-t0);}//用black.jpg完成,因此加载white.jpgif(sourceImage.getAttribute("src"))=="images/black.jpg"){sourceImage.src ="images/white.jpg"}}//加载图片t0 =新的Date();sourceImage.src ="images/black.jpg"; 

控制台输出为...

 图像加载36drawImage 1 0drawImage 2 255drawImage 3 0drawImage 4 0图像加载35drawImage 1 0drawImage 2388drawImage 3 1drawImage 4 1 

我的问题是,为什么第二张画布绘制缓慢?我尝试了各种图像文件和不同的画布大小,但始终看到相同的结果.我已经在Chrome和Safari上进行了测试.

如果在第一个画布上进行缓慢绘制,则我可以接受,尽管.onload被触发,但是图像仍然存在某些问题.但是速度慢是在第二个画布上,即第一个是从图像上绘制的,没有任何问题.

解决方案

我认为您在这里遇到了一个怪异的优化怪癖,对于发生的事情很难给出确切的答案,但是我会尝试尝试做出有根据的猜测.


似乎浏览器在GPU实际执行了分配的作业之前又回到了CPU线程,从而充分利用了任务并行性.

因此,在第一个循环中,GPU将启动一项工作,要求将< img> 绘制到< canvas> ,此图像的数据即使在Chrome中进行了解码,但仍必须将其传输到GPU并转换为实际的位图.
但是正如我们所说,这是并行完成的,因此js脚本可以继续并在执行此工作的同时直接继续进行第二个循环.

但是,当在第二张画布上绘制第一个目标画布时,它会看到有一个正在运行的GPU作业对其进行修改,因此将阻塞CPU线程,直到完成第一个绘图为止.

尽管如此,下一次迭代只会处理< canvas> 源,其位图缓冲区已在GPU上,因此不会花费任何相关时间.

我们可以通过在每次迭代之间等待几毫秒来确认这一点.这样做,所有 canvas-to-canvas 操作将花费大约0ms.

  var url1 ="https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?bar" +Math.random();var url2 ="https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?foo" + Math.random();var sourceImage = new Image();//原始图片sourceImage.crossOrigin = true;var myImages = [];//图片和画布引用的数组myImages [0] = sourceImage;//将第一个myImage设置为图像源//图片加载sourceImage.onload =异步功能(){console.log("Imageload",new Date()-t0);myImages [0] = sourceImage;//事先创建画布以确保它不是问题的一部分对于(var i = 1; i< = 4; i + = 1){//创建画布myImages [i] = document.createElement("canvas");//将画布尺寸设置为与原始图像相同myImages [i] .width = myImages [0] .width;myImages [i] .height = myImages [0] .height;myImages [i] .getContext("2d");}//循环创建并在画布上绘制对于(var i = 1; i< = 4; i + = 1){//在此画布上绘制最后一个画布/图像t0 =新的Date();myImages [i] .getContext("2d").drawImage(myImages [i-1],0,0,myImages [i] .width,myImages [i].高度);console.log("drawImage",i,new Date()-t0);等待新的Promise(r => setTimeout(r,500));}//用black.jpg完成,因此加载white.jpg如果(sourceImage.getAttribute("src")== url1){sourceImage.src = url2}};//加载图片t0 =新的Date();sourceImage.src = url1;  

类似地,如果我们确实生成了每个源的ImageBitmap,我们可以看到花费最多时间的是预期的< img> :

  var url1 ="https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?bar" +Math.random();var url2 ="https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?foo" + Math.random();var sourceImage = new Image();//原始图片sourceImage.crossOrigin = true;var myImages = [];//图片和画布引用的数组myImages [0] = sourceImage;//将第一个myImage设置为图像源//图片加载sourceImage.onload =异步功能(){console.log("Imageload",new Date()-t0);myImages [0] = sourceImage;//预先创建画布以确保它不是问题的一部分对于(var i = 1; i< = 4; i + = 1){//创建画布myImages [i] = document.createElement("canvas");//将画布尺寸设置为与原始图像相同myImages [i] .width = myImages [0] .width;myImages [i] .height = myImages [0] .height;myImages [i] .getContext("2d");}//循环创建并在画布上绘制对于(var i = 1; i< = 4; i + = 1){//等待创建ImageBitmap被创建t0 =新的Date();const img =等待createImageBitmap(myImages [i-1]);console.log("createImageBitmap",i,new Date()-t0);t0 =新的Date();myImages [i] .getContext("2d").drawImage(img,0,0,myImages [i] .width,myImages [i].高度);console.log("drawImage",i,new Date()-t0);}//用black.jpg完成,因此加载white.jpg如果(sourceImage.getAttribute("src")== url1){sourceImage.src = url2}};//加载图片t0 =新的Date();sourceImage.src = url1;  


Ps:可能很想调用 getImageData 来强制同步返回到CPU线程,但是这样做还可以在CPU和GPU之间来回传输所有画布位图,实际上创建了每个循环的速度都一样.

I am seeing slow canvas drawing the first time I use another canvas as the drawing source. Subsequent canvas to canvas .drawImage calls are fine until I swap images (and then I see the same issue again).

Sample code below - an image is loaded and then 4 canvases are created, the 1st canvas is draw from the image itself, the 2nd canvas is drawn from the 1st, etc. After the canvases are created the source image is swapped and the code run again.

        var sourceImage = new Image();  // Original image
        var myImages = []; // Array of image and canvases references
        myImages[0] = sourceImage; // Set first myImage to image source

        // Image onload 
        sourceImage.onload = function () {
     
            console.log("Imageload", new Date() - t0);
            myImages[0] = sourceImage;

            // Loop to create and draw on canvases
            for (var i = 1; i <= 4; i += 1) {

                // Create canvas
                myImages[i] = document.createElement("canvas");

                // Set canvas dimensions to same as original image
                myImages[i].width = myImages[0].width;
                myImages[i].height = myImages[0].height;

                // Draw last canvas / image onto this canvas
                t0 = new Date();
                myImages[i].getContext("2d").drawImage(
                    myImages[i - 1],
                    0,
                    0,
                    myImages[i].width,
                    myImages[i].height
                );
                console.log("drawImage", i,  new Date() - t0); 
                
            }

            // Finished with black.jpg so load white.jpg           
            if (sourceImage.getAttribute("src") == "images/black.jpg") {
                sourceImage.src = "images/white.jpg"
            }

        }

        // Load image
        t0 = new Date();
        sourceImage.src = "images/black.jpg"

The console output is ...

Imageload 36
drawImage 1 0
drawImage 2 255
drawImage 3 0
drawImage 4 0

Imageload 35
drawImage 1 0
drawImage 2 388
drawImage 3 1
drawImage 4 1

My question is why is the 2nd canvas drawing slow? I have tried various image files, and different canvas sizes but always see the same outcome. I have tested on Chrome and Safari.

If the slow draw was on the first canvas I could accept that although the .onload fired there was still something going on with the image. But the slowness is on the second canvas i.e. the first has been drawn from the image without issue.

解决方案

I think you just hit a weird optimization quirk here and it might be quite hard to have a definitive answer as to what happens, but I'll try to make an educated guess anyway.


It seems the browser comes back to the CPU thread before the GPU actually has executed the job it was assigned, taking full advantage of tasks parallelism.

So in the first loop, the GPU will start a job asking to draw the <img> to the <canvas>, this image's data which even though is decoded in Chrome, still has to be transfered to the GPU and converted to an actual bitmap.
But as we said, this is done in parallel and thus the js script can continue and proceed with the second loop directly while doing this job.

However when it comes at drawing the first target canvas on the second, it will see that there is one running GPU job that will modify it, and will thus block the CPU thread until the first drawing has completed.

The next iterations though will only deal with <canvas> sources which bitmap buffers are already on the GPU, so they won't take any relevant time.

We can somehow confirm this by simply waiting a few ms between each iterations. Doing so, all canvas-to-canvas operations will take about 0ms.

var url1 = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?bar" + Math.random();
var url2 = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?foo" + Math.random();

var sourceImage = new Image(); // Original image
sourceImage.crossOrigin = true;
var myImages = []; // Array of image and canvases references
myImages[0] = sourceImage; // Set first myImage to image source

// Image onload 
sourceImage.onload = async function() {

  console.log("Imageload", new Date() - t0);
  myImages[0] = sourceImage;
  // create canvases before hand to be sure it's not part of the issue
  for (var i = 1; i <= 4; i += 1) {
    // Create canvas
    myImages[i] = document.createElement("canvas");

    // Set canvas dimensions to same as original image
    myImages[i].width = myImages[0].width;
    myImages[i].height = myImages[0].height;
    myImages[i].getContext("2d");
  }

  // Loop to create and draw on canvases
  for (var i = 1; i <= 4; i += 1) {
    // Draw last canvas / image onto this canvas
    t0 = new Date();
    myImages[i].getContext("2d").drawImage(
      myImages[i - 1],
      0,
      0,
      myImages[i].width,
      myImages[i].height
    );
    console.log("drawImage", i, new Date() - t0);
    await new Promise(r => setTimeout(r, 500));

  }

  // Finished with black.jpg so load white.jpg           
  if (sourceImage.getAttribute("src") == url1) {
    sourceImage.src = url2
  }

};

// Load image
t0 = new Date();
sourceImage.src = url1;

Similarly, if we do generate ImageBitmaps of each source, we can see that the one taking the most time is as expected the <img>:

var url1 = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?bar" + Math.random();
var url2 = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?foo" + Math.random();

var sourceImage = new Image(); // Original image
sourceImage.crossOrigin = true;
var myImages = []; // Array of image and canvases references
myImages[0] = sourceImage; // Set first myImage to image source

// Image onload 
sourceImage.onload = async function() {

  console.log("Imageload", new Date() - t0);
  myImages[0] = sourceImage;
  // create canvases beforehand to be sure it's not part of the issue
  for (var i = 1; i <= 4; i += 1) {
    // Create canvas
    myImages[i] = document.createElement("canvas");

    // Set canvas dimensions to same as original image
    myImages[i].width = myImages[0].width;
    myImages[i].height = myImages[0].height;
    myImages[i].getContext("2d");
  }

  // Loop to create and draw on canvases
  for (var i = 1; i <= 4; i += 1) {
    // wait for create ImageBitmap to be created
    t0 = new Date();
    const img = await createImageBitmap(myImages[i - 1]);
    console.log("createImageBitmap", i, new Date() - t0);

    t0 = new Date();
    myImages[i].getContext("2d").drawImage(
      img,
      0,
      0,
      myImages[i].width,
      myImages[i].height
    );
    console.log("drawImage", i, new Date() - t0);

  }

  // Finished with black.jpg so load white.jpg           
  if (sourceImage.getAttribute("src") == url1) {
    sourceImage.src = url2
  }

};

// Load image
t0 = new Date();
sourceImage.src = url1;


Ps: one might be tempted to call getImageData to force coming back to the CPU thread synchronously, but doing so we also transfer all the canvases bitmaps back and forth between CPU and GPU, creating actually the same slowness at every loops.

这篇关于画布drawImage第一次将另一个画布用作源参数时速度较慢的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆