BillboardTexture.loadImage throws `TypeError: Cannot read properties of undefined (reading 'width')` when atlas rectangle is unset after await
- 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
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