Skip to content

Commit a45eac5

Browse files
committed
WIP: Write a Preset Authoring Guide
1 parent 5cead09 commit a45eac5

File tree

15 files changed

+743
-31
lines changed

15 files changed

+743
-31
lines changed

content/1.docs/3.preset-authoring/1.index.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ description: A comprehensive guide to Milkdrop presets.
88
This section contains all information required to create new and edit existing Milkdrop presets.
99

1010
Starting with a general overview on the file format and general rendering process, the guide also covers each built-in
11-
effect, syntax and usage of the Milkdrop-specific expressions and th use of HLSL shaders to further improve the
11+
effect, syntax and usage of the Milkdrop-specific expressions and making use of HLSL shaders to further improve the
1212
resulting visuals.
1313

1414
### Available Editors
1515

16-
The original Milkdrop plug-in for WinAmp<span>&trade;</span> has a built-in editor which allows editing all parameters
16+
The original Milkdrop plug-in for WinAmp<span>&reg;</span> has a built-in editor which allows editing all parameters
1717
and code blocks. While not being overly user-friendly, it is the editor which was used to create basically all presets
1818
available in the wild.
1919

20-
Recently, more community projects have started implementing their own editor, including projectM. As of now, none of
21-
these editors are released publicly. Once they become available, we'll add links here.
20+
Recently, more community projects have started implementing their own, improved editors, including projectM. As of now,
21+
none of these editors are released publicly. Once they become available, we'll add links here.

content/1.docs/3.preset-authoring/6.expression-reference/1.index.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,53 @@
22
title: Using Expressions
33
description: Writing math expressions to control preset effect rendering.
44
---
5+
6+
While some effects already produce a certain amount of movement and reactivity, the real magic in Milkdrop presets
7+
happens in the expression code.
8+
9+
**Important:** Expressions are not shaders! They use a different language and solely run on the CPU.
10+
11+
## Features
12+
13+
These expressions are written in a simple scripting language which is similar to what scientific programmable
14+
calculators accept as inputs. It is not a fully-featured scripting language, but powerful enough for basically any use
15+
case. It supports the following language features:
16+
17+
- A single data type: floating-point numbers.
18+
- User-defined variables with names up to 16 characters.
19+
- The usual set of operators for math, logic and assignment.
20+
- A good number of built-in math functions.
21+
- Several control-structure functions for implementing conditionals and simple loops.
22+
23+
The expressions do _not_ support custom functions, writing reusable code blocks and defining data structures.
24+
25+
## Performance
26+
27+
Expressions generally perform very well, at near-native speeds of compiled code.
28+
29+
The original Milkdrop and most derivatives use
30+
the original ["ns-eel2"](https://github.com/projectM-visualizer/milkdrop2/tree/master/src/ns-eel2) (short for "NullSoft
31+
Expression Evaluation Library 2") implementation. It uses handwritten win32 assembly code - while there are ports of the
32+
assembly to x86_64 and PPC architectures, it generally only compiles and runs on the Windows 32-bit platform.
33+
34+
projectM, being a cross-platform, cross-architecture implementation, uses a newly written parser
35+
called ["projectm-eval"](https://github.com/projectM-visualizer/projectm-eval), which is written in C and thus compiles
36+
on basically any platform supporting floating-point math and the C99 standard library.
37+
38+
### Milkdrop / ns-eel2
39+
40+
In the original Milkdrop, code is parsed and the resulting structure is then built from blocks of pre-compiled Intel 486
41+
assembly instructions.
42+
43+
This makes the code basically as performant as natively- or JIT-compiled code. The downside is that the assembly
44+
instructions are quite old and there are probably more performant variants of doing things in modern CPUs, which can't
45+
be used since the compiler cannot optimize the assembly code.
46+
47+
### projectM / projectm-eval
48+
49+
projectM's custom approach builds a simple tree structure from the code, then recursively calls the functions
50+
implementing the language features to execute the code.
51+
52+
Since compilers can optimize the implementation to run faster on modern CPUs and the code has been designed with the
53+
lowest possible runtime overhead (e.g. not using return values, omitting stack frame pointers) in mind, the average
54+
performance should be nearly identical to ns-eel2.

content/1.docs/3.preset-authoring/6.expression-reference/2.syntax.md

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ description: General syntax of the expression code.
88
The expression language only knows a few syntactic features:
99

1010
- Numbers, with or without decimals. Can also contain an _integer_ base-10 exponent using the `e[+-]n` notation.
11-
- Variables to hold a single number, both user-defined and predefined. Variable names may contain characters a-z, 0-9
12-
and the underscore (`_`), while 0-9 must not be the first letter. The maximum length of a variable name defaults to 16
13-
characters and thus should not exceed this count.
14-
- Special constants starting with `$` (see [Undocumented Features](undocumented-features)).
11+
- [Variables](variables) to hold a single number, both user-defined and predefined.
12+
- [Special constants](undocumented-features#constants) starting with `$` (
13+
see [Undocumented Features](undocumented-features)).
1514
- A single expression, separated from other expressions in the same list via semicolons (`;`), with the last semicolon
1615
being optional.
17-
- Calls to built-in functions in the form `func(arg1, arg2, ...)`.
18-
- Mathematical, assignment and other operators.
16+
- Calls to [built-in functions](functions) in the form `func(arg1, arg2, ...)`.
17+
- Mathematical, assignment and other [operators](operators).
1918
- Subscript access `[]` for [megabuf/gmegabuf access](megabuf).
2019
- Parentheses (`()`) to group statements and override precedence.
2120
- In-line comments starting with `//` until the end of the current line
@@ -67,19 +66,7 @@ Functions always need to be followed by parentheses (`sin(expr)`), while variabl
6766
Milkdrop imposes a 16-character limit on variable names. projectm-eval does not have a hard limit, but it's advisable to
6867
not use more than 16 characters for Milkdrop compatibility.
6968

70-
Milkdrop also has a limit of 64 user-defined variables, which also isn't applied by projectm-eval.
71-
72-
### Global "Register" Variables
73-
74-
In addition to gmegabuf, there are an additional 100 variables which can be used to store global values. These variables
75-
are named `reg00`, `reg01` etc. up to `reg99`. Values stored in those variables are available in al execution contexts,
76-
in the same way as gmegabuf.
77-
78-
Note the index must always be written with two digits. `reg3` is _not_ considered a global variable and will only have a
79-
local scope as any other variable. The same is true for more digits like `reg123`.
80-
81-
Same as with gmegabuf, global variables are not necessarily `0` when a preset is initialized, and they can change at any
82-
time when two presets using the same global variables are blended during a transition.
69+
Milkdrop also has a limit of 64 user-defined variables, which also isn't applied by projectM.
8370

8471
## Numbers
8572

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
title: Variables
3+
description: User-defined and special reg/q/t variables.
4+
---
5+
6+
In expression code, variables can be used to store and retrieve a single number. There are three logical types of
7+
variables:
8+
9+
- Predefined variables depending on the execution context which act as inputs and outputs for a specific effect.
10+
- Up to 64 (in the original Milkdrop and derivatives, projectM does not impose a limit) user-defined variables, which
11+
can simply be used without declaration.
12+
- The special `reg`, `q` and `t` variables, which are used to carry over values from context to context in a certain
13+
way.
14+
15+
The `q` and `t` variables predate the `reg` variables and the megabuf feature and were the only way to pass values along
16+
between preset effects in earlier (pre-2.0) Milkdrop versions.
17+
18+
## `reg` Variables
19+
20+
The global `reg` variables are 100 additional, pre-defined variables which can be used to store global values. These
21+
variables are named `reg00`, `reg01` etc. up to `reg99`. Values stored in those variables are available in all execution
22+
contexts, in the same way as gmegabuf.
23+
24+
Register variables are not reassigned/coped between effects, so the values stored within will be passed form one effect
25+
to the next [in the order they are drawn](../preset-rendering-process).
26+
27+
**Important:** The index must always be written with two digits. `reg3` or `reg100` are _not_ considered global
28+
variables and will behave like any other user-defined variable.
29+
30+
Same as with gmegabuf, in Milkdrop and derivatives, global variables are not necessarily set to `0` when a preset is
31+
initialized, and they can change at any time when two presets using the same global variables are blended during a
32+
transition. In projectM, `reg` variables are always preset-specific and set to `0` when the preset starts.
33+
34+
## `q` Variables
35+
36+
In a preset, 32 variables named `q1` to `q32` can be used to pass values around within a preset, including the Warp and
37+
Composite shaders. These variables are passed (copied) in a tree-like manner, which is best visualized as a diagram:
38+
39+
![Flow of q Variables](/content/preset-guide/q-var-diagram.png)
40+
41+
The data flow from preset init code to custom wave/shape init code was not shown in the `q` var diagram Ryan
42+
Geiss' original guide, only documented as being present but mostly useless. In reality, the `q` vars actually _can_ be
43+
useful to pass the values of some predefined per-frame variables to the wave/shape init code which are not available in
44+
those code blocks, such as the aspect ratio.
45+
46+
## `t` Variables
47+
48+
Similar to the `q` variables, an additional 8 variables named `t1` to `t8` are available in the code of Custom Wave and
49+
Custom Shape effects.
50+
51+
For each Custom Shape or Wave, an individual copy of those variables is used, meaning `t1` in shape 1 is different from
52+
`t1` in shape 2.
53+
54+
![Flow of t Variables](/content/preset-guide/t-var-diagram.png)
File renamed without changes.
File renamed without changes.

content/1.docs/3.preset-authoring/6.expression-reference/5.megabuf.md

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
title: Memory Buffers
3+
description: Large memory buffers to store values.
4+
---
5+
6+
Since Milkdrop 2, a new feature to store values called "megabuf" was added. It adds several memory buffers which can be
7+
used in all expressions, with each buffer being able to store up to 8,388,608 numbers.
8+
9+
Each execution context in Milkdrop has its own memory buffer, plus an additional one called "gmegabuf" that is globally
10+
available across all code blocks. Execution contexts are generally tied to a specific effect. This is a list of all
11+
10 contexts and which ones share a megabuf, with one context per line:
12+
13+
- `per_frame_init_` and `per_frame_`
14+
- `per_pixel_`
15+
- `wave_1_init`, `wave_1_per_frame` and `wave_1_per_point`
16+
- `wave_2_init`, `wave_2_per_frame` and `wave_2_per_point`
17+
- `wave_3_init`, `wave_3_per_frame` and `wave_3_per_point`
18+
- `wave_4_init`, `wave_4_per_frame` and `wave_4_per_point`
19+
- `shape_1_init` and `shape_1_per_frame`
20+
- `shape_2_init` and `shape_2_per_frame`
21+
- `shape_3_init` and `shape_3_per_frame`
22+
- `shape_4_init` and `shape_4_per_frame`
23+
24+
This means there are 11 separate buffers per preset, allowing to store 92,274,688 different values. Using all slots
25+
would require 704 MB of RAM (numbers are stored as 64-bit/8-byte double-precision floats internally).
26+
27+
The global memory buffer can be used to circumvent the way of how values are passed using `qNN` and `tN` variables,
28+
similar to how `regNN` variables work, but having a larger capacity. Since this buffer is also not touched between
29+
frames, it can be used to carry values from an effect late in a frame to another effect drawn in the next frame, like
30+
custom waveform/spectrum values to be used in custom shapes. While this will have a single frame of lag, it opens up a
31+
few additional possibilities such as drawing spectrum data using custom shapes or the warp grid.
32+
33+
Warp and composite shaders do not have access to these buffers, as they run on the GPU.
34+
35+
## Buffer Allocation
36+
37+
To reduce the number of memory allocations at runtime, values are not individually allocated in system memory. Rather,
38+
Milkdrop and projectM allocate up to 128 blocks of 65,536 values per buffer.
39+
40+
Each time a value index is accessed, it is checked whether the block this index is stored is already allocated. If it
41+
is, the existing block will be accessed. If not, a new memory block is allocated and filled with zeroes.
42+
43+
### Memory Usage Warning
44+
45+
This in turn means if a preset accesses the first index of each block once, _all_ memory block would be allocated. In
46+
theory, the original ns-eel2 interpreter in Milkdrop supports setting a memory limit, but this feature is not used and
47+
thus there is no memory cap except the size of all buffers combined.
48+
49+
When writing presets, it's highly recommended to not spread memory indices too far, e.g. trying to keep them within one
50+
or two memory blocks if possible. In practice, most presets will probably only make use of the global buffer, ignoring
51+
the context-specific ones altogether. On modern PCs, a few MB of RAM usage won't have a big impact anyway.
52+
53+
## Accessing Memory Buffers
54+
55+
The global and context-specific memory buffers can be accessed with different methods.
56+
57+
### megabuf()
58+
59+
The `megabuf()` functions returns a reference to a single value in the memory buffer of the current execution context.
60+
The return value is a reference, and thus can be both read and assigned to:
61+
62+
```
63+
x = megabuf(1000);
64+
megabuf(1001) = x + 1;
65+
```
66+
67+
### gmegabuf()
68+
69+
The `gmegabuf()` functions returns a reference to a single value in the preset-global memory buffer. The return value is
70+
a reference, and thus can be both read and assigned to:
71+
72+
```
73+
x = gmegabuf(1000);
74+
gmegabuf(1001) = x + 1;
75+
```
76+
77+
### Subscript Operator
78+
79+
The subscript operator or "array access operator" has a slightly different meaning than in other programming languages.
80+
The Milkdrop expression syntax only knows a single data type, individual numbers, so there are no arrays.
81+
82+
If any index value is not an integer, it is _rounded_ to the nearest integer after calculating the final index (e.g.
83+
adding index and offset first, then rounding).
84+
85+
The subscript operator is instead used to address memory locations in megabuf and gmegabuf. There are three possible
86+
syntax variants:
87+
88+
#### Global Memory Access
89+
90+
Accessing the gmegabuf is possible by using the special keyword `gmem`, followed by an index in the subscript brackets.
91+
The following statement sets gmegabuf location 10.000 to the current value of `x`:
92+
93+
```
94+
gmem[10000] = x;
95+
```
96+
97+
Any memory index from 0 to 8.388.607 (= 128 * 65536) can be addressed.
98+
99+
#### Local Memory Access
100+
101+
Accessing the current context memory buffer (megabuf) can be done by writing the index before a set of empty brackets.
102+
The following example is analogous to the above, but it sets index 10.000 of the local memory instead:
103+
104+
```
105+
10000[] = x;
106+
```
107+
108+
#### Local Memory Access with Offset
109+
110+
An optional offset can be provided in the brackets, which is simply added to the index on the outside. The following
111+
example will set memory index 10.123 to the value of `x`:
112+
113+
```
114+
10000[123] = x;
115+
```
116+
117+
Both index and offset values can of course be calculated with expressions. So the following expression is valid:
118+
119+
```
120+
if(x > 5,5000,1000)[(sin(y) + 1 * .5) * 1000] = z;
121+
```
122+
123+
## Initializing a Range
124+
125+
**Note:** This works only on context-specific memory buffers, _not_ gmegabuf!
126+
127+
If a large portion of a context-specific memory buffer needs to be initialized with a specific, single value, the
128+
`memset` function can be used instead of `loop` and iterating over all indices. The following lines are equivalent, with
129+
the second one being more performant - both set 1000 values to 1.123, starting at index 4000:
130+
131+
```
132+
idx=0; loop(1000, 4000[idx] = 1.123; idx += 1);
133+
memset(4000, 1.123, 1000);
134+
```
135+
136+
## Copying Ranges
137+
138+
**Note:** This works only on context-specific memory buffers, _not_ gmegabuf! There is no way of either copying ranges
139+
within gmegabuf or between gmegabuf and a context megabuf.
140+
141+
In the case a large, continuous portion of values need to be copied, the `loop` function in expression code is way
142+
slower than using [the `memcpy`function](functions#memcpydest-src-count). This function takes a destination index, a
143+
source index and a count as arguments and then copies `count` values starting at the source index to the destination
144+
index.
145+
146+
To copy 1000 values from index 2000 to 4000, both of the following lines would do this, with the second one being
147+
way more performant:
148+
149+
```
150+
idx=0; loop(1000, 4000[idx] = 2000[idx]; idx += 1);
151+
memcpy(4000, 2000, 1000);
152+
```
153+
154+
## Freeing Memory
155+
156+
While the expression language defines a function [`freembuf`](functions#freembufindex), it doesn't actually do anything
157+
in Milkdrop or projectM. Once memory has been allocated, it'll stay that way until either the preset is unloaded
158+
(context-specific buffers and gmegabuf in projectM) or Milkdrop is closed (gmegabuf in Milkdrop).
159+
160+
## Cross-Preset Sharing
161+
162+
In the original Milkdrop and most derivatives, the global memory buffer is shared across presets being blended. While
163+
rare, this can occasionally break presets making heavy use of gmegabuf, as the newly loaded preset may use the same
164+
buffer indices as the previous one.
165+
166+
projectM does use one gmegabuf instance per preset, so each preset is fully independent regarding gmegabuf use.

content/1.docs/3.preset-authoring/6.expression-reference/7.compatibility.md

Lines changed: 0 additions & 4 deletions
This file was deleted.
File renamed without changes.

0 commit comments

Comments
 (0)