> ## Documentation Index
> Fetch the complete documentation index at: https://hyperscape-ai-mintlify-docs-update.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Model cache fixes

# Model Cache Fixes (February 2026)

## Overview

Two critical bugs in the IndexedDB processed model cache were fixed in February 2026. These bugs caused missing objects (altars, trees) and lost textures (white/wrong colors) after browser restarts. The fixes ensure reliable model caching without visual corruption.

## Bug #1: Missing Objects

### Symptoms

* Objects disappear after browser restart (altars, trees, buildings)
* Scene hierarchy incomplete
* Console errors about missing meshes
* Inconsistent object counts between sessions

### Root Cause

The `serializeNode()` function used `findIndex`-by-name to map hierarchy nodes to mesh data:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ❌ BROKEN: Name-based lookup
const meshIndex = meshes.findIndex((m) => m.name === node.name);
```

**Problem**: Models with duplicate mesh names (common: `""`, `"Cube"`, `"Cube"`) all resolved to the same index. During deserialization, `Three.js add()` auto-removes objects from their previous parent, so only the last reference survived.

**Example Failure:**

```
Model hierarchy:
  Root
    ├─ Mesh (name: "Cube")      // meshIndex = 0
    ├─ Mesh (name: "Cube")      // meshIndex = 0 (duplicate!)
    └─ Mesh (name: "Cube")      // meshIndex = 0 (duplicate!)

After deserialization:
  Root
    └─ Mesh (name: "Cube")      // Only last one survives
```

### Fix

Use object-identity map instead of name-based lookup:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ✅ FIXED: Identity-based lookup
const meshNodeToIndex = new Map<THREE.Object3D, number>();

scene.traverse((node) => {
  if (node instanceof THREE.Mesh || node instanceof THREE.SkinnedMesh) {
    meshNodeToIndex.set(node, meshes.length);
    meshes.push(serializeMesh(node));
  }
});

const hierarchy = serializeNode(scene, meshNodeToIndex);

function serializeNode(
  node: THREE.Object3D,
  meshNodeToIndex: Map<THREE.Object3D, number>
): SerializedNode {
  const meshIndex = meshNodeToIndex.get(node);  // Identity lookup
  return {
    name: node.name,
    type: node.type,
    meshIndex,
    children: node.children.map(child => serializeNode(child, meshNodeToIndex))
  };
}
```

**Result**: All objects preserved correctly, regardless of duplicate names.

## Bug #2: Lost Textures

### Symptoms

* White or grey materials after browser restart
* Textures not loading
* Correct geometry but wrong colors
* Trees appear grey instead of green

### Root Cause

Textures were serialized as ephemeral `blob:` URLs but never reloaded during deserialization:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ❌ BROKEN: Blob URLs don't persist
const mapSrc = material.map?.source?.data?.src;  // "blob:http://..."
if (mapSrc) props.mapUrl = mapSrc;

// On deserialization:
// Blob URL is invalid (blob was released), texture never loads
```

**Problem**: Blob URLs are ephemeral and only valid during the current page session. After restart, the blob is gone and the texture fails to load.

### Fix

Extract raw RGBA pixel data via canvas and restore as `THREE.DataTexture`:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ✅ FIXED: Extract raw pixels
private textureToPixelData(texture: THREE.Texture): SerializedTextureData | null {
  const image = texture.source?.data ?? texture.image;
  if (!image) return null;

  const w = image.naturalWidth || image.width || 0;
  const h = image.naturalHeight || image.height || 0;
  if (w === 0 || h === 0) return null;

  const canvas = document.createElement('canvas');
  canvas.width = w;
  canvas.height = h;
  const ctx = canvas.getContext('2d')!;
  ctx.drawImage(image, 0, 0);
  const imageData = ctx.getImageData(0, 0, w, h);
  
  return { 
    pixels: imageData.data.buffer,  // Raw RGBA bytes
    width: w, 
    height: h 
  };
}

// On deserialization:
const restoreTex = (td: SerializedTextureData, srgb: boolean): THREE.DataTexture => {
  const tex = new THREE.DataTexture(
    new Uint8ClampedArray(td.pixels),
    td.width,
    td.height,
    THREE.RGBAFormat
  );
  tex.colorSpace = srgb ? THREE.SRGBColorSpace : THREE.LinearSRGBColorSpace;
  tex.needsUpdate = true;
  return tex;
};

if (props.mapData) mat.map = restoreTex(props.mapData, true);
if (props.normalMapData) mat.normalMap = restoreTex(props.normalMapData, false);
```

**Result**: Textures persist correctly across browser restarts with no async loading race conditions.

## Bug #3: Grey Tree Materials (WebGPU)

### Symptoms

* Trees appear grey in WebGPU renderer
* Dissolve effect not working
* `instanceof MeshStandardMaterial` check fails

### Root Cause

The `createDissolveMaterial()` function used `instanceof MeshStandardMaterial` which fails for `MeshStandardNodeMaterial` in the WebGPU build:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ❌ BROKEN: instanceof check fails for NodeMaterial
if (source instanceof THREE.MeshStandardMaterial) {
  material.color.copy(source.color);
  // ...
}
```

**Problem**: ModelCache converts all materials to `MeshStandardNodeMaterial` for WebGPU compatibility, but they don't pass `instanceof MeshStandardMaterial` checks.

### Fix

Use duck-type property check instead of `instanceof`:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ✅ FIXED: Duck-type check
const src = source as THREE.MeshStandardMaterial & {
  map?: THREE.Texture | null;
  normalMap?: THREE.Texture | null;
  // ...
};

if (src.color && src.roughness !== undefined) {
  material.color.copy(src.color);
  material.roughness = src.roughness;
  material.metalness = src.metalness;
  // ...
}
```

**Result**: Dissolve materials work correctly in both WebGL and WebGPU renderers.

## Cache Version Bump

The `PROCESSED_CACHE_VERSION` was bumped from `2` to `3` to invalidate broken cache entries:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const PROCESSED_CACHE_VERSION = 3;  // Was: 2
```

**Effect**: All users automatically rebuild their cache with the fixed serialization on first load after update.

## Debugging Tools

### Disable Cache

Bypass the cache entirely for debugging:

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// In browser console
localStorage.setItem('disable-model-cache', 'true');
// Reload page
```

**Use Cases:**

* Verify cache is causing the issue
* Test model loading without cache
* Force fresh model processing

### Clear Cache

Delete the entire cache database:

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// In browser console
indexedDB.deleteDatabase('hyperscape-processed-models');
// Reload page - cache will rebuild
```

### Inspect Cache

View cached models:

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// In browser console
const req = indexedDB.open('hyperscape-processed-models', 3);
req.onsuccess = () => {
  const db = req.result;
  const tx = db.transaction('models', 'readonly');
  const store = tx.objectStore('models');
  const getAllReq = store.getAll();
  getAllReq.onsuccess = () => {
    console.log('Cached models:', getAllReq.result);
  };
};
```

### Verify Texture Data

Check if textures are properly serialized:

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// After cache rebuild, inspect a cached model
const req = indexedDB.open('hyperscape-processed-models', 3);
req.onsuccess = () => {
  const db = req.result;
  const tx = db.transaction('models', 'readonly');
  const getReq = tx.objectStore('models').get('your-model-url');
  getReq.onsuccess = () => {
    const cached = getReq.result;
    console.log('Meshes:', cached.meshes.length);
    cached.meshes.forEach((mesh, i) => {
      const mat = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
      console.log(`Mesh ${i}:`, {
        name: mesh.name,
        hasMapData: !!mat.mapData,
        hasNormalMapData: !!mat.normalMapData,
        mapSize: mat.mapData ? `${mat.mapData.width}x${mat.mapData.height}` : 'none'
      });
    });
  };
};
```

## Performance Impact

### Cache Size

**Before**: \~2-5 MB per model (blob URLs + metadata)
**After**: \~5-15 MB per model (raw RGBA pixels + metadata)

**Trade-off**: Larger cache size for reliability and instant texture availability.

### Load Time

**Before**:

* Cache hit: 50-100ms (blob URL loading + async texture decode)
* Cache miss: 500-2000ms (GLTF parse + texture load)

**After**:

* Cache hit: 20-50ms (synchronous DataTexture creation)
* Cache miss: 500-2000ms (unchanged)

**Improvement**: 50-80% faster cache hits, zero async loading race conditions.

## Testing

### Verify Fix

1. Load a model with duplicate mesh names (e.g., altar, tree)
2. Verify all objects are visible
3. Reload page (cache hit)
4. Verify all objects still visible
5. Check textures are correct colors

### Regression Test

```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Run model cache tests
cd packages/shared
bun test src/utils/rendering/__tests__/ModelCachePriority.test.ts
```

**Expected**: All tests pass, no missing objects or white textures.

## Rollback

If you encounter issues with the new cache:

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Disable cache and use direct loading
localStorage.setItem('disable-model-cache', 'true');
// Report issue with model URL and browser console logs
```

## Related Changes

**Files Modified:**

* `packages/shared/src/utils/rendering/ModelCache.ts` - Core cache implementation
* `packages/shared/src/systems/shared/world/GPUVegetation.ts` - Dissolve material fix

**Cache Version History:**

* Version 1: Original implementation (pre-2025)
* Version 2: Added collision data caching (2025)
* Version 3: Fixed missing objects and texture persistence (February 2026)

## Related Documentation

* [Arena Performance Optimizations](./arena-performance-optimizations.md) - Related rendering improvements
* [Terrain Height Cache Fix](./terrain-height-cache-fix.md) - Another cache-related fix
* [ClientLoader.ts](../packages/shared/src/systems/client/ClientLoader.ts) - Model loading system
