onejs-sl 0.2.0 → 0.2.1

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ A program built in code or read from JSON is refused past 16 uniforms or 4 textures, as a `.sl` file already was, instead of compiling and then drawing differently on different hosts. A host can read both caps.
6
+
7
+ ### Fixed
8
+
9
+ - 0.2.0 regression: `compile()` dropped `encode()`'s texture check, so a program built in code with a fifth texture compiled and then sampled nothing in the editor and a native player
10
+
11
+ ### Changed
12
+
13
+ - `sl.texture` and `sl.uniform` refuse past the caps where they are declared, in the parser's words
14
+ - `compile` and `fromJSON` refuse a program past either cap, however it was made
15
+ - `onejs-sl/core` exports `UNIFORM_SLOTS` (16) and `TEXTURE_SLOTS` (4), which the parser and the builder both read
16
+ - `MAX_TEXTURES` is a deprecated alias of `TEXTURE_SLOTS`, so 4 rather than 15
17
+ - `VM_UNIFORMS` and `VM_TEXTURES` are the same numbers, and go with the VM
18
+ - The parser's cap messages give OneJS's slots as the reason
19
+
3
20
  ## 0.2.0
4
21
 
5
22
  `compile()` replaces `encode()` for every host that draws compiled: it gives a program's hash, names, defaults and sources with no VM buffer and no budget, so a long program or one with many values at once compiles. Program hashes do not change.
package/README.md CHANGED
@@ -36,7 +36,7 @@ only what it calls.
36
36
  | Entry | Contents |
37
37
  |---|---|
38
38
  | `onejs-sl` | `parse` and `analyze`, `diagnose` (every error in a file, not just the first), `classify` (every token with its class, comments kept, never throws), `fromGLSL` (a pasted Shadertoy or WebGL shader as a `.sl` file, with notes on what did not carry over), the TypeScript form `sl`, the IR types, `SL_IR_VERSION`, `toJSON`/`fromJSON`, `SLParseError` (file, line, column, offset, length, the bare `text`, and a `fix` where one is certain) |
39
- | `onejs-sl/core` | the same without the parser: what a game needs at run time |
39
+ | `onejs-sl/core` | the same without the parser: what a game needs at run time. Also the caps on one program, `UNIFORM_SLOTS` (16) and `TEXTURE_SLOTS` (4), which the parser and the builder enforce and a host can read |
40
40
  | `onejs-sl/tables` | `BUILTINS`, `SL_HLSL`, `INPUTS`, `SL_SDF_SHAPES`, `SL_SDF_PARAMS`, `SL_KEYWORDS`, `SL_TYPES`, `TYPE_WIDTH`, `PRELUDE_NAMES`: what completion and highlighting read. `BUILTIN_PARAMS`, `SL_SDF_PARAM_NAMES` and `LIB_SIGNATURES` name every parameter, so an editor can show `lerp(x, y, s)`, and `BUILTIN_DOCS`, `INPUT_DOCS` and `PRELUDE_DOCS` give each a line for its tooltip |
41
41
  | `onejs-sl/compile` | `compile(program)`: what a host draws a program with. Its hash, its uniform and texture names in slot order, their defaults, and `hlsl`, `wgsl` and `glsl` as lazy getters. No budget |
42
42
  | `onejs-sl/limits` | `vmFit(program)`: whether the VM runs it, and why not. The VM's, until it is deleted |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "generatedBy": "onejs-sl goldens/run.mjs",
3
- "package": "0.2.0",
3
+ "package": "0.2.1",
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": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-sl",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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/compile.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * shader does. `Specs/SL_NEXT.md` section 0.5, step 3.
13
13
  */
14
14
 
15
- import type { Program } from "./ir"
15
+ import { checkCaps, type Program } from "./ir"
16
16
  import { emitShader } from "./hlsl"
17
17
  import { emitGLSL, emitWGSL } from "./web"
18
18
  import { uniformDefaults } from "./sl"
@@ -69,6 +69,7 @@ export interface Compiled {
69
69
  }
70
70
 
71
71
  export function compile(program: Program): Compiled {
72
+ checkCaps(program)
72
73
  const compiled = {
73
74
  hash: program.hash,
74
75
  // Slot order, which is declaration order: Builder.uniform pushes and
package/src/core.ts CHANGED
@@ -16,7 +16,7 @@ export {
16
16
  export type {
17
17
  SLType, InputName, NodeRef, SLNode, Program, UniformDecl, UniformControl, TextureDecl,
18
18
  } from "./ir"
19
- export { SLOP, SL_ARITY, SL_NAME, INPUT_ID, isSampling } from "./ops"
19
+ export { SLOP, SL_ARITY, SL_NAME, INPUT_ID, isSampling, UNIFORM_SLOTS, TEXTURE_SLOTS } from "./ops"
20
20
  export type { SLOpCode } from "./ops"
21
21
  export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
22
22
  export type { SlSdfKind } from "./shapes"
package/src/encode.ts CHANGED
@@ -24,9 +24,9 @@
24
24
  */
25
25
 
26
26
  import {
27
- MAX_TEXTURES, SLError, TYPE, reachable, type NodeRef, type Program, type SLNode, type SLType,
27
+ SLError, TYPE, reachable, type NodeRef, type Program, type SLNode, type SLType,
28
28
  } from "./ir"
29
- import { INPUT_ID, SLOP } from "./ops"
29
+ import { INPUT_ID, SLOP, TEXTURE_SLOTS } from "./ops"
30
30
  import { compile, type Compiled } from "./compile"
31
31
 
32
32
  // Lives in ops.ts with the other wire constants; re-exported so nothing that
@@ -188,7 +188,7 @@ export function checkBudget(program: Program, order: NodeRef[]): void {
188
188
  )
189
189
  }
190
190
  for (const t of program.textures) {
191
- if (t.slot >= MAX_TEXTURES) throw new SLError(`texture "${t.name}" is past the sampler budget`)
191
+ if (t.slot >= TEXTURE_SLOTS) throw new SLError(`texture "${t.name}" is past the sampler budget`)
192
192
  }
193
193
  }
194
194
 
package/src/ir.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  * them, and it is fully testable with no GPU.
27
27
  */
28
28
 
29
- import { SLOP, SL_ARITY, SL_NAME, type SLOpCode } from "./ops"
29
+ import { SLOP, SL_ARITY, SL_NAME, TEXTURE_SLOTS, UNIFORM_SLOTS, type SLOpCode } from "./ops"
30
30
 
31
31
  /** Component count. The only notion of type the IR has. */
32
32
  export const TYPE = { FLOAT: 1, VEC2: 2, VEC3: 3, VEC4: 4 } as const
@@ -199,11 +199,38 @@ export interface Program {
199
199
  }
200
200
 
201
201
  /**
202
- * Sampler slots in a fragment shader on the WebGL2 baseline, minus one for the
203
- * program texture itself. Exceeding it is refused when the program is written
204
- * rather than when it is drawn, with a message naming the limit.
202
+ * @deprecated Use `TEXTURE_SLOTS`. Was 15, the WebGL2 sampler count, which let a
203
+ * program declare textures no native host binds.
205
204
  */
206
- export const MAX_TEXTURES = 15
205
+ export const MAX_TEXTURES = TEXTURE_SLOTS
206
+
207
+ /**
208
+ * The refusals for a program past its caps, worded once for the parser (a
209
+ * `file`) and the builder (a `program`) so the two cannot drift apart.
210
+ */
211
+ export function tooManyUniforms(declared: number, source: "file" | "program"): string {
212
+ return `this ${source} declares ${declared} uniforms and a program may hold ${UNIFORM_SLOTS}. ` +
213
+ `OneJS keeps a program's uniforms in ${UNIFORM_SLOTS} slots, so this one would have no ` +
214
+ `slot of its own; it is refused here rather than drawn wrong. Pack related values into ` +
215
+ `a ${source === "file" ? "float4" : "vec4"}.`
216
+ }
217
+
218
+ export function tooManyTextures(declared: number, source: "file" | "program"): string {
219
+ return `this ${source} declares ${declared} textures and a program may sample ${TEXTURE_SLOTS}. ` +
220
+ `OneJS binds ${TEXTURE_SLOTS} textures to a program in the editor and a native player, so ` +
221
+ `this one would sample nothing there while a browser drew it: two pictures from one ${source}.`
222
+ }
223
+
224
+ /**
225
+ * Refuses a finished program past either cap. The parser and the builder
226
+ * refuse at the declaration, where the message can point at it; this is for
227
+ * a program that arrived some other way, stored as JSON or put together by
228
+ * hand, and `compile` runs it so nothing reaches a host around it.
229
+ */
230
+ export function checkCaps(p: Pick<Program, "uniforms" | "textures">): void {
231
+ if (p.uniforms.length > UNIFORM_SLOTS) throw new SLError(tooManyUniforms(p.uniforms.length, "program"))
232
+ if (p.textures.length > TEXTURE_SLOTS) throw new SLError(tooManyTextures(p.textures.length, "program"))
233
+ }
207
234
 
208
235
  /** Instructions a single program may hold. Generous; the ceiling that matters is registers. */
209
236
  export const MAX_NODES = 4096
@@ -288,6 +315,7 @@ export class Builder {
288
315
  const problem = controlProblem(type, value, control)
289
316
  if (problem !== null) throw new SLError(`uniform "${name}": ${problem.message}`)
290
317
  const slot = this.uniforms.length
318
+ if (slot >= UNIFORM_SLOTS) throw new SLError(tooManyUniforms(slot + 1, "program"))
291
319
  const decl: UniformDecl = { name, type, value: value.slice() }
292
320
  if (colour) decl.colour = true
293
321
  Object.assign(decl, controlOf(control))
@@ -298,12 +326,8 @@ export class Builder {
298
326
  texture(name: string): number {
299
327
  const existing = this.textures.findIndex((t) => t.name === name)
300
328
  if (existing >= 0) return this.textures[existing].slot
301
- if (this.textures.length >= MAX_TEXTURES) {
302
- throw new SLError(
303
- `a program may sample at most ${MAX_TEXTURES} textures, and this one asks for ` +
304
- `${this.textures.length + 1}. That ceiling is the fragment shader's sampler slots ` +
305
- `on the WebGL2 baseline, so it cannot be widened.`,
306
- )
329
+ if (this.textures.length >= TEXTURE_SLOTS) {
330
+ throw new SLError(tooManyTextures(this.textures.length + 1, "program"))
307
331
  }
308
332
  const slot = this.textures.length
309
333
  this.textures.push({ name, slot })
package/src/lang/check.ts CHANGED
@@ -28,8 +28,8 @@
28
28
  * is refused with the reason.
29
29
  */
30
30
 
31
- import { DERIVED_INPUTS, INPUTS } from "../ir"
32
- import { VM_TEXTURES, VM_UNIFORMS } from "../ops"
31
+ import { DERIVED_INPUTS, INPUTS, tooManyTextures, tooManyUniforms } from "../ir"
32
+ import { TEXTURE_SLOTS, UNIFORM_SLOTS } from "../ops"
33
33
  import { SL_GLSL_HINT } from "../ops"
34
34
  import { BUILTINS, NOT_YET } from "./builtins"
35
35
  import { SL_SDF_SHAPES } from "../shapes"
@@ -161,15 +161,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
161
161
  if (!uniforms.has(u.name)) uniforms.set(u.name, u)
162
162
  }
163
163
  attempt(() => {
164
- if (uniforms.size <= VM_UNIFORMS) return
165
- const over = unit.uniforms[VM_UNIFORMS]!
166
- fail(
167
- `this file declares ${uniforms.size} uniforms and a program may hold ${VM_UNIFORMS}. ` +
168
- `That is the size of the VM's uniform array; past it a slot reads the last one instead ` +
169
- `of its own, so it is refused here rather than rendered wrong. Pack related values into ` +
170
- `a float4.`,
171
- over.pos, over.name.length,
172
- )
164
+ if (uniforms.size <= UNIFORM_SLOTS) return
165
+ const over = unit.uniforms[UNIFORM_SLOTS]!
166
+ fail(tooManyUniforms(uniforms.size, "file"), over.pos, over.name.length)
173
167
  })
174
168
 
175
169
  for (const t of unit.textures) {
@@ -180,14 +174,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
180
174
  if (!textures.has(t.name)) textures.set(t.name, t)
181
175
  }
182
176
  attempt(() => {
183
- if (textures.size <= VM_TEXTURES) return
184
- const over = unit.textures[VM_TEXTURES]!
185
- fail(
186
- `this file declares ${textures.size} textures and a program may sample ${VM_TEXTURES}. ` +
187
- `The VM shader declares four samplers; a fifth would sample the fourth in the browser ` +
188
- `and its own after an eject, which is two different pictures from one file.`,
189
- over.pos, over.name.length,
190
- )
177
+ if (textures.size <= TEXTURE_SLOTS) return
178
+ const over = unit.textures[TEXTURE_SLOTS]!
179
+ fail(tooManyTextures(textures.size, "file"), over.pos, over.name.length)
191
180
  })
192
181
 
193
182
  for (const c of unit.consts) {
package/src/ops.ts CHANGED
@@ -35,17 +35,24 @@
35
35
  export const SL_WIRE_VERSION = 2
36
36
 
37
37
  /**
38
- * What the VM shader actually declares, and therefore what a program may use.
38
+ * How many uniforms and textures one program may declare: the language's caps,
39
+ * for a host to read.
39
40
  *
40
- * `FxProgram.shader` has `float4 _Uniforms[16]` and four `sampler2D`s, and
41
- * `SLProgramBridge.cs` fills exactly those. Past either ceiling the VM does not
42
- * fail, it clamps: uniform 17 reads slot 15 and texture slot 4 samples `_Tex3`,
43
- * while the generated HLSL would declare both correctly. That is a silent
44
- * disagreement between the backends, so the text form reports it at the
45
- * declaration with a file and a line. See `Specs/SL_TEXT.md` 3.7.
41
+ * They are what OneJS draws a program with. It keeps a program's uniforms in
42
+ * 16 slots in the editor, a native player and a browser alike
43
+ * (`SLProgramBridge.MaxUniforms`, the web frame's `sl_U`), and binds four
44
+ * textures in the editor and a native player (`_Tex0` to `_Tex3`). A program
45
+ * past either has something with no slot of its own, which one host would drop
46
+ * and another would draw, so the parser and the builder both refuse it at the
47
+ * declaration, the parser with a file and a line. `Specs/SL_NEXT.md` section 8
48
+ * proposes raising both.
46
49
  */
47
- export const VM_UNIFORMS = 16
48
- export const VM_TEXTURES = 4
50
+ export const UNIFORM_SLOTS = 16
51
+ export const TEXTURE_SLOTS = 4
52
+
53
+ /** The VM's names for the same two caps, which go when the VM does. */
54
+ export const VM_UNIFORMS = UNIFORM_SLOTS
55
+ export const VM_TEXTURES = TEXTURE_SLOTS
49
56
 
50
57
  /** Input ids, fixed here because the shader switches on them. */
51
58
  export const INPUT_ID: Record<string, number> = {
package/src/serial.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * older one is migrated to this one and rehashed under it.
10
10
  */
11
11
  import {
12
- INPUTS, SLError, SL_IR_VERSION, TYPE, controlOf, controlProblem, hashProgram,
12
+ INPUTS, SLError, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, hashProgram,
13
13
  type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
14
14
  } from "./ir"
15
15
  import { SL_ARITY, SL_NAME } from "./ops"
@@ -44,6 +44,7 @@ export function fromJSON(json: unknown): Program {
44
44
  if (nodes[result!]!.type !== TYPE.VEC4) fail("its result is not a float4")
45
45
  const uniforms = (j.uniforms ?? []).map(uniform)
46
46
  const textures = (j.textures ?? []).map(texture)
47
+ checkCaps({ uniforms, textures })
47
48
  for (const n of nodes) {
48
49
  if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
49
50
  }
package/src/web.ts CHANGED
@@ -29,12 +29,12 @@ import { SLError, TYPE, type Program, type SLNode, type SLType } from "./ir"
29
29
  import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
30
30
  import { LIB_GLSL } from "./lib/glsl"
31
31
  import { LIB_WGSL } from "./lib/wgsl"
32
- import { INPUT_ID, SLOP, VM_UNIFORMS } from "./ops"
32
+ import { INPUT_ID, SLOP, UNIFORM_SLOTS } from "./ops"
33
33
 
34
34
  export type WebLanguage = "glsl" | "wgsl"
35
35
 
36
36
  /** Uniform slots the host always provides, so every program shares one layout. */
37
- export const WEB_UNIFORM_SLOTS = VM_UNIFORMS
37
+ export const WEB_UNIFORM_SLOTS = UNIFORM_SLOTS
38
38
 
39
39
  export function emitGLSL(p: Program): string {
40
40
  return emitWeb(p, "glsl")