using System.Collections.Concurrent; using System.Runtime.CompilerServices; using AcDream.App.Composition; using AcDream.App.Rendering; using AcDream.App.Rendering.Wb; using AcDream.App.World; using AcDream.Content; 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() { var fixture = new Fixture(); WorldRenderResult result = fixture.Compose(); Assert.Equal(Enum.GetValues(), 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.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(fixture.Compose); Assert.Equal( Enum.GetValues() .TakeWhile(candidate => candidate <= point), fixture.Points); } public static TheoryData FailurePoints() { var data = new TheoryData(); foreach (WorldRenderCompositionPoint point in Enum.GetValues()) { 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(fixture.Compose); Assert.Equal( expectedReleaseOrder.Split('|'), fixture.Factory.Releases); } [Fact] public void PartialHudConstructionRollsBackFontWhenTextCreationFails() { var fixture = new Fixture( failurePoint: WorldRenderCompositionPoint.DebugFontCreated); Assert.Throws(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; public Fixture( bool hasFont = true, WorldRenderCompositionPoint? failurePoint = null, string? publicationFailure = null) { _failurePoint = failurePoint; 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 Points { get; } = []; public ContentEffectsAudioResult Content { get; } public WorldRenderResult Compose() => new WorldRenderCompositionPhase( new WorldRenderDependencies( new WorldEnvironmentController(), Lifetime, ImmediateGpuResourceRetirementQueue.Instance, 0xA9B4FFFFu, _ => { }), Publication, Factory, point => { Points.Add(point); if (point == _failurePoint) throw new InvalidOperationException($"fault at {point}"); }).Compose( new GameWindowPlatformResult(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 factory) { AcquireCalls++; return Atlas; } } private sealed class Factory(bool hasFont) : IWorldRenderCompositionFactory { private readonly Dictionary _names = new(ReferenceEqualityComparer.Instance); public TerrainAtlas Atlas { get; } = Stub(); public int AnisotropicLevel { get; private set; } public List Releases { get; } = []; public void InitializeGlState(GL gl) { } public WorldRegionData LoadRegion(IDatReaderWriter dats) => new(Stub(), new float[256]); public void InitializeEnvironment( WorldEnvironmentController environment, Region region) { } public BindlessSupport RequireBindless(GL gl, Action log) => Stub(); 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("terrain shader"); public SceneLightingUboBinding CreateSceneLighting(GL gl) => Resource("scene lighting"); public DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory) => Resource("debug lines"); public byte[]? TryLoadDebugFont() => hasFont ? [1] : null; public BitmapFont CreateDebugFont(GL gl, byte[] bytes) => Resource("debug font"); public TextRenderer CreateTextRenderer(GL gl, string shadersDirectory) => Resource("text renderer"); public TerrainModernRenderer CreateTerrain( GL gl, BindlessSupport bindless, Shader shader, TerrainAtlas atlas, IGpuResourceRetirementQueue retirement) => Resource("terrain"); public WorldTerrainBuildContext CreateTerrainBuildContext( uint initialCenterLandblockId, float[] heightTable, TerrainAtlas atlas) => new( initialCenterLandblockId, 0xA9, 0xB4, heightTable, Stub(), new ConcurrentDictionary()); public Shader CreateMeshShader(GL gl, string shadersDirectory) => Resource("mesh shader"); public WbMeshAdapter CreateMeshAdapter( GL gl, IDatReaderWriter dats, IGpuResourceRetirementQueue retirement) => Resource("WB mesh adapter"); public TextureCache CreateTextureCache( GL gl, IDatReaderWriter dats, BindlessSupport bindless, IGpuResourceRetirementQueue retirement) => Resource("texture cache"); public SamplerCache CreateSamplerCache(GL gl) => Resource("sampler cache"); public void Release(IDisposable resource) { Releases.Add(_names[resource]); } private T Resource(string name) where T : class, IDisposable { T value = Stub(); _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 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() 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."); } }