Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable unused vertexAttribute to avoid environment-dependent vanishing bugs #5970

Merged
merged 10 commits into from Jan 26, 2023
Merged

Disable unused vertexAttribute to avoid environment-dependent vanishing bugs #5970

merged 10 commits into from Jan 26, 2023

Conversation

inaridarkfox4231
Copy link
Contributor

@inaridarkfox4231 inaridarkfox4231 commented Jan 23, 2023

Resolves #5968

In some environments, such as my favorite Android phone, rendering in webgl of p5.js may not be rendered correctly.
For example, see the sketch below.
minimum_bug_demo

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");
}

My Android doesn't show the blue square in the middle.
On the other hand, my laptop at home displays squares.
bugbug2

Problems like this occur when some shader attributes are unused and the corresponding registers are not closed, causing the data in them to be corrupted. Therefore, I would like to propose a specification change to close unused registers each time so that such a thing does not occur.
It also records information about whether a register is closed or not, so that unnecessary closing instructions are not executed.
Changes:

  1. Have the renderer have an array that keeps track of whether a register is open or not.
  2. Tells the renderer to store true in that number when opening the register in the enableAttrib function.
  3. In the _prepareBuffer function, close the registers reserved by the shader for attributes that the geometry doesn't have that the shader might use. Notify it to shaders and renderers.
  4. Notifying the shader is to open the register when it's likely to be used again, and notifying the renderer is to avoid closing the same register over and over again when repeating the same instruction

PR Checklist

Prepare an array to record the availability of registers to prevent the same register from being disabled more than once.
In the enableAttrib function, when enabling a register, tell the renderer that it is available by using the location number as an argument.
If the registers corresponding to the attributes that are not used for drawing are left open, the data still stored there will cause problems.
So it asks the renderer if the register corresponding to it is open, and if it is, it closes it and notifies the renderer and shader of that state.
Use === instead of ==.
remove trailing space
@inaridarkfox4231
Copy link
Contributor Author

inaridarkfox4231 commented Jan 24, 2023

I haven't read it properly yet, but I found a function in Three.js that looks like this:
WebGLBindingStates.js

function disableUnusedAttributes() {
  
const newAttributes = currentState.newAttributes;
const enabledAttributes = currentState.enabledAttributes;
  
for ( let i = 0, il = enabledAttributes.length; i < il; i ++ ) {
  if ( enabledAttributes[ i ] !== newAttributes[ i ] ) {
    gl.disableVertexAttribArray( i );
    enabledAttributes[ i ] = 0;
    }
  }
}

This operation makes sense if enableAttributes is 1 and newAttributes is 0 before assigning 0 (since assigning 0 is meaningless). The arguments for these arrays are, I guess, the numbers of the registers (since we are assigning their values ​​to the functions).
0 means that the attribute is not needed for drawing, and 1 means that the register is enabled nonetheless. That is, it seems that this is doing the same thing that I'm proposing here.
This process is executed at the very end after finishing the main process of attribute setup. Therefore, I think it is more rational to use register enable/disable than temporary measures such as filling with 0.

@davepagurek
Copy link
Contributor

This works great, and the code is very concise! Also, I tested using a shader that references a disabled attribute like I mentioned in my comment on #5968, and it works just fine: https://editor.p5js.org/davepagurek/sketches/vrgwcBpJh

One thing I was wondering about is how this handles shaders whose attributes are in different locations and if things might break. However, since we loop through all the buffers each time, each buffer's new location will end up properly enabled/disabled. In the future if we allow custom attributes (#5120) that aren't checked in every render, then we'll have to make sure those get disabled too when we switch to a shader or model that doesn't use them, but that'll be a problem for future me 😅

We can maybe add a unit test or two making sure we put the right values in registerEnabled and then this is good to go!

@inaridarkfox4231
Copy link
Contributor Author

I found a bug. With this logic, if another shader has disabled a register it should use, and if that shader doesn't know about it, it won't be able to re-enable it, and the register will remain disabled forever.
See the example below. bug_remained

In the patch I prepared, the lightingShader uses aTexCoord at number 2, and the colorShader uses aVertexColor at number 2. First, the lightingShader is used to draw the background, and then the colorShader is used to draw the spheres. Register 2 is disabled because the sphere does not have aVertexColor, but the lightingShader does not know about it, so it remains disabled.
The result is a black background.

To avoid this, in the enableAttrib function, it is necessary to consider not only the enabled information of the corresponding shader, but also the enabled state of the corresponding register.
If you change to a patch with "_debug" in html, the background will be drawn safely.

If the shader doesn't know that the register has been disabled by another shader, it can't enable it.
Therefore, even if the corresponding shader does not know about it, if the register used by it is disabled, enable processing should be performed.
@davepagurek
Copy link
Contributor

good catch, and thanks for looking into that some more! I didn't look closely enough at the code that enables attributes to notice that it was only conditionally calling gl.enableVertexAttribArray.

Since only the global array (named "registerEnabled") needs to know the enabled/disabled state of a register,
we eliminate the "enabled" property of attribute.
It is impossible for the shader itself to know when a register is available or not. I decided to leave that to the global array.
So I'm going to remove that property.
@inaridarkfox4231
Copy link
Contributor Author

Here is a test to try rewriting the previous code: debug_test
The patch used here is a simple one that implements only per-vertex coloring. It has the changes suggested in this pull request. to clarify the nature of the problem.

What is causing this bug? This is because the shader maintains information about the enabled/disabled state of registers, and p5.js can only enable registers.

First, what does shader attributes enabled mean? It means that the register corresponding to the shader's attribute is enabled and that the register contains some number. Whatever the number, attribute is enabled if the register is enabled. Because that data is used during the draw call.
Now, since multiple shaders share the same register numbers, it's quite common for other shaders to enable and fill the registers used by that shader. Even in that case, it can be said that the attribute of that shader is enabled. That data may not be the data that the shader uses to draw, but the register is enabled and contain data.

But the shader's attribute's enabled property is not true. It's valid, but it's still false.

In order to do this properly, it is necessary to create a horizontal connection in the shader and some other method is required, but it is much easier to manage the on/off of the register globally. For that reason, I decided to prepare an array called "registerEnabled" to manage the flags.
In the first place, what the enabled property is used for is as a flag to open the register that should be used if it is closed. If so, it should be possible to replace it with a global flag, so I did that.

Also, this problem is caused by the shader not being fed data from the geometry and leaving a register that should not be used open even though it contains bad data, so it must be closed.
In other words, open the register if there is data supplied from the geometry, close the register if there is no supply. Properly rewriting the register opening and closing process is all we have to do here. I just managed it with a global flag.

By the way, regarding the unit test, I'm thinking of making sure that the value of this global flag is set properly. Please wait for a while...

@inaridarkfox4231
Copy link
Contributor Author

inaridarkfox4231 commented Jan 25, 2023

I created a test like this:

suite('Test for register availability', function() {
  test('register enable/disable flag test', function(done) {
    const renderer = myp5.createCanvas(16, 16, myp5.WEBGL);

    // geometry without aTexCoord.
    const myGeom = new p5.Geometry(1, 1, function() {
      this.gid = 'registerEnabledTest';
      this.vertices.push(myp5.createVector(-8, -8));
      this.vertices.push(myp5.createVector(8, -8));
      this.vertices.push(myp5.createVector(8, 8));
      this.vertices.push(myp5.createVector(-8, 8));
      this.faces.push([0, 1, 2]);
      this.faces.push([0, 2, 3]);
      this.computeNormals();
    });

    myp5.fill(255);
    myp5.directionalLight(255, 255, 255, 0, 0, -1);

    myp5.triangle(-8, -8, 8, -8, 8, 8);

    // get register location of
    // lightingShader's aTexCoord attribute.
    const attributes = renderer._curShader.attributes;
    const loc = attributes.aTexCoord.location;

    assert.equal(renderer.registerEnabled[loc], true);

    myp5.model(myGeom);
    assert.equal(renderer.registerEnabled[loc], false);

    myp5.triangle(-8, -8, 8, 8, -8, 8);
    assert.equal(renderer.registerEnabled[loc], true);

    done();
  });
});

This test draws a triangle with aTexCoord and a custom square geometry without aTexCoord. After drawing the triangle, the availability of the register used by the lightingShader's aTexCoord should be true if the registerEnabled value is set appropriately. And after drawing geometry without aTexCoord, the register should be closed and this value should be false. Finally, if we draw the triangle with aTexCoord again, it should return true again.

I decided to use a global flag to manage the enabled/disabled state of the registers used to store shader attributes.
This value will be true if the geometry supplies a value, false if it does not.
Therefore, I prepared a geometry with aTexCoord and a geometry without it, and tested whether the value was properly switched.
I accidentally wrote "myp5.createVector" as "createVector". Just a typo. Excuse me.
@inaridarkfox4231
Copy link
Contributor Author

inaridarkfox4231 commented Jan 25, 2023

I have a few concerns that I would like to mention.
First of all, for the example of the custom shader earlier,
minimum_bug_demo copy
Results were different on LAPTOP and Android. The cause is probably p5.js's shader validation, which is considered a custom lighting shader because of aNormal. aNormal is not considered an attribute in the Android implementation. Therefore, I think that it was not regarded as a custom lighting shader and was drawn normally.
In this example, I don't think it's desirable to have that kind of difference in behavior between LAPTOP and Android, so I thought it would be better to use a different attribute or let resetShader() do normal drawing.

Also, You mentioned custom attributes, but I made a sample before. What I did was add a new one to the Retained mode RenderBuffer by pushing.
add attribute
By sending the numbers in the p5.Geometry configuration to the array named there, I was able to draw without any problems.
Also, I changed the site library to "myP5JS_debug.js", which was introduced in the previous comment, and it worked without any problems. Since I only use one shader, it may not be very helpful, but I will post it for the time being.

@davepagurek
Copy link
Contributor

davepagurek commented Jan 25, 2023

Results were different on LAPTOP and Android

Ah I see, it's not getting recognized as an attribute because it's getting optimized away. If, in the fragment shader, I reference vNormal (e.g. adding + vNormal.xy * 0.001 when computing the r and g components of gl_FragColor), then it appears again, and p5 detects it. p5 can probably switch at some point to a more reliable method of detecting what type of shader it is (maybe if a user fill shader is defined, just always use it, and give it whatever lighting values exist if it has those attributes/uniforms?) but we can address that at a later time if we want.

Also, You mentioned custom attributes, but I made a sample before. What I did was add a new one to the Retained mode RenderBuffer by pushing.

Ah I see, as long as we're adding custom buffers to the global list of buffers, then everything should be good, as it will make p5 check for is existence and disable it if you render subsequent geometry that doesn't have data for that buffer. Looks good!

@inaridarkfox4231
Copy link
Contributor Author

inaridarkfox4231 commented Jan 26, 2023

I think I've done everything I can. I've been trying all week to find the cause of this phenomenon. I was able to identify the cause of the problem and create an example to reproduce it. I made a patch and tried to see if my sketch in existing OpenProcessing didn't cause any problems.
I would appreciate it if you could leave a review if there are any problems.

@@ -563,9 +563,11 @@ p5.Shader.prototype.enableAttrib = function(
const loc = attr.location;
if (loc !== -1) {
const gl = this._renderer.GL;
if (!attr.enabled) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, we don't need this any more per shader attribute, now that we have a global one.

Copy link
Contributor

@davepagurek davepagurek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for diagnosing the issue and coming up with this solution! The code makes sense, and I haven't been able to break it in my tests when switching between different shaders/geometry, so I think this is good to go!

@davepagurek davepagurek merged commit f970dd3 into processing:main Jan 26, 2023
@inaridarkfox4231 inaridarkfox4231 deleted the Disable-unused-registers branch January 26, 2023 02:18
@inaridarkfox4231 inaridarkfox4231 changed the title Disable unused registers to avoid environment-dependent vanishing bugs Disable unused vertexAttribute to avoid environment-dependent vanishing bugs Jan 29, 2023
@inaridarkfox4231
Copy link
Contributor Author

I later learned that "register" is not an official term, the official term is "vertexAttribute". I apologize for any confusion this may have caused. However, as for the variable name, I think that "register" is easier to understand in the sense that it is the place where information about the buffer is registered, so I would like to leave it as it is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

On my android phone sometimes objects in sketches drawn with webgl in p5.js disappear
2 participants