Luxar Viewer API Documentation - v2026.9.22
    Preparing search index...

    GPU buffer pool for reusing THREE.BufferGeometry objects.

    Manages separate pools for Points, Lines, and GSplats geometries, each with different attribute layouts and update patterns.

    Index
    • Parameters

      • maxPoolSize: number = 20
      • evictionFrames: number = 300
      • evictBatchSize: number = 5
      • getByteBudget: () => number = ...

      Returns GPUBufferPool

    — points-specific pool state and methods.

    — lines-specific pool state and methods.

    — gsplats-specific pool state and methods.

    activeBuffers: Map<string, PooledBuffer> = ...

    — shared with the per-type adapters.

    frameCount: number = 0

    — shared with the per-type adapters; increments via beginFrame().

    maxPoolSize: number
    evictionFrames: number
    evictBatchSize: number

    Maximum geometries the pool will dispose in a single evictUnused() call when not over the hard limit. Without this cap, a single eviction sweep can dispose dozens of buffers synchronously — each geometry.dispose() is 5–20 ms on slow GPUs, so a burst stutters visibly. Remaining evictable buffers are deferred to the next frame's eviction sweep. The mustEvict (pool-over-limit) path ignores this cap so the pool never grows unbounded.

    getByteBudget: () => number

    Live byte-budget getter (the single VRAM authority shared with LOD retention). The byte-eviction pass targets TOTAL resident bytes (active + pooled) against this value: pooled buffers are disposed (largest-first) only while active + pooled exceeds the budget, and retained for reuse otherwise. Read live (not snapshotted) so the WebGL-context-loss budget backoff applies immediately. Returns 0 to disable byte-budget eviction (count-only behavior).

    stats: {
        allocations: number;
        reuses: number;
        evictions: number;
        capacityGrowths: number;
        deferredEvictions: number;
        byteBudgetEvictions: number;
    } = ...

    — shared mutable stats; adapters bump fields here.

    Type Declaration

    • allocations: number
    • reuses: number
    • evictions: number
    • capacityGrowths: number
    • deferredEvictions: number

      Pooled buffers skipped this evictUnused call due to batch cap.

    • byteBudgetEvictions: number

      Evictions attributable to the BYTE-budget pass alone (a subset of evictions) — see PoolStats.byteBudgetEvictions for why the two must not be conflated.

    _lastAcquireRebuilt: boolean = false

    — written by adapters from acquire paths.

    largePoolWarningEmitted: boolean = false

    One-shot guard: have we already logged the >100MB pooled-buffer warning? Re-checked per evictUnused so the noise stays bounded.

    typeStats: {
        points: { allocations: number; reuses: number; evictions: number };
        lines: { allocations: number; reuses: number; evictions: number };
        gsplats: { allocations: number; reuses: number; evictions: number };
    } = ...
    • Advance the frame counter. Call once per frame before any acquire calls. This ensures eviction timing is based on rendered frames, not acquire calls.

      Returns void

    • Whether the most recent acquire*Geometry call returned a geometry whose GPU-visible buffers (lines' interleaved buffer, the points/gsplat element texture + aSortedIndex) differ from what was previously in use for that node — the returned geometry is a fresh allocation or a different pool candidate (growth is release + reacquire for all three types).

      Callers that hold a THREE.Mesh pointing at the previously-active geometry should call invalidateRenderObjectFor(mesh) (in data/scene-loader/commit/invalidate-render-object.ts) when this returns true. That forces Three's WebGPURenderer to discard the cached RenderObject.vertexBuffers set (a no-op on the classic WebGL backend, which has no such cache); without it, WebGPU binds the old GPU buffer next draw and validation fails with "Instance range … requires a larger buffer than the bound buffer size".

      The flag is overwritten on every acquire call, so consume it immediately after acquirePointsGeometry / acquireLinesGeometry / acquireGSplatsGeometry returns.

      Returns boolean

    • Acquire Points geometry from pool (capacity-aware; the fixed texel layout means any pooled points geometry fits any points node). See PointsBufferAdapter.acquireGeometry for implementation.

      Parameters

      • nodeId: string
      • pointCount: number

      Returns InstancedBufferGeometry

    • Update Points geometry attributes in-place (zero GPU allocations).

      Parameters

      • geometry: InstancedBufferGeometry
      • data: LoadedPointsData
      • count: number
      • Optionaloptions: { preserveOrdering?: boolean; repairFromCount?: number; fromInstance?: number }

        preserveOrdering: keep the geometry's existing aSortedIndex permutation instead of resetting it to identity (same-node same-count recommit — the commit path decides; see commit-points-geometry.ts). fromInstance: append fast path (Phase 4 Stage 2) — write & upload only the [fromInstance, count) suffix, preserving the prefix texels + permutation already on the GPU.

      Returns void

    • Acquire geometry for Lines (texture-backed per-segment storage — the fixed 6-texel layout means capacity is the only matching criterion; see lines-adapter.ts).

      Parameters

      • nodeId: string
      • segmentCount: number

      Returns InstancedBufferGeometry

    • Update Lines geometry in place.

      Parameters

      • geometry: InstancedBufferGeometry
      • data: ProcessedLinesData
      • count: number
      • Optionaloptions: { preserveOrdering?: boolean; repairFromCount?: number; fromInstance?: number }

        preserveOrdering: keep the existing aSortedIndex permutation instead of resetting to identity (same-count recommit of an already-sorted node — the commit path decides; see commit-lines-geometry.ts). fromInstance: append fast path (Phase 4 Stage 2) — write & upload only the [fromInstance, count) segment suffix, preserving the prefix already on the GPU.

      Returns void

    • Acquire geometry for GSplats (splat texture + aSortedIndex).

      Parameters

      • nodeId: string
      • splatCount: number

      Returns InstancedBufferGeometry

    • Update GSplats geometry in place.

      Parameters

      • geometry: InstancedBufferGeometry
      • data: PackedGSplatsData
      • count: number
      • truncationRadius: number = GSPLAT_DEFAULT_TRUNCATION_RADIUS

        Truncation radius in sigmas (defaults to GSPLAT_DEFAULT_TRUNCATION_RADIUS). Must match the material's truncationRadius for correct frustum culling.

      • Optionaloptions: { preserveOrdering?: boolean; repairFromCount?: number; fromInstance?: number }

        preserveOrdering: keep the geometry's existing aSortedIndex permutation instead of resetting it to identity (same-node same-count recommit — the commit path decides; see commit-gsplats-geometry.ts). fromInstance: append fast path (Phase 4 Stage 2) — write & upload only the [fromInstance, count) suffix, preserving the prefix texels while resetting the enlarged ordering to full identity until the commit-triggered sort lands.

      Returns void

    • Get size bucket for capacity-based pooling. Buckets: 1K, 5K, 10K, 50K, 100K, 500K, 1M. — called by adapters; public to satisfy PointsAdapterHost.

      Parameters

      • count: number

      Returns number

    • Evict unused geometries using LRU policy. Returns number of geometries evicted.

      Public for testing and manual pool management.

      Parameters

      • fromAcquire: boolean = false

      Returns number

    • Parameters

      • budget: number
      • fromAcquire: boolean = false

      Returns number

    • Total resident VRAM bytes (active + pooled), using real per-geometry capacities. The single source of truth for budget enforcement: the LOD registry queries it to decide when to demote cold levels, and the pool's own byte-eviction pass keeps it under the live budget. Cheaper than getStats (no per-type breakdown), so it is safe to call once per frame.

      Returns number

    • Register a one-time self-invalidation dispose listener on a freshly created pooled geometry. Called by the per-type adapters at fresh allocation — the single choke point every pooled geometry passes through exactly once (adopted/reused geometries were registered at their original creation).

      The scene-graph disposal helpers (scene-disposal.ts) are pure over a THREE.Scene with NO pool reference, so on a dataset switch / teardown they call geometry.dispose() directly on meshes that still hold pool-owned geometries. Without this guard the pool keeps the disposed geometry recorded in activeBuffers, and a later acquire*Geometry for the same nodeId hands it back as reusable — a use-after-dispose that also over-counts resident bytes.

      When dispose fires this:

      • flags the geometry userData.luxarInvalidated (the acquire fast paths treat a flagged active entry as a miss — defense-in-depth against an ordering where the entry is somehow still present);
      • drops any activeBuffers entry that still references THIS geometry. Matching by identity is inherently safe — a geometry appears in activeBuffers at most once, so a newer entry that replaced it (a grow moves the old geometry to the free list first) is never clobbered (the guard the issue asks for);
      • otherwise drops the geometry's free-bucket entry (a RELEASED buffer disposed out-of-band). Leaving it would keep the disposed geometry counted by getStats / getResidentBytes (phantom bytes that pressure the byte-budget pass into evicting live buffers), holding a maxPoolSize slot (which can flip the LRU sweep into aggressive mustEvict mode), and queued for a second dispose() by the evictors.

      Resident-byte / stats accounting is DERIVED from activeBuffers + free-bucket membership (see getStats / getResidentBytes), so removing the entry is the entire correction — there is no persistent counter to decrement. On the normal release/evict/dispose paths the entry (or its free-bucket slot) is already gone before dispose() runs, so this listener finds nothing and is a safe no-op there — it never double-counts eviction stats and never mutates a bucket array those paths are still iterating (they all commit removals BEFORE disposing). It never calls dispose() again (no re-entrancy) and removes itself on first fire (idempotent).

      — called by the per-type adapters via their host interface.

      Parameters

      • geometry: BufferGeometry

      Returns void

    • Remove the free-bucket entry referencing geometry, if any. A geometry appears in at most one bucket of one type pool, so the scan stops at the first identity match. No eviction counter moves: this is out-of-band disposal bookkeeping, not an eviction.

      Parameters

      • geometry: BufferGeometry

      Returns void