End of Phase O extraction. Final cleanup: - Dropped <ProjectReference> entries to WorldBuilder.Shared and Chorizite.OpenGLSDLBackend from both AcDream.App.csproj and AcDream.Core.csproj. - Added Chorizite.Core NuGet PackageReference to AcDream.Core.csproj (needed by Core.Rendering.Wb.TextureHelpers for TextureFormat enum; previously transitive through the WB project ref). - Added BCnEncoder.Net.ImageSharp (1.1.2) + SixLabors.ImageSharp (3.1.12) as direct PackageReferences to AcDream.App.csproj — previously transitive via Chorizite.OpenGLSDLBackend project; used directly by ObjectMeshManager. Item A (BaseObjectRenderManager static fields): - Inlined CurrentAtlas/CurrentVAO/CurrentIBO into a new RenderStateCache.cs static class (AcDream.App.Rendering.Wb namespace) — the 4 consumers (ManagedGLIndexBuffer, ManagedGLTexture, ManagedGLTextureArray, ParticleBatcher) all reference RenderStateCache.* instead of BaseObjectRenderManager.*. - Dropped using Chorizite.OpenGLSDLBackend.Lib from all 4 consumers and from WbDrawDispatcher (which had it only as a dead import). Item B (ActiveParticleEmitter.ObjectLandblock): - ObjectLandblock? erased to object?; WorldBuilder.Shared.Models.ObjectId? erased to ulong? — both fields are stored but never read by any consumer in our codebase. - Dropped both WB using directives from ActiveParticleEmitter.cs. Item C (IDatReaderWriter / IDatDatabase): - Verbatim copy of both interfaces into IDatReaderWriter.cs in AcDream.App.Rendering.Wb namespace — DatCollectionAdapter and ObjectMeshManager already live in that namespace, so no using changes needed. - Dropped using WorldBuilder.Shared.Services from DatCollectionAdapter.cs and ObjectMeshManager.cs. Additional extractions required by the reference drop: - GeometryUtils.cs: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils (float-precision overloads only; Vector3d double-precision overloads omitted — ObjectMeshManager uses only the float versions). - Dropped using WorldBuilder.Shared.Lib from ObjectMeshManager.cs. WbMeshAdapter.cs cleanup (spec O-D12): - Deleted _wbDats (DefaultDatReaderWriter) field + ctor init + Dispose call. - Deleted the [indoor-upload] NULL_RESULT diagnostic block (lines ~205-262) — its Phase 2 cell-resolution investigation is complete; its _wbDats.ResolveId dependency goes with this commit. - Deleted _pendingEnvCellRequests field + isPendingEnvCell tracking in Tick(). - Simplified Tick() to a clean drain loop. Deleted SplitFormulaDivergenceTest.cs — one-time N.5b data-collection sweep; job done. Verified acceptance criteria: - Zero <ProjectReference> to WorldBuilder.* / Chorizite.OpenGLSDLBackend.* in any csproj. - Zero 'using WorldBuilder.*' / 'using Chorizite.OpenGLSDLBackend.*' in src/. - DefaultDatReaderWriter referenced in zero places in src/ (comments only). Build green (0 warnings, 0 errors). Tests: 1154 total (-1 from deleted SplitFormulaDivergenceTest), 1146 pass, 8 pre-existing failures (unchanged from baseline — physics/input tests unrelated to this change). Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
435 lines
18 KiB
C#
435 lines
18 KiB
C#
using AcDream.Core.Rendering.Wb;
|
|
using Chorizite.Core.Render;
|
|
using Chorizite.Core.Render.Enums;
|
|
// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly
|
|
using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers;
|
|
using Microsoft.Extensions.Logging;
|
|
using Silk.NET.OpenGL;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace AcDream.App.Rendering.Wb {
|
|
public class ManagedGLTextureArray : ITextureArray {
|
|
private readonly bool[] _usedLayers;
|
|
private readonly GL GL;
|
|
private readonly OpenGLGraphicsDevice _device;
|
|
private readonly ILogger _logger;
|
|
private static int _nextId = 0;
|
|
private bool _needsMipmapRegeneration = false;
|
|
private readonly bool _isCompressed;
|
|
private int _mipmapDirtyCount = 0;
|
|
private readonly object _mipmapLock = new object();
|
|
private uint _pboId;
|
|
private int _pboSize;
|
|
private readonly List<TextureLayerUpdate> _pendingUpdates = new();
|
|
|
|
private struct TextureLayerUpdate {
|
|
public int Layer;
|
|
public int Offset;
|
|
public int Size;
|
|
public PixelFormat? UploadPixelFormat;
|
|
public PixelType? UploadPixelType;
|
|
}
|
|
|
|
public int Slot { get; } = _nextId++;
|
|
public int Width { get; private set; }
|
|
public int Height { get; private set; }
|
|
public int Size { get; private set; }
|
|
public TextureFormat Format { get; private set; }
|
|
public nint NativePtr { get; private set; }
|
|
public ulong BindlessHandle { get; private set; }
|
|
public ulong BindlessWrapHandle { get; private set; }
|
|
public ulong BindlessClampHandle { get; private set; }
|
|
public long TotalSizeInBytes => CalculateTotalSize();
|
|
|
|
public ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
|
|
int size, ILogger logger, TextureParameters? texParams = null) {
|
|
var p = texParams ?? TextureParameters.Default;
|
|
if (width <= 0 || height <= 0 || size <= 0) {
|
|
throw new ArgumentException($"Invalid texture array dimensions: {width}x{height}x{size}");
|
|
}
|
|
|
|
Format = format;
|
|
Width = width;
|
|
Height = height;
|
|
Size = size;
|
|
_usedLayers = new bool[size];
|
|
_device = graphicsDevice;
|
|
GL = graphicsDevice.GL;
|
|
_logger = logger;
|
|
_isCompressed = IsCompressedFormat(format);
|
|
GLHelpers.CheckErrors(GL);
|
|
|
|
NativePtr = (nint)GL.GenTexture();
|
|
if (NativePtr == 0) {
|
|
throw new InvalidOperationException("Failed to generate texture array.");
|
|
}
|
|
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
|
|
|
|
GLHelpers.CheckErrors(GL);
|
|
|
|
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
|
GLHelpers.CheckErrors(GL);
|
|
|
|
int maxDimension = Math.Max(width, height);
|
|
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
|
|
|
GL.TexStorage3D(GLEnum.Texture2DArray, (uint)mipLevels, format.ToGL(), (uint)width, (uint)height,
|
|
(uint)size);
|
|
GLHelpers.CheckErrorsWithContext(GL,
|
|
$"Creating texture array storage (Format={format}, Size={width}x{height}x{size}, MipLevels={mipLevels})");
|
|
|
|
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMinFilter,
|
|
(int)p.MinFilter);
|
|
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMaxLevel, (int)mipLevels - 1);
|
|
|
|
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
|
|
|
|
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapS, (int)p.WrapS);
|
|
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapT, (int)p.WrapT);
|
|
|
|
if (p.EnableAnisotropicFiltering && graphicsDevice.RenderSettings.EnableAnisotropicFiltering) {
|
|
float maxAnisotropy = 0f;
|
|
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out maxAnisotropy);
|
|
|
|
if (maxAnisotropy > 0) {
|
|
GL.TexParameter(GLEnum.Texture2DArray, GLEnum.TextureMaxAnisotropy, maxAnisotropy);
|
|
}
|
|
}
|
|
|
|
// Set texture swizzle for single-channel formats
|
|
if (format == TextureFormat.A8) {
|
|
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleR, (int)GLEnum.One);
|
|
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleG, (int)GLEnum.One);
|
|
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleB, (int)GLEnum.One);
|
|
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleA, (int)GLEnum.Red);
|
|
}
|
|
|
|
GLHelpers.CheckErrors(GL);
|
|
|
|
GpuMemoryTracker.TrackAllocation(CalculateTotalSize(), GpuResourceType.Texture);
|
|
|
|
if (_device.HasBindless && _device.BindlessExtension != null) {
|
|
BindlessHandle = _device.BindlessExtension.GetTextureHandle((uint)NativePtr);
|
|
BindlessWrapHandle = _device.BindlessExtension.GetTextureSamplerHandle((uint)NativePtr, _device.WrapSampler);
|
|
BindlessClampHandle = _device.BindlessExtension.GetTextureSamplerHandle((uint)NativePtr, _device.ClampSampler);
|
|
|
|
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
|
|
_device.BindlessExtension.MakeTextureHandleResident(BindlessWrapHandle);
|
|
_device.BindlessExtension.MakeTextureHandleResident(BindlessClampHandle);
|
|
}
|
|
|
|
_pboId = GL.GenBuffer();
|
|
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
|
}
|
|
|
|
public long CalculateTotalSize() {
|
|
int maxDimension = Math.Max(Width, Height);
|
|
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
|
long layerSize = GetExpectedDataSize();
|
|
long totalSize = 0;
|
|
|
|
for (int i = 0; i < mipLevels; i++) {
|
|
int w = Math.Max(1, Width >> i);
|
|
int h = Math.Max(1, Height >> i);
|
|
if (_isCompressed) {
|
|
totalSize += TextureHelpers.GetCompressedLayerSize(w, h, Format) * Size;
|
|
}
|
|
else {
|
|
totalSize += (long)w * h * (layerSize / (Width * Height)) * Size;
|
|
}
|
|
}
|
|
return totalSize;
|
|
}
|
|
|
|
private bool IsCompressedFormat(TextureFormat format) {
|
|
return format == TextureFormat.DXT1 ||
|
|
format == TextureFormat.DXT3 ||
|
|
format == TextureFormat.DXT5;
|
|
}
|
|
|
|
public void Bind(int slot = 0) {
|
|
if (NativePtr == 0) {
|
|
return;
|
|
}
|
|
|
|
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
|
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
|
|
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
|
|
|
|
if (changedUnit) {
|
|
GL.ActiveTexture(targetTextureUnit);
|
|
}
|
|
|
|
GL.BindSampler((uint)slot, 0);
|
|
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
|
|
|
if (changedUnit) {
|
|
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
|
}
|
|
GLHelpers.CheckErrors(GL);
|
|
}
|
|
|
|
public unsafe int AddLayer(byte[] data) {
|
|
return AddLayer(data, null, null);
|
|
}
|
|
|
|
public unsafe int AddLayer(byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
|
for (int i = 0; i < _usedLayers.Length; i++) {
|
|
if (!_usedLayers[i]) {
|
|
UpdateLayerInternal(i, data, uploadPixelFormat, uploadPixelType);
|
|
_usedLayers[i] = true;
|
|
return i;
|
|
}
|
|
}
|
|
|
|
throw new InvalidOperationException(
|
|
$"No free layers available in texture array (Slot={Slot}, Size={Width}x{Height}x{Size}).");
|
|
}
|
|
|
|
public unsafe int AddLayer(Span<byte> data) {
|
|
return AddLayer(data.ToArray());
|
|
}
|
|
|
|
public void UpdateLayer(int layer, byte[] data) {
|
|
UpdateLayer(layer, data, null, null);
|
|
}
|
|
|
|
public void UpdateLayer(int layer, byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
|
UpdateLayerInternal(layer, data, uploadPixelFormat, uploadPixelType);
|
|
_usedLayers[layer] = true;
|
|
}
|
|
|
|
private unsafe void UpdateLayerInternal(int layer, byte[] data, PixelFormat? uploadPixelFormat,
|
|
PixelType? uploadPixelType) {
|
|
if (NativePtr == 0) {
|
|
throw new InvalidOperationException("Texture array not created.");
|
|
}
|
|
|
|
if (layer < 0 || layer >= Size) {
|
|
throw new ArgumentOutOfRangeException(nameof(layer),
|
|
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
|
}
|
|
|
|
int currentPboOffset = 0;
|
|
lock (_mipmapLock) {
|
|
if (_pendingUpdates.Count > 0) {
|
|
var lastUpdate = _pendingUpdates[^1];
|
|
currentPboOffset = lastUpdate.Offset + lastUpdate.Size;
|
|
}
|
|
|
|
// Align to 4 bytes for safety
|
|
currentPboOffset = (currentPboOffset + 3) & ~3;
|
|
|
|
if (currentPboOffset + data.Length > _pboSize) {
|
|
// Flush existing updates first because BufferData will orphan/clear the PBO
|
|
if (_pendingUpdates.Count > 0) {
|
|
ProcessDirtyUpdatesInternal();
|
|
}
|
|
currentPboOffset = 0;
|
|
|
|
int newSize = Math.Max(_pboSize * 2, data.Length);
|
|
newSize = Math.Max(newSize, GetExpectedDataSize() * 4); // Initial size 4 layers
|
|
|
|
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
|
|
GL.BufferData(GLEnum.PixelUnpackBuffer, (nuint)newSize, (void*)0, GLEnum.StreamDraw);
|
|
|
|
if (_pboSize > 0) {
|
|
GpuMemoryTracker.TrackDeallocation(_pboSize, GpuResourceType.Buffer);
|
|
}
|
|
_pboSize = newSize;
|
|
GpuMemoryTracker.TrackAllocation(_pboSize, GpuResourceType.Buffer);
|
|
}
|
|
else {
|
|
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
|
|
}
|
|
|
|
fixed (byte* ptr = data) {
|
|
GL.BufferSubData(GLEnum.PixelUnpackBuffer, (nint)currentPboOffset, (nuint)data.Length, ptr);
|
|
}
|
|
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
|
|
|
_pendingUpdates.Add(new TextureLayerUpdate {
|
|
Layer = layer,
|
|
Offset = currentPboOffset,
|
|
Size = data.Length,
|
|
UploadPixelFormat = uploadPixelFormat,
|
|
UploadPixelType = uploadPixelType
|
|
});
|
|
|
|
_needsMipmapRegeneration = true;
|
|
_mipmapDirtyCount++;
|
|
}
|
|
}
|
|
|
|
public void ProcessDirtyUpdates() {
|
|
lock (_mipmapLock) {
|
|
ProcessDirtyUpdatesInternal();
|
|
}
|
|
}
|
|
|
|
private unsafe void ProcessDirtyUpdatesInternal() {
|
|
if (_pendingUpdates.Count == 0 && !_needsMipmapRegeneration) return;
|
|
|
|
GLHelpers.CheckErrors(GL);
|
|
|
|
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
|
RenderStateCache.CurrentAtlas = 0;
|
|
|
|
GL.GetInteger(GLEnum.TextureBinding2DArray, out int oldBinding);
|
|
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
|
|
|
bool wasResident = false;
|
|
if (BindlessHandle != 0 && _device.BindlessExtension != null && _device.BindlessExtension.IsTextureHandleResident(BindlessHandle)) {
|
|
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
|
|
wasResident = true;
|
|
}
|
|
|
|
if (_pendingUpdates.Count > 0) {
|
|
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
|
|
|
|
foreach (var update in _pendingUpdates) {
|
|
if (_isCompressed) {
|
|
var internalFormat = Format.ToCompressedGL();
|
|
GL.CompressedTexSubImage3D(GLEnum.Texture2DArray, 0, 0, 0, update.Layer,
|
|
(uint)Width, (uint)Height, 1, internalFormat, (uint)update.Size, (void*)update.Offset);
|
|
}
|
|
else {
|
|
var pixelFormat = update.UploadPixelFormat ?? Format.ToPixelFormat();
|
|
var pixelType = update.UploadPixelType ?? Format.ToPixelType();
|
|
GL.TexSubImage3D(GLEnum.Texture2DArray, 0, 0, 0, update.Layer, (uint)Width, (uint)Height, 1,
|
|
pixelFormat, pixelType, (void*)update.Offset);
|
|
}
|
|
}
|
|
|
|
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
|
_pendingUpdates.Clear();
|
|
}
|
|
|
|
if (_needsMipmapRegeneration && _mipmapDirtyCount > 0) {
|
|
if (_isCompressed) {
|
|
_logger.LogDebug("Skipping automatic mipmap generation for compressed texture array (Slot={Slot})", Slot);
|
|
}
|
|
else if (!GLHelpers.ValidateTextureMipmapStatus(GL, GLEnum.Texture2DArray, out var errorMessage)) {
|
|
_logger.LogWarning("Mipmap validation failed for texture array (Slot={Slot}): {Error}", Slot, errorMessage);
|
|
}
|
|
else {
|
|
try {
|
|
GL.GenerateMipmap(GLEnum.Texture2DArray);
|
|
}
|
|
catch (Exception ex) {
|
|
_logger.LogWarning(ex, "Failed to generate mipmaps for texture array (Slot={Slot}).", Slot);
|
|
}
|
|
}
|
|
_mipmapDirtyCount = 0;
|
|
_needsMipmapRegeneration = false;
|
|
}
|
|
|
|
if (wasResident && BindlessHandle != 0 && _device.BindlessExtension != null) {
|
|
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
|
|
}
|
|
|
|
GL.BindTexture(GLEnum.Texture2DArray, (uint)oldBinding);
|
|
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
|
GLHelpers.CheckErrors(GL);
|
|
}
|
|
|
|
private void ClearLayerForMipmap(int layer) {
|
|
// Upload a single black/transparent pixel to make layer defined
|
|
byte[] clearData = new byte[GetExpectedDataSize()];
|
|
Array.Clear(clearData, 0, clearData.Length); // Zero-fill (black/transparent)
|
|
UpdateLayerInternal(layer, clearData, null, null);
|
|
}
|
|
|
|
private int GetExpectedDataSize() {
|
|
if (_isCompressed) {
|
|
return TextureHelpers.GetCompressedLayerSize(Width, Height, Format);
|
|
}
|
|
|
|
return Format switch {
|
|
TextureFormat.RGBA8 => Width * Height * 4,
|
|
TextureFormat.RGB8 => Width * Height * 3,
|
|
TextureFormat.A8 => Width * Height * 1,
|
|
TextureFormat.Rgba32f => Width * Height * 16,
|
|
_ => throw new NotSupportedException($"Unsupported format {Format}")
|
|
};
|
|
}
|
|
|
|
public void RemoveLayer(int layer) {
|
|
if (layer < 0 || layer >= Size) {
|
|
throw new ArgumentOutOfRangeException(nameof(layer),
|
|
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
|
}
|
|
|
|
if (!_usedLayers[layer]) {
|
|
throw new InvalidOperationException($"Layer {layer} is already free (Slot={Slot}).");
|
|
}
|
|
|
|
_usedLayers[layer] = false;
|
|
|
|
// Make layer defined for mipmap completeness (uncompressed only)
|
|
if (!_isCompressed) {
|
|
ClearLayerForMipmap(layer);
|
|
}
|
|
|
|
lock (_mipmapLock) {
|
|
_mipmapDirtyCount++; // Mark dirty to regen
|
|
_needsMipmapRegeneration = true;
|
|
}
|
|
}
|
|
|
|
public bool IsLayerUsed(int layer) {
|
|
if (layer < 0 || layer >= Size) return false;
|
|
return _usedLayers[layer];
|
|
}
|
|
|
|
public int GetUsedLayerCount() {
|
|
return _usedLayers.Count(x => x);
|
|
}
|
|
|
|
public void Unbind() {
|
|
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
|
GLHelpers.CheckErrors(GL);
|
|
}
|
|
|
|
public void GenerateMipmaps() {
|
|
_needsMipmapRegeneration = true;
|
|
lock (_mipmapLock) {
|
|
_mipmapDirtyCount++;
|
|
}
|
|
}
|
|
|
|
public void Dispose() {
|
|
_device.QueueGLAction(GL => {
|
|
if (_device.BindlessExtension != null) {
|
|
if (BindlessHandle != 0) {
|
|
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
|
|
BindlessHandle = 0;
|
|
}
|
|
if (BindlessWrapHandle != 0) {
|
|
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessWrapHandle);
|
|
BindlessWrapHandle = 0;
|
|
}
|
|
if (BindlessClampHandle != 0) {
|
|
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessClampHandle);
|
|
BindlessClampHandle = 0;
|
|
}
|
|
}
|
|
if (NativePtr != 0) {
|
|
GL.DeleteTexture((uint)NativePtr);
|
|
GLHelpers.CheckErrors(GL);
|
|
GpuMemoryTracker.TrackDeallocation(CalculateTotalSize(), GpuResourceType.Texture);
|
|
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
|
NativePtr = 0;
|
|
}
|
|
if (_pboId != 0) {
|
|
GL.DeleteBuffer(_pboId);
|
|
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
|
if (_pboSize > 0) {
|
|
GpuMemoryTracker.TrackDeallocation(_pboSize, GpuResourceType.Buffer);
|
|
}
|
|
_pboId = 0;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|