holodepth

Asset pipeline · Dağıtım & varlık ömrü

Varlık Yönetimi: Registry, Dispose ve Ömür Borusu

İnen her byte sahneye eklenince ömür borcu açar. Registry hangi GPU kaynağının kime ait olduğunu tutar; dispose bu borcu kapatır. Framework hook'ları komşu izlekte, burada vanilla Three sözleşmesi ele alınır.

R3F useLoader dispose davranışı Framework · R3F yükleme yaprağındadır. Bu yaprak vanilla Three için registry + LRU önizlemesidir — hangi temel disiplinin framework'ten bağımsız olarak var olması gerektiğini gösterir.

Bir GLB yüklendiğinde geometry, material ve texture GPU belleğinde yer tutar. Sahne değişse veya mesh kaldırılsa bile bu bellek kendiliğinden boşalmaz — JavaScript çöp toplayıcısı GPU kaynaklarını bilmez. Registry olmadan «hangi dispose çağrılmalı» sorusu cevapsız kalır ve her route değişimi küçük bir bellek borcu biriktirir; bu borç uzun oturumlarda ve mobil cihazlarda çökmeye kadar gidebilir.

Sayfayı bitirdiğinizde şu cümleyi kurabilmelisiniz: «Registry kim-ne-yükledi sorusunun cevabıdır; dispose traverse disiplinidir; paylaşılan kaynaklar referans sayımıyla korunur; LRU bellek tavanını P0'ı bozmadan yönetir.»

Bu sayfanın sınırı

Bu sayfa şunları anlatır:

  • Asset registry ve referans sayımı
  • geometry/material/texture dispose disiplini
  • LRU, bellek tavanı ve sahne geçişi

Bilinçli olarak dışarıda bırakılan konular:

Kısa ayrım: Bu sayfa «Three ömür borusu (vanilla)» · Framework «lifecycle hook üzerinden tetikleme».

Registry: kim, ne yükledi?

Map<assetId, Entry> yapısı: url, kök Object3D, tahmini byte boyutu, refCount ve son kullanım zamanı tutar. Sahne değişiminde «hangi dispose çağrılmalı» sorusu registry'den cevaplanır — sahne grafını elle dolaşıp «hangi mesh artık gereksiz» tahmin etmek yerine, kayıt tutulan tek kaynaktan okunur.

Registry, aynı zamanda tekrar yüklemeyi önler: aynı asset ikinci kez istendiğinde, halihazırda yüklüyse yeni bir ağ isteği açılmaz, mevcut kayıt referans sayısını artırarak paylaşılır.

Dispose borusu: traverse disiplini

traverse ile sahne grafındaki her düğümde geometry, material ve texture dispose() çağrılır. Paylaşılan texture'da doğrudan dispose etmek yerine referans sayısı sıfıra düşünce dispose edilmelidir — aksi halde aynı texture'ı kullanan başka bir mesh, siyah veya eksik doku ile kalır.

Material clone zinciri (örneğin her mesh için material.clone() yapılması) dispose işlemini çoğaltır; mümkünse materyal paylaşılmalı, klonlama yalnızca gerçekten farklı bir görünüm gerektiğinde yapılmalıdır.

LRU: bellek tavanı

Mobil cihazlarda ve uzun oturumlarda registry sınırı zamanla aşılır. LRU (least recently used) politikası: en uzun süredir görünmeyen P2 asset unload edilir, böylece bellek belirli bir tavanın altında tutulur. P0 asla LRU dışına atılmamalıdır — kritik yol asla otomatik temizlik kurbanı olmamalı.

LRU eşiği sabit bir sayı değil, cihaz kapasitesine göre ayarlanabilir bir parametre olarak düşünülmelidir; düşük belleğe sahip cihazlarda daha agresif, güçlü masaüstünde daha gevşek bir tavan makuldür.

Referans sayımı: paylaşılan texture ve material

İki farklı karakter aynı deri texture'ını paylaşabilir; bu durumda texture'ın sahibi tek bir mesh değildir. Referans sayımı, «kaç farklı kullanıcı bu kaynağı tutuyor» bilgisini taşır: her yeni kullanım refCount'u artırır, her unload azaltır; sıfıra ulaşınca gerçek dispose tetiklenir.

Referans sayımı yanlış uygulanırsa iki tipik hata oluşur: sayaç hiç azalmaz (bellek sızıntısı) veya erken sıfırlanır (hâlâ kullanılan bir kaynak dispose edilir, görsel bozulma). Bu yüzden increment/decrement her zaman eşleşen bir çift olarak (yükleme/kaldırma) yazılmalıdır.

Sahne geçişleri: registry ile route değişimi

Bir route'tan diğerine geçişte, önceki sahnenin P0/P1 asset'leri genelde artık gerekli değildir; ama hepsini anında dispose etmek, kullanıcı geri döndüğünde yeniden indirmeyi zorunlu kılar. Registry'nin LRU politikası burada devreye girer: hemen dispose etmek yerine «düşük öncelikli hâle getir, alan gerekirse temizle» stratejisi daha akıcı bir gezinme deneyimi sunar.

Route değişimi anında yalnızca yeni sahnenin P0'ını beklemek, eskiyi arka planda kademeli boşaltmak, ani bir "jank" hissi olmadan geçiş yapılmasını sağlar.

Bellek profili: DevTools ile izleme

Tarayıcı geliştirici araçlarındaki Memory/Performance panelleri, JS heap büyümesini gösterir ama GPU belleğini doğrudan göstermez — bu yüzden dispose'un etkisini «JS heap düştü» ile karıştırmamak gerekir. Chrome'un GPU sekmesi veya renderer.info.memory (Three'nin kendi sayaçları: geometries, textures) daha doğrudan bir sinyaldir.

Düzenli olarak renderer.info.memory.geometries ve .textures sayılarını route geçişlerinden önce ve sonra karşılaştırmak, dispose'un gerçekten çalışıp çalışmadığını doğrulamanın en basit yoludur.

Framework köprüsü: registry'yi kim çağırır?

Vanilla Three'de dispose çağrısı elle tetiklenir; framework entegrasyonlarında (R3F, Vue, Angular) bu çağrı genelde bir lifecycle hook'a (unmount, onBeforeUnmount, ngOnDestroy) bağlanır. Registry'nin kendisi framework'ten bağımsız kalmalı; framework katmanı sadece «şimdi çağır» sinyalini verir.

Bu ayrım, aynı registry mantığının React, Vue veya vanilla bir projede yeniden yazılmadan kullanılabilmesini sağlar — framework değişse bile ömür borusu aynı kalır.

Yanlış ömür kalıpları

  • Dispose hiç çağrılmamış bir SPA (kademeli GPU sızıntısı)
  • Paylaşılan texture'ı erken dispose etmek (başka mesh'te bozulma)
  • Registry yok, sahne grafında «kayıp» mesh'ler birikir
  • Framework hook'u olduğunu sanıp vanilla kodda hiç dispose çağırmamak
  • refCount artırma/azaltma çiftinin simetrik olmaması
  • LRU'nun P0 asset'i de temizlemesine izin vermek

«İndirdim» ile bitmez; registry olmadan her sahne geçişi görünmeyen bir bellek faturası biriktirir.

Simüle: registry + dispose

export class AssetRegistry {
  #entries = new Map()

  register(id, entry) {
    const existing = this.#entries.get(id)
    if (existing) {
      existing.refCount += 1
      existing.lastUsed = performance.now()
      return existing
    }
    const record = { ...entry, refCount: 1, lastUsed: performance.now() }
    this.#entries.set(id, record)
    return record
  }

  release(id) {
    const e = this.#entries.get(id)
    if (!e) return
    e.refCount -= 1
    if (e.refCount <= 0) this.unload(id)
  }

  get(id) {
    const e = this.#entries.get(id)
    if (e) e.lastUsed = performance.now()
    return e
  }

  unload(id) {
    const e = this.#entries.get(id)
    if (!e) return
    e.dispose?.()
    this.#entries.delete(id)
  }

  evictLru(maxEntries, isProtected = () => false) {
    if (this.#entries.size <= maxEntries) return
    const sorted = [...this.#entries.entries()]
      .filter(([id]) => !isProtected(id))
      .sort((a, b) => a[1].lastUsed - b[1].lastUsed)
    while (this.#entries.size > maxEntries && sorted.length) {
      const [id] = sorted.shift()
      this.unload(id)
    }
  }
}

Simüle · derin dispose ve texture tarama.

export function disposeObject3D(root) {
  root.traverse((obj) => {
    obj.geometry?.dispose()
    const mats = [].concat(obj.material || []).filter(Boolean)
    mats.forEach((m) => {
      for (const key of Object.keys(m)) {
        const value = m[key]
        if (value?.isTexture) value.dispose()
      }
      m.dispose()
    })
  })
}

Üç ömür profili

  • 01 · Ömür

    SPA az sahne

    Registry
    Opsiyonel, küçük ölçek
    LRU
    Gerekmeyebilir
    Risk
    İlk sızıntı fark edilmez
  • 02 · Ömür

    Çok sahneli oyun

    Registry
    Zorunlu
    LRU
    Route bazlı tetikli
    Risk
    refCount senkron hatası
  • 03 · Ömür

    Uzun oturum / mobil

    Registry
    Zorunlu + izlemeli
    LRU
    Agresif tavan
    Risk
    Bellek profili hiç kontrol edilmez

Holodepth perspektifleri

İndirmek, sahiplenmek değildir

Asset pipeline «indirmek» ile bitmez; registry olmadan her route değişimi GPU borcu biriktirir ve bu borç genelde ancak çökme anında fark edilir.

Referans sayımı simetrik bir sözleşmedir

Her register çağrısının bir release çağrısı olmalıdır; bu simetriyi bozan kod yolu, ya sızıntı ya da erken dispose üretir.

İzlek burada tamamlanır

Yolculuk → yükleme → versiyon → CDN → manifest → ömür borusu zinciri, bu sayfayla üretim ve dağıtım tarafında kapanır; framework tarafındaki lifecycle hook'ları (R3F, Vue, Angular) aynı registry mantığını kendi bağlamlarına taşır.

  1. Her register çağrısının eşleşen bir release çağrısı var mı?
  2. Dispose, traverse ile geometry/material/texture'a kadar iniyor mu?
  3. Paylaşılan kaynaklar refCount ile korunuyor mu?
  4. LRU tavanı var mı, P0'ı istisna tutuyor mu?
  5. Route geçişinde eski sahne kademeli mi boşaltılıyor, anında mı?
  6. renderer.info.memory ile dispose etkisi doğrulanıyor mu?
  7. Framework hook'u registry'yi çağırıyor mu, yoksa unutuluyor mu?

Holodepth içgörüsü

Asset pipeline izleği burada kapanır: yolculuk → yükleme → versiyon → CDN → manifest → ömür. Framework entegrasyon izlekleri bu sözleşmeyi kendi lifecycle hook'larına bağlar.

Sonraki kapı

İzlek tamamlandı. Framework entegrasyon yaprakları (R3F, Vue, Angular) bu registry sözleşmesini kendi lifecycle hook'larına bağlar; format ve modelleme izlekleri komşu haritada derinliğini sürdürür.