@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.
@@ -210,11 +210,48 @@ type FormatToArray = {
210
210
  int8: Int8Array;
211
211
  uint8: Uint8Array;
212
212
  };
213
- interface AttributeMethods<T extends AttributeDefinition = AttributeDefinition> {
213
+ /**
214
+ * What a participant contributes to a vertex array.
215
+ *
216
+ * `enableVertexAttribArray`, `vertexAttribPointer`, `vertexAttribDivisor` and
217
+ * the `ELEMENT_ARRAY_BUFFER` binding all write into whichever vertex array is
218
+ * bound when they run — they name no destination of their own. That is what
219
+ * makes them a participant in something rather than a property of themselves,
220
+ * and why `vao()` exists: it binds the array first, then asks each participant
221
+ * to write itself in.
222
+ *
223
+ * The returned function undoes the write. It is only used where there is no
224
+ * vertex array to hold the state — a WebGL1 context without
225
+ * `OES_vertex_array_object` — since a real vertex array is thrown away whole.
226
+ */
227
+ interface VertexArrayParticipant {
228
+ applyToVertexArray(): () => void;
229
+ /**
230
+ * State the CONTEXT holds rather than the vertex array, re-applied on every
231
+ * bind because no array can keep it.
232
+ *
233
+ * There is exactly one such thing: the current generic vertex attribute
234
+ * values, which an attribute reads when its array is disabled. The enable
235
+ * flag saying to read them is vertex-array state; the values themselves are
236
+ * not. So a constant source splits across the two hooks — and the tempting
237
+ * optimisation of skipping this when nothing changed would let one draw's
238
+ * constants leak into the next.
239
+ */
240
+ applyToContext?(): void;
241
+ }
242
+ interface AttributeMethods<T extends AttributeDefinition = AttributeDefinition> extends VertexArrayParticipant {
214
243
  buffer: WebGLBuffer;
215
- bind(): void;
244
+ /**
245
+ * Apply this attribute to the DEFAULT vertex array, returning a disposer.
246
+ *
247
+ * Explicitly the default one: these calls have no destination of their own,
248
+ * so without binding first this would edit whichever array happened to be
249
+ * bound — and a vertex array keeps what it is given, so the damage would
250
+ * outlive the call. Use `vao()` to build an array of your own.
251
+ */
252
+ bind(): () => void;
216
253
  set(data: FormatToArray[ResolvedFormat<T>], usage?: GLUsage): {
217
- bind(): void;
254
+ bind(): () => void;
218
255
  };
219
256
  dispose(): void;
220
257
  }
@@ -234,10 +271,48 @@ interface InterleavedAttributeDefinition {
234
271
  instanced?: boolean;
235
272
  }
236
273
  type InterleavedAttributeSchema = Record<string | symbol, InterleavedAttributeDefinition>;
274
+ /**
275
+ * A source is where one draw's values for a layout come from. Both kinds are
276
+ * participants, so either can go into a `vao()`.
277
+ */
278
+ interface Source extends VertexArrayParticipant {
279
+ /**
280
+ * Apply this source to the DEFAULT vertex array, returning a disposer. See
281
+ * `AttributeMethods.bind` — and prefer `vao()`, which is the only way to get
282
+ * a source and its fellow participants into an array of their own.
283
+ */
284
+ bind(): () => void;
285
+ }
286
+ interface BufferSource extends Source {
287
+ /**
288
+ * The same buffer, stepped once per instance rather than once per vertex.
289
+ *
290
+ * A divisor is a property of the draw, not of the data — the same markers
291
+ * are an instanced fleet in one draw and a single mesh in the next — so it
292
+ * is chosen here rather than declared in the schema.
293
+ */
294
+ perInstance: Source;
295
+ set(data: Float32Array, usage?: GLUsage): void;
296
+ }
297
+ interface ConstantAttributeMethods {
298
+ set(...values: number[]): void;
299
+ }
300
+ /**
301
+ * Per-key setters over the same layout the buffer source uses — a constant
302
+ * attribute is a uniform in all but name, so it is written like one.
303
+ */
304
+ type ConstantSource<T extends InterleavedAttributeLayout[]> = Source & {
305
+ [K in T[number] as K['key'] extends string ? K['key'] : never]: ConstantAttributeMethods;
306
+ };
237
307
  interface InterleavedAttributeMethods<T extends InterleavedAttributeLayout[] = InterleavedAttributeLayout[]> {
238
- bind(): void;
308
+ buffer: BufferSource;
309
+ constant: ConstantSource<T>;
310
+ dispose(): void;
311
+ }
312
+ interface VertexArrayMethods {
313
+ /** Binds the vertex array, returning a disposer that restores the previous one */
314
+ bind(): () => void;
239
315
  unbind(): void;
240
- set(data: Float32Array, usage?: GLUsage): void;
241
316
  dispose(): void;
242
317
  }
243
318
  type InterleavedAttributeView<T extends InterleavedAttributeSchema> = {
@@ -250,9 +325,10 @@ interface BufferDefinition {
250
325
  usage?: GLUsage;
251
326
  }
252
327
  type BufferSchema = Record<string | symbol, BufferDefinition>;
253
- interface BufferMethods {
328
+ interface BufferMethods extends VertexArrayParticipant {
254
329
  set(data: Float32Array | Uint16Array | Uint32Array): void;
255
- bind(): void;
330
+ /** Binds the buffer, returning a disposer that restores the previous binding */
331
+ bind(): () => void;
256
332
  dispose(): void;
257
333
  }
258
334
  type BufferView<T extends BufferSchema> = {
@@ -335,6 +411,16 @@ type View<T extends ViewSchema = ViewSchema> = {
335
411
  interleavedAttributes: T['interleavedAttributes'] extends InterleavedAttributeSchema ? Prettify<InterleavedAttributeView<T['interleavedAttributes']>> : {};
336
412
  buffers: T['buffers'] extends BufferSchema ? Prettify<BufferView<T['buffers']>> : {};
337
413
  uniforms: T['uniforms'] extends UniformSchema ? Prettify<UniformView<T['uniforms']>> : {};
414
+ /**
415
+ * Build a vertex array from the participants one draw needs — its attributes,
416
+ * its interleaved layouts, its element buffer.
417
+ *
418
+ * This is the one member the schema cannot declare, because its input is the
419
+ * schema's own output. It is also the only place a vertex array is created:
420
+ * the participants no longer make one each, so which array they end up in is
421
+ * something you say rather than something you inherit from call order.
422
+ */
423
+ vao(participants: VertexArrayParticipant[]): VertexArrayMethods;
338
424
  };
339
425
  /**********************************************************************************/
340
426
  /**********************************************************************************/
@@ -436,4 +522,4 @@ type CompileResult<TVertex extends GLSL, TFragment extends GLSL, TOverrideSchema
436
522
  fragment: string;
437
523
  } : never;
438
524
 
439
- export type { TexImage2DOptions as $, AttributeSchema as A, BufferSchema as B, AttributeMethods as C, InterleavedAttributeLayout as D, InterleavedAttributeDefinition as E, InterleavedAttributeMethods as F, GL as G, BufferDefinition as H, InterleavedAttributeSchema as I, BufferMethods as J, KIND_TO_UNIFORM_FN_NAME_MAP as K, FramebufferDefinition as L, Framebuffer as M, TextureTarget as N, TextureSchema as O, TextureMethods as P, TextureView as Q, RemoveSuffix as R, TextureParameterName as S, TextureDefinition as T, UniformSchema as U, ViewSchema as V, TextureParameterValue as W, TextureParameters as X, InternalFormat as Y, Format as Z, PixelType as _, ViewOptions as a, ViewSchemaPartial as a0, GLSLResult as a1, GLSLSlot as a2, GLSL as a3, UniformToken as a4, UniformTokenFn as a5, AttributeToken as a6, AttributeTokenFn as a7, InterleaveToken as a8, GLSLToSchema as a9, GLSLToView as aa, GLSLSlotsToSchema as ab, FlattenSlots as ac, MergeGLSLSchema as ad, CompileResult as ae, Prettify as af, View as b, UniformView as c, AttributeView as d, InterleavedAttributeView as e, BufferView as f, GLTarget as g, GLUsage as h, GLTextureTarget as i, GLTextureFormat as j, GLTextureType as k, GLTextureFilter as l, GLTextureWrap as m, KIND_TO_SIZE_MAP as n, UniformFnName as o, UniformKind as p, UniformKindMap as q, UniformDefinition as r, UniformElementMethods as s, UniformArrayMethods as t, UniformMethods as u, UniformValue as v, AttributeShortHand as w, AttributeKind as x, AttributeFormat as y, AttributeDefinition as z };
525
+ export type { TextureParameterName as $, AttributeSchema as A, BufferSchema as B, AttributeFormat as C, AttributeDefinition as D, AttributeMethods as E, InterleavedAttributeLayout as F, GL as G, InterleavedAttributeDefinition as H, InterleavedAttributeSchema as I, BufferSource as J, KIND_TO_UNIFORM_FN_NAME_MAP as K, ConstantAttributeMethods as L, ConstantSource as M, InterleavedAttributeMethods as N, BufferDefinition as O, BufferMethods as P, FramebufferDefinition as Q, RemoveSuffix as R, Source as S, TextureDefinition as T, UniformSchema as U, ViewSchema as V, Framebuffer as W, TextureTarget as X, TextureSchema as Y, TextureMethods as Z, TextureView as _, ViewOptions as a, TextureParameterValue as a0, TextureParameters as a1, InternalFormat as a2, Format as a3, PixelType as a4, TexImage2DOptions as a5, ViewSchemaPartial as a6, GLSLResult as a7, GLSLSlot as a8, GLSL as a9, UniformToken as aa, UniformTokenFn as ab, AttributeToken as ac, AttributeTokenFn as ad, InterleaveToken as ae, GLSLToSchema as af, GLSLToView as ag, GLSLSlotsToSchema as ah, FlattenSlots as ai, MergeGLSLSchema as aj, CompileResult as ak, Prettify as al, View as b, VertexArrayParticipant as c, VertexArrayMethods as d, UniformView as e, AttributeView as f, InterleavedAttributeView as g, BufferView as h, GLTarget as i, GLUsage as j, GLTextureTarget as k, GLTextureFormat as l, GLTextureType as m, GLTextureFilter as n, GLTextureWrap as o, KIND_TO_SIZE_MAP as p, UniformFnName as q, UniformKind as r, UniformKindMap as s, UniformDefinition as t, UniformElementMethods as u, UniformArrayMethods as v, UniformMethods as w, UniformValue as x, AttributeShortHand as y, AttributeKind as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigmistqke/view.gl",
3
- "version": "0.1.18",
3
+ "version": "0.2.0",
4
4
  "description": "👀 Utilities for managing WebGL resources.",
5
5
  "exports": {
6
6
  ".": {
package/src/gl.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { RemoveSuffix } from './type-utils'
2
2
  import type { GL } from './types'
3
3
 
4
- function isWebGL2RenderingContext(gl: WebGLRenderingContext | WebGL2RenderingContext): gl is WebGL2RenderingContext {
4
+ function isWebGL2RenderingContext(
5
+ gl: WebGLRenderingContext | WebGL2RenderingContext,
6
+ ): gl is WebGL2RenderingContext {
5
7
  return typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext
6
8
  }
7
9
 
package/src/index.ts CHANGED
@@ -9,10 +9,14 @@ import type {
9
9
  BufferView,
10
10
  GL,
11
11
  UniformView as InferUniformView,
12
+ GLUsage,
13
+ InterleavedAttributeLayout,
12
14
  InterleavedAttributeMethods,
13
15
  InterleavedAttributeSchema,
14
16
  InterleavedAttributeView,
15
17
  UniformSchema,
18
+ VertexArrayMethods,
19
+ VertexArrayParticipant,
16
20
  View,
17
21
  ViewOptions,
18
22
  ViewSchema,
@@ -81,9 +85,96 @@ export function view<TSchema extends ViewSchema>(
81
85
  ? undefined
82
86
  : interleavedAttributeView(gl, program, schema.interleavedAttributes, options),
83
87
  buffers: !schema.buffers ? undefined : bufferView(gl, schema.buffers),
88
+ vao(participants: VertexArrayParticipant[]) {
89
+ return vaoView(gl, participants, options)
90
+ },
84
91
  } as View<TSchema>
85
92
  }
86
93
 
94
+ /**********************************************************************************/
95
+ /* */
96
+ /* Vertex Array */
97
+ /* */
98
+ /**********************************************************************************/
99
+
100
+ /**
101
+ * Bind the default vertex array, returning a disposer that puts the previous
102
+ * one back. The pointer calls name no destination, so anything applied without
103
+ * a vertex array of its own has to say which one it means — and a vertex array
104
+ * keeps what it is given, so writing into a caller's array by accident would
105
+ * outlive the call that did it.
106
+ */
107
+ function bindDefaultVertexArray(gl: GL): () => void {
108
+ const feature = getVertexArrayObject(gl)
109
+ if (!feature) {
110
+ return () => {}
111
+ }
112
+ const previous = gl.getParameter(VERTEX_ARRAY_BINDING) as WebGLVertexArrayObject | null
113
+ feature.bindVertexArray(null)
114
+ return () => feature.bindVertexArray(previous)
115
+ }
116
+
117
+ /**
118
+ * A vertex array over the participants one draw needs.
119
+ *
120
+ * Where the context has vertex arrays, each participant writes itself in once,
121
+ * here, and `bind()` is a single `bindVertexArray`. Where it does not — WebGL1
122
+ * without `OES_vertex_array_object` — there is nowhere to keep the state, so
123
+ * `bind()` re-applies every participant and the disposer puts back what they
124
+ * displaced. Same contract, and the emulation is why the name still fits.
125
+ */
126
+ export function vaoView(
127
+ gl: GL,
128
+ participants: VertexArrayParticipant[],
129
+ { signal }: ViewOptions = {},
130
+ ): VertexArrayMethods {
131
+ const feature = getVertexArrayObject(gl)
132
+
133
+ const methods: VertexArrayMethods = !feature
134
+ ? {
135
+ bind() {
136
+ const restores = participants.map(participant => participant.applyToVertexArray())
137
+ participants.forEach(participant => participant.applyToContext?.())
138
+ return once(() => restores.forEach(restore => restore()))
139
+ },
140
+ unbind() {},
141
+ dispose() {},
142
+ }
143
+ : (() => {
144
+ const vertexArray = assertedNotNullish(feature.createVertexArray())
145
+ const previous = gl.getParameter(VERTEX_ARRAY_BINDING) as WebGLVertexArrayObject | null
146
+ feature.bindVertexArray(vertexArray)
147
+ // Discarding the restorers is the point: the state lives in this array
148
+ // now, and the array is thrown away whole.
149
+ for (const participant of participants) {
150
+ participant.applyToVertexArray()
151
+ }
152
+ feature.bindVertexArray(previous)
153
+
154
+ return {
155
+ bind() {
156
+ const previousVertexArray = gl.getParameter(
157
+ VERTEX_ARRAY_BINDING,
158
+ ) as WebGLVertexArrayObject | null
159
+ feature.bindVertexArray(vertexArray)
160
+ // Context state the array cannot hold — see applyToContext.
161
+ participants.forEach(participant => participant.applyToContext?.())
162
+ return once(() => feature.bindVertexArray(previousVertexArray))
163
+ },
164
+ unbind() {
165
+ feature.bindVertexArray(null)
166
+ },
167
+ dispose() {
168
+ feature.deleteVertexArray(vertexArray)
169
+ },
170
+ }
171
+ })()
172
+
173
+ signal?.addEventListener('abort', () => methods.dispose())
174
+
175
+ return methods
176
+ }
177
+
87
178
  /**********************************************************************************/
88
179
  /* */
89
180
  /* Uniform View */
@@ -222,9 +313,38 @@ function handleAttribute(
222
313
  gl.vertexAttribPointer(location, size, gl[glType], normalized, stride, offset)
223
314
  }
224
315
 
316
+ // Always stated, never inherited: the divisor belongs to the location rather
317
+ // than to the program, so a location left at 1 by an earlier instanced
318
+ // attribute would feed one value to every vertex here.
319
+ // Get instanced-arrays-feature: extension if webgl, gl if webgl2
320
+ const instancedArrays = getInstancedArrays(gl)
225
321
  if (instanced) {
226
- // Get instanced-arrays-feature: extension if webgl, gl if webgl2
227
- assertedNotNullish(getInstancedArrays(gl)).vertexAttribDivisor(location, 1)
322
+ assertedNotNullish(instancedArrays).vertexAttribDivisor(location, 1)
323
+ } else if (instancedArrays) {
324
+ instancedArrays.vertexAttribDivisor(location, 0)
325
+ }
326
+ }
327
+
328
+ // Same enum in webgl2 and in the webgl1 extensions, which expose no constants
329
+ const VERTEX_ATTRIB_ARRAY_DIVISOR = 0x88fe
330
+ const VERTEX_ARRAY_BINDING = 0x85b5
331
+
332
+ // Reads the divisor currently attached to a location, 0 where instancing is
333
+ // unavailable and nothing can have set one
334
+ function readDivisor(gl: GL, location: number): number {
335
+ return getInstancedArrays(gl)
336
+ ? (gl.getVertexAttrib(location, VERTEX_ATTRIB_ARRAY_DIVISOR) as number)
337
+ : 0
338
+ }
339
+
340
+ // Wraps a restore so it runs once: a disposer is per bind() and undoing twice
341
+ // would put back state a later bind() is relying on
342
+ function once(restore: () => void): () => void {
343
+ let done = false
344
+ return () => {
345
+ if (done) return
346
+ done = true
347
+ restore()
228
348
  }
229
349
  }
230
350
 
@@ -237,7 +357,13 @@ export function attributeView<T extends AttributeSchema>(
237
357
  const attributes = mapObject(
238
358
  schema,
239
359
  (
240
- { kind, format, normalized = false, instanced, buffer = assertedNotNullish(gl.createBuffer()) },
360
+ {
361
+ kind,
362
+ format,
363
+ normalized = false,
364
+ instanced,
365
+ buffer = assertedNotNullish(gl.createBuffer()),
366
+ },
241
367
  key,
242
368
  ): AttributeMethods => {
243
369
  const name = toID(key)
@@ -252,11 +378,29 @@ export function attributeView<T extends AttributeSchema>(
252
378
  const glType = FORMAT_TO_GL_TYPE[resolvedFormat]
253
379
  const isIntKind = kind.startsWith('i') || kind.startsWith('u')
254
380
 
381
+ function applyToVertexArray() {
382
+ // Snapshot before the change, restore in the disposer
383
+ const previousDivisor = readDivisor(gl, location)
384
+ // vertexAttribPointer captures whatever is bound to ARRAY_BUFFER when
385
+ // it runs, so this binding is the setup for the pointer, not a lasting
386
+ // part of the vertex array's state.
387
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
388
+ handleAttribute(gl, location, size, 0, 0, glType, isIntKind, normalized, instanced)
389
+ return once(() => {
390
+ getInstancedArrays(gl)?.vertexAttribDivisor(location, previousDivisor)
391
+ })
392
+ }
393
+
255
394
  return {
256
395
  buffer,
396
+ applyToVertexArray,
257
397
  bind() {
258
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
259
- handleAttribute(gl, location, size, 0, 0, glType, isIntKind, normalized, instanced)
398
+ const restoreVertexArray = bindDefaultVertexArray(gl)
399
+ const restore = applyToVertexArray()
400
+ return once(() => {
401
+ restore()
402
+ restoreVertexArray()
403
+ })
260
404
  },
261
405
  dispose() {
262
406
  gl.deleteBuffer(buffer)
@@ -283,6 +427,73 @@ export function attributeView<T extends AttributeSchema>(
283
427
  /* */
284
428
  /**********************************************************************************/
285
429
 
430
+ /**
431
+ * The other place an attribute's value can come from: not a buffer, but the
432
+ * context's current generic vertex attribute values, which an attribute reads
433
+ * for every vertex when its array is disabled.
434
+ *
435
+ * A constant is a uniform in all but name — one value for the whole draw, set
436
+ * by a function chosen from the kind — so it gets the setters a uniform gets,
437
+ * off the same layout the buffer source uses.
438
+ *
439
+ * `vertexAttrib*` tops out at four floats and has no matrix form, so a layout
440
+ * key that is fine in a buffer can be impossible as a constant. That is a hard
441
+ * limit of the API rather than a choice, and it is worth saying so out loud at
442
+ * the point someone reaches for it.
443
+ */
444
+ function constantSource(gl: GL, layout: InterleavedAttributeLayout[], locations: number[]) {
445
+ const setters = layout.map((entry, index) => {
446
+ const size = kindToSize(entry.kind)
447
+ const name = toID(entry.key)
448
+ if (isMatKind(entry.kind) || size > 4) {
449
+ throw new Error(
450
+ `'${name}' is a ${entry.kind}, which cannot be a constant attribute: vertexAttrib* takes at most 4 floats and has no matrix form. Use the buffer source for it.`,
451
+ )
452
+ }
453
+ // The values are context state, not vertex-array state, so they are held
454
+ // here and written again on every bind — see applyToContext.
455
+ let values: number[] = []
456
+ const fn = gl[`vertexAttrib${size as 1 | 2 | 3 | 4}f`].bind(gl)
457
+ return {
458
+ name,
459
+ location: locations[index]!,
460
+ write: () => {
461
+ if (values.length) {
462
+ ;(fn as (location: number, ...args: number[]) => void)(locations[index]!, ...values)
463
+ }
464
+ },
465
+ methods: {
466
+ set(...args: number[]) {
467
+ values = args
468
+ },
469
+ },
470
+ }
471
+ })
472
+
473
+ return Object.assign(Object.fromEntries(setters.map(setter => [setter.name, setter.methods])), {
474
+ applyToVertexArray() {
475
+ const restores = setters.map(({ location }) => {
476
+ const wasEnabled = gl.getVertexAttrib(location, gl.VERTEX_ATTRIB_ARRAY_ENABLED) as boolean
477
+ gl.disableVertexAttribArray(location)
478
+ return () => (wasEnabled ? gl.enableVertexAttribArray(location) : undefined)
479
+ })
480
+ return once(() => restores.forEach(restore => restore()))
481
+ },
482
+ applyToContext() {
483
+ setters.forEach(setter => setter.write())
484
+ },
485
+ bind() {
486
+ const restoreVertexArray = bindDefaultVertexArray(gl)
487
+ const restore = this.applyToVertexArray()
488
+ this.applyToContext()
489
+ return once(() => {
490
+ restore()
491
+ restoreVertexArray()
492
+ })
493
+ },
494
+ })
495
+ }
496
+
286
497
  export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
287
498
  gl: GL,
288
499
  program: WebGLProgram,
@@ -290,11 +501,12 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
290
501
  { signal }: ViewOptions = {},
291
502
  ): InterleavedAttributeView<T> {
292
503
  // Initialize interleaved attributes
293
- const interleavedAttributes = mapObject(schema, ({ layout, instanced }) => {
504
+ const interleavedAttributes = mapObject(schema, ({ layout }) => {
294
505
  // Increment number to keep track of offset
295
506
  let index = 0
296
507
 
297
508
  // Calculate layout information
509
+ const locations: number[] = []
298
510
  const handles = layout.map(layout => {
299
511
  const name = toID(layout.key)
300
512
 
@@ -304,6 +516,8 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
304
516
  throw new Error(`Attribute '${name}' not found`)
305
517
  }
306
518
 
519
+ locations.push(location)
520
+
307
521
  const size = kindToSize(layout.kind)
308
522
  const offset = index
309
523
  const resolvedFormat = layout.format ?? defaultFormat(layout.kind)
@@ -312,7 +526,18 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
312
526
  const normalized = layout.normalized ?? false
313
527
  index += size * FORMAT_BYTE_SIZE[resolvedFormat]
314
528
 
315
- return () => handleAttribute(gl, location, size, stride, offset, glType, isIntKind, normalized, instanced)
529
+ return (divisor: 0 | 1) =>
530
+ handleAttribute(
531
+ gl,
532
+ location,
533
+ size,
534
+ stride,
535
+ offset,
536
+ glType,
537
+ isIntKind,
538
+ normalized,
539
+ divisor === 1,
540
+ )
316
541
  })
317
542
 
318
543
  // Set stride to final index
@@ -321,64 +546,62 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
321
546
  // Create a buffer
322
547
  const buffer = assertedNotNullish(gl.createBuffer())
323
548
 
324
- // Create VAO to cache attribute state
325
- let vao: { unbind(): void; bind(): void; dispose(): void } | undefined = undefined
326
-
327
- // Get VAO-feature: extension if webgl1, gl if webgl2
328
- const feature = getVertexArrayObject(gl)
329
- if (feature) {
330
- const vertexArray = feature.createVertexArray()
331
- feature.bindVertexArray(vertexArray)
549
+ // This layout used to create a vertex array of its own, here, which is why
550
+ // the other participants in a draw had to be bound after it to be included
551
+ // — the array they landed in was whichever one happened to be current.
552
+ // Arrays are `vao()`'s job now; a layout is a layout and a buffer.
553
+ function applyToVertexArray(divisor: 0 | 1) {
554
+ const previousDivisors = locations.map(
555
+ location => [location, readDivisor(gl, location)] as const,
556
+ )
332
557
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
333
558
  for (const handle of handles) {
334
- handle()
559
+ handle(divisor)
335
560
  }
336
- feature.bindVertexArray(null)
337
- vao = {
338
- unbind() {
339
- feature.bindVertexArray(null)
340
- },
561
+ return once(() => {
562
+ const instancedArrays = getInstancedArrays(gl)
563
+ previousDivisors.forEach(([location, previous]) => {
564
+ instancedArrays?.vertexAttribDivisor(location, previous)
565
+ })
566
+ })
567
+ }
568
+
569
+ let constant: ReturnType<typeof constantSource> | undefined
570
+
571
+ /** One layout, one buffer, but a divisor per draw rather than per schema. */
572
+ function bufferSource(divisor: 0 | 1) {
573
+ const apply = () => applyToVertexArray(divisor)
574
+ return {
575
+ applyToVertexArray: apply,
341
576
  bind() {
342
- feature.bindVertexArray(vertexArray)
343
- },
344
- dispose() {
345
- feature.deleteVertexArray(vertexArray)
577
+ const restoreVertexArray = bindDefaultVertexArray(gl)
578
+ const restore = apply()
579
+ return once(() => {
580
+ restore()
581
+ restoreVertexArray()
582
+ })
346
583
  },
347
584
  }
348
585
  }
349
586
 
350
587
  return {
351
- bind() {
352
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
353
- if (vao) {
354
- vao.bind()
355
- } else {
356
- // Fallback: manual attribute setup
357
- for (const handle of handles) {
358
- handle()
359
- }
360
- }
361
- },
362
- unbind() {
363
- if (vao) {
364
- vao.unbind()
365
- }
588
+ buffer: Object.assign(bufferSource(0), {
589
+ /** The same buffer, stepped once per instance instead of once per vertex. */
590
+ perInstance: bufferSource(1),
591
+ set(value: Float32Array, usage: GLUsage = 'STATIC_DRAW') {
592
+ // Uploading is storage, not binding: ARRAY_BUFFER is not vertex-array
593
+ // state, so this touches no array, whichever one is bound.
594
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
595
+ gl.bufferData(gl.ARRAY_BUFFER, value, gl[usage])
596
+ },
597
+ }),
598
+ // Lazy: a layout key can be perfectly good in a buffer and impossible as
599
+ // a constant, and finding that out should wait until someone asks for one.
600
+ get constant() {
601
+ return (constant ??= constantSource(gl, layout, locations))
366
602
  },
367
603
  dispose() {
368
604
  gl.deleteBuffer(buffer)
369
- if (vao) {
370
- vao.dispose()
371
- }
372
- },
373
- set(value, usage = 'STATIC_DRAW') {
374
- if (vao) {
375
- vao.bind()
376
- }
377
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
378
- gl.bufferData(gl.ARRAY_BUFFER, value, gl[usage])
379
- if (vao) {
380
- vao.unbind()
381
- }
382
605
  },
383
606
  } satisfies InterleavedAttributeMethods
384
607
  })
@@ -404,9 +627,31 @@ export function bufferView<T extends BufferSchema>(
404
627
  // Initialize buffers
405
628
  const buffers = mapObject(schema, ({ target = 'ARRAY_BUFFER', usage = 'STATIC_DRAW' }) => {
406
629
  const buffer = assertedNotNullish(gl.createBuffer())
630
+
631
+ function applyToVertexArray() {
632
+ // Snapshot before the change, restore in the disposer. Restoring the
633
+ // previous buffer rather than null matters for ELEMENT_ARRAY_BUFFER,
634
+ // whose binding is recorded in the bound vertex array: clearing it
635
+ // would strip the index buffer from an array the caller may not own.
636
+ const previousBuffer = gl.getParameter(gl[`${target}_BINDING`]) as WebGLBuffer | null
637
+ gl.bindBuffer(gl[target], buffer)
638
+ return once(() => {
639
+ gl.bindBuffer(gl[target], previousBuffer)
640
+ })
641
+ }
642
+
407
643
  return {
644
+ applyToVertexArray,
408
645
  bind() {
409
- gl.bindBuffer(gl[target], buffer)
646
+ // ELEMENT_ARRAY_BUFFER's binding belongs to the bound vertex array, so
647
+ // like the attributes this has to say which array it means. ARRAY_BUFFER
648
+ // is context state and unaffected either way.
649
+ const restoreVertexArray = bindDefaultVertexArray(gl)
650
+ const restore = applyToVertexArray()
651
+ return once(() => {
652
+ restore()
653
+ restoreVertexArray()
654
+ })
410
655
  },
411
656
  dispose() {
412
657
  gl.deleteBuffer(buffer)