Skip to content

Commit 09db23d

Browse files
authored
NME: Bubble up build errors (#16731)
Try to catch what-would-be build errors via state-- so they can eventually be bubbled up to the console UI via `onBuildErrorObservable` notifications-- without throwing and breaking the build process.
1 parent 2da08e7 commit 09db23d

File tree

11 files changed

+46
-34
lines changed

11 files changed

+46
-34
lines changed

packages/dev/core/src/Materials/Node/Blocks/Dual/smartFilterTextureBlock.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { InputBlock } from "../Input/inputBlock";
88
import type { NodeMaterialBlock } from "../../nodeMaterialBlock";
99
import type { NodeMaterial } from "../../nodeMaterial";
1010
import { ScreenSizeBlock } from "../Fragment/screenSizeBlock";
11-
import { Logger } from "core/Misc/logger";
1211
import { ShaderLanguage } from "core/Materials/shaderLanguage";
1312
import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator";
1413
import type { Scene } from "core/scene";
@@ -55,11 +54,11 @@ export class SmartFilterTextureBlock extends CurrentScreenBlock {
5554
this._samplerName = state._getFreeVariableName(this.name);
5655

5756
if (state.sharedData.nodeMaterial.mode !== NodeMaterialModes.SFE) {
58-
Logger.Error("SmartFilterTextureBlock: Should not be used outside of SFE mode.");
57+
state.sharedData.raiseBuildError("SmartFilterTextureBlock should not be used outside of SFE mode.");
5958
}
6059

6160
if (state.sharedData.nodeMaterial.shaderLanguage !== ShaderLanguage.GLSL) {
62-
Logger.Error("SmartFilterTextureBlock: WebGPU is not supported by SFE mode.");
61+
state.sharedData.raiseBuildError("WebGPU is not supported by SmartFilterTextureBlock.");
6362
}
6463

6564
// Tell FragmentOutputBlock ahead of time to store the final color in a temp variable
@@ -109,7 +108,7 @@ export class SmartFilterTextureBlock extends CurrentScreenBlock {
109108
// NOTE: In the future, when we move to vertex shaders, update this to check for the nearest vec2 varying output.
110109
const screenUv = state.sharedData.nodeMaterial.getInputBlockByPredicate((b) => b.isAttribute && b.name === "postprocess_uv");
111110
if (!screenUv || !screenUv.isAnAncestorOf(this)) {
112-
Logger.Error("SmartFilterTextureBlock: 'postprocess_uv' attribute from ScreenUVBlock is required.");
111+
state.sharedData.raiseBuildError("SmartFilterTextureBlock: 'postprocess_uv' attribute from ScreenUVBlock is required.");
113112
return "";
114113
}
115114
return screenUv.associatedVariableName;

packages/dev/core/src/Materials/Node/Blocks/Fragment/fragCoordBlock.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ export class FragCoordBlock extends NodeMaterialBlock {
102102
super._buildBlock(state);
103103

104104
if (state.target === NodeMaterialBlockTargets.Vertex) {
105-
// eslint-disable-next-line no-throw-literal
106-
throw "FragCoordBlock must only be used in a fragment shader";
105+
state.sharedData.raiseBuildError("FragCoordBlock must only be used in a fragment shader");
106+
return this;
107107
}
108108

109109
state.compilationString += this.writeOutputs(state);

packages/dev/core/src/Materials/Node/Blocks/Fragment/frontFacingBlock.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export class FrontFacingBlock extends NodeMaterialBlock {
3838
super._buildBlock(state);
3939

4040
if (state.target === NodeMaterialBlockTargets.Vertex) {
41-
// eslint-disable-next-line no-throw-literal
42-
throw "FrontFacingBlock must only be used in a fragment shader";
41+
state.sharedData.raiseBuildError("FrontFacingBlock must only be used in a fragment shader");
42+
return this;
4343
}
4444

4545
const output = this._outputs[0];

packages/dev/core/src/Materials/Node/Blocks/Fragment/heightToNormalBlock.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { NodeMaterialBlockTargets } from "../../Enums/nodeMaterialBlockTargets";
66
import { RegisterClass } from "../../../../Misc/typeStore";
77
import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator";
88
import type { Scene } from "../../../../scene";
9-
import { Logger } from "../../../../Misc/logger";
109
import { ShaderLanguage } from "../../../../Materials/shaderLanguage";
1110

1211
/**
@@ -109,7 +108,7 @@ export class HeightToNormalBlock extends NodeMaterialBlock {
109108
const fPrefix = state.fSuffix;
110109

111110
if (!this.generateInWorldSpace && !this.worldTangent.isConnected) {
112-
Logger.Error(`You must connect the 'worldTangent' input of the ${this.name} block!`);
111+
state.sharedData.raiseBuildError(`You must connect the 'worldTangent' input of the ${this.name} block!`);
113112
}
114113

115114
const startCode = this.generateInWorldSpace

packages/dev/core/src/Materials/Node/Blocks/Fragment/screenSizeBlock.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ export class ScreenSizeBlock extends NodeMaterialBlock {
8888
this._scene = state.sharedData.scene;
8989

9090
if (state.target === NodeMaterialBlockTargets.Vertex) {
91-
// eslint-disable-next-line no-throw-literal
92-
throw "ScreenSizeBlock must only be used in a fragment shader";
91+
state.sharedData.raiseBuildError("ScreenSizeBlock must only be used in a fragment shader");
92+
return this;
9393
}
9494

9595
state.sharedData.bindableBlocks.push(this);

packages/dev/core/src/Materials/Node/Blocks/PBR/anisotropyBlock.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { NodeMaterialConnectionPointCustomObject } from "../../nodeMaterialConne
99
import { TBNBlock } from "../Fragment/TBNBlock";
1010
import type { Mesh } from "../../../../Meshes/mesh";
1111
import type { Effect } from "../../../effect";
12-
import { Logger } from "core/Misc/logger";
1312
import type { NodeMaterialBuildState } from "../../nodeMaterialBuildState";
1413
import { ShaderLanguage } from "core/Materials/shaderLanguage";
1514

@@ -141,7 +140,7 @@ export class AnisotropyBlock extends NodeMaterialBlock {
141140
// we must set the uv input as optional because we may not end up in this method (in case a PerturbNormal block is linked to the PBR material)
142141
// in which case uv is not required. But if we do come here, we do need the uv, so we have to raise an error but not with throw, else
143142
// it will stop the building of the node material and will lead to errors in the editor!
144-
Logger.Error("You must connect the 'uv' input of the Anisotropy block!");
143+
state.sharedData.raiseBuildError(`You must connect the 'uv' input of the ${this.name} block!`);
145144
}
146145

147146
state._emitExtension("derivatives", "#extension GL_OES_standard_derivatives : enable");

packages/dev/core/src/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1492,7 +1492,7 @@ export class PBRMetallicRoughnessBlock extends NodeMaterialBlock {
14921492
state.compilationString += `#endif\n`;
14931493
}
14941494
} else {
1495-
Logger.Error(`There's no remapping for the ${output.name} end point! No code generated`);
1495+
state.sharedData.raiseBuildError(`There's no remapping for the ${output.name} end point! No code generated`);
14961496
}
14971497
}
14981498
}

packages/dev/core/src/Materials/Node/nodeMaterial.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -784,8 +784,8 @@ export class NodeMaterial extends PushMaterial {
784784

785785
for (const other of this.attachedBlocks) {
786786
if (other.getClassName() === className) {
787-
// eslint-disable-next-line no-throw-literal
788-
throw `Cannot have multiple blocks of type ${className} in the same NodeMaterial`;
787+
this._sharedData.raiseBuildError(`Cannot have multiple blocks of type ${className} in the same NodeMaterial`);
788+
return;
789789
}
790790
}
791791
}
@@ -915,13 +915,15 @@ export class NodeMaterial extends PushMaterial {
915915
const allowEmptyVertexProgram = this._mode === NodeMaterialModes.Particle || this._mode === NodeMaterialModes.SFE;
916916

917917
if (this._vertexOutputNodes.length === 0 && !allowEmptyVertexProgram) {
918-
// eslint-disable-next-line no-throw-literal
919-
throw "You must define at least one vertexOutputNode";
918+
this.onBuildErrorObservable.notifyObservers("You must define at least one vertexOutputNode");
919+
this._buildIsInProgress = false;
920+
return;
920921
}
921922

922923
if (this._fragmentOutputNodes.length === 0) {
923-
// eslint-disable-next-line no-throw-literal
924-
throw "You must define at least one fragmentOutputNode";
924+
this.onBuildErrorObservable.notifyObservers("You must define at least one fragmentOutputNode");
925+
this._buildIsInProgress = false;
926+
return;
925927
}
926928

927929
// Compilation state
@@ -1021,20 +1023,20 @@ export class NodeMaterial extends PushMaterial {
10211023
this._buildId = NodeMaterial._BuildIdGenerator++;
10221024
}
10231025

1024-
// Errors
1025-
const noError = this._sharedData.emitErrors(this.onBuildErrorObservable);
1026-
10271026
if (verbose) {
10281027
Logger.Log("Vertex shader:");
10291028
Logger.Log(this._vertexCompilationState.compilationString);
10301029
Logger.Log("Fragment shader:");
10311030
Logger.Log(this._fragmentCompilationState.compilationString);
10321031
}
10331032

1033+
// Errors
1034+
const noError = this._sharedData.emitErrors();
1035+
10341036
this._buildIsInProgress = false;
1035-
this._buildWasSuccessful = true;
10361037
if (noError) {
10371038
this.onBuildObservable.notifyObservers(this);
1039+
this._buildWasSuccessful = true;
10381040
}
10391041

10401042
// Wipe defines

packages/dev/core/src/Materials/Node/nodeMaterialBuildState.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { NodeMaterialBlock } from "./nodeMaterialBlock";
99
import { Process } from "core/Engines/Processors/shaderProcessor";
1010
import type { _IProcessingOptions } from "core/Engines/Processors/shaderProcessingOptions";
1111
import { WebGLShaderProcessor } from "core/Engines/WebGL/webGLShaderProcessors";
12+
import { Logger } from "core/Misc/logger";
1213

1314
/**
1415
* Class used to store node based material build state
@@ -112,7 +113,8 @@ export class NodeMaterialBuildState {
112113
*/
113114
public async getProcessedShaderAsync(defines: string): Promise<string> {
114115
if (!this._builtCompilationString) {
115-
throw new Error("Shader not built yet.");
116+
Logger.Error("getProcessedShaderAsync: Shader not built yet.");
117+
return "";
116118
}
117119

118120
const engine = this.sharedData.nodeMaterial.getScene().getEngine();

packages/dev/core/src/Materials/Node/nodeMaterialBuildStateSharedData.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { Scene } from "../../scene";
55
import type { Immutable, Nullable } from "../../types";
66
import type { NodeMaterial, NodeMaterialTextureBlocks } from "./nodeMaterial";
77
import { Logger } from "core/Misc/logger";
8-
import type { Observable } from "core/Misc/observable";
98

109
/**
1110
* Class used to store shared data between 2 NodeMaterialBuildState
@@ -143,6 +142,7 @@ export class NodeMaterialBuildStateSharedData {
143142
emitVertex: false,
144143
emitFragment: false,
145144
notConnectedNonOptionalInputs: new Array<NodeMaterialConnectionPoint>(),
145+
customErrors: new Array<string>(),
146146
};
147147

148148
/**
@@ -187,12 +187,21 @@ export class NodeMaterialBuildStateSharedData {
187187
this.defineNames["MAINUV7"] = 0;
188188
}
189189

190+
/**
191+
* Push a new error to the build state, avoiding exceptions that can break the build process
192+
* @param message defines the error message to push
193+
*/
194+
public raiseBuildError(message: string) {
195+
if (this.checks.customErrors.indexOf(message) !== -1) {
196+
this.checks.customErrors.push(message);
197+
}
198+
}
199+
190200
/**
191201
* Emits console errors and exceptions if there is a failing check
192-
* @param errorObservable defines an Observable to send the error message
193202
* @returns true if all checks pass
194203
*/
195-
public emitErrors(errorObservable: Nullable<Observable<string>> = null) {
204+
public emitErrors() {
196205
let errorMessage = "";
197206

198207
if (!this.checks.emitVertex && !this.allowEmptyVertexProgram) {
@@ -206,13 +215,14 @@ export class NodeMaterialBuildStateSharedData {
206215
notConnectedInput.ownerBlock.name
207216
}[${notConnectedInput.ownerBlock.getClassName()}] is not connected and is not optional.\n`;
208217
}
218+
for (const customError of this.checks.customErrors) {
219+
errorMessage += customError + "\n";
220+
}
209221

210222
if (errorMessage) {
211-
if (errorObservable) {
212-
errorObservable.notifyObservers(errorMessage);
213-
}
214-
Logger.Error("Build of NodeMaterial failed:\n" + errorMessage);
215-
223+
errorMessage = "Node material build failed: \n" + errorMessage;
224+
Logger.Error(errorMessage);
225+
this.nodeMaterial.onBuildErrorObservable.notifyObservers(errorMessage);
216226
return false;
217227
}
218228

0 commit comments

Comments
 (0)