@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.
- package/README.md +82 -27
- package/dev/dist/404.html +19 -0
- package/dev/dist/assets/bezier-DrA4cU_H.js +292 -0
- package/dev/dist/assets/game-of-life-BYlzWhEL.js +186 -0
- package/dev/dist/assets/glsl-composition-D_BaFFij.js +97 -0
- package/dev/dist/assets/grid-DfXHNLBm.js +181 -0
- package/dev/dist/assets/index-C2N3u8wj.css +12 -0
- package/dev/dist/assets/index-D-N0FDYs.js +110 -0
- package/dev/dist/assets/main-CSE2fki3.js +32 -0
- package/dev/dist/assets/overview-IeUhUflE.js +80 -0
- package/dev/dist/assets/pix-sim-DMJJA_WO.js +500 -0
- package/dev/dist/assets/ray-casting-DTQetDPd.js +411 -0
- package/dev/dist/assets/ray-casting-modules-Dkfw4tdc.js +765 -0
- package/dev/dist/assets/sdf-CS0uBonx.js +66 -0
- package/dev/dist/assets/symbol-BL_7GJ0M.js +67 -0
- package/dev/dist/assets/tag-C0Vskpp8.js +833 -0
- package/dev/dist/assets/tetris-D8mokodr.js +344 -0
- package/dev/dist/assets/utils-2dzuv_bW.js +62 -0
- package/dev/dist/index.html +51 -0
- package/dist/chunk-BZDGEIOT.mjs +278 -0
- package/dist/chunk-BZDGEIOT.mjs.map +1 -0
- package/dist/chunk-X4M3LV4R.mjs +433 -0
- package/dist/chunk-X4M3LV4R.mjs.map +1 -0
- package/dist/gl.d.mts +2 -2
- package/dist/gl.mjs +1 -1
- package/dist/index.d.mts +23 -6
- package/dist/index.mjs +9 -2
- package/dist/tag.d.mts +6 -1
- package/dist/tag.mjs +13 -5
- package/dist/tag.mjs.map +1 -1
- package/dist/{types-DTSPQclW.d.mts → types-CNdqh7tB.d.mts} +121 -8
- package/package.json +1 -1
- package/src/gl.ts +13 -2
- package/src/index.ts +294 -122
- package/src/tag.ts +18 -2
- package/src/types.ts +127 -7
- package/src/utils.ts +45 -10
- package/test/index.test.ts +339 -84
- package/test/mocks/webgl.ts +20 -0
- package/test/tag.test.ts +41 -0
- package/dist/chunk-CJTT7WPV.mjs +0 -75
- package/dist/chunk-CJTT7WPV.mjs.map +0 -1
- package/dist/chunk-ZP4HP5H7.mjs +0 -405
- package/dist/chunk-ZP4HP5H7.mjs.map +0 -1
package/test/index.test.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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('
|
|
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('
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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].
|
|
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
|
+
})
|
package/test/mocks/webgl.ts
CHANGED
|
@@ -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')}
|