perf(render): consume prepared mesh package at runtime

This commit is contained in:
Erik 2026-07-24 15:07:25 +02:00
parent c42f93b323
commit f05afc07c1
22 changed files with 328 additions and 370 deletions

View file

@ -36,6 +36,9 @@ public sealed class ContentEffectsAudioCompositionTests
Enum.GetValues<ContentEffectsAudioCompositionPoint>(),
fixture.Points);
Assert.Same(fixture.Publication.Dats, result.Dats);
Assert.Same(fixture.Publication.PreparedAssets, result.PreparedAssets);
Assert.Equal("test.pak", fixture.Factory.PreparedAssetPath);
Assert.Same(result.Dats, fixture.Factory.PreparedAssetDats);
Assert.Same(fixture.Publication.Magic, result.MagicCatalog);
Assert.Same(fixture.Publication.Animations, result.AnimationLoader);
Assert.Same(fixture.Publication.Particles, result.ParticleSystem);
@ -167,6 +170,70 @@ public sealed class ContentEffectsAudioCompositionTests
StringComparison.Ordinal);
}
[Fact]
public void ProductionRendererConsumesOnlyThePublishedPreparedAssetSource()
{
string root = FindRepoRoot();
string contentPhase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"ContentEffectsAudioComposition.cs"));
string worldPhase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"WorldRenderComposition.cs"));
string manager = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"Wb",
"ObjectMeshManager.cs"));
string adapter = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"Wb",
"WbMeshAdapter.cs"));
string lifetime = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"GameWindowLifetime.cs"));
Assert.Contains("new PakPreparedAssetSource(path, dats, diagnostic)",
contentPhase, StringComparison.Ordinal);
Assert.Contains("content.PreparedAssets", worldPhase,
StringComparison.Ordinal);
Assert.Contains("_preparedAssets.Read(request.Asset, ct)", manager,
StringComparison.Ordinal);
Assert.DoesNotContain("MeshExtractor", manager,
StringComparison.Ordinal);
Assert.DoesNotContain("IDatReaderWriter", manager,
StringComparison.Ordinal);
Assert.DoesNotContain("GfxObjMesh.Build", adapter,
StringComparison.Ordinal);
int meshStage = lifetime.IndexOf(
"new ResourceShutdownStage(\"mesh adapter\"",
StringComparison.Ordinal);
int preparedRelease = lifetime.IndexOf(
"Hard(\"prepared asset source\"",
StringComparison.Ordinal);
int datRelease = lifetime.IndexOf(
"Hard(\"DAT collection\"",
StringComparison.Ordinal);
Assert.True(meshStage >= 0);
Assert.True(preparedRelease > meshStage);
Assert.True(datRelease > preparedRelease);
}
private sealed class Fixture : IDisposable
{
private readonly ContentEffectsAudioCompositionPoint? _failurePoint;
@ -185,6 +252,7 @@ public sealed class ContentEffectsAudioCompositionTests
typeof(HostInputCameraResult));
Dependencies = new ContentEffectsAudioDependencies(
"test-dat",
"test.pak",
new PhysicsDataCache(),
false,
new Spellbook(),
@ -227,6 +295,7 @@ public sealed class ContentEffectsAudioCompositionTests
IDisposable
{
public IDatReaderWriter? Dats { get; private set; }
public IPreparedAssetSource? PreparedAssets { get; private set; }
public MagicCatalog? Magic { get; private set; }
public IAnimationLoader? Animations { get; private set; }
public LiveEntityCollisionBuilder? Collision { get; private set; }
@ -242,6 +311,8 @@ public sealed class ContentEffectsAudioCompositionTests
public ContentAudioGraph? Audio { get; private set; }
public void PublishDatCollection(IDatReaderWriter value) => Dats = Once(Dats, value);
public void PublishPreparedAssetSource(IPreparedAssetSource value) =>
PreparedAssets = Once(PreparedAssets, value);
public void PublishMagicCatalog(MagicCatalog value) => Magic = Once(Magic, value);
public void PublishAnimationLoader(IAnimationLoader value) =>
Animations = Once(Animations, value);
@ -273,6 +344,8 @@ public sealed class ContentEffectsAudioCompositionTests
Registrations = null;
Audio?.Engine.Dispose();
Audio = null;
PreparedAssets?.Dispose();
PreparedAssets = null;
Dats?.Dispose();
Dats = null;
}
@ -292,11 +365,24 @@ public sealed class ContentEffectsAudioCompositionTests
private readonly MagicCatalog _magic =
(MagicCatalog)RuntimeHelpers.GetUninitializedObject(typeof(MagicCatalog));
private readonly IAnimationLoader _animations = new NullAnimationLoader();
private readonly IPreparedAssetSource _preparedAssets =
new NullPreparedAssetSource();
public int AudioFactoryCalls { get; private set; }
public OpenAlAudioEngine? LastAudioEngine { get; private set; }
public string? PreparedAssetPath { get; private set; }
public IDatReaderWriter? PreparedAssetDats { get; private set; }
public IDatReaderWriter OpenDatCollection(string datDirectory) => _dats;
public IPreparedAssetSource OpenPreparedAssetSource(
string path,
IDatReaderWriter dats,
Action<string> diagnostic)
{
PreparedAssetPath = path;
PreparedAssetDats = dats;
return _preparedAssets;
}
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) => _magic;
public void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog) { }
public int GetSpellCount(MagicCatalog catalog) => 0;
@ -369,6 +455,26 @@ public sealed class ContentEffectsAudioCompositionTests
public Animation? LoadAnimation(uint id) => null;
}
private sealed class NullPreparedAssetSource : IPreparedAssetSource
{
public PreparedAssetSourceStats Stats => default;
public CacheStats DecodedTextureCacheStats => default;
public PreparedAssetPresence Probe(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
PreparedAssetPresence.Missing;
public PreparedAssetReadResult Read(
in PreparedAssetRequest request,
CancellationToken cancellationToken = default) =>
PreparedAssetReadResult.Missing;
public void Dispose()
{
}
}
private sealed class AudioApiFactory(IOpenAlResourceApi api)
: IOpenAlResourceApiFactory
{

View file

@ -265,6 +265,7 @@ public sealed class WorldRenderCompositionTests
public WbMeshAdapter CreateMeshAdapter(
GL gl,
IDatReaderWriter dats,
IPreparedAssetSource preparedAssets,
IGpuResourceRetirementQueue retirement) =>
Resource<WbMeshAdapter>("WB mesh adapter");

View file

@ -95,6 +95,8 @@ public static class ObjectMeshDataEquality {
Assert.True(expected.Indices.SequenceEqual(actual.Indices),
$"{path}.Indices: expected [{string.Join(",", expected.Indices)}], got [{string.Join(",", actual.Indices)}]");
Assert.True(expected.CullMode == actual.CullMode, $"{path}.CullMode: expected {expected.CullMode}, got {actual.CullMode}");
Assert.True(expected.Translucency == actual.Translucency,
$"{path}.Translucency: expected {expected.Translucency}, got {actual.Translucency}");
Assert.True(expected.IsTransparent == actual.IsTransparent, $"{path}.IsTransparent: expected {expected.IsTransparent}, got {actual.IsTransparent}");
Assert.True(expected.IsAdditive == actual.IsAdditive, $"{path}.IsAdditive: expected {expected.IsAdditive}, got {actual.IsAdditive}");
Assert.True(expected.HasWrappingUVs == actual.HasWrappingUVs, $"{path}.HasWrappingUVs: expected {expected.HasWrappingUVs}, got {actual.HasWrappingUVs}");

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Numerics;
using AcDream.Content.Pak;
using AcDream.Core.Meshing;
using Chorizite.Core.Lib;
using Chorizite.Core.Render.Enums;
using DatReaderWriter.DBObjs;
@ -59,6 +60,7 @@ public class ObjectMeshDataSerializerTests {
UploadPixelType = AcDream.Content.UploadPixelType.UnsignedByte,
Indices = new List<ushort> { 0, 1, 2, 2, 3, 0 },
CullMode = CullMode.CounterClockwise,
Translucency = TranslucencyKind.InvAlpha,
IsTransparent = true,
IsAdditive = false,
HasWrappingUVs = true,

View file

@ -1,72 +0,0 @@
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 _));
}
}