diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
index 27d6d0fc..748d6f35 100644
--- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
+++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
@@ -19,11 +19,6 @@ using AcDream.Content;
using AcDream.Core.Rendering.Wb;
using PixelFormat = Silk.NET.OpenGL.PixelFormat;
using BoundingBox = Chorizite.Core.Lib.BoundingBox;
-using BCnEncoder.Decoder;
-using BCnEncoder.Shared;
-using BCnEncoder.ImageSharp;
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.PixelFormats;
namespace AcDream.App.Rendering.Wb {
///
diff --git a/src/AcDream.Content/AcDream.Content.csproj b/src/AcDream.Content/AcDream.Content.csproj
index e54e275e..93c7dbd2 100644
--- a/src/AcDream.Content/AcDream.Content.csproj
+++ b/src/AcDream.Content/AcDream.Content.csproj
@@ -4,6 +4,8 @@
net10.0
enable
enable
+ latest
+ true
true
diff --git a/src/AcDream.Content/IDatReaderWriter.cs b/src/AcDream.Content/IDatReaderWriter.cs
index fc63b11f..164bb42f 100644
--- a/src/AcDream.Content/IDatReaderWriter.cs
+++ b/src/AcDream.Content/IDatReaderWriter.cs
@@ -5,15 +5,14 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
// Phase O-T7: verbatim copy of WorldBuilder.Shared.Services.IDatReaderWriter +
-// IDatDatabase into the AcDream.App.Rendering.Wb namespace so the
-// WorldBuilder.Shared project reference can be dropped.
-// The only consumer of IDatReaderWriter in acdream is DatCollectionAdapter +
-// ObjectMeshManager, both already in this namespace.
+// IDatDatabase, taken into our tree so the WorldBuilder.Shared project
+// reference could be dropped.
//
// MP1a (2026-07-05, Task 4): moved to AcDream.Content, namespace only —
// MeshExtractor's constructor takes IDatReaderWriter and must be GL-free.
+// Consumers: MeshExtractor (this assembly) reads through it;
// DatCollectionAdapter (the concrete DatCollection-backed implementation)
-// stays in AcDream.App; it now implements this Content-namespaced interface.
+// and ObjectMeshManager live in AcDream.App and implement/hold it from there.
namespace AcDream.Content;
diff --git a/src/AcDream.Content/MeshExtractor.cs b/src/AcDream.Content/MeshExtractor.cs
index 5117cb85..bc728132 100644
--- a/src/AcDream.Content/MeshExtractor.cs
+++ b/src/AcDream.Content/MeshExtractor.cs
@@ -49,10 +49,14 @@ public sealed class MeshExtractor {
/// The MP1b bake tool passes its own collector. The sink must be
/// thread-safe: one MeshExtractor is shared by up to MaxParallelLoads (4)
/// decode workers.
+ /// The constructor argument is REQUIRED (no default): a bake tool that
+ /// forgot the sink would silently lose particle-preload meshes; requiring
+ /// the argument forces the decision. The type stays nullable so a caller
+ /// can consciously pass null when preloads are irrelevant.
///
private readonly Action? _sideStagedSink;
- public MeshExtractor(IDatReaderWriter dats, ILogger logger, Action? sideStagedSink = null) {
+ public MeshExtractor(IDatReaderWriter dats, ILogger logger, Action? sideStagedSink) {
_dats = dats;
_logger = logger;
_sideStagedSink = sideStagedSink;
@@ -120,7 +124,7 @@ public sealed class MeshExtractor {
}
}
- public ObjectMeshData? PrepareSetupMeshData(ulong id, Setup setup, CancellationToken ct) {
+ private ObjectMeshData? PrepareSetupMeshData(ulong id, Setup setup, CancellationToken ct) {
var parts = new List<(ulong GfxObjId, Matrix4x4 Transform)>();
var min = new Vector3(float.MaxValue);
var max = new Vector3(float.MinValue);
@@ -146,7 +150,7 @@ public sealed class MeshExtractor {
};
}
- public void CollectEmittersFromScript(uint scriptId, List emitters, CancellationToken ct) {
+ private void CollectEmittersFromScript(uint scriptId, List emitters, CancellationToken ct) {
if (_dats.Portal.TryGet(scriptId, out var script)) {
foreach (var hook in script.ScriptData) {
if (hook.Hook.HookType == AnimationHookType.CreateParticle && hook.Hook is CreateParticleHook particleHook) {
@@ -295,7 +299,7 @@ public sealed class MeshExtractor {
}
}
- public ObjectMeshData? PrepareGfxObjMeshData(ulong id, GfxObj gfxObj, Vector3 scale, CancellationToken ct) {
+ private ObjectMeshData? PrepareGfxObjMeshData(ulong id, GfxObj gfxObj, Vector3 scale, CancellationToken ct) {
var vertices = new List();
var UVLookup = new Dictionary<(ushort vertId, ushort uvIdx, bool isNeg), ushort>();
var batchesByFormat = new Dictionary<(int Width, int Height, TextureFormat Format), List>();
@@ -562,7 +566,7 @@ public sealed class MeshExtractor {
};
}
- public ObjectMeshData? PrepareEnvCellMeshData(ulong id, EnvCell envCell, CancellationToken ct) {
+ private ObjectMeshData? PrepareEnvCellMeshData(ulong id, EnvCell envCell, CancellationToken ct) {
var parts = new List<(ulong GfxObjId, Matrix4x4 Transform)>();
var min = new Vector3(float.MaxValue);
var max = new Vector3(float.MinValue);
@@ -1069,7 +1073,7 @@ public sealed class MeshExtractor {
return (min, max);
}
- public ObjectMeshData? PrepareCellStructEdgeLineData(ulong id, Dictionary cellStructs, Matrix4x4 transform, CancellationToken ct) {
+ private ObjectMeshData? PrepareCellStructEdgeLineData(ulong id, Dictionary cellStructs, Matrix4x4 transform, CancellationToken ct) {
var cellStructList = cellStructs.ToList();
if (cellStructList.Count == 0) {
return null;
diff --git a/src/AcDream.Content/ObjectMeshData.cs b/src/AcDream.Content/ObjectMeshData.cs
index 49089475..b5f7d5b0 100644
--- a/src/AcDream.Content/ObjectMeshData.cs
+++ b/src/AcDream.Content/ObjectMeshData.cs
@@ -48,15 +48,16 @@ public class ObjectMeshData {
public List Batches { get; set; } = new();
///
- /// #125 (2026-06-12): GL upload-retry counter. A failed
- /// (returns null from its
+ /// #125 (2026-06-12): GL upload-retry counter. A failed upload through
+ /// the App-side upload path (it returns null from its
/// catch) used to be dropped permanently — the staged item was consumed,
/// no render data was produced, and the prepared data lingered in the CPU
/// cache where PrepareMeshDataAsync's cache-hit short-circuit
/// returned it without ever re-staging it for upload (session-sticky
/// invisible mesh, one [wb-error] line). The drain loop now re-stages a
- /// failed upload for the NEXT frame up to times. The counter lives on the mesh-data object so
+ /// failed upload for the NEXT frame up to the App-side upload retry
+ /// limit (MaxUploadRetries). The counter lives on the mesh-data
+ /// object so
/// it resets to 0 naturally whenever the id is re-prepared (fresh object),
/// and bounds a deterministic GL failure to a few loud lines instead of a
/// silent permanent drop OR an unbounded per-frame retry storm. Retail
diff --git a/tests/AcDream.Core.Tests/Conformance/Issue119UpNullGfxObjDumpTests.cs b/tests/AcDream.Core.Tests/Conformance/Issue119UpNullGfxObjDumpTests.cs
index be5e4413..0b123199 100644
--- a/tests/AcDream.Core.Tests/Conformance/Issue119UpNullGfxObjDumpTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/Issue119UpNullGfxObjDumpTests.cs
@@ -16,9 +16,11 @@ namespace AcDream.Core.Tests.Conformance;
/// invisible)` at startup (t5-gate-launch.log:33-34); the old tower shows
/// missing stair parts (visible in retail — user axiom). UploadGfxObjMeshData
/// returns null only when the PREPARE phase produced ZERO vertices
-/// (ObjectMeshManager.cs:1780), so the upload is innocent — some extraction
+/// (ObjectMeshManager.UploadGfxObjMeshData's empty-vertices guard), so the
+/// upload is innocent — some extraction
/// gate dropped every polygon. This dump prints the raw dat facts per polygon
-/// and replicates PrepareGfxObjMeshData's gates (ObjectMeshManager.cs:1040-1058)
+/// and replicates MeshExtractor.PrepareGfxObjMeshData's gates (moved from
+/// ObjectMeshManager in MP1a)
/// so the zeroing gate reads directly off the output.
///
public sealed class Issue119UpNullGfxObjDumpTests
diff --git a/tests/AcDream.Core.Tests/Conformance/StipplingSurfaceEquivalenceTests.cs b/tests/AcDream.Core.Tests/Conformance/StipplingSurfaceEquivalenceTests.cs
index 721a0ca1..db553c80 100644
--- a/tests/AcDream.Core.Tests/Conformance/StipplingSurfaceEquivalenceTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/StipplingSurfaceEquivalenceTests.cs
@@ -16,8 +16,8 @@ namespace AcDream.Core.Tests.Conformance;
/// (0x2) and BASE1_CLIPMAP (0x4) are skipped (D3DPolyRender inner draw,
/// Ghidra 0x0059d4a0; default on @0x00820e30). acdream suppresses them at
/// BUILD time via Stippling.NoPos in all four extraction paths
-/// (ObjectMeshManager.PrepareGfxObjMeshData:1046 + PrepareCellStructMeshData
-/// :1394, CellMesh.Build:44, GfxObjMesh.Build:71).
+/// (MeshExtractor.PrepareGfxObjMeshData + PrepareCellStructMeshData
+/// [moved from ObjectMeshManager in MP1a], CellMesh.Build:44, GfxObjMesh.Build:71).
///
/// These criteria are equivalent ONLY if NoPos ⇔ untextured-surface holds on
/// the content. This sweep pins both directions across the populated