• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 16947889290

13 Aug 2025 07:57PM UTC coverage: 74.577% (+0.1%) from 74.429%
16947889290

push

github

web-flow
feat(core): Support struct and array uniforms (#2414)

2118 of 2755 branches covered (76.88%)

Branch coverage included in aggregate %.

114 of 136 new or added lines in 2 files covered. (83.82%)

1 existing line in 1 file now uncovered.

27504 of 36965 relevant lines covered (74.41%)

56.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.17
/modules/core/src/portable/uniform-buffer-layout.ts
1
// luma.gl
1✔
2
// SPDX-License-Identifier: MIT
1✔
3
// Copyright (c) vis.gl contributors
1✔
4

1✔
5
import type {PrimitiveDataType} from '../shadertypes/data-types/data-types';
1✔
6
import type {CompositeShaderType} from '../shadertypes/data-types/shader-types';
1✔
7
import {alignTo} from '../shadertypes/data-types/decode-data-types';
1✔
8
import {getVariableShaderTypeInfo} from '../shadertypes/data-types/decode-shader-types';
1✔
9

1✔
10
import type {UniformValue} from '../adapter/types/uniforms';
1✔
11
import {getScratchArrayBuffer} from '../utils/array-utils-flat';
1✔
12
import {log} from '../utils/log';
1✔
13

1✔
14
export type UniformValues = Record<string, UniformValue | UniformValueStruct | UniformValueArray>;
1✔
15
type UniformValueStruct = Record<string, UniformValue | UniformValueStruct2 | UniformValueArray>;
1✔
16
type UniformValueStruct2 = Record<string, UniformValue | UniformValueStruct3 | UniformValueArray>;
1✔
17
type UniformValueStruct3 = Record<string, UniformValue | UniformValueArray>;
1✔
18
type UniformValueArray = (UniformValue | UniformValueStruct)[];
1✔
19

1✔
20
/**
1✔
21
 * Smallest buffer size that can be used for uniform buffers.
1✔
22
 * TODO - does this depend on device?
1✔
23
 */
1✔
24
const minBufferSize: number = 1024;
1✔
25

1✔
26
/**
1✔
27
 * Std140 layout for uniform buffers
1✔
28
 * Supports manual listing of uniforms
1✔
29
 */
1✔
30
export class UniformBufferLayout {
1✔
31
  readonly layout: Record<string, {offset: number; size: number; type: PrimitiveDataType}> = {};
12✔
32

12✔
33
  /** number of bytes needed for buffer allocation */
12✔
34
  readonly byteLength: number;
12✔
35

12✔
36
  /** Create a new UniformBufferLayout given a map of attributes. */
12✔
37
  constructor(
12✔
38
    uniformTypes: Readonly<Record<string, CompositeShaderType>>,
12✔
39
    uniformSizes: Readonly<Record<string, number>> = {}
12✔
40
  ) {
12✔
41
    let size = 0;
12✔
42

12✔
43
    for (const [key, uniformType] of Object.entries(uniformTypes)) {
12✔
44
      size = this._addToLayout(key, uniformType, size, uniformSizes?.[key]);
193✔
45
    }
193✔
46

12✔
47
    size += (4 - (size % 4)) % 4;
12✔
48
    this.byteLength = Math.max(size * 4, minBufferSize);
12✔
49
  }
12✔
50

12✔
51
  /** Does this layout have a field with specified name */
12✔
52
  has(name: string) {
12✔
NEW
53
    return Boolean(this.layout[name]);
×
NEW
54
  }
×
55

12✔
56
  /** Get offset and size for a field with specified name */
12✔
57
  get(name: string): {offset: number; size: number} | undefined {
12✔
NEW
58
    const layout = this.layout[name];
×
NEW
59
    return layout;
×
UNCOV
60
  }
×
61

12✔
62
  /** Get the data for the complete buffer */
12✔
63
  getData(uniformValues: Readonly<UniformValues>): Uint8Array {
12✔
64
    const buffer = getScratchArrayBuffer(this.byteLength);
3✔
65
    const typedArrays = {
3✔
66
      i32: new Int32Array(buffer),
3✔
67
      u32: new Uint32Array(buffer),
3✔
68
      f32: new Float32Array(buffer),
3✔
69
      f16: new Uint16Array(buffer)
3✔
70
    };
3✔
71

3✔
72
    for (const [name, value] of Object.entries(uniformValues)) {
3✔
73
      this._writeCompositeValue(typedArrays, name, value);
4✔
74
    }
4✔
75

3✔
76
    return new Uint8Array(buffer, 0, this.byteLength);
3✔
77
  }
3✔
78

12✔
79
  // Recursively add a uniform to the layout
12✔
80
  private _addToLayout(
12✔
81
    name: string,
200✔
82
    type: CompositeShaderType,
200✔
83
    offset: number,
200✔
84
    count: number = 1
200✔
85
  ): number {
200✔
86
    if (typeof type === 'string') {
200✔
87
      // Primitive case
196✔
88
      const info = getVariableShaderTypeInfo(type);
196✔
89
      const sizeInSlots = info.components * count;
196✔
90
      const alignedOffset = alignTo(offset, info.components);
196✔
91
      this.layout[name] = {
196✔
92
        offset: alignedOffset,
196✔
93
        size: sizeInSlots,
196✔
94
        type: info.type
196✔
95
      };
196✔
96
      return alignedOffset + sizeInSlots;
196✔
97
    }
196✔
98

4✔
99
    if (Array.isArray(type)) {
200✔
100
      // Array of structs or primitives
1✔
101
      const elementType = type[0];
1✔
102
      // Use count if provided, otherwise default to 1
1✔
103
      const length = count > 1 ? count : type.length > 1 ? type[1] : 1;
1!
104
      let arrayOffset = alignTo(offset, 4); // std140: arrays aligned to 16 bytes
1✔
105

1✔
106
      for (let i = 0; i < length; i++) {
1✔
107
        arrayOffset = this._addToLayout(`${name}[${i}]`, elementType, arrayOffset);
1✔
108
      }
1✔
109
      return arrayOffset;
1✔
110
    }
1✔
111

3✔
112
    if (typeof type === 'object') {
3✔
113
      // Struct case
3✔
114
      let structOffset = alignTo(offset, 4); // std140: structs aligned to 16 bytes
3✔
115
      for (const [memberName, memberType] of Object.entries(type)) {
3✔
116
        structOffset = this._addToLayout(`${name}.${memberName}`, memberType, structOffset);
6✔
117
      }
6✔
118
      return structOffset;
3✔
119
    }
3!
120

×
NEW
121
    throw new Error(`Unsupported CompositeShaderType for ${name}`);
×
122
  }
200✔
123

12✔
124
  private _writeCompositeValue(
12✔
125
    typedArrays: Record<string, any>,
17✔
126
    baseName: string,
17✔
127
    value: any
17✔
128
  ): void {
17✔
129
    if (this.layout[baseName]) {
17✔
130
      // Primitive or flat vector/matrix
7✔
131
      this._writeToBuffer(typedArrays, baseName, value);
7✔
132
      return;
7✔
133
    }
7✔
134

10✔
135
    if (Array.isArray(value)) {
17✔
136
      // Array of values: write each index
2✔
137
      for (let i = 0; i < value.length; i++) {
2✔
138
        const element = value[i];
5✔
139
        const indexedName = `${baseName}[${i}]`;
5✔
140
        this._writeCompositeValue(typedArrays, indexedName, element);
5✔
141
      }
5✔
142
      return;
2✔
143
    }
2✔
144

8✔
145
    if (typeof value === 'object' && value !== null) {
17✔
146
      // Struct: write each member
4✔
147
      for (const [key, subValue] of Object.entries(value)) {
4✔
148
        const nestedName = `${baseName}.${key}`;
8✔
149
        this._writeCompositeValue(typedArrays, nestedName, subValue);
8✔
150
      }
8✔
151
      return;
4✔
152
    }
4✔
153

4✔
154
    log.warn(`Unsupported uniform value for ${baseName}:`, value)();
4✔
155
  }
17✔
156

12✔
157
  private _writeToBuffer(
12✔
158
    typedArrays: Record<string, any>,
7✔
159
    name: string,
7✔
160
    value: UniformValue
7✔
161
  ): void {
7✔
162
    const layout = this.layout[name];
7✔
163
    if (!layout) {
7!
NEW
164
      log.warn(`Uniform ${name} not found in layout`)();
×
NEW
165
      return;
×
NEW
166
    }
×
167

7✔
168
    const {type, size, offset} = layout;
7✔
169
    const array = typedArrays[type];
7✔
170

7✔
171
    if (size === 1) {
7✔
172
      array[offset] = Number(value);
4✔
173
    } else {
7✔
174
      array.set(value as number[], offset);
3✔
175
    }
3✔
176
  }
7✔
177
}
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc