API

Module: Resource Pool

Package path: packages/babylon-lite/src/resource/

Purpose

Provides GPU resource lifecycle management: reference-counted texture ownership, deduplicated sampler creation, and focused GPU buffer creation utilities. Side-effect-free facades preserve the existing APIs while separate implementation modules let bundling retain only the operation groups actually used.

Public API Surface

Texture Ref Counting

/** Increment ref count on a Texture2D. First acquire sets count to 1. */
export function acquireTexture(tex: Texture2D): void;
/** Decrement ref count. Calls tex.texture.destroy() when count reaches 0.
* Returns true if the texture was destroyed. */
export function releaseTexture(tex: Texture2D): boolean;
/** Increment ref count on a raw GPUTexture (for env textures). */
export function acquireGPUTexture(tex: GPUTexture): void;
/** Decrement ref count on a raw GPUTexture. Destroys at 0.
* Returns true if the texture was destroyed. */
export function releaseGPUTexture(tex: GPUTexture): boolean;

Internal Texture State and Capture Seam

/** Install the opaque release notification used by opt-in resource capture. */
export function _setTextureReleaseHook(hook: (tex: Texture2D) => void): void;
/** True only when the current GPUTexture has a retained zero-count entry. */
export function _isTextureReleased(tex: Texture2D): boolean;
/** Return the current GPUTexture owner count, or 0 when no entry exists. */
export function _textureOwners(tex: Texture2D): number;

Sampler Deduplication

/** Get or create a deduplicated sampler. Same config → same GPUSampler.
* Default: all nearest, clamp-to-edge, anisotropy 1. */
export function getOrCreateSampler(engine: EngineContext, desc?: GPUSamplerDescriptor): GPUSampler;
/** Clear sampler cache for a device. */
export function clearSamplerCache(engine: EngineContext): void;

Internal Architecture

Module Boundaries

  • gpu-pool.ts is a side-effect-free re-export facade. Existing imports and the package root API remain unchanged.
  • texture-references.ts is the side-effect-free facade for Texture2D ownership operations.
  • texture-acquire.ts owns facade acquisition and captures texture.texture once before reading or updating its count.
  • texture-release.ts forwards the captured allocation and its facade to shared release bookkeeping.
  • gpu-texture-references.ts is the compatibility facade for the separate raw acquisition and release modules.
  • gpu-texture-acquire.ts owns the shared increment operation; facade acquisition forwards one captured allocation.
  • gpu-texture-release.ts forwards raw releases without facade metadata.
  • texture-allocation-release.ts owns the shared decrement/destroy/retained-zero operation and optional facade notification. A hook may retarget a facade without redirecting the captured allocation's retained-zero update.
  • texture-owner-state.ts owns the recovery-facing released/owner queries.
  • texture-reference-store.ts supplies the one shared, lazily allocated texture-count WeakMap.
  • texture-sampler-pool.ts owns the shared device-local cache and typed normal texture-sampling identity.
  • sampler-pool.ts is the general public boundary. It adds comparison and LOD identity before using the same cache, so ordinary texture loaders need not retain those unused features.

There are no module-initialization allocations. The texture-count and sampler-cache WeakMap instances, and each device's sampler Map, are created only on first use.

Buffer Operation Boundaries

  • gpu-buffers.ts is a side-effect-free facade preserving the existing buffer utility exports.
  • buffer-alignment.ts owns the generic align(n, to) operation.
  • empty-uniform-buffer.ts owns aligned UNIFORM | COPY_DST allocation without an initial upload.
  • uniform-buffer.ts composes empty uniform allocation with the initial queue upload and destroys the unpublished buffer if that upload fails.
  • mapped-buffer.ts owns mapped-at-creation vertex/index/storage uploads, including minimum-size and four-byte alignment.

These boundaries keep sprite and other focused consumers from retaining unrelated buffer upload strategies through the compatibility facade.

Texture Reference Counting

Uses WeakMap<GPUTexture, number> for ref counts:

  • _textureReferences: Maps GPUTexture → reference count (number)
  • acquireTexture(tex) / acquireGPUTexture(tex): Increments count (defaults to 0 if not present, so first acquire → 1)
  • releaseTexture(tex) / releaseGPUTexture(tex): Decrements count (defaults to 1 if not present, so first release → 0 → destroy)
  • At count 0: calls tex.texture.destroy() (Texture2D) or tex.destroy() (raw GPUTexture), then retains a zero-count WeakMap entry
  • releaseTexture ordering is exact: destroy the GPU texture, notify the optional facade release hook, then write count zero
  • releaseGPUTexture never calls the facade hook; it destroys, then writes count zero
  • Returns true if destroyed, false if still referenced

WeakMap rationale: No memory leaks — if the GPUTexture object itself is GC'd (impossible while alive), the entry is automatically cleaned up. More importantly, WeakMap avoids needing explicit cleanup of the tracking map.

The retained zero distinguishes a released allocation from a texture that never had an owner. Both report _textureOwners(tex) === 0, but only the released allocation reports _isTextureReleased(tex) === true.

Two API variants:

  • acquireTexture / releaseTexture: Takes Texture2D (the public API type) and snapshots .texture at function entry. A release hook may replace the facade's allocation, but destruction and the retained-zero write still target the captured allocation.
  • acquireGPUTexture / releaseGPUTexture: Takes raw GPUTexture directly (used internally for environment cubemaps, BRDF LUTs, etc.)

Sampler Deduplication

Uses WeakMap<GPUDevice, Map<string, GPUSampler>> for per-device caching:

  • samplerCaches: Maps device → descriptor-key → sampler
  • A typed immutable defaults table covers normal texture-sampling parameters. Key generation visits that fixed table, not caller-supplied property order.
  • Base key format: ":minFilter:magFilter:mipmapFilter:addressModeU:addressModeV:addressModeW:maxAnisotropy". The public boundary prefixes "compare:lodMinClamp:lodMaxClamp" when any of those parameters differs from WebGPU defaults.
    • Example default-LOD key: ":linear:linear:nearest:clamp-to-edge:clamp-to-edge:clamp-to-edge:1"
    • Example comparison key: "less:0:32:nearest:nearest:nearest:clamp-to-edge:clamp-to-edge:clamp-to-edge:1"
    • Defaults applied: nearest for filters, clamp-to-edge for address modes, 1 for anisotropy, no comparison, LOD minimum 0, and LOD maximum 32
    • Labels do not affect sampling behavior and are excluded from identity. Comparison and LOD clamps do; a comparison sampler must never alias an ordinary sampler.
  • First call with a new key creates the sampler; subsequent calls return cached instance
  • getOrCreateSampler(engine, desc) reads engine._device, passes the original descriptor unchanged to device.createSampler, and caches the result under the computed key
  • clearSamplerCache(engine) removes all cached samplers for engine._device

glTF non-mipmap samplers (lodMaxClamp: 0) use this same cache; no separate allocation bypass is needed now that every sampling parameter participates in identity.

Ordinary texture loaders construct TextureSamplerDescriptor values, which forbid comparison and LOD overrides even when passed through a broader typed variable. They use the restricted two-argument cache entry. Only the general sampler boundary can pass a full descriptor together with its normalized extra key. Both boundaries share default-behavior sampler identities and device-local eviction; recovery and glTF custom samplers use the complete public boundary.

WeakMap<GPUDevice> ensures the cache is automatically invalidated when a device is lost/destroyed without explicit cleanup.

Sampler cache ownership is device-scoped, not scene- or material-group-scoped. GPUSampler objects are immutable and have no explicit destroy operation, and several scenes may share one engine/device cache. Standard/PBR group disposal therefore clears only its own pipeline cache and must not call clearSamplerCache(engine). clearSamplerCache remains an explicit operation for callers that intentionally want to evict the complete cache for one device; a later lookup recreates entries on demand.

Memory Layout

No buffers or GPU memory managed. This module only tracks ownership via JavaScript-side data structures:

_textureReferences: WeakMap<GPUTexture, number>
└── Key: GPUTexture instance
└── Value: integer ref count, including retained 0 after destruction
_samplerCache: WeakMap<GPUDevice, Map<string, GPUSampler>>
└── Key: GPUDevice instance
└── Value: Map from descriptor string key → GPUSampler

Pipeline Configuration

N/A — No GPU pipelines. This module manages texture lifecycle and sampler creation.

Shader Logic

N/A — No shaders.

State Machine / Lifecycle

Texture Lifecycle

Texture created (loadTexture2D, createSolidTexture2D, etc.)
│
▼
acquireTexture(tex) ──► refCount = 1
│
├── acquireTexture(tex) ──► refCount++
│
├── releaseTexture(tex) ──► refCount--
│ │
│ ├── refCount > 0: keep alive
│ │
│ └── refCount <= 0:
│ tex.texture.destroy()
│ optional facade release hook
│ retain refCount = 0
│ return true
│
└── (GPUTexture GC'd if all JS refs gone — WeakMap entry auto-cleaned)

Sampler Lifecycle

getOrCreateSampler(engine, desc)
│
├── Cache hit: return existing GPUSampler
│
└── Cache miss: engine._device.createSampler(desc), cache, return
clearSamplerCache(engine)
└── Explicitly delete all entries for engine._device
Scene/material-group disposal
└── Leave the shared device sampler cache intact

Babylon.js Equivalence Map

Babylon.jsBabylon Lite
ThinEngine._samplerCache
_samplerCache WeakMap + getOrCreateSampler()
Texture.dispose() + InternalTexture._references
acquireTexture() / releaseTexture() ref counting
BaseTexture.releaseInternalTexture()
releaseGPUTexture()

Dependencies

  • ../texture/texture-2d.js — type-only Texture2D dependency for facade ownership and queries
  • ../engine/engine.js — type-only EngineContext dependency for sampler operations
  • texture-reference-store.ts — shared lazy count store used by raw, facade, and query modules

Test Specification

  1. Acquire/release basic: Acquire once, release once → texture destroyed, returns true
  2. Multiple acquires: Acquire 3 times, release 2 times → not destroyed; release 3rd → destroyed
  3. Default release: Release without prior acquire → treats as count 1, destroys
  4. Sampler dedup: Same descriptor returns same GPUSampler instance
  5. Sampler different desc: Different descriptor returns different GPUSampler
  6. Sampler key: Verify key includes all 7 descriptor fields with defaults
  7. Clear sampler cache: Verify cache cleared; next call creates new sampler
  8. Device isolation: Two devices maintain separate sampler caches
  9. GPUTexture variant: Verify acquireGPUTexture/releaseGPUTexture work identically for raw textures
  10. Mixed ownership: Raw and facade owners share one count
  11. Release ordering: Facade release destroys, calls the hook while the old count is observable, then records zero
  12. Absent owner: Raw and facade release default to one implicit owner and destroy
  13. Descriptor passthrough: Sampler creation receives the original descriptor unchanged
  14. Release retargeting: A release hook may replace texture.texture; only the captured old allocation is destroyed and marked zero
  15. Acquire capture: Facade acquisition reads texture.texture once and increments only that allocation

File Manifest

FilePurpose
gpu-pool.ts
Side-effect-free facade preserving all existing exports
texture-reference-store.ts
Shared lazily allocated GPUTexture reference-count WeakMap
texture-references.ts
Side-effect-free facade for Texture2D ownership operations
texture-acquire.ts
Captured-allocation facade acquisition
texture-release.ts
Captured-allocation facade release
gpu-texture-references.ts
Side-effect-free raw ownership facade
gpu-texture-acquire.ts
Shared allocation reference increment
gpu-texture-release.ts
Raw allocation release without facade notification
texture-allocation-release.ts
Shared decrement, destruction, notification ordering, and retained zero
texture-owner-state.ts
Released-state and owner-count queries
sampler-pool.ts
General sampler boundary including comparison and LOD normalization
texture-sampler-pool.ts
Shared per-device cache and type-restricted ordinary texture sampler identity
gpu-buffers.ts
Side-effect-free facade for buffer creation utilities
buffer-alignment.ts
Generic alignment operation
empty-uniform-buffer.ts
Empty aligned uniform-buffer allocation
uniform-buffer.ts
Initialized uniform-buffer allocation and upload rollback
mapped-buffer.ts
Mapped-at-creation buffer upload