@bigmistqke/view.gl 0.1.19 → 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/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 */
@@ -287,15 +378,28 @@ export function attributeView<T extends AttributeSchema>(
287
378
  const glType = FORMAT_TO_GL_TYPE[resolvedFormat]
288
379
  const isIntKind = kind.startsWith('i') || kind.startsWith('u')
289
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
+
290
394
  return {
291
395
  buffer,
396
+ applyToVertexArray,
292
397
  bind() {
293
- // Snapshot before the change, restore in the disposer
294
- const previousDivisor = readDivisor(gl, location)
295
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
296
- handleAttribute(gl, location, size, 0, 0, glType, isIntKind, normalized, instanced)
398
+ const restoreVertexArray = bindDefaultVertexArray(gl)
399
+ const restore = applyToVertexArray()
297
400
  return once(() => {
298
- getInstancedArrays(gl)?.vertexAttribDivisor(location, previousDivisor)
401
+ restore()
402
+ restoreVertexArray()
299
403
  })
300
404
  },
301
405
  dispose() {
@@ -323,6 +427,73 @@ export function attributeView<T extends AttributeSchema>(
323
427
  /* */
324
428
  /**********************************************************************************/
325
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
+
326
497
  export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
327
498
  gl: GL,
328
499
  program: WebGLProgram,
@@ -330,7 +501,7 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
330
501
  { signal }: ViewOptions = {},
331
502
  ): InterleavedAttributeView<T> {
332
503
  // Initialize interleaved attributes
333
- const interleavedAttributes = mapObject(schema, ({ layout, instanced }) => {
504
+ const interleavedAttributes = mapObject(schema, ({ layout }) => {
334
505
  // Increment number to keep track of offset
335
506
  let index = 0
336
507
 
@@ -355,7 +526,7 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
355
526
  const normalized = layout.normalized ?? false
356
527
  index += size * FORMAT_BYTE_SIZE[resolvedFormat]
357
528
 
358
- return () =>
529
+ return (divisor: 0 | 1) =>
359
530
  handleAttribute(
360
531
  gl,
361
532
  location,
@@ -365,7 +536,7 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
365
536
  glType,
366
537
  isIntKind,
367
538
  normalized,
368
- instanced,
539
+ divisor === 1,
369
540
  )
370
541
  })
371
542
 
@@ -375,90 +546,62 @@ export function interleavedAttributeView<T extends InterleavedAttributeSchema>(
375
546
  // Create a buffer
376
547
  const buffer = assertedNotNullish(gl.createBuffer())
377
548
 
378
- // Create VAO to cache attribute state
379
- let vao: { unbind(): void; bind(): void; dispose(): void } | undefined = undefined
380
-
381
- // Get VAO-feature: extension if webgl1, gl if webgl2
382
- const feature = getVertexArrayObject(gl)
383
- if (feature) {
384
- const vertexArray = feature.createVertexArray()
385
- 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
+ )
386
557
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
387
558
  for (const handle of handles) {
388
- handle()
389
- }
390
- feature.bindVertexArray(null)
391
- vao = {
392
- unbind() {
393
- feature.bindVertexArray(null)
394
- },
395
- bind() {
396
- feature.bindVertexArray(vertexArray)
397
- },
398
- dispose() {
399
- feature.deleteVertexArray(vertexArray)
400
- },
559
+ handle(divisor)
401
560
  }
561
+ return once(() => {
562
+ const instancedArrays = getInstancedArrays(gl)
563
+ previousDivisors.forEach(([location, previous]) => {
564
+ instancedArrays?.vertexAttribDivisor(location, previous)
565
+ })
566
+ })
402
567
  }
403
568
 
404
- // A selected vertex array stays selected for every later draw, which would
405
- // then write its own attributes into this one
406
- const unbind = () => {
407
- if (vao) {
408
- vao.unbind()
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,
576
+ bind() {
577
+ const restoreVertexArray = bindDefaultVertexArray(gl)
578
+ const restore = apply()
579
+ return once(() => {
580
+ restore()
581
+ restoreVertexArray()
582
+ })
583
+ },
409
584
  }
410
585
  }
411
586
 
412
587
  return {
413
- bind() {
414
- // Snapshot before the change, restore in the disposer
415
- const previousVertexArray = feature
416
- ? (gl.getParameter(VERTEX_ARRAY_BINDING) as WebGLVertexArrayObject | null)
417
- : null
418
- const previousDivisors = vao
419
- ? []
420
- : locations.map(location => [location, readDivisor(gl, location)] as const)
421
-
422
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
423
- if (vao) {
424
- vao.bind()
425
- } else {
426
- // Fallback: manual attribute setup
427
- for (const handle of handles) {
428
- handle()
429
- }
430
- }
431
-
432
- return once(() => {
433
- if (feature) {
434
- feature.bindVertexArray(previousVertexArray)
435
- }
436
- // Without a vertex array the manual setup wrote divisors straight to
437
- // the locations, so they are ours to put back
438
- if (!vao) {
439
- const instancedArrays = getInstancedArrays(gl)
440
- previousDivisors.forEach(([location, divisor]) => {
441
- instancedArrays?.vertexAttribDivisor(location, divisor)
442
- })
443
- }
444
- })
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))
445
602
  },
446
- unbind,
447
603
  dispose() {
448
604
  gl.deleteBuffer(buffer)
449
- if (vao) {
450
- vao.dispose()
451
- }
452
- },
453
- set(value, usage = 'STATIC_DRAW') {
454
- if (vao) {
455
- vao.bind()
456
- }
457
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
458
- gl.bufferData(gl.ARRAY_BUFFER, value, gl[usage])
459
- if (vao) {
460
- vao.unbind()
461
- }
462
605
  },
463
606
  } satisfies InterleavedAttributeMethods
464
607
  })
@@ -485,16 +628,29 @@ export function bufferView<T extends BufferSchema>(
485
628
  const buffers = mapObject(schema, ({ target = 'ARRAY_BUFFER', usage = 'STATIC_DRAW' }) => {
486
629
  const buffer = assertedNotNullish(gl.createBuffer())
487
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
+
488
643
  return {
644
+ applyToVertexArray,
489
645
  bind() {
490
- // Snapshot before the change, restore in the disposer. Restoring the
491
- // previous buffer rather than null matters for ELEMENT_ARRAY_BUFFER,
492
- // whose binding is recorded in the bound vertex array: clearing it
493
- // would strip the index buffer from an array the caller may not own.
494
- const previousBuffer = gl.getParameter(gl[`${target}_BINDING`]) as WebGLBuffer | null
495
- 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()
496
651
  return once(() => {
497
- gl.bindBuffer(gl[target], previousBuffer)
652
+ restore()
653
+ restoreVertexArray()
498
654
  })
499
655
  },
500
656
  dispose() {
package/src/types.ts CHANGED
@@ -322,9 +322,47 @@ type FormatToArray = {
322
322
  uint8: Uint8Array
323
323
  }
324
324
 
325
- export interface AttributeMethods<T extends AttributeDefinition = AttributeDefinition> {
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 {
326
357
  buffer: WebGLBuffer
327
- /** Binds the attribute, returning a disposer that restores the divisor */
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
+ */
328
366
  bind(): () => void
329
367
  set(data: FormatToArray[ResolvedFormat<T>], usage?: GLUsage): { bind(): () => void }
330
368
  dispose(): void
@@ -354,13 +392,55 @@ export interface InterleavedAttributeDefinition {
354
392
 
355
393
  export type InterleavedAttributeSchema = Record<string | symbol, InterleavedAttributeDefinition>
356
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
+
357
432
  export interface InterleavedAttributeMethods<
358
433
  T extends InterleavedAttributeLayout[] = InterleavedAttributeLayout[],
359
434
  > {
435
+ buffer: BufferSource
436
+ constant: ConstantSource<T>
437
+ dispose(): void
438
+ }
439
+
440
+ export interface VertexArrayMethods {
360
441
  /** Binds the vertex array, returning a disposer that restores the previous one */
361
442
  bind(): () => void
362
443
  unbind(): void
363
- set(data: Float32Array, usage?: GLUsage): void
364
444
  dispose(): void
365
445
  }
366
446
 
@@ -381,7 +461,7 @@ export interface BufferDefinition {
381
461
 
382
462
  export type BufferSchema = Record<string | symbol, BufferDefinition>
383
463
 
384
- export interface BufferMethods {
464
+ export interface BufferMethods extends VertexArrayParticipant {
385
465
  set(data: Float32Array | Uint16Array | Uint32Array): void
386
466
  /** Binds the buffer, returning a disposer that restores the previous binding */
387
467
  bind(): () => void
@@ -563,6 +643,16 @@ export type View<T extends ViewSchema = ViewSchema> = {
563
643
  : {}
564
644
  buffers: T['buffers'] extends BufferSchema ? Prettify<BufferView<T['buffers']>> : {}
565
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
566
656
  }
567
657
 
568
658
  /**********************************************************************************/