@bigmistqke/view.gl 0.2.0 → 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-EYXOSM7B.mjs → chunk-X4M3LV4R.mjs} +60 -133
- 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 +13 -4
- package/dist/index.mjs +7 -2
- package/dist/tag.d.mts +6 -1
- package/dist/tag.mjs +13 -5
- package/dist/tag.mjs.map +1 -1
- package/dist/{types-CyGyGi18.d.mts → types-CNdqh7tB.d.mts} +34 -4
- package/package.json +1 -1
- package/src/gl.ts +13 -2
- package/src/index.ts +55 -39
- package/src/tag.ts +18 -2
- package/src/types.ts +33 -3
- package/src/utils.ts +45 -10
- package/test/index.test.ts +111 -29
- package/test/mocks/webgl.ts +14 -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-EYXOSM7B.mjs.map +0 -1
package/src/types.ts
CHANGED
|
@@ -365,6 +365,15 @@ export interface AttributeMethods<T extends AttributeDefinition = AttributeDefin
|
|
|
365
365
|
*/
|
|
366
366
|
bind(): () => void
|
|
367
367
|
set(data: FormatToArray[ResolvedFormat<T>], usage?: GLUsage): { bind(): () => void }
|
|
368
|
+
/**
|
|
369
|
+
* Delete the buffer, if this attribute is the one that made it.
|
|
370
|
+
*
|
|
371
|
+
* A buffer named by the schema came from somewhere else and is deleted by
|
|
372
|
+
* whoever made it — a shared buffer is the point of passing one in, and one
|
|
373
|
+
* view cannot know whether the others are done. So this is safe to call
|
|
374
|
+
* twice and safe to call on a borrowed buffer, and it is not a way to free
|
|
375
|
+
* one: dispose the view that created it.
|
|
376
|
+
*/
|
|
368
377
|
dispose(): void
|
|
369
378
|
}
|
|
370
379
|
|
|
@@ -502,6 +511,12 @@ export interface TextureDefinition {
|
|
|
502
511
|
export interface Framebuffer {
|
|
503
512
|
texture: WebGLTexture
|
|
504
513
|
framebuffer: WebGLFramebuffer
|
|
514
|
+
/**
|
|
515
|
+
* Delete the framebuffer, and the texture only if `createFramebuffer` made
|
|
516
|
+
* it. A texture handed in belongs to whoever made it. Idempotent, and exactly
|
|
517
|
+
* what aborting the signal does.
|
|
518
|
+
*/
|
|
519
|
+
dispose(): void
|
|
505
520
|
}
|
|
506
521
|
|
|
507
522
|
/**********************************************************************************/
|
|
@@ -617,11 +632,16 @@ export interface ViewOptions {
|
|
|
617
632
|
signal?: AbortSignal
|
|
618
633
|
}
|
|
619
634
|
|
|
635
|
+
/**
|
|
636
|
+
* What a program has names for. Buffers are absent on purpose: a buffer has no
|
|
637
|
+
* name in the source, so it cannot be resolved against a program the way a
|
|
638
|
+
* uniform or an attribute is, and its lifetime is rarely a program's. Build one
|
|
639
|
+
* with `bufferView` and pass it to `vao()` alongside the attributes.
|
|
640
|
+
*/
|
|
620
641
|
export interface ViewSchema {
|
|
621
642
|
uniforms?: UniformSchema
|
|
622
643
|
attributes?: AttributeSchema
|
|
623
644
|
interleavedAttributes?: InterleavedAttributeSchema
|
|
624
|
-
buffers?: BufferSchema
|
|
625
645
|
}
|
|
626
646
|
|
|
627
647
|
type PartialSchema<T> =
|
|
@@ -631,7 +651,6 @@ export interface ViewSchemaPartial {
|
|
|
631
651
|
uniforms?: PartialSchema<UniformSchema>
|
|
632
652
|
attributes?: PartialSchema<AttributeSchema>
|
|
633
653
|
interleavedAttributes?: PartialSchema<InterleavedAttributeSchema>
|
|
634
|
-
buffers?: PartialSchema<BufferSchema>
|
|
635
654
|
}
|
|
636
655
|
|
|
637
656
|
export type View<T extends ViewSchema = ViewSchema> = {
|
|
@@ -641,7 +660,6 @@ export type View<T extends ViewSchema = ViewSchema> = {
|
|
|
641
660
|
interleavedAttributes: T['interleavedAttributes'] extends InterleavedAttributeSchema
|
|
642
661
|
? Prettify<InterleavedAttributeView<T['interleavedAttributes']>>
|
|
643
662
|
: {}
|
|
644
|
-
buffers: T['buffers'] extends BufferSchema ? Prettify<BufferView<T['buffers']>> : {}
|
|
645
663
|
uniforms: T['uniforms'] extends UniformSchema ? Prettify<UniformView<T['uniforms']>> : {}
|
|
646
664
|
/**
|
|
647
665
|
* Build a vertex array from the participants one draw needs — its attributes,
|
|
@@ -653,6 +671,13 @@ export type View<T extends ViewSchema = ViewSchema> = {
|
|
|
653
671
|
* something you say rather than something you inherit from call order.
|
|
654
672
|
*/
|
|
655
673
|
vao(participants: VertexArrayParticipant[]): VertexArrayMethods
|
|
674
|
+
/**
|
|
675
|
+
* Delete everything the view made: the buffers behind its attributes and
|
|
676
|
+
* layouts, and any vertex array still held from `vao()`. Buffers named by the
|
|
677
|
+
* schema are left alone — those belong to whoever made them. Idempotent, and
|
|
678
|
+
* exactly what aborting the signal does.
|
|
679
|
+
*/
|
|
680
|
+
dispose(): void
|
|
656
681
|
}
|
|
657
682
|
|
|
658
683
|
/**********************************************************************************/
|
|
@@ -881,5 +906,10 @@ export type CompileResult<
|
|
|
881
906
|
view: Prettify<View<TSchema>>
|
|
882
907
|
vertex: string
|
|
883
908
|
fragment: string
|
|
909
|
+
/**
|
|
910
|
+
* Delete the program and everything its view made. Idempotent, and
|
|
911
|
+
* exactly what aborting the signal does.
|
|
912
|
+
*/
|
|
913
|
+
dispose(): void
|
|
884
914
|
}
|
|
885
915
|
: never
|
package/src/utils.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
KIND_TO_UNIFORM_FN_NAME_MAP,
|
|
7
7
|
TextureDefinition,
|
|
8
8
|
UniformKind,
|
|
9
|
+
ViewOptions,
|
|
9
10
|
} from './types'
|
|
10
11
|
|
|
11
12
|
export function assertedNotNullish<T>(value: T, message?: string): NonNullable<T> {
|
|
@@ -13,6 +14,24 @@ export function assertedNotNullish<T>(value: T, message?: string): NonNullable<T
|
|
|
13
14
|
return value
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a teardown so it runs once.
|
|
19
|
+
*
|
|
20
|
+
* Two reasons, and both bite. A disposer is per `bind()`, and undoing twice
|
|
21
|
+
* would put back state a later `bind()` is relying on. And a `dispose()` that
|
|
22
|
+
* deletes twice invalidates a handle someone else may still be holding — the
|
|
23
|
+
* object survives while it is bound, so the fault surfaces later, somewhere
|
|
24
|
+
* else. Idempotent is the only safe shape for either.
|
|
25
|
+
*/
|
|
26
|
+
export function once(teardown: () => void): () => void {
|
|
27
|
+
let done = false
|
|
28
|
+
return () => {
|
|
29
|
+
if (done) return
|
|
30
|
+
done = true
|
|
31
|
+
teardown()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
/**********************************************************************************/
|
|
17
36
|
/* */
|
|
18
37
|
/* Kind To Uniform Name */
|
|
@@ -114,8 +133,9 @@ export function createTexture(
|
|
|
114
133
|
height,
|
|
115
134
|
}: TextureDefinition,
|
|
116
135
|
data?: ArrayBufferView | null,
|
|
136
|
+
{ signal }: ViewOptions = {},
|
|
117
137
|
): WebGLTexture {
|
|
118
|
-
const texture = gl.createTexture()
|
|
138
|
+
const texture = assertedNotNullish(gl.createTexture(), 'Failed to create texture')
|
|
119
139
|
|
|
120
140
|
function getTextureConstant(name: string) {
|
|
121
141
|
if (!(name in gl)) {
|
|
@@ -137,13 +157,17 @@ export function createTexture(
|
|
|
137
157
|
data ?? null,
|
|
138
158
|
)
|
|
139
159
|
|
|
140
|
-
console.log(minFilter)
|
|
141
160
|
// Set texture parameters
|
|
142
161
|
gl.texParameteri(gl[target], gl.TEXTURE_MIN_FILTER, gl[minFilter])
|
|
143
162
|
gl.texParameteri(gl[target], gl.TEXTURE_MAG_FILTER, gl[magFilter])
|
|
144
163
|
gl.texParameteri(gl[target], gl.TEXTURE_WRAP_S, gl[wrapS])
|
|
145
164
|
gl.texParameteri(gl[target], gl.TEXTURE_WRAP_T, gl[wrapT])
|
|
146
165
|
|
|
166
|
+
signal?.addEventListener(
|
|
167
|
+
'abort',
|
|
168
|
+
once(() => gl.deleteTexture(texture)),
|
|
169
|
+
)
|
|
170
|
+
|
|
147
171
|
return texture
|
|
148
172
|
}
|
|
149
173
|
|
|
@@ -162,7 +186,7 @@ const FRAMEBUFFER_ATTACHMENT_MAP = {
|
|
|
162
186
|
|
|
163
187
|
class FramebufferError extends Error {
|
|
164
188
|
constructor(gl: GL, status: number) {
|
|
165
|
-
let errorMessage =
|
|
189
|
+
let errorMessage = 'Framebuffer not complete. Status: '
|
|
166
190
|
switch (status) {
|
|
167
191
|
case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
|
|
168
192
|
errorMessage += 'FRAMEBUFFER_INCOMPLETE_ATTACHMENT'
|
|
@@ -185,16 +209,15 @@ class FramebufferError extends Error {
|
|
|
185
209
|
|
|
186
210
|
export function createFramebuffer(
|
|
187
211
|
gl: GL,
|
|
188
|
-
{ attachment, texture, ...
|
|
212
|
+
{ attachment, texture: providedTexture, ...definition }: FramebufferDefinition,
|
|
213
|
+
{ signal }: ViewOptions = {},
|
|
189
214
|
) {
|
|
190
|
-
// Create texture for the framebuffer
|
|
191
|
-
|
|
215
|
+
// Create texture for the framebuffer, unless one was handed in. Which one it
|
|
216
|
+
// is decides who deletes it below — the same rule the attribute buffers follow.
|
|
217
|
+
const texture = providedTexture ?? createTexture(gl, definition)
|
|
192
218
|
|
|
193
219
|
// Create framebuffer
|
|
194
|
-
const framebuffer = assertedNotNullish(
|
|
195
|
-
gl.createFramebuffer(),
|
|
196
|
-
`Failed to create framebuffer: ${name}`,
|
|
197
|
-
)
|
|
220
|
+
const framebuffer = assertedNotNullish(gl.createFramebuffer(), 'Failed to create framebuffer')
|
|
198
221
|
|
|
199
222
|
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
|
|
200
223
|
|
|
@@ -213,9 +236,21 @@ export function createFramebuffer(
|
|
|
213
236
|
throw new FramebufferError(gl, status)
|
|
214
237
|
}
|
|
215
238
|
|
|
239
|
+
const dispose = once(() => {
|
|
240
|
+
gl.deleteFramebuffer(framebuffer)
|
|
241
|
+
// A texture handed in belongs to whoever made it — it commonly outlives the
|
|
242
|
+
// framebuffer, ping-ponged between two of them.
|
|
243
|
+
if (!providedTexture) {
|
|
244
|
+
gl.deleteTexture(texture)
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
signal?.addEventListener('abort', dispose)
|
|
249
|
+
|
|
216
250
|
return {
|
|
217
251
|
texture,
|
|
218
252
|
framebuffer,
|
|
253
|
+
dispose,
|
|
219
254
|
}
|
|
220
255
|
}
|
|
221
256
|
|
package/test/index.test.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
import {
|
|
3
3
|
attributeView,
|
|
4
4
|
bufferView,
|
|
5
|
+
createFramebuffer,
|
|
6
|
+
createTexture,
|
|
5
7
|
interleavedAttributeView,
|
|
6
8
|
uniformView,
|
|
7
9
|
vaoView,
|
|
@@ -45,7 +47,6 @@ describe('view', () => {
|
|
|
45
47
|
expect(result.uniforms!.u_texture.set).toBeInstanceOf(Function)
|
|
46
48
|
expect(result.attributes).toBeUndefined()
|
|
47
49
|
expect(result.interleavedAttributes).toBeUndefined()
|
|
48
|
-
expect(result.buffers).toBeUndefined()
|
|
49
50
|
})
|
|
50
51
|
|
|
51
52
|
it('should create a view with attributes', () => {
|
|
@@ -91,23 +92,6 @@ describe('view', () => {
|
|
|
91
92
|
expect(result.interleavedAttributes!.data.buffer.set).toBeInstanceOf(Function)
|
|
92
93
|
})
|
|
93
94
|
|
|
94
|
-
it('should create a view with buffers', () => {
|
|
95
|
-
const schema = {
|
|
96
|
-
buffers: {
|
|
97
|
-
vertices: { target: 'ARRAY_BUFFER', usage: 'STATIC_DRAW' },
|
|
98
|
-
indices: { target: 'ELEMENT_ARRAY_BUFFER', usage: 'DYNAMIC_DRAW' },
|
|
99
|
-
},
|
|
100
|
-
} satisfies ViewSchema
|
|
101
|
-
|
|
102
|
-
const result = view(gl, program, schema)
|
|
103
|
-
|
|
104
|
-
expect(result.buffers).toBeDefined()
|
|
105
|
-
expect(result.buffers!.vertices).toBeDefined()
|
|
106
|
-
expect(result.buffers!.vertices.bind).toBeInstanceOf(Function)
|
|
107
|
-
expect(result.buffers!.vertices.dispose).toBeInstanceOf(Function)
|
|
108
|
-
expect(result.buffers!.vertices.set).toBeInstanceOf(Function)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
95
|
it('should create a view with all components', () => {
|
|
112
96
|
const schema = {
|
|
113
97
|
uniforms: {
|
|
@@ -122,9 +106,6 @@ describe('view', () => {
|
|
|
122
106
|
instanced: false,
|
|
123
107
|
},
|
|
124
108
|
},
|
|
125
|
-
buffers: {
|
|
126
|
-
vertices: {},
|
|
127
|
-
},
|
|
128
109
|
} satisfies ViewSchema
|
|
129
110
|
|
|
130
111
|
const result = view(gl, program, schema)
|
|
@@ -132,14 +113,12 @@ describe('view', () => {
|
|
|
132
113
|
expect(result.uniforms).toBeDefined()
|
|
133
114
|
expect(result.attributes).toBeDefined()
|
|
134
115
|
expect(result.interleavedAttributes).toBeDefined()
|
|
135
|
-
expect(result.buffers).toBeDefined()
|
|
136
116
|
})
|
|
137
117
|
|
|
138
118
|
it('should create a view with symbols for all component types', () => {
|
|
139
119
|
const u_time_symbol = Symbol('u_time')
|
|
140
120
|
const a_position_symbol = Symbol('a_position')
|
|
141
121
|
const data_symbol = Symbol('data')
|
|
142
|
-
const vertices_symbol = Symbol('vertices')
|
|
143
122
|
|
|
144
123
|
const schema = {
|
|
145
124
|
uniforms: {
|
|
@@ -154,9 +133,6 @@ describe('view', () => {
|
|
|
154
133
|
instanced: false,
|
|
155
134
|
},
|
|
156
135
|
},
|
|
157
|
-
buffers: {
|
|
158
|
-
[vertices_symbol]: {},
|
|
159
|
-
},
|
|
160
136
|
} satisfies ViewSchema
|
|
161
137
|
|
|
162
138
|
const result = view(gl, program, schema)
|
|
@@ -169,9 +145,6 @@ describe('view', () => {
|
|
|
169
145
|
|
|
170
146
|
expect(result.interleavedAttributes![data_symbol]).toBeDefined()
|
|
171
147
|
expect(result.interleavedAttributes![data_symbol].buffer.bind).toBeInstanceOf(Function)
|
|
172
|
-
|
|
173
|
-
expect(result.buffers![vertices_symbol]).toBeDefined()
|
|
174
|
-
expect(result.buffers![vertices_symbol].bind).toBeInstanceOf(Function)
|
|
175
148
|
})
|
|
176
149
|
|
|
177
150
|
it('should create a view with mixed string and symbol keys', () => {
|
|
@@ -201,6 +174,60 @@ describe('view', () => {
|
|
|
201
174
|
})
|
|
202
175
|
})
|
|
203
176
|
|
|
177
|
+
describe('disposing a view', () => {
|
|
178
|
+
let gl: WebGL2RenderingContext
|
|
179
|
+
let program: WebGLProgram
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
const mock = createMockCanvas()
|
|
182
|
+
gl = mock.gl
|
|
183
|
+
program = gl.createProgram()!
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const schema = {
|
|
187
|
+
attributes: { a_position: { kind: 'vec2' } },
|
|
188
|
+
interleavedAttributes: { data: { layout: [{ key: 'a_uv', kind: 'vec2' }] } },
|
|
189
|
+
} satisfies ViewSchema
|
|
190
|
+
|
|
191
|
+
it('deletes the buffers it made, and the arrays it handed out', () => {
|
|
192
|
+
const result = view(gl, program, schema)
|
|
193
|
+
const vertexArray = result.vao([result.attributes.a_position])
|
|
194
|
+
|
|
195
|
+
result.dispose()
|
|
196
|
+
|
|
197
|
+
expect(gl.deleteBuffer).toHaveBeenCalledTimes(2)
|
|
198
|
+
expect(gl.deleteVertexArray).toHaveBeenCalledTimes(1)
|
|
199
|
+
void vertexArray
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('does the same thing on abort as it does on dispose', () => {
|
|
203
|
+
const controller = new AbortController()
|
|
204
|
+
const result = view(gl, program, schema, { signal: controller.signal })
|
|
205
|
+
result.vao([result.attributes.a_position])
|
|
206
|
+
|
|
207
|
+
controller.abort()
|
|
208
|
+
const afterAbort = (gl.deleteBuffer as any).mock.calls.length
|
|
209
|
+
|
|
210
|
+
// Both routes lead to the same teardown, so taking both must not delete
|
|
211
|
+
// twice: the second delete would invalidate a handle someone else holds.
|
|
212
|
+
result.dispose()
|
|
213
|
+
expect((gl.deleteBuffer as any).mock.calls).toHaveLength(afterAbort)
|
|
214
|
+
expect(gl.deleteVertexArray).toHaveBeenCalledTimes(1)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('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
|
+
|
|
204
231
|
describe('uniformView', () => {
|
|
205
232
|
let gl: ReturnType<typeof createMockGL>
|
|
206
233
|
let program: WebGLProgram
|
|
@@ -482,6 +509,28 @@ describe('attributeView', () => {
|
|
|
482
509
|
expect(gl.createBuffer).toHaveBeenCalledTimes(1) // Only for the custom buffer
|
|
483
510
|
})
|
|
484
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
|
+
|
|
485
534
|
it('should handle float attributes', () => {
|
|
486
535
|
const schema = {
|
|
487
536
|
a_scalar: { kind: 'float' },
|
|
@@ -1445,3 +1494,36 @@ describe('the constant source', () => {
|
|
|
1445
1494
|
expect(() => a_instance.constant).toThrow(/cannot be a constant attribute/)
|
|
1446
1495
|
})
|
|
1447
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(() => {
|
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')}
|
package/dist/chunk-CJTT7WPV.mjs
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// src/gl.ts
|
|
2
|
-
function isWebGL2RenderingContext(gl) {
|
|
3
|
-
return typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext;
|
|
4
|
-
}
|
|
5
|
-
function createShader(gl, type, source) {
|
|
6
|
-
const shader = gl.createShader(type);
|
|
7
|
-
if (!shader) throw new Error("Failed to create shader");
|
|
8
|
-
gl.shaderSource(shader, source);
|
|
9
|
-
gl.compileShader(shader);
|
|
10
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
11
|
-
const info = gl.getShaderInfoLog(shader);
|
|
12
|
-
gl.deleteShader(shader);
|
|
13
|
-
throw new Error(
|
|
14
|
-
`Failed to compile ${type === gl.VERTEX_SHADER ? "vertex" : "fragment"} shader: ${info}`
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
return shader;
|
|
18
|
-
}
|
|
19
|
-
function createProgram(gl, vertexSource, fragmentSource) {
|
|
20
|
-
const program = gl.createProgram();
|
|
21
|
-
if (!program) throw new Error("Failed to create WebGL program");
|
|
22
|
-
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
|
|
23
|
-
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
|
|
24
|
-
gl.attachShader(program, vertexShader);
|
|
25
|
-
gl.attachShader(program, fragmentShader);
|
|
26
|
-
gl.linkProgram(program);
|
|
27
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
28
|
-
const info = gl.getProgramInfoLog(program);
|
|
29
|
-
gl.deleteProgram(program);
|
|
30
|
-
gl.deleteShader(vertexShader);
|
|
31
|
-
gl.deleteShader(fragmentShader);
|
|
32
|
-
throw new Error(`Failed to link program: ${info}`);
|
|
33
|
-
}
|
|
34
|
-
gl.deleteShader(vertexShader);
|
|
35
|
-
gl.deleteShader(fragmentShader);
|
|
36
|
-
return program;
|
|
37
|
-
}
|
|
38
|
-
var INSTANCED_ARRAYS_WRAPPER_MAP = /* @__PURE__ */ new WeakMap();
|
|
39
|
-
function getInstancedArrays(gl) {
|
|
40
|
-
if (isWebGL2RenderingContext(gl)) return gl;
|
|
41
|
-
const cached = INSTANCED_ARRAYS_WRAPPER_MAP.get(gl);
|
|
42
|
-
if (cached) return cached;
|
|
43
|
-
const ext = gl.getExtension("ANGLE_instanced_arrays");
|
|
44
|
-
if (!ext) return void 0;
|
|
45
|
-
const wrapper = {
|
|
46
|
-
drawArraysInstanced: ext.drawArraysInstancedANGLE.bind(ext),
|
|
47
|
-
drawElementsInstanced: ext.drawElementsInstancedANGLE.bind(ext),
|
|
48
|
-
vertexAttribDivisor: ext.vertexAttribDivisorANGLE.bind(ext)
|
|
49
|
-
};
|
|
50
|
-
INSTANCED_ARRAYS_WRAPPER_MAP.set(gl, wrapper);
|
|
51
|
-
return wrapper;
|
|
52
|
-
}
|
|
53
|
-
var VERTEX_ARRAY_OBJECT_WRAPPER_MAP = /* @__PURE__ */ new WeakMap();
|
|
54
|
-
function getVertexArrayObject(gl) {
|
|
55
|
-
if (isWebGL2RenderingContext(gl)) return gl;
|
|
56
|
-
const cached = VERTEX_ARRAY_OBJECT_WRAPPER_MAP.get(gl);
|
|
57
|
-
if (cached) return cached;
|
|
58
|
-
const ext = gl.getExtension("OES_vertex_array_object");
|
|
59
|
-
if (!ext) return null;
|
|
60
|
-
const wrapper = {
|
|
61
|
-
bindVertexArray: ext.bindVertexArrayOES.bind(ext),
|
|
62
|
-
createVertexArray: ext.createVertexArrayOES.bind(ext),
|
|
63
|
-
deleteVertexArray: ext.deleteVertexArrayOES.bind(ext)
|
|
64
|
-
};
|
|
65
|
-
VERTEX_ARRAY_OBJECT_WRAPPER_MAP.set(gl, wrapper);
|
|
66
|
-
return wrapper;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export {
|
|
70
|
-
createShader,
|
|
71
|
-
createProgram,
|
|
72
|
-
getInstancedArrays,
|
|
73
|
-
getVertexArrayObject
|
|
74
|
-
};
|
|
75
|
-
//# sourceMappingURL=chunk-CJTT7WPV.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/gl.ts"],"sourcesContent":["import { RemoveSuffix } from './type-utils'\nimport type { GL } from './types'\n\nfunction isWebGL2RenderingContext(\n gl: WebGLRenderingContext | WebGL2RenderingContext,\n): gl is WebGL2RenderingContext {\n return typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext\n}\n\n/**********************************************************************************/\n/* */\n/* Create Shader */\n/* */\n/**********************************************************************************/\n\nexport function createShader(gl: GL, type: number, source: string): WebGLShader {\n const shader = gl.createShader(type)\n if (!shader) throw new Error('Failed to create shader')\n\n gl.shaderSource(shader, source)\n gl.compileShader(shader)\n\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const info = gl.getShaderInfoLog(shader)\n gl.deleteShader(shader)\n throw new Error(\n `Failed to compile ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader: ${info}`,\n )\n }\n\n return shader\n}\n\n/**********************************************************************************/\n/* */\n/* Create Program */\n/* */\n/**********************************************************************************/\n\nexport function createProgram(gl: GL, vertexSource: string, fragmentSource: string): WebGLProgram {\n const program = gl.createProgram()\n if (!program) throw new Error('Failed to create WebGL program')\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)\n\n gl.attachShader(program, vertexShader)\n gl.attachShader(program, fragmentShader)\n gl.linkProgram(program)\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const info = gl.getProgramInfoLog(program)\n gl.deleteProgram(program)\n gl.deleteShader(vertexShader)\n gl.deleteShader(fragmentShader)\n throw new Error(`Failed to link program: ${info}`)\n }\n\n // Clean up shaders after linking\n gl.deleteShader(vertexShader)\n gl.deleteShader(fragmentShader)\n\n return program\n}\n\n/**********************************************************************************/\n/* */\n/* Get Instanced Arrays */\n/* */\n/**********************************************************************************/\n\nconst INSTANCED_ARRAYS_WRAPPER_MAP = new WeakMap<\n GL,\n RemoveSuffix<\n Pick<\n ANGLE_instanced_arrays,\n 'drawArraysInstancedANGLE' | 'drawElementsInstancedANGLE' | 'vertexAttribDivisorANGLE'\n >,\n 'ANGLE'\n > | null\n>()\nexport function getInstancedArrays(gl: GL) {\n if (isWebGL2RenderingContext(gl)) return gl\n\n const cached = INSTANCED_ARRAYS_WRAPPER_MAP.get(gl)\n if (cached) return cached\n\n const ext = gl.getExtension('ANGLE_instanced_arrays')\n if (!ext) return undefined\n\n const wrapper = {\n drawArraysInstanced: ext.drawArraysInstancedANGLE.bind(ext),\n drawElementsInstanced: ext.drawElementsInstancedANGLE.bind(ext),\n vertexAttribDivisor: ext.vertexAttribDivisorANGLE.bind(ext),\n }\n INSTANCED_ARRAYS_WRAPPER_MAP.set(gl, wrapper)\n\n return wrapper\n}\n\n/**********************************************************************************/\n/* */\n/* Get Vertex Array Object */\n/* */\n/**********************************************************************************/\n\nconst VERTEX_ARRAY_OBJECT_WRAPPER_MAP = new WeakMap<\n GL,\n RemoveSuffix<\n Pick<\n OES_vertex_array_object,\n 'bindVertexArrayOES' | 'createVertexArrayOES' | 'deleteVertexArrayOES'\n >,\n 'OES'\n > | null\n>()\nexport function getVertexArrayObject(gl: GL) {\n if (isWebGL2RenderingContext(gl)) return gl\n\n const cached = VERTEX_ARRAY_OBJECT_WRAPPER_MAP.get(gl)\n if (cached) return cached\n\n const ext = gl.getExtension('OES_vertex_array_object')\n if (!ext) return null\n\n const wrapper = {\n bindVertexArray: ext.bindVertexArrayOES.bind(ext),\n createVertexArray: ext.createVertexArrayOES.bind(ext),\n deleteVertexArray: ext.deleteVertexArrayOES.bind(ext),\n }\n VERTEX_ARRAY_OBJECT_WRAPPER_MAP.set(gl, wrapper)\n\n return wrapper\n}\n"],"mappings":";AAGA,SAAS,yBACP,IAC8B;AAC9B,SAAO,OAAO,2BAA2B,eAAe,cAAc;AACxE;AAQO,SAAS,aAAa,IAAQ,MAAc,QAA6B;AAC9E,QAAM,SAAS,GAAG,aAAa,IAAI;AACnC,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,yBAAyB;AAEtD,KAAG,aAAa,QAAQ,MAAM;AAC9B,KAAG,cAAc,MAAM;AAEvB,MAAI,CAAC,GAAG,mBAAmB,QAAQ,GAAG,cAAc,GAAG;AACrD,UAAM,OAAO,GAAG,iBAAiB,MAAM;AACvC,OAAG,aAAa,MAAM;AACtB,UAAM,IAAI;AAAA,MACR,qBAAqB,SAAS,GAAG,gBAAgB,WAAW,UAAU,YAAY,IAAI;AAAA,IACxF;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,cAAc,IAAQ,cAAsB,gBAAsC;AAChG,QAAM,UAAU,GAAG,cAAc;AACjC,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,gCAAgC;AAE9D,QAAM,eAAe,aAAa,IAAI,GAAG,eAAe,YAAY;AACpE,QAAM,iBAAiB,aAAa,IAAI,GAAG,iBAAiB,cAAc;AAE1E,KAAG,aAAa,SAAS,YAAY;AACrC,KAAG,aAAa,SAAS,cAAc;AACvC,KAAG,YAAY,OAAO;AAEtB,MAAI,CAAC,GAAG,oBAAoB,SAAS,GAAG,WAAW,GAAG;AACpD,UAAM,OAAO,GAAG,kBAAkB,OAAO;AACzC,OAAG,cAAc,OAAO;AACxB,OAAG,aAAa,YAAY;AAC5B,OAAG,aAAa,cAAc;AAC9B,UAAM,IAAI,MAAM,2BAA2B,IAAI,EAAE;AAAA,EACnD;AAGA,KAAG,aAAa,YAAY;AAC5B,KAAG,aAAa,cAAc;AAE9B,SAAO;AACT;AAQA,IAAM,+BAA+B,oBAAI,QASvC;AACK,SAAS,mBAAmB,IAAQ;AACzC,MAAI,yBAAyB,EAAE,EAAG,QAAO;AAEzC,QAAM,SAAS,6BAA6B,IAAI,EAAE;AAClD,MAAI,OAAQ,QAAO;AAEnB,QAAM,MAAM,GAAG,aAAa,wBAAwB;AACpD,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU;AAAA,IACd,qBAAqB,IAAI,yBAAyB,KAAK,GAAG;AAAA,IAC1D,uBAAuB,IAAI,2BAA2B,KAAK,GAAG;AAAA,IAC9D,qBAAqB,IAAI,yBAAyB,KAAK,GAAG;AAAA,EAC5D;AACA,+BAA6B,IAAI,IAAI,OAAO;AAE5C,SAAO;AACT;AAQA,IAAM,kCAAkC,oBAAI,QAS1C;AACK,SAAS,qBAAqB,IAAQ;AAC3C,MAAI,yBAAyB,EAAE,EAAG,QAAO;AAEzC,QAAM,SAAS,gCAAgC,IAAI,EAAE;AACrD,MAAI,OAAQ,QAAO;AAEnB,QAAM,MAAM,GAAG,aAAa,yBAAyB;AACrD,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU;AAAA,IACd,iBAAiB,IAAI,mBAAmB,KAAK,GAAG;AAAA,IAChD,mBAAmB,IAAI,qBAAqB,KAAK,GAAG;AAAA,IACpD,mBAAmB,IAAI,qBAAqB,KAAK,GAAG;AAAA,EACtD;AACA,kCAAgC,IAAI,IAAI,OAAO;AAE/C,SAAO;AACT;","names":[]}
|