feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -28,6 +28,30 @@ namespace AcDream.App.Rendering;
/// </summary>
public sealed class TerrainAtlas : IDisposable
{
/// <summary>
/// Retail's category-scoped detail surface, resolved from one
/// <c>TexMerge.TerrainDesc</c> entry. The texture-table slot samples a
/// one-layer 2-D array so it uses the same backend-neutral table contract
/// as every other world texture.
/// </summary>
internal readonly record struct RetailDetailTextureBinding(
GpuTextureSlot TextureSlot,
float Tiling,
uint SurfaceTextureId,
uint RenderSurfaceId,
int Width,
int Height)
{
public bool IsAvailable =>
TextureSlot.IsAssigned
&& SurfaceTextureId != 0
&& RenderSurfaceId != 0;
}
private sealed record DetailTextureResource(
IGpuTexture Texture,
RetailDetailTextureBinding Binding);
public IReadOnlyDictionary<uint, uint> TerrainTypeToLayer { get; }
public int LayerCount { get; }
/// <summary>
@ -54,6 +78,18 @@ public sealed class TerrainAtlas : IDisposable
/// <summary>RCode for each RoadMap, parallel to <see cref="RoadAlphaLayers"/>.</summary>
public IReadOnlyList<uint> RoadAlphaRCodes { get; }
/// <summary>
/// Retail detail category 1. <c>DrawBuilding</c> is the only live object
/// path that consumes it; ordinary scenery and terrain do not.
/// </summary>
internal RetailDetailTextureBinding BuildingDetailTexture { get; }
/// <summary>
/// Retail detail category 2. <c>DrawEnvCell</c> consumes it for interior
/// cell shells.
/// </summary>
internal RetailDetailTextureBinding EnvironmentDetailTexture { get; }
/// <summary>
/// Campaign V slice V6i-2: both arrays are <see cref="IGpuTexture"/>s the
/// device created, and both slots were registered at build time, so
@ -72,6 +108,8 @@ public sealed class TerrainAtlas : IDisposable
public IGpuTexture Alpha { get; } = alpha;
public IGpuSampler AlphaSampler { get; } = alphaSampler;
public IGpuSampler? TerrainSampler { get; set; }
public IGpuTexture? BuildingDetailTexture { get; set; }
public IGpuTexture? EnvironmentDetailTexture { get; set; }
public GpuTextureSlot TerrainSlot { get; set; } = GpuTextureSlot.Unassigned;
public GpuTextureSlot AlphaSlot { get; set; } = GpuTextureSlot.Unassigned;
}
@ -106,7 +144,9 @@ public sealed class TerrainAtlas : IDisposable
IReadOnlyList<byte> roadLayers,
IReadOnlyList<uint> cornerTCodes,
IReadOnlyList<uint> sideTCodes,
IReadOnlyList<uint> roadRCodes)
IReadOnlyList<uint> roadRCodes,
DetailTextureResource? buildingDetail,
DetailTextureResource? environmentDetail)
{
_rhi = new RhiArrays(device, terrain, alpha, alphaSampler);
TerrainTypeToLayer = map;
@ -119,6 +159,10 @@ public sealed class TerrainAtlas : IDisposable
CornerAlphaTCodes = cornerTCodes;
SideAlphaTCodes = sideTCodes;
RoadAlphaRCodes = roadRCodes;
_rhi.BuildingDetailTexture = buildingDetail?.Texture;
_rhi.EnvironmentDetailTexture = environmentDetail?.Texture;
BuildingDetailTexture = buildingDetail?.Binding ?? default;
EnvironmentDetailTexture = environmentDetail?.Binding ?? default;
_rhi.AlphaSlot = device.RegisterTexture(alpha, alphaSampler);
ApplyAnisotropic(RetailMaxAnisotropy);
}
@ -323,6 +367,9 @@ public sealed class TerrainAtlas : IDisposable
int mipLevels = Wb.RhiWorldTextureArray.MipLevelsFor(maxW, maxH);
IGpuTexture? terrainTexture = null;
IGpuTexture? alphaTexture = null;
IGpuSampler? detailSampler = null;
DetailTextureResource? buildingDetail = null;
DetailTextureResource? environmentDetail = null;
try
{
terrainTexture = device.CreateTexture(new GpuTextureDescription(
@ -378,6 +425,31 @@ public sealed class TerrainAtlas : IDisposable
MipFilter = GpuMipFilter.None,
});
// Retail LScape::SetDetailTexturing category indices:
// 1 = building, 2 = environment/EnvCell.
// ChangeRegion and the only reachable SmartBox caller keep
// landscape (0) and object (3) disabled, so do not create or expose
// those categories here.
detailSampler = device.CreateSampler(GpuSamplerDescription.WorldRepeat);
if (terrainDesc is { Count: > 1 })
{
buildingDetail = TryCreateDetailTexture(
device,
dats,
detailSampler,
terrainDesc[1],
"building");
}
if (terrainDesc is { Count: > 2 })
{
environmentDetail = TryCreateDetailTexture(
device,
dats,
detailSampler,
terrainDesc[2],
"environment");
}
Console.WriteLine(
$"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH} ({mipLevels} mip levels)");
Console.WriteLine(
@ -399,16 +471,117 @@ public sealed class TerrainAtlas : IDisposable
alpha.RoadLayers,
alpha.CornerTCodes,
alpha.SideTCodes,
alpha.RoadRCodes);
alpha.RoadRCodes,
buildingDetail,
environmentDetail);
}
catch
{
DisposeDetailTextureResource(device, environmentDetail);
DisposeDetailTextureResource(device, buildingDetail);
alphaTexture?.Dispose();
terrainTexture?.Dispose();
throw;
}
}
private static DetailTextureResource? TryCreateDetailTexture(
IGpuDevice device,
IDatReaderWriter dats,
IGpuSampler sampler,
DatReaderWriter.Types.TMTerrainDesc terrain,
string categoryName)
{
uint surfaceTextureId = (uint)terrain.TerrainTex.DetailTextureId;
if (surfaceTextureId == 0)
return null;
SurfaceTexture? surfaceTexture = dats.Get<SurfaceTexture>(surfaceTextureId);
if (surfaceTexture is null || surfaceTexture.Textures.Count == 0)
{
Console.WriteLine(
$"WARN: retail {categoryName} detail SurfaceTexture "
+ $"0x{surfaceTextureId:X8} missing");
return null;
}
uint renderSurfaceId = (uint)surfaceTexture.Textures[0];
RenderSurface? renderSurface = dats.Get<RenderSurface>(renderSurfaceId);
if (renderSurface is null)
{
Console.WriteLine(
$"WARN: retail {categoryName} detail RenderSurface "
+ $"0x{renderSurfaceId:X8} missing");
return null;
}
Palette? palette = renderSurface.DefaultPaletteId != 0
? dats.Get<Palette>(renderSurface.DefaultPaletteId)
: null;
DecodedTexture decoded = SurfaceDecoder.DecodeRenderSurface(
renderSurface,
palette);
if (ReferenceEquals(decoded, DecodedTexture.Magenta))
{
Console.WriteLine(
$"WARN: retail {categoryName} detail RenderSurface "
+ $"0x{renderSurfaceId:X8} failed to decode");
return null;
}
int mipLevels = Wb.RhiWorldTextureArray.MipLevelsFor(
decoded.Width,
decoded.Height);
IGpuTexture? texture = null;
GpuTextureSlot slot = GpuTextureSlot.Unassigned;
try
{
texture = device.CreateTexture(new GpuTextureDescription(
$"retail-detail-{categoryName}",
GpuTextureKind.Texture2DArray,
GpuTextureFormat.Rgba8Unorm,
decoded.Width,
decoded.Height,
LayerCount: 1,
MipLevelCount: mipLevels));
texture.Upload(0, 0, decoded.Rgba8);
texture.GenerateMipChain();
slot = device.RegisterTexture(texture, sampler);
var binding = new RetailDetailTextureBinding(
slot,
terrain.TerrainTex.DetailTexTiling,
surfaceTextureId,
renderSurfaceId,
decoded.Width,
decoded.Height);
Console.WriteLine(
$"Retail detail {categoryName}: SurfaceTexture "
+ $"0x{surfaceTextureId:X8} -> RenderSurface "
+ $"0x{renderSurfaceId:X8}, {decoded.Width}x{decoded.Height}, "
+ $"tiling={binding.Tiling}");
return new DetailTextureResource(texture, binding);
}
catch
{
if (slot.IsAssigned)
device.ReleaseTextureSlot(slot);
texture?.Dispose();
throw;
}
}
private static void DisposeDetailTextureResource(
IGpuDevice device,
DetailTextureResource? resource)
{
if (resource is null)
return;
if (resource.Binding.IsAvailable)
device.ReleaseTextureSlot(resource.Binding.TextureSlot);
resource.Texture.Dispose();
}
private static DecodedTexture WhitePixel() =>
new([0xFF, 0xFF, 0xFF, 0xFF], 1, 1);
@ -495,13 +668,20 @@ public sealed class TerrainAtlas : IDisposable
Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x");
}
private bool _disposed;
public void Dispose()
{
// Slice V4t's teardown rule: the device dies with its callers, so the
// table entries are not released here — deferring through a
// possibly-disposed retirement queue would turn a clean shutdown into a
// throw. The images themselves route through the device's retirement
// queue, which is what IGpuTexture.Dispose does.
if (_disposed)
return;
_disposed = true;
// Slice V4t's teardown rule remains load-bearing: GameWindowLifetime
// disposes the device before this atlas, so normal teardown must not
// call back into its texture table. Build/registration failure paths
// above still release detail slots while the device is known alive.
_rhi.EnvironmentDetailTexture?.Dispose();
_rhi.BuildingDetailTexture?.Dispose();
_rhi.Alpha.Dispose();
_rhi.Terrain.Dispose();
}