onejs-sl 0.1.8 → 0.1.9
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/CHANGELOG.md +11 -0
- package/README.md +12 -2
- package/goldens/goldens.json +9 -1
- package/package.json +1 -1
- package/src/ir.ts +21 -0
- package/src/lang/README.md +12 -1
- package/src/lang/check.ts +15 -6
- package/src/lang/classify.ts +2 -2
- package/src/lang/glsl.ts +280 -0
- package/src/lang/lower.ts +12 -1
- package/src/params.ts +6 -4
- package/src/sl.ts +10 -0
- package/src/tables.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.9
|
|
4
|
+
|
|
5
|
+
Two inputs built from the others (`Specs/SL_NEXT.md` 6), and a converter that carries more of a pasted shader over. A program that names neither input compiles to exactly what it did.
|
|
6
|
+
|
|
7
|
+
- `texel` is one pixel in uv, and `centered` is uv with 0 at the centre and x scaled by aspect
|
|
8
|
+
- A local, parameter or uniform named `texel` or `centered` keeps working
|
|
9
|
+
- A local named after a builtin can be assigned to
|
|
10
|
+
- `fromGLSL` reads `u_time` and `u_resolution` as the inputs, and drops their declarations
|
|
11
|
+
- `fromGLSL` turns a mat2 rotation into `rotate()`, and points any other mat2 at it
|
|
12
|
+
- `INPUT_DOCS` say `uv` and `fragCoord` are the centre of the pixel
|
|
13
|
+
|
|
3
14
|
## 0.1.8
|
|
4
15
|
|
|
5
16
|
A pasted GLSL shader becomes a `.sl` file (`Specs/SL_NEXT.md` 5). Nothing a program compiles to changes.
|
package/README.md
CHANGED
|
@@ -345,8 +345,18 @@ reset takes the whole page's device, Unity's included.
|
|
|
345
345
|
## What a program is given
|
|
346
346
|
|
|
347
347
|
`uv`, `fragCoord`, `resolution`, `time` and `aspect`, and the last three are the
|
|
348
|
-
**target's**, not the window's.
|
|
349
|
-
|
|
348
|
+
**target's**, not the window's. `uv` and `fragCoord` are the centre of the pixel
|
|
349
|
+
being drawn, so the first column's `fragCoord.x` is 0.5 on both backends.
|
|
350
|
+
|
|
351
|
+
Two more are built from those: `texel`, one pixel in uv (`1 / resolution`), and
|
|
352
|
+
`centered`, uv with 0 at the centre and x scaled by `aspect`, so a circle drawn
|
|
353
|
+
in it stays round. They are recorded only when a program names them, so every
|
|
354
|
+
program that does not compiles to the bytes it always did, and a program with a
|
|
355
|
+
`texel` of its own keeps it: a local, a parameter or a uniform of that name
|
|
356
|
+
shadows the input.
|
|
357
|
+
|
|
358
|
+
Why `resolution` and `aspect` are the target's: a program is drawn with
|
|
359
|
+
`Graphics.Blit` into the element's own render texture, and Unity sets `_ScreenParams` per camera and
|
|
350
360
|
leaves it alone for a blit: reading it from a 64x256 target answers with the
|
|
351
361
|
game view's size. Both backends read the same wrong thing, so they agreed with
|
|
352
362
|
each other and the eject test, which compares them, saw nothing. What saw it was
|
package/goldens/goldens.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"generatedBy": "onejs-sl goldens/run.mjs",
|
|
3
|
-
"package": "0.1.
|
|
3
|
+
"package": "0.1.9",
|
|
4
4
|
"ir": 2,
|
|
5
5
|
"target": "rgba8unorm-srgb: straight alpha, 8 bit, sRGB encoded, the stored bytes of a Linear OneJS game's element",
|
|
6
6
|
"size": [
|
|
@@ -108,6 +108,14 @@
|
|
|
108
108
|
"1.25": [255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255,255,128,64,255]
|
|
109
109
|
}
|
|
110
110
|
},
|
|
111
|
+
"inputs.sl": {
|
|
112
|
+
"hash": "07cd901a",
|
|
113
|
+
"source": "// texel and centered, the inputs built from the others (Specs/SL_NEXT.md 6).\n// A ring that stays round in any output shape, over cells 16 pixels wide.\nfloat4 main() {\n float ring = smoothstep(0.02, 0.0, abs(length(centered) - 0.3));\n float2 cell = frac(uv / (texel * 16.0));\n return float4(ring, cell, 1);\n}\n",
|
|
114
|
+
"samples": {
|
|
115
|
+
"0": [0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,173,171,86,255,139,212,86,255,0,244,86,255,0,110,86,255,207,171,86,255,24,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,253,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,193,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,173,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,139,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,253,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,188,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,207,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,207,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,226,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,24,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,193,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,253,212,237,255,188,244,237,255,207,110,237,255,226,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255],
|
|
116
|
+
"1.25": [0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,173,171,86,255,139,212,86,255,0,244,86,255,0,110,86,255,207,171,86,255,24,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,253,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,193,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,173,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,139,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,253,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,188,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,207,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,207,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,226,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,24,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,193,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,253,212,237,255,188,244,237,255,207,110,237,255,226,171,237,255,0,212,237,255,0,244,237,255,0,110,237,255,0,171,237,255,0,212,237,255,0,244,237,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,202,255,0,171,202,255,0,212,202,255,0,244,202,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,158,255,0,171,158,255,0,212,158,255,0,244,158,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255,0,110,86,255,0,171,86,255,0,212,86,255,0,244,86,255]
|
|
117
|
+
}
|
|
118
|
+
},
|
|
111
119
|
"orient.sl": {
|
|
112
120
|
"hash": "f3b299ff",
|
|
113
121
|
"source": "// An anchor: red is uv.x, green is uv.y, so a golden's orientation and its\n// linear-to-sRGB store can be checked against arithmetic, not against itself.\nfloat4 main() { return float4(uv.x, uv.y, 0, 1); }\n",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onejs-sl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "The OneJS shader language: a small typed language for per pixel programs, compiled to a portable IR and emitted as HLSL, WGSL, GLSL ES or a VM buffer",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
package/src/ir.ts
CHANGED
|
@@ -42,6 +42,27 @@ export const INPUTS = {
|
|
|
42
42
|
} as const
|
|
43
43
|
export type InputName = keyof typeof INPUTS
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Inputs a program can name that are built from the ones above rather than
|
|
47
|
+
* handed over by the host (`Specs/SL_NEXT.md` 6). Each read records the same
|
|
48
|
+
* few nodes a program would if it wrote the expression out, and the builder's
|
|
49
|
+
* hash consing makes every later read the same node. So there is no new
|
|
50
|
+
* instruction, no VM or host change, and a program that never names one holds
|
|
51
|
+
* no extra node, compiling to the bytes it always did.
|
|
52
|
+
*
|
|
53
|
+
* texel 1 / resolution: one pixel, in uv
|
|
54
|
+
* centered (uv - 0.5) * float2(aspect, 1): 0 at the centre, circles stay round
|
|
55
|
+
*/
|
|
56
|
+
export const DERIVED_INPUTS = {
|
|
57
|
+
texel: TYPE.VEC2,
|
|
58
|
+
centered: TYPE.VEC2,
|
|
59
|
+
} as const
|
|
60
|
+
export type DerivedInputName = keyof typeof DERIVED_INPUTS
|
|
61
|
+
|
|
62
|
+
/** Every input a program can name: the host's, then the derived ones. */
|
|
63
|
+
export type SourceInputName = InputName | DerivedInputName
|
|
64
|
+
export const SOURCE_INPUTS: Readonly<Record<SourceInputName, SLType>> = { ...INPUTS, ...DERIVED_INPUTS }
|
|
65
|
+
|
|
45
66
|
/** Index into `Program.nodes`. Always refers backwards. */
|
|
46
67
|
export type NodeRef = number
|
|
47
68
|
|
package/src/lang/README.md
CHANGED
|
@@ -234,7 +234,18 @@ where they were, so the result reads as the shader it came from. What it does:
|
|
|
234
234
|
same. Shadertoy's `uv = fragCoord / iResolution.xy` line is dropped, since
|
|
235
235
|
that is the input `uv`.
|
|
236
236
|
- `iResolution`, `iMouse`, `iChannel0` to `3` and `uniform sampler2D` become
|
|
237
|
-
`resolution`, a `mouse` uniform and declared textures.
|
|
237
|
+
`resolution`, a `mouse` uniform and declared textures. The Book of Shaders'
|
|
238
|
+
`u_time` and `u_resolution`, glslsandbox's `time` and `resolution`, and
|
|
239
|
+
Shadertoy's own inputs where a tool declares them are the inputs: their
|
|
240
|
+
`uniform` declarations go, so the shader does not arrive frozen.
|
|
241
|
+
- A mat2 rotation becomes `rotate(p, angle)`: a function or macro returning
|
|
242
|
+
`mat2(c, -s, s, c)` or `mat2(c, s, -s, c)` of one angle, or the same written
|
|
243
|
+
in place, wherever a vector is multiplied by it (`p *= rot(a)`,
|
|
244
|
+
`rot(a) * p`, `p * rot(a)`). The sign of the angle comes from the matrix and
|
|
245
|
+
from which side the vector is on, since GLSL fills a matrix by columns. The
|
|
246
|
+
function or macro goes once every use is converted; otherwise it stays, and
|
|
247
|
+
the notes point at `rotate()`. A file with a `rotate` of its own is left
|
|
248
|
+
alone, since the calls would reach it. `#define NAME value`
|
|
238
249
|
becomes a const; any other directive is kept as a comment, with a note.
|
|
239
250
|
- A name the language already means something by gets a `_` suffix, and an
|
|
240
251
|
`int` becomes a float (a for-loop counter stays an int), each with a note.
|
package/src/lang/check.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* is refused with the reason.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { INPUTS } from "../ir"
|
|
31
|
+
import { DERIVED_INPUTS, INPUTS } from "../ir"
|
|
32
32
|
import { VM_TEXTURES, VM_UNIFORMS } from "../ops"
|
|
33
33
|
import { SL_GLSL_HINT } from "../ops"
|
|
34
34
|
import { BUILTINS, NOT_YET } from "./builtins"
|
|
@@ -37,6 +37,12 @@ import type { Expr, FuncDecl, Stmt, Unit } from "./ast"
|
|
|
37
37
|
import { SLParseError, type Pos, type SLFix } from "./lexer"
|
|
38
38
|
|
|
39
39
|
const INPUT_NAMES = new Set(Object.keys(INPUTS))
|
|
40
|
+
/**
|
|
41
|
+
* Inputs built from the others (`texel`, `centered`). A declaration may take
|
|
42
|
+
* one's name, as it may a builtin's, because programs wrote their own `texel`
|
|
43
|
+
* long before the language had one, and that must keep compiling.
|
|
44
|
+
*/
|
|
45
|
+
const DERIVED_NAMES = new Set(Object.keys(DERIVED_INPUTS))
|
|
40
46
|
|
|
41
47
|
/**
|
|
42
48
|
* The GLSL spellings whose HLSL name means the same wherever it is written, so
|
|
@@ -133,13 +139,14 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
133
139
|
if (uniforms.has(n)) return "a uniform"
|
|
134
140
|
if (textures.has(n)) return "a texture"
|
|
135
141
|
if (consts.has(n)) return "a const"
|
|
142
|
+
if (DERIVED_NAMES.has(n)) return "a derived input"
|
|
136
143
|
return null
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
/** What a value may not be called: anything spoken for, except a builtin's or a prelude function's name. */
|
|
140
147
|
const valueClash = (n: string): string | null => {
|
|
141
148
|
const why = taken(n)
|
|
142
|
-
if (why !== null && why !== "a builtin") return why
|
|
149
|
+
if (why !== null && why !== "a builtin" && why !== "a derived input") return why
|
|
143
150
|
const fn = funcs.get(n)
|
|
144
151
|
return fn !== undefined && !fn.prelude ? "a function" : null
|
|
145
152
|
}
|
|
@@ -220,7 +227,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
220
227
|
if (main.params.length > 0) {
|
|
221
228
|
fail(
|
|
222
229
|
"main takes no parameters: what a program is given are the free identifiers uv, " +
|
|
223
|
-
"fragCoord, resolution, time and
|
|
230
|
+
"fragCoord, resolution, time, aspect, texel and centered",
|
|
224
231
|
main.params[0]!.pos,
|
|
225
232
|
)
|
|
226
233
|
}
|
|
@@ -365,7 +372,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
365
372
|
}
|
|
366
373
|
if (target.k !== "ident") fail("only a local can be assigned to", target.pos)
|
|
367
374
|
const n = target.name
|
|
368
|
-
|
|
375
|
+
// A local shadows what it is named after, a builtin or a
|
|
376
|
+
// derived input, so it is assignable like any other local.
|
|
377
|
+
const why = scope.has(n) ? null : taken(n)
|
|
369
378
|
if (why !== null) fail(`"${n}" is ${why} and cannot be assigned to`, target.pos, n.length)
|
|
370
379
|
if (counters.has(n)) {
|
|
371
380
|
fail(
|
|
@@ -423,7 +432,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
423
432
|
case "hex":
|
|
424
433
|
return
|
|
425
434
|
case "ident": {
|
|
426
|
-
if (scope.has(e.name) || INPUT_NAMES.has(e.name) || uniforms.has(e.name) || consts.has(e.name)) return
|
|
435
|
+
if (scope.has(e.name) || INPUT_NAMES.has(e.name) || DERIVED_NAMES.has(e.name) || uniforms.has(e.name) || consts.has(e.name)) return
|
|
427
436
|
if (textures.has(e.name)) {
|
|
428
437
|
fail(
|
|
429
438
|
`"${e.name}" is a texture, and a texture is only ever the first argument of ` +
|
|
@@ -581,7 +590,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
581
590
|
/** Refuses a name nothing declares, offering the nearest one spelled almost like it. */
|
|
582
591
|
function unknown(n: string, scope: Set<string>, pos: Pos): never {
|
|
583
592
|
const near = nearest(n, [
|
|
584
|
-
...scope, ...INPUT_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
|
|
593
|
+
...scope, ...INPUT_NAMES, ...DERIVED_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
|
|
585
594
|
...funcs.keys(), ...Object.keys(BUILTINS),
|
|
586
595
|
])
|
|
587
596
|
if (near === null) fail(`"${n}" is not declared`, pos, n.length)
|
package/src/lang/classify.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* wants that should ask it.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { SOURCE_INPUTS } from "../ir"
|
|
17
17
|
import { BUILTINS } from "./builtins"
|
|
18
18
|
import { lex } from "./lexer"
|
|
19
19
|
import { PRELUDE_NAMES } from "./prelude-source"
|
|
@@ -40,7 +40,7 @@ export interface SLClassifiedToken {
|
|
|
40
40
|
length: number
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const INPUT_NAMES: ReadonlySet<string> = new Set(Object.keys(
|
|
43
|
+
const INPUT_NAMES: ReadonlySet<string> = new Set(Object.keys(SOURCE_INPUTS))
|
|
44
44
|
|
|
45
45
|
/** The tokens of `source`, comments included, in order, each with its class. */
|
|
46
46
|
export function classify(source: string): SLClassifiedToken[] {
|
package/src/lang/glsl.ts
CHANGED
|
@@ -107,6 +107,83 @@ const MISSING: Record<string, string> = {
|
|
|
107
107
|
dFdx: "there are no derivatives", dFdy: "there are no derivatives", fwidth: "there are no derivatives",
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Uniforms other hosts declare for what is an input here: the Book of Shaders'
|
|
112
|
+
* and glslCanvas's `u_time`, glslsandbox's `time`, and Shadertoy's own when a
|
|
113
|
+
* tool declares them. The declaration goes and every use reads the input, so a
|
|
114
|
+
* pasted shader does not arrive frozen on a uniform nothing sets.
|
|
115
|
+
*/
|
|
116
|
+
const HOST_UNIFORMS: Record<string, string> = {
|
|
117
|
+
u_time: "time", u_resolution: "resolution", time: "time", resolution: "resolution",
|
|
118
|
+
iTime: "time", iGlobalTime: "time", iResolution: "resolution", iMouse: "mouse",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const QUALIFIERS = new Set(["highp", "mediump", "lowp"])
|
|
122
|
+
/** Words that take a bracket without being called. */
|
|
123
|
+
const CONTROL = new Set(["if", "for", "while", "switch", "return"])
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether a `mat2(...)`, given its four arguments as words, is a rotation, and
|
|
127
|
+
* which way: +1 for `mat2(c, s, -s, c)`, the matrix R(a), and -1 for
|
|
128
|
+
* `mat2(c, -s, s, c)`, which is R(-a) since GLSL fills a matrix by columns.
|
|
129
|
+
* `cos` and `sin` are the words that stand for the cosine and sine.
|
|
130
|
+
*/
|
|
131
|
+
function rotationSign(matArgs: string[], cos: string[], sin: string[]): 1 | -1 | null {
|
|
132
|
+
const kind = (w: string) => cos.includes(w) ? "C" : sin.includes(w) ? "S"
|
|
133
|
+
: w.startsWith("- ") && sin.includes(w.slice(2)) ? "-S" : "?"
|
|
134
|
+
const k = matArgs.map(kind).join(",")
|
|
135
|
+
return k === "C,S,-S,C" ? 1 : k === "C,-S,S,C" ? -1 : null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Words split at the commas outside any bracket. */
|
|
139
|
+
function splitTop(words: string[]): string[] {
|
|
140
|
+
const out: string[] = []
|
|
141
|
+
let depth = 0
|
|
142
|
+
let cur: string[] = []
|
|
143
|
+
for (const w of words) {
|
|
144
|
+
if (w === "(" || w === "[") depth++
|
|
145
|
+
else if (w === ")" || w === "]") depth--
|
|
146
|
+
if (w === "," && depth === 0) { out.push(cur.join(" ")); cur = [] } else cur.push(w)
|
|
147
|
+
}
|
|
148
|
+
out.push(cur.join(" "))
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The arguments of `mat2 ( ... )` as words, when `words` is exactly that, in any number of brackets. */
|
|
153
|
+
function mat2Args(words: string[]): string[] | null {
|
|
154
|
+
while (words[0] === "(" && words[words.length - 1] === ")") words = words.slice(1, -1)
|
|
155
|
+
if (words[0] !== "mat2" || words[1] !== "(" || words[words.length - 1] !== ")") return null
|
|
156
|
+
const inner = words.slice(2, -1)
|
|
157
|
+
let depth = 0
|
|
158
|
+
for (const w of inner) { if (w === "(") depth++; else if (w === ")" && --depth < 0) return null }
|
|
159
|
+
return splitTop(inner)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The sign of a function body that returns a rotation of its parameter:
|
|
164
|
+
* `return mat2(cos(a), -sin(a), sin(a), cos(a));`, or the same after
|
|
165
|
+
* `float c = cos(a), s = sin(a);`. Null for anything else.
|
|
166
|
+
*/
|
|
167
|
+
function rotationBody(words: string[], param: string): 1 | -1 | null {
|
|
168
|
+
const cos = [`cos ( ${param} )`]
|
|
169
|
+
const sin = [`sin ( ${param} )`]
|
|
170
|
+
const statements = words.join(" ").split(/\s*;\s*/).filter((x) => x !== "")
|
|
171
|
+
const last = statements.pop()
|
|
172
|
+
const binding = new RegExp(`^(\\w+) = (cos|sin) \\( ${param} \\)$`)
|
|
173
|
+
for (const st of statements) {
|
|
174
|
+
const parts = splitTop(st.replace(/^float /, "").split(" "))
|
|
175
|
+
for (const part of parts) {
|
|
176
|
+
const m = binding.exec(part)
|
|
177
|
+
if (m === null) return null
|
|
178
|
+
if (m[2] === "cos") cos.push(m[1]!)
|
|
179
|
+
else sin.push(m[1]!)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (last === undefined || !last.startsWith("return ")) return null
|
|
183
|
+
const a = mat2Args(last.slice("return ".length).split(" "))
|
|
184
|
+
return a === null || a.length !== 4 ? null : rotationSign(a, cos, sin)
|
|
185
|
+
}
|
|
186
|
+
|
|
110
187
|
/** Names a pasted value or function cannot keep, because the language already means something by them. */
|
|
111
188
|
function taken(name: string, isFunction: boolean): boolean {
|
|
112
189
|
if (name in INPUTS || (SL_KEYWORDS as readonly string[]).includes(name) || (SL_TYPES as readonly string[]).includes(name)) return true
|
|
@@ -141,6 +218,61 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
141
218
|
}
|
|
142
219
|
return toks.length
|
|
143
220
|
}
|
|
221
|
+
/** The index of the bracket opening the one at `close`, or -1. */
|
|
222
|
+
const opening = (close: number): number => {
|
|
223
|
+
const [o, c] = toks[close]!.text === ")" ? ["(", ")"] : ["[", "]"]
|
|
224
|
+
let depth = 0
|
|
225
|
+
for (let i = close; i >= 0; i--) {
|
|
226
|
+
if (toks[i]!.kind !== "punct") continue
|
|
227
|
+
if (toks[i]!.text === c) depth++
|
|
228
|
+
else if (toks[i]!.text === o && --depth === 0) return i
|
|
229
|
+
}
|
|
230
|
+
return -1
|
|
231
|
+
}
|
|
232
|
+
/** Words for tokens `from` to `to`: no layout, no comments, one space between. */
|
|
233
|
+
const words = (from: number, to: number) =>
|
|
234
|
+
toks.slice(from, to).filter((t) => t.kind !== "ws" && t.kind !== "comment").map((t) => t.text)
|
|
235
|
+
/**
|
|
236
|
+
* The last token of the operand starting at `i`: a name, a number, a call
|
|
237
|
+
* or a bracket, then any members and indexes. -1 when it is none of those.
|
|
238
|
+
*/
|
|
239
|
+
const operandAfter = (i: number): number => {
|
|
240
|
+
const t = toks[i]
|
|
241
|
+
let j: number
|
|
242
|
+
if (t === undefined) return -1
|
|
243
|
+
if (t.kind === "number") j = i
|
|
244
|
+
else if (is(i, "(")) j = closing(i)
|
|
245
|
+
else if (t.kind === "ident") { const n = skip(i + 1); j = is(n, "(") ? closing(n) : i }
|
|
246
|
+
else return -1
|
|
247
|
+
for (;;) {
|
|
248
|
+
if (j >= toks.length) return -1
|
|
249
|
+
const n = skip(j + 1)
|
|
250
|
+
if (is(n, ".") && toks[skip(n + 1)]?.kind === "ident") j = skip(n + 1)
|
|
251
|
+
else if (is(n, "[")) j = closing(n)
|
|
252
|
+
else return j
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** The first token of the operand ending at `j`, the same shapes read backwards. -1 when there is none. */
|
|
256
|
+
const operandBefore = (j: number): number => {
|
|
257
|
+
let i = j
|
|
258
|
+
for (;;) {
|
|
259
|
+
if (is(i, "]")) { const o = opening(i); if (o < 0) return -1; i = back(o - 1); continue }
|
|
260
|
+
let start: number
|
|
261
|
+
if (is(i, ")")) {
|
|
262
|
+
const o = opening(i)
|
|
263
|
+
if (o < 0) return -1
|
|
264
|
+
const p = back(o - 1)
|
|
265
|
+
start = toks[p]?.kind === "ident" && !CONTROL.has(toks[p]!.text) ? p : o
|
|
266
|
+
} else if (toks[i]?.kind === "ident" || toks[i]?.kind === "number") {
|
|
267
|
+
start = i
|
|
268
|
+
} else {
|
|
269
|
+
return -1
|
|
270
|
+
}
|
|
271
|
+
const p = back(start - 1)
|
|
272
|
+
if (!is(p, ".")) return start
|
|
273
|
+
i = back(p - 1)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
144
276
|
/** A call's arguments, as token ranges, for the call whose "(" is at `open`. */
|
|
145
277
|
const args = (open: number, close: number): Array<[number, number]> => {
|
|
146
278
|
const out: Array<[number, number]> = []
|
|
@@ -173,6 +305,142 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
173
305
|
if (taken(name, isFunction)) clashes.set(name, name + "_")
|
|
174
306
|
}
|
|
175
307
|
|
|
308
|
+
// ---------- what other hosts declare ----------
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Token ranges written as something else, keyed by their first token: a
|
|
312
|
+
* declaration that goes, a rotation that becomes a call. They nest, and
|
|
313
|
+
* never cross.
|
|
314
|
+
*/
|
|
315
|
+
const spans = new Map<number, { end: number; render: (inEntry: boolean) => string }>()
|
|
316
|
+
const crosses = (start: number, end: number) =>
|
|
317
|
+
[...spans].some(([s, v]) => (s < start && start <= v.end && v.end < end) || (start < s && s <= end && end < v.end))
|
|
318
|
+
/** A range and the whitespace after it, gone. */
|
|
319
|
+
const drop = (start: number, end: number) => {
|
|
320
|
+
spans.set(start, { end: toks[end + 1]?.kind === "ws" ? end + 1 : end, render: () => "" })
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const hostUniforms = new Set<string>()
|
|
324
|
+
for (let i = 0; i < toks.length; i++) {
|
|
325
|
+
if (!is(i, "uniform")) continue
|
|
326
|
+
let t = skip(i + 1)
|
|
327
|
+
while (QUALIFIERS.has(toks[t]?.text ?? "")) t = skip(t + 1)
|
|
328
|
+
const n = skip(t + 1)
|
|
329
|
+
const name = toks[n]?.text ?? ""
|
|
330
|
+
const semi = skip(n + 1)
|
|
331
|
+
if (toks[n]?.kind !== "ident" || HOST_UNIFORMS[name] === undefined || !is(semi, ";")) continue
|
|
332
|
+
hostUniforms.add(name)
|
|
333
|
+
// Its name is the input, not a clash with it.
|
|
334
|
+
clashes.delete(name)
|
|
335
|
+
const input = HOST_UNIFORMS[name]!
|
|
336
|
+
noteOnce("host:" + name, toks[i]!.line, `the uniform ${name} is the input ${input}, so its declaration is dropped`)
|
|
337
|
+
drop(i, semi)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------- rotations ----------
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* There are no matrices, and the matrix a shader almost always has is a
|
|
344
|
+
* rotation: `mat2 rot(float a)` returning `mat2(c, -s, s, c)`, the same as
|
|
345
|
+
* a macro, or written in place. Each use that multiplies a vector becomes
|
|
346
|
+
* `rotate(p, angle)`, the angle's sign set by the matrix and by which side
|
|
347
|
+
* the vector is on, since GLSL's `p * M` is M transposed times p. A
|
|
348
|
+
* function or macro whose every use is converted goes. Anything else keeps
|
|
349
|
+
* its `mat2`, which `errors` then points at, with a note naming rotate().
|
|
350
|
+
*/
|
|
351
|
+
const rotations = new Map<string, { sign: 1 | -1; def: [number, number]; line: number; uses: number; converted: number }>()
|
|
352
|
+
for (let i = 0; i < toks.length; i++) {
|
|
353
|
+
const t = toks[i]!
|
|
354
|
+
if (t.kind === "directive") {
|
|
355
|
+
const m = /^#\s*define\s+([A-Za-z_]\w*)\(\s*([A-Za-z_]\w*)\s*\)(.*)$/s.exec(t.text)
|
|
356
|
+
if (m === null) continue
|
|
357
|
+
const body = tokenize(m[3]!.replace(/\\\n/g, " ")).filter((x) => x.kind !== "ws" && x.kind !== "comment").map((x) => x.text)
|
|
358
|
+
const a = mat2Args(body)
|
|
359
|
+
const sign = a === null || a.length !== 4 ? null : rotationSign(a, [`cos ( ${m[2]} )`], [`sin ( ${m[2]} )`])
|
|
360
|
+
if (sign !== null) rotations.set(m[1]!, { sign, def: [i, i], line: t.line, uses: 0, converted: 0 })
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
if (!is(i, "mat2")) continue
|
|
364
|
+
const n = skip(i + 1)
|
|
365
|
+
const open = skip(n + 1)
|
|
366
|
+
if (toks[n]?.kind !== "ident" || !is(open, "(")) continue
|
|
367
|
+
const close = closing(open)
|
|
368
|
+
const params = words(open + 1, close).filter((w) => w !== "in")
|
|
369
|
+
const body = skip(close + 1)
|
|
370
|
+
if (params.length !== 2 || params[0] !== "float" || !is(body, "{")) continue
|
|
371
|
+
const end = closing(body)
|
|
372
|
+
const sign = rotationBody(words(body + 1, end), params[1]!)
|
|
373
|
+
if (sign !== null) rotations.set(toks[n]!.text, { sign, def: [i, end], line: t.line, uses: 0, converted: 0 })
|
|
374
|
+
}
|
|
375
|
+
// A file with a rotate of its own would have the calls written here call it.
|
|
376
|
+
const ownRotate = toks.some((t, i) => t.text === "rotate" && t.kind === "ident" && GLSL_TYPES.has(toks[back(i - 1)]?.text ?? "") &&
|
|
377
|
+
!(rotations.has("rotate") && toks[back(i - 1)]!.text === "mat2"))
|
|
378
|
+
|
|
379
|
+
/** The angle, negated when `sign` is -1, as text. */
|
|
380
|
+
const angle = (range: [number, number], sign: 1 | -1, inEntry: boolean) => {
|
|
381
|
+
const a = emit(range[0], range[1], inEntry).trim()
|
|
382
|
+
if (sign === 1) return a
|
|
383
|
+
if (/^-[\w.]+$/.test(a)) return a.slice(1)
|
|
384
|
+
return /^[\w.]+$/.test(a) ? `-${a}` : `-(${a})`
|
|
385
|
+
}
|
|
386
|
+
for (let i = 0; i < toks.length && !ownRotate; i++) {
|
|
387
|
+
if (toks[i]!.kind !== "ident" || is(back(i - 1), ".")) continue
|
|
388
|
+
const open = skip(i + 1)
|
|
389
|
+
if (!is(open, "(")) continue
|
|
390
|
+
const close = closing(open)
|
|
391
|
+
const a = args(open, close)
|
|
392
|
+
const fn = rotations.get(toks[i]!.text)
|
|
393
|
+
let sign: 1 | -1 | null = null
|
|
394
|
+
let range: [number, number] | null = null
|
|
395
|
+
if (fn !== undefined && (i < fn.def[0] || i > fn.def[1])) {
|
|
396
|
+
fn.uses++
|
|
397
|
+
if (a.length === 1) { sign = fn.sign; range = a[0]! }
|
|
398
|
+
} else if (is(i, "mat2") && a.length === 4) {
|
|
399
|
+
// In place: the angle is whatever cos() is given.
|
|
400
|
+
const first = skip(a[0]![0])
|
|
401
|
+
const inner = skip(first + 1)
|
|
402
|
+
if (is(first, "cos") && is(inner, "(") && closing(inner) === back(a[0]![1] - 1)) {
|
|
403
|
+
const x = words(inner + 1, closing(inner)).join(" ")
|
|
404
|
+
sign = rotationSign(a.map(([x0, x1]) => words(x0, x1).join(" ")), [`cos ( ${x} )`], [`sin ( ${x} )`])
|
|
405
|
+
range = [inner + 1, closing(inner)]
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (sign === null || range === null) continue
|
|
409
|
+
const r = range
|
|
410
|
+
const s = sign
|
|
411
|
+
const before = back(i - 1)
|
|
412
|
+
const after = skip(close + 1)
|
|
413
|
+
let span: [number, number, (inEntry: boolean) => string] | null = null
|
|
414
|
+
if (is(before, "=") && is(before - 1, "*") && is(after, ";")) {
|
|
415
|
+
// p *= M is p = p * M.
|
|
416
|
+
const lhsEnd = back(before - 2)
|
|
417
|
+
const lhs = operandBefore(lhsEnd)
|
|
418
|
+
if (lhs >= 0) span = [lhs, close, (e) => { const p = emit(lhs, lhsEnd + 1, e).trim(); return `${p} = rotate(${p}, ${angle(r, -s as 1 | -1, e)})` }]
|
|
419
|
+
} else if (is(after, "*") && !is(after + 1, "=") && !["/", "%", "*"].includes(toks[before]?.text ?? "")) {
|
|
420
|
+
// M * p.
|
|
421
|
+
const x = skip(after + 1)
|
|
422
|
+
const xEnd = operandAfter(x)
|
|
423
|
+
if (xEnd >= 0) span = [i, xEnd, (e) => `rotate(${emit(x, xEnd + 1, e).trim()}, ${angle(r, s, e)})`]
|
|
424
|
+
} else if (is(before, "*") && !is(after, "*")) {
|
|
425
|
+
// p * M.
|
|
426
|
+
const xEnd = back(before - 1)
|
|
427
|
+
const x = operandBefore(xEnd)
|
|
428
|
+
if (x >= 0 && !["/", "%"].includes(toks[back(x - 1)]?.text ?? "")) {
|
|
429
|
+
span = [x, close, (e) => `rotate(${emit(x, xEnd + 1, e).trim()}, ${angle(r, -s as 1 | -1, e)})`]
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (span === null || crosses(span[0], span[1]) || spans.has(span[0])) continue
|
|
433
|
+
const [start, end, render] = span
|
|
434
|
+
spans.set(start, { end, render })
|
|
435
|
+
noteOnce("rotation", toks[i]!.line, "a mat2 rotation is rotate(p, angle) here, since there are no matrices; the angle's sign follows which side p was multiplied on")
|
|
436
|
+
if (fn !== undefined) fn.converted++
|
|
437
|
+
}
|
|
438
|
+
for (const [name, fn] of rotations) {
|
|
439
|
+
if (ownRotate || fn.converted !== fn.uses) continue
|
|
440
|
+
drop(fn.def[0], fn.def[1])
|
|
441
|
+
note(fn.line, `${name} built a rotation matrix, and each use of it is rotate() now, so it is removed`)
|
|
442
|
+
}
|
|
443
|
+
|
|
176
444
|
// ---------- the entry point ----------
|
|
177
445
|
|
|
178
446
|
let colour = "fragColor"
|
|
@@ -231,6 +499,8 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
231
499
|
let out = ""
|
|
232
500
|
for (let i = from; i < to; i++) {
|
|
233
501
|
const t = toks[i]!
|
|
502
|
+
const span = spans.get(i)
|
|
503
|
+
if (span !== undefined && span.end < to) { out += span.render(inEntry); i = span.end; continue }
|
|
234
504
|
if (t.kind === "directive") { out += directive(t); continue }
|
|
235
505
|
if (t.kind === "number") { out += t.text.replace(/[uU]$/, ""); continue }
|
|
236
506
|
if (t.kind !== "ident") { out += t.text; continue }
|
|
@@ -286,6 +556,16 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
286
556
|
out += to2
|
|
287
557
|
continue
|
|
288
558
|
}
|
|
559
|
+
if (hostUniforms.has(name) && (name === "u_time" || name === "u_resolution")) { out += HOST_UNIFORMS[name]; continue }
|
|
560
|
+
if (name === "mat2") {
|
|
561
|
+
noteOnce("mat2", t.line, "there are no matrices here; a mat2 rotation is rotate(p, angle): p *= mat2(c, -s, s, c) is rotate(p, a), and mat2(c, -s, s, c) * p is rotate(p, -a)")
|
|
562
|
+
out += name
|
|
563
|
+
continue
|
|
564
|
+
}
|
|
565
|
+
const unconverted = rotations.get(name)
|
|
566
|
+
if (unconverted !== undefined && called && (i < unconverted.def[0] || i > unconverted.def[1])) {
|
|
567
|
+
noteOnce("rot:" + t.line + name, t.line, `${name} builds a rotation matrix, and this use of it was not converted; write it as rotate(p, angle)`)
|
|
568
|
+
}
|
|
289
569
|
if (/^iChannel\d$/.test(name)) { textures.add(name); out += name; continue }
|
|
290
570
|
if (name === "iMouse") {
|
|
291
571
|
mouse = true
|
package/src/lang/lower.ts
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* than an optimisation.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import { INPUTS, type Program, type SLType } from "../ir"
|
|
36
|
+
import { DERIVED_INPUTS, INPUTS, type Program, type SLType } from "../ir"
|
|
37
37
|
import type { SlSdfKind } from "../shapes"
|
|
38
38
|
import * as sl from "../sl"
|
|
39
39
|
import type { Num, Val } from "../sl"
|
|
@@ -161,6 +161,17 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
161
161
|
for (const [name, width] of Object.entries(INPUTS)) {
|
|
162
162
|
global.declare(name, { width: width as SLType, value: (inputs as never)[name] })
|
|
163
163
|
}
|
|
164
|
+
// Read through, so a program that never names one records nothing
|
|
165
|
+
// for it. The setter only serves a refused assignment, which the
|
|
166
|
+
// checker has already reported and lowering carries on past.
|
|
167
|
+
for (const [name, width] of Object.entries(DERIVED_INPUTS)) {
|
|
168
|
+
let value: LV | undefined
|
|
169
|
+
global.declare(name, {
|
|
170
|
+
width: width as SLType,
|
|
171
|
+
get value() { return value ??= (inputs as never)[name] },
|
|
172
|
+
set value(v: LV) { value = v },
|
|
173
|
+
})
|
|
174
|
+
}
|
|
164
175
|
|
|
165
176
|
// Uniforms and textures take their slots in DECLARATION order, before
|
|
166
177
|
// anything is lowered, so a slot is a property of the file rather than
|
package/src/params.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { BUILTINS } from "./lang/builtins"
|
|
|
20
20
|
import { LIB_FUNCTIONS, SDF_CALLS } from "./lib/table"
|
|
21
21
|
import { LIB_PARAM_NAMES } from "./lib/names"
|
|
22
22
|
import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
|
|
23
|
-
import type {
|
|
23
|
+
import type { SourceInputName } from "./ir"
|
|
24
24
|
|
|
25
25
|
/** One function of the helper library, as its source declares it. */
|
|
26
26
|
export interface LibSignature {
|
|
@@ -169,10 +169,12 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
|
|
|
169
169
|
* the output is a UI element in OneJS and an image on a Magerie board, and the
|
|
170
170
|
* clock is whichever one the host runs.
|
|
171
171
|
*/
|
|
172
|
-
export const INPUT_DOCS: Readonly<Record<
|
|
173
|
-
uv: "
|
|
174
|
-
fragCoord: "
|
|
172
|
+
export const INPUT_DOCS: Readonly<Record<SourceInputName, string>> = {
|
|
173
|
+
uv: "The centre of this pixel in the output, 0 to 1 on each axis, with y up.",
|
|
174
|
+
fragCoord: "The centre of this pixel in the output, in pixels, with y up: the first column is at 0.5.",
|
|
175
175
|
resolution: "The output's size in pixels.",
|
|
176
176
|
time: "The clock, in seconds.",
|
|
177
177
|
aspect: "The output's width divided by its height.",
|
|
178
|
+
texel: "One pixel, in uv: 1 / resolution. Step by it to read a neighbouring pixel.",
|
|
179
|
+
centered: "uv with 0 at the centre and x scaled by aspect, so a circle drawn in it stays round.",
|
|
178
180
|
}
|
package/src/sl.ts
CHANGED
|
@@ -254,6 +254,10 @@ export interface ProgramInputs {
|
|
|
254
254
|
resolution: Vec2
|
|
255
255
|
time: Float
|
|
256
256
|
aspect: Float
|
|
257
|
+
/** One pixel, in uv: `1 / resolution`. Recorded only if read. */
|
|
258
|
+
texel: Vec2
|
|
259
|
+
/** `(uv - 0.5) * vec2(aspect, 1)`: 0 at the centre, and a circle stays round. Recorded only if read. */
|
|
260
|
+
centered: Vec2
|
|
257
261
|
}
|
|
258
262
|
|
|
259
263
|
/**
|
|
@@ -271,6 +275,12 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
|
|
|
271
275
|
const ref = b.add({ k: "input", type: width as SLType, name: name as InputName })
|
|
272
276
|
;(inputs as any)[name] = mk(b, ref, width as SLType)
|
|
273
277
|
}
|
|
278
|
+
// Getters, so a program that never reads one records nothing for it
|
|
279
|
+
// (DERIVED_INPUTS in ir.ts). A second read is hash consed to the first.
|
|
280
|
+
Object.defineProperties(inputs, {
|
|
281
|
+
texel: { enumerable: true, get: () => vec2(1, 1).div(inputs.resolution) },
|
|
282
|
+
centered: { enumerable: true, get: () => inputs.uv.sub(0.5).mul(vec2(inputs.aspect, 1)) },
|
|
283
|
+
})
|
|
274
284
|
const out = fn(inputs)
|
|
275
285
|
if (!(out instanceof Val)) throw new SLError("a program must return an sl value, not " + typeof out)
|
|
276
286
|
if (out.width !== TYPE.VEC4) {
|
package/src/tables.ts
CHANGED
|
@@ -13,7 +13,8 @@ export {
|
|
|
13
13
|
SLOP, SL_ARITY, SL_NAME, SL_HLSL, SL_CALL_NAMES, SL_GLSL_HINT, SL_UNIMPLEMENTED,
|
|
14
14
|
} from "./ops"
|
|
15
15
|
export type { SLSurface } from "./ops"
|
|
16
|
-
export { INPUTS } from "./ir"
|
|
16
|
+
export { DERIVED_INPUTS, INPUTS, SOURCE_INPUTS } from "./ir"
|
|
17
|
+
export type { DerivedInputName, InputName, SourceInputName } from "./ir"
|
|
17
18
|
export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
|
|
18
19
|
export { BUILTIN_DOCS, BUILTIN_PARAMS, INPUT_DOCS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES } from "./params"
|
|
19
20
|
export { PRELUDE_DOCS, PRELUDE_NAMES } from "./lang/prelude-source"
|