onejs-sl 0.2.1 → 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 +26 -0
- package/README.md +61 -53
- package/goldens/goldens.json +98 -63
- package/lib/common.hlsl +5 -6
- package/package.json +2 -4
- package/src/body.ts +35 -12
- package/src/compile.ts +3 -4
- package/src/core.ts +1 -1
- package/src/hlsl.ts +17 -21
- package/src/index.ts +2 -4
- package/src/ir.ts +138 -46
- package/src/lang/README.md +11 -15
- package/src/lang/builtins.ts +10 -23
- package/src/lang/check.ts +24 -11
- package/src/lang/glsl.ts +1 -2
- package/src/lang/index.ts +1 -1
- package/src/lang/lower.ts +31 -37
- package/src/lang/parser.ts +2 -2
- package/src/lang/prelude.ts +2 -2
- package/src/ops.ts +15 -44
- package/src/params.ts +2 -0
- package/src/serial.ts +66 -14
- package/src/sl.ts +113 -84
- package/src/web.ts +50 -18
- package/src/encode.ts +0 -376
- package/src/limits.ts +0 -50
- package/src/vm.ts +0 -11
package/src/encode.ts
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Turns a recorded program into the buffer the VM shader evaluates.
|
|
3
|
-
*
|
|
4
|
-
* Phase 2 of `Specs/SHADER_LANG.md`, and the first consumer of the Phase 1 IR.
|
|
5
|
-
* Three passes, all here, all pure TypeScript:
|
|
6
|
-
*
|
|
7
|
-
* 1. reachable drop anything the result does not depend on
|
|
8
|
-
* 2. live ranges the last instruction that reads each value
|
|
9
|
-
* 3. linear scan assign a register, free it when its range ends
|
|
10
|
-
*
|
|
11
|
-
* WHY EIGHT REGISTERS. Phase 0 measured a dynamically indexed register file in a
|
|
12
|
-
* fragment shader on three stacks. On ANGLE D3D11 and on NVIDIA's GL driver,
|
|
13
|
-
* shrinking the file from 16 entries to 8 costs 3.0x to 4.0x less, which is what
|
|
14
|
-
* a spill to per thread local memory looks like. Eight is not a guess, it is the
|
|
15
|
-
* number the hardware asked for.
|
|
16
|
-
*
|
|
17
|
-
* WHY AN INDEXED STORE AND NOT AN UNROLLED CHAIN. At 8 registers the two Windows
|
|
18
|
-
* backends want opposite things: D3D11 is 1.38x to 1.44x FASTER with the indexed
|
|
19
|
-
* store, GL is about 0.84x faster with the unrolled chain, and both reach the
|
|
20
|
-
* same best case on their preferred one. Branching on the renderer would take
|
|
21
|
-
* the best of both and ship a code path that can only ever be tested on one of
|
|
22
|
-
* the two machines that disagree. One shape everywhere has the better worst
|
|
23
|
-
* case, 0.199 against 0.237, and no untestable branch.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
SLError, TYPE, reachable, type NodeRef, type Program, type SLNode, type SLType,
|
|
28
|
-
} from "./ir"
|
|
29
|
-
import { INPUT_ID, SLOP, TEXTURE_SLOTS } from "./ops"
|
|
30
|
-
import { compile, type Compiled } from "./compile"
|
|
31
|
-
|
|
32
|
-
// Lives in ops.ts with the other wire constants; re-exported so nothing that
|
|
33
|
-
// reached it through the encoder has to move.
|
|
34
|
-
export { INPUT_ID }
|
|
35
|
-
// Lives in ir.ts, since a host reads a graph with it too (`inputsUsed`).
|
|
36
|
-
export { reachable }
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Registers in the VM's file. Phase 0's answer, not a preference.
|
|
40
|
-
*
|
|
41
|
-
* A program that needs more is refused rather than spilled. A spill slot is a
|
|
42
|
-
* second dynamic index into a second array, and any program that needs one is
|
|
43
|
-
* already past the budget this design is honest about.
|
|
44
|
-
*/
|
|
45
|
-
export const REGISTERS = 8
|
|
46
|
-
|
|
47
|
-
/** Instructions in one program. The VM's loop is bounded and must be. */
|
|
48
|
-
export const MAX_INSTRUCTIONS = 256
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Two texels per instruction, fixed width, so instruction `i` sits at texels
|
|
52
|
-
* `2i` and `2i+1` and the shader needs no cursor to advance.
|
|
53
|
-
*
|
|
54
|
-
* Fixed width wastes a texel on instructions with no immediate. That is the
|
|
55
|
-
* right trade: variable width means decoding a length and advancing a cursor,
|
|
56
|
-
* which is a dependent read per instruction and defeats the point of storing the
|
|
57
|
-
* program in a texture at all.
|
|
58
|
-
*
|
|
59
|
-
* texel 2i [ op, dst, a, b ]
|
|
60
|
-
* texel 2i+1 depends on the op:
|
|
61
|
-
* SWIZZLE [ c0, c1, c2, c3 ], unused channels are -1
|
|
62
|
-
* COMPOSE [ c, d, argCount, 0 ], the third and fourth args
|
|
63
|
-
* SAMPLE [ slot, 0, 0, 0 ]
|
|
64
|
-
* FBM [ octaves, 0, 0, 0 ]
|
|
65
|
-
* CONST [ x, y, z, w ]
|
|
66
|
-
* otherwise unused, and zero
|
|
67
|
-
*
|
|
68
|
-
* Every value is a float. Integers ride in floats exactly up to 2^24, which is
|
|
69
|
-
* far above any field here, and the encoder asserts each one is in range rather
|
|
70
|
-
* than trusting it.
|
|
71
|
-
*/
|
|
72
|
-
export const TEXELS_PER_INSTRUCTION = 2
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* A program as the VM's buffer, plus everything `compile` gives a host.
|
|
76
|
-
*
|
|
77
|
-
* The metadata and the lazy sources are `compile`'s, so the two cannot drift.
|
|
78
|
-
*/
|
|
79
|
-
export interface Encoded extends Compiled {
|
|
80
|
-
/** Flat RGBA float data, `TEXELS_PER_INSTRUCTION * 4` numbers per instruction. */
|
|
81
|
-
data: Float32Array
|
|
82
|
-
instructions: number
|
|
83
|
-
/** Register holding the result when the program halts. */
|
|
84
|
-
resultRegister: number
|
|
85
|
-
/** Registers actually used, for the shader to size nothing and for reporting. */
|
|
86
|
-
registersUsed: number
|
|
87
|
-
/**
|
|
88
|
-
* The lowest VM encoding that can run this buffer. A VM older than this
|
|
89
|
-
* refuses the program; see `SL_WIRE_VERSION`.
|
|
90
|
-
*/
|
|
91
|
-
wire: number
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface Instr {
|
|
95
|
-
op: number
|
|
96
|
-
dst: number
|
|
97
|
-
a: number
|
|
98
|
-
b: number
|
|
99
|
-
imm: [number, number, number, number]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* The last position in `order` that reads each node.
|
|
104
|
-
*
|
|
105
|
-
* A register is free the instant its value's last reader has run, which is what
|
|
106
|
-
* lets an eight entry file hold a program with far more than eight values in it.
|
|
107
|
-
*/
|
|
108
|
-
export function liveRanges(nodes: SLNode[], order: NodeRef[], result: NodeRef): Map<NodeRef, number> {
|
|
109
|
-
const pos = new Map<NodeRef, number>()
|
|
110
|
-
order.forEach((ref, i) => pos.set(ref, i))
|
|
111
|
-
const last = new Map<NodeRef, number>()
|
|
112
|
-
order.forEach((ref, i) => {
|
|
113
|
-
const n = nodes[ref]
|
|
114
|
-
const reads: NodeRef[] = n.k === "swizzle" ? [n.src] : n.k === "call" ? n.args : []
|
|
115
|
-
for (const r of reads) last.set(r, i)
|
|
116
|
-
})
|
|
117
|
-
// The result outlives every instruction, or it could be freed and overwritten
|
|
118
|
-
// before anybody reads it.
|
|
119
|
-
last.set(result, order.length)
|
|
120
|
-
for (const ref of order) if (!last.has(ref)) last.set(ref, pos.get(ref)!)
|
|
121
|
-
return last
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Names the loops behind an over-long program. A loop unrolls, so a count that
|
|
126
|
-
* reads as one line of source is emitted once per iteration, and without this
|
|
127
|
-
* the error blames "instructions" the author never wrote. A loop is named only
|
|
128
|
-
* when it fills a quarter of the budget or more: a two iteration loop in a
|
|
129
|
-
* program made long by other code is not the thing to lower.
|
|
130
|
-
*
|
|
131
|
-
* Worded for both surfaces: the span comes from `sl.repeat` in the EDSL and
|
|
132
|
-
* from `for` in a `.sl` file, and an author who wrote one should not be told
|
|
133
|
-
* about the other.
|
|
134
|
-
*/
|
|
135
|
-
function blameLoops(program: Program, order: NodeRef[]): string {
|
|
136
|
-
const parts: string[] = []
|
|
137
|
-
for (const loop of program.loops ?? []) {
|
|
138
|
-
const ops = order.filter((r) => r >= loop.start && r < loop.end).length
|
|
139
|
-
if (ops * 4 >= MAX_INSTRUCTIONS) parts.push(`a loop of ${loop.count} accounts for ${ops}`)
|
|
140
|
-
}
|
|
141
|
-
if (parts.length === 0) return ""
|
|
142
|
-
return ` A loop unrolls, so every iteration is emitted in full: ${parts.join(", ")}. ` +
|
|
143
|
-
`Lower the count or slim the body.`
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* The program as the VM runs it.
|
|
148
|
-
*
|
|
149
|
-
* The IR carries a shape's whole parameter list, up to six. One instruction
|
|
150
|
-
* holds a shape id and four immediates, so a shape given a fifth or sixth
|
|
151
|
-
* becomes SDF_WIDE: its first two parameters stay immediates and the other
|
|
152
|
-
* four come from a constant register. Only the VM sees this; every compiled
|
|
153
|
-
* backend reads the IR's SDF directly. The hash is the program's own, since
|
|
154
|
-
* nothing about what it computes changed.
|
|
155
|
-
*/
|
|
156
|
-
export function forVm(program: Program): Program {
|
|
157
|
-
const wide = (n: SLNode) => n.k === "call" && n.op === SLOP.SDF && (n.imm?.length ?? 0) > 5
|
|
158
|
-
if (!program.nodes.some(wide)) return program
|
|
159
|
-
const nodes: SLNode[] = []
|
|
160
|
-
const map: NodeRef[] = []
|
|
161
|
-
program.nodes.forEach((n, i) => {
|
|
162
|
-
if (n.k === "call" && wide(n)) {
|
|
163
|
-
const im = n.imm!
|
|
164
|
-
const rest = nodes.push({ k: "const", type: TYPE.VEC4, v: [im[3] ?? 0, im[4] ?? 0, im[5] ?? 0, im[6] ?? 0] }) - 1
|
|
165
|
-
map[i] = nodes.push({
|
|
166
|
-
k: "call", type: n.type, op: SLOP.SDF_WIDE, args: [map[n.args[0]!]!, rest], imm: [im[0] ?? 0, im[1] ?? 0, im[2] ?? 0, 0],
|
|
167
|
-
}) - 1
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
if (n.k === "swizzle") map[i] = nodes.push({ ...n, src: map[n.src]! }) - 1
|
|
171
|
-
else if (n.k === "call") map[i] = nodes.push({ ...n, args: n.args.map((a) => map[a]!) }) - 1
|
|
172
|
-
else map[i] = nodes.push(n) - 1
|
|
173
|
-
})
|
|
174
|
-
const at = (ref: NodeRef) => ref < map.length ? map[ref]! : nodes.length
|
|
175
|
-
return { ...program, nodes, result: map[program.result]!, loops: program.loops.map((l) => ({ ...l, start: at(l.start), end: at(l.end) })) }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* The limits `encode` refuses a program for that are not about registers: its
|
|
180
|
-
* length and its sampler slots. Shared with `vmFit`, so the two cannot
|
|
181
|
-
* disagree about what fits.
|
|
182
|
-
*/
|
|
183
|
-
export function checkBudget(program: Program, order: NodeRef[]): void {
|
|
184
|
-
if (order.length > MAX_INSTRUCTIONS) {
|
|
185
|
-
throw new SLError(
|
|
186
|
-
`this program is ${order.length} operations and the VM runs at most ${MAX_INSTRUCTIONS}. ` +
|
|
187
|
-
`The loop in the shader is bounded and has to be.` + blameLoops(program, order),
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
for (const t of program.textures) {
|
|
191
|
-
if (t.slot >= TEXTURE_SLOTS) throw new SLError(`texture "${t.name}" is past the sampler budget`)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* A register for every node in `order`, by linear scan, and the most in use
|
|
197
|
-
* at once. Throws when the file runs out. Shared with `vmFit`.
|
|
198
|
-
*/
|
|
199
|
-
export function allocate(program: Program, order: NodeRef[]): { reg: Map<NodeRef, number>, peak: number } {
|
|
200
|
-
const last = liveRanges(program.nodes, order, program.result)
|
|
201
|
-
const reg = new Map<NodeRef, number>()
|
|
202
|
-
const free: number[] = []
|
|
203
|
-
for (let r = REGISTERS - 1; r >= 0; r--) free.push(r)
|
|
204
|
-
/** Which node currently owns each register, for freeing. */
|
|
205
|
-
const owner = new Array<NodeRef | null>(REGISTERS).fill(null)
|
|
206
|
-
let peak = 0
|
|
207
|
-
|
|
208
|
-
order.forEach((ref, i) => {
|
|
209
|
-
// Free first, so a value whose last reader is THIS instruction can hand
|
|
210
|
-
// its register straight to this instruction's result. Without that an
|
|
211
|
-
// eight register file would behave like a seven register one on every
|
|
212
|
-
// chained expression, which is most of them.
|
|
213
|
-
for (let r = 0; r < REGISTERS; r++) {
|
|
214
|
-
const o = owner[r]
|
|
215
|
-
if (o !== null && last.get(o)! < i) { owner[r] = null; free.push(r) }
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const args = readsOf(program.nodes[ref]).map((r) => {
|
|
219
|
-
const rr = reg.get(r)
|
|
220
|
-
if (rr === undefined) throw new SLError(`internal: node ${r} has no register when ${ref} needs it`)
|
|
221
|
-
return rr
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
// Operands are read before the destination is written, so a register
|
|
225
|
-
// whose last use is this instruction can also be the destination.
|
|
226
|
-
for (let r = 0; r < REGISTERS; r++) {
|
|
227
|
-
const o = owner[r]
|
|
228
|
-
if (o !== null && last.get(o)! === i && !reg.has(ref)) {
|
|
229
|
-
// Only reclaim a register this instruction is actually reading;
|
|
230
|
-
// anything else is still live for a later instruction.
|
|
231
|
-
if (args.includes(r)) { owner[r] = null; free.push(r) }
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const dst = free.pop()
|
|
236
|
-
if (dst === undefined) {
|
|
237
|
-
throw new SLError(
|
|
238
|
-
`this program needs more than ${REGISTERS} registers at once. The VM has ${REGISTERS} ` +
|
|
239
|
-
`because that is what the hardware measured fastest, and spilling is deliberately not ` +
|
|
240
|
-
`implemented: a spill slot is a second dynamic index into a second array. Split the ` +
|
|
241
|
-
`program, or reuse fewer intermediate values at the same time.`,
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
|
-
owner[dst] = ref
|
|
245
|
-
reg.set(ref, dst)
|
|
246
|
-
peak = Math.max(peak, REGISTERS - free.length)
|
|
247
|
-
})
|
|
248
|
-
return { reg, peak }
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function readsOf(n: SLNode): NodeRef[] {
|
|
252
|
-
return n.k === "swizzle" ? [n.src] : n.k === "call" ? n.args : []
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* The lowest VM that can run `vm`, the result of `forVm(source)`: 2 only where
|
|
257
|
-
* a wide shape made SDF_WIDE, so everything else still runs on a wire 1 VM.
|
|
258
|
-
*/
|
|
259
|
-
export function wireOf(source: Program, vm: Program): number {
|
|
260
|
-
return vm === source ? 1 : 2
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function encode(source: Program): Encoded {
|
|
264
|
-
const program = forVm(source)
|
|
265
|
-
const order = reachable(program.nodes, program.result)
|
|
266
|
-
checkBudget(program, order)
|
|
267
|
-
const { reg, peak } = allocate(program, order)
|
|
268
|
-
|
|
269
|
-
const out = order.map((ref) => {
|
|
270
|
-
const n = program.nodes[ref]
|
|
271
|
-
const reads = readsOf(n)
|
|
272
|
-
// The width of the first operand, for the ops whose result narrows.
|
|
273
|
-
const srcWidth = reads.length > 0 ? program.nodes[reads[0]].type : undefined
|
|
274
|
-
return emit(n, reg.get(ref)!, reads.map((r) => reg.get(r)!), srcWidth)
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
const data = new Float32Array(out.length * TEXELS_PER_INSTRUCTION * 4)
|
|
278
|
-
out.forEach((ins, i) => {
|
|
279
|
-
const o = i * 8
|
|
280
|
-
data[o] = ins.op; data[o + 1] = ins.dst; data[o + 2] = ins.a; data[o + 3] = ins.b
|
|
281
|
-
data[o + 4] = ins.imm[0]; data[o + 5] = ins.imm[1]
|
|
282
|
-
data[o + 6] = ins.imm[2]; data[o + 7] = ins.imm[3]
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
// The hash, names, defaults and sources are compile's, from the SOURCE
|
|
286
|
-
// program: every compiled backend reads the IR's own SDF, and forVm keeps
|
|
287
|
-
// the hash, since nothing about what the program computes changed.
|
|
288
|
-
const encoded = compile(source) as Encoded
|
|
289
|
-
encoded.data = data
|
|
290
|
-
encoded.instructions = out.length
|
|
291
|
-
encoded.resultRegister = reg.get(program.result)!
|
|
292
|
-
encoded.registersUsed = peak
|
|
293
|
-
encoded.wire = wireOf(source, program)
|
|
294
|
-
return encoded
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const NONE: [number, number, number, number] = [0, 0, 0, 0]
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Ops whose result is narrower than their input, so the shader cannot infer how
|
|
301
|
-
* many components to read from the destination type.
|
|
302
|
-
*/
|
|
303
|
-
const WIDTH_SENSITIVE = new Set<number>([
|
|
304
|
-
SLOP.LENGTH, SLOP.DISTANCE, SLOP.DOT, SLOP.NORMALIZE, SLOP.LUMINANCE,
|
|
305
|
-
])
|
|
306
|
-
|
|
307
|
-
function emit(n: SLNode, dst: number, args: number[], srcWidth?: SLType): Instr {
|
|
308
|
-
switch (n.k) {
|
|
309
|
-
case "const": {
|
|
310
|
-
const v = n.v
|
|
311
|
-
return { op: SLOP.CONST, dst, a: 0, b: n.type, imm: [v[0] ?? 0, v[1] ?? 0, v[2] ?? 0, v[3] ?? 0] }
|
|
312
|
-
}
|
|
313
|
-
case "input":
|
|
314
|
-
return { op: SLOP.INPUT, dst, a: INPUT_ID[n.name], b: n.type, imm: [...NONE] }
|
|
315
|
-
case "uniform":
|
|
316
|
-
return { op: SLOP.UNIFORM, dst, a: n.slot, b: n.type, imm: [...NONE] }
|
|
317
|
-
case "swizzle": {
|
|
318
|
-
const c = n.chans
|
|
319
|
-
return {
|
|
320
|
-
op: SLOP.SWIZZLE, dst, a: args[0], b: n.type,
|
|
321
|
-
imm: [c[0] ?? -1, c[1] ?? -1, c[2] ?? -1, c[3] ?? -1],
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
case "call": {
|
|
325
|
-
if (n.op === SLOP.COMPOSE) {
|
|
326
|
-
if (args.length > 4) throw new SLError(`compose takes at most 4 parts, got ${args.length}`)
|
|
327
|
-
return {
|
|
328
|
-
op: SLOP.COMPOSE, dst, a: args[0] ?? 0, b: args[1] ?? 0,
|
|
329
|
-
imm: [args[2] ?? 0, args[3] ?? 0, args.length, n.type],
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
// Ops whose RESULT is narrower than their input need the source
|
|
333
|
-
// width, because a register is a float4 whatever the value in it is
|
|
334
|
-
// and length(vec2) is not length of four components. Found by
|
|
335
|
-
// writing the shader, not by writing the encoder: nothing in the IR
|
|
336
|
-
// is wrong, the information simply was not reaching the consumer.
|
|
337
|
-
if (WIDTH_SENSITIVE.has(n.op)) {
|
|
338
|
-
const src = srcWidth
|
|
339
|
-
if (src === undefined) throw new SLError(`internal: ${n.op} has no source width`)
|
|
340
|
-
return { op: n.op, dst, a: args[0] ?? 0, b: args[1] ?? 0, imm: [src, 0, 0, 0] }
|
|
341
|
-
}
|
|
342
|
-
// SDF carries a shape id plus four parameters, which is one more
|
|
343
|
-
// number than the immediate holds. The id goes in the second
|
|
344
|
-
// operand slot, free because SDF reads one register.
|
|
345
|
-
if (n.op === SLOP.SDF) {
|
|
346
|
-
const im = n.imm ?? []
|
|
347
|
-
return {
|
|
348
|
-
op: SLOP.SDF, dst, a: args[0] ?? 0, b: im[0] ?? 0,
|
|
349
|
-
imm: [im[1] ?? 0, im[2] ?? 0, im[3] ?? 0, im[4] ?? 0],
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (args.length > 3) {
|
|
353
|
-
throw new SLError(`internal: ${n.op} has ${args.length} operands and the encoding holds 3`)
|
|
354
|
-
}
|
|
355
|
-
// A third operand rides in the immediate, which is free for every op
|
|
356
|
-
// that has one (clamp, mix, smoothstep, select) because none of them
|
|
357
|
-
// carries a literal.
|
|
358
|
-
const imm: [number, number, number, number] = n.imm !== undefined
|
|
359
|
-
? [n.imm[0] ?? 0, n.imm[1] ?? 0, n.imm[2] ?? 0, n.imm[3] ?? 0]
|
|
360
|
-
: [args[2] ?? 0, 0, 0, 0]
|
|
361
|
-
return { op: n.op, dst, a: args[0] ?? 0, b: args[1] ?? 0, imm }
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Every field must survive a float32 round trip exactly, or an instruction
|
|
368
|
-
* decodes as a different instruction. Cheap to check and impossible to debug
|
|
369
|
-
* from the wrong picture it would otherwise produce.
|
|
370
|
-
*/
|
|
371
|
-
export function checkEncodable(v: number, what: string): void {
|
|
372
|
-
if (!Number.isFinite(v)) throw new SLError(`${what} is ${v}, which cannot be encoded`)
|
|
373
|
-
if (Number.isInteger(v) && Math.abs(v) > 0x1000000) {
|
|
374
|
-
throw new SLError(`${what} is ${v}, past the 2^24 an integer survives in a float`)
|
|
375
|
-
}
|
|
376
|
-
}
|
package/src/limits.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `onejs-sl/limits`: whether a program fits the VM, without encoding it.
|
|
3
|
-
*
|
|
4
|
-
* The VM is the one backend with limits. A compiled backend (every web build,
|
|
5
|
-
* an editor with a generated shader) runs any program; the VM runs one that is
|
|
6
|
-
* at most `MAX_INSTRUCTIONS` long and never needs more than `REGISTERS`
|
|
7
|
-
* values at once. OneJS refuses an over-limit program only in a build that
|
|
8
|
-
* still runs the VM. A host that wants to warn an author ahead of that asks
|
|
9
|
-
* here, and gets the same answer `encode` would give, because both run the
|
|
10
|
-
* same checks.
|
|
11
|
-
*/
|
|
12
|
-
import { SLError, type Program } from "./ir"
|
|
13
|
-
import { allocate, checkBudget, forVm, reachable, wireOf } from "./encode"
|
|
14
|
-
|
|
15
|
-
export { REGISTERS, MAX_INSTRUCTIONS } from "./encode"
|
|
16
|
-
export { VM_UNIFORMS, VM_TEXTURES } from "./ops"
|
|
17
|
-
|
|
18
|
-
export interface VmFit {
|
|
19
|
-
/** Every OneJS VM at `wire` or newer runs it. */
|
|
20
|
-
fits: boolean
|
|
21
|
-
/** Operations after dead code is dropped, as the VM would run them. */
|
|
22
|
-
instructions: number
|
|
23
|
-
/** The most values live at once, when the register file held them all. */
|
|
24
|
-
registers?: number
|
|
25
|
-
/** The oldest VM encoding that can run it. */
|
|
26
|
-
wire: number
|
|
27
|
-
/** Why it does not fit, in the words `encode` throws with. */
|
|
28
|
-
reason?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function vmFit(source: Program): VmFit {
|
|
32
|
-
const program = forVm(source)
|
|
33
|
-
const order = reachable(program.nodes, program.result)
|
|
34
|
-
const reasons: string[] = []
|
|
35
|
-
let registers: number | undefined
|
|
36
|
-
const attempt = (check: () => void) => {
|
|
37
|
-
try {
|
|
38
|
-
check()
|
|
39
|
-
} catch (e) {
|
|
40
|
-
if (!(e instanceof SLError)) throw e
|
|
41
|
-
reasons.push(e.message)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
attempt(() => checkBudget(program, order))
|
|
45
|
-
attempt(() => { registers = allocate(program, order).peak })
|
|
46
|
-
const fit: VmFit = { fits: reasons.length === 0, instructions: order.length, wire: wireOf(source, program) }
|
|
47
|
-
if (registers !== undefined) fit.registers = registers
|
|
48
|
-
if (reasons.length > 0) fit.reason = reasons.join(" ")
|
|
49
|
-
return fit
|
|
50
|
-
}
|
package/src/vm.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `onejs-sl/vm`: the buffer OneJS's shader language VM evaluates.
|
|
3
|
-
*
|
|
4
|
-
* Only a host that runs the VM needs this. `SL_WIRE_VERSION` is the newest
|
|
5
|
-
* encoding it writes; each result's `wire` is the oldest VM that can run it.
|
|
6
|
-
*/
|
|
7
|
-
export {
|
|
8
|
-
encode, forVm, reachable, liveRanges, REGISTERS, MAX_INSTRUCTIONS, TEXELS_PER_INSTRUCTION,
|
|
9
|
-
} from "./encode"
|
|
10
|
-
export type { Encoded } from "./encode"
|
|
11
|
-
export { SL_WIRE_VERSION, INPUT_ID, VM_UNIFORMS, VM_TEXTURES } from "./ops"
|