@clypra/runtime 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.releaserc.json +16 -0
- package/package.json +47 -0
- package/src/__tests__/integration.test.ts +345 -0
- package/src/graph/__tests__/builder.test.ts +204 -0
- package/src/graph/__tests__/validator.test.ts +381 -0
- package/src/graph/builder.ts +263 -0
- package/src/graph/index.ts +14 -0
- package/src/graph/types.ts +176 -0
- package/src/graph/validator.ts +208 -0
- package/src/index.ts +28 -0
- package/src/pixi/filters.ts +98 -0
- package/src/pixi/index.ts +11 -0
- package/src/pixi/renderer.ts +375 -0
- package/src/pixi/texture-pool.ts +159 -0
- package/src/pixi/types.ts +58 -0
- package/src/planner/index.ts +10 -0
- package/src/planner/optimizer.ts +247 -0
- package/src/planner/planner.ts +201 -0
- package/src/planner/types.ts +56 -0
- package/src/resources/cache.ts +166 -0
- package/src/resources/index.ts +9 -0
- package/src/resources/manager.ts +184 -0
- package/src/resources/types.ts +29 -0
- package/src/testing/benchmarkRunner.ts +399 -0
- package/src/testing/goldenTests.ts +390 -0
- package/src/validation/effectValidator.ts +571 -0
- package/src/validation/index.ts +9 -0
- package/src/validation/resource-validator.ts +173 -0
- package/src/validation/shader-validator.ts +154 -0
- package/src/validation/types.ts +31 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +18 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates effect definitions before publishing to ensure quality and correctness.
|
|
5
|
+
* Checks shader compilation, parameter schemas, graph structure, and metadata completeness.
|
|
6
|
+
*
|
|
7
|
+
* Phase 6 Week 10 - Publishing Pipeline #1
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
valid: boolean;
|
|
12
|
+
errors: ValidationError[];
|
|
13
|
+
warnings: ValidationWarning[];
|
|
14
|
+
metadata: {
|
|
15
|
+
effectId: string;
|
|
16
|
+
effectName: string;
|
|
17
|
+
validatedAt: string;
|
|
18
|
+
validator: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ValidationError {
|
|
23
|
+
type: "shader" | "schema" | "graph" | "metadata" | "performance";
|
|
24
|
+
severity: "error";
|
|
25
|
+
message: string;
|
|
26
|
+
location?: string;
|
|
27
|
+
suggestion?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ValidationWarning {
|
|
31
|
+
type: "shader" | "schema" | "graph" | "metadata" | "performance";
|
|
32
|
+
severity: "warning";
|
|
33
|
+
message: string;
|
|
34
|
+
location?: string;
|
|
35
|
+
suggestion?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface EffectDefinition {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
version: string;
|
|
42
|
+
category: string;
|
|
43
|
+
description: string;
|
|
44
|
+
schema: {
|
|
45
|
+
parameters: Record<string, any>;
|
|
46
|
+
inputs: Record<string, any>;
|
|
47
|
+
outputs: Record<string, any>;
|
|
48
|
+
};
|
|
49
|
+
nodes: any[];
|
|
50
|
+
edges: any[];
|
|
51
|
+
metadata: {
|
|
52
|
+
author?: string;
|
|
53
|
+
tags?: string[];
|
|
54
|
+
thumbnail?: string;
|
|
55
|
+
previewVideo?: string;
|
|
56
|
+
requiredFeatures?: string[];
|
|
57
|
+
};
|
|
58
|
+
capabilities: {
|
|
59
|
+
temporal: boolean;
|
|
60
|
+
stateful: boolean;
|
|
61
|
+
spatial: boolean;
|
|
62
|
+
geometry: boolean;
|
|
63
|
+
inputsCount: number;
|
|
64
|
+
};
|
|
65
|
+
requirements: {
|
|
66
|
+
temporalRadius: number;
|
|
67
|
+
preferredPrecision: string;
|
|
68
|
+
multipass: boolean;
|
|
69
|
+
supportsHalfResolution: boolean;
|
|
70
|
+
};
|
|
71
|
+
presets?: any[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Effect Validator
|
|
76
|
+
*/
|
|
77
|
+
export class EffectValidator {
|
|
78
|
+
private errors: ValidationError[] = [];
|
|
79
|
+
private warnings: ValidationWarning[] = [];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate an effect definition
|
|
83
|
+
*/
|
|
84
|
+
validate(effect: EffectDefinition): ValidationResult {
|
|
85
|
+
this.errors = [];
|
|
86
|
+
this.warnings = [];
|
|
87
|
+
|
|
88
|
+
// Run validation checks
|
|
89
|
+
this.validateMetadata(effect);
|
|
90
|
+
this.validateSchema(effect);
|
|
91
|
+
this.validateGraph(effect);
|
|
92
|
+
this.validateShaders(effect);
|
|
93
|
+
this.validatePresets(effect);
|
|
94
|
+
this.validatePerformance(effect);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
valid: this.errors.length === 0,
|
|
98
|
+
errors: this.errors,
|
|
99
|
+
warnings: this.warnings,
|
|
100
|
+
metadata: {
|
|
101
|
+
effectId: effect.id,
|
|
102
|
+
effectName: effect.name,
|
|
103
|
+
validatedAt: new Date().toISOString(),
|
|
104
|
+
validator: "EffectValidator v1.0.0",
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validate metadata completeness
|
|
111
|
+
*/
|
|
112
|
+
private validateMetadata(effect: EffectDefinition): void {
|
|
113
|
+
// Required fields
|
|
114
|
+
if (!effect.id) {
|
|
115
|
+
this.errors.push({
|
|
116
|
+
type: "metadata",
|
|
117
|
+
severity: "error",
|
|
118
|
+
message: "Effect ID is required",
|
|
119
|
+
suggestion: "Add a unique effect ID like 'video.my-effect'",
|
|
120
|
+
});
|
|
121
|
+
} else if (!effect.id.includes(".")) {
|
|
122
|
+
this.warnings.push({
|
|
123
|
+
type: "metadata",
|
|
124
|
+
severity: "warning",
|
|
125
|
+
message: "Effect ID should follow category.name pattern",
|
|
126
|
+
location: `id: "${effect.id}"`,
|
|
127
|
+
suggestion: "Use format like 'video.my-effect' or 'body.my-effect'",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!effect.name) {
|
|
132
|
+
this.errors.push({
|
|
133
|
+
type: "metadata",
|
|
134
|
+
severity: "error",
|
|
135
|
+
message: "Effect name is required",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!effect.version || !effect.version.match(/^\d+\.\d+\.\d+$/)) {
|
|
140
|
+
this.errors.push({
|
|
141
|
+
type: "metadata",
|
|
142
|
+
severity: "error",
|
|
143
|
+
message: "Valid semantic version is required (e.g., 1.0.0)",
|
|
144
|
+
location: `version: "${effect.version}"`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!effect.description) {
|
|
149
|
+
this.warnings.push({
|
|
150
|
+
type: "metadata",
|
|
151
|
+
severity: "warning",
|
|
152
|
+
message: "Effect description is recommended",
|
|
153
|
+
suggestion: "Add a brief description of what the effect does",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!effect.category) {
|
|
158
|
+
this.errors.push({
|
|
159
|
+
type: "metadata",
|
|
160
|
+
severity: "error",
|
|
161
|
+
message: "Effect category is required",
|
|
162
|
+
suggestion: "Use 'video', 'transition', 'body', or other category",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Metadata fields
|
|
167
|
+
if (!effect.metadata) {
|
|
168
|
+
this.warnings.push({
|
|
169
|
+
type: "metadata",
|
|
170
|
+
severity: "warning",
|
|
171
|
+
message: "Metadata object is missing",
|
|
172
|
+
suggestion: "Add metadata with author, tags, etc.",
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
if (!effect.metadata.author) {
|
|
176
|
+
this.warnings.push({
|
|
177
|
+
type: "metadata",
|
|
178
|
+
severity: "warning",
|
|
179
|
+
message: "Author field is recommended",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!effect.metadata.tags || effect.metadata.tags.length === 0) {
|
|
184
|
+
this.warnings.push({
|
|
185
|
+
type: "metadata",
|
|
186
|
+
severity: "warning",
|
|
187
|
+
message: "Tags are recommended for discoverability",
|
|
188
|
+
suggestion: "Add relevant tags like ['blur', 'artistic', 'mask']",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!effect.metadata.thumbnail) {
|
|
193
|
+
this.warnings.push({
|
|
194
|
+
type: "metadata",
|
|
195
|
+
severity: "warning",
|
|
196
|
+
message: "Thumbnail is recommended for preview",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validate parameter schema
|
|
204
|
+
*/
|
|
205
|
+
private validateSchema(effect: EffectDefinition): void {
|
|
206
|
+
if (!effect.schema) {
|
|
207
|
+
this.errors.push({
|
|
208
|
+
type: "schema",
|
|
209
|
+
severity: "error",
|
|
210
|
+
message: "Schema is required",
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Validate parameters
|
|
216
|
+
if (!effect.schema.parameters) {
|
|
217
|
+
this.warnings.push({
|
|
218
|
+
type: "schema",
|
|
219
|
+
severity: "warning",
|
|
220
|
+
message: "No parameters defined",
|
|
221
|
+
suggestion: "Effects typically have at least one adjustable parameter",
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
for (const [key, param] of Object.entries(effect.schema.parameters)) {
|
|
225
|
+
this.validateParameter(key, param);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Validate inputs
|
|
230
|
+
if (!effect.schema.inputs) {
|
|
231
|
+
this.errors.push({
|
|
232
|
+
type: "schema",
|
|
233
|
+
severity: "error",
|
|
234
|
+
message: "Inputs schema is required",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Validate outputs
|
|
239
|
+
if (!effect.schema.outputs) {
|
|
240
|
+
this.errors.push({
|
|
241
|
+
type: "schema",
|
|
242
|
+
severity: "error",
|
|
243
|
+
message: "Outputs schema is required",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Validate a single parameter
|
|
250
|
+
*/
|
|
251
|
+
private validateParameter(key: string, param: any): void {
|
|
252
|
+
if (!param.type) {
|
|
253
|
+
this.errors.push({
|
|
254
|
+
type: "schema",
|
|
255
|
+
severity: "error",
|
|
256
|
+
message: `Parameter "${key}" missing type`,
|
|
257
|
+
location: `parameters.${key}`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (param.type === "number") {
|
|
262
|
+
if (param.min === undefined || param.max === undefined) {
|
|
263
|
+
this.warnings.push({
|
|
264
|
+
type: "schema",
|
|
265
|
+
severity: "warning",
|
|
266
|
+
message: `Number parameter "${key}" should have min and max`,
|
|
267
|
+
location: `parameters.${key}`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (param.default === undefined) {
|
|
272
|
+
this.warnings.push({
|
|
273
|
+
type: "schema",
|
|
274
|
+
severity: "warning",
|
|
275
|
+
message: `Parameter "${key}" should have a default value`,
|
|
276
|
+
location: `parameters.${key}`,
|
|
277
|
+
});
|
|
278
|
+
} else if (param.min !== undefined && param.max !== undefined) {
|
|
279
|
+
if (param.default < param.min || param.default > param.max) {
|
|
280
|
+
this.errors.push({
|
|
281
|
+
type: "schema",
|
|
282
|
+
severity: "error",
|
|
283
|
+
message: `Parameter "${key}" default value out of range`,
|
|
284
|
+
location: `parameters.${key}.default`,
|
|
285
|
+
suggestion: `Default should be between ${param.min} and ${param.max}`,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!param.label) {
|
|
292
|
+
this.warnings.push({
|
|
293
|
+
type: "schema",
|
|
294
|
+
severity: "warning",
|
|
295
|
+
message: `Parameter "${key}" missing label`,
|
|
296
|
+
location: `parameters.${key}`,
|
|
297
|
+
suggestion: "Add a human-readable label",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!param.description) {
|
|
302
|
+
this.warnings.push({
|
|
303
|
+
type: "schema",
|
|
304
|
+
severity: "warning",
|
|
305
|
+
message: `Parameter "${key}" missing description`,
|
|
306
|
+
location: `parameters.${key}`,
|
|
307
|
+
suggestion: "Add a description of what this parameter does",
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Validate graph structure
|
|
314
|
+
*/
|
|
315
|
+
private validateGraph(effect: EffectDefinition): void {
|
|
316
|
+
if (!effect.nodes || effect.nodes.length === 0) {
|
|
317
|
+
this.errors.push({
|
|
318
|
+
type: "graph",
|
|
319
|
+
severity: "error",
|
|
320
|
+
message: "Effect must have at least one node",
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!effect.edges || effect.edges.length === 0) {
|
|
326
|
+
this.warnings.push({
|
|
327
|
+
type: "graph",
|
|
328
|
+
severity: "warning",
|
|
329
|
+
message: "Effect has no edges (disconnected nodes)",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Find input and output nodes
|
|
334
|
+
const hasInput = effect.nodes.some((n) => n.type === "Input");
|
|
335
|
+
const hasOutput = effect.nodes.some((n) => n.type === "Output");
|
|
336
|
+
|
|
337
|
+
if (!hasInput) {
|
|
338
|
+
this.errors.push({
|
|
339
|
+
type: "graph",
|
|
340
|
+
severity: "error",
|
|
341
|
+
message: "Graph must have an Input node",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!hasOutput) {
|
|
346
|
+
this.errors.push({
|
|
347
|
+
type: "graph",
|
|
348
|
+
severity: "error",
|
|
349
|
+
message: "Graph must have an Output node",
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Check for duplicate node IDs
|
|
354
|
+
const nodeIds = new Set<string>();
|
|
355
|
+
for (const node of effect.nodes) {
|
|
356
|
+
if (!node.id) {
|
|
357
|
+
this.errors.push({
|
|
358
|
+
type: "graph",
|
|
359
|
+
severity: "error",
|
|
360
|
+
message: "Node missing ID",
|
|
361
|
+
location: `nodes[${effect.nodes.indexOf(node)}]`,
|
|
362
|
+
});
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (nodeIds.has(node.id)) {
|
|
367
|
+
this.errors.push({
|
|
368
|
+
type: "graph",
|
|
369
|
+
severity: "error",
|
|
370
|
+
message: `Duplicate node ID: "${node.id}"`,
|
|
371
|
+
location: `nodes`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
nodeIds.add(node.id);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Validate edges reference valid nodes
|
|
378
|
+
for (const edge of effect.edges) {
|
|
379
|
+
if (!nodeIds.has(edge.from)) {
|
|
380
|
+
this.errors.push({
|
|
381
|
+
type: "graph",
|
|
382
|
+
severity: "error",
|
|
383
|
+
message: `Edge references non-existent node: "${edge.from}"`,
|
|
384
|
+
location: `edges`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!nodeIds.has(edge.to)) {
|
|
389
|
+
this.errors.push({
|
|
390
|
+
type: "graph",
|
|
391
|
+
severity: "error",
|
|
392
|
+
message: `Edge references non-existent node: "${edge.to}"`,
|
|
393
|
+
location: `edges`,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Validate shader code
|
|
401
|
+
*/
|
|
402
|
+
private validateShaders(effect: EffectDefinition): void {
|
|
403
|
+
const shaderNodes = effect.nodes.filter((n) => n.type === "ShaderNode");
|
|
404
|
+
|
|
405
|
+
if (shaderNodes.length === 0) {
|
|
406
|
+
this.warnings.push({
|
|
407
|
+
type: "shader",
|
|
408
|
+
severity: "warning",
|
|
409
|
+
message: "Effect has no shader nodes",
|
|
410
|
+
suggestion: "Most effects require at least one shader",
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (const node of shaderNodes) {
|
|
416
|
+
if (!node.params || !node.params.shader) {
|
|
417
|
+
this.errors.push({
|
|
418
|
+
type: "shader",
|
|
419
|
+
severity: "error",
|
|
420
|
+
message: `ShaderNode "${node.id}" missing shader code`,
|
|
421
|
+
location: `nodes.${node.id}`,
|
|
422
|
+
});
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const shader = node.params.shader;
|
|
427
|
+
|
|
428
|
+
// Basic GLSL validation
|
|
429
|
+
if (!shader.includes("void main()")) {
|
|
430
|
+
this.errors.push({
|
|
431
|
+
type: "shader",
|
|
432
|
+
severity: "error",
|
|
433
|
+
message: `ShaderNode "${node.id}" shader missing main() function`,
|
|
434
|
+
location: `nodes.${node.id}.params.shader`,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!shader.includes("gl_FragColor")) {
|
|
439
|
+
this.warnings.push({
|
|
440
|
+
type: "shader",
|
|
441
|
+
severity: "warning",
|
|
442
|
+
message: `ShaderNode "${node.id}" shader doesn't set gl_FragColor`,
|
|
443
|
+
location: `nodes.${node.id}.params.shader`,
|
|
444
|
+
suggestion: "Fragment shaders should set gl_FragColor",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!shader.includes("precision")) {
|
|
449
|
+
this.warnings.push({
|
|
450
|
+
type: "shader",
|
|
451
|
+
severity: "warning",
|
|
452
|
+
message: `ShaderNode "${node.id}" shader missing precision qualifier`,
|
|
453
|
+
location: `nodes.${node.id}.params.shader`,
|
|
454
|
+
suggestion: "Add 'precision highp float;' or 'precision mediump float;'",
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Check for common mistakes
|
|
459
|
+
if (shader.includes("texture2D") && !shader.includes("sampler2D")) {
|
|
460
|
+
this.warnings.push({
|
|
461
|
+
type: "shader",
|
|
462
|
+
severity: "warning",
|
|
463
|
+
message: `ShaderNode "${node.id}" uses texture2D but no sampler2D uniforms`,
|
|
464
|
+
location: `nodes.${node.id}.params.shader`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Validate presets
|
|
472
|
+
*/
|
|
473
|
+
private validatePresets(effect: EffectDefinition): void {
|
|
474
|
+
if (!effect.presets || effect.presets.length === 0) {
|
|
475
|
+
this.warnings.push({
|
|
476
|
+
type: "metadata",
|
|
477
|
+
severity: "warning",
|
|
478
|
+
message: "Effect has no presets",
|
|
479
|
+
suggestion: "Add at least 2-3 presets to demonstrate effect range",
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (effect.presets.length < 2) {
|
|
485
|
+
this.warnings.push({
|
|
486
|
+
type: "metadata",
|
|
487
|
+
severity: "warning",
|
|
488
|
+
message: "Effect has only one preset",
|
|
489
|
+
suggestion: "Add more presets to show different use cases",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
for (const preset of effect.presets) {
|
|
494
|
+
if (!preset.id) {
|
|
495
|
+
this.errors.push({
|
|
496
|
+
type: "metadata",
|
|
497
|
+
severity: "error",
|
|
498
|
+
message: "Preset missing ID",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!preset.name) {
|
|
503
|
+
this.errors.push({
|
|
504
|
+
type: "metadata",
|
|
505
|
+
severity: "error",
|
|
506
|
+
message: "Preset missing name",
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!preset.parameters) {
|
|
511
|
+
this.errors.push({
|
|
512
|
+
type: "metadata",
|
|
513
|
+
severity: "error",
|
|
514
|
+
message: `Preset "${preset.id}" missing parameters`,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Validate performance characteristics
|
|
522
|
+
*/
|
|
523
|
+
private validatePerformance(effect: EffectDefinition): void {
|
|
524
|
+
if (!effect.requirements) {
|
|
525
|
+
this.warnings.push({
|
|
526
|
+
type: "performance",
|
|
527
|
+
severity: "warning",
|
|
528
|
+
message: "Performance requirements not specified",
|
|
529
|
+
});
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const shaderNodeCount = effect.nodes.filter((n) => n.type === "ShaderNode").length;
|
|
534
|
+
|
|
535
|
+
if (shaderNodeCount > 10) {
|
|
536
|
+
this.warnings.push({
|
|
537
|
+
type: "performance",
|
|
538
|
+
severity: "warning",
|
|
539
|
+
message: `Effect has ${shaderNodeCount} shader passes`,
|
|
540
|
+
suggestion: "Consider optimizing to reduce pass count",
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (effect.requirements.multipass && shaderNodeCount === 1) {
|
|
545
|
+
this.warnings.push({
|
|
546
|
+
type: "performance",
|
|
547
|
+
severity: "warning",
|
|
548
|
+
message: "Effect marked as multipass but has only one shader node",
|
|
549
|
+
location: "requirements.multipass",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!effect.requirements.multipass && shaderNodeCount > 1) {
|
|
554
|
+
this.warnings.push({
|
|
555
|
+
type: "performance",
|
|
556
|
+
severity: "warning",
|
|
557
|
+
message: "Effect has multiple passes but not marked as multipass",
|
|
558
|
+
location: "requirements.multipass",
|
|
559
|
+
suggestion: "Set multipass: true",
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Convenience function to validate an effect
|
|
567
|
+
*/
|
|
568
|
+
export function validateEffect(effect: EffectDefinition): ValidationResult {
|
|
569
|
+
const validator = new EffectValidator();
|
|
570
|
+
return validator.validate(effect);
|
|
571
|
+
}
|