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>
This commit is contained in:
Erik 2026-07-13 21:04:22 +02:00
parent 84b7d2d7cd
commit bb5acab9e6
7 changed files with 297 additions and 7 deletions

View file

@ -153,6 +153,15 @@ use from WB rather than re-implement.
| `TextureAtlasManager` | Texture atlas builder |
| `VertexLandscape` | Terrain vertex format |
**Modern terrain adapter:** acdream's bindless path uses `TerrainAtlas` plus
`TerrainModernRenderer` rather than WB's draw manager, but retains
`LandSurfaceManager`'s layer-indexed `TerrainTex.TexTiling` contract. The
36-entry table is uploaded to `terrain_modern.frag`; base, overlay, and road
layers each use their owning repeat count while alpha masks stay at cell scale.
Retail `TexMerge::CopyAndTile` (`0x00503580`) and `TexMerge::Merge`
(`0x005038C0`) are the behavior oracle; see
`docs/research/2026-07-13-retail-terrain-texture-tiling-pseudocode.md`.
### Scenery (procedural placement: trees, bushes, rocks, fences)
| Component | What it does |

View file

@ -0,0 +1,109 @@
# 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
```text
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.