diff --git a/src/AcDream.App/Rendering/Wb/AcSurfaceMetadata.cs b/src/AcDream.App/Rendering/Wb/AcSurfaceMetadata.cs new file mode 100644 index 0000000..4e6e325 --- /dev/null +++ b/src/AcDream.App/Rendering/Wb/AcSurfaceMetadata.cs @@ -0,0 +1,21 @@ +using AcDream.Core.Meshing; + +namespace AcDream.App.Rendering.Wb; + +/// +/// AC-specific surface render metadata that WB's MeshBatchData +/// doesn't carry. Computed at mesh-extraction time and looked up by the +/// draw dispatcher to drive translucency / sky-pass / fog behavior. +/// +/// +/// All fields mirror those on today's so +/// behavior is preserved bit-for-bit through the migration. +/// +/// +public sealed record AcSurfaceMetadata( + TranslucencyKind Translucency, + float Luminosity, + float Diffuse, + float SurfOpacity, + bool NeedsUvRepeat, + bool DisableFog); diff --git a/src/AcDream.App/Rendering/Wb/AcSurfaceMetadataTable.cs b/src/AcDream.App/Rendering/Wb/AcSurfaceMetadataTable.cs new file mode 100644 index 0000000..20b9278 --- /dev/null +++ b/src/AcDream.App/Rendering/Wb/AcSurfaceMetadataTable.cs @@ -0,0 +1,27 @@ +using System.Collections.Concurrent; + +namespace AcDream.App.Rendering.Wb; + +/// +/// Thread-safe side-table mapping (gfxObjId, surfaceIdx) to +/// . Populated when a GfxObj's mesh data +/// is extracted; queried at draw time. +/// +/// +/// Keyed by (gfxObjId, surfaceIdx) 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. +/// +/// +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(); +} diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/AcSurfaceMetadataTableTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/AcSurfaceMetadataTableTests.cs new file mode 100644 index 0000000..23aa231 --- /dev/null +++ b/tests/AcDream.Core.Tests/Rendering/Wb/AcSurfaceMetadataTableTests.cs @@ -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 _)); + } +}