AppleyAppley Docs
Docs/Optimization

Streaming Assets

Stream textures, models, and audio on demand to reduce memory and load times.

30 min read Advanced Updated 2026-07-01 v2.0 ~125 min to complete

Introduction

AAA games stream assets from disk/CDN as needed rather than loading everything at start. This guide explains how professional engines manage asset bundles, use reference counting, and prefetch — then builds an asset streaming system in Appley.

Advanced Guide 16 of 25

This is part of the Advanced Documentation collection. Category: Optimization. Estimated completion: 125 minutes. Difficulty: Advanced/Professional.

Screenshot

Streaming Assets
Streaming Assets

How Professional Games Do It

1

U

n

2

G

r

3

U

s

4

T

r

5

I

m

6

L

o

7

P

r

8

P

r

9

U

s

10

W

h

11

C

o

12

U

s

FluxScript Implementation

Here is how to build this system in Appley using FluxScript (Appley Scripting):

Streaming Assets — FluxScript
// Asset streaming system with reference counting and LRU
const MEMORY_BUDGET = 1024 * 1024 * 1024  // 1GB
loadedAssets = {}  // id -> { asset, refCount, lastAccess, size }
currentMemory = 0

// Request an asset (increments ref count)
func requestAsset(assetId) {
  if loadedAssets[assetId] != null {
    loadedAssets[assetId].refCount += 1
    loadedAssets[assetId].lastAccess = time.now()
    return loadedAssets[assetId].asset
  }

  // Not loaded — check memory budget
  assetSize = getAssetSize(assetId)
  while currentMemory + assetSize > MEMORY_BUDGET {
    evictLRU()
  }

  // Load async
  loadAssetAsync(assetId)
  return null  // Returns null while loading
}

// Release an asset (decrements ref count)
func releaseAsset(assetId) {
  if loadedAssets[assetId] == null return
  loadedAssets[assetId].refCount -= 1
  if loadedAssets[assetId].refCount <= 0 {
    // Unload if no references and not recently used
    // (Keep in memory briefly for potential reuse)
    loadedAssets[assetId].refCount = 0
  }
}

// Async asset loading
func loadAssetAsync(assetId) {
  storage.load("assets/" + assetId).then((data) => {
    asset = createAssetFromData(data)
    size = getAssetSize(assetId)
    loadedAssets[assetId] = {
      asset: asset,
      refCount: 1,
      lastAccess: time.now(),
      size: size
    }
    currentMemory += size
    // Notify objects waiting for this asset
    emit("assetLoaded", { id: assetId, asset: asset })
  })
}

// LRU eviction — unload least recently used
func evictLRU() {
  oldest = null
  oldestKey = null
  for key in loadedAssets {
    entry = loadedAssets[key]
    // Only evict assets with 0 references
    if entry.refCount <= 0 {
      if oldest == null || entry.lastAccess < oldest.lastAccess {
        oldest = entry
        oldestKey = key
      }
    }
  }

  if oldest != null {
    oldest.asset.unload()
    currentMemory -= oldest.size
    loadedAssets[oldestKey] = null
    print("Evicted asset: " + oldestKey + " (freed " + oldest.size + " bytes)")
  }
}

// Prefetch assets for upcoming area
func prefetchArea(areaId) {
  manifest = getAreaManifest(areaId)
  for assetId in manifest.assets {
    if loadedAssets[assetId] == null {
      // Pre-load without incrementing refcount
      loadAssetAsync(assetId)
      loadedAssets[assetId].refCount = 0  // Available but unreferenced
    }
  }
}

// Sector-based asset streaming
on update(dt) {
  // Determine player sector
  sectorX = math.floor(player.position.x / 100)
  sectorZ = math.floor(player.position.z / 100)

  // Prefetch adjacent sectors
  for dx = -1 to 1 {
    for dz = -1 to 1 {
      sectorId = (sectorX + dx) + "," + (sectorZ + dz)
      if !prefetchedSectors.has(sectorId) {
        prefetchArea(sectorId)
        prefetchedSectors.add(sectorId)
      }
    }
  }

  // Release distant sector assets
  for sectorId in prefetchedSectors {
    parts = string.split(sectorId, ",")
    sx = math.parse(parts[0])
    sz = math.parse(parts[1])
    dist = math.max(math.abs(sx - sectorX), math.abs(sz - sectorZ))
    if dist > 2 {
      releaseSectorAssets(sectorId)
      prefetchedSectors.delete(sectorId)
    }
  }
}

Node Graph Equivalent

The same system implemented as a Node Graph in Appley Node Scripting:

Streaming Assets — Node Graph
[Function: requestAsset]
   --> [Branch: already loaded?]
        +-- True --> [refCount += 1, update lastAccess] --> [Return asset]
   --> [Check: memory budget]
        --> [While: over budget --> evictLRU]
   --> [Load: async from storage]
   --> [Return: null (loading)]

[Function: releaseAsset]
   --> [refCount -= 1]
   --> [Branch: refCount <= 0]
        +-- True --> [Mark: available for eviction]

[Function: evictLRU]
   --> [Find: oldest asset with 0 references]
   --> [Unload: GPU resource]
   --> [Reduce: currentMemory]

[On Update]
   --> [Calculate: player sector]
   --> [Prefetch: 3x3 surrounding sectors]
        --> [For Each: not prefetched]
             --> [loadAssetAsync: all sector assets]
   --> [Release: sectors beyond distance 2]
        --> [releaseSectorAssets]

Professional Optimization Tips

How AAA studios optimize these systems — applicable to your Appley project:

  • Use reference counting for asset lifecycle. Load when refcount goes 0 to 1. Unload when it returns to 0. This prevents unloading assets still in use and avoids loading already-loaded assets.
  • Prefetch adjacent sectors. When the player enters a sector, start loading the 3x3 surrounding sectors. By the time they arrive, assets are ready. This hides streaming behind gameplay.
  • Use LRU eviction with a memory budget. Set a hard memory limit (e.g., 1GB). When exceeded, unload the least-recently-used unreferenced assets. This prevents out-of-memory crashes.
  • Use GPU texture compression (ASTC, DXT). Compressed textures are 4-8x smaller on disk and in memory. Decompression happens on the GPU — no CPU cost. This is the single biggest memory saver.

Common Mistakes

Loading everything at startup

Loading all assets at start causes long load times and high memory. Stream assets on demand based on player location and needs.

No reference counting

Without refcounting, you either never unload (memory leak) or unload assets still in use (crash). Reference counting is essential for safe streaming.