phase(N.4): AcSurfaceMetadata side-table for WB-pristine surface props

Holds Translucency / Luminosity / Diffuse / SurfOpacity / NeedsUvRepeat /
DisableFog keyed by (gfxObjId, surfaceIdx). Populated at extraction time,
queried by the draw dispatcher. ConcurrentDictionary because mesh
extraction happens on background workers.

No fork patches required — keeps WB's MeshBatchData pristine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-08 13:08:56 +02:00
parent 81b5ed8c68
commit 46deed6019
3 changed files with 120 additions and 0 deletions

View 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);

View 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();
}

View file

@ -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 _));
}
}