@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
288 lines (262 loc) • 8.94 kB
text/typescript
import {BaseNodeType} from '../_Base';
import {Poly} from '../../Poly';
import {CorePerformance} from '../../../core/performance/CorePerformance';
import {NodeCookPerformanceformanceController} from './cook/PerformanceController';
import {ContainerMap} from '../../containers/utils/ContainerMap';
import {NodeContext} from '../../poly/NodeContext';
import {ContainableMap} from '../../containers/utils/ContainableMap';
import {touchNodeRef} from '../../../core/reactivity/NodeReactivity';
import {NodeInputsController} from './io/InputsController';
enum ErrorType {
FROM_INPUTS = 'node inputs error',
INTERNAL = 'node internal error',
}
export type OnCookCompleteHook = () => void;
export class NodeCookController<NC extends NodeContext> {
private _corePerformance: CorePerformance;
private _cooking: boolean = false;
private _cookingDirtyTimestamp: number | undefined;
private _performanceController: NodeCookPerformanceformanceController = new NodeCookPerformanceformanceController(
this
);
private _inputContainers: Array<ContainerMap[NC] | null> = [];
private _inputContents: ContainableMap[NC][] = [];
private _EMPTY_ARRAY: ContainableMap[NC][] = [];
constructor(private node: BaseNodeType) {
this._corePerformance = this.node.scene().performance;
}
performanceRecordStarted() {
return this._corePerformance.started();
}
dispose() {
this._clearHooks();
}
// Disallowing inputs evaluation is important for switch nodes (such as SOP and COP)
// that should not evaluate all inputs, but only a single one, depending on a param value
// currently only for switch SOP and COP
private _inputsEvaluationRequired: boolean = true;
disallowInputsEvaluation() {
this._inputsEvaluationRequired = false;
}
isCooking(): boolean {
return this._cooking === true;
}
private _startCookIfNoErrors(inputContents: ContainableMap[NC][]) {
if (this.node.states.error.active() || this.node.disposed() == true) {
this.endCook();
} else {
try {
this._performanceController.recordCookStart();
// make sure we treat rejected promise
// if cook is async
const promise = this.node.cook(inputContents);
if (promise != null) {
promise.catch((e: any) => {
this._onError(e, ErrorType.INTERNAL, false);
});
}
} catch (e) {
this._onError(e, ErrorType.INTERNAL);
}
}
}
async cookMain() {
if (this.isCooking()) {
return;
}
if (this.node.disposed() == true) {
return;
}
this._initCookingState();
this.node.states.error.clear();
this.node.scene().cookController.addNode(this.node);
try {
// we need to try/catch inputs fetching,
// as some nodes like the sop/normalsHelper
// currently fail when being cloned
// const inputContents: ContainableMap[NC][] | undefined = this._inputsEvaluationRequired
// ? await this._evaluateInputs()
// : undefined;
this._inputContents.length = 0;
if (this._inputsEvaluationRequired) {
await this._evaluateInputs(this._inputContents);
}
if (this.node.params.paramsEvalRequired()) {
await this._evaluateParams();
}
this._startCookIfNoErrors(this._inputContents);
} catch (e) {
this._onError(e, ErrorType.FROM_INPUTS);
}
}
private _onError(e: unknown, errorType: ErrorType, alwaysSet: boolean = true) {
if (alwaysSet || !this.node.states.error.active()) {
const processedError = this.node.processError(e);
this.node.states.error.set(`${errorType}: '${processedError}'.`);
Poly.warn(e);
}
this.endCook();
}
async cookMainWithoutInputs() {
this.node.scene().cookController.addNode(this.node);
if (this.isCooking()) {
// TODO:
// this seems to happen because when we flush the cooker queue,
// some graph nodes will trigger more updates, which will then make dependent nodes
// dirty again
Poly.warn('cook_main_without_inputs already cooking', this.node.path());
return;
}
this._initCookingState();
this.node.states.error.clear();
if (this.node.params.paramsEvalRequired()) {
await this._evaluateParams();
}
this._startCookIfNoErrors(this._EMPTY_ARRAY);
}
endCook(/*message?: string | null*/) {
this._finalizeCookPerformance();
const dirtyTimestamp = this.node.dirtyController.dirtyTimestamp();
const timestampUnchangedSinceCookStarted =
dirtyTimestamp == null || dirtyTimestamp === this._cookingDirtyTimestamp;
if (
timestampUnchangedSinceCookStarted ||
this.node.flags?.bypass?.active() /* || this._scenePlayingAndNodeAlreadyCookedForCurrentFrame()*/
) {
this.node.removeDirtyState();
this._terminateCookProcess();
} else {
if (this.node.disposed() == true) {
return;
}
Poly.log('COOK AGAIN', dirtyTimestamp, this._cookingDirtyTimestamp, this.node.path());
this._cooking = false;
this.cookMain();
}
}
// private _lastFrameCooked: number | undefined;
// private _scenePlayingAndNodeAlreadyCookedForCurrentFrame() {
// if (this.node.scene().timeController.playing()) {
// return this._lastFrameCooked == this.node.scene().frame();
// }
// return false;
// }
private _initCookingState() {
this._cooking = true;
// this._lastFrameCooked = this.node.scene().frame();
this._cookingDirtyTimestamp = this.node.dirtyController.dirtyTimestamp();
}
private _terminateCookProcess() {
if (this.isCooking() || this.node.flags?.bypass?.active()) {
this._cooking = false;
// setTimeout(this.node.containerController.notifyRequesters.bind(this.node.containerController), 0);
this.node.containerController.notifyRequesters();
this._runOnCookCompleteHooks();
if (this.node.disposed() == true) {
return;
}
touchNodeRef(this.node.path());
}
}
private async _evaluateInputs(inputContents: ContainableMap[NC][]): Promise<ContainableMap[NC][]> {
this._performanceController.recordInputsStart();
const ioOnputs: NodeInputsController<NC> = this.node.io.inputs;
// const inputContainers: (ContainerMap[NC] | null)[] = this._inputsEvaluationRequired
// ? ioOnputs.isGraphNodeDirty()
// ? await ioOnputs.evalRequiredInputs()
// : ioOnputs.containersWithoutEvaluation()
// : [];
this._inputContainers.length = 0;
if (this._inputsEvaluationRequired) {
if (ioOnputs.isGraphNodeDirty()) {
await ioOnputs.evalRequiredInputs(this._inputContainers);
} else {
ioOnputs.containersWithoutEvaluation(this._inputContainers);
}
}
const inputs = ioOnputs.inputs();
// const inputContents: ContainableMap[NC][] = [];
let inputContainer: ContainerMap[NC] | null;
for (let i = 0; i < inputs.length; i++) {
inputContainer = this._inputContainers[i];
if (inputContainer) {
if (ioOnputs.cloneRequired(i)) {
inputContents[i] = inputContainer.coreContentCloned() as ContainableMap[NC];
} else {
inputContents[i] = inputContainer.coreContent() as ContainableMap[NC];
}
}
}
this._performanceController.recordInputsEnd();
return inputContents;
}
private async _evaluateParams() {
this._performanceController.recordParamsStart();
await this.node.params.evalAll();
this._performanceController.recordParamsEnd();
}
//
//
// PERFORMANCE
//
//
cooksCount(): number {
return this._performanceController.cooksCount();
}
cookTime(): number {
return this._performanceController.data().cookTime;
}
private _finalizeCookPerformance() {
if (!this._corePerformance.started()) {
return;
}
this._performanceController.recordCookEnd();
this._corePerformance.recordNodeCookData(this.node, this._performanceController.data());
}
//
//
// HOOK
//
//
private _onCookCompleteHookNames: string[] | undefined;
private _onCookCompleteHooks: OnCookCompleteHook[] | undefined;
registerOnCookEnd(callbackName: string, callback: OnCookCompleteHook) {
this._onCookCompleteHookNames = this._onCookCompleteHookNames || [];
this._onCookCompleteHooks = this._onCookCompleteHooks || [];
this._onCookCompleteHookNames.push(callbackName);
this._onCookCompleteHooks.push(callback);
}
private _clearHooks() {
if (!this._onCookCompleteHookNames || !this._onCookCompleteHooks) {
return;
}
for (const hookName of this._onCookCompleteHookNames) {
this.deregisterOnCookEnd(hookName);
}
}
deregisterOnCookEnd(callbackName: string) {
if (!this._onCookCompleteHookNames || !this._onCookCompleteHooks) {
return;
}
const index = this._onCookCompleteHookNames?.indexOf(callbackName);
this._onCookCompleteHookNames.splice(index, 1);
this._onCookCompleteHooks.splice(index, 1);
if (this._onCookCompleteHookNames.length == 0) {
this._onCookCompleteHookNames = undefined;
}
if (this._onCookCompleteHooks.length == 0) {
this._onCookCompleteHooks = undefined;
}
}
private _runOnCookCompleteHooks() {
if (this._onCookCompleteHooks) {
const hooks = [...this._onCookCompleteHooks];
for (let hook of hooks) {
hook();
}
}
}
onCookEndCallbackNames() {
return this._onCookCompleteHookNames;
}
}