fix(pipeline): MP1a - sink delegate restores immediate side-stage enqueue (exception-path faithfulness)
Coordinator-directed follow-up. The buffer-and-drain seam diverged from the original on the exception path: pre-MP1a, CollectEmittersFromScript enqueued particle-preload meshes DIRECTLY into _stagedMeshData mid-Prepare, so preloads staged before a later throw in the same Prepare* call (reachable via PrepareEnvCellMeshData side-staging during its StaticObjects loop, then PrepareCellStructMeshData throwing on a malformed-dat texture decode) were already safely enqueued. The drain version only flushed after a successful return — on throw, entries stranded on the shared extractor until an unrelated successful call flushed them, and were silently dropped on dispose. Fix: MeshExtractor takes an Action<ObjectMeshData>? sideStagedSink constructor parameter; the two CollectEmittersFromScript sites become _sideStagedSink?.Invoke(meshData) — the original code shape (immediate hand-off) at those exact lines. ObjectMeshManager wires the sink to _stagedMeshData.Enqueue, restoring the original immediate-enqueue semantics including on mid-Prepare throw. _sideStaged buffer, DrainSideStaged(), and the ProcessQueueAsync drain loop are deleted. The MP1b bake tool passes its own collector. Inventory doc updated: MP1a note now records the sink seam and the Content-owned upload enums, so its no-behavior-change claim is accurate. dotnet build green; full test suite 4059 passed / 0 failed / 4 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5722681bf5
commit
932f904e00
3 changed files with 39 additions and 37 deletions
|
|
@ -53,8 +53,26 @@ behavior change, no divergence-register row.
|
||||||
- `src/AcDream.Content/TextureKey.cs` — the atlas dedup key, lifted out of
|
- `src/AcDream.Content/TextureKey.cs` — the atlas dedup key, lifted out of
|
||||||
the GL-owning `TextureAtlasManager` (which stays in App and now
|
the GL-owning `TextureAtlasManager` (which stays in App and now
|
||||||
references the lifted struct).
|
references the lifted struct).
|
||||||
|
- `src/AcDream.Content/UploadFormats.cs` — Content-owned
|
||||||
|
`UploadPixelFormat`/`UploadPixelType` enums carried by
|
||||||
|
`MeshBatchData`/`TextureBatchData` instead of
|
||||||
|
`Silk.NET.OpenGL.PixelFormat`/`PixelType` (Content must stay
|
||||||
|
Silk.NET-free — the bake tool must not ship GL binaries). Underlying
|
||||||
|
values are the GL ABI constants, numerically identical to the Silk.NET
|
||||||
|
members; App casts at its single upload boundary (the `AddTexture` call
|
||||||
|
in `UploadGfxObjMeshData`) via a lifted nullable enum conversion —
|
||||||
|
value- and null-preserving.
|
||||||
- `src/AcDream.Content/IDatReaderWriter.cs`, `EdgeLineBuilder.cs` — GL-free
|
- `src/AcDream.Content/IDatReaderWriter.cs`, `EdgeLineBuilder.cs` — GL-free
|
||||||
dependencies of the extractor, moved (namespace-only) alongside it.
|
dependencies of the extractor, moved (namespace-only) alongside it.
|
||||||
|
- **Side-stage sink seam:** `CollectEmittersFromScript` pre-loads particle
|
||||||
|
GfxObj meshes mid-extraction and, pre-MP1a, enqueued them directly onto
|
||||||
|
`ObjectMeshManager._stagedMeshData`. The extractor now takes an
|
||||||
|
`Action<ObjectMeshData>? sideStagedSink` constructor parameter; App wires
|
||||||
|
it to `_stagedMeshData.Enqueue`, preserving the original
|
||||||
|
immediate-enqueue semantics exactly — including on a mid-`Prepare*`
|
||||||
|
throw (preloads staged before a malformed-dat texture-decode exception
|
||||||
|
survive, as they always did). The MP1b bake tool passes its own
|
||||||
|
collector.
|
||||||
- **Stays in `src/AcDream.App/Rendering/Wb/`:** `ObjectMeshManager` (now a
|
- **Stays in `src/AcDream.App/Rendering/Wb/`:** `ObjectMeshManager` (now a
|
||||||
thin wrapper owning the staged-queue/worker-pool/Dispose-quiesce lifecycle
|
thin wrapper owning the staged-queue/worker-pool/Dispose-quiesce lifecycle
|
||||||
and all GL upload — constructs one `MeshExtractor` and delegates every
|
and all GL upload — constructs one `MeshExtractor` and delegates every
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,12 @@ namespace AcDream.App.Rendering.Wb {
|
||||||
_graphicsDevice = graphicsDevice;
|
_graphicsDevice = graphicsDevice;
|
||||||
_dats = dats;
|
_dats = dats;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_extractor = new MeshExtractor(_dats, _logger);
|
// Side-stage sink: particle-preload meshes staged mid-extraction go
|
||||||
|
// straight onto the staged-upload queue, exactly as the pre-MP1a code
|
||||||
|
// did — immediate enqueue, surviving a later throw in the same
|
||||||
|
// Prepare* call. ConcurrentQueue.Enqueue is thread-safe for the
|
||||||
|
// extractor's up-to-4 concurrent decode workers.
|
||||||
|
_extractor = new MeshExtractor(_dats, _logger, data => _stagedMeshData.Enqueue(data));
|
||||||
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
||||||
if (_useModernRendering) {
|
if (_useModernRendering) {
|
||||||
GlobalBuffer = new GlobalMeshBuffer(_graphicsDevice.GL);
|
GlobalBuffer = new GlobalMeshBuffer(_graphicsDevice.GL);
|
||||||
|
|
@ -475,12 +480,6 @@ namespace AcDream.App.Rendering.Wb {
|
||||||
// If it's a direct setup or gfxobj, make sure background loads don't abort half-way
|
// If it's a direct setup or gfxobj, make sure background loads don't abort half-way
|
||||||
data = _extractor.PrepareMeshData(id, isSetup, CancellationToken.None);
|
data = _extractor.PrepareMeshData(id, isSetup, CancellationToken.None);
|
||||||
}
|
}
|
||||||
// MP1a: drain any mesh data the extractor staged as a side effect
|
|
||||||
// (particle emitter GfxObj preloads inside CollectEmittersFromScript)
|
|
||||||
// and enqueue it the same way the top-level result is enqueued below.
|
|
||||||
foreach (var sideStaged in _extractor.DrainSideStaged()) {
|
|
||||||
_stagedMeshData.Enqueue(sideStaged);
|
|
||||||
}
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
lock (_cpuMeshCache) {
|
lock (_cpuMeshCache) {
|
||||||
if (_cpuMeshCache.Count >= _maxCpuCacheSize) {
|
if (_cpuMeshCache.Count >= _maxCpuCacheSize) {
|
||||||
|
|
|
||||||
|
|
@ -39,38 +39,23 @@ public sealed class MeshExtractor {
|
||||||
private readonly ThreadLocal<BcDecoder> _bcDecoder = new(() => new BcDecoder());
|
private readonly ThreadLocal<BcDecoder> _bcDecoder = new(() => new BcDecoder());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MP1a mechanical seam: retail's particle-preload side effect (see
|
/// MP1a mechanical seam: receives particle-preload meshes staged
|
||||||
/// <see cref="CollectEmittersFromScript"/>) used to enqueue directly onto
|
/// mid-extraction (see <see cref="CollectEmittersFromScript"/>). The App
|
||||||
/// ObjectMeshManager's runtime staged-upload queue (<c>_stagedMeshData</c>).
|
/// wires this to its staged-upload queue, restoring the original
|
||||||
/// That queue is a GL-upload-lifecycle concern and stays in App per the
|
/// immediate-enqueue semantics — entries must survive a subsequent throw
|
||||||
/// plan's binding rules, so the extractor collects the same side-effect
|
/// in the same Prepare* call (retail's code enqueued directly onto
|
||||||
/// output here instead; ObjectMeshManager drains it via
|
/// ObjectMeshManager's <c>_stagedMeshData</c> mid-Prepare, so preloads
|
||||||
/// <see cref="DrainSideStaged"/> immediately after each Prepare* call and
|
/// staged before a malformed-dat texture-decode throw were already safe).
|
||||||
/// enqueues the results itself. No behavior change — same objects reach
|
/// The MP1b bake tool passes its own collector. The sink must be
|
||||||
/// the same queue, just via an explicit hand-off instead of a shared field.
|
/// thread-safe: one MeshExtractor is shared by up to MaxParallelLoads (4)
|
||||||
/// ConcurrentQueue because one MeshExtractor is shared by up to
|
/// decode workers.
|
||||||
/// MaxParallelLoads (4) decode workers — the original enqueued to the
|
|
||||||
/// thread-safe _stagedMeshData, so the hand-off buffer must be
|
|
||||||
/// thread-safe too.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentQueue<ObjectMeshData> _sideStaged = new();
|
private readonly Action<ObjectMeshData>? _sideStagedSink;
|
||||||
|
|
||||||
public MeshExtractor(IDatReaderWriter dats, ILogger logger) {
|
public MeshExtractor(IDatReaderWriter dats, ILogger logger, Action<ObjectMeshData>? sideStagedSink = null) {
|
||||||
_dats = dats;
|
_dats = dats;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
_sideStagedSink = sideStagedSink;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Drains and returns any mesh data staged as a side effect of the most
|
|
||||||
/// recent Prepare* call (particle emitter GfxObj preloads). See
|
|
||||||
/// <see cref="_sideStaged"/>.
|
|
||||||
/// </summary>
|
|
||||||
public List<ObjectMeshData> DrainSideStaged() {
|
|
||||||
var result = new List<ObjectMeshData>();
|
|
||||||
while (_sideStaged.TryDequeue(out var item)) {
|
|
||||||
result.Add(item);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -176,13 +161,13 @@ public sealed class MeshExtractor {
|
||||||
if (emitter.HwGfxObjId.DataId != 0) {
|
if (emitter.HwGfxObjId.DataId != 0) {
|
||||||
var meshData = PrepareMeshData(emitter.HwGfxObjId.DataId, false, ct);
|
var meshData = PrepareMeshData(emitter.HwGfxObjId.DataId, false, ct);
|
||||||
if (meshData != null) {
|
if (meshData != null) {
|
||||||
_sideStaged.Enqueue(meshData);
|
_sideStagedSink?.Invoke(meshData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (emitter.GfxObjId.DataId != 0 && emitter.GfxObjId.DataId != emitter.HwGfxObjId.DataId) {
|
if (emitter.GfxObjId.DataId != 0 && emitter.GfxObjId.DataId != emitter.HwGfxObjId.DataId) {
|
||||||
var meshData = PrepareMeshData(emitter.GfxObjId.DataId, false, ct);
|
var meshData = PrepareMeshData(emitter.GfxObjId.DataId, false, ct);
|
||||||
if (meshData != null) {
|
if (meshData != null) {
|
||||||
_sideStaged.Enqueue(meshData);
|
_sideStagedSink?.Invoke(meshData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue