Skip to content

Commit e8068b7

Browse files
feat(vinculum): build CLI pipeline and fix cycle detection logic
1 parent a2705af commit e8068b7

5 files changed

Lines changed: 115 additions & 12 deletions

File tree

docs/vinculum-spec.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ modules:
2525
shader: "src/compute/diffusion.wgsl"
2626
reads: [rock, soil, sand, water, ice, organic]
2727
writes: [soil, sand]
28+
requires: [advection]
2829
flux_producer: false
2930

3031
semi_implicit_conservation:
3132
shader: "src/compute/semi_implicit_conservation.wgsl"
3233
reads: [water, sand] # minimal: only channels that were written
3334
writes: [water, sand, soil, rock, ice, organic] # renormalization touches all
34-
requires: [advection] # must run after any flux_producer
35+
requires: [advection, diffusion] # must run after any flux_producer and modifiers
3536
injection_rule: "auto-on-write" # compiler injects when water/sand are mutated
3637

3738
qef_extract:

scripts/manifold-build.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as fs from 'fs';
2+
import * as yaml from 'js-yaml';
3+
import * as path from 'path';
4+
import { VinculumCompiler } from '../src/compiler/vinculum-compiler';
5+
6+
const specPath = path.resolve(process.cwd(), 'docs/vinculum-spec.md');
7+
const outPath = path.resolve(process.cwd(), 'src/dispatchSequence.ts');
8+
9+
try {
10+
const fileContents = fs.readFileSync(specPath, 'utf8');
11+
// Extract yaml from markdown code blocks or just parse directly if it's pure yaml.
12+
// Our spec has some markdown. Let's try to parse the whole thing, or strip markdown.
13+
const yamlMatch = fileContents.match(/```yaml\n([\s\S]*?)\n```/);
14+
const yamlString = yamlMatch ? yamlMatch[1] : fileContents;
15+
16+
const spec = yaml.load(yamlString) as any;
17+
18+
console.log("Compiling Vinculum Dependency Graph...");
19+
const compiler = new VinculumCompiler(spec);
20+
const code = compiler.generateDispatchCode();
21+
22+
fs.writeFileSync(outPath, code, 'utf8');
23+
console.log(`Successfully generated ${outPath}`);
24+
} catch (e) {
25+
console.error("Vinculum Compilation Failed:", e);
26+
process.exit(1);
27+
}

src/compiler/vinculum-compiler.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface VinculumModule {
1111

1212
interface VinculumSpec {
1313
version: string;
14+
engine: string;
1415
channels: string[];
1516
modules: Record<string, VinculumModule>;
1617
compiler: {
@@ -35,9 +36,10 @@ export class VinculumCompiler {
3536
this.dag.set(name, new Set(mod.requires || []));
3637
// Auto-inject conservation if flux_producer writes water/sand
3738
if (mod.flux_producer && (mod.writes.includes('water') || mod.writes.includes('sand'))) {
38-
if (!mod.requires?.includes('semi_implicit_conservation')) {
39-
mod.requires = [...(mod.requires || []), 'semi_implicit_conservation'];
40-
this.dag.set(name, new Set(mod.requires));
39+
const consMod = this.spec.modules['semi_implicit_conservation'];
40+
if (consMod && !consMod.requires?.includes(name)) {
41+
consMod.requires = [...(consMod.requires || []), name];
42+
this.dag.set('semi_implicit_conservation', new Set(consMod.requires));
4143
}
4244
}
4345
}
@@ -93,18 +95,36 @@ export class VinculumCompiler {
9395

9496
// Generate WebGPU dispatch sequence
9597
let code = `// Auto-generated by Vinculum Compiler v${this.spec.version}\n`;
96-
code += `const dispatchSequence = [\n`;
98+
code += `// Pipeline: ${this.spec.engine}\n\n`;
99+
code += `export const dispatchSequence = [\n`;
100+
101+
let currentState = 'state_A';
97102

98103
for (const modName of order) {
99104
const mod = this.spec.modules[modName];
100-
code += ` { name: '${modName}', shader: '${mod.shader}', binds: this.setupBindGroup('${modName}') },\n`;
105+
const mutatesState = mod.writes && mod.writes.length > 0;
106+
107+
let inputBuffer = currentState;
108+
let outputBuffer = currentState;
109+
110+
if (mutatesState) {
111+
outputBuffer = currentState === 'state_A' ? 'state_B' : 'state_A';
112+
currentState = outputBuffer; // state flips for next read
113+
}
114+
115+
code += ` {\n`;
116+
code += ` name: '${modName}',\n`;
117+
code += ` shader: '${mod.shader}',\n`;
118+
code += ` bufferRoles: { input: '${inputBuffer}', output: '${outputBuffer}' },\n`;
119+
code += ` binds: () => setupBindGroup('${modName}')\n`;
120+
code += ` },\n`;
101121
}
102-
code += `];\n`;
122+
code += `];\n\n`;
103123

104124
// Inject adaptive substep hint if high-flux modules present
105-
if (this.spec.compiler.injection.substep_hint === 'high-flux') {
125+
if (this.spec.compiler?.injection?.substep_hint === 'high-flux') {
106126
code += `// Hint: enable adaptive substepping for high-flux modules\n`;
107-
code += `simulationParams.substepThreshold = 0.05;\n`;
127+
code += `export const recommendedSubstepThreshold = 0.05;\n`;
108128
}
109129

110130
return code;
@@ -116,24 +136,35 @@ export class VinculumCompiler {
116136
const result: string[] = [];
117137
const queue: string[] = [];
118138

139+
console.log("DAG contents:");
140+
for (const [mod, deps] of this.dag.entries()) {
141+
console.log(`${mod} requires:`, Array.from(deps));
142+
}
143+
119144
// Initialize in-degrees
120145
for (const mod of Object.keys(this.spec.modules)) {
121146
inDegree.set(mod, 0);
122147
}
123-
for (const deps of this.dag.values()) {
148+
// If mod requires dep, edge is dep -> mod.
149+
// So mod's inDegree increases.
150+
for (const [mod, deps] of this.dag) {
124151
for (const dep of deps) {
125-
inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
152+
// If a required dependency isn't in modules, skip or initialize
153+
if (inDegree.has(dep)) {
154+
inDegree.set(mod, (inDegree.get(mod) || 0) + 1);
155+
}
126156
}
127157
}
128158

129-
// Start with nodes that have no dependencies
159+
// Start with nodes that have no dependencies (inDegree = 0)
130160
for (const [mod, deg] of inDegree) {
131161
if (deg === 0) queue.push(mod);
132162
}
133163

134164
while (queue.length) {
135165
const curr = queue.shift()!;
136166
result.push(curr);
167+
// curr just executed. Find any mod that requires curr, and decrement its inDegree.
137168
for (const [mod, deps] of this.dag) {
138169
if (deps.has(curr)) {
139170
inDegree.set(mod, inDegree.get(mod)! - 1);

src/dispatchSequence.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Auto-generated by Vinculum Compiler v0.1.0
2+
// Pipeline: hyperpoly-terrain
3+
4+
export const dispatchSequence = [
5+
{
6+
name: 'advection',
7+
shader: 'src/compute/advection.wgsl',
8+
bufferRoles: { input: 'state_A', output: 'state_B' },
9+
binds: () => setupBindGroup('advection')
10+
},
11+
{
12+
name: 'qef_extract',
13+
shader: 'src/compute/qef_solve.wgsl',
14+
bufferRoles: { input: 'state_B', output: 'state_B' },
15+
binds: () => setupBindGroup('qef_extract')
16+
},
17+
{
18+
name: 'diffusion',
19+
shader: 'src/compute/diffusion.wgsl',
20+
bufferRoles: { input: 'state_B', output: 'state_A' },
21+
binds: () => setupBindGroup('diffusion')
22+
},
23+
{
24+
name: 'semi_implicit_conservation',
25+
shader: 'src/compute/semi_implicit_conservation.wgsl',
26+
bufferRoles: { input: 'state_A', output: 'state_B' },
27+
binds: () => setupBindGroup('semi_implicit_conservation')
28+
},
29+
];
30+
31+
// Hint: enable adaptive substepping for high-flux modules
32+
export const recommendedSubstepThreshold = 0.05;

tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2022",
4+
"module": "commonjs",
5+
"rootDir": "./",
6+
"outDir": "./dist",
7+
"esModuleInterop": true,
8+
"forceConsistentCasingInFileNames": true,
9+
"strict": false,
10+
"skipLibCheck": true
11+
}
12+
}

0 commit comments

Comments
 (0)