egametang/ET · critical · Exception

Too few salt bits: {m_saltBits}

Error message

Too few salt bits: {m_saltBits}

What it means

DtTileCache packs each tile reference into a 31-bit value split into a tile-index portion and a 'salt' portion. The salt lets the cache detect references to tiles that have since been recycled. The constructor derives m_tileBits = ilog2(nextPow2(maxTiles)) and m_saltBits = min(31, 32 - m_tileBits); if fewer than 10 salt bits remain, reference integrity degrades and the constructor refuses to build the cache. This fires once, at construction, so the DtTileCache is never usable until params change.

Source

Thrown at Packages/cn.etetet.recast/Scripts/Core/Share/Detour.TileCache/DtTileCache.cs:84

            {
                m_tileLutSize = 1;
            }

            m_tileLutMask = m_tileLutSize - 1;
            m_tiles = new DtCompressedTile[m_params.maxTiles];
            m_posLookup = new DtCompressedTile[m_tileLutSize];
            for (int i = m_params.maxTiles - 1; i >= 0; --i)
            {
                m_tiles[i] = new DtCompressedTile(i);
                m_tiles[i].next = m_nextFreeTile;
                m_nextFreeTile = m_tiles[i];
            }

            m_tileBits = DtUtils.Ilog2(DtUtils.NextPow2(m_params.maxTiles));
            m_saltBits = Math.Min(31, 32 - m_tileBits);
            if (m_saltBits < 10)
            {
                throw new Exception("Too few salt bits: " + m_saltBits);
            }
        }

        private bool Contains(List<long> a, long v)
        {
            return a.Contains(v);
        }

        /// Encodes a tile id.
        private long EncodeTileId(int salt, int it)
        {
            return ((long)salt << m_tileBits) | (long)it;
        }

        /// Decodes a tile salt.
        private int DecodeTileIdSalt(long refs)
        {
            long saltMask = (1L << m_saltBits) - 1;

View on GitHub (pinned to 5cab01f7a8)

Solutions

  1. Lower DtTileCacheParams.maxTiles to the smallest power-of-two that covers the real concurrent tile count (commonly a few thousand), so ilog2(nextPow2(maxTiles)) <= 22.
  2. If you genuinely need more than ~4M tiles, partition the world into multiple DtTileCache instances (one per region/streaming chunk) instead of one giant cache.
  3. Assert at config-load time that Ilog2(NextPow2(maxTiles)) <= 22 so the failure surfaces in your own pipeline with a clearer message.

Example fix

// before
var p = new DtTileCacheParams { maxTiles = 8_000_000, ... };
var tc = new DtTileCache(p, storage, mesh, comp, proc);

// after: size to actual need, power of two
var p = new DtTileCacheParams { maxTiles = 4096, ... };
System.Diagnostics.Debug.Assert(
    DotRecast.Recast.DtUtils.Ilog2(DotRecast.Recast.DtUtils.NextPow2(p.maxTiles)) <= 22);
var tc = new DtTileCache(p, storage, mesh, comp, proc);
Defensive patterns

Strategy: validation

Validate before calling

// Run before constructing DtTileCache
int tileBits = DtUtils.Ilog2(DtUtils.NextPow2(p.maxTiles));
int saltBits = Math.Min(31, 32 - tileBits);
if (saltBits < 10)
    throw new ArgumentException(
        $"maxTiles={p.maxTiles} leaves only {saltBits} salt bits (need >=10). " +
        "Reduce maxTiles (<= ~4,194,304) or split into multiple caches.");

Prevention

When it happens

Trigger: Constructing `new DtTileCache(DtTileCacheParams option, ...)` where `option.maxTiles` is so large that `DtUtils.Ilog2(DtUtils.NextPow2(option.maxTiles))` exceeds 22 (i.e. nextPow2(maxTiles) >= 2^23, so maxTiles beyond ~4.19 million). Happens when maxTiles is set to a huge literal or copied from another cache without scaling.

Common situations: Setting maxTiles to a very large number 'to be safe' (e.g. int.MaxValue / a million); reusing DtTileCacheParams from a different world size; generating params programmatically and forgetting to clamp; non-power-of-two huge values that nextPow2 rounds up even further.

Related errors


AI-assisted analysis of egametang/ET@5cab01f7a8 (2026-08-13). Data as JSON: /api/errors/f910aefe5d91a442. Report an issue: GitHub.