如何使用WebGL提高性能?

时间:2016-02-26 10:24:53

标签: javascript canvas webgl

我正在使用WebGL渲染纹理,但是,我渲染的方式是渲染几行数据然后将这些行移动到右边再次绘制另一组线。

例如:我有640 * 480的图像,其中包含640 * 480 * 4像素的RGBA,但我只填充alpha值,它是GrayScale医疗Dicom图像。

现在,我面临的问题是它是用抖动渲染纹理,图像渲染没有顺利进行。

例如,这就是: 有640行数据要呈现。

所以,我拿了一个640 * 480 * 4的数据缓冲区然后,假设第一行通过websocket从服务器来到客户端进行渲染,然后我将索引填充为3,640 + 3,640 * 2 + 3, 640 * 3 + 3等等,直到640 * 480 + 3。然后当接收到第二行时,我将第一行移动到第二行,如3-> 7,640 + 3-> 640 + 7,...... 640 * 480 + 3-> 640 * 480 +7。然后新接收的线将渲染为3,640 + 3,640 * 2 + 3,640 * 3 + 3,这将持续到第640行图像数据。

这是我所做的代码。

代码:

    var renderLineData = function (imageAttr) {
    var data = imageAttr.data;
    var LINES_PER_CHUNK = imageAttr.lines;
    var alpha = 4;
    if(imageAttr.newImage) {
        newBuffer = new ArrayBuffer(imageAttr.width * imageAttr.height * alpha);dataTypedArray = new Uint8Array(newBuffer);
        // provide texture coordinates for the rectangle.
        provideTextureCoordsForRect();
        setParams();
        // Upload the image into the texture.
        // look up uniform locations
        uploadImageToTexture(gl.getUniformLocation(program, 'u_matrix'));
    } else {
        for (var z = imageAttr.index; z > 0; z--) {
            for (i = 0 ; i < LINES_PER_CHUNK; i++) {
                for (j = 0 ; j < imageAttr.height; j++) {
                    dataTypedArray[i * alpha + imageAttr.width*alpha * j + 3 + LINES_PER_CHUNK  * alpha * z] = dataTypedArray[i * alpha + imageAttr.width*alpha * j +  3 + LINES_PER_CHUNK  * alpha * (z-1)];
                }
            }
        }
    }
    for (i = 0, k = imageAttr.height*LINES_PER_CHUNK; i < LINES_PER_CHUNK; i++) {
        for (j = 0 ; j < imageAttr.height; j++) {
            dataTypedArray[i * alpha + imageAttr.width*4 * j + 3] = data[k - imageAttr.height + j];
        }
        k = k - imageAttr.height;
    }
    imageAttrTemp = imageAttr;
    renderImgSlowly(gl, imageAttr, dataTypedArray);
};
function renderImgSlowly (gl, image, dataTypedArray)  {
    gl.clear(gl.COLOR_BUFFER_BIT || gl.DEPTH_BUFFER_BIT);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, dataTypedArray);
    //Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
}

2 个答案:

答案 0 :(得分:2)

首先,你所做的一切都不是速度问题。 640x320的图像不是那么大,你在JavaScript中处理的处理量不太可能成为瓶颈。

除此之外,WebGL可以毫不费力地绘制一个四元组,这就是你正在绘制的。上传640x480纹理也不会有问题。

瓶颈是网络。通过网络发送块很慢。

另一方面,如果您想进行优化,为什么要在JavaScript中改变数据?只需将其放在纹理中的正确位置即可开始使用gl.texSubImage2D。如果您只想绘制已放入数据的部分,则调整纹理坐标以选择纹理的那一部分

另外,如果您只需要一个频道,为什么要使用RGBA?使用LUMINANCE

  if (imageAttr.newImage) {
      destColumn = imageAttr.width;
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, imageAttr.width, imageAttr.height, 0,
                    gl.LUMINANCE, gl.UNSIGNED_BYTE, null);
  }
  destColumn -= imageAttr.lines;
  // should check it destColumn does not go negative!
  gl.texSubImage2D(gl.TEXTURE_2D, 0, destColumn, 0, imageAttr.lines, imageAttr.height,
                   gl.LUMINANCE, gl.UNSIGNED_BYTE, imageAttr.data);

  var srcX = destColumn;
  var srcY = 0;
  var srcWidth  = imageAttr.width - destColumn;
  var srcHeight = imageAttr.height;

  var dstX = destColumn * gl.canvas.width / imageAttr.width;
  var dstY = 0;
  var dstWidth  = srcWidth * gl.canvas.width / imageAttr.width;
  var dstHeight = srcHeight;

  var texWidth     = imageAttr.width;
  var texHeight    = imageAttr.height;
  var targetWidth  = gl.canvas.width;
  var targetHeight = gl.canvas.height;

  drawImageInWebGL(
    tex, texWidth, texHeight,  
    srcX, srcY, srcWidth, srcHeight,
    dstX, dstY, dstWidth, dstHeight,
    targetWidth, targetHeight);
}

这是一个例子

var m4 = twgl.m4;
var gl = document.getElementById("c").getContext("webgl");
// compiles shader, links and looks up locations
var programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);

// a unit quad
var arrays = {
  position: { 
    numComponents: 2, 
    data: [
      0, 0,  
      1, 0, 
      0, 1, 
      0, 1, 
      1, 0,  
      1, 1,
    ],
  },
};
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for each array
var bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);
      
// we're only using 1 texture so just make and bind it now
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
      
      
var destColumn = 0;
      
// We're using 1 byte wide texture pieces so we need to 
// set UNPACK_ALIGNMENT to 1 as it defaults to 4
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
            
simulateSendingAnImageNColumnsAtATime(1, 1, addLinesToImageAndDraw);
  
function addLinesToImageAndDraw(imageAttr) {
  if (imageAttr.newImage) {
      destColumn = imageAttr.width;
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, imageAttr.width, imageAttr.height, 0,
                    gl.LUMINANCE, gl.UNSIGNED_BYTE, null);
  }
  destColumn -= imageAttr.lines;
  // should check it destColumn does not go negative!
  gl.texSubImage2D(gl.TEXTURE_2D, 0, destColumn, 0, imageAttr.lines, imageAttr.height,
                   gl.LUMINANCE, gl.UNSIGNED_BYTE, imageAttr.data);
  
  var srcX = destColumn;
  var srcY = 0;
  var srcWidth  = imageAttr.width - destColumn;
  var srcHeight = imageAttr.height;

  var dstX = destColumn * gl.canvas.width / imageAttr.width;
  var dstY = 0;
  var dstWidth  = srcWidth * gl.canvas.width / imageAttr.width;
  var dstHeight = gl.canvas.height;

  var texWidth     = imageAttr.width;
  var texHeight    = imageAttr.height;
  var targetWidth  = gl.canvas.width;
  var targetHeight = gl.canvas.height;
      
  drawImage(
    tex, texWidth, texHeight,  
    srcX, srcY, srcWidth, srcHeight,
    dstX, dstY, dstWidth, dstHeight,
    targetWidth, targetHeight);
}
      

// we pass in texWidth and texHeight because unlike images
// we can't look up the width and height of a texture

// we pass in targetWidth and targetHeight to tell it
// the size of the thing we're drawing too. We could look 
// up the size of the canvas with gl.canvas.width and
// gl.canvas.height but maybe we want to draw to a framebuffer
// etc.. so might as well pass those in.

// srcX, srcY, srcWidth, srcHeight are in pixels 
// computed from texWidth and texHeight

// dstX, dstY, dstWidth, dstHeight are in pixels
// computed from targetWidth and targetHeight
function drawImage(
    tex, texWidth, texHeight,
    srcX, srcY, srcWidth, srcHeight,
    dstX, dstY, dstWidth, dstHeight,
    targetWidth, targetHeight) {
  var mat  = m4.identity();
  var tmat = m4.identity();
  
  var uniforms = {
    matrix: mat,
    textureMatrix: tmat,
    texture: tex,
  };

  // these adjust the unit quad to generate texture coordinates
  // to select part of the src texture

  // NOTE: no check is done that srcX + srcWidth go outside of the
  // texture or are in range in any way. Same for srcY + srcHeight

  m4.translate(tmat, [srcX / texWidth, srcY / texHeight, 0], tmat);
  m4.scale(tmat, [srcWidth / texWidth, srcHeight / texHeight, 1], tmat);

  // these convert from pixels to clip space
  m4.ortho(0, targetWidth, targetHeight, 0, -1, 1, mat)

  // these move and scale the unit quad into the size we want
  // in the target as pixels
  m4.translate(mat, [dstX, dstY, 0], mat);
  m4.scale(mat, [dstWidth, dstHeight, 1], mat);

  gl.useProgram(programInfo.program);
  // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  // calls gl.uniformXXX, gl.activeTexture, gl.bindTexture
  twgl.setUniforms(programInfo, uniforms);
  // calls gl.drawArray or gl.drawElements
  twgl.drawBufferInfo(gl, gl.TRIANGLES, bufferInfo);
  
}


// =====================================================================
// Everything below this line represents stuff from the server.
// so it's irrelevant to the answer
//

function simulateSendingAnImageNColumnsAtATime(minColumnsPerChunk, maxColumnsPerChunk, callback) {
  var imageData = createImageToSend(640, 480);
  
  // cut data into columns at start because this work would be done on
  // the server
  var columns = [];
  var x = 0;
  while (x < imageData.width) {
    // how many columns are left?
    var maxWidth = imageData.width - x;
    
    // how many columns should we send
    var columnWidth = Math.min(maxWidth, rand(minColumnsPerChunk, maxColumnsPerChunk + 1));
    
    var data = createImageChunk(imageData, imageData.width - x - columnWidth, 0, columnWidth, imageData.height);    
    
    columns.push({
      newImage: x === 0,
      lines: columnWidth,
      width: imageData.width,
      height: imageData.height,
      data: data,
    });
    
    x += columnWidth;
  }
  
  var columnNdx = 0;
  sendNextColumn();
  
  function sendNextColumn() {
    if (columnNdx < columns.length) {
      callback(columns[columnNdx++]);
      if (columnNdx < columns.length) {
        // should we make this random to siumlate network speed
        var timeToNextChunkMS = 17;
        setTimeout(sendNextColumn, timeToNextChunkMS);
      }
    }
  }
}

function createImageChunk(imageData, x, y, width, height) {
  var data = new Uint8Array(width * height);
  for (var yy = 0; yy < height; ++yy) {
    for (var xx = 0; xx < width; ++xx) {
      var srcOffset = ((yy + y) * imageData.width + xx + x) * 4;
      var dstOffset = yy * width + xx;
      // compute gray scale
      var gray = Math.max(imageData.data[srcOffset], imageData.data[srcOffset + 1], imageData.data[srcOffset + 2]);
      data[dstOffset] = gray;
    }
  }
  return data;
}

function rand(min, max) {
  return Math.floor(Math.random() * max - min) + min;
}

function createImageToSend(width, height) {
  // create a texture using a canvas so we don't have to download one
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.width = width;
  ctx.height = height;
  ctx.fillStyle = "#222";
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.lineWidth = 20;
  ["#AAA", "#888", "#666"].forEach(function(color, ndx, array) {
    ctx.strokeStyle = color;
    ctx.beginPath();
    ctx.arc((ndx + 1) / (array.length + 1) * ctx.canvas.width, ctx.canvas.height / 2,
            ctx.canvas.height * 0.4, 0, Math.PI * 2, false);
    ctx.stroke();
  });
  ctx.fillStyle = "white";
  ctx.font = "40px sans-serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("Some Image", ctx.canvas.width / 2, ctx.canvas.height / 2);
  return ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
}
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/twgl-full.min.js"></script>
<script id="vs" type="not-js">
// we will always pass a 0 to 1 unit quad
// and then use matrices to manipulate it
attribute vec4 position;   

uniform mat4 matrix;
uniform mat4 textureMatrix;

varying vec2 texcoord;

void main () {
  gl_Position = matrix * position;
  
  texcoord = (textureMatrix * position).xy;
}
</script>
<script id="fs" type="not-js">
precision mediump float;

varying vec2 texcoord;
uniform sampler2D texture;

void main() {
  gl_FragColor = texture2D(texture, texcoord);
}
</script>
<canvas id="c" width="640" height="480"></canvas>

注意:这不会很顺利,因为它使用setTimeout来模拟接收网络数据,但这正是您可能会看到的。

这是一个旋转图像的示例,与更新纹理无关。你可以看到它运行得非常顺畅。缓慢不是WebGL,缓慢是网络(由setTimeout模拟)

var m4 = twgl.m4;
var gl = document.getElementById("c").getContext("webgl");
// compiles shader, links and looks up locations
var programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);

// a unit quad
var arrays = {
  position: { 
    numComponents: 2, 
    data: [
      0, 0,  
      1, 0, 
      0, 1, 
      0, 1, 
      1, 0,  
      1, 1,
    ],
  },
};
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for each array
var bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays);
      
// we're only using 1 texture so just make and bind it now
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
      
      
var destColumn = 0;
var imageWidth;
var imageHeight;
      
// We're using 1 byte wide texture pieces so we need to 
// set UNPACK_ALIGNMENT to 1 as it defaults to 4
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
            
simulateSendingAnImageNColumnsAtATime(1, 1, addLinesToImageAndDraw);
  
function addLinesToImageAndDraw(imageAttr) {
  if (imageAttr.newImage) {
      destColumn  = imageAttr.width;
      imageWidth  = imageAttr.width;
      imageHeight = imageAttr.height;
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, imageAttr.width, imageAttr.height, 0,
                    gl.LUMINANCE, gl.UNSIGNED_BYTE, null);
  }
  destColumn -= imageAttr.lines;
  // should check it destColumn does not go negative!
  gl.texSubImage2D(gl.TEXTURE_2D, 0, destColumn, 0, imageAttr.lines, imageAttr.height,
                   gl.LUMINANCE, gl.UNSIGNED_BYTE, imageAttr.data);
}
      
function render(time) {
  if (imageWidth) {
    var srcX = destColumn;
    var srcY = 0;
    var srcWidth  = imageWidth - destColumn;
    var srcHeight = imageHeight;

    var dstX = destColumn * gl.canvas.width / imageWidth;
    var dstY = 0;
    var dstWidth  = srcWidth * gl.canvas.width / imageWidth;
    var dstHeight = gl.canvas.height;

    var texWidth     = imageWidth;
    var texHeight    = imageHeight;
    var targetWidth  = gl.canvas.width;
    var targetHeight = gl.canvas.height;

    drawImageWithRotation(
      time * 0.001,
      tex, texWidth, texHeight,  
      srcX, srcY, srcWidth, srcHeight,
      dstX, dstY, dstWidth, dstHeight,
      targetWidth, targetHeight);
  }
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
      

// we pass in texWidth and texHeight because unlike images
// we can't look up the width and height of a texture

// we pass in targetWidth and targetHeight to tell it
// the size of the thing we're drawing too. We could look 
// up the size of the canvas with gl.canvas.width and
// gl.canvas.height but maybe we want to draw to a framebuffer
// etc.. so might as well pass those in.

// srcX, srcY, srcWidth, srcHeight are in pixels 
// computed from texWidth and texHeight

// dstX, dstY, dstWidth, dstHeight are in pixels
// computed from targetWidth and targetHeight
function drawImageWithRotation(
    rotation,
    tex, texWidth, texHeight,
    srcX, srcY, srcWidth, srcHeight,
    dstX, dstY, dstWidth, dstHeight,
    targetWidth, targetHeight) {
  var mat  = m4.identity();
  var tmat = m4.identity();
  
  var uniforms = {
    matrix: mat,
    textureMatrix: tmat,
    texture: tex,
  };

  // these adjust the unit quad to generate texture coordinates
  // to select part of the src texture

  // NOTE: no check is done that srcX + srcWidth go outside of the
  // texture or are in range in any way. Same for srcY + srcHeight
  m4.translate(tmat, [srcX / texWidth, srcY / texHeight, 0], tmat);
  m4.scale(tmat, [srcWidth / texWidth, srcHeight / texHeight, 1], tmat);
    
  // convert from pixels to clipspace
  m4.ortho(0, targetWidth, targetHeight, 0, -1, 1, mat);
      
  // rotate around center of canvas
  m4.translate(mat, [targetWidth / 2, targetHeight / 2, 0], mat);
  m4.rotateZ(mat, rotation, mat);
  m4.translate(mat, [-targetWidth / 2, -targetHeight / 2, 0], mat);
      
  // these move and scale the unit quad into the size we want
  // in the target as pixels
  m4.translate(mat, [dstX, dstY, 0], mat);
  m4.scale(mat, [dstWidth, dstHeight, 1], mat);

  gl.useProgram(programInfo.program);
  // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  // calls gl.uniformXXX, gl.activeTexture, gl.bindTexture
  twgl.setUniforms(programInfo, uniforms);
  // calls gl.drawArray or gl.drawElements
  twgl.drawBufferInfo(gl, bufferInfo);
  
}


// =====================================================================
// Everything below this line represents stuff from the server.
// so it's irrelevant to the answer
//

function simulateSendingAnImageNColumnsAtATime(minColumnsPerChunk, maxColumnsPerChunk, callback) {
  var imageData = createImageToSend(640, 480);
  
  // cut data into columns at start because this work would be done on
  // the server
  var columns = [];
  var x = 0;
  while (x < imageData.width) {
    // how many columns are left?
    var maxWidth = imageData.width - x;
    
    // how many columns should we send
    var columnWidth = Math.min(maxWidth, rand(minColumnsPerChunk, maxColumnsPerChunk + 1));
    
    var data = createImageChunk(imageData, imageData.width - x - columnWidth, 0, columnWidth, imageData.height);    
    
    columns.push({
      newImage: x === 0,
      lines: columnWidth,
      width: imageData.width,
      height: imageData.height,
      data: data,
    });
    
    x += columnWidth;
  }
  
  var columnNdx = 0;
  sendNextColumn();
  
  function sendNextColumn() {
    if (columnNdx < columns.length) {
      callback(columns[columnNdx++]);
      if (columnNdx < columns.length) {
        // should we make this random to siumlate network speed
        var timeToNextChunkMS = 17;
        setTimeout(sendNextColumn, timeToNextChunkMS);
      }
    }
  }
}

function createImageChunk(imageData, x, y, width, height) {
  var data = new Uint8Array(width * height);
  for (var yy = 0; yy < height; ++yy) {
    for (var xx = 0; xx < width; ++xx) {
      var srcOffset = ((yy + y) * imageData.width + xx + x) * 4;
      var dstOffset = yy * width + xx;
      // compute gray scale
      var gray = Math.max(imageData.data[srcOffset], imageData.data[srcOffset + 1], imageData.data[srcOffset + 2]);
      data[dstOffset] = gray;
    }
  }
  return data;
}

function rand(min, max) {
  return Math.floor(Math.random() * max - min) + min;
}

function createImageToSend(width, height) {
  // create a texture using a canvas so we don't have to download one
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.width = width;
  ctx.height = height;
  ctx.fillStyle = "#222";
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.lineWidth = 20;
  ["#AAA", "#888", "#666"].forEach(function(color, ndx, array) {
    ctx.strokeStyle = color;
    ctx.beginPath();
    ctx.arc((ndx + 1) / (array.length + 1) * ctx.canvas.width, ctx.canvas.height / 2,
            ctx.canvas.height * 0.4, 0, Math.PI * 2, false);
    ctx.stroke();
  });
  ctx.fillStyle = "white";
  ctx.font = "40px sans-serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("Some Image", ctx.canvas.width / 2, ctx.canvas.height / 2);
  return ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
}
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<script id="vs" type="not-js">
// we will always pass a 0 to 1 unit quad
// and then use matrices to manipulate it
attribute vec4 position;   

uniform mat4 matrix;
uniform mat4 textureMatrix;

varying vec2 texcoord;

void main () {
  gl_Position = matrix * position;
  
  texcoord = (textureMatrix * position).xy;
}
</script>
<script id="fs" type="not-js">
precision mediump float;

varying vec2 texcoord;
uniform sampler2D texture;

void main() {
  gl_FragColor = texture2D(texture, texcoord);
}
</script>
<canvas id="c" width="640" height="480"></canvas>

答案 1 :(得分:1)

gl.texImage2D很慢,没有太多可以做的改进。原因是texImage2D涉及状态更改,并要求GPU暂停所有渲染,然后从CPU RAM中获取数据。根据硬件的不同,主板和GPU之间的接口可能非常慢(与RAM访问速度相比)

您还会添加图像分辨率的问题。 GPU上的所有图像的大小均为2(32,64,128,256,512,1024 ...),独立于高度和宽度。发送640 x 480的图像不符合此规则。为了适应不良尺寸,GPU将分配W 1024×H 512像素的图像,因此必须重新确定图像数据的尺寸以适应内部尺寸(快速,因为它们不是他们擅长的东西)。取决于硬件,这将导致在已经很慢的数据传输之上的额外减速。

如果使数据缓冲区等于两个规则(POT)(1024,512)的幂,则可能会略有改善。

您最好的选择是在整个图像加载之前避免传输,然后只执行一次。

如果您真的需要它,那么我建议您将图像分成较小的图像并发送较小的单独图像。例如,对于POT图像尺寸,1024×512可以被分成128×64个图像或1024×8,从而产生64个较小的图像。仅在小图像可用时发送,并在GPU上将图像重新组合为渲染期间的图像。这将使您将图像发送到GPU所需的时间提高了近64倍。

除此之外,没有其他事情可以做。 GPU擅长渲染,GPU在主板IO方面很糟糕,不惜一切代价(渲染时)避免这种情况,以充分利用图形硬件。