@aelionsdk/material-compiler 0.1.0-beta.1

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/dist/webgpu.js ADDED
@@ -0,0 +1,206 @@
1
+ import { buildMaterialExecutionPlan, compileMaterialGraph } from './compiler.js';
2
+ function identifier(value) {
3
+ return value.replaceAll(/[^a-zA-Z0-9_]/gu, '_');
4
+ }
5
+ function literal(value) {
6
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
7
+ throw new TypeError('WebGPU material expressions only support finite numeric literals');
8
+ }
9
+ return Number.isInteger(value) ? `${value.toString()}.0` : value.toString();
10
+ }
11
+ function graphHash(graph) {
12
+ const serialized = JSON.stringify(graph);
13
+ let hash = 2_166_136_261;
14
+ for (let index = 0; index < serialized.length; index += 1) {
15
+ hash ^= serialized.charCodeAt(index);
16
+ hash = Math.imul(hash, 16_777_619);
17
+ }
18
+ return (hash >>> 0).toString(16).padStart(8, '0');
19
+ }
20
+ function uniformExpression(binding, uniforms) {
21
+ let source;
22
+ if ('parameter' in binding)
23
+ source = { kind: 'parameter', id: binding.parameter };
24
+ if ('system' in binding)
25
+ source = { kind: 'system', id: binding.system };
26
+ if (source === undefined)
27
+ return undefined;
28
+ const name = `u_${source.kind}_${identifier(source.id)}`;
29
+ if (!uniforms.has(name)) {
30
+ uniforms.set(name, { name, type: 'float', source });
31
+ }
32
+ const index = [...uniforms.keys()].indexOf(name);
33
+ return `uniforms.values[${index.toString()}].x`;
34
+ }
35
+ function bindingExpression(binding, nodeExpressions, inputPorts, uniforms) {
36
+ if ('value' in binding)
37
+ return literal(binding.value);
38
+ const uniform = uniformExpression(binding, uniforms);
39
+ if (uniform !== undefined)
40
+ return uniform;
41
+ if ('inputPort' in binding) {
42
+ inputPorts.add(binding.inputPort);
43
+ return `textureSample(input_${identifier(binding.inputPort)}, source_sampler, vertex.uv)`;
44
+ }
45
+ if ('node' in binding) {
46
+ const expression = nodeExpressions.get(binding.node);
47
+ if (expression === undefined)
48
+ throw new TypeError(`Node ${binding.node} has no expression`);
49
+ return expression;
50
+ }
51
+ throw new TypeError('Resource bindings are not executable in the Phase 0 WebGPU backend');
52
+ }
53
+ function nodeExpression(type, input, binding, options) {
54
+ switch (type) {
55
+ case 'time.transition-curve': {
56
+ const curveBinding = binding('curve');
57
+ const curve = 'value' in curveBinding
58
+ ? curveBinding.value
59
+ : 'parameter' in curveBinding
60
+ ? options.specializationValues?.[curveBinding.parameter]
61
+ : undefined;
62
+ if (curve === 'linear')
63
+ return `clamp(${input('progress')}, 0.0, 1.0)`;
64
+ if (curve === 'smooth') {
65
+ return `smoothstep(0.0, 1.0, clamp(${input('progress')}, 0.0, 1.0))`;
66
+ }
67
+ throw new TypeError('time.transition-curve requires a linear or smooth specialization value');
68
+ }
69
+ case 'composite.mix':
70
+ return `mix(${input('a')}, ${input('b')}, clamp(${input('amount')}, 0.0, 1.0))`;
71
+ case 'color.temperature':
72
+ return `aelion_temperature(${input('source')}, ${input('amount')})`;
73
+ case 'color.lift-black':
74
+ return `aelion_lift_black(${input('source')}, ${input('amount')})`;
75
+ case 'color.scale-rgb':
76
+ return `aelion_scale_rgb(${input('source')}, ${input('scale')})`;
77
+ case 'composite.screen':
78
+ return `aelion_screen(${input('base')}, ${input('overlay')})`;
79
+ case 'math.add':
80
+ return `(${input('a')} + ${input('b')})`;
81
+ case 'math.subtract':
82
+ return `(${input('a')} - ${input('b')})`;
83
+ case 'math.multiply':
84
+ return `(${input('a')} * ${input('b')})`;
85
+ case 'math.divide':
86
+ return `(${input('a')} / ${input('b')})`;
87
+ case 'math.clamp':
88
+ return `clamp(${input('value')}, ${input('min')}, ${input('max')})`;
89
+ case 'math.smoothstep':
90
+ return `smoothstep(${input('edge0')}, ${input('edge1')}, ${input('x')})`;
91
+ case 'color.exposure':
92
+ return `aelion_scale_rgb(${input('source')}, exp2(${input('stops')}))`;
93
+ case 'color.contrast':
94
+ return `aelion_contrast(${input('source')}, ${input('amount')})`;
95
+ case 'color.saturation':
96
+ return `aelion_saturation(${input('source')}, ${input('amount')})`;
97
+ case 'color.invert':
98
+ return `aelion_invert(${input('source')})`;
99
+ case 'composite.multiply':
100
+ return `aelion_multiply(${input('base')}, ${input('overlay')})`;
101
+ case 'composite.add':
102
+ return `aelion_add(${input('base')}, ${input('overlay')})`;
103
+ default:
104
+ throw new TypeError(`Node ${type} is not supported by the Phase 0 WebGPU backend`);
105
+ }
106
+ }
107
+ const HELPERS = `
108
+ fn aelion_temperature(color: vec4f, amount: f32) -> vec4f {
109
+ let scale = vec3f(1.0 + amount, 1.0 + amount * 0.15, 1.0 - amount * 0.55);
110
+ return vec4f(max(color.rgb * scale, vec3f(0.0)), color.a);
111
+ }
112
+ fn aelion_lift_black(color: vec4f, amount: f32) -> vec4f {
113
+ return vec4f(mix(color.rgb, vec3f(1.0), clamp(amount, 0.0, 1.0)), color.a);
114
+ }
115
+ fn aelion_scale_rgb(color: vec4f, scale: f32) -> vec4f {
116
+ return vec4f(max(color.rgb * scale, vec3f(0.0)), color.a);
117
+ }
118
+ fn aelion_screen(base: vec4f, overlay: vec4f) -> vec4f {
119
+ return vec4f(1.0 - (1.0 - base.rgb) * (1.0 - overlay.rgb), base.a + overlay.a - base.a * overlay.a);
120
+ }
121
+ fn aelion_contrast(color: vec4f, amount: f32) -> vec4f {
122
+ return vec4f((color.rgb - vec3f(0.5)) * amount + vec3f(0.5), color.a);
123
+ }
124
+ fn aelion_saturation(color: vec4f, amount: f32) -> vec4f {
125
+ let luma = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722));
126
+ return vec4f(mix(vec3f(luma), color.rgb, amount), color.a);
127
+ }
128
+ fn aelion_invert(color: vec4f) -> vec4f { return vec4f(color.a - color.rgb, color.a); }
129
+ fn aelion_multiply(base: vec4f, overlay: vec4f) -> vec4f { return vec4f(base.rgb * overlay.rgb, base.a * overlay.a); }
130
+ fn aelion_add(base: vec4f, overlay: vec4f) -> vec4f { return min(base + overlay, vec4f(1.0)); }
131
+ `;
132
+ export function compileMaterialGraphToWebGpu(graph, options) {
133
+ const analysis = compileMaterialGraph(graph, options);
134
+ const errors = analysis.diagnostics.filter(value => value.severity === 'error');
135
+ if (errors.length > 0) {
136
+ throw new TypeError(`Material graph is not executable: ${errors.map(value => value.code).join(', ')}`);
137
+ }
138
+ if (graph.nodes.some(node => node.type === 'blur.gaussian')) {
139
+ throw new TypeError('MATERIAL_BACKEND_UNAVAILABLE: multi-pass blur requires WebGL2 in Phase 0');
140
+ }
141
+ const nodes = new Map(graph.nodes.map(node => [node.id, node]));
142
+ const expressions = new Map();
143
+ const inputPorts = new Set();
144
+ const uniforms = new Map();
145
+ for (const id of analysis.order) {
146
+ const node = nodes.get(id);
147
+ if (node === undefined)
148
+ continue;
149
+ const input = (name) => {
150
+ const value = node.inputs[name];
151
+ if (value === undefined)
152
+ throw new TypeError(`Node ${node.id} is missing ${name}`);
153
+ return bindingExpression(value, expressions, inputPorts, uniforms);
154
+ };
155
+ const binding = (name) => {
156
+ const value = node.inputs[name];
157
+ if (value === undefined)
158
+ throw new TypeError(`Node ${node.id} is missing ${name}`);
159
+ return value;
160
+ };
161
+ expressions.set(id, nodeExpression(node.type, input, binding, options));
162
+ }
163
+ const output = graph.outputs.result ?? Object.values(graph.outputs)[0];
164
+ if (output === undefined)
165
+ throw new TypeError('Material graph has no output');
166
+ const resultExpression = expressions.get(output.node);
167
+ if (resultExpression === undefined)
168
+ throw new TypeError('Material graph output is unavailable');
169
+ const ports = [...inputPorts];
170
+ const uniformValues = [...uniforms.values()];
171
+ const textureDeclarations = ports
172
+ .map((port, index) => `@group(0) @binding(${(index + 1).toString()}) var input_${identifier(port)}: texture_2d<f32>;`)
173
+ .join('\n');
174
+ const uniformBinding = ports.length + 1;
175
+ const uniformSlots = Math.max(1, uniformValues.length);
176
+ const hash = graphHash({
177
+ ...graph,
178
+ ...(options.specializationValues === undefined
179
+ ? {}
180
+ : { specializationValues: options.specializationValues }),
181
+ });
182
+ return {
183
+ backend: 'webgpu',
184
+ nodeSet: graph.nodeSet,
185
+ graphHash: hash,
186
+ inputPorts: ports,
187
+ uniforms: uniformValues,
188
+ executionPlan: buildMaterialExecutionPlan(graph, analysis),
189
+ shader: `
190
+ struct Uniforms { values: array<vec4f, ${uniformSlots.toString()}> };
191
+ @group(0) @binding(0) var source_sampler: sampler;
192
+ ${textureDeclarations}
193
+ @group(0) @binding(${uniformBinding.toString()}) var<uniform> uniforms: Uniforms;
194
+ struct VertexOut { @builtin(position) position: vec4f, @location(0) uv: vec2f };
195
+ @vertex fn vs(@builtin(vertex_index) index: u32) -> VertexOut {
196
+ var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
197
+ var uvs = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));
198
+ return VertexOut(vec4f(positions[index], 0.0, 1.0), uvs[index]);
199
+ }
200
+ ${HELPERS}
201
+ @fragment fn fs(vertex: VertexOut) -> @location(0) vec4f {
202
+ return ${resultExpression};
203
+ }
204
+ `,
205
+ };
206
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@aelionsdk/material-compiler",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Aelion Material Protocol graph compiler for WebGL2 and WebGPU",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/FoyonaCZY/AelionSDK.git",
9
+ "directory": "packages/material-compiler"
10
+ },
11
+ "keywords": [
12
+ "aelion",
13
+ "video",
14
+ "shader",
15
+ "effects"
16
+ ],
17
+ "sideEffects": false,
18
+ "type": "module",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "!dist/.tsbuildinfo"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20.19"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "provenance": true
35
+ },
36
+ "dependencies": {
37
+ "@aelionsdk/core": "0.1.0-beta.1"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -b",
41
+ "typecheck": "tsc -b --pretty false"
42
+ }
43
+ }