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

excaliburjs / Excalibur / 13488918148

24 Feb 2025 01:18AM UTC coverage: 89.318% (-0.7%) from 90.052%
13488918148

push

github

web-flow
feat: Implement Uniform Buffers Object Support (#3360)

This PR implements UBO support in Excalibur

```typescript
var material = game.graphicsContext.createMaterial({
  name: 'light-material',
  fragmentSource: `#version 300 es
    precision mediump float;

    struct Light {
     vec2 pos;
     float radius;
     vec4 color;
    };

    layout(std140) uniform Lighting {
        Light lights[2];
    };

    in vec2 v_uv;
    out vec4 color;

    void main() {
        float distanceToLights = 1.0;
        vec4 finalColor = vec4(0.0, 0.0, 0.0, 1.0);
        for (int i = 0; i < 2; i++) {
            float dist = length(lights[i].pos - v_uv);
            dist = smoothstep(lights[i].radius-.2, lights[i].radius+.2, dist);
            finalColor += lights[i].color * (1.0 - dist);
        }

        color = finalColor;
        // premultiply alpha
        color.rgb = color.rgb * color.a;
    }`,
  uniforms: {
    // prettier-ignore
    Lighting: new Float32Array([
      0.5, 0.5, 0.1, 0.1, // light 1 pos
      0, 1, 0, 1, // light 1 color
      1, 1, 0.1, 0.1, // light2 pos
      0, 0, 1, 1 // light 2 color
    ])
  }
}) as ex.Material;

actor.graphics.material = material;
ex.coroutine(
  game,
  function* () {
    let time = 0;
    while (true) {
      const elapsed = yield;
      time += elapsed / 1000;
      const x1 = Math.cos(time);
      const y1 = Math.sin(time);
      const x2 = Math.cos(-time);
      const y2 = Math.sin(-time);

      // prettier-ignore
      material.uniforms.Lighting = new Float32Array([
        0.2 * x1 + 0.2, 0.2 * y1 + 0.2, 0.1, 0,
        0, 1, 0, 1,
        0.5 * x2 + 0.5, 0.5 * y2 + 0.5, 0.1, 0,
        0, 0, 1, 1
      ]);    }
  }.bind(this)
);

game.start(new ex.Loader([tex]));
```

Additionally there are some DX enhancements to working with uniforms
- You can now set uniform values directly on materials/shader instances and will be upload on next `.use()`
   `material.uniforms.Lighting = ...`

6332 of 8231 branches covered (76.93%)

79 of 209 new or added lines in 9 files covered. (37.8%)

5 existing lines in 1 file now uncovered.

13839 of 15494 relevant lines covered (89.32%)

25321.43 hits per line

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

96.88
/src/engine/GarbageCollector.ts
1
export interface GarbageCollectionOptions {
2
  /**
3
   * Textures that aren't drawn after a certain number of milliseconds are unloaded from the GPU
4
   * Default 60_000 ms
5
   */
6
  textureCollectInterval?: number; // default 60_000 ms
7
  // TODO future work to integrate the font and text configuration, refactor existing collection mechanism
8
  // /**
9
  //  * Font pre-renders that aren't drawn after a certain number of milliseconds are unloaded from the GPU
10
  //  * Default 60_000 ms
11
  //  */
12
  // fontCollectInterval: number; // default 60_000 ms
13
  // /**
14
  //  * Text measurements that aren't used after a certain number of milliseconds are unloaded from the GPU
15
  //  * Default 60_000 ms
16
  //  */
17
  // textMeasurementCollectInterval: number; // default 60_000 ms
18
}
19

20
export const DefaultGarbageCollectionOptions: GarbageCollectionOptions = {
1✔
21
  textureCollectInterval: 60_000
22
  // TODO future work to integrate the font and text configuration, refactor existing collection mechanism
23
  // fontCollectInterval: 60_000,
24
  // textMeasurementCollectInterval: 60_000,
25
};
26

27
export interface GarbageCollectorOptions {
28
  /**
29
   * Returns a timestamp in milliseconds representing now
30
   */
31
  getTimestamp: () => number;
32
}
33

34
export class GarbageCollector {
35
  private _collectHandle: number;
36
  private _running = false;
732✔
37
  private _collectionMap = new Map<any, [type: string, time: number]>();
732✔
38
  private _collectors = new Map<string, [(resource: any) => boolean, interval: number]>();
732✔
39

40
  constructor(public options: GarbageCollectorOptions) {}
732✔
41

42
  /**
43
   *
44
   * @param type Resource type
45
   * @param timeoutInterval If resource type exceeds interval in milliseconds collect() is called
46
   * @param collect Collection implementation, returns true if collected
47
   */
48
  registerCollector(type: string, timeoutInterval: number, collect: (resource: any) => boolean) {
49
    this._collectors.set(type, [collect, timeoutInterval]);
729✔
50
  }
51

52
  /**
53
   * Add a resource to be tracked for collection
54
   * @param type
55
   * @param resource
56
   */
57
  addCollectableResource(type: string, resource: any) {
58
    this._collectionMap.set(resource, [type, this.options.getTimestamp()]);
164✔
59
  }
60

61
  /**
62
   * Update the resource last used timestamp preventing collection
63
   * @param resource
64
   */
65
  touch(resource: any) {
66
    const collectionData = this._collectionMap.get(resource);
1,735✔
67
    if (collectionData) {
1,735!
68
      this._collectionMap.set(resource, [collectionData[0], this.options.getTimestamp()]);
1,735✔
69
    }
70
  }
71

72
  /**
73
   * Runs the collection loop to cleanup any stale resources given the registered collect handlers
74
   */
75
  public collectStaleResources = (deadline?: IdleDeadline) => {
732✔
76
    if (!this._running) {
723✔
77
      return;
9✔
78
    }
79
    for (const [type, [collector, timeoutInterval]] of this._collectors.entries()) {
714✔
80
      const now = this.options.getTimestamp();
704✔
81
      for (const [resource, [resourceType, time]] of this._collectionMap.entries()) {
704✔
82
        if (type !== resourceType || time + timeoutInterval >= now) {
102✔
83
          continue;
100✔
84
        }
85

86
        const collected = collector(resource);
2✔
87
        if (collected) {
2!
88
          this._collectionMap.delete(resource);
2✔
89
        }
90
      }
91
    }
92

93
    this._collectHandle = requestIdleCallback(this.collectStaleResources);
714✔
94
  };
95

96
  /**
97
   * Force collect all resources, useful for shutting down a game
98
   * or if you know that you will not use anything you've allocated before now
99
   */
100
  public forceCollectAll() {
101
    for (const [_, [collector]] of this._collectors.entries()) {
724✔
102
      for (const [resource] of this._collectionMap.entries()) {
723✔
103
        const collected = collector(resource);
162✔
104
        if (collected) {
162!
105
          this._collectionMap.delete(resource);
162✔
106
        }
107
      }
108
    }
109
  }
110

111
  running(): boolean {
NEW
112
    return this._running;
×
113
  }
114

115
  /**
116
   * Starts the garbage collection loop
117
   */
118
  start() {
119
    this._running = true;
570✔
120
    this.collectStaleResources();
570✔
121
  }
122

123
  /**
124
   * Stops the garbage collection loop
125
   */
126
  stop() {
127
    this._running = false;
573✔
128
    cancelIdleCallback(this._collectHandle);
573✔
129
  }
130
}
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