acdream/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs
Erik 8a7a0837e1 feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.

GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.

A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.

Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.

Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 02:19:53 +02:00

401 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Residency;
using AcDream.App.World;
using AcDream.App.Tests.Rendering.Gpu;
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;
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("scene lighting", "scene lighting")]
[InlineData("debug lines", "debug lines")]
[InlineData("HUD", "text renderer|debug font")]
[InlineData("terrain", "terrain")]
[InlineData("WB mesh adapter", "WB mesh adapter")]
[InlineData("texture cache", "texture 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;
private readonly IGpuDevice _gpuDevice = new RecordingGpuDevice();
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,
Path.Combine(Path.GetTempPath(), "acdream-tests"),
_ => { },
_gpuDevice,
new GpuDeviceFrameLifetime(_gpuDevice)),
Publication,
Factory,
point =>
{
Points.Add(point);
if (point == _failurePoint)
throw new InvalidOperationException($"fault at {point}");
}).Compose(
new GameWindowPlatformResult<GameWindowGraphics, IInputContext>(TestGameWindowGraphics.Instance, null!),
Content,
new SettingsDevToolsResult(
QualitySettings.From(QualityPreset.High)));
}
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 WorldRegionData LoadRegion(IDatReaderWriter dats) =>
new(Stub<Region>(), new float[256]);
public void InitializeEnvironment(
WorldEnvironmentController environment,
Region region) { }
/// <summary>
/// Campaign V slice V6i-2: the arm a backend with no GL context takes.
/// Returns the same stub atlas through the same lifetime owner, so the
/// composition assertions do not care which arm ran.
/// </summary>
public TerrainAtlas AcquireBackendNeutralTerrainAtlas(
IGameRenderResourceLifetime lifetime,
IGpuDevice device,
IDatReaderWriter dats) =>
lifetime.AcquireTerrainAtlas(() => Atlas);
/// <summary>
/// Recorded rather than run: the exercise needs a real
/// <see cref="IGpuDevice"/> to create images through. Its behaviour is
/// covered by <c>RhiWorldTextureArrayTests</c> and by the Vulkan
/// composition-host run — see plan ç5.5.13.
/// </summary>
public void ExerciseBackendNeutralWorldTextures(
IGpuDevice device,
Action<string> log) =>
WorldTextureExerciseCount++;
public int WorldTextureExerciseCount { get; private set; }
public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) =>
AnisotropicLevel = level;
public SceneLightingUboBinding CreateBackendNeutralSceneLighting(
ICurrentGpuFrameSource frameSource,
IWorldPassScope scope) =>
Resource<SceneLightingUboBinding>("scene lighting");
public DebugLineRenderer CreateDebugLines(
IGpuDevice device, ICurrentGpuFrameSource frameSource, string shadersDirectory) =>
Resource<DebugLineRenderer>("debug lines");
public byte[]? TryLoadDebugFont() => hasFont ? [1] : null;
public BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes) =>
Resource<BitmapFont>("debug font");
public TextRenderer CreateTextRenderer(
IGpuDevice device, ICurrentGpuFrameSource frameSource, string shadersDirectory) =>
Resource<TextRenderer>("text renderer");
public TerrainModernRenderer CreateBackendNeutralTerrain(
IGpuDevice gpuDevice,
ICurrentGpuFrameSource frameSource,
IWorldPassScope scope,
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 WbMeshAdapter CreateMeshAdapter(
GL? gl,
IGpuDevice device,
IDatReaderWriter dats,
IPreparedAssetSource preparedAssets,
IGpuResourceRetirementQueue retirement,
ResidencyBudgetOptions budgets)
{
MeshBudgets = budgets;
return Resource<WbMeshAdapter>("WB mesh adapter");
}
public TextureCache CreateTextureCache(
IGpuDevice device,
IDatReaderWriter dats,
IGpuResourceRetirementQueue retirement,
string diagnosticsDirectory,
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 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 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 PublishWbMeshAdapter(WbMeshAdapter value)
{
Fail("WB mesh adapter");
MeshAdapter = value;
}
public void PublishTextureCache(TextureCache value)
{
Fail("texture cache");
TextureCache = value;
}
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.");
}
}