acdream/docs/research/2026-07-13-retail-terrain-texture-tiling-pseudocode.md
Erik bb5acab9e6 fix(rendering): restore retail terrain texture tiling
Carry TerrainTex.TexTiling through the terrain atlas and upload a layer-indexed table to the modern bindless shader so base, overlay, and road textures repeat at retail scale. Keep alpha masks cell-scaled and preserve retail's verified source-level-zero high-detail selection.

Add the named-retail pseudocode, WorldBuilder/ACE/ACME cross-reference, adapter conformance tests, and inventory documentation so a future renderer migration keeps the contract.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-13 21:04:22 +02:00

4 KiB

Retail terrain texture tiling

Question

Why do acdream's grass, dirt, and road textures look lower-detail than the September 2013 retail client even though the modern terrain atlas loads 512 x 512 source images, generates mipmaps, and enables anisotropic filtering?

Retail oracle

Named retail symbols in docs/research/named-retail/acclient_2013_pseudo_c.txt:

  • TexMerge::CopyAndTile at 0x00503580
  • TexMerge::Merge at 0x005038C0
  • ImgTex::TileCSI at 0x0053E740
  • ImgTex::GetSurfaceDID at 0x0053F0E0
  • Render::ShouldDropHighDetail at 0x0054C700

The verbatim TerrainTex declaration in docs/research/named-retail/acclient.h contains unsigned int tex_tiling.

Readable pseudocode

CopyAndTile(destination, destinationSize, terrainTexture):
    baseTexture = terrainTexture.base_texture
    if baseTexture is null:
        if terrainTexture.tex_gid is valid:
            terrainTexture.base_texture = load ImgTex(terrainTexture.tex_gid)
        baseTexture = terrainTexture.base_texture

    if baseTexture is null:
        CopyCSI(destination, destinationSize, destinationSize, null, 1)
        return false

    CopyCSI(
        destination,
        destinationSize,
        destinationSize,
        baseTexture,
        terrainTexture.tex_tiling)
    return true

Merge(destination, destinationSize, alphaTexture, rotation, terrainTexture):
    if alphaTexture is null:
        return false

    if terrainTexture.base_texture is null:
        if not terrainTexture.InitEnd():
            return false

    MergeTexture(
        destination,
        destinationSize,
        destinationSize,
        terrainTexture.base_texture,
        terrainTexture.tex_tiling,
        alphaTexture,
        rotation)
    return true

GetSurfaceDID(surfaceTexture):
    if surfaceTexture has one source level:
        return sourceLevels[0]
    if surfaceTexture has two source levels:
        if ShouldDropHighDetail():
            return sourceLevels[1]
        return sourceLevels[0]
    report invalid source-level count

ImgTex::TileCSI repeats the source image tex_tiling times in both axes. TexMerge::Merge applies the same repeat count to every terrain layer before the cell-scale alpha mask is merged. Therefore the modern shader must multiply the terrain UV independently for the base layer, each overlay layer, and each road layer. The alpha-map UV must not be multiplied.

The source image selection is a separate policy. Retail uses source level zero unless its quality policy explicitly drops high detail, so changing acdream's atlas from SurfaceTexture.Textures[0] would invert the verified retail rule.

Cross-references

  • Extracted WorldBuilder lineage: LandSurfaceManager stores a 36-entry layer-indexed tiling table populated from TerrainTex.TexTiling, and its terrain shader multiplies each texture layer's UV by that value. The pinned WorldBuilder change introducing the path is commit 0d1405af65.
  • ACE: ACE.Server/Physics/Common/TexMerge.cs forwards TerrainTex.TexTiling from both CopyAndTile and Merge; ImgTex.cs resolves the normal source as Textures[0].
  • ACME-derived tests/AcDream.Core.Tests/Terrain/ClientReference.cs covers terrain split, pal/road codes, and blend selection, but not image tiling or source-level selection. It supplies no competing behavior for this path.

Retail remains the oracle where a reference differs: the pinned WorldBuilder reader selects the last surface source, while retail's GetSurfaceDID proves that the normal/high-detail source is index zero.

acdream port shape

  1. Capture TerrainTex.TexTiling while TerrainAtlas assigns each terrain type to an array layer.
  2. Convert the type-keyed values into the renderer's 36-entry layer-indexed uniform table. Unused layers default to 1.0; mapped values are preserved verbatim.
  3. Upload the table after binding terrain_modern.frag.
  4. Sample base, overlay, and road textures with cellUv * uTexTiling[layer]; sample alpha masks with their original UV.