@bigmistqke/view.gl 0.1.18 → 0.2.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 +26 -0
- package/dist/{chunk-WSNE4JVQ.mjs → chunk-CJTT7WPV.mjs} +1 -1
- package/dist/chunk-CJTT7WPV.mjs.map +1 -0
- package/dist/{chunk-CLMGHUTT.mjs → chunk-EYXOSM7B.mjs} +208 -51
- package/dist/chunk-EYXOSM7B.mjs.map +1 -0
- package/dist/gl.d.mts +1 -1
- package/dist/gl.mjs +1 -1
- package/dist/index.d.mts +13 -5
- package/dist/index.mjs +4 -2
- package/dist/tag.d.mts +1 -1
- package/dist/tag.mjs +2 -2
- package/dist/{types-CXqYX05f.d.mts → types-CyGyGi18.d.mts} +94 -8
- package/package.json +1 -1
- package/src/gl.ts +3 -1
- package/src/index.ts +299 -54
- package/src/types.ts +104 -16
- package/test/index.test.ts +333 -54
- package/test/mocks/webgl.ts +14 -0
- package/dist/chunk-CLMGHUTT.mjs.map +0 -1
- package/dist/chunk-WSNE4JVQ.mjs.map +0 -1
package/src/types.ts
CHANGED
|
@@ -285,14 +285,7 @@ export type AttributeKind =
|
|
|
285
285
|
| 'uvec3'
|
|
286
286
|
| 'uvec4'
|
|
287
287
|
|
|
288
|
-
export type AttributeFormat =
|
|
289
|
-
| 'float32'
|
|
290
|
-
| 'int32'
|
|
291
|
-
| 'uint32'
|
|
292
|
-
| 'int16'
|
|
293
|
-
| 'uint16'
|
|
294
|
-
| 'int8'
|
|
295
|
-
| 'uint8'
|
|
288
|
+
export type AttributeFormat = 'float32' | 'int32' | 'uint32' | 'int16' | 'uint16' | 'int8' | 'uint8'
|
|
296
289
|
|
|
297
290
|
export interface AttributeDefinition {
|
|
298
291
|
kind: AttributeKind
|
|
@@ -313,7 +306,9 @@ type DefaultFormat<K extends AttributeKind> = K extends IntKind
|
|
|
313
306
|
? 'uint32'
|
|
314
307
|
: 'float32'
|
|
315
308
|
|
|
316
|
-
type ResolvedFormat<T extends AttributeDefinition> = T extends {
|
|
309
|
+
type ResolvedFormat<T extends AttributeDefinition> = T extends {
|
|
310
|
+
format: infer F extends AttributeFormat
|
|
311
|
+
}
|
|
317
312
|
? F
|
|
318
313
|
: DefaultFormat<T['kind']>
|
|
319
314
|
|
|
@@ -327,10 +322,49 @@ type FormatToArray = {
|
|
|
327
322
|
uint8: Uint8Array
|
|
328
323
|
}
|
|
329
324
|
|
|
330
|
-
|
|
325
|
+
/**
|
|
326
|
+
* What a participant contributes to a vertex array.
|
|
327
|
+
*
|
|
328
|
+
* `enableVertexAttribArray`, `vertexAttribPointer`, `vertexAttribDivisor` and
|
|
329
|
+
* the `ELEMENT_ARRAY_BUFFER` binding all write into whichever vertex array is
|
|
330
|
+
* bound when they run — they name no destination of their own. That is what
|
|
331
|
+
* makes them a participant in something rather than a property of themselves,
|
|
332
|
+
* and why `vao()` exists: it binds the array first, then asks each participant
|
|
333
|
+
* to write itself in.
|
|
334
|
+
*
|
|
335
|
+
* The returned function undoes the write. It is only used where there is no
|
|
336
|
+
* vertex array to hold the state — a WebGL1 context without
|
|
337
|
+
* `OES_vertex_array_object` — since a real vertex array is thrown away whole.
|
|
338
|
+
*/
|
|
339
|
+
export interface VertexArrayParticipant {
|
|
340
|
+
applyToVertexArray(): () => void
|
|
341
|
+
/**
|
|
342
|
+
* State the CONTEXT holds rather than the vertex array, re-applied on every
|
|
343
|
+
* bind because no array can keep it.
|
|
344
|
+
*
|
|
345
|
+
* There is exactly one such thing: the current generic vertex attribute
|
|
346
|
+
* values, which an attribute reads when its array is disabled. The enable
|
|
347
|
+
* flag saying to read them is vertex-array state; the values themselves are
|
|
348
|
+
* not. So a constant source splits across the two hooks — and the tempting
|
|
349
|
+
* optimisation of skipping this when nothing changed would let one draw's
|
|
350
|
+
* constants leak into the next.
|
|
351
|
+
*/
|
|
352
|
+
applyToContext?(): void
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface AttributeMethods<T extends AttributeDefinition = AttributeDefinition>
|
|
356
|
+
extends VertexArrayParticipant {
|
|
331
357
|
buffer: WebGLBuffer
|
|
332
|
-
|
|
333
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Apply this attribute to the DEFAULT vertex array, returning a disposer.
|
|
360
|
+
*
|
|
361
|
+
* Explicitly the default one: these calls have no destination of their own,
|
|
362
|
+
* so without binding first this would edit whichever array happened to be
|
|
363
|
+
* bound — and a vertex array keeps what it is given, so the damage would
|
|
364
|
+
* outlive the call. Use `vao()` to build an array of your own.
|
|
365
|
+
*/
|
|
366
|
+
bind(): () => void
|
|
367
|
+
set(data: FormatToArray[ResolvedFormat<T>], usage?: GLUsage): { bind(): () => void }
|
|
334
368
|
dispose(): void
|
|
335
369
|
}
|
|
336
370
|
|
|
@@ -358,12 +392,55 @@ export interface InterleavedAttributeDefinition {
|
|
|
358
392
|
|
|
359
393
|
export type InterleavedAttributeSchema = Record<string | symbol, InterleavedAttributeDefinition>
|
|
360
394
|
|
|
395
|
+
/**
|
|
396
|
+
* A source is where one draw's values for a layout come from. Both kinds are
|
|
397
|
+
* participants, so either can go into a `vao()`.
|
|
398
|
+
*/
|
|
399
|
+
export interface Source extends VertexArrayParticipant {
|
|
400
|
+
/**
|
|
401
|
+
* Apply this source to the DEFAULT vertex array, returning a disposer. See
|
|
402
|
+
* `AttributeMethods.bind` — and prefer `vao()`, which is the only way to get
|
|
403
|
+
* a source and its fellow participants into an array of their own.
|
|
404
|
+
*/
|
|
405
|
+
bind(): () => void
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export interface BufferSource extends Source {
|
|
409
|
+
/**
|
|
410
|
+
* The same buffer, stepped once per instance rather than once per vertex.
|
|
411
|
+
*
|
|
412
|
+
* A divisor is a property of the draw, not of the data — the same markers
|
|
413
|
+
* are an instanced fleet in one draw and a single mesh in the next — so it
|
|
414
|
+
* is chosen here rather than declared in the schema.
|
|
415
|
+
*/
|
|
416
|
+
perInstance: Source
|
|
417
|
+
set(data: Float32Array, usage?: GLUsage): void
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export interface ConstantAttributeMethods {
|
|
421
|
+
set(...values: number[]): void
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Per-key setters over the same layout the buffer source uses — a constant
|
|
426
|
+
* attribute is a uniform in all but name, so it is written like one.
|
|
427
|
+
*/
|
|
428
|
+
export type ConstantSource<T extends InterleavedAttributeLayout[]> = Source & {
|
|
429
|
+
[K in T[number] as K['key'] extends string ? K['key'] : never]: ConstantAttributeMethods
|
|
430
|
+
}
|
|
431
|
+
|
|
361
432
|
export interface InterleavedAttributeMethods<
|
|
362
433
|
T extends InterleavedAttributeLayout[] = InterleavedAttributeLayout[],
|
|
363
434
|
> {
|
|
364
|
-
|
|
435
|
+
buffer: BufferSource
|
|
436
|
+
constant: ConstantSource<T>
|
|
437
|
+
dispose(): void
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export interface VertexArrayMethods {
|
|
441
|
+
/** Binds the vertex array, returning a disposer that restores the previous one */
|
|
442
|
+
bind(): () => void
|
|
365
443
|
unbind(): void
|
|
366
|
-
set(data: Float32Array, usage?: GLUsage): void
|
|
367
444
|
dispose(): void
|
|
368
445
|
}
|
|
369
446
|
|
|
@@ -384,9 +461,10 @@ export interface BufferDefinition {
|
|
|
384
461
|
|
|
385
462
|
export type BufferSchema = Record<string | symbol, BufferDefinition>
|
|
386
463
|
|
|
387
|
-
export interface BufferMethods {
|
|
464
|
+
export interface BufferMethods extends VertexArrayParticipant {
|
|
388
465
|
set(data: Float32Array | Uint16Array | Uint32Array): void
|
|
389
|
-
|
|
466
|
+
/** Binds the buffer, returning a disposer that restores the previous binding */
|
|
467
|
+
bind(): () => void
|
|
390
468
|
dispose(): void
|
|
391
469
|
}
|
|
392
470
|
|
|
@@ -565,6 +643,16 @@ export type View<T extends ViewSchema = ViewSchema> = {
|
|
|
565
643
|
: {}
|
|
566
644
|
buffers: T['buffers'] extends BufferSchema ? Prettify<BufferView<T['buffers']>> : {}
|
|
567
645
|
uniforms: T['uniforms'] extends UniformSchema ? Prettify<UniformView<T['uniforms']>> : {}
|
|
646
|
+
/**
|
|
647
|
+
* Build a vertex array from the participants one draw needs — its attributes,
|
|
648
|
+
* its interleaved layouts, its element buffer.
|
|
649
|
+
*
|
|
650
|
+
* This is the one member the schema cannot declare, because its input is the
|
|
651
|
+
* schema's own output. It is also the only place a vertex array is created:
|
|
652
|
+
* the participants no longer make one each, so which array they end up in is
|
|
653
|
+
* something you say rather than something you inherit from call order.
|
|
654
|
+
*/
|
|
655
|
+
vao(participants: VertexArrayParticipant[]): VertexArrayMethods
|
|
568
656
|
}
|
|
569
657
|
|
|
570
658
|
/**********************************************************************************/
|
package/test/index.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
bufferView,
|
|
5
5
|
interleavedAttributeView,
|
|
6
6
|
uniformView,
|
|
7
|
+
vaoView,
|
|
7
8
|
view,
|
|
8
9
|
} from '../src/index'
|
|
9
10
|
import type {
|
|
@@ -83,10 +84,11 @@ describe('view', () => {
|
|
|
83
84
|
|
|
84
85
|
expect(result.interleavedAttributes).toBeDefined()
|
|
85
86
|
expect(result.interleavedAttributes!.data).toBeDefined()
|
|
86
|
-
expect(result.interleavedAttributes!.data.bind).toBeInstanceOf(Function)
|
|
87
|
-
expect(result.interleavedAttributes!.data.
|
|
87
|
+
expect(result.interleavedAttributes!.data.buffer.bind).toBeInstanceOf(Function)
|
|
88
|
+
expect(result.interleavedAttributes!.data.buffer.perInstance.bind).toBeInstanceOf(Function)
|
|
89
|
+
expect(result.interleavedAttributes!.data.constant.bind).toBeInstanceOf(Function)
|
|
88
90
|
expect(result.interleavedAttributes!.data.dispose).toBeInstanceOf(Function)
|
|
89
|
-
expect(result.interleavedAttributes!.data.set).toBeInstanceOf(Function)
|
|
91
|
+
expect(result.interleavedAttributes!.data.buffer.set).toBeInstanceOf(Function)
|
|
90
92
|
})
|
|
91
93
|
|
|
92
94
|
it('should create a view with buffers', () => {
|
|
@@ -166,7 +168,7 @@ describe('view', () => {
|
|
|
166
168
|
expect(result.attributes![a_position_symbol].bind).toBeInstanceOf(Function)
|
|
167
169
|
|
|
168
170
|
expect(result.interleavedAttributes![data_symbol]).toBeDefined()
|
|
169
|
-
expect(result.interleavedAttributes![data_symbol].bind).toBeInstanceOf(Function)
|
|
171
|
+
expect(result.interleavedAttributes![data_symbol].buffer.bind).toBeInstanceOf(Function)
|
|
170
172
|
|
|
171
173
|
expect(result.buffers![vertices_symbol]).toBeDefined()
|
|
172
174
|
expect(result.buffers![vertices_symbol].bind).toBeInstanceOf(Function)
|
|
@@ -409,6 +411,64 @@ describe('attributeView', () => {
|
|
|
409
411
|
expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 1)
|
|
410
412
|
})
|
|
411
413
|
|
|
414
|
+
it('should return a disposer from bind that clears the instancing divisor', () => {
|
|
415
|
+
const schema = {
|
|
416
|
+
a_instancePosition: { kind: 'vec3', instanced: true },
|
|
417
|
+
} satisfies AttributeSchema
|
|
418
|
+
|
|
419
|
+
const attributes = attributeView(gl, program, schema)
|
|
420
|
+
|
|
421
|
+
// The divisor is global to the location rather than to the program, so
|
|
422
|
+
// leaving it at 1 makes the next non-instanced attribute bound there read
|
|
423
|
+
// a single value for every vertex.
|
|
424
|
+
const unbind = attributes.a_instancePosition.bind()
|
|
425
|
+
expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 1)
|
|
426
|
+
|
|
427
|
+
unbind()
|
|
428
|
+
expect(gl.vertexAttribDivisor).toHaveBeenLastCalledWith(expect.any(Number), 0)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should state a zero divisor for non-instanced attributes rather than inherit one', () => {
|
|
432
|
+
const schema = {
|
|
433
|
+
a_position: { kind: 'vec2' },
|
|
434
|
+
} satisfies AttributeSchema
|
|
435
|
+
|
|
436
|
+
const attributes = attributeView(gl, program, schema)
|
|
437
|
+
|
|
438
|
+
// A location left at 1 by an earlier instanced attribute would otherwise
|
|
439
|
+
// feed a single value to every vertex here.
|
|
440
|
+
attributes.a_position.bind()
|
|
441
|
+
expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 0)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('should restore the previous divisor rather than assume it was zero', () => {
|
|
445
|
+
const schema = {
|
|
446
|
+
a_instancePosition: { kind: 'vec3', instanced: true },
|
|
447
|
+
} satisfies AttributeSchema
|
|
448
|
+
|
|
449
|
+
const attributes = attributeView(gl, program, schema)
|
|
450
|
+
;(gl.getVertexAttrib as any).mockReturnValue(2)
|
|
451
|
+
|
|
452
|
+
const unbind = attributes.a_instancePosition.bind()
|
|
453
|
+
unbind()
|
|
454
|
+
expect(gl.vertexAttribDivisor).toHaveBeenLastCalledWith(expect.any(Number), 2)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('should return a disposer that only restores once', () => {
|
|
458
|
+
const schema = {
|
|
459
|
+
a_instancePosition: { kind: 'vec3', instanced: true },
|
|
460
|
+
} satisfies AttributeSchema
|
|
461
|
+
|
|
462
|
+
const attributes = attributeView(gl, program, schema)
|
|
463
|
+
|
|
464
|
+
const unbind = attributes.a_instancePosition.bind()
|
|
465
|
+
unbind()
|
|
466
|
+
const callsAfterFirst = (gl.vertexAttribDivisor as any).mock.calls.length
|
|
467
|
+
// Undoing twice would put back state a later bind() is relying on.
|
|
468
|
+
unbind()
|
|
469
|
+
expect((gl.vertexAttribDivisor as any).mock.calls).toHaveLength(callsAfterFirst)
|
|
470
|
+
})
|
|
471
|
+
|
|
412
472
|
it('should use custom buffer if provided', () => {
|
|
413
473
|
const customBuffer = gl.createBuffer()!
|
|
414
474
|
const schema = {
|
|
@@ -720,10 +780,38 @@ describe('interleavedAttributeView', () => {
|
|
|
720
780
|
const interleavedAttributes = interleavedAttributeView(gl, program, schema)
|
|
721
781
|
|
|
722
782
|
expect(interleavedAttributes.vertexData).toBeDefined()
|
|
723
|
-
expect(interleavedAttributes.vertexData.bind).toBeInstanceOf(Function)
|
|
724
|
-
expect(interleavedAttributes.vertexData.
|
|
783
|
+
expect(interleavedAttributes.vertexData.buffer.bind).toBeInstanceOf(Function)
|
|
784
|
+
expect(interleavedAttributes.vertexData.buffer.perInstance.bind).toBeInstanceOf(Function)
|
|
725
785
|
expect(interleavedAttributes.vertexData.dispose).toBeInstanceOf(Function)
|
|
726
|
-
expect(interleavedAttributes.vertexData.set).toBeInstanceOf(Function)
|
|
786
|
+
expect(interleavedAttributes.vertexData.buffer.set).toBeInstanceOf(Function)
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
it('should return a disposer from bind that restores the default vertex array', () => {
|
|
790
|
+
const schema = {
|
|
791
|
+
vertexData: {
|
|
792
|
+
layout: [
|
|
793
|
+
{ key: 'a_position', kind: 'vec3' },
|
|
794
|
+
{ key: 'a_color', kind: 'vec4' },
|
|
795
|
+
],
|
|
796
|
+
instanced: true,
|
|
797
|
+
},
|
|
798
|
+
} satisfies InterleavedAttributeSchema
|
|
799
|
+
|
|
800
|
+
const interleavedAttributes = interleavedAttributeView(gl, program, schema)
|
|
801
|
+
|
|
802
|
+
// A bound vertex array stays selected for everything drawn afterwards, so
|
|
803
|
+
// the next draw would write its own attributes into THIS array and corrupt
|
|
804
|
+
// the layer that owns it. The disposer hands the default back.
|
|
805
|
+
// Restoring the previous array rather than null keeps nested binds working.
|
|
806
|
+
const previous = { id: 'previously-bound-vao' }
|
|
807
|
+
;(gl.getParameter as any).mockReturnValue(previous)
|
|
808
|
+
|
|
809
|
+
const unbind = interleavedAttributes.vertexData.buffer.bind()
|
|
810
|
+
expect(unbind).toBeInstanceOf(Function)
|
|
811
|
+
|
|
812
|
+
;(gl.bindVertexArray as any).mockClear()
|
|
813
|
+
unbind()
|
|
814
|
+
expect(gl.bindVertexArray).toHaveBeenCalledWith(previous)
|
|
727
815
|
})
|
|
728
816
|
|
|
729
817
|
it('should calculate correct stride and offsets', () => {
|
|
@@ -740,7 +828,7 @@ describe('interleavedAttributeView', () => {
|
|
|
740
828
|
|
|
741
829
|
const interleavedAttributes = interleavedAttributeView(gl, program, schema)
|
|
742
830
|
|
|
743
|
-
interleavedAttributes.vertexData.bind()
|
|
831
|
+
interleavedAttributes.vertexData.buffer.bind()
|
|
744
832
|
|
|
745
833
|
// Check vertexAttribPointer calls
|
|
746
834
|
const calls = (gl.vertexAttribPointer as any).mock.calls
|
|
@@ -764,23 +852,28 @@ describe('interleavedAttributeView', () => {
|
|
|
764
852
|
expect(calls[2][5]).toBe(24) // offset
|
|
765
853
|
})
|
|
766
854
|
|
|
767
|
-
it('
|
|
855
|
+
it('steps per instance or per vertex from the same buffer', () => {
|
|
856
|
+
// A divisor is a property of the draw, not of the data, so it is chosen
|
|
857
|
+
// here rather than declared in the schema.
|
|
768
858
|
const schema = {
|
|
769
859
|
instanceData: {
|
|
770
860
|
layout: [
|
|
771
861
|
{ key: 'a_instancePos', kind: 'vec3' },
|
|
772
862
|
{ key: 'a_instanceScale', kind: 'float' },
|
|
773
863
|
],
|
|
774
|
-
instanced: true,
|
|
775
864
|
},
|
|
776
865
|
} satisfies InterleavedAttributeSchema
|
|
777
866
|
|
|
778
867
|
const interleavedAttributes = interleavedAttributeView(gl, program, schema)
|
|
779
868
|
|
|
780
|
-
interleavedAttributes.instanceData.bind()
|
|
781
|
-
|
|
869
|
+
interleavedAttributes.instanceData.buffer.perInstance.bind()
|
|
782
870
|
expect(gl.vertexAttribDivisor).toHaveBeenCalledTimes(2)
|
|
783
871
|
expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 1)
|
|
872
|
+
|
|
873
|
+
vi.mocked(gl.vertexAttribDivisor).mockClear()
|
|
874
|
+
|
|
875
|
+
interleavedAttributes.instanceData.buffer.bind()
|
|
876
|
+
expect(gl.vertexAttribDivisor).toHaveBeenCalledWith(expect.any(Number), 0)
|
|
784
877
|
})
|
|
785
878
|
|
|
786
879
|
it('should handle integer attributes in layout', () => {
|
|
@@ -797,7 +890,7 @@ describe('interleavedAttributeView', () => {
|
|
|
797
890
|
|
|
798
891
|
const interleavedAttributes = interleavedAttributeView(gl, program, schema)
|
|
799
892
|
|
|
800
|
-
interleavedAttributes.data.bind()
|
|
893
|
+
interleavedAttributes.data.buffer.bind()
|
|
801
894
|
|
|
802
895
|
const floatCalls = (gl.vertexAttribPointer as any).mock.calls
|
|
803
896
|
const intCalls = (gl.vertexAttribIPointer as any).mock.calls
|
|
@@ -822,7 +915,7 @@ describe('interleavedAttributeView', () => {
|
|
|
822
915
|
} satisfies InterleavedAttributeSchema
|
|
823
916
|
|
|
824
917
|
const interleavedAttributes = interleavedAttributeView(gl, program, schema)
|
|
825
|
-
interleavedAttributes.data.bind()
|
|
918
|
+
interleavedAttributes.data.buffer.bind()
|
|
826
919
|
|
|
827
920
|
const calls = (gl.vertexAttribPointer as any).mock.calls
|
|
828
921
|
const stride = 16 // 8 + 4 + 4
|
|
@@ -875,25 +968,13 @@ describe('interleavedAttributeView', () => {
|
|
|
875
968
|
1, // color
|
|
876
969
|
])
|
|
877
970
|
|
|
878
|
-
interleavedAttributes.vertexData.set(data)
|
|
971
|
+
interleavedAttributes.vertexData.buffer.set(data)
|
|
879
972
|
expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
|
|
880
973
|
})
|
|
881
974
|
|
|
882
|
-
it('
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
createVertexArrayOES: vi.fn(() => vertexArray),
|
|
886
|
-
bindVertexArrayOES: vi.fn(),
|
|
887
|
-
deleteVertexArrayOES: vi.fn(),
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// Override the getExtension method for this test
|
|
891
|
-
const originalGetExtension = gl.getExtension
|
|
892
|
-
gl.getExtension = vi.fn((name: string) => {
|
|
893
|
-
if (name === 'OES_vertex_array_object') return vaoExt as any
|
|
894
|
-
return originalGetExtension.call(gl, name)
|
|
895
|
-
}) as any
|
|
896
|
-
|
|
975
|
+
it('does not create a vertex array of its own', () => {
|
|
976
|
+
// A layout used to make one here, which is why the other participants in a
|
|
977
|
+
// draw had to be bound after it to be included. Arrays are vao()'s job.
|
|
897
978
|
const schema = {
|
|
898
979
|
vertexData: {
|
|
899
980
|
layout: [{ key: 'a_position', kind: 'vec2' }],
|
|
@@ -901,26 +982,9 @@ describe('interleavedAttributeView', () => {
|
|
|
901
982
|
},
|
|
902
983
|
} satisfies InterleavedAttributeSchema
|
|
903
984
|
|
|
904
|
-
|
|
985
|
+
interleavedAttributeView(gl as any, program, schema)
|
|
905
986
|
|
|
906
|
-
expect(
|
|
907
|
-
expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(vertexArray)
|
|
908
|
-
expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(null) // unbind after setup
|
|
909
|
-
|
|
910
|
-
// Test bind
|
|
911
|
-
interleavedAttributes.vertexData.bind()
|
|
912
|
-
expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(vertexArray)
|
|
913
|
-
|
|
914
|
-
// Test unbind
|
|
915
|
-
interleavedAttributes.vertexData.unbind()
|
|
916
|
-
expect(vaoExt.bindVertexArrayOES).toHaveBeenCalledWith(null)
|
|
917
|
-
|
|
918
|
-
// Test dispose
|
|
919
|
-
interleavedAttributes.vertexData.dispose()
|
|
920
|
-
expect(vaoExt.deleteVertexArrayOES).toHaveBeenCalledWith(vertexArray)
|
|
921
|
-
|
|
922
|
-
// Restore original method
|
|
923
|
-
gl.getExtension = originalGetExtension
|
|
987
|
+
expect(gl.createVertexArray).not.toHaveBeenCalled()
|
|
924
988
|
})
|
|
925
989
|
|
|
926
990
|
it('should dispose buffers', () => {
|
|
@@ -999,13 +1063,13 @@ describe('interleavedAttributeView', () => {
|
|
|
999
1063
|
const interleavedAttributes = interleavedAttributeView(gl, program, schema)
|
|
1000
1064
|
|
|
1001
1065
|
expect(interleavedAttributes[data_symbol]).toBeDefined()
|
|
1002
|
-
expect(interleavedAttributes[data_symbol].bind).toBeInstanceOf(Function)
|
|
1003
|
-
expect(interleavedAttributes[data_symbol].
|
|
1066
|
+
expect(interleavedAttributes[data_symbol].buffer.bind).toBeInstanceOf(Function)
|
|
1067
|
+
expect(interleavedAttributes[data_symbol].buffer.perInstance.bind).toBeInstanceOf(Function)
|
|
1004
1068
|
expect(interleavedAttributes[data_symbol].dispose).toBeInstanceOf(Function)
|
|
1005
|
-
expect(interleavedAttributes[data_symbol].set).toBeInstanceOf(Function)
|
|
1069
|
+
expect(interleavedAttributes[data_symbol].buffer.set).toBeInstanceOf(Function)
|
|
1006
1070
|
|
|
1007
1071
|
// Test binding
|
|
1008
|
-
interleavedAttributes[data_symbol].bind()
|
|
1072
|
+
interleavedAttributes[data_symbol].buffer.bind()
|
|
1009
1073
|
|
|
1010
1074
|
// Should bind buffer and set up attributes
|
|
1011
1075
|
expect(gl.bindBuffer).toHaveBeenCalled()
|
|
@@ -1033,7 +1097,7 @@ describe('interleavedAttributeView', () => {
|
|
|
1033
1097
|
expect(interleavedAttributes[vertexData]).toBeDefined()
|
|
1034
1098
|
|
|
1035
1099
|
// Test that it correctly calculates stride
|
|
1036
|
-
interleavedAttributes[vertexData].bind()
|
|
1100
|
+
interleavedAttributes[vertexData].buffer.bind()
|
|
1037
1101
|
|
|
1038
1102
|
const calls = (gl.vertexAttribPointer as any).mock.calls
|
|
1039
1103
|
// All calls should have the same stride (3+3+2)*4 = 32 bytes
|
|
@@ -1080,6 +1144,26 @@ describe('bufferView', () => {
|
|
|
1080
1144
|
expect(gl.bindBuffer).toHaveBeenCalledWith(gl.ELEMENT_ARRAY_BUFFER, expect.any(Object))
|
|
1081
1145
|
})
|
|
1082
1146
|
|
|
1147
|
+
it('should return a disposer from bind that releases the target', () => {
|
|
1148
|
+
const schema = {
|
|
1149
|
+
indices: { target: 'ELEMENT_ARRAY_BUFFER' },
|
|
1150
|
+
} satisfies BufferSchema
|
|
1151
|
+
|
|
1152
|
+
const buffers = bufferView(gl, schema)
|
|
1153
|
+
|
|
1154
|
+
// An ELEMENT_ARRAY_BUFFER binding is recorded in whichever vertex array is
|
|
1155
|
+
// bound, so restoring the previous buffer rather than null keeps the
|
|
1156
|
+
// disposer from stripping the index buffer off an array we do not own.
|
|
1157
|
+
const previous = { id: 'previously-bound' }
|
|
1158
|
+
;(gl.getParameter as any).mockReturnValue(previous)
|
|
1159
|
+
|
|
1160
|
+
const unbind = buffers.indices.bind()
|
|
1161
|
+
expect(unbind).toBeInstanceOf(Function)
|
|
1162
|
+
|
|
1163
|
+
unbind()
|
|
1164
|
+
expect(gl.bindBuffer).toHaveBeenLastCalledWith(gl.ELEMENT_ARRAY_BUFFER, previous)
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1083
1167
|
it('should set buffer data with specified usage', () => {
|
|
1084
1168
|
const schema = {
|
|
1085
1169
|
vertices: { target: 'ARRAY_BUFFER', usage: 'STATIC_DRAW' },
|
|
@@ -1166,3 +1250,198 @@ describe('bufferView', () => {
|
|
|
1166
1250
|
expect(gl.bufferData).toHaveBeenCalledWith(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW)
|
|
1167
1251
|
})
|
|
1168
1252
|
})
|
|
1253
|
+
|
|
1254
|
+
describe('vaoView', () => {
|
|
1255
|
+
let gl: WebGL2RenderingContext
|
|
1256
|
+
let program: WebGLProgram
|
|
1257
|
+
beforeEach(() => {
|
|
1258
|
+
const mock = createMockCanvas()
|
|
1259
|
+
gl = mock.gl
|
|
1260
|
+
program = gl.createProgram()!
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
function participants() {
|
|
1264
|
+
const attributes = attributeView(gl as any, program, {
|
|
1265
|
+
a_corner: { kind: 'vec2' },
|
|
1266
|
+
} satisfies AttributeSchema)
|
|
1267
|
+
const interleaved = interleavedAttributeView(gl as any, program, {
|
|
1268
|
+
a_instance: {
|
|
1269
|
+
layout: [{ key: 'a_position', kind: 'vec2' }],
|
|
1270
|
+
},
|
|
1271
|
+
} satisfies InterleavedAttributeSchema)
|
|
1272
|
+
const buffers = bufferView(gl as any, {
|
|
1273
|
+
b_index: { target: 'ELEMENT_ARRAY_BUFFER' },
|
|
1274
|
+
} satisfies BufferSchema)
|
|
1275
|
+
return { attributes, interleaved, buffers }
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
it('writes every participant into one array, at construction', () => {
|
|
1279
|
+
const { attributes, interleaved, buffers } = participants()
|
|
1280
|
+
const vertexArray = { id: 'vao' } as unknown as WebGLVertexArrayObject
|
|
1281
|
+
gl.createVertexArray = vi.fn(() => vertexArray) as any
|
|
1282
|
+
|
|
1283
|
+
const calls: string[] = []
|
|
1284
|
+
gl.bindVertexArray = vi.fn(v => calls.push(v === vertexArray ? 'bind' : 'unbind')) as any
|
|
1285
|
+
gl.vertexAttribPointer = vi.fn(() => calls.push('pointer')) as any
|
|
1286
|
+
gl.bindBuffer = vi.fn((target: number) =>
|
|
1287
|
+
calls.push(target === gl.ELEMENT_ARRAY_BUFFER ? 'indices' : 'array'),
|
|
1288
|
+
) as any
|
|
1289
|
+
|
|
1290
|
+
vaoView(gl as any, [
|
|
1291
|
+
attributes.a_corner,
|
|
1292
|
+
interleaved.a_instance.buffer.perInstance,
|
|
1293
|
+
buffers.b_index,
|
|
1294
|
+
])
|
|
1295
|
+
|
|
1296
|
+
// Every participant's state is written while the new array is bound —
|
|
1297
|
+
// that is the whole contract, and what call order used to decide.
|
|
1298
|
+
const bound = calls.indexOf('bind')
|
|
1299
|
+
const unbound = calls.lastIndexOf('unbind')
|
|
1300
|
+
expect(bound).toBeGreaterThanOrEqual(0)
|
|
1301
|
+
expect(calls.indexOf('pointer')).toBeGreaterThan(bound)
|
|
1302
|
+
expect(calls.indexOf('indices')).toBeGreaterThan(bound)
|
|
1303
|
+
expect(unbound).toBeGreaterThan(calls.lastIndexOf('indices'))
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1306
|
+
it('binds and restores, and disposes the array', () => {
|
|
1307
|
+
const { attributes } = participants()
|
|
1308
|
+
const vertexArray = { id: 'vao' } as unknown as WebGLVertexArrayObject
|
|
1309
|
+
const foreign = { id: 'foreign' } as unknown as WebGLVertexArrayObject
|
|
1310
|
+
// Swapped before the first getVertexArrayObject call: on a context without
|
|
1311
|
+
// native vertex arrays the extension wrapper binds these once, up front.
|
|
1312
|
+
gl.createVertexArray = vi.fn(() => vertexArray) as any
|
|
1313
|
+
const bindVertexArray = vi.fn()
|
|
1314
|
+
gl.bindVertexArray = bindVertexArray as any
|
|
1315
|
+
gl.getParameter = vi.fn(() => foreign) as any
|
|
1316
|
+
|
|
1317
|
+
const vao = vaoView(gl as any, [attributes.a_corner])
|
|
1318
|
+
bindVertexArray.mockClear()
|
|
1319
|
+
|
|
1320
|
+
const restore = vao.bind()
|
|
1321
|
+
expect(bindVertexArray).toHaveBeenLastCalledWith(vertexArray)
|
|
1322
|
+
restore()
|
|
1323
|
+
expect(bindVertexArray).toHaveBeenLastCalledWith(foreign)
|
|
1324
|
+
|
|
1325
|
+
vao.dispose()
|
|
1326
|
+
expect(gl.deleteVertexArray).toHaveBeenCalledWith(vertexArray)
|
|
1327
|
+
})
|
|
1328
|
+
|
|
1329
|
+
it('re-applies each participant per bind when the context has no vertex arrays', () => {
|
|
1330
|
+
// WebGL1 without OES_vertex_array_object: nowhere to keep the state, so
|
|
1331
|
+
// the array is emulated by doing the work again on every bind.
|
|
1332
|
+
const mock = createMockGL()
|
|
1333
|
+
const gl1 = mock as any
|
|
1334
|
+
gl1.createVertexArray = undefined
|
|
1335
|
+
gl1.getExtension = vi.fn(() => null)
|
|
1336
|
+
const program1 = gl1.createProgram()
|
|
1337
|
+
|
|
1338
|
+
const attributes = attributeView(gl1, program1, {
|
|
1339
|
+
a_corner: { kind: 'vec2' },
|
|
1340
|
+
} satisfies AttributeSchema)
|
|
1341
|
+
|
|
1342
|
+
const vao = vaoView(gl1, [attributes.a_corner])
|
|
1343
|
+
expect(gl1.vertexAttribPointer).not.toHaveBeenCalled()
|
|
1344
|
+
|
|
1345
|
+
vao.bind()
|
|
1346
|
+
expect(gl1.vertexAttribPointer).toHaveBeenCalledTimes(1)
|
|
1347
|
+
vao.bind()
|
|
1348
|
+
expect(gl1.vertexAttribPointer).toHaveBeenCalledTimes(2)
|
|
1349
|
+
})
|
|
1350
|
+
})
|
|
1351
|
+
|
|
1352
|
+
describe('binding a participant on its own', () => {
|
|
1353
|
+
let gl: WebGL2RenderingContext
|
|
1354
|
+
let program: WebGLProgram
|
|
1355
|
+
beforeEach(() => {
|
|
1356
|
+
const mock = createMockCanvas()
|
|
1357
|
+
gl = mock.gl
|
|
1358
|
+
program = gl.createProgram()!
|
|
1359
|
+
})
|
|
1360
|
+
|
|
1361
|
+
it('targets the default vertex array, not whichever one is current', () => {
|
|
1362
|
+
// The pointer calls name no destination. Without binding first, this would
|
|
1363
|
+
// edit a caller's array — and an array keeps what it is given, so the
|
|
1364
|
+
// damage would outlive the call.
|
|
1365
|
+
const foreign = { id: 'foreign' } as unknown as WebGLVertexArrayObject
|
|
1366
|
+
gl.getParameter = vi.fn(() => foreign) as any
|
|
1367
|
+
const bindVertexArray = vi.fn()
|
|
1368
|
+
gl.bindVertexArray = bindVertexArray as any
|
|
1369
|
+
|
|
1370
|
+
const attributes = attributeView(gl as any, program, {
|
|
1371
|
+
a_corner: { kind: 'vec2' },
|
|
1372
|
+
} satisfies AttributeSchema)
|
|
1373
|
+
|
|
1374
|
+
const restore = attributes.a_corner.bind()
|
|
1375
|
+
expect(bindVertexArray).toHaveBeenCalledWith(null)
|
|
1376
|
+
restore()
|
|
1377
|
+
expect(bindVertexArray).toHaveBeenLastCalledWith(foreign)
|
|
1378
|
+
})
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
describe('the constant source', () => {
|
|
1382
|
+
let gl: WebGL2RenderingContext
|
|
1383
|
+
let program: WebGLProgram
|
|
1384
|
+
beforeEach(() => {
|
|
1385
|
+
const mock = createMockCanvas()
|
|
1386
|
+
gl = mock.gl
|
|
1387
|
+
program = gl.createProgram()!
|
|
1388
|
+
})
|
|
1389
|
+
|
|
1390
|
+
const schema = {
|
|
1391
|
+
a_instance: {
|
|
1392
|
+
layout: [
|
|
1393
|
+
{ key: 'a_pos', kind: 'vec2' },
|
|
1394
|
+
{ key: 'a_size', kind: 'float' },
|
|
1395
|
+
],
|
|
1396
|
+
},
|
|
1397
|
+
} satisfies InterleavedAttributeSchema
|
|
1398
|
+
|
|
1399
|
+
it('gives the layout per-key setters, like a uniform', () => {
|
|
1400
|
+
const { a_instance } = interleavedAttributeView(gl as any, program, schema)
|
|
1401
|
+
|
|
1402
|
+
expect(a_instance.constant.a_pos.set).toBeInstanceOf(Function)
|
|
1403
|
+
expect(a_instance.constant.a_size.set).toBeInstanceOf(Function)
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
it('disables the arrays, so the attributes read the constants', () => {
|
|
1407
|
+
const { a_instance } = interleavedAttributeView(gl as any, program, schema)
|
|
1408
|
+
|
|
1409
|
+
a_instance.constant.applyToVertexArray()
|
|
1410
|
+
|
|
1411
|
+
expect(gl.disableVertexAttribArray).toHaveBeenCalledTimes(2)
|
|
1412
|
+
})
|
|
1413
|
+
|
|
1414
|
+
it('writes the values on every bind, not once into the array', () => {
|
|
1415
|
+
// The enable flag is vertex-array state; the values are context state, and
|
|
1416
|
+
// no array can keep them. Applying them once at construction would leave a
|
|
1417
|
+
// later draw showing whatever the previous one set.
|
|
1418
|
+
const { a_instance } = interleavedAttributeView(gl as any, program, schema)
|
|
1419
|
+
a_instance.constant.a_pos.set(1, 2)
|
|
1420
|
+
a_instance.constant.a_size.set(4)
|
|
1421
|
+
|
|
1422
|
+
const vao = vaoView(gl as any, [a_instance.constant])
|
|
1423
|
+
expect(gl.vertexAttrib2f).not.toHaveBeenCalled()
|
|
1424
|
+
|
|
1425
|
+
vao.bind()
|
|
1426
|
+
expect(gl.vertexAttrib2f).toHaveBeenCalledWith(expect.any(Number), 1, 2)
|
|
1427
|
+
expect(gl.vertexAttrib1f).toHaveBeenCalledWith(expect.any(Number), 4)
|
|
1428
|
+
|
|
1429
|
+
a_instance.constant.a_pos.set(7, 8)
|
|
1430
|
+
vao.bind()
|
|
1431
|
+
expect(gl.vertexAttrib2f).toHaveBeenLastCalledWith(expect.any(Number), 7, 8)
|
|
1432
|
+
})
|
|
1433
|
+
|
|
1434
|
+
it('refuses a kind vertexAttrib* cannot express, when asked for one', () => {
|
|
1435
|
+
// A hard limit of the API, not a choice: vertexAttrib* takes at most four
|
|
1436
|
+
// floats and has no matrix form. The buffer source handles it fine, so the
|
|
1437
|
+
// complaint waits until someone reaches for a constant.
|
|
1438
|
+
const matrix = {
|
|
1439
|
+
a_instance: { layout: [{ key: 'a_model', kind: 'mat4' }] },
|
|
1440
|
+
} satisfies InterleavedAttributeSchema
|
|
1441
|
+
|
|
1442
|
+
const { a_instance } = interleavedAttributeView(gl as any, program, matrix)
|
|
1443
|
+
|
|
1444
|
+
expect(() => a_instance.buffer.bind()).not.toThrow()
|
|
1445
|
+
expect(() => a_instance.constant).toThrow(/cannot be a constant attribute/)
|
|
1446
|
+
})
|
|
1447
|
+
})
|