BabylonJS/Babylon.js · error

reserveStreamingPart: capacity ${capacity} would grow the at

Error message

reserveStreamingPart: capacity ${capacity} would grow the atlas to ${projectedSplats} splats (plus a sentinel), exceeding the maximum atlas capacity ${maxCapacity}

What it means

reserveStreamingPart projects the resulting atlas size (existing splats + new region + one sentinel texel row) against the device's maxTextureSize^2 limit and throws if it would exceed it. It rejects before mutating any state so a failed reservation leaves the compound mesh intact. The sentinel slot is required by _getTextureSize and is not optional.

Source

Thrown at packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.ts:2100

        // atlas rows via scissor without ever touching a preceding part that shares a row.
        //   - Front alignment: start the usable region on the next row boundary. This only consumes the
        //     preceding parts' already-allocated last-row tail padding, so it costs no extra memory.
        //   - Capacity alignment: pad the region up to a whole number of rows so its end is a row boundary too
        //     (needed when another part follows, e.g. multiple streaming parts).
        const atlasWidth = this._getTextureSize(1).x;
        const startOffset = this._vertexCount; // first atlas index the reserved part occupies
        const alignedBase = Math.ceil(startOffset / atlasWidth) * atlasWidth;
        const frontPad = alignedBase - startOffset; // invisible padding that fills the preceding row
        const alignedCapacity = Math.ceil(capacity / atlasWidth) * atlasWidth;
        const regionSplats = frontPad + alignedCapacity;

        // Validate the RESULTING atlas, not just `capacity`: existing splats, row-alignment padding, and the trailing
        // empty sentinel slot (`+ 1`, see _getTextureSize) all count against the device's square-texture limit.
        // _getTextureSize would silently clamp and drop the sentinel past the limit, so reject here — before any
        // state is mutated below, so a rejected reservation leaves the compound intact.
        const projectedSplats = startOffset + regionSplats;
        if (projectedSplats + 1 > maxCapacity) {
            throw new Error(
                `reserveStreamingPart: capacity ${capacity} would grow the atlas to ${projectedSplats} splats (plus a sentinel), exceeding the maximum atlas capacity ${maxCapacity}`
            );
        }

        // Back the atlas with a render-targetable MRT so a streaming engine can GPU-decode into the reserved
        // region. Must be set before _addPartsInternal so the (forced) full rebuild builds MRT attachments and
        // allocates covariance B as RGBA. Harmless/no-op if a streaming part was already reserved.
        this._useMrtAtlas = true;
        this._useRGBACovariants = true;

        // Higher-order SH: convert the SH textures to render-targetable integer MRTs so the stream can bake SH into
        // its region, and set the compound's SH degree so the draw path lights the decoded splats. Sized for the
        // MAX SH texture count across parts (a later higher-degree part grows it; lower-degree parts neutral-fill).
        if (shTextureCount > 0 && shDegree > 0) {
            this._useShMrtAtlas = true;
            this._shMrtAtlasTextureCount = Math.max(this._shMrtAtlasTextureCount, shTextureCount);
            // Streaming SH degree is tracked separately from _maxShDegree (which folds in static parts too) and is
            // recomputed from the surviving states on removal — so it shrinks correctly. The _addPartsInternal

View on GitHub (pinned to 0592b347b8)

Solutions

  1. Split the splat cloud across more GaussianSplattingMesh instances instead of one atlas
  2. Remove unused streaming parts (freeing their regions) before reserving new ones
  3. Check this.scene.getEngine().getCaps().maxTextureSize ahead of time and clamp total splat count to maxTextureSize^2 - 1 minus existing splats

Example fix

// before
mesh.reserveStreamingPart(2_000_000); // blows past atlas cap
// after
const cap = engine.getCaps().maxTextureSize ** 2;
if (existingSplats + 2_000_000 + 1 > cap) {
    // split into a second mesh or shrink the part
}
Defensive patterns

Strategy: validation

Validate before calling

const cap = engine.getCaps().maxTextureSize ** 2;
if (mesh.getTotalSplatCount?.() + regionSplats + 1 > cap) {
  throw new RangeError('reservation would exceed atlas capacity');
}

Type guard

null

Try / catch

try {
  mesh.reserveStreamingPart(capacity);
} catch (e) {
  if (String(e).includes('maximum atlas capacity')) {
    splitIntoSecondMesh(capacity); // fallback
  } else throw e;
}

Prevention

When it happens

Trigger: Reserving a streaming part whose startOffset + regionSplats + 1 exceeds maxTextureSize * maxTextureSize; e.g. adding parts to an already near-full atlas on a GPU with a small max texture size (4096 or 8192).

Common situations: Streaming very large splat clouds on mobile GPUs with low maxTextureSize caps; accumulating many streaming parts without ever removing or compacting them.

Related errors


AI-assisted analysis of BabylonJS/Babylon.js@0592b347b8 (2026-08-30). Data as JSON: /api/errors/31639cad02ad9148. Report an issue: GitHub.