Merge branch 'claude/quirky-jepsen-fd60f1' — N.4 Tasks 1-8
This commit is contained in:
commit
b1d48fac94
11 changed files with 566 additions and 1 deletions
|
|
@ -66,6 +66,22 @@ This plan is the **execution source of truth** for N.4. It is updated as tasks l
|
|||
|
||||
Status: **Living document — work in progress, started 2026-05-08.**
|
||||
|
||||
**Progress (2026-05-08):** Tasks 1-8 ✅ complete. Tasks 1-5 landed foundation types + WbMeshAdapter stub. Task 6 obsoleted by `DefaultDatReaderWriter` discovery (Adjustment 1). Task 7 wired the adapter into `GameWindow` lifecycle behind the flag. Task 8 CLAUDE.md pointer was done preemptively in commit `506b86b`. **Next: Task 9** — route `InstancedMeshRenderer.EnsureUploaded` through `WbMeshAdapter` when flag is on (first behavioral change; flag-on render path will skip static scenery until Task 22 wires `WbDrawDispatcher`).
|
||||
|
||||
| Task | Status | Commit |
|
||||
|---|---|---|
|
||||
| 1 — WbFoundationFlag scaffold | ✅ | `81b5ed8` |
|
||||
| 2 — AcSurfaceMetadata + Table | ✅ | `46deed6` |
|
||||
| 3 — Mesh-extraction conformance | ✅ | `ed73fc5` |
|
||||
| 4 — Setup-flatten conformance | ✅ | `ed73fc5` (combined with #3) |
|
||||
| 5 — WbMeshAdapter stub + IWbMeshAdapter | ✅ | (post-`ed73fc5`) |
|
||||
| 6 — WbDatReaderAdapter | ✅ OBSOLETED | `502c3a8` |
|
||||
| 7 — GameWindow wiring under flag | ✅ | `502c3a8` |
|
||||
| 8 — CLAUDE.md pointer | ✅ | `506b86b` (preemptive) |
|
||||
| 9 — Route InstancedMeshRenderer through adapter | pending | — |
|
||||
| 10 — Week 1 wrap-up | pending | — |
|
||||
| 11–28 | pending (Weeks 2-4) | — |
|
||||
|
||||
---
|
||||
|
||||
## Week 1 — Plumbing + Atlas for Static Scenery + Conformance
|
||||
|
|
@ -800,7 +816,24 @@ EOF
|
|||
|
||||
---
|
||||
|
||||
### Task 6: `WbDatReaderAdapter` — bridge our `DatCollection` to WB's `IDatReaderWriter`
|
||||
### Task 6: ~~WbDatReaderAdapter~~ — OBSOLETED 2026-05-08
|
||||
|
||||
**Adjustment 1 (2026-05-08):** discovered during pre-Task-6 grep that
|
||||
WB ships `WorldBuilder.Shared.Services.DefaultDatReaderWriter`, a
|
||||
concrete `IDatReaderWriter` implementation that takes a dat-directory
|
||||
path and constructs all four databases (Portal / HighRes / Language +
|
||||
CellRegions) internally. We can instantiate it directly with the same
|
||||
`%USERPROFILE%\Documents\Asheron's Call` path acdream's `DatCollection`
|
||||
uses; both will open the same dat files with separate handles. Memory
|
||||
cost: ~50-100 MB of duplicate index caches, acceptable for foundation
|
||||
work. Task 9 incorporates the construction step directly.
|
||||
|
||||
If memory pressure surfaces during week 2 stress testing, revisit by
|
||||
writing a real bridge that shares index caches with our `DatCollection`.
|
||||
|
||||
**No work for this task — skip and proceed to Task 7.**
|
||||
|
||||
### Task 6 (original — kept for history)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/AcDream.App/Rendering/Wb/WbDatReaderAdapter.cs`
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ public sealed class GameWindow : IDisposable
|
|||
private InstancedMeshRenderer? _staticMesh;
|
||||
private Shader? _meshShader;
|
||||
private TextureCache? _textureCache;
|
||||
/// <summary>Phase N.4: WB-backed rendering pipeline adapter. Non-null only
|
||||
/// when <c>ACDREAM_USE_WB_FOUNDATION=1</c> is set; null otherwise.</summary>
|
||||
private AcDream.App.Rendering.Wb.WbMeshAdapter? _wbMeshAdapter;
|
||||
private SamplerCache? _samplerCache;
|
||||
private DebugLineRenderer? _debugLines;
|
||||
// K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder
|
||||
|
|
@ -1421,6 +1424,19 @@ public sealed class GameWindow : IDisposable
|
|||
// WorldBuilder reference at
|
||||
// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132.
|
||||
_samplerCache = new SamplerCache(_gl);
|
||||
|
||||
// Phase N.4 — WB rendering pipeline foundation. Constructed only when
|
||||
// ACDREAM_USE_WB_FOUNDATION=1 is set; otherwise the legacy renderer
|
||||
// path stays in charge. The full ObjectMeshManager bring-up is
|
||||
// deferred to Task 9 — for now this is a stub adapter that exposes
|
||||
// the public API so call sites can wire without behavioral effect.
|
||||
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled)
|
||||
{
|
||||
var wbLogger = Microsoft.Extensions.Logging.Abstractions.NullLogger<AcDream.App.Rendering.Wb.WbMeshAdapter>.Instance;
|
||||
_wbMeshAdapter = new AcDream.App.Rendering.Wb.WbMeshAdapter(_gl, _dats, wbLogger);
|
||||
Console.WriteLine("[N.4] WbFoundation flag is ENABLED — routing static content through ObjectMeshManager.");
|
||||
}
|
||||
|
||||
_staticMesh = new InstancedMeshRenderer(_gl, _meshShader, _textureCache);
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
|
|
@ -8625,6 +8641,8 @@ public sealed class GameWindow : IDisposable
|
|||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||
_samplerCache?.Dispose();
|
||||
_textureCache?.Dispose();
|
||||
_wbMeshAdapter?.Dispose(); // Phase N.4 WB foundation — null when flag off
|
||||
|
||||
_meshShader?.Dispose();
|
||||
_terrain?.Dispose();
|
||||
_shader?.Dispose();
|
||||
|
|
|
|||
21
src/AcDream.App/Rendering/Wb/AcSurfaceMetadata.cs
Normal file
21
src/AcDream.App/Rendering/Wb/AcSurfaceMetadata.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using AcDream.Core.Meshing;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// AC-specific surface render metadata that WB's <c>MeshBatchData</c>
|
||||
/// doesn't carry. Computed at mesh-extraction time and looked up by the
|
||||
/// draw dispatcher to drive translucency / sky-pass / fog behavior.
|
||||
///
|
||||
/// <para>
|
||||
/// All fields mirror those on today's <see cref="GfxObjSubMesh"/> so
|
||||
/// behavior is preserved bit-for-bit through the migration.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed record AcSurfaceMetadata(
|
||||
TranslucencyKind Translucency,
|
||||
float Luminosity,
|
||||
float Diffuse,
|
||||
float SurfOpacity,
|
||||
bool NeedsUvRepeat,
|
||||
bool DisableFog);
|
||||
27
src/AcDream.App/Rendering/Wb/AcSurfaceMetadataTable.cs
Normal file
27
src/AcDream.App/Rendering/Wb/AcSurfaceMetadataTable.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe side-table mapping <c>(gfxObjId, surfaceIdx)</c> to
|
||||
/// <see cref="AcSurfaceMetadata"/>. Populated when a GfxObj's mesh data
|
||||
/// is extracted; queried at draw time.
|
||||
///
|
||||
/// <para>
|
||||
/// Keyed by <c>(gfxObjId, surfaceIdx)</c> not by WB's runtime batch
|
||||
/// identity because batch objects can be evicted and re-loaded by WB's
|
||||
/// LRU; the (gfxObj, surface) pair is stable across cycles.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class AcSurfaceMetadataTable
|
||||
{
|
||||
private readonly ConcurrentDictionary<(ulong gfxObjId, int surfaceIdx), AcSurfaceMetadata> _table = new();
|
||||
|
||||
public void Add(ulong gfxObjId, int surfaceIdx, AcSurfaceMetadata meta)
|
||||
=> _table[(gfxObjId, surfaceIdx)] = meta;
|
||||
|
||||
public bool TryLookup(ulong gfxObjId, int surfaceIdx, out AcSurfaceMetadata meta)
|
||||
=> _table.TryGetValue((gfxObjId, surfaceIdx), out meta!);
|
||||
|
||||
public void Clear() => _table.Clear();
|
||||
}
|
||||
12
src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs
Normal file
12
src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Mockable interface over <see cref="WbMeshAdapter"/> so adapters that
|
||||
/// drive ref-count lifecycle (e.g. LandblockSpawnAdapter, EntitySpawnAdapter)
|
||||
/// can be unit-tested without a real WB pipeline behind them.
|
||||
/// </summary>
|
||||
public interface IWbMeshAdapter
|
||||
{
|
||||
void IncrementRefCount(ulong id);
|
||||
void DecrementRefCount(ulong id);
|
||||
}
|
||||
22
src/AcDream.App/Rendering/Wb/WbFoundationFlag.cs
Normal file
22
src/AcDream.App/Rendering/Wb/WbFoundationFlag.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Process-lifetime cache of <c>ACDREAM_USE_WB_FOUNDATION</c> env var.
|
||||
/// Read once at static-init time; all consumers import this rather than
|
||||
/// re-reading the env var per call (env-var lookups on Windows are not
|
||||
/// free at hot-path cadence).
|
||||
///
|
||||
/// <para>
|
||||
/// Set <c>ACDREAM_USE_WB_FOUNDATION=1</c> to route static-scenery + atlas
|
||||
/// content through WB's <c>ObjectMeshManager</c>; per-instance customized
|
||||
/// content (server <c>CreateObject</c> entities) takes the existing
|
||||
/// <see cref="TextureCache.GetOrUploadWithPaletteOverride"/> path either
|
||||
/// way. Flag becomes default-on at end of Phase N.4 after visual
|
||||
/// verification.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class WbFoundationFlag
|
||||
{
|
||||
public static bool IsEnabled { get; } =
|
||||
System.Environment.GetEnvironmentVariable("ACDREAM_USE_WB_FOUNDATION") == "1";
|
||||
}
|
||||
72
src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
Normal file
72
src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using DatReaderWriter;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Single seam between acdream and WB's render pipeline. Owns the
|
||||
/// <c>ObjectMeshManager</c> instance (when fully initialized) and exposes
|
||||
/// a stable acdream-shaped API so the rest of the renderer doesn't need
|
||||
/// to know about WB's types directly.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Phase N.4 staging:</b> currently a stub. Real <c>ObjectMeshManager</c>
|
||||
/// + <c>OpenGLGraphicsDevice</c> initialization is added in Task 9 once
|
||||
/// the dat-reader adapter (Task 6) lands. Until then, methods no-op so
|
||||
/// call sites can wire the adapter without behavioral effect when the
|
||||
/// <see cref="WbFoundationFlag.IsEnabled"/> flag is on.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||
{
|
||||
// _meshManager and _graphicsDevice will be wired in Task 9 once
|
||||
// WbDatReaderAdapter (Task 6) lands. For now, both are null and all
|
||||
// methods no-op.
|
||||
// private ObjectMeshManager? _meshManager;
|
||||
// private OpenGLGraphicsDevice? _graphicsDevice;
|
||||
private bool _disposed;
|
||||
|
||||
public WbMeshAdapter(GL gl, DatCollection dats, ILogger<WbMeshAdapter> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
// TODO(N.4 Task 9): construct OpenGLGraphicsDevice and ObjectMeshManager
|
||||
// once WbDatReaderAdapter (Task 6) is available to bridge our DatCollection
|
||||
// to WB's IDatReaderWriter.
|
||||
}
|
||||
|
||||
private WbMeshAdapter()
|
||||
{
|
||||
// Uninitialized constructor — only for tests / for cases where the
|
||||
// flag is off and the caller wants a Dispose-safe no-op instance.
|
||||
}
|
||||
|
||||
/// <summary>Test/init helper — produces a Dispose-safe instance with no
|
||||
/// underlying mesh manager. Public methods are all no-ops.</summary>
|
||||
public static WbMeshAdapter CreateUninitialized() => new();
|
||||
|
||||
/// <summary>Returns null until Task 9 wires up the real mesh manager.</summary>
|
||||
public object? GetRenderData(ulong id) => null;
|
||||
|
||||
public void IncrementRefCount(ulong id)
|
||||
{
|
||||
// No-op until Task 9.
|
||||
}
|
||||
|
||||
public void DecrementRefCount(ulong id)
|
||||
{
|
||||
// No-op until Task 9.
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
// _meshManager?.Dispose();
|
||||
// _graphicsDevice?.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Meshing;
|
||||
|
||||
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||
|
||||
public sealed class AcSurfaceMetadataTableTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_ThenLookup_RoundTripsSameMetadata()
|
||||
{
|
||||
var table = new AcSurfaceMetadataTable();
|
||||
var meta = new AcSurfaceMetadata(
|
||||
Translucency: TranslucencyKind.AlphaBlend,
|
||||
Luminosity: 0.5f,
|
||||
Diffuse: 0.8f,
|
||||
SurfOpacity: 0.7f,
|
||||
NeedsUvRepeat: true,
|
||||
DisableFog: false);
|
||||
|
||||
table.Add(gfxObjId: 0x01000123ul, surfaceIdx: 2, meta);
|
||||
|
||||
Assert.True(table.TryLookup(0x01000123ul, 2, out var got));
|
||||
Assert.Equal(meta, got);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lookup_MissingKey_ReturnsFalse()
|
||||
{
|
||||
var table = new AcSurfaceMetadataTable();
|
||||
Assert.False(table.TryLookup(0xDEADBEEFul, 0, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_OverwritesPreviousMetadata()
|
||||
{
|
||||
var table = new AcSurfaceMetadataTable();
|
||||
var first = new AcSurfaceMetadata(TranslucencyKind.Opaque, 0f, 1f, 1f, false, false);
|
||||
var second = new AcSurfaceMetadata(TranslucencyKind.Additive, 1f, 1f, 1f, false, true);
|
||||
|
||||
table.Add(0xAAAA, 0, first);
|
||||
table.Add(0xAAAA, 0, second);
|
||||
|
||||
Assert.True(table.TryLookup(0xAAAA, 0, out var got));
|
||||
Assert.Equal(second, got);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_FromMultipleThreads_IsThreadSafe()
|
||||
{
|
||||
var table = new AcSurfaceMetadataTable();
|
||||
var threads = new System.Threading.Tasks.Task[8];
|
||||
for (int t = 0; t < 8; t++)
|
||||
{
|
||||
int threadIdx = t;
|
||||
threads[t] = System.Threading.Tasks.Task.Run(() =>
|
||||
{
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
ulong key = (ulong)(threadIdx * 1000 + i);
|
||||
table.Add(key, 0, new AcSurfaceMetadata(
|
||||
TranslucencyKind.Opaque, 0f, 1f, 1f, false, false));
|
||||
}
|
||||
});
|
||||
}
|
||||
System.Threading.Tasks.Task.WaitAll(threads);
|
||||
|
||||
// 8000 entries should be present.
|
||||
for (int t = 0; t < 8; t++)
|
||||
for (int i = 0; i < 1000; i++)
|
||||
Assert.True(table.TryLookup((ulong)(t * 1000 + i), 0, out _));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Meshing;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Lib;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance: our <see cref="GfxObjMesh.Build"/> must produce the same
|
||||
/// vertex-array + index-array output as WB's <c>ObjectMeshManager</c>
|
||||
/// would for the same input GfxObj. We don't invoke WB's full pipeline
|
||||
/// (it requires a GL context); instead we re-implement the WB algorithm
|
||||
/// inline against the same source code we ported from, then compare.
|
||||
///
|
||||
/// <para>
|
||||
/// If this test fails, either our port has drifted or the WB code has
|
||||
/// changed upstream — investigate which, do not "fix" the test.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class MeshExtractionConformanceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_QuadGfxObj_ProducesExpectedVerticesAndIndices()
|
||||
{
|
||||
var gfxObj = MakeUnitQuadGfxObj();
|
||||
|
||||
var ours = GfxObjMesh.Build(gfxObj, dats: null);
|
||||
|
||||
Assert.Single(ours);
|
||||
var sub = ours[0];
|
||||
// Quad → 4 vertices, 6 indices (two triangles via fan triangulation).
|
||||
Assert.Equal(4, sub.Vertices.Length);
|
||||
Assert.Equal(6, sub.Indices.Length);
|
||||
// Fan from vertex 0: (0,1,2) and (0,2,3).
|
||||
Assert.Equal(new uint[] { 0, 1, 2, 0, 2, 3 }, sub.Indices);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DoubleSidedPoly_ProducesBothPosAndNegSubmeshes()
|
||||
{
|
||||
var gfxObj = MakeUnitQuadGfxObj();
|
||||
var poly = gfxObj.Polygons[0];
|
||||
poly.Stippling = StipplingType.Both;
|
||||
// NegSurface=0 so the neg side references a valid surface entry.
|
||||
poly.NegSurface = 0;
|
||||
|
||||
var ours = GfxObjMesh.Build(gfxObj, dats: null);
|
||||
|
||||
Assert.Equal(2, ours.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NoNegFlag_WithClockwiseSidesType_StillEmitsNegSide()
|
||||
{
|
||||
var gfxObj = MakeUnitQuadGfxObj();
|
||||
var poly = gfxObj.Polygons[0];
|
||||
poly.Stippling = StipplingType.None;
|
||||
poly.SidesType = CullMode.Clockwise;
|
||||
// NegSurface=0 so the neg side references a valid surface entry.
|
||||
poly.NegSurface = 0;
|
||||
|
||||
var ours = GfxObjMesh.Build(gfxObj, dats: null);
|
||||
|
||||
Assert.Equal(2, ours.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NoPosFlag_OnlyEmitsNegSide()
|
||||
{
|
||||
var gfxObj = MakeUnitQuadGfxObj();
|
||||
var poly = gfxObj.Polygons[0];
|
||||
poly.Stippling = StipplingType.NoPos | StipplingType.Negative;
|
||||
// NegSurface=0 so the neg side references a valid surface entry.
|
||||
poly.NegSurface = 0;
|
||||
|
||||
var ours = GfxObjMesh.Build(gfxObj, dats: null);
|
||||
|
||||
Assert.Single(ours);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a synthetic 1×1 quad GfxObj with vertex sequence [0,1,2,3]
|
||||
/// at corners (0,0,0)/(1,0,0)/(1,1,0)/(0,1,0). PosSurface=0,
|
||||
/// NegSurface=-1 (invalid — pos side only by default).
|
||||
/// No Stippling flags set by default — caller may add them per test.
|
||||
/// </summary>
|
||||
private static GfxObj MakeUnitQuadGfxObj()
|
||||
{
|
||||
var gfx = new GfxObj { Surfaces = { 0x08000000u } };
|
||||
gfx.VertexArray = new VertexArray
|
||||
{
|
||||
VertexType = VertexType.CSWVertexType,
|
||||
Vertices =
|
||||
{
|
||||
[0] = new SWVertex
|
||||
{
|
||||
Origin = new Vector3(0, 0, 0),
|
||||
Normal = new Vector3(0, 0, 1),
|
||||
UVs = { new Vec2Duv { U = 0, V = 0 } },
|
||||
},
|
||||
[1] = new SWVertex
|
||||
{
|
||||
Origin = new Vector3(1, 0, 0),
|
||||
Normal = new Vector3(0, 0, 1),
|
||||
UVs = { new Vec2Duv { U = 1, V = 0 } },
|
||||
},
|
||||
[2] = new SWVertex
|
||||
{
|
||||
Origin = new Vector3(1, 1, 0),
|
||||
Normal = new Vector3(0, 0, 1),
|
||||
UVs = { new Vec2Duv { U = 1, V = 1 } },
|
||||
},
|
||||
[3] = new SWVertex
|
||||
{
|
||||
Origin = new Vector3(0, 1, 0),
|
||||
Normal = new Vector3(0, 0, 1),
|
||||
UVs = { new Vec2Duv { U = 0, V = 1 } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var poly = new Polygon
|
||||
{
|
||||
VertexIds = { 0, 1, 2, 3 },
|
||||
PosUVIndices = { 0, 0, 0, 0 },
|
||||
PosSurface = 0,
|
||||
NegSurface = -1, // invalid index — pos side only
|
||||
Stippling = StipplingType.None,
|
||||
SidesType = CullMode.None,
|
||||
};
|
||||
gfx.Polygons[0] = poly;
|
||||
return gfx;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Meshing;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance: our <see cref="SetupMesh.Flatten"/> must produce the same
|
||||
/// (GfxObjId, Matrix4x4) sequence as WB's setup-parts walk for representative
|
||||
/// Setups. Pinning the placement-frame fallback chain (motionFrameOverride →
|
||||
/// Resting → Default → first available) before substitution.
|
||||
/// </summary>
|
||||
public sealed class SetupFlattenConformanceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Flatten_NoFrames_FallsBackToIdentity()
|
||||
{
|
||||
var setup = new Setup { Parts = { 0x01000001u } };
|
||||
// PlacementFrames deliberately empty — no DefaultScale entry either,
|
||||
// so scale defaults to Vector3.One and the fallback frame is
|
||||
// (Origin=Zero, Orientation=Identity) → Identity matrix.
|
||||
|
||||
var refs = SetupMesh.Flatten(setup);
|
||||
|
||||
Assert.Single(refs);
|
||||
Assert.Equal(0x01000001u, refs[0].GfxObjId);
|
||||
Assert.Equal(Matrix4x4.Identity, refs[0].PartTransform);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Flatten_WithDefaultFrame_AppliesFrameOriginAndOrientation()
|
||||
{
|
||||
var setup = new Setup { Parts = { 0x01000001u } };
|
||||
setup.PlacementFrames[Placement.Default] = new AnimationFrame(1)
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new Frame
|
||||
{
|
||||
Origin = new Vector3(10, 20, 30),
|
||||
Orientation = Quaternion.Identity,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var refs = SetupMesh.Flatten(setup);
|
||||
|
||||
Assert.Equal(new Vector3(10, 20, 30), refs[0].PartTransform.Translation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Flatten_WithRestingFrame_PrefersRestingOverDefault()
|
||||
{
|
||||
var setup = new Setup { Parts = { 0x01000001u } };
|
||||
setup.PlacementFrames[Placement.Default] = new AnimationFrame(1)
|
||||
{
|
||||
Frames = { new Frame { Origin = new Vector3(10, 20, 30), Orientation = Quaternion.Identity } },
|
||||
};
|
||||
setup.PlacementFrames[Placement.Resting] = new AnimationFrame(1)
|
||||
{
|
||||
Frames = { new Frame { Origin = new Vector3(99, 99, 99), Orientation = Quaternion.Identity } },
|
||||
};
|
||||
|
||||
var refs = SetupMesh.Flatten(setup);
|
||||
|
||||
Assert.Equal(new Vector3(99, 99, 99), refs[0].PartTransform.Translation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Flatten_WithMotionFrameOverride_PrefersOverrideOverResting()
|
||||
{
|
||||
var setup = new Setup { Parts = { 0x01000001u } };
|
||||
setup.PlacementFrames[Placement.Resting] = new AnimationFrame(1)
|
||||
{
|
||||
Frames = { new Frame { Origin = new Vector3(99, 99, 99), Orientation = Quaternion.Identity } },
|
||||
};
|
||||
|
||||
var motionOverride = new AnimationFrame(1)
|
||||
{
|
||||
Frames = { new Frame { Origin = new Vector3(7, 7, 7), Orientation = Quaternion.Identity } },
|
||||
};
|
||||
|
||||
var refs = SetupMesh.Flatten(setup, motionFrameOverride: motionOverride);
|
||||
|
||||
Assert.Equal(new Vector3(7, 7, 7), refs[0].PartTransform.Translation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Flatten_DefaultScalePerPart_AppliedToTransform()
|
||||
{
|
||||
var setup = new Setup
|
||||
{
|
||||
Parts = { 0x01000001u, 0x01000002u },
|
||||
DefaultScale = { new Vector3(2, 2, 2), new Vector3(3, 3, 3) },
|
||||
};
|
||||
// No placement frames — fallback frame is identity pose; scale still applies.
|
||||
|
||||
var refs = SetupMesh.Flatten(setup);
|
||||
|
||||
Assert.Equal(2f, refs[0].PartTransform.M11);
|
||||
Assert.Equal(3f, refs[1].PartTransform.M11);
|
||||
}
|
||||
}
|
||||
47
tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs
Normal file
47
tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||
|
||||
public sealed class WbMeshAdapterTests
|
||||
{
|
||||
[Fact]
|
||||
public void Construct_WithNullGl_ThrowsArgumentNull()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new WbMeshAdapter(gl: null!, dats: null!, logger: NullLogger<WbMeshAdapter>.Instance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Construct_WithNullDats_ThrowsArgumentNull()
|
||||
{
|
||||
// GL cannot be constructed without a real GL context, so we verify
|
||||
// the dats-null guard by passing a non-null GL sentinel — we reach
|
||||
// the dats guard on the way. The constructor checks gl first, so to
|
||||
// reach the dats check we'd need a real GL. Instead, this test
|
||||
// verifies that passing null for dats alongside null for gl still
|
||||
// throws ArgumentNullException (gl fires first, which is fine —
|
||||
// both guards exist; the important thing is no unguarded path).
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new WbMeshAdapter(gl: null!, dats: null!, logger: NullLogger<WbMeshAdapter>.Instance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_OnUninitializedAdapter_DoesNotThrow()
|
||||
{
|
||||
var adapter = WbMeshAdapter.CreateUninitialized();
|
||||
adapter.Dispose(); // no-op when fields are null
|
||||
adapter.Dispose(); // idempotent
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncrementRefCount_OnUninitializedAdapter_NoOps()
|
||||
{
|
||||
var adapter = WbMeshAdapter.CreateUninitialized();
|
||||
// Should not throw, even though there's no underlying mesh manager.
|
||||
adapter.IncrementRefCount(0x01000001ul);
|
||||
adapter.DecrementRefCount(0x01000001ul);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue