@bigmistqke/view.gl 0.2.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.
Files changed (43) hide show
  1. package/README.md +94 -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-EYXOSM7B.mjs → chunk-H56LJYS5.mjs} +63 -133
  23. package/dist/chunk-H56LJYS5.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 +13 -4
  27. package/dist/index.mjs +7 -2
  28. package/dist/tag.d.mts +6 -1
  29. package/dist/tag.mjs +19 -6
  30. package/dist/tag.mjs.map +1 -1
  31. package/dist/{types-CyGyGi18.d.mts → types-k1Xz1iO3.d.mts} +44 -2
  32. package/package.json +1 -1
  33. package/src/gl.ts +13 -2
  34. package/src/index.ts +63 -39
  35. package/src/tag.ts +23 -2
  36. package/src/types.ts +43 -1
  37. package/src/utils.ts +45 -10
  38. package/test/index.test.ts +145 -29
  39. package/test/mocks/webgl.ts +14 -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-EYXOSM7B.mjs.map +0 -1
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<
@@ -127,6 +132,11 @@ export function compile<
127
132
  ..._vertex.schema.interleavedAttributes,
128
133
  ..._fragment.schema.interleavedAttributes,
129
134
  },
135
+ // Never produced by a tag — a buffer has no name in the source to be
136
+ // parsed out of one — but `options.schema` may name buffers for the view to
137
+ // own, and the deep merge below indexes this object by every key it finds
138
+ // there. Missing, that threw.
139
+ buffers: {} as NonNullable<ViewSchema['buffers']>,
130
140
  }
131
141
 
132
142
  // Deep merge options.schema with generated schema
@@ -145,10 +155,21 @@ export function compile<
145
155
  try {
146
156
  const program = createProgram(gl, _vertex.template, _fragment.template)
147
157
 
158
+ // The same rule the views follow: `compile` made this program, so `compile`
159
+ // is what deletes it. A program handed in from outside would not be ours to
160
+ // delete, and there is no way to hand one in.
161
+ const resolvedView = view(gl, program, schema)
162
+ const dispose = once(() => {
163
+ resolvedView.dispose()
164
+ gl.deleteProgram(program)
165
+ })
166
+ options?.signal?.addEventListener('abort', dispose)
167
+
148
168
  return {
149
169
  program,
150
170
  schema,
151
- view: view(gl, program, schema),
171
+ view: resolvedView,
172
+ dispose,
152
173
  vertex: _vertex.template,
153
174
  fragment: _fragment.template,
154
175
  } as unknown as Prettify<CompileResult<TVertex, TFragment, TSchema>>
package/src/types.ts CHANGED
@@ -365,6 +365,15 @@ export interface AttributeMethods<T extends AttributeDefinition = AttributeDefin
365
365
  */
366
366
  bind(): () => void
367
367
  set(data: FormatToArray[ResolvedFormat<T>], usage?: GLUsage): { bind(): () => void }
368
+ /**
369
+ * Delete the buffer, if this attribute is the one that made it.
370
+ *
371
+ * A buffer named by the schema came from somewhere else and is deleted by
372
+ * whoever made it — a shared buffer is the point of passing one in, and one
373
+ * view cannot know whether the others are done. So this is safe to call
374
+ * twice and safe to call on a borrowed buffer, and it is not a way to free
375
+ * one: dispose the view that created it.
376
+ */
368
377
  dispose(): void
369
378
  }
370
379
 
@@ -502,6 +511,12 @@ export interface TextureDefinition {
502
511
  export interface Framebuffer {
503
512
  texture: WebGLTexture
504
513
  framebuffer: WebGLFramebuffer
514
+ /**
515
+ * Delete the framebuffer, and the texture only if `createFramebuffer` made
516
+ * it. A texture handed in belongs to whoever made it. Idempotent, and exactly
517
+ * what aborting the signal does.
518
+ */
519
+ dispose(): void
505
520
  }
506
521
 
507
522
  /**********************************************************************************/
@@ -617,6 +632,21 @@ export interface ViewOptions {
617
632
  signal?: AbortSignal
618
633
  }
619
634
 
635
+ /**
636
+ * What a view builds for one program.
637
+ *
638
+ * A buffer declared here is OWNED by the view: it lives and dies with it, on
639
+ * the one signal, which is the common case — an index buffer whose lifetime is
640
+ * its program's has no reason to be declared apart from the attributes it is
641
+ * drawn with.
642
+ *
643
+ * The other two lifetimes have their own spellings, and neither is this one. A
644
+ * buffer that outlives the view, or is shared between views, is a standalone
645
+ * `bufferView` — `compile.toQuad`'s per-context quad, a buffer handed from one
646
+ * program's view to another's. A buffer that belongs to somebody else is NAMED
647
+ * by an attribute schema, and is borrowed: the view uses it and never deletes
648
+ * it.
649
+ */
620
650
  export interface ViewSchema {
621
651
  uniforms?: UniformSchema
622
652
  attributes?: AttributeSchema
@@ -641,8 +671,8 @@ export type View<T extends ViewSchema = ViewSchema> = {
641
671
  interleavedAttributes: T['interleavedAttributes'] extends InterleavedAttributeSchema
642
672
  ? Prettify<InterleavedAttributeView<T['interleavedAttributes']>>
643
673
  : {}
644
- buffers: T['buffers'] extends BufferSchema ? Prettify<BufferView<T['buffers']>> : {}
645
674
  uniforms: T['uniforms'] extends UniformSchema ? Prettify<UniformView<T['uniforms']>> : {}
675
+ buffers: T['buffers'] extends BufferSchema ? Prettify<BufferView<T['buffers']>> : {}
646
676
  /**
647
677
  * Build a vertex array from the participants one draw needs — its attributes,
648
678
  * its interleaved layouts, its element buffer.
@@ -653,6 +683,13 @@ export type View<T extends ViewSchema = ViewSchema> = {
653
683
  * something you say rather than something you inherit from call order.
654
684
  */
655
685
  vao(participants: VertexArrayParticipant[]): VertexArrayMethods
686
+ /**
687
+ * Delete everything the view made: the buffers behind its attributes and
688
+ * layouts, and any vertex array still held from `vao()`. Buffers named by the
689
+ * schema are left alone — those belong to whoever made them. Idempotent, and
690
+ * exactly what aborting the signal does.
691
+ */
692
+ dispose(): void
656
693
  }
657
694
 
658
695
  /**********************************************************************************/
@@ -881,5 +918,10 @@ export type CompileResult<
881
918
  view: Prettify<View<TSchema>>
882
919
  vertex: string
883
920
  fragment: string
921
+ /**
922
+ * Delete the program and everything its view made. Idempotent, and
923
+ * exactly what aborting the signal does.
924
+ */
925
+ dispose(): void
884
926
  }
885
927
  : never
package/src/utils.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  KIND_TO_UNIFORM_FN_NAME_MAP,
7
7
  TextureDefinition,
8
8
  UniformKind,
9
+ ViewOptions,
9
10
  } from './types'
10
11
 
11
12
  export function assertedNotNullish<T>(value: T, message?: string): NonNullable<T> {
@@ -13,6 +14,24 @@ export function assertedNotNullish<T>(value: T, message?: string): NonNullable<T
13
14
  return value
14
15
  }
15
16
 
17
+ /**
18
+ * Wraps a teardown so it runs once.
19
+ *
20
+ * Two reasons, and both bite. A disposer is per `bind()`, and undoing twice
21
+ * would put back state a later `bind()` is relying on. And a `dispose()` that
22
+ * deletes twice invalidates a handle someone else may still be holding — the
23
+ * object survives while it is bound, so the fault surfaces later, somewhere
24
+ * else. Idempotent is the only safe shape for either.
25
+ */
26
+ export function once(teardown: () => void): () => void {
27
+ let done = false
28
+ return () => {
29
+ if (done) return
30
+ done = true
31
+ teardown()
32
+ }
33
+ }
34
+
16
35
  /**********************************************************************************/
17
36
  /* */
18
37
  /* Kind To Uniform Name */
@@ -114,8 +133,9 @@ export function createTexture(
114
133
  height,
115
134
  }: TextureDefinition,
116
135
  data?: ArrayBufferView | null,
136
+ { signal }: ViewOptions = {},
117
137
  ): WebGLTexture {
118
- const texture = gl.createTexture()
138
+ const texture = assertedNotNullish(gl.createTexture(), 'Failed to create texture')
119
139
 
120
140
  function getTextureConstant(name: string) {
121
141
  if (!(name in gl)) {
@@ -137,13 +157,17 @@ export function createTexture(
137
157
  data ?? null,
138
158
  )
139
159
 
140
- console.log(minFilter)
141
160
  // Set texture parameters
142
161
  gl.texParameteri(gl[target], gl.TEXTURE_MIN_FILTER, gl[minFilter])
143
162
  gl.texParameteri(gl[target], gl.TEXTURE_MAG_FILTER, gl[magFilter])
144
163
  gl.texParameteri(gl[target], gl.TEXTURE_WRAP_S, gl[wrapS])
145
164
  gl.texParameteri(gl[target], gl.TEXTURE_WRAP_T, gl[wrapT])
146
165
 
166
+ signal?.addEventListener(
167
+ 'abort',
168
+ once(() => gl.deleteTexture(texture)),
169
+ )
170
+
147
171
  return texture
148
172
  }
149
173
 
@@ -162,7 +186,7 @@ const FRAMEBUFFER_ATTACHMENT_MAP = {
162
186
 
163
187
  class FramebufferError extends Error {
164
188
  constructor(gl: GL, status: number) {
165
- let errorMessage = `Framebuffer '${name}' not complete. Status: `
189
+ let errorMessage = 'Framebuffer not complete. Status: '
166
190
  switch (status) {
167
191
  case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
168
192
  errorMessage += 'FRAMEBUFFER_INCOMPLETE_ATTACHMENT'
@@ -185,16 +209,15 @@ class FramebufferError extends Error {
185
209
 
186
210
  export function createFramebuffer(
187
211
  gl: GL,
188
- { attachment, texture, ...options }: FramebufferDefinition,
212
+ { attachment, texture: providedTexture, ...definition }: FramebufferDefinition,
213
+ { signal }: ViewOptions = {},
189
214
  ) {
190
- // Create texture for the framebuffer
191
- texture ??= createTexture(gl, options)
215
+ // Create texture for the framebuffer, unless one was handed in. Which one it
216
+ // is decides who deletes it below — the same rule the attribute buffers follow.
217
+ const texture = providedTexture ?? createTexture(gl, definition)
192
218
 
193
219
  // Create framebuffer
194
- const framebuffer = assertedNotNullish(
195
- gl.createFramebuffer(),
196
- `Failed to create framebuffer: ${name}`,
197
- )
220
+ const framebuffer = assertedNotNullish(gl.createFramebuffer(), 'Failed to create framebuffer')
198
221
 
199
222
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
200
223
 
@@ -213,9 +236,21 @@ export function createFramebuffer(
213
236
  throw new FramebufferError(gl, status)
214
237
  }
215
238
 
239
+ const dispose = once(() => {
240
+ gl.deleteFramebuffer(framebuffer)
241
+ // A texture handed in belongs to whoever made it — it commonly outlives the
242
+ // framebuffer, ping-ponged between two of them.
243
+ if (!providedTexture) {
244
+ gl.deleteTexture(texture)
245
+ }
246
+ })
247
+
248
+ signal?.addEventListener('abort', dispose)
249
+
216
250
  return {
217
251
  texture,
218
252
  framebuffer,
253
+ dispose,
219
254
  }
220
255
  }
221
256
 
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
2
  import {
3
3
  attributeView,
4
4
  bufferView,
5
+ createFramebuffer,
6
+ createTexture,
5
7
  interleavedAttributeView,
6
8
  uniformView,
7
9
  vaoView,
@@ -45,7 +47,6 @@ describe('view', () => {
45
47
  expect(result.uniforms!.u_texture.set).toBeInstanceOf(Function)
46
48
  expect(result.attributes).toBeUndefined()
47
49
  expect(result.interleavedAttributes).toBeUndefined()
48
- expect(result.buffers).toBeUndefined()
49
50
  })
50
51
 
51
52
  it('should create a view with attributes', () => {
@@ -91,23 +92,6 @@ describe('view', () => {
91
92
  expect(result.interleavedAttributes!.data.buffer.set).toBeInstanceOf(Function)
92
93
  })
93
94
 
94
- it('should create a view with buffers', () => {
95
- const schema = {
96
- buffers: {
97
- vertices: { target: 'ARRAY_BUFFER', usage: 'STATIC_DRAW' },
98
- indices: { target: 'ELEMENT_ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
99
- },
100
- } satisfies ViewSchema
101
-
102
- const result = view(gl, program, schema)
103
-
104
- expect(result.buffers).toBeDefined()
105
- expect(result.buffers!.vertices).toBeDefined()
106
- expect(result.buffers!.vertices.bind).toBeInstanceOf(Function)
107
- expect(result.buffers!.vertices.dispose).toBeInstanceOf(Function)
108
- expect(result.buffers!.vertices.set).toBeInstanceOf(Function)
109
- })
110
-
111
95
  it('should create a view with all components', () => {
112
96
  const schema = {
113
97
  uniforms: {
@@ -122,9 +106,6 @@ describe('view', () => {
122
106
  instanced: false,
123
107
  },
124
108
  },
125
- buffers: {
126
- vertices: {},
127
- },
128
109
  } satisfies ViewSchema
129
110
 
130
111
  const result = view(gl, program, schema)
@@ -132,14 +113,12 @@ describe('view', () => {
132
113
  expect(result.uniforms).toBeDefined()
133
114
  expect(result.attributes).toBeDefined()
134
115
  expect(result.interleavedAttributes).toBeDefined()
135
- expect(result.buffers).toBeDefined()
136
116
  })
137
117
 
138
118
  it('should create a view with symbols for all component types', () => {
139
119
  const u_time_symbol = Symbol('u_time')
140
120
  const a_position_symbol = Symbol('a_position')
141
121
  const data_symbol = Symbol('data')
142
- const vertices_symbol = Symbol('vertices')
143
122
 
144
123
  const schema = {
145
124
  uniforms: {
@@ -154,9 +133,6 @@ describe('view', () => {
154
133
  instanced: false,
155
134
  },
156
135
  },
157
- buffers: {
158
- [vertices_symbol]: {},
159
- },
160
136
  } satisfies ViewSchema
161
137
 
162
138
  const result = view(gl, program, schema)
@@ -169,9 +145,6 @@ describe('view', () => {
169
145
 
170
146
  expect(result.interleavedAttributes![data_symbol]).toBeDefined()
171
147
  expect(result.interleavedAttributes![data_symbol].buffer.bind).toBeInstanceOf(Function)
172
-
173
- expect(result.buffers![vertices_symbol]).toBeDefined()
174
- expect(result.buffers![vertices_symbol].bind).toBeInstanceOf(Function)
175
148
  })
176
149
 
177
150
  it('should create a view with mixed string and symbol keys', () => {
@@ -201,6 +174,94 @@ describe('view', () => {
201
174
  })
202
175
  })
203
176
 
177
+ describe('disposing a view', () => {
178
+ let gl: WebGL2RenderingContext
179
+ let program: WebGLProgram
180
+ beforeEach(() => {
181
+ const mock = createMockCanvas()
182
+ gl = mock.gl
183
+ program = gl.createProgram()!
184
+ })
185
+
186
+ const schema = {
187
+ attributes: { a_position: { kind: 'vec2' } },
188
+ interleavedAttributes: { data: { layout: [{ key: 'a_uv', kind: 'vec2' }] } },
189
+ } satisfies ViewSchema
190
+
191
+ it('deletes the buffers it made, and the arrays it handed out', () => {
192
+ const result = view(gl, program, schema)
193
+ const vertexArray = result.vao([result.attributes.a_position])
194
+
195
+ result.dispose()
196
+
197
+ expect(gl.deleteBuffer).toHaveBeenCalledTimes(2)
198
+ expect(gl.deleteVertexArray).toHaveBeenCalledTimes(1)
199
+ void vertexArray
200
+ })
201
+
202
+ it('does the same thing on abort as it does on dispose', () => {
203
+ const controller = new AbortController()
204
+ const result = view(gl, program, schema, { signal: controller.signal })
205
+ result.vao([result.attributes.a_position])
206
+
207
+ controller.abort()
208
+ const afterAbort = (gl.deleteBuffer as any).mock.calls.length
209
+
210
+ // Both routes lead to the same teardown, so taking both must not delete
211
+ // twice: the second delete would invalidate a handle someone else holds.
212
+ result.dispose()
213
+ expect((gl.deleteBuffer as any).mock.calls).toHaveLength(afterAbort)
214
+ expect(gl.deleteVertexArray).toHaveBeenCalledTimes(1)
215
+ })
216
+
217
+ it('deletes a buffer the schema declared, on its own signal', () => {
218
+ // The regression this key exists for. It used to be built with the options
219
+ // dropped — `bufferView(gl, schema.buffers)` — so a view's buffers were the
220
+ // only resources it did not dispose, and every caller had to remember one
221
+ // extra line beside an abort that covered everything else.
222
+ const controller = new AbortController()
223
+ const withBuffer = {
224
+ attributes: { a_position: { kind: 'vec2' } },
225
+ buffers: { b_index: { target: 'ELEMENT_ARRAY_BUFFER' } },
226
+ } satisfies ViewSchema
227
+ const result = view(gl, program, withBuffer, { signal: controller.signal })
228
+ void result.buffers.b_index
229
+
230
+ controller.abort()
231
+
232
+ // The attribute's buffer and the declared one.
233
+ expect(gl.deleteBuffer).toHaveBeenCalledTimes(2)
234
+ })
235
+
236
+ it('leaves a buffer the schema NAMED to whoever made it', () => {
237
+ // Borrowed, not owned — the other half of the rule. A named buffer is the
238
+ // point of sharing one, and one view cannot know whether the others are
239
+ // done with it.
240
+ const borrowed = bufferView(gl, { shared: { target: 'ARRAY_BUFFER' } })
241
+ ;(gl.deleteBuffer as any).mockClear()
242
+ const result = view(gl, program, {
243
+ attributes: { a_position: { kind: 'vec2', buffer: borrowed.shared } },
244
+ } satisfies ViewSchema)
245
+
246
+ result.dispose()
247
+
248
+ expect(gl.deleteBuffer).not.toHaveBeenCalled()
249
+ })
250
+
251
+ it('forgets a vertex array the caller disposed itself', () => {
252
+ const result = view(gl, program, schema)
253
+ const vertexArray = result.vao([result.attributes.a_position])
254
+
255
+ vertexArray.dispose()
256
+ vertexArray.dispose()
257
+ result.dispose()
258
+
259
+ // Held only until disposed, so a view handing out an array per frame does
260
+ // not accumulate them — and disposing one twice deletes it once.
261
+ expect(gl.deleteVertexArray).toHaveBeenCalledTimes(1)
262
+ })
263
+ })
264
+
204
265
  describe('uniformView', () => {
205
266
  let gl: ReturnType<typeof createMockGL>
206
267
  let program: WebGLProgram
@@ -482,6 +543,28 @@ describe('attributeView', () => {
482
543
  expect(gl.createBuffer).toHaveBeenCalledTimes(1) // Only for the custom buffer
483
544
  })
484
545
 
546
+ it('does not delete a buffer it did not create', () => {
547
+ const customBuffer = gl.createBuffer()!
548
+ const controller = new AbortController()
549
+ const attributes = attributeView(
550
+ gl,
551
+ program,
552
+ {
553
+ a_position: { kind: 'vec2', buffer: customBuffer },
554
+ a_normal: { kind: 'vec2' },
555
+ } satisfies AttributeSchema,
556
+ { signal: controller.signal },
557
+ )
558
+ const own = attributes.a_normal.buffer
559
+
560
+ controller.abort()
561
+
562
+ // A schema can name a buffer another view owns — compile.toQuad shares one
563
+ // across every program on a context — and disposing here must not take it.
564
+ expect(gl.deleteBuffer).not.toHaveBeenCalledWith(customBuffer)
565
+ expect(gl.deleteBuffer).toHaveBeenCalledWith(own)
566
+ })
567
+
485
568
  it('should handle float attributes', () => {
486
569
  const schema = {
487
570
  a_scalar: { kind: 'float' },
@@ -1445,3 +1528,36 @@ describe('the constant source', () => {
1445
1528
  expect(() => a_instance.constant).toThrow(/cannot be a constant attribute/)
1446
1529
  })
1447
1530
  })
1531
+
1532
+ describe('createFramebuffer', () => {
1533
+ let gl: ReturnType<typeof createMockGL>
1534
+
1535
+ beforeEach(() => {
1536
+ gl = createMockGL()
1537
+ gl.checkFramebufferStatus = vi.fn(() => gl.FRAMEBUFFER_COMPLETE) as any
1538
+ })
1539
+
1540
+ const definition = { attachment: 'color', width: 4, height: 4 } as const
1541
+
1542
+ it('deletes the texture it made', () => {
1543
+ const controller = new AbortController()
1544
+ const { texture } = createFramebuffer(gl, definition, { signal: controller.signal })
1545
+
1546
+ controller.abort()
1547
+
1548
+ expect(gl.deleteTexture).toHaveBeenCalledWith(texture)
1549
+ })
1550
+
1551
+ it('leaves a texture it was handed', () => {
1552
+ // Ping-ponged textures outlive the framebuffers pointing at them, so the
1553
+ // one case that matters is the one where the texture came from outside.
1554
+ const texture = createTexture(gl, { width: 4, height: 4 })
1555
+ const target = createFramebuffer(gl, { ...definition, texture })
1556
+
1557
+ target.dispose()
1558
+ target.dispose()
1559
+
1560
+ expect(gl.deleteFramebuffer).toHaveBeenCalledTimes(1)
1561
+ expect(gl.deleteTexture).not.toHaveBeenCalled()
1562
+ })
1563
+ })
@@ -64,6 +64,20 @@ export function createMockGL() {
64
64
  LINEAR: 0x2601,
65
65
  TEXTURE_MAG_FILTER: 0x2800,
66
66
  TEXTURE_MIN_FILTER: 0x2801,
67
+ TEXTURE_WRAP_S: 0x2802,
68
+ TEXTURE_WRAP_T: 0x2803,
69
+ CLAMP_TO_EDGE: 0x812f,
70
+ REPEAT: 0x2901,
71
+ RGBA: 0x1908,
72
+ RGB: 0x1907,
73
+ DEPTH_ATTACHMENT: 0x8d00,
74
+ STENCIL_ATTACHMENT: 0x8d20,
75
+ DEPTH_STENCIL_ATTACHMENT: 0x821a,
76
+ FRAMEBUFFER_COMPLETE: 0x8cd5,
77
+ FRAMEBUFFER_INCOMPLETE_ATTACHMENT: 0x8cd6,
78
+ FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: 0x8cd7,
79
+ FRAMEBUFFER_INCOMPLETE_DIMENSIONS: 0x8cd9,
80
+ FRAMEBUFFER_UNSUPPORTED: 0x8cdd,
67
81
 
68
82
  // Buffer methods
69
83
  createBuffer: vi.fn(() => {
package/test/tag.test.ts CHANGED
@@ -1205,6 +1205,47 @@ describe('compile', () => {
1205
1205
  gl.createProgram = vi.fn(() => mockProgram)
1206
1206
  })
1207
1207
 
1208
+ it('disposes the same way whether by hand or by signal', () => {
1209
+ const vertex = glsl`${attribute.vec2('a_position')}
1210
+ void main() { gl_Position = vec4(a_position, 0.0, 1.0); }`
1211
+ const fragment = glsl`void main() {}`
1212
+ gl.deleteProgram = vi.fn()
1213
+ gl.deleteBuffer = vi.fn()
1214
+
1215
+ const controller = new AbortController()
1216
+ const result = compile(gl as WebGLRenderingContext, vertex, fragment, {
1217
+ signal: controller.signal,
1218
+ })
1219
+
1220
+ controller.abort()
1221
+ result.dispose()
1222
+
1223
+ // Idempotent across both routes: the program and the view's own buffer go
1224
+ // exactly once, however many times teardown is asked for.
1225
+ expect(gl.deleteProgram).toHaveBeenCalledTimes(1)
1226
+ expect(gl.deleteBuffer).toHaveBeenCalledTimes(1)
1227
+ })
1228
+
1229
+ it('deletes the program it created, and nothing it borrowed', () => {
1230
+ const vertex = glsl`${attribute.vec2('a_position')}
1231
+ void main() { gl_Position = vec4(a_position, 0.0, 1.0); }`
1232
+ const fragment = glsl`void main() {}`
1233
+ const borrowed = gl.createBuffer()!
1234
+ gl.deleteProgram = vi.fn()
1235
+ gl.deleteBuffer = vi.fn()
1236
+
1237
+ const controller = new AbortController()
1238
+ compile(gl as WebGLRenderingContext, vertex, fragment, {
1239
+ signal: controller.signal,
1240
+ schema: { attributes: { a_position: { buffer: borrowed } } },
1241
+ })
1242
+
1243
+ controller.abort()
1244
+
1245
+ expect(gl.deleteProgram).toHaveBeenCalledWith(mockProgram)
1246
+ expect(gl.deleteBuffer).not.toHaveBeenCalledWith(borrowed)
1247
+ })
1248
+
1208
1249
  it('should compile shaders and return program with schema', () => {
1209
1250
  const vertex = glsl`
1210
1251
  ${attribute.vec2('a_position')}
@@ -1,75 +0,0 @@
1
- // src/gl.ts
2
- function isWebGL2RenderingContext(gl) {
3
- return typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext;
4
- }
5
- function createShader(gl, type, source) {
6
- const shader = gl.createShader(type);
7
- if (!shader) throw new Error("Failed to create shader");
8
- gl.shaderSource(shader, source);
9
- gl.compileShader(shader);
10
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
11
- const info = gl.getShaderInfoLog(shader);
12
- gl.deleteShader(shader);
13
- throw new Error(
14
- `Failed to compile ${type === gl.VERTEX_SHADER ? "vertex" : "fragment"} shader: ${info}`
15
- );
16
- }
17
- return shader;
18
- }
19
- function createProgram(gl, vertexSource, fragmentSource) {
20
- const program = gl.createProgram();
21
- if (!program) throw new Error("Failed to create WebGL program");
22
- const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
23
- const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
24
- gl.attachShader(program, vertexShader);
25
- gl.attachShader(program, fragmentShader);
26
- gl.linkProgram(program);
27
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
28
- const info = gl.getProgramInfoLog(program);
29
- gl.deleteProgram(program);
30
- gl.deleteShader(vertexShader);
31
- gl.deleteShader(fragmentShader);
32
- throw new Error(`Failed to link program: ${info}`);
33
- }
34
- gl.deleteShader(vertexShader);
35
- gl.deleteShader(fragmentShader);
36
- return program;
37
- }
38
- var INSTANCED_ARRAYS_WRAPPER_MAP = /* @__PURE__ */ new WeakMap();
39
- function getInstancedArrays(gl) {
40
- if (isWebGL2RenderingContext(gl)) return gl;
41
- const cached = INSTANCED_ARRAYS_WRAPPER_MAP.get(gl);
42
- if (cached) return cached;
43
- const ext = gl.getExtension("ANGLE_instanced_arrays");
44
- if (!ext) return void 0;
45
- const wrapper = {
46
- drawArraysInstanced: ext.drawArraysInstancedANGLE.bind(ext),
47
- drawElementsInstanced: ext.drawElementsInstancedANGLE.bind(ext),
48
- vertexAttribDivisor: ext.vertexAttribDivisorANGLE.bind(ext)
49
- };
50
- INSTANCED_ARRAYS_WRAPPER_MAP.set(gl, wrapper);
51
- return wrapper;
52
- }
53
- var VERTEX_ARRAY_OBJECT_WRAPPER_MAP = /* @__PURE__ */ new WeakMap();
54
- function getVertexArrayObject(gl) {
55
- if (isWebGL2RenderingContext(gl)) return gl;
56
- const cached = VERTEX_ARRAY_OBJECT_WRAPPER_MAP.get(gl);
57
- if (cached) return cached;
58
- const ext = gl.getExtension("OES_vertex_array_object");
59
- if (!ext) return null;
60
- const wrapper = {
61
- bindVertexArray: ext.bindVertexArrayOES.bind(ext),
62
- createVertexArray: ext.createVertexArrayOES.bind(ext),
63
- deleteVertexArray: ext.deleteVertexArrayOES.bind(ext)
64
- };
65
- VERTEX_ARRAY_OBJECT_WRAPPER_MAP.set(gl, wrapper);
66
- return wrapper;
67
- }
68
-
69
- export {
70
- createShader,
71
- createProgram,
72
- getInstancedArrays,
73
- getVertexArrayObject
74
- };
75
- //# sourceMappingURL=chunk-CJTT7WPV.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/gl.ts"],"sourcesContent":["import { RemoveSuffix } from './type-utils'\nimport type { GL } from './types'\n\nfunction isWebGL2RenderingContext(\n gl: WebGLRenderingContext | WebGL2RenderingContext,\n): gl is WebGL2RenderingContext {\n return typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext\n}\n\n/**********************************************************************************/\n/* */\n/* Create Shader */\n/* */\n/**********************************************************************************/\n\nexport function createShader(gl: GL, type: number, source: string): WebGLShader {\n const shader = gl.createShader(type)\n if (!shader) throw new Error('Failed to create shader')\n\n gl.shaderSource(shader, source)\n gl.compileShader(shader)\n\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const info = gl.getShaderInfoLog(shader)\n gl.deleteShader(shader)\n throw new Error(\n `Failed to compile ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader: ${info}`,\n )\n }\n\n return shader\n}\n\n/**********************************************************************************/\n/* */\n/* Create Program */\n/* */\n/**********************************************************************************/\n\nexport function createProgram(gl: GL, vertexSource: string, fragmentSource: string): WebGLProgram {\n const program = gl.createProgram()\n if (!program) throw new Error('Failed to create WebGL program')\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)\n\n gl.attachShader(program, vertexShader)\n gl.attachShader(program, fragmentShader)\n gl.linkProgram(program)\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const info = gl.getProgramInfoLog(program)\n gl.deleteProgram(program)\n gl.deleteShader(vertexShader)\n gl.deleteShader(fragmentShader)\n throw new Error(`Failed to link program: ${info}`)\n }\n\n // Clean up shaders after linking\n gl.deleteShader(vertexShader)\n gl.deleteShader(fragmentShader)\n\n return program\n}\n\n/**********************************************************************************/\n/* */\n/* Get Instanced Arrays */\n/* */\n/**********************************************************************************/\n\nconst INSTANCED_ARRAYS_WRAPPER_MAP = new WeakMap<\n GL,\n RemoveSuffix<\n Pick<\n ANGLE_instanced_arrays,\n 'drawArraysInstancedANGLE' | 'drawElementsInstancedANGLE' | 'vertexAttribDivisorANGLE'\n >,\n 'ANGLE'\n > | null\n>()\nexport function getInstancedArrays(gl: GL) {\n if (isWebGL2RenderingContext(gl)) return gl\n\n const cached = INSTANCED_ARRAYS_WRAPPER_MAP.get(gl)\n if (cached) return cached\n\n const ext = gl.getExtension('ANGLE_instanced_arrays')\n if (!ext) return undefined\n\n const wrapper = {\n drawArraysInstanced: ext.drawArraysInstancedANGLE.bind(ext),\n drawElementsInstanced: ext.drawElementsInstancedANGLE.bind(ext),\n vertexAttribDivisor: ext.vertexAttribDivisorANGLE.bind(ext),\n }\n INSTANCED_ARRAYS_WRAPPER_MAP.set(gl, wrapper)\n\n return wrapper\n}\n\n/**********************************************************************************/\n/* */\n/* Get Vertex Array Object */\n/* */\n/**********************************************************************************/\n\nconst VERTEX_ARRAY_OBJECT_WRAPPER_MAP = new WeakMap<\n GL,\n RemoveSuffix<\n Pick<\n OES_vertex_array_object,\n 'bindVertexArrayOES' | 'createVertexArrayOES' | 'deleteVertexArrayOES'\n >,\n 'OES'\n > | null\n>()\nexport function getVertexArrayObject(gl: GL) {\n if (isWebGL2RenderingContext(gl)) return gl\n\n const cached = VERTEX_ARRAY_OBJECT_WRAPPER_MAP.get(gl)\n if (cached) return cached\n\n const ext = gl.getExtension('OES_vertex_array_object')\n if (!ext) return null\n\n const wrapper = {\n bindVertexArray: ext.bindVertexArrayOES.bind(ext),\n createVertexArray: ext.createVertexArrayOES.bind(ext),\n deleteVertexArray: ext.deleteVertexArrayOES.bind(ext),\n }\n VERTEX_ARRAY_OBJECT_WRAPPER_MAP.set(gl, wrapper)\n\n return wrapper\n}\n"],"mappings":";AAGA,SAAS,yBACP,IAC8B;AAC9B,SAAO,OAAO,2BAA2B,eAAe,cAAc;AACxE;AAQO,SAAS,aAAa,IAAQ,MAAc,QAA6B;AAC9E,QAAM,SAAS,GAAG,aAAa,IAAI;AACnC,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,yBAAyB;AAEtD,KAAG,aAAa,QAAQ,MAAM;AAC9B,KAAG,cAAc,MAAM;AAEvB,MAAI,CAAC,GAAG,mBAAmB,QAAQ,GAAG,cAAc,GAAG;AACrD,UAAM,OAAO,GAAG,iBAAiB,MAAM;AACvC,OAAG,aAAa,MAAM;AACtB,UAAM,IAAI;AAAA,MACR,qBAAqB,SAAS,GAAG,gBAAgB,WAAW,UAAU,YAAY,IAAI;AAAA,IACxF;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,cAAc,IAAQ,cAAsB,gBAAsC;AAChG,QAAM,UAAU,GAAG,cAAc;AACjC,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,gCAAgC;AAE9D,QAAM,eAAe,aAAa,IAAI,GAAG,eAAe,YAAY;AACpE,QAAM,iBAAiB,aAAa,IAAI,GAAG,iBAAiB,cAAc;AAE1E,KAAG,aAAa,SAAS,YAAY;AACrC,KAAG,aAAa,SAAS,cAAc;AACvC,KAAG,YAAY,OAAO;AAEtB,MAAI,CAAC,GAAG,oBAAoB,SAAS,GAAG,WAAW,GAAG;AACpD,UAAM,OAAO,GAAG,kBAAkB,OAAO;AACzC,OAAG,cAAc,OAAO;AACxB,OAAG,aAAa,YAAY;AAC5B,OAAG,aAAa,cAAc;AAC9B,UAAM,IAAI,MAAM,2BAA2B,IAAI,EAAE;AAAA,EACnD;AAGA,KAAG,aAAa,YAAY;AAC5B,KAAG,aAAa,cAAc;AAE9B,SAAO;AACT;AAQA,IAAM,+BAA+B,oBAAI,QASvC;AACK,SAAS,mBAAmB,IAAQ;AACzC,MAAI,yBAAyB,EAAE,EAAG,QAAO;AAEzC,QAAM,SAAS,6BAA6B,IAAI,EAAE;AAClD,MAAI,OAAQ,QAAO;AAEnB,QAAM,MAAM,GAAG,aAAa,wBAAwB;AACpD,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU;AAAA,IACd,qBAAqB,IAAI,yBAAyB,KAAK,GAAG;AAAA,IAC1D,uBAAuB,IAAI,2BAA2B,KAAK,GAAG;AAAA,IAC9D,qBAAqB,IAAI,yBAAyB,KAAK,GAAG;AAAA,EAC5D;AACA,+BAA6B,IAAI,IAAI,OAAO;AAE5C,SAAO;AACT;AAQA,IAAM,kCAAkC,oBAAI,QAS1C;AACK,SAAS,qBAAqB,IAAQ;AAC3C,MAAI,yBAAyB,EAAE,EAAG,QAAO;AAEzC,QAAM,SAAS,gCAAgC,IAAI,EAAE;AACrD,MAAI,OAAQ,QAAO;AAEnB,QAAM,MAAM,GAAG,aAAa,yBAAyB;AACrD,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU;AAAA,IACd,iBAAiB,IAAI,mBAAmB,KAAK,GAAG;AAAA,IAChD,mBAAmB,IAAI,qBAAqB,KAAK,GAAG;AAAA,IACpD,mBAAmB,IAAI,qBAAqB,KAAK,GAAG;AAAA,EACtD;AACA,kCAAgC,IAAI,IAAI,OAAO;AAE/C,SAAO;AACT;","names":[]}