Repository: processing/p5.js (JavaScript)

Issue:
On my android phone sometimes objects in sketches drawn with webgl in p5.js disappear
### Most appropriate sub-area of p5.js?

- [ ] Accessibility
- [ ] Color
- [X] Core/Environment/Rendering
- [ ] Data
- [ ] DOM
- [ ] Events
- [ ] Image
- [ ] IO
- [ ] Math
- [ ] Typography
- [ ] Utilities
- [X] WebGL
- [ ] Build Process
- [ ] Unit Testing
- [ ] Internalization
- [ ] Friendly Errors
- [ ] Other (specify if possible)

### p5.js version

1.5.0

### Web browser and version

Chrome

### Operating System

Android 12

### Steps to reproduce this

### Steps:
1. First, draw a textured triangle with p5.js webgl.
2. Then use p5.Geometry to draw an untextured square with lighting enabled.
3. However, when I view this sketch on my Android smartphone, this square is not drawn.

### Snippet:

```js
function setup() {
  createCanvas(400, 400, WEBGL);
  const geom = new p5.Geometry();
  geom.vertices.push(
    createVector(-100,-100), createVector(100,-100),
    createVector(100,100), createVector(-100,100)
  );
  geom.faces.push([0,1,2],[0,2,3]);
  geom.computeNormals();
  this._renderer.createBuffers("myPlane", geom);
  
  const gr = createGraphics(100,100);
  gr.background(255);

  background(0);
  texture(gr);
  triangle(-200,-200,0,-200,0,0);

  directionalLight(255,255,255,0,0,-1);
  ambientLight(64);
  ambientMaterial(255);
  fill(0,0,255);
  this._renderer.drawBuffers("myPlane");
}
```
![bugbug2](https://user-images.githubusercontent.com/39549290/213927281-3d1c54a5-7f16-464a-a205-a70074609e25.png)
In order to investigate this phenomenon, various experiments were performed, such as overwriting the drawBuffers function. As a result, it turned out that the cause was in the register.

First, the textured triangle and the square we draw afterward use the same shader.
When you draw a textured triangle, the UV information is stored in registers reserved for textures by the lightingShader. It is data for 3 vertices.
After that, the square created by p5.Geometry is drawn, but since the texture register remains valid at that time, the previous data is used as it is.
Since there are only 3 vertices in the data, it is insufficient to give a square with 4 vertices. This is unusual.
But it's not used for drawing, so it doesn't matter what it contains. At that time, most environments seem to handle the lack of data by filling it with 0 (probably) to prevent problems from occurring.

However, in some environments like my Android, it seems that the lack of data is recognized as abnormal and it does not draw.

Of course, it may occur because the data is not properly prepared, and it can be considered that the person writing the code is bad. However, if you don't assume drawing with textures like this, you'll have to write unnatural code to avoid this. I don't think that's a very good thing.

Problems like this occur with Shaders where not all attributes are used, such as lightingShader. Until 1.5.0, it was possible only with lightingShader, but in the next version, a new attribute called aVertexColor will be implemented. This increases the number of drawing options and makes problems like this even more likely.

Therefore, I would like to propose to disable registers for unused attributes with the disable instruction so that such problems do not occur.

## solution I think

First, let RendererGL have an array that stores flags for whether registers are enabled (For example, with a name like registerEnabled).
```javascript
this.registerEnabled = [];
```
Then set this flag in the enableAttrib function. Use the location number as an argument.
```javascript
if (!attr.enabled) {
  gl.enableVertexAttribArray(loc);
  this._renderer.registerEnabled[loc] = true; // Record register enabled state
  attr.enabled = true;
}
```

Finally, in the _prepareBuffer function, if the if branch is not for an attribute whose length is 0, if the register reserved for that attribute by the shader is enabled, disable it Add processing to make:
```javascript
if (src.length > 0) {
  /* ~~~~~~~~~~~~~~~~~~~~ */
} else {
  // Do nothing if register is not enabled
  const loc = attr.location;
  if (loc == -1 || !this._renderer.registerEnabled[loc]) { return; }
  // If enabled, disable it.
  gl.disableVertexAttribArray(loc);
  attr.enabled = false;
  this._renderer.registerEnabled[loc] = false; // Record register enabled state
}
```
It has been confirmed that the above-mentioned problems can be avoided by this specification change.
However, _prepareBuffer is an important function that affects the entire drawing process, so I can't decide on my own whether or not such a change should be made. So I will wait for the review.


Discussion: Thanks for isolating the problem! Adding `geom.uvs.push([0, 0], [0, 0], [0, 0], [0, 0])` to your sketch seems to fix the issue, so that can be a temporary fix if anyone else stumbles upon this issue.

I think your solution of enabling/disabling attributes works! I think the question will be whether or not we want our shaders (and users' custom shaders) to have to handle attributes potentially being disabled. How do you feel about, for now, auto-filling geometry buffers with 0s when we first use them? Then user shaders can always assume the `aTexCoord` attribute will exist without having to worry about whether or not a given geometry has them? Otherwise, if a user's custom shader uses `aTexCoord` but a geometry doesn't have any, maybe we'd want to display a friendly error.
Actually, if the attribute is disabled, it seems like one can still use `aTexCoord` in a shader, given that your sphere example works. So I think either approach is fine! I'm not sure without looking more closely if anything else relies on all the attribute data being defined/enabled, so if we implement this by enabling/disabling attributes, let's just make sure we add unit tests to make sure switching between shaders and immediate/retained geometry and retained geometry with/without attribute data works.
First of all, I think that it is better not to adopt the method of filling with 0.
The reason is that if you do it by drawing with fill, _useVertexColor, which is determined by the length of the array, will become true, and even if you try to draw in a single color, you will only be able to draw in black.
Regarding stroke, this problem does not occur because the vertexStrokeColors array used for judgment and the array configured by _edgesToVertices to send to the buffer are separate, but if you try to do the same thing with fill, the array will be duplicated. You will have to.
You'll have to do something like this every time you add a new attribute, but relying on register enable/disable allows you to handle any number of attributes in the same way.

Here's an example to show that the problem is with registers on and off.
[vertexColor_bug_sample](https://editor.p5js.org/dark_fox/sketches/NukJ-i3oK)
```javascript
let gr;

function setup() {
  createCanvas(400, 400, WEBGL);

  noStroke();
  gr = createGraphics(256, 256);
  gr.background(255);

  image(gr, -200, -200, 400, 400);

  fill(255, 0, 0);
  sphere(120); // The red sphere disappears.
}
```
In this example, the 3rd register is turned on when drawing with the image function. If it does not close, it will be judged that the information necessary for drawing is insufficient, and the red sphere will not be displayed in the center on my Android.
Closing the register like this draws nicely on my Android as well.
resolve version: [vertexColor_bug_resolve](https://editor.p5js.org/dark_fox/sketches/x6PFRwB5x)
This uses a non-lighting shader to draw the sphere, but using a lighting shader gives the same result.
[vertexColor_bug_sample2](https://editor.p5js.org/dark_fox/sketches/hl2NEt7vP)
[vertexColor_bug_resolve2](https://editor.p5js.org/dark_fox/sketches/PQS6H9adE)

I'm not familiar with it, so I can't decide which is more disadvantageous for drawing, turning registers on and off or padding arrays. However, it seems somewhat reasonable to rely on register on/off.

By recording the on/off status of registers in the global array, you can also avoid calling functions that close registers unnecessarily.
Also, when closing a register, it notifies the shader's attributes that use that register that it has been closed (attr.enabled = false), so it can be reliably enabled if the register is needed for subsequent drawing. And if you draw under the same conditions, the register will be left open and enabled will not be called unnecessarily.

And about the unit test, I'm sorry, I'm not familiar with it, but can it handle environment-dependent bugs? If possible, I'd like to create appropriate tests with examples like the ones I've given here.
I think you can try it with your normal workflow to see if this specification change has any effect on conventional drawing. If that doesn't work, I guess I'll try another method...
For the time being, I made a pull request in advance to check whether normal drawing is affected.
The workflow went through, so it doesn't seem to affect normal drawing.

As for the unit test, if it is possible to deal with environment-dependent bugs, I think I will try it with an example like the one I made here. In other words, it is a test to see if there are any bugs caused by opening reserved registers that use attributes that are not used for drawing.
Other than that, what about drawing different shapes with multiple shaders, for example? Sorry, I don't know what to try, so I would appreciate it if you could show me an example.



Fix (patch):
diff --git a/src/webgl/p5.RenderBuffer.js b/src/webgl/p5.RenderBuffer.js
index a586fb0323..84304fdd8c 100644
--- a/src/webgl/p5.RenderBuffer.js
+++ b/src/webgl/p5.RenderBuffer.js
@@ -58,6 +58,13 @@ p5.RenderBuffer.prototype._prepareBuffer = function(geometry, shader) {
     }
     // enable the attribute
     shader.enableAttrib(attr, this.size);
+  } else {
+    const loc = attr.location;
+    if (loc === -1 || !this._renderer.registerEnabled[loc]) { return; }
+    // Disable register corresponding to unused attribute
+    gl.disableVertexAttribArray(loc);
+    // Record register availability
+    this._renderer.registerEnabled[loc] = false;
   }
 };
 
diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js
index 09e994e175..986d39443d 100755
--- a/src/webgl/p5.RendererGL.js
+++ b/src/webgl/p5.RendererGL.js
@@ -113,6 +113,8 @@ p5.RendererGL = function(elt, pInst, isMainCanvas, attr) {
   this._useLineColor = false;
   this._useVertexColor = false;
 
+  this.registerEnabled = [];
+
   this._tint = [255, 255, 255, 255];
 
   // lightFalloff variables
diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js
index f67696a506..0439a0e34e 100644
--- a/src/webgl/p5.Shader.js
+++ b/src/webgl/p5.Shader.js
@@ -563,9 +563,11 @@ p5.Shader.prototype.enableAttrib = function(
     const loc = attr.location;
     if (loc !== -1) {
       const gl = this._renderer.GL;
-      if (!attr.enabled) {
+      // Enable register even if it is disabled
+      if (!this._renderer.registerEnabled[loc]) {
         gl.enableVertexAttribArray(loc);
-        attr.enabled = true;
+        // Record register availability
+        this._renderer.registerEnabled[loc] = true;
       }
       this._renderer.GL.vertexAttribPointer(
         loc,