onejs-sl 0.7.0 → 0.9.0
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 +33 -0
- package/README.md +61 -15
- package/goldens/goldens.json +56 -5
- package/package.json +1 -1
- package/src/body.ts +87 -8
- package/src/core.ts +3 -2
- package/src/hlsl.ts +59 -4
- package/src/index.ts +2 -2
- package/src/ir.ts +129 -12
- package/src/lang/README.md +41 -12
- package/src/lang/ast.ts +6 -1
- package/src/lang/attributes.ts +75 -2
- package/src/lang/builtins.ts +2 -0
- package/src/lang/check.ts +14 -6
- package/src/lang/classify.ts +48 -4
- package/src/lang/glsl.ts +13 -4
- package/src/lang/index.ts +2 -2
- package/src/lang/lower.ts +17 -2
- package/src/lang/parser.ts +6 -5
- package/src/ops.ts +23 -9
- package/src/params.ts +12 -2
- package/src/serial.ts +16 -5
- package/src/sl.ts +41 -7
- package/src/tables.ts +5 -3
- package/src/web.ts +68 -15
package/src/lang/attributes.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* shares.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type
|
|
14
|
-
import { TYPE_WIDTH, type Attribute, type AttributeArg, type UniformDecl } from "./ast"
|
|
13
|
+
import { TEXTURE_FILTERS, TEXTURE_WRAPS, type TextureFilter, type TextureWrap, type UniformControl } from "../ir"
|
|
14
|
+
import { TYPE_WIDTH, type Attribute, type AttributeArg, type TextureDecl, type UniformDecl } from "./ast"
|
|
15
15
|
import type { Pos, SLFix } from "./lexer"
|
|
16
16
|
|
|
17
17
|
export interface AttributeProblem {
|
|
@@ -42,6 +42,75 @@ const ALIASES: Record<string, AttributeName> = {
|
|
|
42
42
|
|
|
43
43
|
const LIST = "Range, Toggle, Enum, Header, Label, Color and Hide"
|
|
44
44
|
|
|
45
|
+
/** A texture's attributes (`Specs/SL_NEXT.md` 8): how the program samples it. */
|
|
46
|
+
export const TEXTURE_ATTRIBUTE_NAMES = ["Wrap", "Filter"] as const
|
|
47
|
+
export type TextureAttributeName = (typeof TEXTURE_ATTRIBUTE_NAMES)[number]
|
|
48
|
+
|
|
49
|
+
/** What each texture attribute takes, as written, from Unity's TextureWrapMode and FilterMode where they agree. */
|
|
50
|
+
const TEXTURE_VALUES: Record<TextureAttributeName, Record<string, TextureWrap | TextureFilter>> = {
|
|
51
|
+
Wrap: Object.fromEntries(TEXTURE_WRAPS.map((w) => [w[0]!.toUpperCase() + w.slice(1), w])),
|
|
52
|
+
Filter: Object.fromEntries(TEXTURE_FILTERS.map((f) => [f[0]!.toUpperCase() + f.slice(1), f])),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Unity's spellings that mean one of ours, and the ones no host can match, with why. */
|
|
56
|
+
const TEXTURE_VALUE_ALIASES: Record<string, string> = { Bilinear: "Linear", Nearest: "Point" }
|
|
57
|
+
const TEXTURE_VALUE_REFUSED: Record<string, string> = {
|
|
58
|
+
Trilinear: "a program samples one mip level at a time, so there is no blend between levels to ask for; " +
|
|
59
|
+
"write [Filter(Linear)], and tex2Dlod with a fractional level blends two levels",
|
|
60
|
+
MirrorOnce: "WebGPU has no mirror once, so a browser could not draw it; write [Wrap(Mirror)] or [Wrap(Clamp)]",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ReadTextureAttributes {
|
|
64
|
+
/** What could be read. Neither set, the texture samples as the bound texture is set. */
|
|
65
|
+
sampling: { wrap?: TextureWrap; filter?: TextureFilter }
|
|
66
|
+
problems: AttributeProblem[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `[Wrap(Repeat)]` and `[Filter(Point)]` on a `texture2D`, read into its sampler. */
|
|
70
|
+
export function readTextureAttributes(t: TextureDecl): ReadTextureAttributes {
|
|
71
|
+
const out: ReadTextureAttributes = { sampling: {}, problems: [] }
|
|
72
|
+
const seen = new Set<string>()
|
|
73
|
+
const problem = (at: Attribute | AttributeArg, message: string, fix?: SLFix) => {
|
|
74
|
+
out.problems.push({ message, pos: at.pos, length: at.length, ...(fix === undefined ? {} : { fix }) })
|
|
75
|
+
}
|
|
76
|
+
for (const a of t.attrs) {
|
|
77
|
+
const name = (TEXTURE_ATTRIBUTE_NAMES as readonly string[]).includes(a.name) ? a.name as TextureAttributeName
|
|
78
|
+
: a.name === "WrapMode" ? "Wrap" : a.name === "FilterMode" ? "Filter" : null
|
|
79
|
+
if (name === null) {
|
|
80
|
+
const uniform = (ATTRIBUTE_NAMES as readonly string[]).includes(a.name) || a.name in ALIASES
|
|
81
|
+
problem(a, uniform
|
|
82
|
+
? `[${a.name}] is for a uniform; a texture takes [Wrap(Repeat)], [Wrap(Mirror)], [Wrap(Clamp)] and [Filter(Point)], [Filter(Linear)]`
|
|
83
|
+
: `"${a.name}" is not an attribute of a texture; a texture takes Wrap and Filter`)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (name !== a.name) {
|
|
87
|
+
problem(a, `"${a.name}" is ${name} here: [${name}(...)]`, { title: `Replace ${a.name} with ${name}`, replacement: name })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (seen.has(name)) { problem(a, `this texture already has [${name}]`); continue }
|
|
91
|
+
seen.add(name)
|
|
92
|
+
const values = TEXTURE_VALUES[name]
|
|
93
|
+
const listed = Object.keys(values).join(", ")
|
|
94
|
+
const arg = a.args.length === 1 && a.args[0]!.k === "ident" ? a.args[0]! as Extract<AttributeArg, { k: "ident" }> : null
|
|
95
|
+
if (arg === null) {
|
|
96
|
+
problem(a.args[0] ?? a, `[${name}] takes one of ${listed}, as in [${name}(${Object.keys(values)[0]})]`)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
const v = values[arg.name]
|
|
100
|
+
if (v !== undefined) {
|
|
101
|
+
if (name === "Wrap") out.sampling.wrap = v as TextureWrap
|
|
102
|
+
else out.sampling.filter = v as TextureFilter
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const refused = TEXTURE_VALUE_REFUSED[arg.name]
|
|
106
|
+
if (refused !== undefined) { problem(arg, `${name}(${arg.name}) is not offered: ${refused}`); continue }
|
|
107
|
+
const to = TEXTURE_VALUE_ALIASES[arg.name] ?? Object.keys(values).find((k) => k.toLowerCase() === arg.name.toLowerCase())
|
|
108
|
+
problem(arg, `${name} takes ${listed}, and this is ${arg.name}${to === undefined ? "" : `. Did you mean ${to}?`}`,
|
|
109
|
+
to === undefined ? undefined : { title: `Replace ${arg.name} with ${to}`, replacement: to })
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
45
114
|
export function readAttributes(u: UniformDecl): ReadAttributes {
|
|
46
115
|
const out: ReadAttributes = { control: {}, colour: false, from: {}, problems: [] }
|
|
47
116
|
const seen = new Set<string>()
|
|
@@ -62,6 +131,10 @@ export function readAttributes(u: UniformDecl): ReadAttributes {
|
|
|
62
131
|
|
|
63
132
|
for (const a of u.attrs) {
|
|
64
133
|
const known = (ATTRIBUTE_NAMES as readonly string[]).includes(a.name)
|
|
134
|
+
if (!known && ((TEXTURE_ATTRIBUTE_NAMES as readonly string[]).includes(a.name) || a.name === "WrapMode" || a.name === "FilterMode")) {
|
|
135
|
+
problem(a, `[${a.name}] is for a texture, as in [${a.name.replace("Mode", "")}(${a.name.startsWith("Wrap") ? "Repeat" : "Point"})] texture2D grain;`)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
65
138
|
if (!known) {
|
|
66
139
|
const to = ALIASES[a.name] ?? ATTRIBUTE_NAMES.find((n) => n.toLowerCase() === a.name.toLowerCase())
|
|
67
140
|
const hint = a.name === "IntRange" ? "; a whole number slider is [Range(min, max, 1)]" : ""
|
package/src/lang/builtins.ts
CHANGED
package/src/lang/check.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { SL_GLSL_HINT } from "../ops"
|
|
|
34
34
|
import { BUILTINS, NOT_YET } from "./builtins"
|
|
35
35
|
import { SL_SDF_SHAPES } from "../shapes"
|
|
36
36
|
import { TYPE_WIDTH, type Expr, type FuncDecl, type Stmt, type Unit } from "./ast"
|
|
37
|
-
import { readAttributes } from "./attributes"
|
|
37
|
+
import { readAttributes, readTextureAttributes } from "./attributes"
|
|
38
38
|
import { SLParseError, type Pos, type SLFix } from "./lexer"
|
|
39
39
|
|
|
40
40
|
const INPUT_NAMES = new Set(Object.keys(INPUTS))
|
|
@@ -68,7 +68,7 @@ const GLSL_RENAMES = new Set([
|
|
|
68
68
|
])
|
|
69
69
|
|
|
70
70
|
/** The builtins whose first argument is a texture, which is not a value anywhere else. */
|
|
71
|
-
const SAMPLES = new Set(["tex2D", "tex2Dlod"])
|
|
71
|
+
const SAMPLES = new Set(["tex2D", "tex2Dlod", "textureSize", "tex2Dfetch"])
|
|
72
72
|
|
|
73
73
|
const rename = (from: string, to: string): SLFix => ({ title: `Replace ${from} with ${to}`, replacement: to })
|
|
74
74
|
|
|
@@ -188,6 +188,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
188
188
|
const clash = valueClash(t.name)
|
|
189
189
|
if (clash !== null) fail(`"${t.name}" already names ${clash}`, t.pos, t.name.length)
|
|
190
190
|
})
|
|
191
|
+
for (const p of readTextureAttributes(t).problems) attempt(() => fail(p.message, p.pos, p.length, p.fix))
|
|
191
192
|
if (!textures.has(t.name)) textures.set(t.name, t)
|
|
192
193
|
}
|
|
193
194
|
attempt(() => {
|
|
@@ -512,7 +513,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
512
513
|
if (textures.has(e.name)) {
|
|
513
514
|
fail(
|
|
514
515
|
`"${e.name}" is a texture, and a texture is only ever the first argument of ` +
|
|
515
|
-
`tex2D or
|
|
516
|
+
`tex2D, tex2Dlod, tex2Dfetch or textureSize. Write \`tex2D(${e.name}, uv)\``,
|
|
516
517
|
e.pos, e.name.length,
|
|
517
518
|
)
|
|
518
519
|
}
|
|
@@ -636,11 +637,18 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
636
637
|
e.pos,
|
|
637
638
|
)
|
|
638
639
|
}
|
|
640
|
+
if (previous && n === "textureSize") {
|
|
641
|
+
fail("previous is the size this program draws at, which is `resolution`", e.pos)
|
|
642
|
+
}
|
|
643
|
+
if (previous && n === "tex2Dfetch") {
|
|
644
|
+
fail("previous is read at a uv: `tex2D(previous, uv)`, or `tex2D(previous, (pixel + 0.5) / resolution)` for a pixel", e.pos)
|
|
645
|
+
}
|
|
639
646
|
if (!previous && (tex === undefined || tex.k !== "ident" || !textures.has(tex.name))) {
|
|
640
|
-
const
|
|
647
|
+
const rest = n === "tex2Dlod" ? "uv, 0" : n === "textureSize" ? "" : n === "tex2Dfetch" ? "pixel" : "uv"
|
|
648
|
+
const verb = n === "textureSize" ? "measures" : n === "tex2Dfetch" ? "reads" : "samples"
|
|
641
649
|
fail(
|
|
642
|
-
`${n}
|
|
643
|
-
`then \`${n}(grain,
|
|
650
|
+
`${n} ${verb} a texture declared in this file, as in \`texture2D grain;\` ` +
|
|
651
|
+
`then \`${n}(grain${rest === "" ? "" : ", " + rest})\``,
|
|
644
652
|
(tex ?? e).pos,
|
|
645
653
|
)
|
|
646
654
|
}
|
package/src/lang/classify.ts
CHANGED
|
@@ -46,11 +46,54 @@ const INPUT_NAMES: ReadonlySet<string> = new Set([...Object.keys(SOURCE_INPUTS),
|
|
|
46
46
|
|
|
47
47
|
/** The tokens of `source`, comments included, in order, each with its class. */
|
|
48
48
|
export function classify(source: string): SLClassifiedToken[] {
|
|
49
|
+
return run(source, "", "").tokens
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Where a line starts, for `classifyLine`: what the lines before it leave
|
|
54
|
+
* behind that changes how it reads.
|
|
55
|
+
*/
|
|
56
|
+
export interface SLLineState {
|
|
57
|
+
/** The line before ended inside a block comment. */
|
|
58
|
+
readonly comment: boolean
|
|
59
|
+
/** The last two tokens before the line that were not comments, which decide what a name after a dot or a bracket is. */
|
|
60
|
+
readonly before: string
|
|
61
|
+
readonly beforeThat: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const FIRST_LINE: SLLineState = { comment: false, before: "", beforeThat: "" }
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* One line of a file, for an editor that colours a line at a time, as Monaco
|
|
68
|
+
* does: its tokens, with offsets within the line, and the state the next line
|
|
69
|
+
* starts from. Classifying every line of a file this way, each from the state
|
|
70
|
+
* the one before ended in, gives exactly what `classify` gives the whole file,
|
|
71
|
+
* a block comment over several lines coming back as one piece per line.
|
|
72
|
+
*/
|
|
73
|
+
export function classifyLine(line: string, state: SLLineState = FIRST_LINE): { tokens: SLClassifiedToken[]; state: SLLineState } {
|
|
74
|
+
// A line that starts inside a block comment is read with the comment's
|
|
75
|
+
// opening put back in front, so the lexer decides where it closes, and
|
|
76
|
+
// those two characters are taken off the tokens again.
|
|
77
|
+
const shift = state.comment ? 2 : 0
|
|
78
|
+
const read = run(state.comment ? "/*" + line : line, state.before, state.beforeThat)
|
|
79
|
+
const last = read.tokens[read.tokens.length - 1]
|
|
80
|
+
const comment = last !== undefined && last.kind === "comment" && last.text.startsWith("/*")
|
|
81
|
+
&& !(last.text.length >= 4 && last.text.endsWith("*/"))
|
|
82
|
+
const tokens: SLClassifiedToken[] = []
|
|
83
|
+
for (const t of read.tokens) {
|
|
84
|
+
if (shift === 0) { tokens.push(t); continue }
|
|
85
|
+
if (t.offset === 0) {
|
|
86
|
+
if (t.length > shift) tokens.push({ ...t, text: t.text.slice(shift), length: t.length - shift })
|
|
87
|
+
} else {
|
|
88
|
+
tokens.push({ ...t, col: t.col - shift, offset: t.offset - shift })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { tokens, state: { comment, before: read.before, beforeThat: read.beforeThat } }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function run(source: string, before: string, beforeThat: string): { tokens: SLClassifiedToken[]; before: string; beforeThat: string } {
|
|
49
95
|
const tokens = lex(source, "", true)
|
|
50
96
|
const out: SLClassifiedToken[] = []
|
|
51
|
-
// The last token that was not a comment, which decides what a name after a dot is.
|
|
52
|
-
let before = ""
|
|
53
|
-
let beforeThat = ""
|
|
54
97
|
for (const t of tokens) {
|
|
55
98
|
if (t.kind === "eof") break
|
|
56
99
|
let kind: SLTokenClass
|
|
@@ -68,7 +111,8 @@ export function classify(source: string): SLClassifiedToken[] {
|
|
|
68
111
|
kind = t.kind
|
|
69
112
|
}
|
|
70
113
|
out.push({ kind, text: t.text, line: t.line, col: t.col, offset: t.offset, length: t.length })
|
|
114
|
+
// The last token that was not a comment, which decides what a name after a dot is.
|
|
71
115
|
if (t.kind !== "comment") { beforeThat = before; before = t.text }
|
|
72
116
|
}
|
|
73
|
-
return out
|
|
117
|
+
return { tokens: out, before, beforeThat }
|
|
74
118
|
}
|
package/src/lang/glsl.ts
CHANGED
|
@@ -98,10 +98,8 @@ const MISSING: Record<string, string> = {
|
|
|
98
98
|
iFrameRate: "there is no frame rate",
|
|
99
99
|
iDate: "there is no date",
|
|
100
100
|
iChannelTime: "a texture has no time of its own",
|
|
101
|
-
iChannelResolution: "a
|
|
101
|
+
iChannelResolution: "write textureSize(iChannel0) for a channel's width and height",
|
|
102
102
|
iSampleRate: "there is no audio",
|
|
103
|
-
texelFetch: "a texture cannot be read without filtering yet",
|
|
104
|
-
textureSize: "a texture's size cannot be read yet",
|
|
105
103
|
dFdx: "there are no derivatives", dFdy: "there are no derivatives", fwidth: "there are no derivatives",
|
|
106
104
|
}
|
|
107
105
|
|
|
@@ -490,6 +488,8 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
490
488
|
// ---------- the rewrite ----------
|
|
491
489
|
|
|
492
490
|
const textures = new Set<string>()
|
|
491
|
+
/** Channels the shader declares itself, as a WebGL port of a Shadertoy shader does, which need no second declaration. */
|
|
492
|
+
const declaredChannels = new Set<string>()
|
|
493
493
|
let mouse = false
|
|
494
494
|
let uvDropped = false
|
|
495
495
|
|
|
@@ -530,6 +530,8 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
530
530
|
// `uniform sampler2D tex;` is `texture2D tex;`.
|
|
531
531
|
out += "texture2D"
|
|
532
532
|
i = skip(i + 1)
|
|
533
|
+
const declares = toks[skip(i + 1)]?.text ?? ""
|
|
534
|
+
if (/^iChannel\d$/.test(declares)) declaredChannels.add(declares)
|
|
533
535
|
continue
|
|
534
536
|
}
|
|
535
537
|
if (name === "sampler2D") { out += "texture2D"; continue }
|
|
@@ -616,6 +618,13 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
616
618
|
call = `pow(2.0, ${a[0]})`
|
|
617
619
|
} else if (name === "log2" && a.length === 1) {
|
|
618
620
|
call = `(log(${a[0]}) * 1.4426950408889634)`
|
|
621
|
+
} else if ((name === "texelFetch" && a.length === 3) || (name === "textureSize" && a.length === 2)) {
|
|
622
|
+
// Both read the full size texture here, so the level goes.
|
|
623
|
+
const level = a[a.length - 1]!
|
|
624
|
+
if (!/^0(\.0*)?$/.test(level)) {
|
|
625
|
+
note(t.line, `${name} reads level ${level} in GLSL, and ${name === "texelFetch" ? "tex2Dfetch" : name} reads the full size texture here`)
|
|
626
|
+
}
|
|
627
|
+
call = name === "texelFetch" ? `tex2Dfetch(${a[0]}, ${a[1]})` : `textureSize(${a[0]})`
|
|
619
628
|
}
|
|
620
629
|
if (call !== null) { out += call; i = close; continue }
|
|
621
630
|
}
|
|
@@ -683,7 +692,7 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
683
692
|
note(at >= 0 ? toks[at]!.line : 1, "the uv line is dropped: fragCoord / resolution is the input uv, y up")
|
|
684
693
|
}
|
|
685
694
|
|
|
686
|
-
const declared = [...textures].sort().map((t) => `texture2D ${t};`)
|
|
695
|
+
const declared = [...textures].filter((t) => !declaredChannels.has(t)).sort().map((t) => `texture2D ${t};`)
|
|
687
696
|
if (mouse) declared.unshift("uniform float4 mouse;")
|
|
688
697
|
const source = (declared.length > 0 ? declared.join("\n") + "\n" : "") + body.replace(/^\n+/, "")
|
|
689
698
|
const lineOf = (n: string) => Number(/^line (\d+)/.exec(n)?.[1] ?? 0)
|
package/src/lang/index.ts
CHANGED
|
@@ -33,8 +33,8 @@ export { TYPE_WIDTH } from "./ast"
|
|
|
33
33
|
export { SLParseError }
|
|
34
34
|
export type { Pos, SLFix, Token } from "./lexer"
|
|
35
35
|
export { tokenize } from "./lexer"
|
|
36
|
-
export { classify } from "./classify"
|
|
37
|
-
export type { SLClassifiedToken, SLTokenClass } from "./classify"
|
|
36
|
+
export { classify, classifyLine } from "./classify"
|
|
37
|
+
export type { SLClassifiedToken, SLLineState, SLTokenClass } from "./classify"
|
|
38
38
|
export { SL_KEYWORDS, SL_TYPES } from "./words"
|
|
39
39
|
export { parseUnit } from "./parser"
|
|
40
40
|
export { PRELUDE_SOURCE, preludeFunctions } from "./prelude"
|
package/src/lang/lower.ts
CHANGED
|
@@ -58,7 +58,7 @@ import type { SlSdfKind } from "../shapes"
|
|
|
58
58
|
import * as sl from "../sl"
|
|
59
59
|
import { Val, type Num } from "../sl"
|
|
60
60
|
import { TYPE_KIND, TYPE_WIDTH, type BinaryOp, type Expr, type FuncDecl, type Stmt, type TypeName } from "./ast"
|
|
61
|
-
import { readAttributes } from "./attributes"
|
|
61
|
+
import { readAttributes, readTextureAttributes } from "./attributes"
|
|
62
62
|
import { BUILTINS } from "./builtins"
|
|
63
63
|
import { terminates, type Checked } from "./check"
|
|
64
64
|
import { SLParseError, type Pos } from "./lexer"
|
|
@@ -288,7 +288,10 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
288
288
|
global.declare(u.name, { width, kind: TYPE_KIND[u.type], value })
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
|
|
291
|
+
// The checker reported any attribute it could not read; what it could goes in.
|
|
292
|
+
for (const t of unit.textures) {
|
|
293
|
+
samplers.set(t.name, at(t.pos, () => sl.texture(t.name, readTextureAttributes(t).sampling)))
|
|
294
|
+
}
|
|
292
295
|
// Unless the file declared a texture of its own by the name. The
|
|
293
296
|
// checker refuses tex2Dlod on it, and anything else that took the
|
|
294
297
|
// name, before lowering could reach either.
|
|
@@ -296,6 +299,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
296
299
|
samplers.set(PREVIOUS, {
|
|
297
300
|
sample: (uv) => inputs.previous.sample(uv),
|
|
298
301
|
sampleLevel: () => { throw new Error("tex2Dlod(previous) reached lowering; the checker refuses it") },
|
|
302
|
+
size: () => { throw new Error("textureSize(previous) reached lowering; the checker refuses it") },
|
|
303
|
+
fetch: () => { throw new Error("tex2Dfetch(previous) reached lowering; the checker refuses it") },
|
|
299
304
|
})
|
|
300
305
|
}
|
|
301
306
|
|
|
@@ -1160,6 +1165,16 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
1160
1165
|
if (n === "int" || n === "uint" || n === "bool") return convert(n, e, scope)
|
|
1161
1166
|
if (n in TYPE_WIDTH) return construct(n as TypeName, floats(e.args), e.pos)
|
|
1162
1167
|
|
|
1168
|
+
if (n === "textureSize" || n === "tex2Dfetch") {
|
|
1169
|
+
const tex = samplers.get((e.args[0] as Extract<Expr, { k: "ident" }>).name)!
|
|
1170
|
+
if (n === "textureSize") return at(e.pos, () => tex.size() as unknown as Val)
|
|
1171
|
+
const pixel = toFloat(lowerExpr(e.args[1]!, scope), e.args[1]!.pos)
|
|
1172
|
+
if (typeof pixel === "number" || pixel.width !== 2) {
|
|
1173
|
+
fail("tex2Dfetch reads at a float2 pixel, as in tex2Dfetch(t, fragCoord)", e.args[1]!.pos)
|
|
1174
|
+
}
|
|
1175
|
+
return at(e.pos, () => tex.fetch(pixel as never) as unknown as Val)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1163
1178
|
if (n === "tex2D" || n === "tex2Dlod") {
|
|
1164
1179
|
const texName = (e.args[0] as Extract<Expr, { k: "ident" }>).name
|
|
1165
1180
|
const tex = samplers.get(texName)!
|
package/src/lang/parser.ts
CHANGED
|
@@ -212,17 +212,18 @@ class Parser {
|
|
|
212
212
|
|
|
213
213
|
if (t.text === "[") {
|
|
214
214
|
const attrs = this.parseAttributes()
|
|
215
|
+
if (this.at("texture2D")) { unit.textures.push(this.parseTexture(attrs)); return }
|
|
215
216
|
if (!this.at("uniform")) {
|
|
216
217
|
this.fail(
|
|
217
|
-
`an attribute belongs on a uniform, as in \`[Range(0, 1)] uniform float amount = 0.5
|
|
218
|
-
`and this one is followed by ${describe(this.peek())}`, t,
|
|
218
|
+
`an attribute belongs on a uniform or a texture, as in \`[Range(0, 1)] uniform float amount = 0.5;\` ` +
|
|
219
|
+
`or \`[Wrap(Repeat)] texture2D grain;\`, and this one is followed by ${describe(this.peek())}`, t,
|
|
219
220
|
)
|
|
220
221
|
}
|
|
221
222
|
unit.uniforms.push(this.parseUniform(attrs))
|
|
222
223
|
return
|
|
223
224
|
}
|
|
224
225
|
if (t.text === "uniform") { unit.uniforms.push(this.parseUniform([])); return }
|
|
225
|
-
if (t.text === "texture2D") { unit.textures.push(this.parseTexture()); return }
|
|
226
|
+
if (t.text === "texture2D") { unit.textures.push(this.parseTexture([])); return }
|
|
226
227
|
if (t.text === "const") { unit.consts.push(this.parseConst()); return }
|
|
227
228
|
if (t.kind === "ident") {
|
|
228
229
|
const fn = this.parseFunction()
|
|
@@ -277,7 +278,7 @@ class Parser {
|
|
|
277
278
|
this.fail(`expected a number, a word or a "string" as an attribute's argument, got ${describe(t)}`, t)
|
|
278
279
|
}
|
|
279
280
|
|
|
280
|
-
private parseTexture(): TextureDecl {
|
|
281
|
+
private parseTexture(attrs: Attribute[]): TextureDecl {
|
|
281
282
|
const kw = this.next()
|
|
282
283
|
const name = this.expectName("a texture name")
|
|
283
284
|
if (this.at("=")) {
|
|
@@ -287,7 +288,7 @@ class Parser {
|
|
|
287
288
|
)
|
|
288
289
|
}
|
|
289
290
|
this.expect(";", "after a texture declaration")
|
|
290
|
-
return { name: name.text, pos: kw }
|
|
291
|
+
return { name: name.text, attrs, pos: kw }
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
private parseConst(): Extract<Stmt, { k: "const" }> {
|
package/src/ops.ts
CHANGED
|
@@ -26,16 +26,21 @@
|
|
|
26
26
|
* for a host to read.
|
|
27
27
|
*
|
|
28
28
|
* They are what OneJS draws a program with. It keeps a program's uniforms in
|
|
29
|
-
*
|
|
30
|
-
* (`SLProgramBridge.MaxUniforms`, the web frame's `sl_U`), and binds
|
|
31
|
-
* textures
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
29
|
+
* 64 slots in the editor, a native player and a browser alike
|
|
30
|
+
* (`SLProgramBridge.MaxUniforms`, the web frame's `sl_U`), and binds eight
|
|
31
|
+
* textures everywhere (`_Tex0` to `_Tex7`). A program past either has
|
|
32
|
+
* something with no slot of its own, which one host would drop and another
|
|
33
|
+
* would draw, so the parser and the builder both refuse it at the declaration,
|
|
34
|
+
* the parser with a file and a line.
|
|
35
|
+
*
|
|
36
|
+
* Both sit inside every backend's minimum (`Specs/SL_NEXT.md` 8). Eight
|
|
37
|
+
* textures and the previous frame are nine sampled textures and at most nine
|
|
38
|
+
* samplers, where WebGPU, WebGL2, OpenGL ES 3 and Direct3D 11 all guarantee 16
|
|
39
|
+
* a fragment stage. 64 float4s are 1 KB of uniforms, where WebGL2 and OpenGL
|
|
40
|
+
* ES 3 guarantee 224 vectors and WebGPU 64 KB.
|
|
36
41
|
*/
|
|
37
|
-
export const UNIFORM_SLOTS =
|
|
38
|
-
export const TEXTURE_SLOTS =
|
|
42
|
+
export const UNIFORM_SLOTS = 64
|
|
43
|
+
export const TEXTURE_SLOTS = 8
|
|
39
44
|
|
|
40
45
|
/** Input ids, which the web emitter switches on. */
|
|
41
46
|
export const INPUT_ID: Record<string, number> = {
|
|
@@ -149,6 +154,11 @@ export const SLOP = {
|
|
|
149
154
|
// What this program drew the frame before, at a uv (IR 5). No slot: the
|
|
150
155
|
// host keeps it, and a program that never reads it has none.
|
|
151
156
|
SAMPLE_PREVIOUS: 146,
|
|
157
|
+
// IR 6. A texture's size in pixels, a float2, its slot the one immediate.
|
|
158
|
+
TEXTURE_SIZE: 147,
|
|
159
|
+
// IR 6. One texel, unfiltered, at a float2 pixel: floored and held inside
|
|
160
|
+
// the texture, so every backend reads the same texel at every pixel.
|
|
161
|
+
SAMPLE_FETCH: 148,
|
|
152
162
|
} as const
|
|
153
163
|
|
|
154
164
|
export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
|
|
@@ -191,6 +201,7 @@ export const SL_ARITY: Record<number, number> = {
|
|
|
191
201
|
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: -1, [SLOP.RIDGED]: -1,
|
|
192
202
|
|
|
193
203
|
[SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2, [SLOP.SAMPLE_PREVIOUS]: 1,
|
|
204
|
+
[SLOP.TEXTURE_SIZE]: 0, [SLOP.SAMPLE_FETCH]: 1,
|
|
194
205
|
}
|
|
195
206
|
|
|
196
207
|
/** Name per opcode, for error messages and for the HLSL emitter's comments. */
|
|
@@ -316,6 +327,8 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
316
327
|
[SLOP.SAMPLE]: { call: "tex2D" },
|
|
317
328
|
[SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
|
|
318
329
|
[SLOP.SAMPLE_PREVIOUS]: { syntax: "tex2D(previous, uv)" },
|
|
330
|
+
[SLOP.TEXTURE_SIZE]: { call: "textureSize" },
|
|
331
|
+
[SLOP.SAMPLE_FETCH]: { call: "tex2Dfetch" },
|
|
319
332
|
}
|
|
320
333
|
|
|
321
334
|
/**
|
|
@@ -347,6 +360,7 @@ export const SL_GLSL_HINT: Record<string, string> = {
|
|
|
347
360
|
mod: "%",
|
|
348
361
|
texture: "tex2D",
|
|
349
362
|
textureLod: "tex2Dlod",
|
|
363
|
+
texelFetch: "tex2Dfetch",
|
|
350
364
|
vec2: "float2",
|
|
351
365
|
vec3: "float3",
|
|
352
366
|
vec4: "float4",
|
package/src/params.ts
CHANGED
|
@@ -21,7 +21,7 @@ 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
23
|
import type { PREVIOUS, SourceInputName } from "./ir"
|
|
24
|
-
import type { AttributeName } from "./lang/attributes"
|
|
24
|
+
import type { AttributeName, TextureAttributeName } from "./lang/attributes"
|
|
25
25
|
|
|
26
26
|
/** One function of the helper library, as its source declares it. */
|
|
27
27
|
export interface LibSignature {
|
|
@@ -55,6 +55,8 @@ const INTRINSIC: Record<string, readonly string[]> = {
|
|
|
55
55
|
lerp: ["x", "y", "s"], step: ["y", "x"], smoothstep: ["min", "max", "x"],
|
|
56
56
|
tex2D: ["s", "t"],
|
|
57
57
|
tex2Dlod: ["s", "t", "lod"],
|
|
58
|
+
textureSize: ["s"],
|
|
59
|
+
tex2Dfetch: ["s", "pixel"],
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
|
@@ -162,7 +164,9 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
|
|
|
162
164
|
turbulence: "Simplex creases layered in octaves, 1 to 4: fire, smoke, marble.",
|
|
163
165
|
ridged: "Turbulence made bright at its creases: ridges, lightning, cracks.",
|
|
164
166
|
tex2D: "The colour of texture s at the uv t, with straight alpha.",
|
|
165
|
-
tex2Dlod: "
|
|
167
|
+
tex2Dlod: "Texture s at uv t from mip level lod: 0 is full size, 1 half, and a fraction blends the two.",
|
|
168
|
+
textureSize: "The width and height of texture s in pixels, at full size, as a float2.",
|
|
169
|
+
tex2Dfetch: "One texel of texture s at pixel, unfiltered; a pixel past an edge reads the edge.",
|
|
166
170
|
sdf: "The signed distance from p to a shape, negative inside: `sdf.circle(p, r)`.",
|
|
167
171
|
ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
|
|
168
172
|
}
|
|
@@ -181,6 +185,12 @@ export const ATTRIBUTE_DOCS: Readonly<Record<AttributeName, string>> = {
|
|
|
181
185
|
Hide: "No control: the host sets this uniform from code.",
|
|
182
186
|
}
|
|
183
187
|
|
|
188
|
+
/** One line on each texture attribute, for the same tooltips. */
|
|
189
|
+
export const TEXTURE_ATTRIBUTE_DOCS: Readonly<Record<TextureAttributeName, string>> = {
|
|
190
|
+
Wrap: "How uv outside 0 to 1 reads: [Wrap(Repeat)] tiles, [Wrap(Mirror)] tiles mirrored, [Wrap(Clamp)] holds the edge.",
|
|
191
|
+
Filter: "How the texture reads between texels: [Filter(Point)] the nearest, [Filter(Linear)] a blend of four.",
|
|
192
|
+
}
|
|
193
|
+
|
|
184
194
|
/**
|
|
185
195
|
* One line on each input, for the same tooltips. In the words of no one host:
|
|
186
196
|
* the output is a UI element in OneJS and an image on a Magerie board, and the
|
package/src/serial.ts
CHANGED
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import {
|
|
12
12
|
INPUTS, STEP_INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram, intUniformProblem,
|
|
13
|
-
programVersion, type HostInputName, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
|
|
13
|
+
programVersion, samplingProblem, type HostInputName, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
|
|
14
14
|
} from "./ir"
|
|
15
15
|
import { SLOP, SL_ARITY, SL_NAME } from "./ops"
|
|
16
16
|
|
|
17
|
+
/** The ops that name a texture slot in their first immediate. */
|
|
18
|
+
const TEXTURE_OPS = new Set<number>([SLOP.SAMPLE, SLOP.SAMPLE_LOD, SLOP.TEXTURE_SIZE, SLOP.SAMPLE_FETCH])
|
|
19
|
+
|
|
17
20
|
const HOST_INPUTS: Readonly<Record<HostInputName, SLType>> = { ...INPUTS, ...STEP_INPUTS }
|
|
18
21
|
|
|
19
22
|
export interface ProgramJSON {
|
|
@@ -54,7 +57,8 @@ export function fromJSON(json: unknown): Program {
|
|
|
54
57
|
// as they are) or was edited. One that claims less than its nodes need
|
|
55
58
|
// holds something its version never had, and a version 1 file has
|
|
56
59
|
// nothing IR 3 added.
|
|
57
|
-
const
|
|
60
|
+
const textures = (j.textures ?? []).map(texture)
|
|
61
|
+
const needs = programVersion(written, j.result!, textures)
|
|
58
62
|
if (v < needs && (v >= 2 || needs >= 3)) fail(`it says IR version ${v} and holds nodes version ${needs} added; it was changed after it was written`)
|
|
59
63
|
const { nodes, result } = v < 3 ? toOperands(written, j.result!) : { nodes: written, result: j.result! }
|
|
60
64
|
nodes.forEach((n, i) => {
|
|
@@ -64,12 +68,14 @@ export function fromJSON(json: unknown): Program {
|
|
|
64
68
|
const out = nodes[result]!
|
|
65
69
|
if (out.k === "if" || out.k === "loop" || out.type !== TYPE.VEC4 || out.kind !== undefined) fail("its result is not a float4")
|
|
66
70
|
const uniforms = (j.uniforms ?? []).map(uniform)
|
|
67
|
-
const textures = (j.textures ?? []).map(texture)
|
|
68
71
|
checkCaps({ uniforms, textures })
|
|
69
72
|
for (const n of nodes) {
|
|
70
73
|
if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
|
|
74
|
+
if (n.k === "call" && TEXTURE_OPS.has(n.op) && !(n.imm !== undefined && n.imm[0]! < textures.length)) {
|
|
75
|
+
fail(`a node reads texture slot ${String(n.imm?.[0])}, which is not declared`)
|
|
76
|
+
}
|
|
71
77
|
}
|
|
72
|
-
const version = programVersion(nodes, result)
|
|
78
|
+
const version = programVersion(nodes, result, textures)
|
|
73
79
|
const hash = hashProgram(nodes, result, uniforms, textures)
|
|
74
80
|
// The same IR version and hash scheme, so the same hash; a different one
|
|
75
81
|
// means the file was edited or damaged, and its cached shader belongs to
|
|
@@ -245,7 +251,12 @@ function uniform(u: unknown, i: number): UniformDecl {
|
|
|
245
251
|
function texture(t: unknown, i: number): TextureDecl {
|
|
246
252
|
const x = (t ?? {}) as Partial<TextureDecl>
|
|
247
253
|
if (typeof x.name !== "string" || x.slot !== i) fail(`texture ${i} needs a name and slot ${i}`)
|
|
248
|
-
|
|
254
|
+
const problem = samplingProblem(x)
|
|
255
|
+
if (problem !== null) fail(`texture ${i}: ${problem}`)
|
|
256
|
+
const out: TextureDecl = { name: x.name!, slot: i }
|
|
257
|
+
if (x.wrap !== undefined) out.wrap = x.wrap
|
|
258
|
+
if (x.filter !== undefined) out.filter = x.filter
|
|
259
|
+
return out
|
|
249
260
|
}
|
|
250
261
|
|
|
251
262
|
function fail(why: string): never {
|
package/src/sl.ts
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
import { parseColor as parseHex } from "./color"
|
|
26
26
|
import {
|
|
27
27
|
Builder, INPUTS, RAMP_STOP_COMPUTED, STEP_INPUTS, SLError, TYPE, hashProgram, programVersion, truncateHeld, widthName, writtenColour,
|
|
28
|
-
type HostInputName, type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type
|
|
28
|
+
type HostInputName, type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type TextureFilter,
|
|
29
|
+
type TextureWrap, type UniformControl,
|
|
29
30
|
} from "./ir"
|
|
30
31
|
import { SLOP, SL_HLSL, SL_NAME, type SLOpCode } from "./ops"
|
|
31
32
|
import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
|
|
@@ -561,7 +562,7 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
|
|
|
561
562
|
}
|
|
562
563
|
const nodes: SLNode[] = b.nodes.slice()
|
|
563
564
|
return {
|
|
564
|
-
version: programVersion(nodes, out.ref),
|
|
565
|
+
version: programVersion(nodes, out.ref, b.textures),
|
|
565
566
|
nodes,
|
|
566
567
|
result: out.ref,
|
|
567
568
|
uniforms: b.uniforms.slice(),
|
|
@@ -1033,19 +1034,42 @@ export const uniform = {
|
|
|
1033
1034
|
}
|
|
1034
1035
|
|
|
1035
1036
|
export interface Texture {
|
|
1036
|
-
/**
|
|
1037
|
+
/**
|
|
1038
|
+
* The colour at `uv`, filtered and wrapped as the texture's own settings
|
|
1039
|
+
* say, or as `sl.texture`'s sampling says when it was given one.
|
|
1040
|
+
*/
|
|
1037
1041
|
sample(uv: Vec2): Vec4
|
|
1038
1042
|
/**
|
|
1039
1043
|
* The colour at `uv` from mip level `lod`: 0 is the full size texture, 1
|
|
1040
|
-
* half, and a fraction blends two levels
|
|
1041
|
-
* A texture with no mips has only level 0.
|
|
1044
|
+
* half, and a fraction blends the two levels either side, whatever the
|
|
1045
|
+
* texture's filter mode. A texture with no mips has only level 0.
|
|
1042
1046
|
*/
|
|
1043
1047
|
sampleLevel(uv: Vec2, lod: Num): Vec4
|
|
1048
|
+
/** The texture's width and height in pixels, at full size. `textureSize(t)` in a `.sl` file. */
|
|
1049
|
+
size(): Vec2
|
|
1050
|
+
/**
|
|
1051
|
+
* One texel, unfiltered, at a pixel: (0, 0) is the texel `uv` (0, 0)
|
|
1052
|
+
* samples. The pixel is floored and held inside the texture, so a pixel
|
|
1053
|
+
* past an edge reads the edge on every host. Full size only, and the
|
|
1054
|
+
* sampling settings do not apply. `tex2Dfetch(t, pixel)` in a `.sl` file.
|
|
1055
|
+
*/
|
|
1056
|
+
fetch(pixel: Vec2): Vec4
|
|
1044
1057
|
}
|
|
1045
1058
|
|
|
1046
|
-
|
|
1059
|
+
/**
|
|
1060
|
+
* How a program samples a texture, when it should not follow the bound
|
|
1061
|
+
* texture's own settings. Either half set, the program owns the sampler and
|
|
1062
|
+
* the other half is clamp or linear. `[Wrap(Repeat)]` and `[Filter(Point)]`
|
|
1063
|
+
* in a `.sl` file.
|
|
1064
|
+
*/
|
|
1065
|
+
export interface TextureSamplingOptions {
|
|
1066
|
+
wrap?: TextureWrap
|
|
1067
|
+
filter?: TextureFilter
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
export function texture(name: string, sampling: TextureSamplingOptions = {}): Texture {
|
|
1047
1071
|
const b = ctx()
|
|
1048
|
-
const slot = b.texture(name)
|
|
1072
|
+
const slot = b.texture(name, sampling)
|
|
1049
1073
|
return {
|
|
1050
1074
|
sample(uv: Vec2): Vec4 {
|
|
1051
1075
|
if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
|
|
@@ -1060,6 +1084,16 @@ export function texture(name: string): Texture {
|
|
|
1060
1084
|
}
|
|
1061
1085
|
return mk(b, b.call(SLOP.SAMPLE_LOD, TYPE.VEC4, [uv.ref, typeof level === "number" ? level : level.ref], [slot]), TYPE.VEC4)
|
|
1062
1086
|
},
|
|
1087
|
+
size(): Vec2 {
|
|
1088
|
+
return mk(b, b.call(SLOP.TEXTURE_SIZE, TYPE.VEC2, [], [slot]), TYPE.VEC2)
|
|
1089
|
+
},
|
|
1090
|
+
fetch(pixel: Vec2): Vec4 {
|
|
1091
|
+
if (pixel.owner !== b) throw new SLError("a value from another program cannot be used in this one")
|
|
1092
|
+
if (pixel.width !== TYPE.VEC2 || pixel.kind !== undefined) {
|
|
1093
|
+
throw new SLError(`a texel is fetched at a float2 pixel, and this is ${pixel.kind === undefined ? `a ${widthName(pixel.width)}` : `an ${pixel.kind}`}`)
|
|
1094
|
+
}
|
|
1095
|
+
return mk(b, b.call(SLOP.SAMPLE_FETCH, TYPE.VEC4, [pixel.ref], [slot]), TYPE.VEC4)
|
|
1096
|
+
},
|
|
1063
1097
|
}
|
|
1064
1098
|
}
|
|
1065
1099
|
|
package/src/tables.ts
CHANGED
|
@@ -16,9 +16,11 @@ export type { SLSurface } from "./ops"
|
|
|
16
16
|
export { DERIVED_INPUTS, INPUTS, PREVIOUS, SOURCE_INPUTS, STEP_INPUTS } from "./ir"
|
|
17
17
|
export type { DerivedInputName, HostInputName, InputName, SourceInputName, StepInputName } from "./ir"
|
|
18
18
|
export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
|
|
19
|
-
export {
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
export {
|
|
20
|
+
ATTRIBUTE_DOCS, BUILTIN_DOCS, BUILTIN_PARAMS, INPUT_DOCS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES, TEXTURE_ATTRIBUTE_DOCS,
|
|
21
|
+
} from "./params"
|
|
22
|
+
export { ATTRIBUTE_NAMES, TEXTURE_ATTRIBUTE_NAMES } from "./lang/attributes"
|
|
23
|
+
export type { AttributeName, TextureAttributeName } from "./lang/attributes"
|
|
22
24
|
export { PRELUDE_DOCS, PRELUDE_NAMES } from "./lang/prelude-source"
|
|
23
25
|
export { SL_KEYWORDS, SL_TYPES } from "./lang/words"
|
|
24
26
|
export { TYPE_WIDTH } from "./lang/ast"
|