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>
401 lines
15 KiB
C#
401 lines
15 KiB
C#
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.");
|
||
}
|
||
}
|