Commit 2 deleted the GL rendering backend's implementations; this step removes the package references and shader vocabulary they leave behind, so nothing in the App project still spells Silk.NET.OpenGL. Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are used directly and extensively across the Wb texture/mesh pipeline, independent of the deleted GL IUniformBuffer implementers the package comment used to cite. The stale comment is corrected in place. IMeshPipelineDevice.Gl is removed along with the GL? gl parameter threaded through WbMeshAdapter's four constructors, WorldRenderComposition's CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null implementation — nothing read any of them once the legacy per-mesh upload bodies were gone (confirmed by grep: the sole non-doc-comment hit was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed a real bug along the way: its teardown still pattern-matched the deleted GL GpuFrameFlightController to decide whether to wait for submitted work, which VulkanFrameFlightController replaced at slice V6a without this site being updated — so the wait had been silently dead on every Vulkan run since then. Retargeted to VulkanFrameFlightController, which carries the same WaitForSubmittedWork(). The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for upload validation is replaced by AcDream.Content's existing Silk.NET-free UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake tool GL-free); two new members (Rgb, Red, Float) extend that enum with their GL ABI constants to cover the full vocabulary WorldTextureArray needs, since MP1a's original set only covered what the extractor itself emits. ObjectMeshManager's App-boundary cast `(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct pass-through now that both sides share the type. GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of the Vulkan texture table) is deleted and StorageBindingCount drops from 10 to 9; the descriptor-set-layout code that builds from that count (VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just allocates one fewer always-dummy-seeded, always-unused binding. Several fully dead GL-only classes came along for the ride, confirmed by zero construction sites: SilkFramebufferViewportTarget (NullFramebufferViewportTarget is the sole production IFramebufferViewportTarget), SilkRenderGlStateReader (NullRenderGlStateReader.Instance is the sole IRenderGlStateReader), RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a pass load-op instead), and GpuFrameTimer plus FrameProfiler's GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame bracket (RecordGpuSample is the only GPU-timing path any backend uses now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no longer applies, since WbDrawDispatcher's own diagnostic GPU sampling already moved to the device's Vulkan timer pool). GpuFrameFlightController itself stays (never constructed with a real fence API in production, but its retirement-ledger/serial-ring logic is backend-neutral and still covered by its own unit tests) — only its GL-specific parts (the public GL constructor overload, SilkGpuFenceApi) are deleted, since removing the whole class would mean restructuring the frozen Slice-8 composition shape's GpuFrameFlightController? threading, which is out of this commit's scope. TextureParameters.cs and BufferUsageExtensions.cs (zero callers each) are deleted outright. common.glsl is deleted: nothing in the actual Vulkan .spv build reads it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own complete self-contained preamble per file; common.glsl's textual concatenation was exclusively Shader.cs's GL-only mechanism, deleted at Commit 2. The five shader files that named it in comments (mesh_modern.vert, particle.vert, particle.frag, sky.frag, terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the mandatory modern path already made unreachable, with zero C# consumers and no compiled .spv — are deleted too. Regenerated via tools/compile-shaders.ps1: 9/9 remaining shader pairs compile (previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests doc comment's "nine of ten are not Vulkan-expressible" was already stale before this commit). Test fallout: dead-subject test methods/files are deleted rather than patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs, GpuResourceRetirementTransactionTests.cs's GL queue tests, one WorldRenderDiagnosticsTests source-order test, one RenderFrameResourceControllerTests clear-phase-order test); tests whose subject moved or was renamed are updated in place rather than deleted (GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests' pinned seven-member surface now reads six, ParticleBindlessInstanceTests' cross-dialect check now covers the one surviving dialect, WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was always the parameter that actually threw). Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors, with the Silk.NET.OpenGL/.Extensions.ARB package references physically removed from the csproj (not just unreferenced in code). Tests: full-solution `dotnet test` green across every project. Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
399 lines
15 KiB
C#
399 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;
|
||
|
||
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(
|
||
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.");
|
||
}
|
||
}
|