@bigmistqke/view.gl 0.1.19 → 0.3.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.
Files changed (44) hide show
  1. package/README.md +82 -27
  2. package/dev/dist/404.html +19 -0
  3. package/dev/dist/assets/bezier-DrA4cU_H.js +292 -0
  4. package/dev/dist/assets/game-of-life-BYlzWhEL.js +186 -0
  5. package/dev/dist/assets/glsl-composition-D_BaFFij.js +97 -0
  6. package/dev/dist/assets/grid-DfXHNLBm.js +181 -0
  7. package/dev/dist/assets/index-C2N3u8wj.css +12 -0
  8. package/dev/dist/assets/index-D-N0FDYs.js +110 -0
  9. package/dev/dist/assets/main-CSE2fki3.js +32 -0
  10. package/dev/dist/assets/overview-IeUhUflE.js +80 -0
  11. package/dev/dist/assets/pix-sim-DMJJA_WO.js +500 -0
  12. package/dev/dist/assets/ray-casting-DTQetDPd.js +411 -0
  13. package/dev/dist/assets/ray-casting-modules-Dkfw4tdc.js +765 -0
  14. package/dev/dist/assets/sdf-CS0uBonx.js +66 -0
  15. package/dev/dist/assets/symbol-BL_7GJ0M.js +67 -0
  16. package/dev/dist/assets/tag-C0Vskpp8.js +833 -0
  17. package/dev/dist/assets/tetris-D8mokodr.js +344 -0
  18. package/dev/dist/assets/utils-2dzuv_bW.js +62 -0
  19. package/dev/dist/index.html +51 -0
  20. package/dist/chunk-BZDGEIOT.mjs +278 -0
  21. package/dist/chunk-BZDGEIOT.mjs.map +1 -0
  22. package/dist/chunk-X4M3LV4R.mjs +433 -0
  23. package/dist/chunk-X4M3LV4R.mjs.map +1 -0
  24. package/dist/gl.d.mts +2 -2
  25. package/dist/gl.mjs +1 -1
  26. package/dist/index.d.mts +23 -6
  27. package/dist/index.mjs +9 -2
  28. package/dist/tag.d.mts +6 -1
  29. package/dist/tag.mjs +13 -5
  30. package/dist/tag.mjs.map +1 -1
  31. package/dist/{types-DTSPQclW.d.mts → types-CNdqh7tB.d.mts} +121 -8
  32. package/package.json +1 -1
  33. package/src/gl.ts +13 -2
  34. package/src/index.ts +294 -122
  35. package/src/tag.ts +18 -2
  36. package/src/types.ts +127 -7
  37. package/src/utils.ts +45 -10
  38. package/test/index.test.ts +339 -84
  39. package/test/mocks/webgl.ts +20 -0
  40. package/test/tag.test.ts +41 -0
  41. package/dist/chunk-CJTT7WPV.mjs +0 -75
  42. package/dist/chunk-CJTT7WPV.mjs.map +0 -1
  43. package/dist/chunk-ZP4HP5H7.mjs +0 -405
  44. package/dist/chunk-ZP4HP5H7.mjs.map +0 -1
package/src/index.ts CHANGED
@@ -9,10 +9,14 @@ import type {
9
9
  BufferView,
10
10
  GL,
11
11
  UniformView as InferUniformView,
12
+ GLUsage,
13
+ InterleavedAttributeLayout,
12
14
  InterleavedAttributeMethods,
13
15
  InterleavedAttributeSchema,
14
16
  InterleavedAttributeView,
15
17
  UniformSchema,
18
+ VertexArrayMethods,
19
+ VertexArrayParticipant,
16
20
  View,
17
21
  ViewOptions,
18
22
  ViewSchema,
@@ -24,9 +28,13 @@ import {
24
28
  assertedNotNullish,
25
29
  kindToSize,
26
30
  forEachObject,
31
+ once,
27
32
  } from './utils'
28
33
  import { createUpsertMap, mapObject } from './utils'
29
34
  export * from './types'
35
+ // Documented under Utils but reachable from nowhere until now: utils is not a
36
+ // build entry, and named rather than star so the internal helpers stay internal.
37
+ export { createFramebuffer, createTexture } from './utils'
30
38
 
31
39
  /**********************************************************************************/
32
40
  /* */
@@ -70,20 +78,132 @@ export function view<TSchema extends ViewSchema>(
70
78
  gl: GL,
71
79
  program: WebGLProgram,
72
80
  schema: TSchema,
73
- options?: ViewOptions,
81
+ { signal }: ViewOptions = {},
74
82
  ): View<TSchema> {
83
+ // The sub-views are built without the signal and the view wires disposal
84
+ // once, so that `dispose()` and aborting do the same thing rather than each
85
+ // doing half of it.
86
+ const attributes = !schema.attributes ? undefined : attributeView(gl, program, schema.attributes)
87
+ const interleavedAttributes = !schema.interleavedAttributes
88
+ ? undefined
89
+ : interleavedAttributeView(gl, program, schema.interleavedAttributes)
90
+
91
+ // Vertex arrays are made on demand rather than declared, so they are the one
92
+ // thing the view cannot dispose from the schema alone. Held until disposed —
93
+ // by the view or by the caller — and dropped either way, so a view that hands
94
+ // out an array per frame does not accumulate them.
95
+ const vertexArrays = new Set<VertexArrayMethods>()
96
+
97
+ const dispose = once(() => {
98
+ if (attributes) forEachObject(attributes, value => value.dispose())
99
+ if (interleavedAttributes) forEachObject(interleavedAttributes, value => value.dispose())
100
+ vertexArrays.forEach(vertexArray => vertexArray.dispose())
101
+ vertexArrays.clear()
102
+ })
103
+
104
+ signal?.addEventListener('abort', dispose)
105
+
75
106
  return {
76
107
  uniforms: !schema.uniforms ? undefined : uniformView(gl, program, schema.uniforms),
77
- attributes: !schema.attributes
78
- ? undefined
79
- : attributeView(gl, program, schema.attributes, options),
80
- interleavedAttributes: !schema.interleavedAttributes
81
- ? undefined
82
- : interleavedAttributeView(gl, program, schema.interleavedAttributes, options),
83
- buffers: !schema.buffers ? undefined : bufferView(gl, schema.buffers),
108
+ attributes,
109
+ interleavedAttributes,
110
+ vao(participants: VertexArrayParticipant[]) {
111
+ const vertexArray = vaoView(gl, participants)
112
+ vertexArrays.add(vertexArray)
113
+ return {
114
+ ...vertexArray,
115
+ dispose: once(() => {
116
+ vertexArrays.delete(vertexArray)
117
+ vertexArray.dispose()
118
+ }),
119
+ }
120
+ },
121
+ dispose,
84
122
  } as View<TSchema>
85
123
  }
86
124
 
125
+ /**********************************************************************************/
126
+ /* */
127
+ /* Vertex Array */
128
+ /* */
129
+ /**********************************************************************************/
130
+
131
+ /**
132
+ * Bind the default vertex array, returning a disposer that puts the previous
133
+ * one back. The pointer calls name no destination, so anything applied without
134
+ * a vertex array of its own has to say which one it means — and a vertex array
135
+ * keeps what it is given, so writing into a caller's array by accident would
136
+ * outlive the call that did it.
137
+ */
138
+ function bindDefaultVertexArray(gl: GL): () => void {
139
+ const feature = getVertexArrayObject(gl)
140
+ if (!feature) {
141
+ return () => {}
142
+ }
143
+ const previous = gl.getParameter(VERTEX_ARRAY_BINDING) as WebGLVertexArrayObject | null
144
+ feature.bindVertexArray(null)
145
+ return () => feature.bindVertexArray(previous)
146
+ }
147
+
148
+ /**
149
+ * A vertex array over the participants one draw needs.
150
+ *
151
+ * Where the context has vertex arrays, each participant writes itself in once,
152
+ * here, and `bind()` is a single `bindVertexArray`. Where it does not — WebGL1
153
+ * without `OES_vertex_array_object` — there is nowhere to keep the state, so
154
+ * `bind()` re-applies every participant and the disposer puts back what they
155
+ * displaced. Same contract, and the emulation is why the name still fits.
156
+ */
157
+ export function vaoView(
158
+ gl: GL,
159
+ participants: VertexArrayParticipant[],
160
+ { signal }: ViewOptions = {},
161
+ ): VertexArrayMethods {
162
+ const feature = getVertexArrayObject(gl)
163
+
164
+ const methods: VertexArrayMethods = !feature
165
+ ? {
166
+ bind() {
167
+ const restores = participants.map(participant => participant.applyToVertexArray())
168
+ participants.forEach(participant => participant.applyToContext?.())
169
+ return once(() => restores.forEach(restore => restore()))
170
+ },
171
+ unbind() {},
172
+ dispose() {},
173
+ }
174
+ : (() => {
175
+ const vertexArray = assertedNotNullish(feature.createVertexArray())
176
+ const previous = gl.getParameter(VERTEX_ARRAY_BINDING) as WebGLVertexArrayObject | null
177
+ feature.bindVertexArray(vertexArray)
178
+ // Discarding the restorers is the point: the state lives in this array
179
+ // now, and the array is thrown away whole.
180
+ for (const participant of participants) {
181
+ participant.applyToVertexArray()
182
+ }
183
+ feature.bindVertexArray(previous)
184
+
185
+ return {
186
+ bind() {
187
+ const previousVertexArray = gl.getParameter(
188
+ VERTEX_ARRAY_BINDING,
189
+ ) as WebGLVertexArrayObject | null
190
+ feature.bindVertexArray(vertexArray)
191
+ // Context state the array cannot hold — see applyToContext.
192
+ participants.forEach(participant => participant.applyToContext?.())
193
+ return once(() => feature.bindVertexArray(previousVertexArray))
194
+ },
195
+ unbind() {
196
+ feature.bindVertexArray(null)
197
+ },
198
+ dispose: once(() => feature.deleteVertexArray(vertexArray)),
199
+ }
200
+ })()
201
+
202
+ signal?.addEventListener('abort', () => methods.dispose())
203
+
204
+ return methods
205
+ }
206
+
87
207
  /**********************************************************************************/
88
208
  /* */
89
209
  /* Uniform View */
@@ -246,17 +366,6 @@ function readDivisor(gl: GL, location: number): number {
246
366
  : 0
247
367
  }
248
368
 
249
- // Wraps a restore so it runs once: a disposer is per bind() and undoing twice
250
- // would put back state a later bind() is relying on
251
- function once(restore: () => void): () => void {
252
- let done = false
253
- return () => {
254
- if (done) return
255
- done = true
256
- restore()
257
- }
258
- }
259
-
260
369
  export function attributeView<T extends AttributeSchema>(
261
370
  gl: GL,
262
371
  program: WebGLProgram,
@@ -266,17 +375,17 @@ export function attributeView<T extends AttributeSchema>(
266
375
  const attributes = mapObject(
267
376
  schema,
268
377
  (
269
- {
270
- kind,
271
- format,
272
- normalized = false,
273
- instanced,
274
- buffer = assertedNotNullish(gl.createBuffer()),
275
- },
378
+ { kind, format, normalized = false, instanced, buffer: providedBuffer },
276
379
  key,
277
380
  ): AttributeMethods => {
278
381
  const name = toID(key)
279
382
 
383
+ // Whoever made the buffer deletes it. A schema can name a buffer that
384
+ // belongs to another view — the quad `compile.toQuad` shares across every
385
+ // program on a context, an attribute two programs step over — and
386
+ // deleting one of those would take it out from under its owner.
387
+ const buffer = providedBuffer ?? assertedNotNullish(gl.createBuffer())
388
+
280
389
  const location = gl.getAttribLocation(program, name)
281
390
  if (location < 0) {
282
391
  throw new Error(`Attribute '${name}' not found`)
@@ -287,20 +396,35 @@ export function attributeView<T extends AttributeSchema>(
287
396
  const glType = FORMAT_TO_GL_TYPE[resolvedFormat]
288
397
  const isIntKind = kind.startsWith('i') || kind.startsWith('u')
289
398
 
399
+ function applyToVertexArray() {
400
+ // Snapshot before the change, restore in the disposer
401
+ const previousDivisor = readDivisor(gl, location)
402
+ // vertexAttribPointer captures whatever is bound to ARRAY_BUFFER when
403
+ // it runs, so this binding is the setup for the pointer, not a lasting
404
+ // part of the vertex array's state.
405
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
406
+ handleAttribute(gl, location, size, 0, 0, glType, isIntKind, normalized, instanced)
407
+ return once(() => {
408
+ getInstancedArrays(gl)?.vertexAttribDivisor(location, previousDivisor)
409
+ })
410
+ }
411
+
290
412
  return {
291
413
  buffer,
414
+ applyToVertexArray,
292
415
  bind() {
293
- // Snapshot before the change, restore in the disposer
294
- const previousDivisor = readDivisor(gl, location)
295
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
296
- handleAttribute(gl, location, size, 0, 0, glType, isIntKind, normalized, instanced)
416
+ const restoreVertexArray = bindDefaultVertexArray(gl)
417
+ const restore = applyToVertexArray()
297
418
  return once(() => {
298
- getInstancedArrays(gl)?.vertexAttribDivisor(location, previousDivisor)
419
+ restore()
420
+ restoreVertexArray()
299
421
  })
300
422
  },
301
- dispose() {
302
- gl.deleteBuffer(buffer)
303
- },
423
+ dispose: once(() => {
424
+ if (!providedBuffer) {
425
+ gl.deleteBuffer(buffer)
426
+ }
427
+ }),
304
428
  set(data, usage = 'STATIC_DRAW') {
305
429
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
306
430
  gl.bufferData(gl.ARRAY_BUFFER, data, gl[usage])
@@ -323,6 +447,73 @@ export function attributeView<T extends AttributeSchema>(
323
447
  /* */
324
448
  /**********************************************************************************/
325
449
 
450
+ /**
451
+ * The other place an attribute's value can come from: not a buffer, but the
452
+ * context's current generic vertex attribute values, which an attribute reads
453
+ * for every vertex when its array is disabled.
454
+ *
455
+ * A constant is a uniform in all but name — one value for the whole draw, set
456
+ * by a function chosen from the kind — so it gets the setters a uniform gets,
457
+ * off the same layout the buffer source uses.
458
+ *
459
+ * `vertexAttrib*` tops out at four floats and has no matrix form, so a layout
460
+ * key that is fine in a buffer can be impossible as a constant. That is a hard
461
+ * limit of the API rather than a choice, and it is worth saying so out loud at
462
+ * the point someone reaches for it.
463
+ */
464
+ function constantSource(gl: GL, layout: InterleavedAttributeLayout[], locations: number[]) {
465
+ const setters = layout.map((entry, index) => {
466
+ const size = kindToSize(entry.kind)
467
+ const name = toID(entry.key)
468
+ if (isMatKind(entry.kind) || size > 4) {
469
+ throw new Error(
470
+ `'${name}' is a ${entry.kind}, which cannot be a constant attribute: vertexAttrib* takes at most 4 floats and has no matrix form. Use the buffer source for it.`,
471
+ )
472
+ }
473
+ // The values are context state, not vertex-array state, so they are held
474
+ // here and written again on every bind — see applyToContext.
475
+ let values: number[] = []
476
+ const fn = gl[`vertexAttrib${size as 1 | 2 | 3 | 4}f`].bind(gl)
477
+ return {
478
+ name,
479
+ location: locations[index]!,
480
+ write: () => {
481
+ if (values.length) {
482
+ ;(fn as (location: number, ...args: number[]) => void)(locations[index]!, ...values)
483
+ }
484
+ },
485
+ methods: {
486
+ set(...args: number[]) {
487
+ values = args
488
+ },
489
+ },
490
+ }
491
+ })
492
+
493
+ return Object.assign(Object.fromEntries(setters.map(setter => [setter.name, setter.methods])), {
494
+ applyToVertexArray() {
495
+ const restores = setters.map(({ location }) => {
496
+ const wasEnabled = gl.getVertexAttrib(location, gl.VERTEX_ATTRIB_ARRAY_ENABLED) as boolean
497
+ gl.disableVertexAttribArray(location)
498
+ return () => (wasEnabled ? gl.enableVertexAttribArray(location) : undefined)
499
+ })
500
+ return once(() => restores.forEach(restore => restore()))
501
+ },
502
+ applyToContext() {
503
+ setters.forEach(setter => setter.write())
504
+ },
505
+ bind() {
506
+ const restoreVertexArray = bindDefaultVertexArray(gl)
507
+ const restore = this.applyToVertexArray()
508
+ this.applyToContext()
509
+ return once(() => {
510
+ restore()
511
+ restoreVertexArray()
512
+ })
513
+ },
514
+ })
515
+ }
516
+
326
517
  export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
327
518
  gl: GL,
328
519
  program: WebGLProgram,
@@ -330,7 +521,7 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
330
521
  { signal }: ViewOptions = {},
331
522
  ): InterleavedAttributeView<T> {
332
523
  // Initialize interleaved attributes
333
- const interleavedAttributes = mapObject(schema, ({ layout, instanced }) => {
524
+ const interleavedAttributes = mapObject(schema, ({ layout }) => {
334
525
  // Increment number to keep track of offset
335
526
  let index = 0
336
527
 
@@ -355,7 +546,7 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
355
546
  const normalized = layout.normalized ?? false
356
547
  index += size * FORMAT_BYTE_SIZE[resolvedFormat]
357
548
 
358
- return () =>
549
+ return (divisor: 0 | 1) =>
359
550
  handleAttribute(
360
551
  gl,
361
552
  location,
@@ -365,7 +556,7 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
365
556
  glType,
366
557
  isIntKind,
367
558
  normalized,
368
- instanced,
559
+ divisor === 1,
369
560
  )
370
561
  })
371
562
 
@@ -375,91 +566,61 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
375
566
  // Create a buffer
376
567
  const buffer = assertedNotNullish(gl.createBuffer())
377
568
 
378
- // Create VAO to cache attribute state
379
- let vao: { unbind(): void; bind(): void; dispose(): void } | undefined = undefined
380
-
381
- // Get VAO-feature: extension if webgl1, gl if webgl2
382
- const feature = getVertexArrayObject(gl)
383
- if (feature) {
384
- const vertexArray = feature.createVertexArray()
385
- feature.bindVertexArray(vertexArray)
569
+ // This layout used to create a vertex array of its own, here, which is why
570
+ // the other participants in a draw had to be bound after it to be included
571
+ // — the array they landed in was whichever one happened to be current.
572
+ // Arrays are `vao()`'s job now; a layout is a layout and a buffer.
573
+ function applyToVertexArray(divisor: 0 | 1) {
574
+ const previousDivisors = locations.map(
575
+ location => [location, readDivisor(gl, location)] as const,
576
+ )
386
577
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
387
578
  for (const handle of handles) {
388
- handle()
389
- }
390
- feature.bindVertexArray(null)
391
- vao = {
392
- unbind() {
393
- feature.bindVertexArray(null)
394
- },
395
- bind() {
396
- feature.bindVertexArray(vertexArray)
397
- },
398
- dispose() {
399
- feature.deleteVertexArray(vertexArray)
400
- },
579
+ handle(divisor)
401
580
  }
581
+ return once(() => {
582
+ const instancedArrays = getInstancedArrays(gl)
583
+ previousDivisors.forEach(([location, previous]) => {
584
+ instancedArrays?.vertexAttribDivisor(location, previous)
585
+ })
586
+ })
402
587
  }
403
588
 
404
- // A selected vertex array stays selected for every later draw, which would
405
- // then write its own attributes into this one
406
- const unbind = () => {
407
- if (vao) {
408
- vao.unbind()
589
+ let constant: ReturnType<typeof constantSource> | undefined
590
+
591
+ /** One layout, one buffer, but a divisor per draw rather than per schema. */
592
+ function bufferSource(divisor: 0 | 1) {
593
+ const apply = () => applyToVertexArray(divisor)
594
+ return {
595
+ applyToVertexArray: apply,
596
+ bind() {
597
+ const restoreVertexArray = bindDefaultVertexArray(gl)
598
+ const restore = apply()
599
+ return once(() => {
600
+ restore()
601
+ restoreVertexArray()
602
+ })
603
+ },
409
604
  }
410
605
  }
411
606
 
412
607
  return {
413
- bind() {
414
- // Snapshot before the change, restore in the disposer
415
- const previousVertexArray = feature
416
- ? (gl.getParameter(VERTEX_ARRAY_BINDING) as WebGLVertexArrayObject | null)
417
- : null
418
- const previousDivisors = vao
419
- ? []
420
- : locations.map(location => [location, readDivisor(gl, location)] as const)
421
-
422
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
423
- if (vao) {
424
- vao.bind()
425
- } else {
426
- // Fallback: manual attribute setup
427
- for (const handle of handles) {
428
- handle()
429
- }
430
- }
431
-
432
- return once(() => {
433
- if (feature) {
434
- feature.bindVertexArray(previousVertexArray)
435
- }
436
- // Without a vertex array the manual setup wrote divisors straight to
437
- // the locations, so they are ours to put back
438
- if (!vao) {
439
- const instancedArrays = getInstancedArrays(gl)
440
- previousDivisors.forEach(([location, divisor]) => {
441
- instancedArrays?.vertexAttribDivisor(location, divisor)
442
- })
443
- }
444
- })
445
- },
446
- unbind,
447
- dispose() {
448
- gl.deleteBuffer(buffer)
449
- if (vao) {
450
- vao.dispose()
451
- }
452
- },
453
- set(value, usage = 'STATIC_DRAW') {
454
- if (vao) {
455
- vao.bind()
456
- }
457
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
458
- gl.bufferData(gl.ARRAY_BUFFER, value, gl[usage])
459
- if (vao) {
460
- vao.unbind()
461
- }
608
+ buffer: Object.assign(bufferSource(0), {
609
+ /** The same buffer, stepped once per instance instead of once per vertex. */
610
+ perInstance: bufferSource(1),
611
+ set(value: Float32Array, usage: GLUsage = 'STATIC_DRAW') {
612
+ // Uploading is storage, not binding: ARRAY_BUFFER is not vertex-array
613
+ // state, so this touches no array, whichever one is bound.
614
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
615
+ gl.bufferData(gl.ARRAY_BUFFER, value, gl[usage])
616
+ },
617
+ }),
618
+ // Lazy: a layout key can be perfectly good in a buffer and impossible as
619
+ // a constant, and finding that out should wait until someone asks for one.
620
+ get constant() {
621
+ return (constant ??= constantSource(gl, layout, locations))
462
622
  },
623
+ dispose: once(() => gl.deleteBuffer(buffer)),
463
624
  } satisfies InterleavedAttributeMethods
464
625
  })
465
626
 
@@ -485,21 +646,32 @@ export function bufferView<T extends BufferSchema>(
485
646
  const buffers = mapObject(schema, ({ target = 'ARRAY_BUFFER', usage = 'STATIC_DRAW' }) => {
486
647
  const buffer = assertedNotNullish(gl.createBuffer())
487
648
 
649
+ function applyToVertexArray() {
650
+ // Snapshot before the change, restore in the disposer. Restoring the
651
+ // previous buffer rather than null matters for ELEMENT_ARRAY_BUFFER,
652
+ // whose binding is recorded in the bound vertex array: clearing it
653
+ // would strip the index buffer from an array the caller may not own.
654
+ const previousBuffer = gl.getParameter(gl[`${target}_BINDING`]) as WebGLBuffer | null
655
+ gl.bindBuffer(gl[target], buffer)
656
+ return once(() => {
657
+ gl.bindBuffer(gl[target], previousBuffer)
658
+ })
659
+ }
660
+
488
661
  return {
662
+ applyToVertexArray,
489
663
  bind() {
490
- // Snapshot before the change, restore in the disposer. Restoring the
491
- // previous buffer rather than null matters for ELEMENT_ARRAY_BUFFER,
492
- // whose binding is recorded in the bound vertex array: clearing it
493
- // would strip the index buffer from an array the caller may not own.
494
- const previousBuffer = gl.getParameter(gl[`${target}_BINDING`]) as WebGLBuffer | null
495
- gl.bindBuffer(gl[target], buffer)
664
+ // ELEMENT_ARRAY_BUFFER's binding belongs to the bound vertex array, so
665
+ // like the attributes this has to say which array it means. ARRAY_BUFFER
666
+ // is context state and unaffected either way.
667
+ const restoreVertexArray = bindDefaultVertexArray(gl)
668
+ const restore = applyToVertexArray()
496
669
  return once(() => {
497
- gl.bindBuffer(gl[target], previousBuffer)
670
+ restore()
671
+ restoreVertexArray()
498
672
  })
499
673
  },
500
- dispose() {
501
- gl.deleteBuffer(buffer)
502
- },
674
+ dispose: once(() => gl.deleteBuffer(buffer)),
503
675
  set(data: Float32Array) {
504
676
  gl.bindBuffer(gl[target], buffer)
505
677
  gl.bufferData(gl[target], data, gl[usage])
package/src/tag.ts CHANGED
@@ -18,7 +18,7 @@ import type {
18
18
  ViewSchema,
19
19
  ViewSchemaPartial,
20
20
  } from './types'
21
- import { createUpsertMap } from './utils'
21
+ import { createUpsertMap, once } from './utils'
22
22
 
23
23
  export function glsl<TSlot extends GLSLSlot, const TSlots extends TSlot[]>(
24
24
  template: TemplateStringsArray,
@@ -99,6 +99,11 @@ export function interleave<
99
99
  export interface CompileOptions<TSchema extends ViewSchemaPartial> {
100
100
  schema?: TSchema
101
101
  webgl2?: boolean
102
+ /**
103
+ * Disposes what `compile` made: the program, and the buffers the view created
104
+ * for itself. Buffers named by the schema belong to whoever made them.
105
+ */
106
+ signal?: AbortSignal
102
107
  }
103
108
 
104
109
  export function compile<
@@ -145,10 +150,21 @@ export function compile<
145
150
  try {
146
151
  const program = createProgram(gl, _vertex.template, _fragment.template)
147
152
 
153
+ // The same rule the views follow: `compile` made this program, so `compile`
154
+ // is what deletes it. A program handed in from outside would not be ours to
155
+ // delete, and there is no way to hand one in.
156
+ const resolvedView = view(gl, program, schema)
157
+ const dispose = once(() => {
158
+ resolvedView.dispose()
159
+ gl.deleteProgram(program)
160
+ })
161
+ options?.signal?.addEventListener('abort', dispose)
162
+
148
163
  return {
149
164
  program,
150
165
  schema,
151
- view: view(gl, program, schema),
166
+ view: resolvedView,
167
+ dispose,
152
168
  vertex: _vertex.template,
153
169
  fragment: _fragment.template,
154
170
  } as unknown as Prettify<CompileResult<TVertex, TFragment, TSchema>>