When is customShaderNameResolve called?

I’m making some custom shaders and replacing the source code of Babylon shaders. I’m using the customShaderNameResolve method. For some reason, sometimes this seems to get called every frame. Is this supposed to get called every frame?

No, if it is called every frame it means the material effect is recreated each frame which should normally not happen. It means you are doing some changes on the material that triggers a recompilation each frame.

A repro would be great if it happens without changing materials properties on standard/pbr/nme as it could be a pretty annoying bug ?

1 Like

I found at least one case I don’t understand, my material defines._renderId is for some reason always behind the scene’s renderID. I discovered this by debugging PushMaterial.prototype._isReadyForSubMesh. Every frame, the defines._renderId is two lower than the scene render ID.

Both render IDs increment every frame, but the mesh material is always lower.

I’m trying to track down how my material gets into this state.

I seem to get into this state when, on my material, I set material.bumpSampler = someTexture, where I have someTexture previously set as new BABYLON.Texture('/somefile.jpg', scene). I’m not re-creating someTexture every frame. But the act of setting this property:

sceneData.mesh.material[input.property] = newValue

Where input.property is bumpSampler, then I get into the state where the renderID lags behind the scene.

I’m only setting this value once, I don’t set bumpSampler every loop.

I’m trying to re-create what my code is doing in a playground. I haven’t yet been able to reproduce my bug in a playground, but you can see generally what I’m doing at this link, if anything looks out of place:

This is not a bug, it’s normal operation.

scene.renderId is incremented once if render targets are enabled (which is the default, even if there are no render targets to render) and once more before each camera is processed. Thus, at each frame, scene.renderId is incremented by at least 2 (more if there are render targets, more than one camera, or for other reasons).

The PushMaterial._isReadyForSubMesh function is an optimization to avoid checking a material more than once in a frame. So the first time this function is run (for a material/subMesh) for a frame, you will have a difference between defines._renderId and scene.renderId of at least 2.

I think I’m confused, because you said:

if [customShaderNameResolve] called every frame it means the material effect is recreated each frame which should normally not happen

But now you are saying

This is not a bug, it’s normal operation.

In my debugging image, you can see that _isReadyForSubMesh is false. The call stack looks like:

Mesh.prototype.render calls material.isReadyForSubMesh(...).

That itself calls this._isReadyForSubMesh(subMesh) and inside of there it checks defines._renderId === this.getScene().getRenderId() which you’re saying will always be false.

So then back up in isReadyForSubMesh it continues on to call var effect = this._prepareEffect(...) which itself calls customShaderNameResolve.

So because the render IDs are different every frame, customShaderNameResolve is called every frame.

Is this expected behavior? Is something else in isReadyForSubMesh supposed to bail out before it gets to _prepareEffect to avoid customShaderNameResolve getting called every frame?

Not exactly, it will be false the first time it is called in a frame, but true in case it is called multiple times in the same frame.

No, this._prepareEffect(...) won’t call customShaderNameResolve if defines.isDirty === false.

The fact that in your case customShaderNameResolve is called every time means that defines.isDirty === true, which means that you are modifying the material with every frame and dirtyfies it. You should find out why the material is dirty in each frame.

Hmm. You’re right, defines._isdirty is true. When I compare the defines object between two frames, only one key is different between the frames, which is _renderId. I’m not explicitly setting _renderId. All the other keys/values on the two defines objects are === identical.

Does Babylon’s internals setting _renderId mark the defines as dirty?

No, renderId is set to scene.renderId at the end of isReadyForSubMesh and does not dirtyfy the material.

You should look at these properties of the defines object:

    public _areLightsDirty;
    public _areLightsDisposed;
    public _areAttributesDirty;
    public _areTexturesDirty;
    public _areFresnelDirty;
    public _areMiscDirty;
    public _arePrePassDirty;
    public _areImageProcessingDirty;

You should have at least one being true when defines.isDirty == true. That should help you find which changes to the material dirtyfies it.

Interesting. All of my _are* keys appear to be false, but _isDirty is still true. Here’s a copy-paste from all of those keys:

_areAttributesDirty: false
_areFresnelDirty: false
_areImageProcessingDirty: false
_areLightsDirty: false
_areLightsDisposed: false
_areMiscDirty: false
_arePrePassDirty: false
_areTexturesDirty: false
_isDirty: true
_keys: (218) [...]
_needNormals: true
_needUVs: true
_normals: true
_renderId: 9976
_uvs: true

The only key suffixed with “Dirty” that’s true is the _isDirty. Do either any of the other keys like _needsNormals affect dirty?

Still tracing, looks like markAsUnprocessed() is getting called because _isReadyInternal returns false, because there is no _pipelineContext on the effect? Each frame it gets to the last line of this method

    Effect.prototype._isReadyInternal = function () {
        if (this._isReady) {
            return true;
        }
        if (this._pipelineContext) {
            return this._pipelineContext.isReady;
        }
        return false;
    };

I’m starting to suspect this is happening because my shader is failing to compile, causing no pipeline context to be created, but I’m still investigating.

I might need to open a separate thread for this.

I found an issue, which has been confusing me for a while. I have been getting a compile error after setting valid GLSL inside of processFinalCode. After some debugging it seems like processFinalCode is a poor API name, because it’s not “final.”

In the GLSL I put in proessFinalCode, I create my own out variable, as in out vec4 myCustomOut;. Babylon also apparently hijacks the code after processFinalCode and adds its own out variable: https://github.com/BabylonJS/Babylon.js/blob/6fa632432ec5e7940933922c5018ccfd25aa2c47/packages/dev/core/src/Engines/WebGL/webGL2ShaderProcessors.ts#L32

code = code.replace(/void\s+?main\s*\(/g, (hasDrawBuffersExtension ? "" : "out vec4 glFragColor;\n") + "void main(");

The end result of this is my shader ends up containing two output variables:

out vec4 myCustomOut;
...
out vec4 glFragColor;
void main() {

This causes the GLSL error:

Error: FRAGMENT SHADER ERROR: 0:114: ‘myCustomOut’ : must explicitly specify all locations when using multiple fragment outputs
ERROR: 0:658: ‘glFragColor’ : must explicitly specify all locations when using multiple fragment outputs

My suspicion is that the invalid GLSL in the shader causes a compilation failure which prevents creation of a pipeineContext, which I first noticed as customShaderNameResolve getting called on each frame. At least, I think, I’m not sure. I can’t reproduce this bug in the playground I linked above. If I enter invalid GLSL in the textarea then I don’t see customShaderNameResolve called in a loop. Is there some edge case bug in Babylon where invalid GLSL causes customShaderNameResolve to get called every frame?

My goal is to take the raw GLSL created by a Babylon PBRMaterial, manipulate it with a compiler, and then put the compiled GLSL back into the source code of a new PBRMaterial. Is there a way to prevent Babylon from manipulating the code after the processFinalCode method? I’m trying to set my own out variable, which is conflicting with the one that Babylon hijacks the GLSL with.

Yes, you need to fix the compilation errors you have first, because once you fix them, your other errors might disappear.

In fact, processFinalCode is really called as a last step: the injection of out vec4 glFragColor is done before, so you can remove it by string manipulation if you want (replacing it with an empty string - there’s no way to avoid Babylon doing this injection).

See this PG:

It dumps the fragment code from the processFinalCode callback: you will see that out vec4 glFragColor is there.

Making progress! I think what I’m seeing happening is that my material customShaderNameResolve/processFinalCode is getting called multiple times as part of my compilation process. I intentionally bail out of customShaderNameResolve if my internal compiled source code hasn’t changed. I’m trying to determine if that’s the issue - that on subsequent calls of customShaderNameResolve for (what should be) the same custom material, since I bail out, it doesn’t set processFinalCode, so it doesn’t remove the glFragColor line on subsequent calls.

In my compiler, what I’m trying to do is:

  • I kick off my compile process, something like myEngine.compile()

  • I create a new PBRMaterial and create a processFinalCode so that I can scrape its source code.

  • Right after creation, I call newPbrShaderMaterial.forceCompilation(mesh) to force the scrape to occur. My expectation is that forceCompilation is a synchronous operation, all within the same frame.

  • I (synchronously) pass the returned source code back to the scene component, which itself creates a new PBRMaterial with processFinalCode that injects the compiled source code (the first paragraph of this post).

I have a few questions now, because I think my assumptions are wrong:

  1. Can I count on forceCompilation to be synchronous like I am to scrape the source code? Or must I wait until the callback happens? The code path I described works, I get the initial PBR source code synchronously (I think).

  2. When I call newPbrShaderMaterial.forceCompilation(mesh) - I think it also causes the mesh’s current material to recompile. Does cause both newPbrShaderMaterial and mesh.material to recompile? Does it cause any other materials in the scene to recompile? Can I force the compilation of a single material only?

  3. I notice you call the original _saveCustomShaderNameResolve on the shader - should I be doing that in my case? What is the risk of not doing that?

Thanks for your help by the way, you’ve moved me very far along.

No, it is not synchronous, except if you set engine.getCaps().parallelShaderCompile = null. It can be synchronous even when parallelShaderCompile is enabled, depending on the fact that the effect already exists or not. If it does not exist and a new one must be created, then it is not synchronous because it “waits” (using setTimeout regularly) for the effect to be ready before calling the onCompiled callback.

No, only the material for which you call forceCompilation will be recompiled, for the mesh you pass in parameter.

If you know that nobody else will have registered a customShaderNameResolve then you are safe not calling the existing one (if it exists). In any case, it’s not used by the system, so you can’t generate internal errors if you don’t call an existing customShaderNameResolve.

Ah, ok, I think I have been able to reproduce the specific bug that’s confusing me.

Click “change texture” and observe the console. https://playground.babylonjs.com/#YPXI4C#19

How can this infinite dirty/re-check be prevented when modifying a material such as this? And why does it only happen when a material is added?

From debugging the code, it’s because the renderId is different in _isReadyForSubMesh.

You are doing defines.AMBIENTDIRECTUV = 0.00001 * id(); in the callback, so in effect you are dirtyfying the material each time the callback is called. [EDIT] Hum, not so sure, even if removing the line makes it work… Disabling parallel shading compilation makes it also work, so I will have to debug it when I come back to work next week. [/EDIT]

You probably don’t need this line, as customShaderNameResolve being called means the effect is being recreated.