BabylonJS/Babylon.js · error
_writeStreamingSplats: range [${globalOffset}, ${globalOffse
Error message
_writeStreamingSplats: range [${globalOffset}, ${globalOffset + count}) is outside the atlas bounds [0, ${capacity}) What it means
_writeStreamingSplats validates the requested global [globalOffset, globalOffset+count) range against the splat atlas capacity (this._splatPositions.length / 4) and throws when it is non-integral, negative, or exceeds bounds. This protects other parts' texels from being overwritten and avoids huge transient allocations.
Source
Thrown at packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.ts:3168
* (guaranteed after `reserveStreamingPart`). Bounds of the written centers are accumulated into `min`/`max`
* when provided (so the caller can grow the owning part's bounding info).
* @param globalOffset first atlas splat index to write
* @param count number of splats to write
* @param splatsData raw `.splat` bytes for `count` splats (stride 32)
* @param min optional running min accumulator for the written centers
* @param max optional running max accumulator for the written centers
*/
protected _writeStreamingSplats(globalOffset: number, count: number, splatsData: ArrayBuffer | ArrayBufferView, min?: Vector3, max?: Vector3): void {
if (!this._splatPositions) {
return;
}
// Validate the range against the atlas and the input length (independent of GPU state): an out-of-range
// offset/count would write over another part's texels or allocate a huge transient (the CPU arrays are
// atlas-indexed up to `end`); a short input would read past its end.
const capacity = this._splatPositions.length / 4;
const uBuffer = GaussianSplattingMeshBase._GetSplatDataBytes(splatsData);
if (!Number.isInteger(globalOffset) || !Number.isInteger(count) || globalOffset < 0 || count < 0 || globalOffset + count > capacity) {
throw new Error(`_writeStreamingSplats: range [${globalOffset}, ${globalOffset + count}) is outside the atlas bounds [0, ${capacity})`);
}
if (uBuffer.length < count * 32) {
throw new Error(`_writeStreamingSplats: splatsData has ${uBuffer.length} bytes, need ${count * 32} for ${count} splats (stride 32)`);
}
if (count === 0 || !this._covariancesATexture) {
return;
}
const textureSize = this._getTextureSize(this._vertexCount);
const width = textureSize.x;
const covBSItemSize = this._useRGBACovariants ? 4 : 2;
const end = globalOffset + count;
const fBuffer = GaussianSplattingMeshBase._GetSplatDataFloats(splatsData);
// Transients are sized to `count`, not `end` (globalOffset + count): sizing by atlas position would allocate
// tens of MB for a small write near a large atlas's tail. dstIndex still addresses the atlas _splatPositions.
const covA = new Uint16Array(count * 4);
const covB = new Uint16Array(count * covBSItemSize);View on GitHub (pinned to 0592b347b8)
Solutions
- Clamp/validate globalOffset + count <= this._splatPositions.length / 4 before writing
- Re-reserve the atlas to the needed capacity before streaming in more splats
- Ensure offsets are integers derived from splat counts, not byte offsets
Example fix
// before
base._writeStreamingSplats(byteOffset / 4, count, data);
// after
const capacity = base._splatPositions.length / 4;
const splatOffset = byteOffset / 32;
if (splatOffset + count > capacity) throw new RangeError('exceeds atlas');
base._writeStreamingSplats(splatOffset, count, data); Defensive patterns
Strategy: validation
Validate before calling
const capacity = mesh._splatPositions.length / 4;
if (!Number.isInteger(offset) || !Number.isInteger(count) || offset < 0 || count < 0 || offset + count > capacity) {
throw new RangeError('write range outside atlas');
} Type guard
const inAtlas = (o: number, c: number, capacity: number) => Number.isInteger(o) && Number.isInteger(c) && o >= 0 && c >= 0 && o + c <= capacity;
Try / catch
try {
base._writeStreamingSplats(offset, count, data);
} catch (e) {
if (String(e).includes('outside the atlas bounds')) { reReserve(capacityNeeded); retry(); }
else throw e;
} Prevention
- Track atlas capacity alongside streaming offsets
- Convert byte offsets to splat indices consistently (divide by stride, not 4)
- Split oversized writes across reservations
When it happens
Trigger: Calling _writeStreamingSplats (or a streaming write API) with an atlas-level offset beyond the mesh's current splat capacity, or with a count that runs past the end.
Common situations: Using a global splat index from a manifest that no longer matches the rebuilt atlas size; reserving less capacity than the streaming source contains.
Related errors
- reserveStreamingPart: shTextureCount ${shTextureCount} does
- ${method}: local range [${offset}, ${offset + count}) is out
- GaussianSplattingDebugger.addMesh: mesh must have a Gaussian
- To call addPart()/addParts(), each source mesh must be fully
- To call addPart()/addParts(), each source mesh must not be a
AI-assisted analysis of BabylonJS/Babylon.js@0592b347b8 (2026-08-30).
Data as JSON: /api/errors/9699e14af7648d8a.
Report an issue: GitHub.