Complete Slice D4 by adding aggregate lifecycle occupancy and traffic facts, validating physical source reports, and including decoded audio under the typed startup budget. Exercise every domain under forced pressure and retain the real cache/fence convergence gates.
400 lines
14 KiB
C#
400 lines
14 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Runtime.CompilerServices;
|
|
using AcDream.App.Composition;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Rendering.Residency;
|
|
using AcDream.App.World;
|
|
using AcDream.Content;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Terrain;
|
|
using AcDream.UI.Abstractions.Settings;
|
|
using DatReaderWriter.DBObjs;
|
|
using Silk.NET.Input;
|
|
using Silk.NET.OpenGL;
|
|
using Shader = AcDream.App.Rendering.Shader;
|
|
|
|
namespace AcDream.App.Tests.Composition;
|
|
|
|
public sealed class WorldRenderCompositionTests
|
|
{
|
|
[Fact]
|
|
public void SuccessPublishesExactModernFoundationInFrozenOrder()
|
|
{
|
|
ResidencyBudgetOptions budgets =
|
|
ResidencyBudgetOptions.Default with
|
|
{
|
|
ObjectMeshGpuBytes = 321,
|
|
CompositePhysicalBytes = 654,
|
|
};
|
|
var fixture = new Fixture(budgets: budgets);
|
|
|
|
WorldRenderResult result = fixture.Compose();
|
|
|
|
Assert.Equal(Enum.GetValues<WorldRenderCompositionPoint>(), fixture.Points);
|
|
Assert.Same(fixture.Publication.Terrain, result.Foundation.Terrain);
|
|
Assert.Same(fixture.Publication.MeshAdapter, result.Foundation.MeshAdapter);
|
|
Assert.Same(fixture.Publication.TextureCache, result.Foundation.TextureCache);
|
|
Assert.Same(fixture.Lifetime.Atlas, result.Foundation.TerrainAtlas);
|
|
Assert.Equal(QualitySettings.From(QualityPreset.High).AnisotropicLevel,
|
|
fixture.Factory.AnisotropicLevel);
|
|
Assert.Same(budgets, result.Foundation.Residency.Budgets);
|
|
Assert.Same(budgets, fixture.Factory.MeshBudgets);
|
|
Assert.Same(budgets, fixture.Factory.TextureBudgets);
|
|
Assert.Same(result.Foundation.Residency, fixture.Factory.RegisteredResidency);
|
|
Assert.Equal(1, fixture.Lifetime.AcquireCalls);
|
|
Assert.Empty(fixture.Factory.Releases);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingDebugFontSkipsOnlyTheOptionalHudPrefix()
|
|
{
|
|
var fixture = new Fixture(hasFont: false);
|
|
|
|
WorldRenderResult result = fixture.Compose();
|
|
|
|
Assert.Null(result.Foundation.DebugFont);
|
|
Assert.Null(result.Foundation.TextRenderer);
|
|
Assert.DoesNotContain(WorldRenderCompositionPoint.DebugFontCreated, fixture.Points);
|
|
Assert.DoesNotContain(WorldRenderCompositionPoint.TextRendererCreated, fixture.Points);
|
|
Assert.DoesNotContain(WorldRenderCompositionPoint.HudResourcesPublished, fixture.Points);
|
|
Assert.Contains(WorldRenderCompositionPoint.HudResourcesCompleted, fixture.Points);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(FailurePoints))]
|
|
public void FaultAfterEachBoundaryStopsTheExactSuffix(int pointValue)
|
|
{
|
|
var point = (WorldRenderCompositionPoint)pointValue;
|
|
var fixture = new Fixture(failurePoint: point);
|
|
|
|
Assert.Throws<InvalidOperationException>(fixture.Compose);
|
|
|
|
Assert.Equal(
|
|
Enum.GetValues<WorldRenderCompositionPoint>()
|
|
.TakeWhile(candidate => candidate <= point),
|
|
fixture.Points);
|
|
}
|
|
|
|
public static TheoryData<int> FailurePoints()
|
|
{
|
|
var data = new TheoryData<int>();
|
|
foreach (WorldRenderCompositionPoint point in
|
|
Enum.GetValues<WorldRenderCompositionPoint>())
|
|
{
|
|
data.Add((int)point);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("terrain shader", "terrain shader")]
|
|
[InlineData("scene lighting", "scene lighting")]
|
|
[InlineData("debug lines", "debug lines")]
|
|
[InlineData("HUD", "text renderer|debug font")]
|
|
[InlineData("terrain", "terrain")]
|
|
[InlineData("mesh shader", "mesh shader")]
|
|
[InlineData("WB mesh adapter", "WB mesh adapter")]
|
|
[InlineData("texture cache", "texture cache")]
|
|
[InlineData("sampler cache", "sampler cache")]
|
|
public void FailedPublicationRollsBackOnlyItsUnpublishedResourcePrefix(
|
|
string publication,
|
|
string expectedReleaseOrder)
|
|
{
|
|
var fixture = new Fixture(publicationFailure: publication);
|
|
|
|
Assert.Throws<InvalidOperationException>(fixture.Compose);
|
|
|
|
Assert.Equal(
|
|
expectedReleaseOrder.Split('|'),
|
|
fixture.Factory.Releases);
|
|
}
|
|
|
|
[Fact]
|
|
public void PartialHudConstructionRollsBackFontWhenTextCreationFails()
|
|
{
|
|
var fixture = new Fixture(
|
|
failurePoint: WorldRenderCompositionPoint.DebugFontCreated);
|
|
|
|
Assert.Throws<InvalidOperationException>(fixture.Compose);
|
|
|
|
Assert.Equal(["debug font"], fixture.Factory.Releases);
|
|
}
|
|
|
|
[Fact]
|
|
public void GameWindowUsesPhaseAndNoLongerBuildsWorldFoundationInline()
|
|
{
|
|
string source = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindow.cs"));
|
|
|
|
Assert.Contains("new WorldRenderCompositionPhase(", source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("BindlessSupport.TryCreate(_gl", source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_terrainModernShader = new Shader", source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_wbMeshAdapter = new", source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_textureCache = new TextureCache", source,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
private sealed class Fixture
|
|
{
|
|
private readonly WorldRenderCompositionPoint? _failurePoint;
|
|
private readonly ResidencyBudgetOptions _budgets;
|
|
|
|
public Fixture(
|
|
bool hasFont = true,
|
|
WorldRenderCompositionPoint? failurePoint = null,
|
|
string? publicationFailure = null,
|
|
ResidencyBudgetOptions? budgets = null)
|
|
{
|
|
_failurePoint = failurePoint;
|
|
_budgets = budgets ?? ResidencyBudgetOptions.Default;
|
|
Factory = new Factory(hasFont);
|
|
Publication = new Publication(publicationFailure);
|
|
Lifetime = new RenderLifetime(Factory.Atlas);
|
|
var content = (ContentEffectsAudioResult)
|
|
RuntimeHelpers.GetUninitializedObject(typeof(ContentEffectsAudioResult));
|
|
Content = content;
|
|
}
|
|
|
|
public Factory Factory { get; }
|
|
public Publication Publication { get; }
|
|
public RenderLifetime Lifetime { get; }
|
|
public List<WorldRenderCompositionPoint> Points { get; } = [];
|
|
public ContentEffectsAudioResult Content { get; }
|
|
|
|
public WorldRenderResult Compose() =>
|
|
new WorldRenderCompositionPhase(
|
|
new WorldRenderDependencies(
|
|
new WorldEnvironmentController(),
|
|
Lifetime,
|
|
ImmediateGpuResourceRetirementQueue.Instance,
|
|
_budgets,
|
|
0xA9B4FFFFu,
|
|
_ => { }),
|
|
Publication,
|
|
Factory,
|
|
point =>
|
|
{
|
|
Points.Add(point);
|
|
if (point == _failurePoint)
|
|
throw new InvalidOperationException($"fault at {point}");
|
|
}).Compose(
|
|
new GameWindowPlatformResult<GL, IInputContext>(null!, null!),
|
|
Content,
|
|
new SettingsDevToolsResult(
|
|
QualitySettings.From(QualityPreset.High),
|
|
null));
|
|
}
|
|
|
|
private sealed class RenderLifetime(TerrainAtlas atlas)
|
|
: IGameRenderResourceLifetime
|
|
{
|
|
public TerrainAtlas Atlas { get; } = atlas;
|
|
public int AcquireCalls { get; private set; }
|
|
|
|
public TerrainAtlas AcquireTerrainAtlas(Func<TerrainAtlas> factory)
|
|
{
|
|
AcquireCalls++;
|
|
return Atlas;
|
|
}
|
|
}
|
|
|
|
private sealed class Factory(bool hasFont) : IWorldRenderCompositionFactory
|
|
{
|
|
private readonly Dictionary<IDisposable, string> _names =
|
|
new(ReferenceEqualityComparer.Instance);
|
|
|
|
public TerrainAtlas Atlas { get; } = Stub<TerrainAtlas>();
|
|
public int AnisotropicLevel { get; private set; }
|
|
public List<string> Releases { get; } = [];
|
|
public ResidencyBudgetOptions? MeshBudgets { get; private set; }
|
|
public ResidencyBudgetOptions? TextureBudgets { get; private set; }
|
|
public ResidencyManager? RegisteredResidency { get; private set; }
|
|
|
|
public void InitializeGlState(GL gl) { }
|
|
|
|
public WorldRegionData LoadRegion(IDatReaderWriter dats) =>
|
|
new(Stub<Region>(), new float[256]);
|
|
|
|
public void InitializeEnvironment(
|
|
WorldEnvironmentController environment,
|
|
Region region) { }
|
|
|
|
public BindlessSupport RequireBindless(GL gl, Action<string> log) =>
|
|
Stub<BindlessSupport>();
|
|
|
|
public TerrainAtlas AcquireTerrainAtlas(
|
|
IGameRenderResourceLifetime lifetime,
|
|
GL gl,
|
|
IDatReaderWriter dats,
|
|
BindlessSupport bindless) =>
|
|
lifetime.AcquireTerrainAtlas(() => Atlas);
|
|
|
|
public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) =>
|
|
AnisotropicLevel = level;
|
|
|
|
public Shader CreateTerrainShader(GL gl, string shadersDirectory) =>
|
|
Resource<Shader>("terrain shader");
|
|
|
|
public SceneLightingUboBinding CreateSceneLighting(GL gl) =>
|
|
Resource<SceneLightingUboBinding>("scene lighting");
|
|
|
|
public DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory) =>
|
|
Resource<DebugLineRenderer>("debug lines");
|
|
|
|
public byte[]? TryLoadDebugFont() => hasFont ? [1] : null;
|
|
|
|
public BitmapFont CreateDebugFont(GL gl, byte[] bytes) =>
|
|
Resource<BitmapFont>("debug font");
|
|
|
|
public TextRenderer CreateTextRenderer(GL gl, string shadersDirectory) =>
|
|
Resource<TextRenderer>("text renderer");
|
|
|
|
public TerrainModernRenderer CreateTerrain(
|
|
GL gl,
|
|
BindlessSupport bindless,
|
|
Shader shader,
|
|
TerrainAtlas atlas,
|
|
IGpuResourceRetirementQueue retirement) =>
|
|
Resource<TerrainModernRenderer>("terrain");
|
|
|
|
public WorldTerrainBuildContext CreateTerrainBuildContext(
|
|
uint initialCenterLandblockId,
|
|
float[] heightTable,
|
|
TerrainAtlas atlas) =>
|
|
new(
|
|
initialCenterLandblockId,
|
|
0xA9,
|
|
0xB4,
|
|
heightTable,
|
|
Stub<TerrainBlendingContext>(),
|
|
new ConcurrentDictionary<uint, SurfaceInfo>());
|
|
|
|
public Shader CreateMeshShader(GL gl, string shadersDirectory) =>
|
|
Resource<Shader>("mesh shader");
|
|
|
|
public WbMeshAdapter CreateMeshAdapter(
|
|
GL gl,
|
|
IDatReaderWriter dats,
|
|
IPreparedAssetSource preparedAssets,
|
|
IGpuResourceRetirementQueue retirement,
|
|
ResidencyBudgetOptions budgets)
|
|
{
|
|
MeshBudgets = budgets;
|
|
return Resource<WbMeshAdapter>("WB mesh adapter");
|
|
}
|
|
|
|
public TextureCache CreateTextureCache(
|
|
GL gl,
|
|
IDatReaderWriter dats,
|
|
BindlessSupport bindless,
|
|
IGpuResourceRetirementQueue retirement,
|
|
ResidencyBudgetOptions budgets)
|
|
{
|
|
TextureBudgets = budgets;
|
|
return Resource<TextureCache>("texture cache");
|
|
}
|
|
|
|
public void RegisterResidencySources(
|
|
ResidencyManager manager,
|
|
WbMeshAdapter meshes,
|
|
TextureCache textures,
|
|
IPreparedAssetSource preparedAssets,
|
|
IAnimationLoader animations,
|
|
AcDream.Core.Audio.DatSoundCache? audio)
|
|
{
|
|
RegisteredResidency = manager;
|
|
}
|
|
|
|
public SamplerCache CreateSamplerCache(GL gl) =>
|
|
Resource<SamplerCache>("sampler cache");
|
|
|
|
public void Release(IDisposable resource)
|
|
{
|
|
Releases.Add(_names[resource]);
|
|
}
|
|
|
|
private T Resource<T>(string name)
|
|
where T : class, IDisposable
|
|
{
|
|
T value = Stub<T>();
|
|
_names.Add(value, name);
|
|
return value;
|
|
}
|
|
}
|
|
|
|
private sealed class Publication(string? failure) : IGameWindowWorldRenderPublication
|
|
{
|
|
public TerrainModernRenderer? Terrain { get; private set; }
|
|
public WbMeshAdapter? MeshAdapter { get; private set; }
|
|
public TextureCache? TextureCache { get; private set; }
|
|
|
|
public void PublishBindlessSupport(BindlessSupport value) =>
|
|
Fail("bindless");
|
|
public void PublishTerrainShader(Shader value) =>
|
|
Fail("terrain shader");
|
|
public void PublishSceneLighting(SceneLightingUboBinding value) =>
|
|
Fail("scene lighting");
|
|
public void PublishDebugLines(DebugLineRenderer value) =>
|
|
Fail("debug lines");
|
|
public void PublishHudResources(BitmapFont font, TextRenderer text) =>
|
|
Fail("HUD");
|
|
|
|
public void PublishTerrain(TerrainModernRenderer value)
|
|
{
|
|
Fail("terrain");
|
|
Terrain = value;
|
|
}
|
|
|
|
public void PublishTerrainBuildState(
|
|
float[] heightTable,
|
|
TerrainBlendingContext blending,
|
|
ConcurrentDictionary<uint, SurfaceInfo> surfaceCache) =>
|
|
Fail("terrain build state");
|
|
public void PublishMeshShader(Shader value) => Fail("mesh shader");
|
|
|
|
public void PublishWbMeshAdapter(WbMeshAdapter value)
|
|
{
|
|
Fail("WB mesh adapter");
|
|
MeshAdapter = value;
|
|
}
|
|
|
|
public void PublishTextureCache(TextureCache value)
|
|
{
|
|
Fail("texture cache");
|
|
TextureCache = value;
|
|
}
|
|
|
|
public void PublishSamplerCache(SamplerCache value) =>
|
|
Fail("sampler cache");
|
|
|
|
private void Fail(string point)
|
|
{
|
|
if (string.Equals(failure, point, StringComparison.Ordinal))
|
|
throw new InvalidOperationException($"publication failed at {point}");
|
|
}
|
|
}
|
|
|
|
private static T Stub<T>() where T : class =>
|
|
(T)RuntimeHelpers.GetUninitializedObject(typeof(T));
|
|
|
|
private static string FindRepoRoot()
|
|
{
|
|
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
|
while (directory is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
|
return directory.FullName;
|
|
directory = directory.Parent;
|
|
}
|
|
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
|
}
|
|
}
|