Vulkan is the sole, user-signed-off backend (V10 landed) and step 1 already removed ImGui/Studio/DevTools. This step deletes the GL rendering backend itself: every Gpu/Gl/** implementation, the Wb ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/ BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache, RenderBootstrap, and RenderFrameGlStateController. GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/ OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone — there is nothing left to select between. The five world-draw dual-arm renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer, SkyRenderer) and the composition roots (WorldRenderComposition, HostInputCameraComposition, LivePresentationComposition, FrameRootComposition) collapse to their RHI-only arm. GL-only diagnostic properties with a live external reader (DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op rather than disappearing, since the reader is out of this commit's scope. A few GL-flavored mechanisms turned out to be backend-neutral once isolated: GlConstructionCleanupLedger is renamed ResourceConstructionCleanupLedger (exception-chain walking has nothing to do with GL), and GlfwNativePlatformProbe moved out of the otherwise GL-only GraphicalCapabilityRecord.cs into GraphicalWindowBackendSelection.cs before the rest of that file was deleted. Test files with no surviving subject are deleted outright (GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests, PortalDepthShaderParityTests, TextureCacheBindlessTests, TextRendererFailureSafetyTests, ClipFrameUploadTests, every Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests); others get their dead GL-only members trimmed while their live assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now reads GpuBindingModel.StorageClipRegions, the same binding index under its new backend-neutral name; GpuResourceRetirementTransactionTests drops its OpenGLGraphicsDevice-subclassing test double and the two GL queue tests it existed for). EnvCellRendererTests' construction helper now builds a real ObjectMeshManager via VulkanMeshPipelineDevice instead of passing null through a null-forgiving operator, since the RHI constructor never tolerated a null mesh manager and the old GL constructor (which did) is gone. Deferred to the next two steps, deliberately not touched here: the Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl (WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale csproj comment (the package itself is still load-bearing — TextureFormat and friends are used well beyond the deleted ManagedGLUniformBuffer), and the CI/gate scripts. Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors. Tests: full-solution `dotnet test` green across every project (App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all others 100%); the 2 App.Tests names that flake under full-suite parallel execution (#250-family, documented pre-existing) pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
508 lines
20 KiB
C#
508 lines
20 KiB
C#
using AcDream.App.Rendering.Gpu;
|
||
using AcDream.Core.Textures;
|
||
using DatReaderWriter;
|
||
using AcDream.Content;
|
||
using DatReaderWriter.DBObjs;
|
||
using DatReaderWriter.Enums;
|
||
using DatPixelFormat = DatReaderWriter.Enums.PixelFormat;
|
||
|
||
namespace AcDream.App.Rendering;
|
||
|
||
/// <summary>
|
||
/// Holds both texture arrays the terrain renderer samples from:
|
||
/// <list type="bullet">
|
||
/// <item><description>
|
||
/// <b>Terrain atlas</b> — one GL_TEXTURE_2D_ARRAY layer per terrain type
|
||
/// (grass, dirt, sand, forest...), sourced from
|
||
/// Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc.
|
||
/// </description></item>
|
||
/// <item><description>
|
||
/// <b>Alpha atlas</b> — one GL_TEXTURE_2D_ARRAY layer per blend mask,
|
||
/// sourced from CornerTerrainMaps / SideTerrainMaps / RoadMaps in the
|
||
/// same TexMerge. Used by the fragment shader to blend up to three
|
||
/// terrain overlays and two roads on top of a base cell texture.
|
||
/// </description></item>
|
||
/// </list>
|
||
/// The alpha atlas is built but not yet sampled by any shader — that wiring
|
||
/// lands in Phase 3c.4 along with the shader rewrite.
|
||
/// </summary>
|
||
public sealed class TerrainAtlas : IDisposable
|
||
{
|
||
public IReadOnlyDictionary<uint, uint> TerrainTypeToLayer { get; }
|
||
public int LayerCount { get; }
|
||
/// <summary>
|
||
/// UV repeat count for each terrain-array layer. Retail forwards
|
||
/// <c>TerrainTex::tex_tiling</c> to both base and merged terrain textures
|
||
/// (`TexMerge::CopyAndTile` 0x00503580, `TexMerge::Merge` 0x005038C0).
|
||
/// </summary>
|
||
public IReadOnlyList<float> TilingByLayer { get; }
|
||
|
||
// --- Alpha atlas (new in Phase 3c.2) ---
|
||
public int AlphaLayerCount { get; }
|
||
/// <summary>Layer indices in the alpha atlas for CornerTerrainMaps (typically 4 entries).</summary>
|
||
public IReadOnlyList<byte> CornerAlphaLayers { get; }
|
||
/// <summary>Layer indices in the alpha atlas for SideTerrainMaps (typically 4 entries).</summary>
|
||
public IReadOnlyList<byte> SideAlphaLayers { get; }
|
||
/// <summary>Layer indices in the alpha atlas for RoadMaps (variable count).</summary>
|
||
public IReadOnlyList<byte> RoadAlphaLayers { get; }
|
||
|
||
// --- Parallel TCode/RCode arrays (added in Phase 3c.4 for BuildSurface) ---
|
||
/// <summary>TCode for each CornerTerrainMap, parallel to <see cref="CornerAlphaLayers"/>.</summary>
|
||
public IReadOnlyList<uint> CornerAlphaTCodes { get; }
|
||
/// <summary>TCode for each SideTerrainMap, parallel to <see cref="SideAlphaLayers"/>.</summary>
|
||
public IReadOnlyList<uint> SideAlphaTCodes { get; }
|
||
/// <summary>RCode for each RoadMap, parallel to <see cref="RoadAlphaLayers"/>.</summary>
|
||
public IReadOnlyList<uint> RoadAlphaRCodes { 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
|
||
/// <see cref="TextureSlots"/> is a field read rather than a residency
|
||
/// negotiation. The GL construction path this used to sit alongside is
|
||
/// deleted as of Campaign V slice V11.
|
||
/// </summary>
|
||
private sealed class RhiArrays(
|
||
IGpuDevice device,
|
||
IGpuTexture terrain,
|
||
IGpuTexture alpha,
|
||
IGpuSampler alphaSampler)
|
||
{
|
||
public IGpuDevice Device { get; } = device;
|
||
public IGpuTexture Terrain { get; } = terrain;
|
||
public IGpuTexture Alpha { get; } = alpha;
|
||
public IGpuSampler AlphaSampler { get; } = alphaSampler;
|
||
public IGpuSampler? TerrainSampler { get; set; }
|
||
public GpuTextureSlot TerrainSlot { get; set; } = GpuTextureSlot.Unassigned;
|
||
public GpuTextureSlot AlphaSlot { get; set; } = GpuTextureSlot.Unassigned;
|
||
}
|
||
|
||
private readonly RhiArrays _rhi;
|
||
|
||
/// <summary>
|
||
/// The device-table slots for the terrain and alpha arrays. Registered once
|
||
/// at build time and re-registered only by <see cref="SetAnisotropic"/>,
|
||
/// which changes the sampler.
|
||
/// </summary>
|
||
internal (GpuTextureSlot Terrain, GpuTextureSlot Alpha) TextureSlots =>
|
||
(_rhi.TerrainSlot, _rhi.AlphaSlot);
|
||
|
||
/// <summary>
|
||
/// Retail's terrain arrays are trilinear-filtered with the highest anisotropy
|
||
/// the quality preset allows; <see cref="SetAnisotropic"/> lowers it.
|
||
/// </summary>
|
||
private const float RetailMaxAnisotropy = 16f;
|
||
|
||
private TerrainAtlas(
|
||
IGpuDevice device,
|
||
IGpuTexture terrain,
|
||
IGpuTexture alpha,
|
||
IGpuSampler alphaSampler,
|
||
IReadOnlyDictionary<uint, uint> map,
|
||
int layerCount,
|
||
IReadOnlyList<float> tilingByLayer,
|
||
int alphaLayerCount,
|
||
IReadOnlyList<byte> cornerLayers,
|
||
IReadOnlyList<byte> sideLayers,
|
||
IReadOnlyList<byte> roadLayers,
|
||
IReadOnlyList<uint> cornerTCodes,
|
||
IReadOnlyList<uint> sideTCodes,
|
||
IReadOnlyList<uint> roadRCodes)
|
||
{
|
||
_rhi = new RhiArrays(device, terrain, alpha, alphaSampler);
|
||
TerrainTypeToLayer = map;
|
||
LayerCount = layerCount;
|
||
TilingByLayer = tilingByLayer;
|
||
AlphaLayerCount = alphaLayerCount;
|
||
CornerAlphaLayers = cornerLayers;
|
||
SideAlphaLayers = sideLayers;
|
||
RoadAlphaLayers = roadLayers;
|
||
CornerAlphaTCodes = cornerTCodes;
|
||
SideAlphaTCodes = sideTCodes;
|
||
RoadAlphaRCodes = roadRCodes;
|
||
_rhi.AlphaSlot = device.RegisterTexture(alpha, alphaSampler);
|
||
ApplyAnisotropic(RetailMaxAnisotropy);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Campaign V slice V6i-2: the decode both construction paths share.
|
||
/// Splitting it out is what keeps the CPU logic single while the upload
|
||
/// forks — plan §3.1's rule. Nothing here touches a graphics API.
|
||
/// </summary>
|
||
private readonly record struct TerrainLayerDecode(
|
||
Dictionary<uint, DecodedTexture> DecodedByType,
|
||
Dictionary<uint, uint> TilingByType,
|
||
int MaxWidth,
|
||
int MaxHeight);
|
||
|
||
private static TerrainLayerDecode DecodeTerrainLayers(
|
||
IDatReaderWriter dats,
|
||
IReadOnlyList<DatReaderWriter.Types.TMTerrainDesc> terrainDesc)
|
||
{
|
||
var decodedByType = new Dictionary<uint, DecodedTexture>();
|
||
var tilingByType = new Dictionary<uint, uint>();
|
||
int maxW = 1, maxH = 1;
|
||
foreach (var tmtd in terrainDesc)
|
||
{
|
||
uint typeKey = (uint)tmtd.TerrainType;
|
||
if (decodedByType.ContainsKey(typeKey))
|
||
continue;
|
||
|
||
// Retail terrain composition repeats this surface exactly
|
||
// TerrainTex::tex_tiling times in each axis before applying the
|
||
// cell-scale alpha mask. Preserve the dat field alongside the
|
||
// decoded surface while layer assignment is still type-keyed.
|
||
tilingByType[typeKey] = tmtd.TerrainTex.TexTiling;
|
||
|
||
var surfaceTextureId = (uint)tmtd.TerrainTex.TextureId;
|
||
var st = dats.Get<SurfaceTexture>(surfaceTextureId);
|
||
if (st is null || st.Textures.Count == 0)
|
||
{
|
||
Console.WriteLine($"WARN: TerrainType {tmtd.TerrainType} SurfaceTexture 0x{surfaceTextureId:X8} missing");
|
||
decodedByType[typeKey] = DecodedTexture.Magenta;
|
||
continue;
|
||
}
|
||
|
||
// Retail ImgTex::GetSurfaceDID (0x0053F0E0) returns source level
|
||
// zero unless Render::ShouldDropHighDetail explicitly selects
|
||
// level one. acdream currently has no low-detail quality mode, so
|
||
// index zero is the retail high-detail source.
|
||
var rs = dats.Get<RenderSurface>((uint)st.Textures[0]);
|
||
if (rs is null)
|
||
{
|
||
decodedByType[typeKey] = DecodedTexture.Magenta;
|
||
continue;
|
||
}
|
||
|
||
Palette? palette = rs.DefaultPaletteId != 0
|
||
? dats.Get<Palette>(rs.DefaultPaletteId)
|
||
: null;
|
||
|
||
var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette);
|
||
decodedByType[typeKey] = decoded;
|
||
if (decoded.Width > maxW) maxW = decoded.Width;
|
||
if (decoded.Height > maxH) maxH = decoded.Height;
|
||
}
|
||
|
||
return new TerrainLayerDecode(decodedByType, tilingByType, maxW, maxH);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Slice V6i-2: the alpha-map decode, shared by both construction paths for
|
||
/// the same reason <see cref="DecodeTerrainLayers"/> is.
|
||
/// </summary>
|
||
private sealed record AlphaLayerDecode(
|
||
List<DecodedTexture> Decoded,
|
||
List<byte> CornerLayers,
|
||
List<byte> SideLayers,
|
||
List<byte> RoadLayers,
|
||
List<uint> CornerTCodes,
|
||
List<uint> SideTCodes,
|
||
List<uint> RoadRCodes,
|
||
int MaxWidth,
|
||
int MaxHeight);
|
||
|
||
private static AlphaLayerDecode DecodeAlphaLayers(
|
||
IDatReaderWriter dats,
|
||
DatReaderWriter.Types.TexMerge texMerge)
|
||
{
|
||
var decoded = new List<DecodedTexture>();
|
||
var cornerLayers = new List<byte>();
|
||
var sideLayers = new List<byte>();
|
||
var roadLayers = new List<byte>();
|
||
var cornerTCodes = new List<uint>();
|
||
var sideTCodes = new List<uint>();
|
||
var roadRCodes = new List<uint>();
|
||
|
||
foreach (var entry in texMerge.CornerTerrainMaps)
|
||
{
|
||
if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex))
|
||
{
|
||
cornerLayers.Add((byte)decoded.Count);
|
||
cornerTCodes.Add(entry.TCode);
|
||
decoded.Add(dtex);
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine($"WARN: CornerTerrainMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode");
|
||
}
|
||
}
|
||
foreach (var entry in texMerge.SideTerrainMaps)
|
||
{
|
||
if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex))
|
||
{
|
||
sideLayers.Add((byte)decoded.Count);
|
||
sideTCodes.Add(entry.TCode);
|
||
decoded.Add(dtex);
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine($"WARN: SideTerrainMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode");
|
||
}
|
||
}
|
||
foreach (var entry in texMerge.RoadMaps)
|
||
{
|
||
if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex))
|
||
{
|
||
roadLayers.Add((byte)decoded.Count);
|
||
roadRCodes.Add(entry.RCode);
|
||
decoded.Add(dtex);
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine($"WARN: RoadMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode");
|
||
}
|
||
}
|
||
|
||
// Alpha maps should all be uniform size (WorldBuilder asserts 512×512).
|
||
// Fall back to the max observed so a stray mismatch doesn't crash us.
|
||
int decodedMaxW = 1, decodedMaxH = 1;
|
||
foreach (var d in decoded)
|
||
{
|
||
if (d.Width > decodedMaxW) decodedMaxW = d.Width;
|
||
if (d.Height > decodedMaxH) decodedMaxH = d.Height;
|
||
}
|
||
|
||
return new AlphaLayerDecode(
|
||
decoded,
|
||
cornerLayers,
|
||
sideLayers,
|
||
roadLayers,
|
||
cornerTCodes,
|
||
sideTCodes,
|
||
roadRCodes,
|
||
decodedMaxW,
|
||
decodedMaxH);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Builds both arrays through <see cref="IGpuDevice"/>. The terrain array
|
||
/// gets a full mip chain (<see cref="IGpuTexture.GenerateMipChain"/> blits
|
||
/// it, because RGBA8 is a legal blit destination) and a repeat/anisotropic
|
||
/// sampler; the alpha array is single-level and clamped, matching what the
|
||
/// deleted GL path's texture parameters said.
|
||
/// </summary>
|
||
internal static TerrainAtlas BuildBackendNeutral(IGpuDevice device, IDatReaderWriter dats)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(device);
|
||
ArgumentNullException.ThrowIfNull(dats);
|
||
|
||
var region = dats.Get<Region>(0x13000000u)
|
||
?? throw new InvalidOperationException("Region dat id 0x13000000 missing");
|
||
var texMerge = region.TerrainInfo?.LandSurfaces?.TexMerge;
|
||
var terrainDesc = texMerge?.TerrainDesc;
|
||
|
||
Dictionary<uint, DecodedTexture> decodedByType;
|
||
Dictionary<uint, uint> tilingByType;
|
||
int maxW, maxH;
|
||
if (terrainDesc is null || terrainDesc.Count == 0)
|
||
{
|
||
Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer");
|
||
decodedByType = new Dictionary<uint, DecodedTexture> { [0u] = WhitePixel() };
|
||
tilingByType = [];
|
||
maxW = 1;
|
||
maxH = 1;
|
||
}
|
||
else
|
||
{
|
||
TerrainLayerDecode decode = DecodeTerrainLayers(dats, terrainDesc);
|
||
decodedByType = decode.DecodedByType;
|
||
tilingByType = decode.TilingByType;
|
||
maxW = decode.MaxWidth;
|
||
maxH = decode.MaxHeight;
|
||
}
|
||
|
||
AlphaLayerDecode alpha = texMerge is null
|
||
? new AlphaLayerDecode([], [], [], [], [], [], [], 1, 1)
|
||
: DecodeAlphaLayers(dats, texMerge);
|
||
List<DecodedTexture> alphaDecoded = alpha.Decoded;
|
||
int alphaLayerCount = Math.Max(1, alphaDecoded.Count);
|
||
int alphaW = alphaDecoded.Count == 0 ? 1 : alpha.MaxWidth;
|
||
int alphaH = alphaDecoded.Count == 0 ? 1 : alpha.MaxHeight;
|
||
|
||
int layerCount = decodedByType.Count;
|
||
int mipLevels = Wb.RhiWorldTextureArray.MipLevelsFor(maxW, maxH);
|
||
IGpuTexture? terrainTexture = null;
|
||
IGpuTexture? alphaTexture = null;
|
||
try
|
||
{
|
||
terrainTexture = device.CreateTexture(new GpuTextureDescription(
|
||
"terrain-atlas",
|
||
GpuTextureKind.Texture2DArray,
|
||
GpuTextureFormat.Rgba8Unorm,
|
||
maxW,
|
||
maxH,
|
||
layerCount,
|
||
mipLevels));
|
||
|
||
var map = new Dictionary<uint, uint>(layerCount);
|
||
int layerIdx = 0;
|
||
foreach (var kvp in decodedByType)
|
||
{
|
||
byte[] buffer = ResizeRgba8Nearest(kvp.Value, maxW, maxH);
|
||
terrainTexture.Upload(0, layerIdx, buffer);
|
||
map[kvp.Key] = (uint)layerIdx;
|
||
layerIdx++;
|
||
}
|
||
|
||
// A.5 T19's mip chain, built by the device rather than by
|
||
// glGenerateMipmap. RGBA8 blits, so this is the GPU path.
|
||
terrainTexture.GenerateMipChain();
|
||
|
||
var tilingByLayer = TerrainTextureTilingTable.Build(
|
||
map.Select(entry =>
|
||
(entry.Value, tilingByType.TryGetValue(entry.Key, out uint repeatCount)
|
||
? repeatCount
|
||
: 1u)));
|
||
|
||
alphaTexture = device.CreateTexture(new GpuTextureDescription(
|
||
"terrain-alpha-atlas",
|
||
GpuTextureKind.Texture2DArray,
|
||
GpuTextureFormat.Rgba8Unorm,
|
||
alphaW,
|
||
alphaH,
|
||
alphaLayerCount,
|
||
MipLevelCount: 1));
|
||
if (alphaDecoded.Count == 0)
|
||
{
|
||
Console.WriteLine("WARN: no alpha maps loaded; alpha atlas will be a 1x1 white fallback");
|
||
alphaTexture.Upload(0, 0, [0xFF, 0xFF, 0xFF, 0xFF]);
|
||
}
|
||
else
|
||
{
|
||
for (int i = 0; i < alphaDecoded.Count; i++)
|
||
alphaTexture.Upload(0, i, ResizeRgba8Nearest(alphaDecoded[i], alphaW, alphaH));
|
||
}
|
||
|
||
IGpuSampler alphaSampler = device.CreateSampler(GpuSamplerDescription.WorldClamp with
|
||
{
|
||
MipFilter = GpuMipFilter.None,
|
||
});
|
||
|
||
Console.WriteLine(
|
||
$"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH} ({mipLevels} mip levels)");
|
||
Console.WriteLine(
|
||
$"AlphaAtlas: {alphaLayerCount} layers at {alphaW}x{alphaH} "
|
||
+ $"(corners={alpha.CornerLayers.Count}, sides={alpha.SideLayers.Count}, "
|
||
+ $"roads={alpha.RoadLayers.Count})");
|
||
|
||
return new TerrainAtlas(
|
||
device,
|
||
terrainTexture,
|
||
alphaTexture,
|
||
alphaSampler,
|
||
map,
|
||
layerCount,
|
||
tilingByLayer,
|
||
alphaLayerCount,
|
||
alpha.CornerLayers,
|
||
alpha.SideLayers,
|
||
alpha.RoadLayers,
|
||
alpha.CornerTCodes,
|
||
alpha.SideTCodes,
|
||
alpha.RoadRCodes);
|
||
}
|
||
catch
|
||
{
|
||
alphaTexture?.Dispose();
|
||
terrainTexture?.Dispose();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private static DecodedTexture WhitePixel() =>
|
||
new([0xFF, 0xFF, 0xFF, 0xFF], 1, 1);
|
||
|
||
/// <summary>
|
||
/// Slice V6i-2: the backend-neutral anisotropy change. GL mutates a texture
|
||
/// parameter; Vulkan bakes filtering into the sampler, so the level change
|
||
/// is a new sampler and a re-registration of the terrain slot. The
|
||
/// superseded slot is released in the same step, exactly as the bindless arm
|
||
/// does when a handle changes.
|
||
/// </summary>
|
||
private void ApplyAnisotropic(float level)
|
||
{
|
||
RhiArrays rhi = _rhi!;
|
||
IGpuSampler sampler = rhi.Device.CreateSampler(GpuSamplerDescription.WorldRepeat with
|
||
{
|
||
MaxAnisotropy = Math.Max(1f, level),
|
||
});
|
||
if (ReferenceEquals(rhi.TerrainSampler, sampler) && rhi.TerrainSlot.IsAssigned)
|
||
return;
|
||
|
||
if (rhi.TerrainSlot.IsAssigned)
|
||
rhi.Device.ReleaseTextureSlot(rhi.TerrainSlot);
|
||
rhi.TerrainSampler = sampler;
|
||
rhi.TerrainSlot = rhi.Device.RegisterTexture(rhi.Terrain, sampler);
|
||
}
|
||
|
||
private static bool TryDecodeAlphaMap(IDatReaderWriter dats, uint surfaceTextureId, out DecodedTexture decoded)
|
||
{
|
||
decoded = DecodedTexture.Magenta;
|
||
|
||
var st = dats.Get<SurfaceTexture>(surfaceTextureId);
|
||
if (st is null || st.Textures.Count == 0)
|
||
return false;
|
||
|
||
var rs = dats.Get<RenderSurface>((uint)st.Textures[0]);
|
||
if (rs is null)
|
||
return false;
|
||
|
||
// Alpha maps ship as PFID_CUSTOM_LSCAPE_ALPHA (AC's landscape-alpha
|
||
// format) or the more generic PFID_A8; terrain blending alpha masks
|
||
// MUST use isAdditive=true so R=G=B=A=val — the terrain fragment shader
|
||
// reads .r for the blend weight. Palette is not used.
|
||
var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true);
|
||
if (ReferenceEquals(d, DecodedTexture.Magenta))
|
||
return false;
|
||
|
||
decoded = d;
|
||
return true;
|
||
}
|
||
|
||
private static byte[] ResizeRgba8Nearest(DecodedTexture src, int dstW, int dstH)
|
||
{
|
||
if (src.Width == dstW && src.Height == dstH)
|
||
return src.Rgba8;
|
||
|
||
var dst = new byte[dstW * dstH * 4];
|
||
for (int y = 0; y < dstH; y++)
|
||
{
|
||
int srcY = y * src.Height / dstH;
|
||
for (int x = 0; x < dstW; x++)
|
||
{
|
||
int srcX = x * src.Width / dstW;
|
||
int si = (srcY * src.Width + srcX) * 4;
|
||
int di = (y * dstW + x) * 4;
|
||
dst[di + 0] = src.Rgba8[si + 0];
|
||
dst[di + 1] = src.Rgba8[si + 1];
|
||
dst[di + 2] = src.Rgba8[si + 2];
|
||
dst[di + 3] = src.Rgba8[si + 3];
|
||
}
|
||
}
|
||
return dst;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Update terrain-array anisotropy at runtime (called by
|
||
/// <see cref="AcDream.App.Settings.RuntimeSettingsController.ReapplyQualityPreset"/> when
|
||
/// the user changes Quality preset mid-session). Idempotent — calling with
|
||
/// the same level as the current setting is safe and produces no visual
|
||
/// change.
|
||
/// </summary>
|
||
public void SetAnisotropic(int level)
|
||
{
|
||
ApplyAnisotropic(level);
|
||
Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x");
|
||
}
|
||
|
||
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.
|
||
_rhi.Alpha.Dispose();
|
||
_rhi.Terrain.Dispose();
|
||
}
|
||
}
|