gpujs / gpu.js

GPU Accelerated JavaScript
https://gpu.rocks
MIT License
15.1k stars 654 forks source link

doesn't work in ipad pro #632

Open hiukim opened 4 years ago

hiukim commented 4 years ago

A GIF or MEME to give some spice of the internet

What is wrong?

It's very weird. The example (https://observablehq.com/@fil/image-to-gpu) doesn't work at all on my ipad pro (3rd generation). i've just updated iOS version to 13.6, but not sure whether it's related. Have tried with both chrome and safari, both didn't work. I have tried other devices including android and iphone, no problem.

Attached is the screenshot showing the error: render = Error: Error compiling vertex shader: null

How do we replicate the issue?

Just open the example page: https://observablehq.com/@fil/image-to-gpu

How important is this (1-5)?

5

Other Comments

IMG_D6C7F647831A-1

hiukim commented 4 years ago

I have also tried with a simplest possible code gpu = new GPU(), and doesn't work.

robertleeplummerjr commented 4 years ago

Still working to reproduce, fyi.

hiukim commented 4 years ago

@robertleeplummerjr So I've been trying to dig into the code, and I've located some symptoms. The error throw around the getFeatures method https://github.com/gpujs/gpu.js/blob/d16b5eca5bc4e784eeaabca94bfe1114e767433b/src/backend/gl/kernel.js#L115

I also dig deeper and found that the result of getIsFloatRead is weird in my ipad safari. I managed to isolate a lot of code from getIsFloatRead and create a small scripts that can reproduce the errors (this can be run entirely in an independent html file without gpu.js):

<script type="x-shader/x-vertex" id="vertex-shader">
  precision lowp float;
  precision lowp int;
  precision lowp sampler2D;

  attribute vec2 aPos;
  attribute vec2 aTexCoord;

  varying vec2 vTexCoord;
  uniform vec2 ratio;

  void main(void) {
    gl_Position = vec4((aPos + vec2(1)) * ratio + vec2(-1), 0, 1);
    vTexCoord = aTexCoord;
  }
</script>

<script type="x-shader/x-fragment" id="fragment-shader">
  precision lowp float;
  precision lowp int;
  precision lowp sampler2D;

  ivec3 uOutputDim = ivec3(1, 1, 1);
  ivec2 uTexSize = ivec2(1, 1);
  varying vec2 vTexCoord;

  int index;
  float kernelResult;
  void kernel() {
    kernelResult = 3.0;
    return;
  }
  void main(void) {
    index = int(vTexCoord.s * float(uTexSize.x)) + int(vTexCoord.t * float(uTexSize.y)) * uTexSize.x;
    kernel();
    gl_FragData[0][0] = kernelResult;
  }
</script>

<script>
  const canvas = document.createElement('canvas');
  const gl = canvas.getContext('webgl');
  const testExtensions = {
    OES_texture_float: gl.getExtension('OES_texture_float'),
    OES_texture_float_linear: gl.getExtension('OES_texture_float_linear'),
    OES_element_index_uint: gl.getExtension('OES_element_index_uint'),
    WEBGL_draw_buffers: gl.getExtension('WEBGL_draw_buffers'),
  };

  const texSize = [1, 1];
  const maxTexSize = [1, 1];
  gl.enable(gl.SCISSOR_TEST);

  var source = document.querySelector("#vertex-shader").innerHTML;
  var vertexShader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(vertexShader,source);
  gl.compileShader(vertexShader);

  source = document.querySelector("#fragment-shader").innerHTML
  var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.shaderSource(fragmentShader,source);
  gl.compileShader(fragmentShader);

  program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);

  const framebuffer = gl.createFramebuffer();
  framebuffer.width = texSize[0];
  framebuffer.height = texSize[1];
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

  const vertices = new Float32Array([
    -1, -1,
    1, -1,
    -1, 1,
    1, 1
  ]);
  const texCoords = new Float32Array([
    0, 0,
    1, 0,
    0, 1,
    1, 1
  ]);

  const texCoordOffset = vertices.byteLength;
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices.byteLength + texCoords.byteLength, gl.STATIC_DRAW);
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, vertices);

  const aPosLoc = gl.getAttribLocation(program, 'aPos');
  gl.enableVertexAttribArray(aPosLoc);
  gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 0, 0);

  gl.useProgram(program);

  const loc = gl.getUniformLocation(program, 'ratio')
  gl.uniform2f(loc, texSize[0] / maxTexSize[0], texSize[1] / maxTexSize[1]);

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

  const w = 1;
  const h = 1;
  const result = new Uint8Array(w * h * 4);
  gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, result);
  console.log("result 1", result);

  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, texSize[0], texSize[1], 0, gl.RGBA, gl.FLOAT, null);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, result);
  console.log("result 2", result);

  const formatValues = (array, width) => {
    const xResults = new Float32Array(width);
    let i = 0;
    for (let x = 0; x < width; x++) {
      xResults[x] = array[i];
      i += 4;
    }
    return xResults;
  };

  const output = [1];
  const result2 = new Float32Array(w * h * 4);
  gl.readPixels(0, 0, w, h, gl.RGBA, gl.FLOAT, result2);
  const buildResult = formatValues(
    result2,
    output[0],
    output[1],
    output[2]
  );
  console.log("result 3", buildResult);
</script>

In my desktop chrome browser, the output (correct output) is:

result 1 Uint8Array(4) [0, 0, 0, 0]
result 2 Uint8Array(4) [255, 0, 0, 0]
result 3 Float32Array [3]

whereas in my ipad pro safari:

result 1 Uint8Array(4) [0, 0, 0, 0]
result 2 Uint8Array(4) [0, 0, 0, 0]
result 3 Float32Array [0]

Unfortunately I'm pretty new to webgl, so I don't understand majority of the code and have a hard time going further. Maybe you could share some insighs?

hiukim commented 4 years ago

I might have figured out the problem:

quick temporary fix

Let me give out the fix first before I go into more details about my findings: https://github.com/hiukim/gpu.js/commit/1c08d76d7b67ddfa9179f1430ffec50ed14f9ac7#diff-d0869ed9081a7d67554e369dca2c1e3dR50 It's not a good fix, as It's only suppressing the symptom. I can't be sure about the root cause, but I will give my findings and see if anyone have more information.

details

When startup, it checks the kernel feature. More specifically, there is a getIsFloatRead method.

https://github.com/gpujs/gpu.js/blob/d16b5eca5bc4e784eeaabca94bfe1114e767433b/src/backend/gl/kernel.js#L32-L50

If I understand correctly, this method assume precision: single is supported and then run the testing kernel and try whether the result matches.

Unfortunately, when precision is set to single. A float texture will be created down the road when building the kernel. i.e.

https://github.com/gpujs/gpu.js/blob/d16b5eca5bc4e784eeaabca94bfe1114e767433b/src/backend/web-gl/kernel.js#L718-L723 It tries to create a texture gl.FLOAT instead of gl.UNSIGNED_BYTE.

In my ipad pro safari, this is not successful. i.e. gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT);

Normally, it should be fine. because this method is checking whether float is support, and apparently my ipad pro safari doesn't support. So all good.

Now comes the weird things. The code continues to run until the next method try to create kernel. It fails to compile the shader, so the error throws in this line:

https://github.com/gpujs/gpu.js/blob/d16b5eca5bc4e784eeaabca94bfe1114e767433b/src/backend/web-gl/kernel.js#L518-L520

I have tried to comment out the getIsFloatRead method and there is no problem. So I can confirm that some side effect is created while running the getIsFloatRead method that somehow crashed the gl context, making it fails to further compile shaders.

Then I dig further, and found that it might be related to the texture or framebuffer (but this part I'm not sure). Anyway, after some trial and error, I added a line kernel.context.bufferData(kernel.context.ARRAY_BUFFER, 0, kernel.context.STATIC_DRAW); inside the getIsFloatRead method, and the problem goes away. Code here:

https://github.com/hiukim/gpu.js/blob/1c08d76d7b67ddfa9179f1430ffec50ed14f9ac7/src/backend/gl/kernel.js#L45-L53

It seems to be able to suppress the problem for now, but a better fix is probably required.

mauriceackel commented 3 years ago

@hiukim Unfortunately when using your quick-fix, gpu.js falls back to CPU mode for me and then fails (at least in my use case)

hiukim commented 3 years ago

@mauriceackel Now I simply return false in the first line of getIsFloatRead. You can try. It might affect accuracies or performances in some devices I guess.