CesiumGS / CesiumGS/cesium

BillboardTexture.loadImage throws `TypeError: Cannot read properties of undefined (reading 'width')` when atlas rectangle is unset after await

Open Beginner friendly
#13,486 1 comment 0 reactions 0 assignees View on GitHub
needs triage type - bug
Dominant language
JavaScript
Stars
15.7k
Forks
3.9k
Avg merge
4d 6h
Merged PRs (30d)
34

Description

### What happened?

Cesium 1.141 (and current `main`) has a race in `BillboardTexture.prototype.loadImage`. After awaiting `TextureAtlas.addImage`, it reads `atlas.rectangles[index].width` with no guard. When the rectangle slot is still unset (e.g. the queue was resized and that slot got orphaned), the read throws and escapes as an unhandled promise rejection:

```
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'width')
at BillboardTexture.loadImage (cesium.js)
at new Billboard (cesium.js)
at BillboardCollection.add (cesium.js)
at FeatureConverter.csAddBillboard (olcs/FeatureConverter)
...
```

File: `packages/engine/Source/Scene/BillboardTexture.js`, in `BillboardTexture.prototype.loadImage`, the tail of the function (after `await atlas.addImage(...)`):

```js
billboardTexture._index = index;
billboardTexture._loadState = BillboardLoadState.LOADED;

const rectangle = atlas.rectangles[index]; // <-- can be undefined
billboardTexture._width = rectangle.width; // <-- TypeError thrown here
billboardTexture._height = rectangle.height;

if (this._id !== id) {
return;
}

this._index = index;
this._loadState = BillboardLoadState.LOADED;
this._width = rectangle.width;
this._height = rectangle.height;

this.dirty = true;
```

The existing method already has a `FAILED` branch for `!defined(index) || index === -1`; we just need to extend it to cover `!defined(rectangle)`.

### Reproduction steps

1. `addImage` increments `_nextIndex` and queues an `AddImageRequest`.
2. Next frame, `_processImageQueue` runs. The first `texturePacker.pack(index, image)` returns `undefined` (image won't fit). The function calls `_resize(context, i)` and `break`s.
3. After `_resize` succeeds (`this._rectangles = newRectangles`), the function falls through to the second loop that calls `_copyImageToTexture(queue[i])` for **every** queue entry.
4. `_copyImageToTexture` resolves the queued promise with `index`.
5. The `await` in `BillboardTexture.loadImage` resumes; reads `atlas.rectangles[index]`. In some interleavings this slot is still empty (typically when a parallel atlas-recreation or a destroyed/recreated texture invalidates the index between resize and copy). Crash.

### Sandcastle example

_No response_

### Environment

Browser: Google Chrome 147.0.7727.138 (Official Build) (64-bit)
CesiumJS Version: 1.141.0
Operating System: Windows 11

### AI acknowledgment

- [x] I used AI to generate this issue report.
- [x] (If the above is checked) I have reviewed the AI-generated content before submitting.

Contributor guide

Open the contributing guide

Research direction

Start in packages/engine/Source/Scene/BillboardTexture.js at BillboardTexture.prototype.loadImage, focusing on the code after await atlas.addImage(...). Trace the described TextureAtlas queue and resize path to confirm how atlas.rectangles[index] can be unset. Done means the unset-rectangle case follows the existing FAILED handling without an unhandled promise rejection.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
computer-graphics
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.