refactor(pipeline): MP1a - Content-owned upload enums; drop Silk.NET from Content

Coordinator-directed follow-up: AcDream.Content must stay Silk.NET-free
(the MP1b bake tool must not ship GL binaries). The Silk.NET.OpenGL
PackageReference added for the PixelFormat?/PixelType? upload hints is
replaced by Content-owned UploadPixelFormat/UploadPixelType enums
(UploadFormats.cs) whose underlying values are the GL ABI constants
(Rgba = 0x1908, UnsignedByte = 0x1401), verified numerically identical
to the Silk.NET.OpenGL members against 2.23.0. This is the one
sanctioned edit to the verbatim-moved Prepare* bodies: enum literal for
enum literal, numeric value identical, behavior unchanged. App casts at
the single upload boundary (AddTexture call in UploadGfxObjMeshData)
via lifted nullable enum conversion — value- and null-preserving.

Also hardens the MP1a _sideStaged hand-off seam: List -> ConcurrentQueue.
One MeshExtractor is shared by up to MaxParallelLoads (4) decode workers;
the original code enqueued to the thread-safe _stagedMeshData directly,
so the hand-off buffer must be thread-safe too. Drain ordering verified:
side-staged entries enqueue BEFORE the top-level result, preserving the
original mid-Prepare FIFO order.

Verified: grep -i silk on the csproj -> no matches; deps.json has zero
Silk entries; dotnet build 0 errors; full test suite green (4059 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-05 20:27:38 +02:00
parent 30cc1e282e
commit 1477cda60a
5 changed files with 89 additions and 50 deletions

View file

@ -807,7 +807,13 @@ namespace AcDream.App.Rendering.Wb {
atlasList.Add(atlasManager);
}
textureIndex = atlasManager.AddTexture(batch.Key, batch.TextureData, batch.UploadPixelFormat, batch.UploadPixelType);
// MP1a: AcDream.Content is Silk.NET-free — the extraction records
// carry Content-owned UploadPixelFormat/UploadPixelType enums whose
// underlying values are the GL ABI constants (numerically identical
// to Silk.NET.OpenGL.PixelFormat/PixelType), so this lifted nullable
// cast is value- and null-preserving.
textureIndex = atlasManager.AddTexture(batch.Key, batch.TextureData,
(PixelFormat?)batch.UploadPixelFormat, (PixelType?)batch.UploadPixelType);
if (_useModernRendering) {
ibo = GlobalBuffer!.IBO;

View file

@ -12,14 +12,12 @@
exact versions (mirror AcDream.App.csproj — keep in lockstep). -->
<PackageReference Include="BCnEncoder.Net.ImageSharp" Version="1.1.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<!-- MP1a (Task 3, compiler-demanded): MeshBatchData/TextureBatchData carry
Silk.NET.OpenGL.PixelFormat?/PixelType? upload-format hints computed
during CPU extraction (which format to hand the eventual GL upload).
These are plain enums with no GL context/device/unsafe surface — the
plan's own boundary survey says the only GL-DOMAIN reference in the
Prepare* region is TextureAtlasManager.TextureKey; this is the same
class of dependency (a data-only enum), not a functional GL call. -->
<PackageReference Include="Silk.NET.OpenGL" Version="2.23.0" />
<!-- NO GL binding packages here — ever. This assembly is the GL-free
extraction half; the MP1b bake tool references it and must not ship
GL binaries. Upload-format hints use the Content-owned
UploadPixelFormat / UploadPixelType enums (UploadFormats.cs),
numerically identical to the GL ABI constants; App casts at the
upload boundary. -->
</ItemGroup>
<ItemGroup>

View file

@ -15,8 +15,6 @@ using System.Linq;
using System.Numerics;
using System.Threading;
using CullMode = DatReaderWriter.Enums.CullMode;
using PixelFormat = Silk.NET.OpenGL.PixelFormat;
using PixelType = Silk.NET.OpenGL.PixelType;
using BoundingBox = Chorizite.Core.Lib.BoundingBox;
namespace AcDream.Content;
@ -50,8 +48,12 @@ public sealed class MeshExtractor {
/// <see cref="DrainSideStaged"/> immediately after each Prepare* call and
/// enqueues the results itself. No behavior change — same objects reach
/// the same queue, just via an explicit hand-off instead of a shared field.
/// ConcurrentQueue because one MeshExtractor is shared by up to
/// MaxParallelLoads (4) decode workers — the original enqueued to the
/// thread-safe _stagedMeshData, so the hand-off buffer must be
/// thread-safe too.
/// </summary>
private readonly List<ObjectMeshData> _sideStaged = new();
private readonly ConcurrentQueue<ObjectMeshData> _sideStaged = new();
public MeshExtractor(IDatReaderWriter dats, ILogger logger) {
_dats = dats;
@ -64,9 +66,10 @@ public sealed class MeshExtractor {
/// <see cref="_sideStaged"/>.
/// </summary>
public List<ObjectMeshData> DrainSideStaged() {
if (_sideStaged.Count == 0) return new List<ObjectMeshData>();
var result = new List<ObjectMeshData>(_sideStaged);
_sideStaged.Clear();
var result = new List<ObjectMeshData>();
while (_sideStaged.TryDequeue(out var item)) {
result.Add(item);
}
return result;
}
@ -173,13 +176,13 @@ public sealed class MeshExtractor {
if (emitter.HwGfxObjId.DataId != 0) {
var meshData = PrepareMeshData(emitter.HwGfxObjId.DataId, false, ct);
if (meshData != null) {
_sideStaged.Add(meshData);
_sideStaged.Enqueue(meshData);
}
}
if (emitter.GfxObjId.DataId != 0 && emitter.GfxObjId.DataId != emitter.HwGfxObjId.DataId) {
var meshData = PrepareMeshData(emitter.GfxObjId.DataId, false, ct);
if (meshData != null) {
_sideStaged.Add(meshData);
_sideStaged.Enqueue(meshData);
}
}
}
@ -368,8 +371,8 @@ public sealed class MeshExtractor {
int texWidth, texHeight;
byte[] textureData;
TextureFormat textureFormat;
PixelFormat? uploadPixelFormat = null;
PixelType? uploadPixelType = null;
UploadPixelFormat? uploadPixelFormat = null;
UploadPixelType? uploadPixelType = null;
bool isSolid = poly.Stippling.HasFlag(StipplingType.NoPos) || surface.Type.HasFlag(SurfaceType.Base1Solid);
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
uint paletteId = 0;
@ -382,7 +385,7 @@ public sealed class MeshExtractor {
texWidth = texHeight = 32;
textureData = TextureHelpers.CreateSolidColorTexture(surface.ColorValue, texWidth, texHeight);
textureFormat = TextureFormat.RGBA8;
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
}
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
var renderSurfaceId = surfaceTexture.Textures.First();
@ -403,7 +406,7 @@ public sealed class MeshExtractor {
if (TextureHelpers.IsCompressedFormat(renderSurface.Format)) {
isDxt3or5 = renderSurface.Format == DatReaderWriter.Enums.PixelFormat.PFID_DXT3 || renderSurface.Format == DatReaderWriter.Enums.PixelFormat.PFID_DXT5;
textureFormat = TextureFormat.RGBA8;
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
if (_decodedTextureCache.TryGetValue(renderSurfaceId, out textureData!)) {
// use cached data
@ -446,36 +449,36 @@ public sealed class MeshExtractor {
case DatReaderWriter.Enums.PixelFormat.PFID_A8R8G8B8:
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillA8R8G8B8(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_R8G8B8:
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillR8G8B8(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_INDEX16:
if (!_dats.Portal.TryGet<Palette>(renderSurface.DefaultPaletteId, out var paletteData))
throw new Exception($"Unable to load Palette: 0x{renderSurface.DefaultPaletteId:X8}");
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillIndex16(renderSurface.SourceData, paletteData, textureData.AsSpan(), texWidth, texHeight, isClipMap);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_P8:
if (!_dats.Portal.TryGet<Palette>(renderSurface.DefaultPaletteId, out var p8PaletteData))
throw new Exception($"Unable to load Palette: 0x{renderSurface.DefaultPaletteId:X8}");
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillP8(renderSurface.SourceData, p8PaletteData, textureData.AsSpan(), texWidth, texHeight, isClipMap);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_R5G6B5:
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillR5G6B5(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_A4R4G4B4:
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillA4R4G4B4(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_A8:
case DatReaderWriter.Enums.PixelFormat.PFID_CUSTOM_LSCAPE_ALPHA:
@ -486,7 +489,7 @@ public sealed class MeshExtractor {
else {
TextureHelpers.FillA8(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
}
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
default:
throw new NotSupportedException($"Unsupported surface format: {renderSurface.Format}");
@ -722,8 +725,8 @@ public sealed class MeshExtractor {
int texWidth, texHeight;
byte[] textureData;
TextureFormat textureFormat;
PixelFormat? uploadPixelFormat = null;
PixelType? uploadPixelType = null;
UploadPixelFormat? uploadPixelFormat = null;
UploadPixelType? uploadPixelType = null;
bool isSolid = poly.Stippling.HasFlag(StipplingType.NoPos) || surface.Type.HasFlag(SurfaceType.Base1Solid);
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
uint paletteId = 0;
@ -736,7 +739,7 @@ public sealed class MeshExtractor {
texWidth = texHeight = 32;
textureData = TextureHelpers.CreateSolidColorTexture(surface.ColorValue, texWidth, texHeight);
textureFormat = TextureFormat.RGBA8;
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
}
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
var renderSurfaceId = surfaceTexture.Textures.First();
@ -757,13 +760,13 @@ public sealed class MeshExtractor {
if (_decodedTextureCache.TryGetValue(renderSurfaceId, out var cachedData)) {
textureData = cachedData;
textureFormat = TextureFormat.RGBA8;
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
}
else {
if (TextureHelpers.IsCompressedFormat(renderSurface.Format)) {
isDxt3or5 = renderSurface.Format == DatReaderWriter.Enums.PixelFormat.PFID_DXT3 || renderSurface.Format == DatReaderWriter.Enums.PixelFormat.PFID_DXT5;
textureFormat = TextureFormat.RGBA8;
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
textureData = new byte[texWidth * texHeight * 4];
CompressionFormat compressionFormat = renderSurface.Format switch {
@ -784,34 +787,34 @@ public sealed class MeshExtractor {
case DatReaderWriter.Enums.PixelFormat.PFID_A8R8G8B8:
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillA8R8G8B8(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_R8G8B8:
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillR8G8B8(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_INDEX16:
if (!_dats.Portal.TryGet<Palette>(renderSurface.DefaultPaletteId, out var paletteData)) return;
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillIndex16(renderSurface.SourceData, paletteData, textureData.AsSpan(), texWidth, texHeight, isClipMap);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_P8:
if (!_dats.Portal.TryGet<Palette>(renderSurface.DefaultPaletteId, out var p8PaletteData)) return;
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillP8(renderSurface.SourceData, p8PaletteData, textureData.AsSpan(), texWidth, texHeight, isClipMap);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_R5G6B5:
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillR5G6B5(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_A4R4G4B4:
textureData = new byte[texWidth * texHeight * 4];
TextureHelpers.FillA4R4G4B4(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
case DatReaderWriter.Enums.PixelFormat.PFID_A8:
case DatReaderWriter.Enums.PixelFormat.PFID_CUSTOM_LSCAPE_ALPHA:
@ -822,7 +825,7 @@ public sealed class MeshExtractor {
else {
TextureHelpers.FillA8(renderSurface.SourceData, textureData.AsSpan(), texWidth, texHeight);
}
uploadPixelFormat = PixelFormat.Rgba;
uploadPixelFormat = UploadPixelFormat.Rgba;
break;
default: return;
}
@ -1144,8 +1147,8 @@ public sealed class MeshExtractor {
},
TextureIndex = 0,
TextureData = transparentTexture,
UploadPixelFormat = PixelFormat.Rgba,
UploadPixelType = PixelType.UnsignedByte,
UploadPixelFormat = UploadPixelFormat.Rgba,
UploadPixelType = UploadPixelType.UnsignedByte,
CullMode = CullMode.None
}
},
@ -1161,8 +1164,8 @@ public sealed class MeshExtractor {
IsSolid = true
},
TextureData = transparentTexture,
UploadPixelFormat = PixelFormat.Rgba,
UploadPixelType = PixelType.UnsignedByte,
UploadPixelFormat = UploadPixelFormat.Rgba,
UploadPixelType = UploadPixelType.UnsignedByte,
CullMode = CullMode.None,
IsTransparent = false // Render in opaque pass but transparent
}

View file

@ -7,8 +7,6 @@ using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using BoundingBox = Chorizite.Core.Lib.BoundingBox;
using PixelFormat = Silk.NET.OpenGL.PixelFormat;
using PixelType = Silk.NET.OpenGL.PixelType;
namespace AcDream.Content;
@ -104,8 +102,8 @@ public class MeshBatchData {
public TextureKey TextureKey { get; set; }
public int TextureIndex { get; set; }
public byte[] TextureData { get; set; } = Array.Empty<byte>();
public PixelFormat? UploadPixelFormat { get; set; }
public PixelType? UploadPixelType { get; set; }
public UploadPixelFormat? UploadPixelFormat { get; set; }
public UploadPixelType? UploadPixelType { get; set; }
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
}
@ -115,8 +113,8 @@ public class MeshBatchData {
public class TextureBatchData {
public TextureKey Key { get; set; }
public byte[] TextureData { get; set; } = Array.Empty<byte>();
public PixelFormat? UploadPixelFormat { get; set; }
public PixelType? UploadPixelType { get; set; }
public UploadPixelFormat? UploadPixelFormat { get; set; }
public UploadPixelType? UploadPixelType { get; set; }
public List<ushort> Indices { get; set; } = new();
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
public bool IsTransparent { get; set; }

View file

@ -0,0 +1,34 @@
namespace AcDream.Content;
// MP1a follow-up (2026-07-05): Content-owned upload-format hint enums.
// AcDream.Content must stay Silk.NET-free (the MP1b bake tool must not ship
// GL binaries), so the moved MeshBatchData/TextureBatchData records carry
// these instead of Silk.NET.OpenGL.PixelFormat/PixelType. The underlying
// values are the OpenGL ABI constants, kept numerically identical to the
// corresponding Silk.NET.OpenGL members (verified against Silk.NET.OpenGL
// 2.23.0) so the App-boundary cast
// `(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` is
// value-preserving. GL enum constants are a stable specification ABI —
// they cannot drift between Silk.NET versions.
//
// Members are ONLY the values the extraction code actually assigns
// (see MeshExtractor.cs). Add new members with their GL constant if a
// future decode path needs them — never renumber.
/// <summary>
/// GL pixel-format upload hint computed at extraction time.
/// Numeric values = OpenGL <c>GL_*</c> constants (= Silk.NET.OpenGL.PixelFormat).
/// </summary>
public enum UploadPixelFormat {
/// <summary>GL_RGBA.</summary>
Rgba = 0x1908,
}
/// <summary>
/// GL pixel-type upload hint computed at extraction time.
/// Numeric values = OpenGL <c>GL_*</c> constants (= Silk.NET.OpenGL.PixelType).
/// </summary>
public enum UploadPixelType {
/// <summary>GL_UNSIGNED_BYTE.</summary>
UnsignedByte = 0x1401,
}