@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
@@ -2,8 +2,11 @@ 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,
9
+ vaoView,
7
10
  view,
8
11
  } from '../src/index'
9
12
  import type {
@@ -44,7 +47,6 @@ describe('view', () => {
44
47
  expect(result.uniforms!.u_texture.set).toBeInstanceOf(Function)
45
48
  expect(result.attributes).toBeUndefined()
46
49
  expect(result.interleavedAttributes).toBeUndefined()
47
- expect(result.buffers).toBeUndefined()
48
50
  })
49
51
 
50
52
  it('should create a view with attributes', () => {
@@ -83,27 +85,11 @@ describe('view', () => {
83
85
 
84
86
  expect(result.interleavedAttributes).toBeDefined()
85
87
  expect(result.interleavedAttributes!.data).toBeDefined()
86
- expect(result.interleavedAttributes!.data.bind).toBeInstanceOf(Function)
87
- expect(result.interleavedAttributes!.data.unbind).toBeInstanceOf(Function)
88
+ expect(result.interleavedAttributes!.data.buffer.bind).toBeInstanceOf(Function)
89
+ expect(result.interleavedAttributes!.data.buffer.perInstance.bind).toBeInstanceOf(Function)
90
+ expect(result.interleavedAttributes!.data.constant.bind).toBeInstanceOf(Function)
88
91
  expect(result.interleavedAttributes!.data.dispose).toBeInstanceOf(Function)
89
- expect(result.interleavedAttributes!.data.set).toBeInstanceOf(Function)
90
- })
91
-
92
- it('should create a view with buffers', () => {
93
- const schema = {
94
- buffers: {
95
- vertices: { target: 'ARRAY_BUFFER', usage: 'STATIC_DRAW' },
96
- indices: { target: 'ELEMENT_ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
97
- },
98
- } satisfies ViewSchema
99
-
100
- const result = view(gl, program, schema)
101
-
102
- expect(result.buffers).toBeDefined()
103
- expect(result.buffers!.vertices).toBeDefined()
104
- expect(result.buffers!.vertices.bind).toBeInstanceOf(Function)
105
- expect(result.buffers!.vertices.dispose).toBeInstanceOf(Function)
106
- expect(result.buffers!.vertices.set).toBeInstanceOf(Function)
92
+ expect(result.interleavedAttributes!.data.buffer.set).toBeInstanceOf(Function)
107
93
  })
108
94
 
109
95
  it('should create a view with all components', () => {
@@ -120,9 +106,6 @@ describe('view', () => {
120
106
  instanced: false,
121
107
  },
122
108
  },
123
- buffers: {
124
- vertices: {},
125
- },
126
109
  } satisfies ViewSchema
127
110
 
128
111
  const result = view(gl, program, schema)
@@ -130,14 +113,12 @@ describe('view', () => {
130
113
  expect(result.uniforms).toBeDefined()
131
114
  expect(result.attributes).toBeDefined()
132
115
  expect(result.interleavedAttributes).toBeDefined()
133
- expect(result.buffers).toBeDefined()
134
116
  })
135
117
 
136
118
  it('should create a view with symbols for all component types', () => {
137
119
  const u_time_symbol = Symbol('u_time')
138
120
  const a_position_symbol = Symbol('a_position')
139
121
  const data_symbol = Symbol('data')
140
- const vertices_symbol = Symbol('vertices')
141
122
 
142
123
  const schema = {
143
124
  uniforms: {
@@ -152,9 +133,6 @@ describe('view', () => {
152
133
  instanced: false,
153
134
  },
154
135
  },
155
- buffers: {
156
- [vertices_symbol]: {},
157
- },
158
136
  } satisfies ViewSchema
159
137
 
160
138
  const result = view(gl, program, schema)
@@ -166,10 +144,7 @@ describe('view', () => {
166
144
  expect(result.attributes![a_position_symbol].bind).toBeInstanceOf(Function)
167
145
 
168
146
  expect(result.interleavedAttributes![data_symbol]).toBeDefined()
169
- expect(result.interleavedAttributes![data_symbol].bind).toBeInstanceOf(Function)
170
-
171
- expect(result.buffers![vertices_symbol]).toBeDefined()
172
- expect(result.buffers![vertices_symbol].bind).toBeInstanceOf(Function)
147
+ expect(result.interleavedAttributes![data_symbol].buffer.bind).toBeInstanceOf(Function)
173
148
  })
174
149
 
175
150
  it('should create a view with mixed string and symbol keys', () => {
@@ -199,6 +174,60 @@ describe('view', () => {
199
174
  })
200
175
  })
201
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('forgets a vertex array the caller disposed itself', () => {
218
+ const result = view(gl, program, schema)
219
+ const vertexArray = result.vao([result.attributes.a_position])
220
+
221
+ vertexArray.dispose()
222
+ vertexArray.dispose()
223
+ result.dispose()
224
+
225
+ // Held only until disposed, so a view handing out an array per frame does
226
+ // not accumulate them — and disposing one twice deletes it once.
227
+ expect(gl.deleteVertexArray).toHaveBeenCalledTimes(1)
228
+ })
229
+ })
230
+
202
231
  describe('uniformView', () => {
203
232
  let gl: ReturnType<typeof createMockGL>
204
233
  let program: WebGLProgram
@@ -480,6 +509,28 @@ describe('attributeView', () => {
480
509
  expect(gl.createBuffer).toHaveBeenCalledTimes(1) // Only for the custom buffer
481
510
  })
482
511
 
512
+ it('does not delete a buffer it did not create', () => {
513
+ const customBuffer = gl.createBuffer()!
514
+ const controller = new AbortController()
515
+ const attributes = attributeView(
516
+ gl,
517
+ program,
518
+ {
519
+ a_position: { kind: 'vec2', buffer: customBuffer },
520
+ a_normal: { kind: 'vec2' },
521
+ } satisfies AttributeSchema,
522
+ { signal: controller.signal },
523
+ )
524
+ const own = attributes.a_normal.buffer
525
+
526
+ controller.abort()
527
+
528
+ // A schema can name a buffer another view owns — compile.toQuad shares one
529
+ // across every program on a context — and disposing here must not take it.
530
+ expect(gl.deleteBuffer).not.toHaveBeenCalledWith(customBuffer)
531
+ expect(gl.deleteBuffer).toHaveBeenCalledWith(own)
532
+ })
533
+
483
534
  it('should handle float attributes', () => {
484
535
  const schema = {
485
536
  a_scalar: { kind: 'float' },
@@ -778,10 +829,10 @@ describe('interleavedAttributeView', () => {
778
829
  const interleavedAttributes = interleavedAttributeView(gl, program, schema)
779
830
 
780
831
  expect(interleavedAttributes.vertexData).toBeDefined()
781
- expect(interleavedAttributes.vertexData.bind).toBeInstanceOf(Function)
782
- expect(interleavedAttributes.vertexData.unbind).toBeInstanceOf(Function)
832
+ expect(interleavedAttributes.vertexData.buffer.bind).toBeInstanceOf(Function)
833
+ expect(interleavedAttributes.vertexData.buffer.perInstance.bind).toBeInstanceOf(Function)
783
834
  expect(interleavedAttributes.vertexData.dispose).toBeInstanceOf(Function)
784
- expect(interleavedAttributes.vertexData.set).toBeInstanceOf(Function)
835
+ expect(interleavedAttributes.vertexData.buffer.set).toBeInstanceOf(Function)
785
836
  })
786
837
 
787
838
  it('should return a disposer from bind that restores the default vertex array', () => {
@@ -804,7 +855,7 @@ describe('interleavedAttributeView', () => {
804
855
  const previous = { id: 'previously-bound-vao' }
805
856
  ;(gl.getParameter as any).mockReturnValue(previous)
806
857
 
807
- const unbind = interleavedAttributes.vertexData.bind()
858
+ const unbind = interleavedAttributes.vertexData.buffer.bind()
808
859
  expect(unbind).toBeInstanceOf(Function)
809
860
 
810
861
  ;(gl.bindVertexArray as any).mockClear()
@@ -826,7 +877,7 @@ describe('interleavedAttributeView', () => {
826
877
 
827
878
  const interleavedAttributes = interleavedAttributeView(gl, program, schema)
828
879
 
829
- interleavedAttributes.vertexData.bind()
880
+ interleavedAttributes.vertexData.buffer.bind()
830
881
 
831
882
  // Check vertexAttribPointer calls
832
883
  const calls = (gl.vertexAttribPointer as any).mock.calls
@@ -850,23 +901,28 @@ describe('interleavedAttributeView', () => {
850
901
  expect(calls[2][5]).toBe(24) // offset
851
902
  })
852
903
 
853
- it('should handle instanced interleaved attributes', () => {
904
+ it('steps per instance or per vertex from the same buffer', () => {
905
+ // A divisor is a property of the draw, not of the data, so it is chosen
906
+ // here rather than declared in the schema.
854
907
  const schema = {
855
908
  instanceData: {
856
909
  layout: [
857
910
  { key: 'a_instancePos', kind: 'vec3' },
858
911
  { key: 'a_instanceScale', kind: 'float' },
859
912
  ],
860
- instanced: true,
861
913
  },
862
914
  } satisfies InterleavedAttributeSchema
863
915
 
864
916
  const interleavedAttributes = interleavedAttributeView(gl, program, schema)
865
917
 
866
- interleavedAttributes.instanceData.bind()
867
-
918
+ interleavedAttributes.instanceData.buffer.perInstance.bind()
868
919
  expect(gl.vertexAttribDivisor).toHaveBeenCalledTimes(2)
869
920
  expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 1)
921
+
922
+ vi.mocked(gl.vertexAttribDivisor).mockClear()
923
+
924
+ interleavedAttributes.instanceData.buffer.bind()
925
+ expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 0)
870
926
  })
871
927
 
872
928
  it('should handle integer attributes in layout', () => {
@@ -883,7 +939,7 @@ describe('interleavedAttributeView', () => {
883
939
 
884
940
  const interleavedAttributes = interleavedAttributeView(gl, program, schema)
885
941
 
886
- interleavedAttributes.data.bind()
942
+ interleavedAttributes.data.buffer.bind()
887
943
 
888
944
  const floatCalls = (gl.vertexAttribPointer as any).mock.calls
889
945
  const intCalls = (gl.vertexAttribIPointer as any).mock.calls
@@ -908,7 +964,7 @@ describe('interleavedAttributeView', () => {
908
964
  } satisfies InterleavedAttributeSchema
909
965
 
910
966
  const interleavedAttributes = interleavedAttributeView(gl, program, schema)
911
- interleavedAttributes.data.bind()
967
+ interleavedAttributes.data.buffer.bind()
912
968
 
913
969
  const calls = (gl.vertexAttribPointer as any).mock.calls
914
970
  const stride = 16 // 8 + 4 + 4
@@ -961,25 +1017,13 @@ describe('interleavedAttributeView', () => {
961
1017
  1, // color
962
1018
  ])
963
1019
 
964
- interleavedAttributes.vertexData.set(data)
1020
+ interleavedAttributes.vertexData.buffer.set(data)
965
1021
  expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
966
1022
  })
967
1023
 
968
- it('should use VAO when available', () => {
969
- const vertexArray = { id: 1 }
970
- const vaoExt = {
971
- createVertexArrayOES: vi.fn(() => vertexArray),
972
- bindVertexArrayOES: vi.fn(),
973
- deleteVertexArrayOES: vi.fn(),
974
- }
975
-
976
- // Override the getExtension method for this test
977
- const originalGetExtension = gl.getExtension
978
- gl.getExtension = vi.fn((name: string) => {
979
- if (name === 'OES_vertex_array_object') return vaoExt as any
980
- return originalGetExtension.call(gl, name)
981
- }) as any
982
-
1024
+ it('does not create a vertex array of its own', () => {
1025
+ // A layout used to make one here, which is why the other participants in a
1026
+ // draw had to be bound after it to be included. Arrays are vao()'s job.
983
1027
  const schema = {
984
1028
  vertexData: {
985
1029
  layout: [{ key: 'a_position', kind: 'vec2' }],
@@ -987,26 +1031,9 @@ describe('interleavedAttributeView', () => {
987
1031
  },
988
1032
  } satisfies InterleavedAttributeSchema
989
1033
 
990
- const interleavedAttributes = interleavedAttributeView(gl as any, program, schema)
991
-
992
- expect(vaoExt.createVertexArrayOES).toHaveBeenCalled()
993
- expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(vertexArray)
994
- expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(null) // unbind after setup
995
-
996
- // Test bind
997
- interleavedAttributes.vertexData.bind()
998
- expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(vertexArray)
999
-
1000
- // Test unbind
1001
- interleavedAttributes.vertexData.unbind()
1002
- expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(null)
1003
-
1004
- // Test dispose
1005
- interleavedAttributes.vertexData.dispose()
1006
- expect(vaoExt.deleteVertexArrayOES).toHaveBeenCalledWith(vertexArray)
1034
+ interleavedAttributeView(gl as any, program, schema)
1007
1035
 
1008
- // Restore original method
1009
- gl.getExtension = originalGetExtension
1036
+ expect(gl.createVertexArray).not.toHaveBeenCalled()
1010
1037
  })
1011
1038
 
1012
1039
  it('should dispose buffers', () => {
@@ -1085,13 +1112,13 @@ describe('interleavedAttributeView', () => {
1085
1112
  const interleavedAttributes = interleavedAttributeView(gl, program, schema)
1086
1113
 
1087
1114
  expect(interleavedAttributes[data_symbol]).toBeDefined()
1088
- expect(interleavedAttributes[data_symbol].bind).toBeInstanceOf(Function)
1089
- expect(interleavedAttributes[data_symbol].unbind).toBeInstanceOf(Function)
1115
+ expect(interleavedAttributes[data_symbol].buffer.bind).toBeInstanceOf(Function)
1116
+ expect(interleavedAttributes[data_symbol].buffer.perInstance.bind).toBeInstanceOf(Function)
1090
1117
  expect(interleavedAttributes[data_symbol].dispose).toBeInstanceOf(Function)
1091
- expect(interleavedAttributes[data_symbol].set).toBeInstanceOf(Function)
1118
+ expect(interleavedAttributes[data_symbol].buffer.set).toBeInstanceOf(Function)
1092
1119
 
1093
1120
  // Test binding
1094
- interleavedAttributes[data_symbol].bind()
1121
+ interleavedAttributes[data_symbol].buffer.bind()
1095
1122
 
1096
1123
  // Should bind buffer and set up attributes
1097
1124
  expect(gl.bindBuffer).toHaveBeenCalled()
@@ -1119,7 +1146,7 @@ describe('interleavedAttributeView', () => {
1119
1146
  expect(interleavedAttributes[vertexData]).toBeDefined()
1120
1147
 
1121
1148
  // Test that it correctly calculates stride
1122
- interleavedAttributes[vertexData].bind()
1149
+ interleavedAttributes[vertexData].buffer.bind()
1123
1150
 
1124
1151
  const calls = (gl.vertexAttribPointer as any).mock.calls
1125
1152
  // All calls should have the same stride (3+3+2)*4 = 32 bytes
@@ -1272,3 +1299,231 @@ describe('bufferView', () => {
1272
1299
  expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW)
1273
1300
  })
1274
1301
  })
1302
+
1303
+ describe('vaoView', () => {
1304
+ let gl: WebGL2RenderingContext
1305
+ let program: WebGLProgram
1306
+ beforeEach(() => {
1307
+ const mock = createMockCanvas()
1308
+ gl = mock.gl
1309
+ program = gl.createProgram()!
1310
+ })
1311
+
1312
+ function participants() {
1313
+ const attributes = attributeView(gl as any, program, {
1314
+ a_corner: { kind: 'vec2' },
1315
+ } satisfies AttributeSchema)
1316
+ const interleaved = interleavedAttributeView(gl as any, program, {
1317
+ a_instance: {
1318
+ layout: [{ key: 'a_position', kind: 'vec2' }],
1319
+ },
1320
+ } satisfies InterleavedAttributeSchema)
1321
+ const buffers = bufferView(gl as any, {
1322
+ b_index: { target: 'ELEMENT_ARRAY_BUFFER' },
1323
+ } satisfies BufferSchema)
1324
+ return { attributes, interleaved, buffers }
1325
+ }
1326
+
1327
+ it('writes every participant into one array, at construction', () => {
1328
+ const { attributes, interleaved, buffers } = participants()
1329
+ const vertexArray = { id: 'vao' } as unknown as WebGLVertexArrayObject
1330
+ gl.createVertexArray = vi.fn(() => vertexArray) as any
1331
+
1332
+ const calls: string[] = []
1333
+ gl.bindVertexArray = vi.fn(v => calls.push(v === vertexArray ? 'bind' : 'unbind')) as any
1334
+ gl.vertexAttribPointer = vi.fn(() => calls.push('pointer')) as any
1335
+ gl.bindBuffer = vi.fn((target: number) =>
1336
+ calls.push(target === gl.ELEMENT_ARRAY_BUFFER ? 'indices' : 'array'),
1337
+ ) as any
1338
+
1339
+ vaoView(gl as any, [
1340
+ attributes.a_corner,
1341
+ interleaved.a_instance.buffer.perInstance,
1342
+ buffers.b_index,
1343
+ ])
1344
+
1345
+ // Every participant's state is written while the new array is bound —
1346
+ // that is the whole contract, and what call order used to decide.
1347
+ const bound = calls.indexOf('bind')
1348
+ const unbound = calls.lastIndexOf('unbind')
1349
+ expect(bound).toBeGreaterThanOrEqual(0)
1350
+ expect(calls.indexOf('pointer')).toBeGreaterThan(bound)
1351
+ expect(calls.indexOf('indices')).toBeGreaterThan(bound)
1352
+ expect(unbound).toBeGreaterThan(calls.lastIndexOf('indices'))
1353
+ })
1354
+
1355
+ it('binds and restores, and disposes the array', () => {
1356
+ const { attributes } = participants()
1357
+ const vertexArray = { id: 'vao' } as unknown as WebGLVertexArrayObject
1358
+ const foreign = { id: 'foreign' } as unknown as WebGLVertexArrayObject
1359
+ // Swapped before the first getVertexArrayObject call: on a context without
1360
+ // native vertex arrays the extension wrapper binds these once, up front.
1361
+ gl.createVertexArray = vi.fn(() => vertexArray) as any
1362
+ const bindVertexArray = vi.fn()
1363
+ gl.bindVertexArray = bindVertexArray as any
1364
+ gl.getParameter = vi.fn(() => foreign) as any
1365
+
1366
+ const vao = vaoView(gl as any, [attributes.a_corner])
1367
+ bindVertexArray.mockClear()
1368
+
1369
+ const restore = vao.bind()
1370
+ expect(bindVertexArray).toHaveBeenLastCalledWith(vertexArray)
1371
+ restore()
1372
+ expect(bindVertexArray).toHaveBeenLastCalledWith(foreign)
1373
+
1374
+ vao.dispose()
1375
+ expect(gl.deleteVertexArray).toHaveBeenCalledWith(vertexArray)
1376
+ })
1377
+
1378
+ it('re-applies each participant per bind when the context has no vertex arrays', () => {
1379
+ // WebGL1 without OES_vertex_array_object: nowhere to keep the state, so
1380
+ // the array is emulated by doing the work again on every bind.
1381
+ const mock = createMockGL()
1382
+ const gl1 = mock as any
1383
+ gl1.createVertexArray = undefined
1384
+ gl1.getExtension = vi.fn(() => null)
1385
+ const program1 = gl1.createProgram()
1386
+
1387
+ const attributes = attributeView(gl1, program1, {
1388
+ a_corner: { kind: 'vec2' },
1389
+ } satisfies AttributeSchema)
1390
+
1391
+ const vao = vaoView(gl1, [attributes.a_corner])
1392
+ expect(gl1.vertexAttribPointer).not.toHaveBeenCalled()
1393
+
1394
+ vao.bind()
1395
+ expect(gl1.vertexAttribPointer).toHaveBeenCalledTimes(1)
1396
+ vao.bind()
1397
+ expect(gl1.vertexAttribPointer).toHaveBeenCalledTimes(2)
1398
+ })
1399
+ })
1400
+
1401
+ describe('binding a participant on its own', () => {
1402
+ let gl: WebGL2RenderingContext
1403
+ let program: WebGLProgram
1404
+ beforeEach(() => {
1405
+ const mock = createMockCanvas()
1406
+ gl = mock.gl
1407
+ program = gl.createProgram()!
1408
+ })
1409
+
1410
+ it('targets the default vertex array, not whichever one is current', () => {
1411
+ // The pointer calls name no destination. Without binding first, this would
1412
+ // edit a caller's array — and an array keeps what it is given, so the
1413
+ // damage would outlive the call.
1414
+ const foreign = { id: 'foreign' } as unknown as WebGLVertexArrayObject
1415
+ gl.getParameter = vi.fn(() => foreign) as any
1416
+ const bindVertexArray = vi.fn()
1417
+ gl.bindVertexArray = bindVertexArray as any
1418
+
1419
+ const attributes = attributeView(gl as any, program, {
1420
+ a_corner: { kind: 'vec2' },
1421
+ } satisfies AttributeSchema)
1422
+
1423
+ const restore = attributes.a_corner.bind()
1424
+ expect(bindVertexArray).toHaveBeenCalledWith(null)
1425
+ restore()
1426
+ expect(bindVertexArray).toHaveBeenLastCalledWith(foreign)
1427
+ })
1428
+ })
1429
+
1430
+ describe('the constant source', () => {
1431
+ let gl: WebGL2RenderingContext
1432
+ let program: WebGLProgram
1433
+ beforeEach(() => {
1434
+ const mock = createMockCanvas()
1435
+ gl = mock.gl
1436
+ program = gl.createProgram()!
1437
+ })
1438
+
1439
+ const schema = {
1440
+ a_instance: {
1441
+ layout: [
1442
+ { key: 'a_pos', kind: 'vec2' },
1443
+ { key: 'a_size', kind: 'float' },
1444
+ ],
1445
+ },
1446
+ } satisfies InterleavedAttributeSchema
1447
+
1448
+ it('gives the layout per-key setters, like a uniform', () => {
1449
+ const { a_instance } = interleavedAttributeView(gl as any, program, schema)
1450
+
1451
+ expect(a_instance.constant.a_pos.set).toBeInstanceOf(Function)
1452
+ expect(a_instance.constant.a_size.set).toBeInstanceOf(Function)
1453
+ })
1454
+
1455
+ it('disables the arrays, so the attributes read the constants', () => {
1456
+ const { a_instance } = interleavedAttributeView(gl as any, program, schema)
1457
+
1458
+ a_instance.constant.applyToVertexArray()
1459
+
1460
+ expect(gl.disableVertexAttribArray).toHaveBeenCalledTimes(2)
1461
+ })
1462
+
1463
+ it('writes the values on every bind, not once into the array', () => {
1464
+ // The enable flag is vertex-array state; the values are context state, and
1465
+ // no array can keep them. Applying them once at construction would leave a
1466
+ // later draw showing whatever the previous one set.
1467
+ const { a_instance } = interleavedAttributeView(gl as any, program, schema)
1468
+ a_instance.constant.a_pos.set(1, 2)
1469
+ a_instance.constant.a_size.set(4)
1470
+
1471
+ const vao = vaoView(gl as any, [a_instance.constant])
1472
+ expect(gl.vertexAttrib2f).not.toHaveBeenCalled()
1473
+
1474
+ vao.bind()
1475
+ expect(gl.vertexAttrib2f).toHaveBeenCalledWith(expect.any(Number), 1, 2)
1476
+ expect(gl.vertexAttrib1f).toHaveBeenCalledWith(expect.any(Number), 4)
1477
+
1478
+ a_instance.constant.a_pos.set(7, 8)
1479
+ vao.bind()
1480
+ expect(gl.vertexAttrib2f).toHaveBeenLastCalledWith(expect.any(Number), 7, 8)
1481
+ })
1482
+
1483
+ it('refuses a kind vertexAttrib* cannot express, when asked for one', () => {
1484
+ // A hard limit of the API, not a choice: vertexAttrib* takes at most four
1485
+ // floats and has no matrix form. The buffer source handles it fine, so the
1486
+ // complaint waits until someone reaches for a constant.
1487
+ const matrix = {
1488
+ a_instance: { layout: [{ key: 'a_model', kind: 'mat4' }] },
1489
+ } satisfies InterleavedAttributeSchema
1490
+
1491
+ const { a_instance } = interleavedAttributeView(gl as any, program, matrix)
1492
+
1493
+ expect(() => a_instance.buffer.bind()).not.toThrow()
1494
+ expect(() => a_instance.constant).toThrow(/cannot be a constant attribute/)
1495
+ })
1496
+ })
1497
+
1498
+ describe('createFramebuffer', () => {
1499
+ let gl: ReturnType<typeof createMockGL>
1500
+
1501
+ beforeEach(() => {
1502
+ gl = createMockGL()
1503
+ gl.checkFramebufferStatus = vi.fn(() => gl.FRAMEBUFFER_COMPLETE) as any
1504
+ })
1505
+
1506
+ const definition = { attachment: 'color', width: 4, height: 4 } as const
1507
+
1508
+ it('deletes the texture it made', () => {
1509
+ const controller = new AbortController()
1510
+ const { texture } = createFramebuffer(gl, definition, { signal: controller.signal })
1511
+
1512
+ controller.abort()
1513
+
1514
+ expect(gl.deleteTexture).toHaveBeenCalledWith(texture)
1515
+ })
1516
+
1517
+ it('leaves a texture it was handed', () => {
1518
+ // Ping-ponged textures outlive the framebuffers pointing at them, so the
1519
+ // one case that matters is the one where the texture came from outside.
1520
+ const texture = createTexture(gl, { width: 4, height: 4 })
1521
+ const target = createFramebuffer(gl, { ...definition, texture })
1522
+
1523
+ target.dispose()
1524
+ target.dispose()
1525
+
1526
+ expect(gl.deleteFramebuffer).toHaveBeenCalledTimes(1)
1527
+ expect(gl.deleteTexture).not.toHaveBeenCalled()
1528
+ })
1529
+ })
@@ -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(() => {
@@ -134,6 +148,12 @@ export function createMockGL() {
134
148
  // which tests override where the previous value is what is under test.
135
149
  getParameter: vi.fn(() => null),
136
150
  getVertexAttrib: vi.fn(() => 0),
151
+ disableVertexAttribArray: vi.fn(),
152
+ vertexAttrib1f: vi.fn(),
153
+ vertexAttrib2f: vi.fn(),
154
+ vertexAttrib3f: vi.fn(),
155
+ vertexAttrib4f: vi.fn(),
156
+ VERTEX_ATTRIB_ARRAY_ENABLED: 0x8622,
137
157
 
138
158
  // Uniform methods
139
159
  getUniformLocation: vi.fn((program: MockWebGLProgram, name: string) => {
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')}