onejs-sl 0.1.1 → 0.1.2

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/src/body.ts CHANGED
@@ -11,7 +11,9 @@
11
11
  * The subset is HLSL's spelling (`float2`, `lerp`, `frac`, `fmod`, `atan2`,
12
12
  * `saturate`), which Metal takes through a host's compatibility defines. `fmod`
13
13
  * is the same truncating remainder in both. Nothing here uses `mul`, `static`,
14
- * derivatives, or an overload the library does not resolve by width.
14
+ * derivatives, or an overload the library does not resolve by width. The
15
+ * library text `emitLibrary` prints is the same subset, translated from
16
+ * `lib/*.hlsl` by `lib/translate.ts`.
15
17
  *
16
18
  * One local per reachable node, in order, named `n<index>` and unreadable on
17
19
  * purpose: generated code that looks hand written invites hand editing, and a
@@ -20,6 +22,8 @@
20
22
  */
21
23
 
22
24
  import { SLError, TYPE, type InputName, type Program, type SLNode, type SLType } from "./ir"
25
+ import { libClosure, LIB_FUNCTIONS } from "./lib"
26
+ import { LIB_HLSL } from "./lib/hlsl"
23
27
  import { SLOP } from "./ops"
24
28
 
25
29
  export interface BodyTarget {
@@ -48,7 +52,7 @@ export interface Body {
48
52
  uniforms: number[]
49
53
  /** Texture slots the body samples, ascending. */
50
54
  textures: number[]
51
- /** Library functions the body calls, sorted: `sl_fbm`, `sl_sdfDistance`, ... */
55
+ /** Library functions the body calls, sorted: `sl_fbm`, `sl_sdfDistance`, ... `emitLibrary` prints them. */
52
56
  helpers: string[]
53
57
  }
54
58
  }
@@ -77,6 +81,8 @@ export function emitBody(p: Program, target: BodyTarget): Body {
77
81
  const textures = new Set<number>()
78
82
  const helpers = new Set<string>()
79
83
  const helper = (fn: string) => { helpers.add(fn); return fn }
84
+ /** A node at width `w`: a scalar repeated into a constructor, since HLSL refuses float3(x) and Metal x.xxx. */
85
+ const splat = (ref: number, w: SLType) => (p.nodes[ref]!.type === TYPE.FLOAT && w > 1 ? ctor(w, Array(w).fill(name(ref))) : name(ref))
80
86
 
81
87
  const expr = (n: SLNode): string => {
82
88
  switch (n.k) {
@@ -92,6 +98,8 @@ export function emitBody(p: Program, target: BodyTarget): Body {
92
98
  return n.type === TYPE.VEC4 ? v : `${v}.${SWZ.slice(0, n.type)}`
93
99
  }
94
100
  case "swizzle":
101
+ // Metal has no swizzle of a scalar, so a scalar widens by constructor.
102
+ if (p.nodes[n.src]!.type === TYPE.FLOAT) return ctor(n.type, n.chans.map(() => name(n.src)))
95
103
  return `${name(n.src)}.${n.chans.map((c) => SWZ[c]).join("")}`
96
104
  case "call":
97
105
  return call(n)
@@ -100,6 +108,14 @@ export function emitBody(p: Program, target: BodyTarget): Body {
100
108
 
101
109
  const call = (n: Extract<SLNode, { k: "call" }>): string => {
102
110
  const a = n.args.map(name)
111
+ const t = n.type
112
+ // Every argument of an element wise intrinsic at the result's width,
113
+ // and every literal at its exact type. HLSL broadcasts a scalar and
114
+ // converts an int; Metal finds max(float2, float) has no overload and
115
+ // max(float, 0) is ambiguous, so the shared subset spells both out.
116
+ const s = n.args.map((r) => splat(r, t))
117
+ const k = (v: number, w: SLType) => ctor(w, Array(w).fill(lit(v)))
118
+ const w0 = n.args.length > 0 ? p.nodes[n.args[0]!]!.type : TYPE.FLOAT
103
119
  const imm = n.imm ?? []
104
120
  switch (n.op) {
105
121
  case SLOP.COMPOSE: return ctor(n.type, a)
@@ -108,50 +124,52 @@ export function emitBody(p: Program, target: BodyTarget): Body {
108
124
  case SLOP.SUB: return `(${a[0]} - ${a[1]})`
109
125
  case SLOP.MUL: return `(${a[0]} * ${a[1]})`
110
126
  case SLOP.DIV: return `(${a[0]} / ${a[1]})`
111
- case SLOP.MOD: return `fmod(${a[0]}, ${a[1]})`
127
+ case SLOP.MOD: return `fmod(${s[0]}, ${s[1]})`
112
128
  // abs on the base, matching the VM. pow of a negative base is
113
129
  // undefined in HLSL and the backends must be undefined in the same
114
130
  // direction.
115
- case SLOP.POW: return `pow(abs(${a[0]}), ${a[1]})`
131
+ case SLOP.POW: return `pow(abs(${s[0]}), ${s[1]})`
116
132
  case SLOP.NEG: return `(-${a[0]})`
117
133
  case SLOP.RECIP: return `(1.0 / ${a[0]})`
118
134
 
119
135
  case SLOP.SIN: return `sin(${a[0]})`
120
136
  case SLOP.COS: return `cos(${a[0]})`
121
137
  case SLOP.TAN: return `tan(${a[0]})`
122
- case SLOP.ASIN: return `asin(clamp(${a[0]}, -1, 1))`
123
- case SLOP.ACOS: return `acos(clamp(${a[0]}, -1, 1))`
124
- case SLOP.ATAN2: return `atan2(${a[0]}, ${a[1]})`
138
+ case SLOP.ASIN: return `asin(clamp(${a[0]}, ${k(-1, t)}, ${k(1, t)}))`
139
+ case SLOP.ACOS: return `acos(clamp(${a[0]}, ${k(-1, t)}, ${k(1, t)}))`
140
+ case SLOP.ATAN2: return `atan2(${s[0]}, ${s[1]})`
125
141
  case SLOP.EXP: return `exp(${a[0]})`
126
- case SLOP.LOG: return `log(max(${a[0]}, 1e-8))`
127
- case SLOP.SQRT: return `sqrt(max(${a[0]}, 0))`
142
+ case SLOP.LOG: return `log(max(${a[0]}, ${k(1e-8, t)}))`
143
+ case SLOP.SQRT: return `sqrt(max(${a[0]}, ${k(0, t)}))`
128
144
  case SLOP.ABS: return `abs(${a[0]})`
129
145
  case SLOP.SIGN: return `sign(${a[0]})`
130
146
  case SLOP.FLOOR: return `floor(${a[0]})`
131
147
  case SLOP.CEIL: return `ceil(${a[0]})`
132
148
  case SLOP.ROUND: return `round(${a[0]})`
133
149
  case SLOP.FRACT: return `frac(${a[0]})`
134
- case SLOP.MIN: return `min(${a[0]}, ${a[1]})`
135
- case SLOP.MAX: return `max(${a[0]}, ${a[1]})`
136
- case SLOP.CLAMP: return `clamp(${a[0]}, ${a[1]}, ${a[2]})`
150
+ case SLOP.MIN: return `min(${s[0]}, ${s[1]})`
151
+ case SLOP.MAX: return `max(${s[0]}, ${s[1]})`
152
+ case SLOP.CLAMP: return `clamp(${s[0]}, ${s[1]}, ${s[2]})`
137
153
  case SLOP.SATURATE: return `saturate(${a[0]})`
138
154
 
139
- case SLOP.LENGTH: return `length(${a[0]})`
140
- case SLOP.DISTANCE: return `distance(${a[0]}, ${a[1]})`
141
- case SLOP.DOT: return `dot(${a[0]}, ${a[1]})`
155
+ // Of a scalar, Metal's are ambiguous, so what the VM computes for one
156
+ // (its lanes past the width hold 0) is written out.
157
+ case SLOP.LENGTH: return w0 === 1 ? `abs(${a[0]})` : `length(${a[0]})`
158
+ case SLOP.DISTANCE: return w0 === 1 ? `abs(${a[0]} - ${a[1]})` : `distance(${a[0]}, ${a[1]})`
159
+ case SLOP.DOT: return w0 === 1 ? `(${a[0]} * ${a[1]})` : `dot(${a[0]}, ${a[1]})`
142
160
  case SLOP.CROSS: return `cross(${a[0]}, ${a[1]})`
143
- case SLOP.NORMALIZE: return `normalize(${a[0]})`
144
- case SLOP.REFLECT: return `reflect(${a[0]}, ${a[1]})`
161
+ case SLOP.NORMALIZE: return w0 === 1 ? `(${a[0]} / abs(${a[0]}))` : `normalize(${a[0]})`
162
+ case SLOP.REFLECT: return w0 === 1 ? `(${a[0]} - 2.0 * ${a[1]} * ${a[0]} * ${a[1]})` : `reflect(${a[0]}, ${a[1]})`
145
163
  case SLOP.LUMINANCE: return `${helper("sl_luminance")}(${a[0]}.rgb)`
146
164
  case SLOP.TO_LINEAR: return target.colour === "linear" ? `${helper("sl_toLinear")}(${a[0]})` : a[0]!
147
165
 
148
- case SLOP.MIX: return `lerp(${a[0]}, ${a[1]}, ${a[2]})`
149
- case SLOP.STEP: return `step(${a[0]}, ${a[1]})`
150
- case SLOP.SMOOTHSTEP: return `smoothstep(${a[0]}, ${a[1]}, ${a[2]})`
166
+ case SLOP.MIX: return `lerp(${s[0]}, ${s[1]}, ${s[2]})`
167
+ case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
168
+ case SLOP.SMOOTHSTEP: return `smoothstep(${s[0]}, ${s[1]}, ${s[2]})`
151
169
  // Matches the VM exactly, branchlessly, rather than using an if.
152
170
  // Two backends that pick differently here disagree on every edge
153
171
  // value.
154
- case SLOP.SELECT: return `lerp(${a[2]}, ${a[1]}, step(0.5, ${a[0]}))`
172
+ case SLOP.SELECT: return `lerp(${s[2]}, ${s[1]}, step(${k(0.5, t)}, ${s[0]}))`
155
173
 
156
174
  case SLOP.HSV2RGB: return `${helper("sl_hsv2rgb")}(${a[0]})`
157
175
  case SLOP.NOISE: return `${helper("sl_valueNoise")}(${a[0]})`
@@ -200,3 +218,26 @@ export function emitBody(p: Program, target: BodyTarget): Body {
200
218
  uses: { uniforms: sorted(uniforms), textures: sorted(textures), helpers: [...helpers].sort() },
201
219
  }
202
220
  }
221
+
222
+ /**
223
+ * The library functions a body calls (`Body.uses.helpers`) and everything they
224
+ * call, in the same subset and in dependency order, with the colour branch the
225
+ * target asked for: the text a host puts ahead of the function it wraps the
226
+ * body in.
227
+ *
228
+ * Separate from `emitBody` because OneJS's own frame never needs it: the Unity
229
+ * shader includes `SLCommon.cginc`, the library's source, and a bundle that
230
+ * prints only Unity shaders should not carry the library's text as well.
231
+ */
232
+ export function emitLibrary(helpers: readonly string[], colour: "gamma" | "linear"): string {
233
+ const wanted = new Set(helpers)
234
+ const named = LIB_FUNCTIONS.flatMap((f, i) => (wanted.has(f.name) ? [i] : []))
235
+ if (named.length < wanted.size) {
236
+ const known = new Set(LIB_FUNCTIONS.map((f) => f.name))
237
+ throw new SLError(`the library has no ${[...wanted].filter((h) => !known.has(h)).join(", ")}`)
238
+ }
239
+ return libClosure(named).map((i) => {
240
+ const text = LIB_HLSL[i]!
241
+ return (typeof text === "string" ? text : text[colour]).trim()
242
+ }).join("\n")
243
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * `onejs-sl/emit/hlsl-body`: a program as a function body in the HLSL and Metal
3
- * shared subset, for a host that supplies its own frame. See `BodyTarget`.
3
+ * shared subset, for a host that supplies its own frame (see `BodyTarget`), and
4
+ * the library functions that body calls, in the same subset.
4
5
  */
5
- export { emitBody } from "../body"
6
+ export { emitBody, emitLibrary } from "../body"
6
7
  export type { Body, BodyTarget } from "../body"