onejs-sl 0.3.0 → 0.4.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 +15 -0
- package/README.md +38 -13
- package/goldens/goldens.json +98 -63
- package/package.json +1 -1
- package/src/body.ts +29 -6
- package/src/core.ts +1 -1
- package/src/hlsl.ts +6 -3
- package/src/ir.ts +130 -14
- package/src/lang/README.md +2 -2
- package/src/lang/builtins.ts +5 -17
- package/src/lang/check.ts +24 -11
- package/src/lang/glsl.ts +1 -2
- package/src/lang/lower.ts +23 -27
- package/src/ops.ts +4 -3
- package/src/params.ts +2 -0
- package/src/serial.ts +66 -14
- package/src/sl.ts +90 -24
- package/src/web.ts +44 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onejs-sl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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 or GLSL ES",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
package/src/body.ts
CHANGED
|
@@ -51,6 +51,14 @@ export interface BodyTarget {
|
|
|
51
51
|
* as it is, and a host that composites premultiplied multiplies on write.
|
|
52
52
|
*/
|
|
53
53
|
sample: (slot: number, uv: string) => string
|
|
54
|
+
/**
|
|
55
|
+
* A float4 sample of the texture in `slot` at `uv` from mip level `lod`, a
|
|
56
|
+
* float expression: 0 is the full size texture, 1 half, and a fraction
|
|
57
|
+
* blends two levels where the texture's filter does. Otherwise the same
|
|
58
|
+
* contract as `sample`. `tex2Dlod` in a program; HLSL's
|
|
59
|
+
* `tex2Dlod(s, float4(uv, 0, lod))` and Metal's `sample(s, uv, level(lod))`.
|
|
60
|
+
*/
|
|
61
|
+
sampleLevel: (slot: number, uv: string, lod: string) => string
|
|
54
62
|
/**
|
|
55
63
|
* `linear`: `toLinear` calls `sl_toLinear`, for a host whose target holds
|
|
56
64
|
* linear light. `gamma`: `toLinear` is the identity, decided here, so hex
|
|
@@ -100,6 +108,11 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
100
108
|
const helpers = new Set<string>()
|
|
101
109
|
const helper = (fn: string) => { helpers.add(fn); return fn }
|
|
102
110
|
/** A node at width `w`: a scalar repeated into a constructor, since HLSL refuses float3(x) and Metal x.xxx. */
|
|
111
|
+
/** A constant node's value, or null for anything computed. */
|
|
112
|
+
const constantOf = (ref: number): number[] | null => {
|
|
113
|
+
const c = p.nodes[ref]!
|
|
114
|
+
return c.k === "const" ? c.v : null
|
|
115
|
+
}
|
|
103
116
|
const splat = (ref: number, w: SLType) => (p.nodes[ref]!.type === TYPE.FLOAT && w > 1 ? ctor(w, Array(w).fill(name(ref))) : name(ref))
|
|
104
117
|
|
|
105
118
|
const expr = (n: SLNode): string => {
|
|
@@ -192,14 +205,19 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
192
205
|
case SLOP.HSV2RGB: return `${helper("sl_hsv2rgb")}(${a[0]})`
|
|
193
206
|
case SLOP.NOISE: return `${helper("sl_valueNoise")}(${a[0]})`
|
|
194
207
|
case SLOP.SIMPLEX: return `${helper("sl_simplex")}(${a[0]})`
|
|
195
|
-
case SLOP.FBM:
|
|
196
|
-
case SLOP.TURBULENCE:
|
|
197
|
-
case SLOP.RIDGED:
|
|
208
|
+
case SLOP.FBM:
|
|
209
|
+
case SLOP.TURBULENCE:
|
|
210
|
+
case SLOP.RIDGED: {
|
|
211
|
+
// The count rounded here and held to 1 to 4 by sl_fbm; a constant
|
|
212
|
+
// one is the whole number it is. fbm's kind is the one immediate.
|
|
213
|
+
const c = constantOf(n.args[1]!)
|
|
214
|
+
const count = c !== null ? String(Math.round(c[0]!)) : `int(floor(${a[1]} + 0.5))`
|
|
215
|
+
const kind = n.op === SLOP.TURBULENCE ? 2 : n.op === SLOP.RIDGED ? 3 : Math.round(imm[0] ?? 0)
|
|
216
|
+
return `${helper("sl_fbm")}(${a[0]}, ${count}, ${kind})`
|
|
217
|
+
}
|
|
198
218
|
case SLOP.SDF: {
|
|
199
219
|
const id = Math.round(imm[0] ?? 0)
|
|
200
|
-
|
|
201
|
-
const r = [imm[5] ?? 0, imm[6] ?? 0].map(lit)
|
|
202
|
-
return `${helper("sl_sdfDistance")}(${id}, ${a[0]}, float4(${q.join(", ")}), float2(${r.join(", ")}))`
|
|
220
|
+
return `${helper("sl_sdfDistance")}(${id}, ${a[0]}, ${a[1]}, ${a[2]})`
|
|
203
221
|
}
|
|
204
222
|
case SLOP.VORONOI: return `${helper("sl_voronoi")}(${a[0]})`
|
|
205
223
|
case SLOP.SAMPLE: {
|
|
@@ -207,6 +225,11 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
207
225
|
textures.add(slot)
|
|
208
226
|
return target.sample(slot, a[0]!)
|
|
209
227
|
}
|
|
228
|
+
case SLOP.SAMPLE_LOD: {
|
|
229
|
+
const slot = Math.round(imm[0] ?? 0)
|
|
230
|
+
textures.add(slot)
|
|
231
|
+
return target.sampleLevel(slot, a[0]!, a[1]!)
|
|
232
|
+
}
|
|
210
233
|
|
|
211
234
|
default:
|
|
212
235
|
throw new SLError(
|
package/src/core.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export * as sl from "./sl"
|
|
11
11
|
export type { Float, Vec2, Vec3, Vec4, Num, ProgramInputs, Texture } from "./sl"
|
|
12
12
|
export {
|
|
13
|
-
TYPE, INPUTS, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, inputsUsed,
|
|
13
|
+
TYPE, INPUTS, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, SL_HASH_VERSION, inputsUsed,
|
|
14
14
|
CONTROL_FIELDS, controlProblem,
|
|
15
15
|
} from "./ir"
|
|
16
16
|
export type {
|
package/src/hlsl.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* next time it is generated.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { SLError, type Program } from "./ir"
|
|
18
|
+
import { SLError, SL_HASH_VERSION, type Program } from "./ir"
|
|
19
19
|
import { emitBody, lit, type BodyTarget } from "./body"
|
|
20
20
|
|
|
21
21
|
/** The property name a uniform gets. Prefixed so it cannot collide with ours. */
|
|
@@ -74,7 +74,9 @@ export function emitShader(p: Program, options: EmitOptions = {}): string {
|
|
|
74
74
|
//
|
|
75
75
|
// Source of truth is the program this was emitted from; edits here are lost the
|
|
76
76
|
// next time it is generated. The name carries the program hash, which is how the
|
|
77
|
-
// runtime pairs the two. See Specs/SHADER_LANG.md section 6.
|
|
77
|
+
// runtime pairs the two. See Specs/SHADER_LANG.md section 6. The scheme that
|
|
78
|
+
// hash was made under, which a build compares with the apps' manifests:
|
|
79
|
+
// SL_HASH_VERSION ${SL_HASH_VERSION}
|
|
78
80
|
Shader "${name}"
|
|
79
81
|
{
|
|
80
82
|
Properties
|
|
@@ -123,7 +125,7 @@ ${body}
|
|
|
123
125
|
|
|
124
126
|
/**
|
|
125
127
|
* The frame's side of the body: OneJS's names for the inputs, a uniform's
|
|
126
|
-
* material property, and `tex2D` on the slot's sampler. `sl_toLinear` stays a
|
|
128
|
+
* material property, and `tex2D` and `tex2Dlod` on the slot's sampler. `sl_toLinear` stays a
|
|
127
129
|
* call because `SLCommon.cginc` decides Gamma or Linear per project, at the
|
|
128
130
|
* shader compile, so this target always emits it.
|
|
129
131
|
*/
|
|
@@ -148,6 +150,7 @@ function unityTarget(p: Program): BodyTarget {
|
|
|
148
150
|
},
|
|
149
151
|
uniform: (slot) => uniformProperty(p.uniforms[slot]!.name),
|
|
150
152
|
sample: (slot, uv) => `tex2D(_Tex${slot}, ${uv})`,
|
|
153
|
+
sampleLevel: (slot, uv, lod) => `tex2Dlod(_Tex${slot}, float4(${uv}, 0.0, ${lod}))`,
|
|
151
154
|
colour: "linear",
|
|
152
155
|
indent: " ",
|
|
153
156
|
}
|
package/src/ir.ts
CHANGED
|
@@ -171,7 +171,10 @@ export interface TextureDecl {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
export interface Program {
|
|
174
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* The `SL_IR_VERSION` this program's nodes mean what they mean under: the
|
|
176
|
+
* lowest one that has every node it holds. See `programVersion`.
|
|
177
|
+
*/
|
|
175
178
|
version: number
|
|
176
179
|
nodes: SLNode[]
|
|
177
180
|
/** Must be VEC4: a program produces a colour. */
|
|
@@ -210,6 +213,24 @@ export function checkCaps(p: Pick<Program, "uniforms" | "textures">): void {
|
|
|
210
213
|
if (p.textures.length > TEXTURE_SLOTS) throw new SLError(tooManyTextures(p.textures.length, "program"))
|
|
211
214
|
}
|
|
212
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The colour as written behind a colour value: the operand of the `TO_LINEAR`
|
|
218
|
+
* that reading a hex or a colour uniform applied, or null for a value computed
|
|
219
|
+
* in the working space. What a ramp's stop has to be, since a ramp blends its
|
|
220
|
+
* stops as written (`sl.ramp`).
|
|
221
|
+
*/
|
|
222
|
+
export function writtenColour(nodes: readonly SLNode[], ref: NodeRef): NodeRef | null {
|
|
223
|
+
const n = nodes[ref]!
|
|
224
|
+
if (n.k !== "call" || n.op !== SLOP.TO_LINEAR || (n.type !== TYPE.VEC3 && n.type !== TYPE.VEC4)) return null
|
|
225
|
+
return n.args[0]!
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Why a computed value is not a ramp's stop, in one wording for both surfaces. */
|
|
229
|
+
export const RAMP_STOP_COMPUTED =
|
|
230
|
+
"a ramp's stop is a colour as written: a hex like #ff8040, or a uniform or const that holds one. " +
|
|
231
|
+
"This one is computed, and a ramp blends its stops as written before it converts them, so it has " +
|
|
232
|
+
"no written colour to blend. Blend computed colours with lerp instead"
|
|
233
|
+
|
|
213
234
|
/** Operations a single program may hold. Generous: a guard against a runaway builder, not a budget. */
|
|
214
235
|
export const MAX_NODES = 4096
|
|
215
236
|
|
|
@@ -402,6 +423,13 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
402
423
|
// digests, so two graphs that compute the same thing agree however they were
|
|
403
424
|
// assembled. It also ignores nodes not reachable from the result, which is
|
|
404
425
|
// correct: dead nodes generate no shader code.
|
|
426
|
+
//
|
|
427
|
+
// NORMALISED, so two ways of writing one computation agree (hash version 2).
|
|
428
|
+
// A commutative op's operands are digested in sorted order, so `a + b` is
|
|
429
|
+
// `b + a`; and a swizzle of a constant is digested as the constant it picks,
|
|
430
|
+
// so `8 * uv` (8 broadcast by a swizzle) is `uv * 8` (a float2 constant).
|
|
431
|
+
// Only where every backend computes the same bits either way: min and max
|
|
432
|
+
// are left out, since which of -0 and +0 they return depends on the order.
|
|
405
433
|
const digest = new Map<NodeRef, string>()
|
|
406
434
|
|
|
407
435
|
const of = (ref: NodeRef): string => {
|
|
@@ -417,8 +445,19 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
417
445
|
// produce different shaders and must not share a hash. The names go
|
|
418
446
|
// in separately below.
|
|
419
447
|
case "uniform": body = `u:${n.slot}:${n.type}`; break
|
|
420
|
-
case "swizzle":
|
|
421
|
-
|
|
448
|
+
case "swizzle": {
|
|
449
|
+
const src = nodes[n.src]!
|
|
450
|
+
body = src.k === "const"
|
|
451
|
+
? `c:${n.type}:${n.chans.map((c) => fixed(src.v[c]!)).join(",")}`
|
|
452
|
+
: `s:${of(n.src)}:${n.chans.join("")}`
|
|
453
|
+
break
|
|
454
|
+
}
|
|
455
|
+
case "call": {
|
|
456
|
+
const args = n.args.map(of)
|
|
457
|
+
if (COMMUTATIVE.has(n.op)) args.sort()
|
|
458
|
+
body = `f:${n.op}:${args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}`
|
|
459
|
+
break
|
|
460
|
+
}
|
|
422
461
|
}
|
|
423
462
|
const d = fnv1a(body + "|" + n.type)
|
|
424
463
|
digest.set(ref, d)
|
|
@@ -426,12 +465,15 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
426
465
|
}
|
|
427
466
|
|
|
428
467
|
// The IR version is in the hash, so a change to what an opcode means in the
|
|
429
|
-
// IR changes
|
|
430
|
-
// serving the old maths.
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
// library
|
|
434
|
-
|
|
468
|
+
// IR changes the hash of every program using it, and every cache keyed by
|
|
469
|
+
// one recompiles rather than serving the old maths. It is the PROGRAM's
|
|
470
|
+
// version, the lowest that has its nodes, so a version that adds a node
|
|
471
|
+
// leaves every program without one on the hash it had. A change to the
|
|
472
|
+
// helper library alone (a noise's arithmetic, say) is not one: every host
|
|
473
|
+
// includes the library's text when it compiles, and none caches a compiled
|
|
474
|
+
// program by hash across a library update, so the new text reaches every
|
|
475
|
+
// shader without a bump.
|
|
476
|
+
const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes)}`, of(result)]
|
|
435
477
|
for (const u of uniforms) parts.push(`U:${u.name}:${u.type}:${u.value.map(fixed).join(",")}`)
|
|
436
478
|
for (const t of textures) parts.push(`T:${t.name}:${t.slot}`)
|
|
437
479
|
return fnv1a(parts.join("|"))
|
|
@@ -449,8 +491,79 @@ export function controlOf(c: UniformControl): UniformControl {
|
|
|
449
491
|
return out
|
|
450
492
|
}
|
|
451
493
|
|
|
452
|
-
/**
|
|
453
|
-
|
|
494
|
+
/**
|
|
495
|
+
* The lowest `SL_IR_VERSION` that has every node here, and never below 2.
|
|
496
|
+
*
|
|
497
|
+
* A version that adds a node, or a form of one, raises only the programs that
|
|
498
|
+
* hold it. Everything else keeps the version, the hash and the JSON it had, so
|
|
499
|
+
* a program recorded before the bump still finds its shader, and a reader that
|
|
500
|
+
* predates the bump still reads it. Every node counts, used or not: the JSON
|
|
501
|
+
* carries them all, and an older reader has to refuse by the version rather
|
|
502
|
+
* than halfway through the nodes.
|
|
503
|
+
*/
|
|
504
|
+
export function programVersion(nodes: readonly SLNode[]): number {
|
|
505
|
+
let v = 2
|
|
506
|
+
for (const n of nodes) v = Math.max(v, nodeVersion(n))
|
|
507
|
+
return v
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The version that added this node's form. An sdf or octave call with one
|
|
512
|
+
* operand is the form before IR 3, which only a file an older compiler wrote
|
|
513
|
+
* holds, and `fromJSON` moves it to the form IR 3 has.
|
|
514
|
+
*/
|
|
515
|
+
function nodeVersion(n: SLNode): number {
|
|
516
|
+
if (n.k !== "call") return 2
|
|
517
|
+
switch (n.op) {
|
|
518
|
+
case SLOP.SAMPLE_LOD: return 3
|
|
519
|
+
case SLOP.SDF:
|
|
520
|
+
case SLOP.FBM:
|
|
521
|
+
case SLOP.TURBULENCE:
|
|
522
|
+
case SLOP.RIDGED:
|
|
523
|
+
return n.args.length > 1 ? 3 : 2
|
|
524
|
+
default: return 2
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* What is wrong with a call's form, for the ops whose operand list alone does
|
|
530
|
+
* not say it, or null. The builder only makes the right ones; this is for a
|
|
531
|
+
* program read from JSON, which an emitter would otherwise print as something
|
|
532
|
+
* else.
|
|
533
|
+
*
|
|
534
|
+
* SDF the point, a float4 and a float2 holding the six shape parameters,
|
|
535
|
+
* the shape id the one immediate
|
|
536
|
+
* FBM, TURBULENCE, RIDGED the point and the octave count, FBM's kind the
|
|
537
|
+
* one immediate
|
|
538
|
+
*/
|
|
539
|
+
export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
|
|
540
|
+
const widths = n.args.map((a) => nodes[a]!.type).join(",")
|
|
541
|
+
switch (n.op) {
|
|
542
|
+
case SLOP.SDF:
|
|
543
|
+
if (widths === "2,4,2" && n.imm?.length === 1) return null
|
|
544
|
+
return "an sdf takes a float2, a float4 and a float2, and its shape as the one immediate"
|
|
545
|
+
case SLOP.FBM:
|
|
546
|
+
case SLOP.TURBULENCE:
|
|
547
|
+
case SLOP.RIDGED: {
|
|
548
|
+
const rest = n.op === SLOP.FBM ? 1 : 0
|
|
549
|
+
if (widths === "2,1" && (n.imm?.length ?? 0) === rest) return null
|
|
550
|
+
return `${SL_NAME[n.op]} takes a float2 and its octave count, a float`
|
|
551
|
+
}
|
|
552
|
+
default:
|
|
553
|
+
return null
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** The ops whose two operands the hash takes in either order: exact in IEEE arithmetic on every backend. */
|
|
558
|
+
const COMMUTATIVE = new Set<number>([SLOP.ADD, SLOP.MUL, SLOP.DOT, SLOP.DISTANCE])
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Bumped when the hashing scheme changes, which invalidates generated shaders.
|
|
562
|
+
*
|
|
563
|
+
* 1 the Merkle hash over the reachable graph
|
|
564
|
+
* 2 normalised: commutative operands in either order, a swizzle of a constant as the constant
|
|
565
|
+
*/
|
|
566
|
+
export const SL_HASH_VERSION = 2
|
|
454
567
|
|
|
455
568
|
/**
|
|
456
569
|
* Bumped whenever the IR changes in a way a reader has to know about: a new
|
|
@@ -467,13 +580,16 @@ export const SL_HASH_VERSION = 1
|
|
|
467
580
|
* records it again, for no gain.
|
|
468
581
|
*
|
|
469
582
|
* A reader accepts every version up to its own and refuses a newer one with a
|
|
470
|
-
* message naming both, the rule the particle wire and fx follow.
|
|
471
|
-
*
|
|
583
|
+
* message naming both, the rule the particle wire and fx follow. A program's
|
|
584
|
+
* version is the lowest that has its nodes (`programVersion`), and that is what
|
|
585
|
+
* its hash carries, so a bump rehashes only the programs using what it added.
|
|
472
586
|
*
|
|
473
587
|
* 1 the first versioned IR
|
|
474
588
|
* 2 an SDF call carries up to six shape parameters, not four (#129)
|
|
589
|
+
* 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
|
|
590
|
+
* operands, where they were immediates
|
|
475
591
|
*/
|
|
476
|
-
export const SL_IR_VERSION =
|
|
592
|
+
export const SL_IR_VERSION = 3
|
|
477
593
|
|
|
478
594
|
function fnv1a(s: string): string {
|
|
479
595
|
let h = 0x811c9dc5
|
package/src/lang/README.md
CHANGED
|
@@ -152,13 +152,13 @@ one the local does not have, is refused.
|
|
|
152
152
|
|
|
153
153
|
## Not yet spellable
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
Two things the EDSL or the opcode table has and a file cannot say. Each is a
|
|
156
156
|
recorded gap, not an oversight, and each has its own error message rather than
|
|
157
157
|
"unknown identifier".
|
|
158
158
|
|
|
159
159
|
| | Why |
|
|
160
160
|
|---|---|
|
|
161
|
-
| `rgb2hsv
|
|
161
|
+
| `rgb2hsv` | The opcode is numbered and **no backend implements it**; every emitter throws on it. |
|
|
162
162
|
| `fbm`'s simplex base | `sl.fbm(p, octaves, "simplex")` picks the base with a string, and the language has no strings. `fbm(p, octaves)` is the value base, and `turbulence` and `ridged` are the simplex family. |
|
|
163
163
|
|
|
164
164
|
A `.sl` file and the EDSL are held to the same caps, `UNIFORM_SLOTS` (16) and
|
package/src/lang/builtins.ts
CHANGED
|
@@ -125,12 +125,10 @@ value(SLOP.LUMINANCE, 1, 1, (a) => sl.luminance(colourOperand("luminance", a[0]!
|
|
|
125
125
|
value(SLOP.NOISE, 1, 1, (a) => sl.noise(point("noise", a[0]!) as never))
|
|
126
126
|
value(SLOP.SIMPLEX, 1, 1, (a) => sl.simplex(point("simplex", a[0]!) as never))
|
|
127
127
|
value(SLOP.VORONOI, 1, 1, (a) => sl.voronoi(point("voronoi", a[0]!) as never))
|
|
128
|
-
// The octave count is
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
value(SLOP.
|
|
132
|
-
value(SLOP.TURBULENCE, 1, 2, (a) => sl.turbulence(point("turbulence", a[0]!) as never, octaves("turbulence", a[1])))
|
|
133
|
-
value(SLOP.RIDGED, 1, 2, (a) => sl.ridged(point("ridged", a[0]!) as never, octaves("ridged", a[1])))
|
|
128
|
+
// The octave count is 3 when left out, and may be a value (`sl.fbm` says how).
|
|
129
|
+
value(SLOP.FBM, 1, 2, (a) => sl.fbm(point("fbm", a[0]!) as never, a[1] ?? 3))
|
|
130
|
+
value(SLOP.TURBULENCE, 1, 2, (a) => sl.turbulence(point("turbulence", a[0]!) as never, a[1] ?? 3))
|
|
131
|
+
value(SLOP.RIDGED, 1, 2, (a) => sl.ridged(point("ridged", a[0]!) as never, a[1] ?? 3))
|
|
134
132
|
|
|
135
133
|
/** Every field is sampled at a float2. */
|
|
136
134
|
function point(fn: string, v: Num): Val {
|
|
@@ -138,20 +136,10 @@ function point(fn: string, v: Num): Val {
|
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
special(SLOP.SAMPLE, 2, 2)
|
|
139
|
+
special(SLOP.SAMPLE_LOD, 3, 3)
|
|
141
140
|
special(SLOP.SDF, 1, 7)
|
|
142
141
|
special(SLOP.RAMP, 3, 64)
|
|
143
142
|
|
|
144
|
-
function octaves(fn: string, v: Num | undefined): number {
|
|
145
|
-
if (v === undefined) return 3
|
|
146
|
-
if (typeof v !== "number") {
|
|
147
|
-
throw new SLError(
|
|
148
|
-
`${fn}'s octave count is part of the operation, so it has to be a constant. ` +
|
|
149
|
-
`A uniform or a computed value cannot change how many octaves the shader runs.`,
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
return v
|
|
153
|
-
}
|
|
154
|
-
|
|
155
143
|
export const BUILTINS: Readonly<Record<string, Builtin>> = table
|
|
156
144
|
|
|
157
145
|
/**
|
package/src/lang/check.ts
CHANGED
|
@@ -49,11 +49,15 @@ const DERIVED_NAMES = new Set(Object.keys(DERIVED_INPUTS))
|
|
|
49
49
|
* The GLSL spellings whose HLSL name means the same wherever it is written, so
|
|
50
50
|
* the hint can be a one click fix (`Specs/SL_NEXT.md` 5, Decision 1 A). The
|
|
51
51
|
* rest of `SL_GLSL_HINT` gets the hint alone: `mod` floors where `%` truncates,
|
|
52
|
-
* `ivec2` truncates where `float2` does not, `atan` is `atan2` only with two
|
|
53
|
-
* arguments (handled at the call)
|
|
54
|
-
* implemented yet.
|
|
52
|
+
* `ivec2` truncates where `float2` does not, and `atan` is `atan2` only with two
|
|
53
|
+
* arguments (handled at the call).
|
|
55
54
|
*/
|
|
56
|
-
const GLSL_RENAMES = new Set([
|
|
55
|
+
const GLSL_RENAMES = new Set([
|
|
56
|
+
"mix", "fract", "texture", "textureLod", "vec2", "vec3", "vec4", "gl_FragCoord", "iTime", "iResolution",
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
/** The builtins whose first argument is a texture, which is not a value anywhere else. */
|
|
60
|
+
const SAMPLES = new Set(["tex2D", "tex2Dlod"])
|
|
57
61
|
|
|
58
62
|
const rename = (from: string, to: string): SLFix => ({ title: `Replace ${from} with ${to}`, replacement: to })
|
|
59
63
|
|
|
@@ -427,7 +431,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
427
431
|
if (textures.has(e.name)) {
|
|
428
432
|
fail(
|
|
429
433
|
`"${e.name}" is a texture, and a texture is only ever the first argument of ` +
|
|
430
|
-
`tex2D. Write \`tex2D(${e.name}, uv)\``,
|
|
434
|
+
`tex2D or tex2Dlod. Write \`tex2D(${e.name}, uv)\``,
|
|
431
435
|
e.pos, e.name.length,
|
|
432
436
|
)
|
|
433
437
|
}
|
|
@@ -458,9 +462,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
458
462
|
// Only reached while collecting: the call was refused, and its
|
|
459
463
|
// arguments are separate questions, so a mistake inside one is
|
|
460
464
|
// not hidden by the call's own. `fract(uv * wrap)` is two
|
|
461
|
-
// mistakes.
|
|
462
|
-
//
|
|
463
|
-
const tex = e.callee.k === "ident" && e.callee.name
|
|
465
|
+
// mistakes. A sample's first argument is the texture, which is
|
|
466
|
+
// not a value.
|
|
467
|
+
const tex = e.callee.k === "ident" && SAMPLES.has(e.callee.name)
|
|
464
468
|
for (const a of tex ? e.args.slice(1) : e.args) attempt(() => checkExpr(fn, a, scope))
|
|
465
469
|
return
|
|
466
470
|
}
|
|
@@ -529,13 +533,22 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
529
533
|
|
|
530
534
|
const builtin = BUILTINS[n]
|
|
531
535
|
if (builtin !== undefined) {
|
|
532
|
-
if (n
|
|
536
|
+
if (SAMPLES.has(n)) {
|
|
533
537
|
const [tex, ...rest] = e.args
|
|
538
|
+
// HLSL's own tex2Dlod packs the level into a float4 with the uv.
|
|
539
|
+
if (n === "tex2Dlod" && e.args.length === 2) {
|
|
540
|
+
fail(
|
|
541
|
+
"tex2Dlod takes the uv and the mip level separately, `tex2Dlod(t, uv, lod)`, " +
|
|
542
|
+
"rather than HLSL's float4(uv, 0, lod)",
|
|
543
|
+
e.pos,
|
|
544
|
+
)
|
|
545
|
+
}
|
|
534
546
|
arity(e, n, builtin.min, builtin.max)
|
|
535
547
|
if (tex === undefined || tex.k !== "ident" || !textures.has(tex.name)) {
|
|
548
|
+
const lod = n === "tex2Dlod" ? ", 0" : ""
|
|
536
549
|
fail(
|
|
537
|
-
|
|
538
|
-
`then
|
|
550
|
+
`${n} samples a texture declared in this file, as in \`texture2D grain;\` ` +
|
|
551
|
+
`then \`${n}(grain, uv${lod})\``,
|
|
539
552
|
(tex ?? e).pos,
|
|
540
553
|
)
|
|
541
554
|
}
|
package/src/lang/glsl.ts
CHANGED
|
@@ -75,7 +75,7 @@ const GLSL_TYPES = new Set(["float", "int", "uint", "bool", "vec2", "vec3", "vec
|
|
|
75
75
|
/** A straight rename, the meaning unchanged. */
|
|
76
76
|
const RENAME: Record<string, string> = {
|
|
77
77
|
vec2: "float2", vec3: "float3", vec4: "float4", mix: "lerp", fract: "frac",
|
|
78
|
-
texture: "tex2D", texture2D: "tex2D", iTime: "time", iGlobalTime: "time", gl_FragCoord: "fragCoord",
|
|
78
|
+
texture: "tex2D", texture2D: "tex2D", textureLod: "tex2Dlod", iTime: "time", iGlobalTime: "time", gl_FragCoord: "fragCoord",
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** A rename that changes what the value can hold, so it is said. */
|
|
@@ -101,7 +101,6 @@ const MISSING: Record<string, string> = {
|
|
|
101
101
|
iChannelTime: "a texture has no time of its own",
|
|
102
102
|
iChannelResolution: "a texture's size cannot be read yet",
|
|
103
103
|
iSampleRate: "there is no audio",
|
|
104
|
-
textureLod: "tex2Dlod is not implemented yet; tex2D samples with the texture's own filtering",
|
|
105
104
|
texelFetch: "a texture cannot be read without filtering yet",
|
|
106
105
|
textureSize: "a texture's size cannot be read yet",
|
|
107
106
|
dFdx: "there are no derivatives", dFdy: "there are no derivatives", fwidth: "there are no derivatives",
|
package/src/lang/lower.ts
CHANGED
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
* than an optimisation.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
controlProblem, DERIVED_INPUTS, INPUTS, RAMP_STOP_COMPUTED, writtenColour, type Program, type SLType, type UniformControl,
|
|
37
|
+
} from "../ir"
|
|
36
38
|
import type { SlSdfKind } from "../shapes"
|
|
37
39
|
import * as sl from "../sl"
|
|
38
40
|
import type { Num, Val } from "../sl"
|
|
@@ -602,19 +604,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
602
604
|
if (typeof p === "number" || p.width !== 2) {
|
|
603
605
|
fail(`sdf.${shape} measures the distance to a point, so it takes a float2`, e.args[0]!.pos)
|
|
604
606
|
}
|
|
605
|
-
const params
|
|
606
|
-
for (const a of e.args.slice(1)) {
|
|
607
|
-
const v = lowerExpr(a, scope)
|
|
608
|
-
if (typeof v !== "number") {
|
|
609
|
-
fail(
|
|
610
|
-
`sdf.${shape}'s shape parameters are part of the operation, so they have to ` +
|
|
611
|
-
`be constants. A size that changes belongs on the point: scale or offset it ` +
|
|
612
|
-
`before the call`,
|
|
613
|
-
a.pos,
|
|
614
|
-
)
|
|
615
|
-
}
|
|
616
|
-
params.push(v)
|
|
617
|
-
}
|
|
607
|
+
const params = e.args.slice(1).map((a) => lowerExpr(a, scope))
|
|
618
608
|
return at(e.pos, () => sl.sdf(shape, p as never, params))
|
|
619
609
|
}
|
|
620
610
|
|
|
@@ -624,29 +614,35 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
624
614
|
return construct(n as TypeName, e.args.map((a) => lowerExpr(a, scope)), e.pos)
|
|
625
615
|
}
|
|
626
616
|
|
|
627
|
-
if (n === "tex2D") {
|
|
617
|
+
if (n === "tex2D" || n === "tex2Dlod") {
|
|
628
618
|
const texName = (e.args[0] as Extract<Expr, { k: "ident" }>).name
|
|
629
619
|
const tex = samplers.get(texName)!
|
|
630
620
|
const uv = lowerExpr(e.args[1]!, scope)
|
|
631
621
|
if (typeof uv === "number" || uv.width !== 2) {
|
|
632
|
-
fail(
|
|
622
|
+
fail(`${n} samples at a float2`, e.args[1]!.pos)
|
|
633
623
|
}
|
|
634
|
-
return at(e.pos, () => tex.sample(uv as never) as unknown as Val)
|
|
624
|
+
if (n === "tex2D") return at(e.pos, () => tex.sample(uv as never) as unknown as Val)
|
|
625
|
+
const lod = lowerExpr(e.args[2]!, scope)
|
|
626
|
+
if (typeof lod !== "number" && lod.width !== 1) {
|
|
627
|
+
fail(`a mip level is one number, and this is a ${widthType(lod.width)}`, e.args[2]!.pos)
|
|
628
|
+
}
|
|
629
|
+
return at(e.pos, () => tex.sampleLevel(uv as never, lod) as unknown as Val)
|
|
635
630
|
}
|
|
636
631
|
|
|
637
632
|
if (n === "ramp") {
|
|
638
633
|
const t = lowerExpr(e.args[0]!, scope)
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
)
|
|
634
|
+
// A hex stays the string it was, so a ramp of hexes is the program
|
|
635
|
+
// it always was. Anything else is a value, which has to be a colour
|
|
636
|
+
// as written (`sl.ramp` says why), a colour uniform say.
|
|
637
|
+
const stops = e.args.slice(1).map((a) => {
|
|
638
|
+
if (a.k === "hex") return a.hex
|
|
639
|
+
const v = lowerExpr(a, scope)
|
|
640
|
+
if (typeof v === "number" || v.width < 3) {
|
|
641
|
+
fail(`a ramp's stop is a colour, a float3 or a float4, and this is a ${typeof v === "number" ? "float" : widthType(v.width)}`, a.pos)
|
|
647
642
|
}
|
|
648
|
-
|
|
649
|
-
|
|
643
|
+
if (writtenColour(v.owner.nodes, v.ref) === null) fail(RAMP_STOP_COMPUTED, a.pos)
|
|
644
|
+
return v as never
|
|
645
|
+
})
|
|
650
646
|
return at(e.pos, () => sl.ramp(t, stops) as unknown as Val)
|
|
651
647
|
}
|
|
652
648
|
|
package/src/ops.ts
CHANGED
|
@@ -156,8 +156,10 @@ export const SL_ARITY: Record<number, number> = {
|
|
|
156
156
|
|
|
157
157
|
[SLOP.RAMP]: -1, [SLOP.HSV2RGB]: 1, [SLOP.RGB2HSV]: 1, [SLOP.LUMINANCE]: 1, [SLOP.TO_LINEAR]: 1,
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
// An SDF takes the point, or the point and its parameters as operands; an
|
|
160
|
+
// octave op the point, or the point and its count (`formProblem` in ir.ts).
|
|
161
|
+
[SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: -1, [SLOP.SDF]: -1,
|
|
162
|
+
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: -1, [SLOP.RIDGED]: -1,
|
|
161
163
|
|
|
162
164
|
[SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2,
|
|
163
165
|
}
|
|
@@ -282,7 +284,6 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
282
284
|
*/
|
|
283
285
|
export const SL_UNIMPLEMENTED: Record<number, string> = {
|
|
284
286
|
[SLOP.RGB2HSV]: "no backend has a case for it yet",
|
|
285
|
-
[SLOP.SAMPLE_LOD]: "no backend samples at an explicit LOD yet; use tex2D",
|
|
286
287
|
[SLOP.REMAP]: "the surface form is a macro over arithmetic, so the opcode is unused",
|
|
287
288
|
}
|
|
288
289
|
|
package/src/params.ts
CHANGED
|
@@ -54,6 +54,7 @@ const INTRINSIC: Record<string, readonly string[]> = {
|
|
|
54
54
|
distance: ["x", "y"], dot: ["x", "y"], cross: ["x", "y"], reflect: ["i", "n"],
|
|
55
55
|
lerp: ["x", "y", "s"], step: ["y", "x"], smoothstep: ["min", "max", "x"],
|
|
56
56
|
tex2D: ["s", "t"],
|
|
57
|
+
tex2Dlod: ["s", "t", "lod"],
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -161,6 +162,7 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
|
|
|
161
162
|
turbulence: "Simplex creases layered in octaves, 1 to 4: fire, smoke, marble.",
|
|
162
163
|
ridged: "Turbulence made bright at its creases: ridges, lightning, cracks.",
|
|
163
164
|
tex2D: "The colour of texture s at the uv t, with straight alpha.",
|
|
165
|
+
tex2Dlod: "The colour of texture s at the uv t from mip level lod: 0 is the full size, 1 half, and so on.",
|
|
164
166
|
sdf: "The signed distance from p to a shape, negative inside: `sdf.circle(p, r)`.",
|
|
165
167
|
ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
|
|
166
168
|
}
|