acdream/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
Erik dc722e70bd feat(O-T7): drop WB project references; complete extraction
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>
2026-05-21 17:17:33 +02:00

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;
}
});
}
}
}