milkdrop-toolchain MilkDrop preset compiler, analyzers and exporters

The part of a MilkDrop engine that never touches a GPU.

A TypeScript toolchain for .milk presets: a lossless parser, an EEL2 interpreter and JIT, an intermediate representation, HLSL analysis of the warp and composite shaders with GLSL emission, EEL-to-WGSL compute lowering, a formatter, exact MilkDrop 2 export, and dataflow analysis that reads which audio reaches which control. It keeps presets as .milk files: exportMilkdrop2Preset writes the file format MilkDrop 2 saves, which MilkDrop and projectM open directly and Butterchurn reads only after conversion to its JSON preset format. Born in Stims, where the same code compiles every preset the visualizer plays.

npm install milkdrop-toolchain Try it in the browser Read the README

Pipeline

Preset text goes through a lossless line tree, a field parser and the IR builder, which also parses every equation and classifies the shader text. From the IR, a program block runs on the tree-walking interpreter or the JavaScript JIT, or is lowered to WGSL by the generator; all three read one function table. The formatter and exporter turn the IR back into text.

The compile pipeline: .milk text to syntax tree to fields to IR, then to the interpreter, JIT, WGSL, text and dataflow outputs; shader text to HLSL parse to GLSL and into the IR. .milk text preset file parsePresetSyntax lossless line tree parseMilkdropPreset fields + diagnostics MilkdropPresetIR createIR evaluateMilkdropExpression EEL2 interpreter compileMilkdropProgram EEL2 to JavaScript (JIT) compileProgramToWgsl EEL2 to WGSL compute format / exportMilkdrop2 .milk text back out analyzePresetDataflow which audio reaches what warp / comp shader HLSL text parseMilkdropShaderStatement shader AST + extractShaderControls normalizeHlslToGlsl GLSL ES for WebGL shader controls + classification
Hand-drawn from the module graph in src/. Gold outlines are outputs; the IR is the one shared reading of what a preset means.

Playground

The library, bundled from the same dist/ the package ships, running in your tab. Edit the preset; everything below recompiles as you type.

Loading the library…

Diagnostics compiled.diagnostics

Waiting for the first compile.

IR summary compiled.ir

Waiting for the first compile.

Dataflow analyzePresetDataflow

Waiting for the first compile.

Per-frame block as WGSL compileProgramToWgsl

Waiting for the first compile.

Text back out

Waiting for the first compile.

Expression scratchpad parseMilkdropExpression + evaluateMilkdropExpression

…

Every other name reads as 0. 3 / 0 contributes 0, not Infinity; that is EEL2, not a bug.

API at a glance

Everything is exported from the package root; every module under src/ is also importable as milkdrop-toolchain/src/<file>.ts. The main entries, grouped as in the README.

Preset text

ExportPurpose
parsePresetSyntax(source) / printPresetSyntax(tree)Lossless line-level syntax tree of a .milk file; printing gives back the exact bytes.
fieldAssignments(tree), isShaderSection(name), splitInlineComment(line)Helpers over the syntax tree.
parseMilkdropPreset(source)Fields and sections of a preset, with parse diagnostics.

Compiler

ExportPurpose
compileMilkdropPresetSource(raw, source?, options?)Parse, build the IR, format, and cache; returns a MilkdropCompiledPreset.
createIR(ast, diagnostics, source?, options?), createMilkdropIr(...)The IR builder without the cache or formatter.
buildParityReport(ir, options)Per-backend parity report: what runs as written, what is approximated, what is blocked.
classifyFidelity, buildBackendDivergence, buildVisualFallbacks, buildDegradationReasonsThe pieces of the compatibility classification.
DEFAULT_MILKDROP_STATE, createDefaultCustomWaveSlot(), createDefaultShapeSlot()MilkDrop's default per-frame state and slot templates.
clearCompiledPresetCache(), warmupCompiledPresetCache(presets)Control the compile cache.

EEL2 expressions

ExportPurpose
parseMilkdropExpression(source, line)Parse one expression to an AST, with diagnostics.
parseMilkdropStatement(source, line), splitMilkdropStatements(source)Parse target = expr; statements, split a block on ;.
evaluateMilkdropExpression(node, env, helpers?)Tree-walking interpreter.
compileMilkdropProgram(block)JIT a program block to a JavaScript function; falls back to the interpreter when new Function is blocked by CSP.
compileProgramToWgsl(block, options?), buildWgslExpressionString(node, ...)Lower a program block, or one expression, to WGSL.
MILKDROP_WGSL_SIGNAL_FIELDS, MILKDROP_WGSL_SIGNAL_ALIAS_MAPThe VmSignals uniform layout and its aliases.
EEL_FUNCTIONS, EEL_BINARY_OPERATORS, EEL_UNARY_OPERATORS, getEelFunctionSpec(name)The one function table the interpreter, JIT and WGSL generator all read.
MILKDROP_FIELDS, buildFieldAliasMap(), MILKDROP2_FIELD_PAIRSThe field table: every known preset field, its type, default and spellings.
MILKDROP_BUILTIN_DOCS, MILKDROP_INTRINSIC_FUNCTION_NAMES, findNearestMatch(name, candidates)Documentation of every builtin, and spelling suggestions, for editors and linters.

Shader text

ExportPurpose
extractShaderSource(fields, stage), ensureShaderBody(text)Recover the warp or comp shader text from the preset's backtick lines.
parseMilkdropShaderStatement(line), evaluateMilkdropShaderExpression(node, env)Parse one HLSL statement to a shader AST; constant-evaluate a shader expression.
extractShaderControls(...), evaluateMilkdropShaderControlProgram(...)Read, and evaluate per frame, the colour, texture and warp controls a shader body sets.
normalizeHlslToGlsl(body, ...), generateGlslFromShaderStatements(statements, stage)Translate an HLSL shader body to GLSL ES, or rebuild GLSL from parsed statements.
describeShaderTranslations(compiled), resolveShaderExecutionMode(compiled, backend)Per-stage summary: source, GLSL, which path produced it, and whether each backend runs it as written.
MILKDROP_SHADER_TEXTURE_SAMPLERS, MILKDROP_TEXTURE_FILESThe sampler name tables and the texture files MilkDrop ships.

Formatting and export

ExportPurpose
formatMilkdropPreset(compiled)Serialize a compiled preset back to .milk text in the editor dialect.
exportMilkdrop2Preset(compiled)Write the MilkDrop 2 file format: MILKDROP_PRESET_VERSION, MilkDrop's key names, backtick shader lines.
readMilkdropField(source, key), upsertMilkdropField(source, key, value), upsertMilkdropFields(source, entries)Read or set fields in preset text without disturbing the rest.
isFieldShadowedByEquations(...), getFieldOverwriteKind(...)Which fields an equation overwrites each frame.
lineageFromFields(fields), lineageFieldLines(refs)Remix lineage metadata stored in the preset.

Static analysis

ExportPurpose
analyzePresetDataflow(ir)Which audio signals reach which control or drawn part, read from the equations.
controlAudio, drawnPartAudio, frameValueName, dataflowSignatureQuery and fingerprint a dataflow result.
analyzePresetMath(source)Motion, reactivity and complexity summary of a preset's equations.

The MilkdropCompiledPreset, MilkdropPresetIR, MilkdropProgramBlock, MilkdropExpressionNode, MilkdropDiagnostic, MilkdropShaderStatement and MilkdropRuntimeSignals types are exported from the root.

What is not here

The package stops where the GPU starts. Four things a complete engine needs that live in the host instead.

The renderer

Drawing waves, shapes, the warp mesh and the composite pass, and feeding audio into the signal environment, is the host's job. Stims does it with three.js on WebGL and WebGPU.

The CPU VM

compileMilkdropProgram gives you a function per program block; the loop that runs init, per-frame, per-pixel and the custom wave and shape programs in order, with the right register bank, and blends between presets, lives in Stims (vm.ts).

The GPU compute VM

Dispatching the WGSL this package generates and reading the state buffer back.

HLSL to WGSL

In Stims the shader text becomes WGSL through three.js TSL nodes, which this package does not depend on. The HLSL side here stops at the GLSL translation and the control analysis.

Semantics

The EEL2 semantics implemented by the interpreter, the JIT and the WGSL generator follow the corpus in the sibling package eel-conformance (npm): portable JSON cases, a JSON Schema and a runner contract. This package does not depend on the corpus or run it. Stims runs it against three CPU paths, single-statement evaluation, the CSP-fallback interpreter and the JIT, in tests/unit/eel-conformance-spec.test.ts; no WGSL tier runs it. Five behaviours that differ from JavaScript and trip up most reimplementations:

  1. Division by exact zero yields 0, not Infinity or NaN (3 / 0 is 0).
  2. % truncates both operands to integers first, and a zero divisor yields 0.
  3. Truthiness is |v| > 0.00001: if(0.000001, a, b) takes the b branch.
  4. bor and band are logical, not bitwise: bor(0.5, 0) is 1.
  5. Identifiers are case-insensitive: Zoom, ZOOM and zoom are the same variable, and pi and e are ordinary prepopulated variables a preset may overwrite.

Try any of them in the scratchpad above.