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>
654 lines
26 KiB
C#
654 lines
26 KiB
C#
using System.Collections.Concurrent;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Rendering.Residency;
|
|
using AcDream.App.World;
|
|
using AcDream.Content;
|
|
using AcDream.Content.Vfx;
|
|
using AcDream.Core.Audio;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Terrain;
|
|
using AcDream.UI.Abstractions.Settings;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Silk.NET.Input;
|
|
|
|
namespace AcDream.App.Composition;
|
|
|
|
internal sealed record WorldRegionData(Region Region, float[] HeightTable);
|
|
|
|
internal sealed record WorldTerrainBuildContext(
|
|
uint InitialCenterLandblockId,
|
|
int InitialCenterX,
|
|
int InitialCenterY,
|
|
float[] HeightTable,
|
|
TerrainBlendingContext Blending,
|
|
ConcurrentDictionary<uint, SurfaceInfo> SurfaceCache);
|
|
|
|
/// <summary>
|
|
/// The render foundation the later phases build on.
|
|
///
|
|
/// <para>Every member here is backend-neutral. The raw-GL-only members this
|
|
/// record used to carry (<c>Bindless</c>, <c>TerrainShader</c>, <c>MeshShader</c>,
|
|
/// <c>Samplers</c>) were deleted at Campaign V slice V11 along with the GL arm
|
|
/// that was their only producer.</para>
|
|
/// </summary>
|
|
internal sealed record WorldRenderFoundation(
|
|
string ShadersDirectory,
|
|
TerrainAtlas? TerrainAtlas,
|
|
SceneLightingUboBinding? SceneLighting,
|
|
DebugLineRenderer DebugLines,
|
|
BitmapFont? DebugFont,
|
|
TextRenderer? TextRenderer,
|
|
TerrainModernRenderer? Terrain,
|
|
WbMeshAdapter? MeshAdapter,
|
|
TextureCache TextureCache,
|
|
ResidencyManager Residency);
|
|
|
|
internal sealed record WorldRenderResult(
|
|
WorldTerrainBuildContext TerrainBuild,
|
|
WorldRenderFoundation Foundation);
|
|
|
|
internal sealed record WorldRenderDependencies(
|
|
WorldEnvironmentController Environment,
|
|
IGameRenderResourceLifetime RenderResources,
|
|
IGpuResourceRetirementQueue ResourceRetirement,
|
|
ResidencyBudgetOptions ResidencyBudgets,
|
|
uint InitialCenterLandblockId,
|
|
string DiagnosticsDirectory,
|
|
Action<string> Log,
|
|
AcDream.App.Rendering.Gpu.IGpuDevice GpuDevice,
|
|
AcDream.App.Rendering.ICurrentGpuFrameSource GpuFrameSource);
|
|
|
|
internal interface IGameWindowWorldRenderPublication
|
|
{
|
|
void PublishSceneLighting(SceneLightingUboBinding value);
|
|
void PublishDebugLines(DebugLineRenderer value);
|
|
void PublishHudResources(BitmapFont font, TextRenderer text);
|
|
void PublishTerrain(TerrainModernRenderer value);
|
|
void PublishTerrainBuildState(
|
|
float[] heightTable,
|
|
TerrainBlendingContext blending,
|
|
ConcurrentDictionary<uint, SurfaceInfo> surfaceCache);
|
|
void PublishWbMeshAdapter(WbMeshAdapter value);
|
|
void PublishTextureCache(TextureCache value);
|
|
}
|
|
|
|
internal interface IWorldRenderCompositionFactory
|
|
{
|
|
WorldRegionData LoadRegion(IDatReaderWriter dats);
|
|
void InitializeEnvironment(WorldEnvironmentController environment, Region region);
|
|
/// <summary>
|
|
/// Campaign V slice V6i-2: the terrain atlas built through
|
|
/// <see cref="AcDream.App.Rendering.Gpu.IGpuDevice"/>. The raw-GL arm this
|
|
/// used to fork from (<c>AcquireTerrainAtlas</c>) was deleted at slice V11;
|
|
/// the name keeps its "BackendNeutral" suffix rather than being renamed, to
|
|
/// avoid touching every call site for a purely cosmetic change.
|
|
/// </summary>
|
|
TerrainAtlas AcquireBackendNeutralTerrainAtlas(
|
|
IGameRenderResourceLifetime lifetime,
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats);
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6i-2: one shared world array of each format family and
|
|
/// one composite array, created and released through the RHI so the new
|
|
/// creation path is exercised under the validation layer and inside the
|
|
/// ownership ledger rather than merely existing. Creation only — nothing
|
|
/// draws these.
|
|
/// </summary>
|
|
void ExerciseBackendNeutralWorldTextures(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
Action<string> log);
|
|
|
|
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
|
|
/// <summary>
|
|
/// Campaign V slice V6j: scene lighting on a backend with no global uniform
|
|
/// binding point. It publishes a ring section on the world pass scope and
|
|
/// each renderer binds it inside the pass. The raw-GL global-uniform-binding
|
|
/// arm (<c>CreateSceneLighting</c>) was deleted at slice V11.
|
|
/// </summary>
|
|
SceneLightingUboBinding CreateBackendNeutralSceneLighting(
|
|
ICurrentGpuFrameSource frameSource,
|
|
IWorldPassScope scope);
|
|
DebugLineRenderer CreateDebugLines(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
ICurrentGpuFrameSource frameSource,
|
|
string shadersDirectory);
|
|
byte[]? TryLoadDebugFont();
|
|
BitmapFont CreateDebugFont(AcDream.App.Rendering.Gpu.IGpuDevice device, byte[] bytes);
|
|
TextRenderer CreateTextRenderer(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
ICurrentGpuFrameSource frameSource,
|
|
string shadersDirectory);
|
|
/// <summary>
|
|
/// Campaign V slice V6j: terrain's RHI arm. No GL context, no linked
|
|
/// program — the pipeline compiles <c>terrain_modern</c> from the committed
|
|
/// SPIR-V and records into the world pass the scope publishes. The raw-GL
|
|
/// arm (<c>CreateTerrain</c>) was deleted at slice V11.
|
|
/// </summary>
|
|
TerrainModernRenderer CreateBackendNeutralTerrain(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
|
|
ICurrentGpuFrameSource frameSource,
|
|
IWorldPassScope scope,
|
|
TerrainAtlas atlas,
|
|
IGpuResourceRetirementQueue retirement);
|
|
/// <param name="atlas">
|
|
/// The built terrain atlas, or null on a backend that has none. The atlas is
|
|
/// where the blending layer/T-code tables come from, so a null one yields an
|
|
/// empty <see cref="TerrainBlendingContext"/>: streaming still builds real
|
|
/// landblock heightfields and collision, but every surface resolves to
|
|
/// <see cref="SurfaceInfo.None"/> because nothing is going to sample it.
|
|
/// Campaign V slice V4t builds those tables without GL.
|
|
/// </param>
|
|
WorldTerrainBuildContext CreateTerrainBuildContext(
|
|
uint initialCenterLandblockId,
|
|
float[] heightTable,
|
|
TerrainAtlas? atlas);
|
|
WbMeshAdapter CreateMeshAdapter(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats,
|
|
IPreparedAssetSource preparedAssets,
|
|
IGpuResourceRetirementQueue retirement,
|
|
ResidencyBudgetOptions budgets);
|
|
TextureCache CreateTextureCache(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats,
|
|
IGpuResourceRetirementQueue retirement,
|
|
string diagnosticsDirectory,
|
|
ResidencyBudgetOptions budgets);
|
|
void RegisterResidencySources(
|
|
ResidencyManager manager,
|
|
WbMeshAdapter? meshes,
|
|
TextureCache textures,
|
|
IPreparedAssetSource preparedAssets,
|
|
IAnimationLoader animations,
|
|
DatSoundCache? audio);
|
|
void Release(IDisposable resource);
|
|
}
|
|
|
|
internal sealed class RetailWorldRenderCompositionFactory
|
|
: IWorldRenderCompositionFactory
|
|
{
|
|
public WorldRegionData LoadRegion(IDatReaderWriter dats)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dats);
|
|
Region region = dats.Get<Region>(0x13000000u)
|
|
?? throw new InvalidOperationException(
|
|
"Region dat id 0x13000000 missing");
|
|
float[]? heightTable = region.LandDefs.LandHeightTable;
|
|
if (heightTable is null || heightTable.Length < 256)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Region.LandDefs.LandHeightTable missing or truncated");
|
|
}
|
|
return new WorldRegionData(region, heightTable);
|
|
}
|
|
|
|
public void InitializeEnvironment(
|
|
WorldEnvironmentController environment,
|
|
Region region)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(environment);
|
|
ArgumentNullException.ThrowIfNull(region);
|
|
environment.Initialize(region);
|
|
}
|
|
|
|
public TerrainAtlas AcquireBackendNeutralTerrainAtlas(
|
|
IGameRenderResourceLifetime lifetime,
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(lifetime);
|
|
return lifetime.AcquireTerrainAtlas(
|
|
() => TerrainAtlas.BuildBackendNeutral(device, dats));
|
|
}
|
|
|
|
public void ExerciseBackendNeutralWorldTextures(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
Action<string> log) =>
|
|
BackendNeutralWorldTextures.Exercise(device, log);
|
|
|
|
public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) =>
|
|
atlas.SetAnisotropic(level);
|
|
|
|
public SceneLightingUboBinding CreateBackendNeutralSceneLighting(
|
|
ICurrentGpuFrameSource frameSource,
|
|
IWorldPassScope scope) =>
|
|
new(frameSource, scope.Sections);
|
|
|
|
public DebugLineRenderer CreateDebugLines(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
ICurrentGpuFrameSource frameSource,
|
|
string shadersDirectory) =>
|
|
new(device, frameSource, shadersDirectory);
|
|
|
|
public byte[]? TryLoadDebugFont() =>
|
|
BitmapFont.TryLoadSystemMonospaceFont();
|
|
|
|
public BitmapFont CreateDebugFont(AcDream.App.Rendering.Gpu.IGpuDevice device, byte[] bytes) =>
|
|
new(device, bytes, pixelHeight: 15f, atlasSize: 512);
|
|
|
|
public TextRenderer CreateTextRenderer(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
ICurrentGpuFrameSource frameSource,
|
|
string shadersDirectory) =>
|
|
new(device, frameSource, shadersDirectory);
|
|
|
|
public TerrainModernRenderer CreateBackendNeutralTerrain(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
|
|
ICurrentGpuFrameSource frameSource,
|
|
IWorldPassScope scope,
|
|
TerrainAtlas atlas,
|
|
IGpuResourceRetirementQueue retirement) =>
|
|
new(gpuDevice, frameSource, scope, atlas, retirement);
|
|
|
|
public WorldTerrainBuildContext CreateTerrainBuildContext(
|
|
uint initialCenterLandblockId,
|
|
float[] heightTable,
|
|
TerrainAtlas? atlas)
|
|
{
|
|
int centerX = (int)((initialCenterLandblockId >> 24) & 0xFFu);
|
|
int centerY = (int)((initialCenterLandblockId >> 16) & 0xFFu);
|
|
if (atlas is null)
|
|
{
|
|
return new WorldTerrainBuildContext(
|
|
initialCenterLandblockId,
|
|
centerX,
|
|
centerY,
|
|
heightTable,
|
|
new TerrainBlendingContext(
|
|
TerrainTypeToLayer: new Dictionary<uint, byte>(),
|
|
RoadLayer: SurfaceInfo.None,
|
|
CornerAlphaLayers: [],
|
|
SideAlphaLayers: [],
|
|
RoadAlphaLayers: [],
|
|
CornerAlphaTCodes: [],
|
|
SideAlphaTCodes: [],
|
|
RoadAlphaRCodes: []),
|
|
new ConcurrentDictionary<uint, SurfaceInfo>());
|
|
}
|
|
|
|
var layers = new Dictionary<uint, byte>(atlas.TerrainTypeToLayer.Count);
|
|
foreach ((uint terrainType, uint layer) in atlas.TerrainTypeToLayer)
|
|
layers[terrainType] = (byte)layer;
|
|
|
|
const uint RoadTypeEnumValue = 0x20;
|
|
byte roadLayer = layers.TryGetValue(RoadTypeEnumValue, out byte road)
|
|
? road
|
|
: SurfaceInfo.None;
|
|
var blending = new TerrainBlendingContext(
|
|
TerrainTypeToLayer: layers,
|
|
RoadLayer: roadLayer,
|
|
CornerAlphaLayers: atlas.CornerAlphaLayers,
|
|
SideAlphaLayers: atlas.SideAlphaLayers,
|
|
RoadAlphaLayers: atlas.RoadAlphaLayers,
|
|
CornerAlphaTCodes: atlas.CornerAlphaTCodes,
|
|
SideAlphaTCodes: atlas.SideAlphaTCodes,
|
|
RoadAlphaRCodes: atlas.RoadAlphaRCodes);
|
|
return new WorldTerrainBuildContext(
|
|
initialCenterLandblockId,
|
|
centerX,
|
|
centerY,
|
|
heightTable,
|
|
blending,
|
|
new ConcurrentDictionary<uint, SurfaceInfo>());
|
|
}
|
|
|
|
public WbMeshAdapter CreateMeshAdapter(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats,
|
|
IPreparedAssetSource preparedAssets,
|
|
IGpuResourceRetirementQueue retirement,
|
|
ResidencyBudgetOptions budgets) =>
|
|
new(
|
|
device,
|
|
dats,
|
|
preparedAssets,
|
|
NullLogger<WbMeshAdapter>.Instance,
|
|
retirement,
|
|
budgets);
|
|
|
|
public TextureCache CreateTextureCache(
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats,
|
|
IGpuResourceRetirementQueue retirement,
|
|
string diagnosticsDirectory,
|
|
ResidencyBudgetOptions budgets) =>
|
|
new(
|
|
device,
|
|
dats,
|
|
retirement,
|
|
diagnosticsDirectory,
|
|
budgets);
|
|
|
|
public void RegisterResidencySources(
|
|
ResidencyManager manager,
|
|
WbMeshAdapter? meshes,
|
|
TextureCache textures,
|
|
IPreparedAssetSource preparedAssets,
|
|
IAnimationLoader animations,
|
|
DatSoundCache? audio)
|
|
{
|
|
meshes?.RegisterResidencySources(manager);
|
|
textures.RegisterResidencySources(manager);
|
|
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
|
ResidencyDomain.PreparedPackage,
|
|
() => new ResidencyDomainSnapshot(
|
|
ResidencyDomain.PreparedPackage,
|
|
EntryCount: 1,
|
|
OwnerCount: 1,
|
|
Charges: new ResidencyCharges(
|
|
MappedVirtualBytes:
|
|
preparedAssets.MappedVirtualBytes))));
|
|
if (animations is RetailAnimationLoader retailAnimations)
|
|
{
|
|
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
|
ResidencyDomain.Animations,
|
|
() =>
|
|
{
|
|
AnimationCacheDiagnostics diagnostics =
|
|
retailAnimations.Diagnostics;
|
|
return new ResidencyDomainSnapshot(
|
|
ResidencyDomain.Animations,
|
|
EntryCount: diagnostics.Count,
|
|
OwnerCount: 0,
|
|
Charges: new ResidencyCharges(
|
|
DecodedBytes:
|
|
diagnostics.EstimatedBytes),
|
|
BudgetBytes: diagnostics.BudgetBytes,
|
|
Hits: diagnostics.Stats.Hits,
|
|
Misses: diagnostics.Stats.Misses,
|
|
Evictions: diagnostics.Stats.Evictions);
|
|
}));
|
|
}
|
|
if (audio is not null)
|
|
{
|
|
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
|
ResidencyDomain.Audio,
|
|
() =>
|
|
{
|
|
DatSoundCacheDiagnostics diagnostics = audio.Diagnostics;
|
|
return new ResidencyDomainSnapshot(
|
|
ResidencyDomain.Audio,
|
|
EntryCount: diagnostics.CachedWaveCount,
|
|
OwnerCount: 0,
|
|
Charges: new ResidencyCharges(
|
|
DecodedBytes: diagnostics.ResidentWaveBytes),
|
|
BudgetBytes: diagnostics.BudgetBytes,
|
|
Hits: diagnostics.Hits,
|
|
Misses: diagnostics.Misses,
|
|
Evictions: diagnostics.Evictions);
|
|
}));
|
|
}
|
|
}
|
|
|
|
public void Release(IDisposable resource) => resource.Dispose();
|
|
}
|
|
|
|
internal enum WorldRenderCompositionPoint
|
|
{
|
|
RegionLoaded,
|
|
EnvironmentInitialized,
|
|
TerrainAtlasAcquired,
|
|
SceneLightingPublished,
|
|
DebugLinesPublished,
|
|
DebugFontCreated,
|
|
TextRendererCreated,
|
|
HudResourcesPublished,
|
|
HudResourcesCompleted,
|
|
TerrainPublished,
|
|
TerrainBuildStatePublished,
|
|
MeshAdapterPublished,
|
|
TextureCachePublished,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Production Phase 4. It preserves the mandatory modern renderer and sole
|
|
/// DAT path while moving construction into an ordered, failure-injectable
|
|
/// ownership boundary.
|
|
/// </summary>
|
|
internal sealed class WorldRenderCompositionPhase
|
|
: IWorldRenderCompositionPhase<
|
|
GameWindowPlatformResult<GameWindowGraphics, IInputContext>,
|
|
ContentEffectsAudioResult,
|
|
SettingsDevToolsResult,
|
|
WorldRenderResult>
|
|
{
|
|
private readonly WorldRenderDependencies _dependencies;
|
|
private readonly IGameWindowWorldRenderPublication _publication;
|
|
private readonly IWorldRenderCompositionFactory _factory;
|
|
private readonly Action<WorldRenderCompositionPoint>? _faultInjection;
|
|
|
|
public WorldRenderCompositionPhase(
|
|
WorldRenderDependencies dependencies,
|
|
IGameWindowWorldRenderPublication publication,
|
|
IWorldRenderCompositionFactory? factory = null,
|
|
Action<WorldRenderCompositionPoint>? faultInjection = null)
|
|
{
|
|
_dependencies = dependencies
|
|
?? throw new ArgumentNullException(nameof(dependencies));
|
|
_publication = publication
|
|
?? throw new ArgumentNullException(nameof(publication));
|
|
_factory = factory ?? new RetailWorldRenderCompositionFactory();
|
|
_faultInjection = faultInjection;
|
|
}
|
|
|
|
public WorldRenderResult Compose(
|
|
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform,
|
|
ContentEffectsAudioResult content,
|
|
SettingsDevToolsResult settings)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(platform);
|
|
ArgumentNullException.ThrowIfNull(content);
|
|
ArgumentNullException.ThrowIfNull(settings);
|
|
|
|
var scope = new CompositionAcquisitionScope();
|
|
try
|
|
{
|
|
var residency = new ResidencyManager(
|
|
_dependencies.ResidencyBudgets);
|
|
|
|
WorldRegionData region = _factory.LoadRegion(content.Dats);
|
|
Fault(WorldRenderCompositionPoint.RegionLoaded);
|
|
_factory.InitializeEnvironment(_dependencies.Environment, region.Region);
|
|
Fault(WorldRenderCompositionPoint.EnvironmentInitialized);
|
|
|
|
// Campaign V slice V6i-2: the atlas builds through IGpuDevice on
|
|
// every remaining backend. The raw-GL arm that built it from raw
|
|
// texture names was deleted at slice V11.
|
|
TerrainAtlas terrainAtlas = _factory.AcquireBackendNeutralTerrainAtlas(
|
|
_dependencies.RenderResources,
|
|
_dependencies.GpuDevice,
|
|
content.Dats);
|
|
_factory.SetTerrainAnisotropic(
|
|
terrainAtlas,
|
|
settings.ResolvedQuality.AnisotropicLevel);
|
|
Fault(WorldRenderCompositionPoint.TerrainAtlasAcquired);
|
|
|
|
// The rest of the world texture stack's creation path: one shared
|
|
// array of each format family and one composite array, created and
|
|
// released here. Nothing draws them; see the type's own
|
|
// documentation for why they are built anyway. It claims no
|
|
// composition point and enters no acquisition scope — the frozen
|
|
// publication order is a pinned assertion, and an exercise that
|
|
// owns nothing past its own statement is not a published owner.
|
|
_factory.ExerciseBackendNeutralWorldTextures(
|
|
_dependencies.GpuDevice,
|
|
_dependencies.Log);
|
|
|
|
string shadersDirectory = Path.Combine(
|
|
AppContext.BaseDirectory,
|
|
"Rendering",
|
|
"Shaders");
|
|
// Campaign V slice V6j: scene lighting publishes a ring section on
|
|
// the world pass scope; the raw-GL global-uniform-binding arm was
|
|
// deleted at slice V11.
|
|
IWorldPassScope worldPassScope = platform.Graphics.WorldPassScope
|
|
?? throw new InvalidOperationException(
|
|
"The graphics backend must publish a world pass scope.");
|
|
SceneLightingUboBinding sceneLighting = AcquireAndPublish(
|
|
scope,
|
|
"scene lighting",
|
|
() => _factory.CreateBackendNeutralSceneLighting(
|
|
_dependencies.GpuFrameSource,
|
|
worldPassScope),
|
|
_publication.PublishSceneLighting,
|
|
WorldRenderCompositionPoint.SceneLightingPublished);
|
|
DebugLineRenderer debugLines = AcquireAndPublish(
|
|
scope,
|
|
"debug lines",
|
|
() => _factory.CreateDebugLines(
|
|
_dependencies.GpuDevice,
|
|
_dependencies.GpuFrameSource,
|
|
shadersDirectory),
|
|
_publication.PublishDebugLines,
|
|
WorldRenderCompositionPoint.DebugLinesPublished);
|
|
|
|
(BitmapFont? debugFont, TextRenderer? textRenderer) =
|
|
ComposeOptionalHudResources(scope, shadersDirectory);
|
|
|
|
// Campaign V slice V6j: terrain records into the world pass and
|
|
// reads the same backend-neutral atlas built above. The raw-GL arm
|
|
// was deleted at slice V11.
|
|
TerrainModernRenderer terrain = AcquireAndPublish(
|
|
scope,
|
|
"terrain renderer",
|
|
() => _factory.CreateBackendNeutralTerrain(
|
|
_dependencies.GpuDevice,
|
|
_dependencies.GpuFrameSource,
|
|
worldPassScope,
|
|
terrainAtlas,
|
|
_dependencies.ResourceRetirement),
|
|
_publication.PublishTerrain,
|
|
WorldRenderCompositionPoint.TerrainPublished);
|
|
|
|
WorldTerrainBuildContext terrainBuild =
|
|
_factory.CreateTerrainBuildContext(
|
|
_dependencies.InitialCenterLandblockId,
|
|
region.HeightTable,
|
|
terrainAtlas);
|
|
_publication.PublishTerrainBuildState(
|
|
terrainBuild.HeightTable,
|
|
terrainBuild.Blending,
|
|
terrainBuild.SurfaceCache);
|
|
Fault(WorldRenderCompositionPoint.TerrainBuildStatePublished);
|
|
|
|
// Campaign V slice V6i-3: the mesh pipeline builds the same arena,
|
|
// atlases and render data on every remaining backend, which is what
|
|
// makes streaming's publication into GPU state real rather than a
|
|
// no-op. The raw-GL arm was deleted at slice V11, and its
|
|
// constructor's GL? gl parameter (always null here) was removed in
|
|
// the same slice's package/shader cleanup.
|
|
WbMeshAdapter meshAdapter = AcquireAndPublish(
|
|
scope,
|
|
"WB mesh adapter",
|
|
() => _factory.CreateMeshAdapter(
|
|
_dependencies.GpuDevice,
|
|
content.Dats,
|
|
content.PreparedAssets,
|
|
_dependencies.ResourceRetirement,
|
|
residency.Budgets),
|
|
_publication.PublishWbMeshAdapter,
|
|
WorldRenderCompositionPoint.MeshAdapterPublished);
|
|
TextureCache textureCache = AcquireAndPublish(
|
|
scope,
|
|
"texture cache",
|
|
() => _factory.CreateTextureCache(
|
|
_dependencies.GpuDevice,
|
|
content.Dats,
|
|
_dependencies.ResourceRetirement,
|
|
_dependencies.DiagnosticsDirectory,
|
|
residency.Budgets),
|
|
_publication.PublishTextureCache,
|
|
WorldRenderCompositionPoint.TextureCachePublished);
|
|
_factory.RegisterResidencySources(
|
|
residency,
|
|
meshAdapter,
|
|
textureCache,
|
|
content.PreparedAssets,
|
|
content.AnimationLoader,
|
|
content.Audio?.SoundCache);
|
|
|
|
scope.Complete();
|
|
_dependencies.Log(
|
|
"[N.4+N.5] WB foundation + modern path active — " +
|
|
"routing all content through ObjectMeshManager.");
|
|
return new WorldRenderResult(
|
|
terrainBuild,
|
|
new WorldRenderFoundation(
|
|
shadersDirectory,
|
|
terrainAtlas,
|
|
sceneLighting,
|
|
debugLines,
|
|
debugFont,
|
|
textRenderer,
|
|
terrain,
|
|
meshAdapter,
|
|
textureCache,
|
|
residency));
|
|
}
|
|
catch (Exception failure)
|
|
{
|
|
scope.RollbackAndThrow(failure);
|
|
throw new System.Diagnostics.UnreachableException();
|
|
}
|
|
}
|
|
|
|
private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(
|
|
CompositionAcquisitionScope scope,
|
|
string shadersDirectory)
|
|
{
|
|
byte[]? fontBytes = _factory.TryLoadDebugFont();
|
|
if (fontBytes is null)
|
|
{
|
|
_dependencies.Log("world-hud font: no system monospace font found");
|
|
Fault(WorldRenderCompositionPoint.HudResourcesCompleted);
|
|
return (null, null);
|
|
}
|
|
|
|
var fontLease = scope.Acquire(
|
|
"world HUD font",
|
|
() => _factory.CreateDebugFont(_dependencies.GpuDevice, fontBytes),
|
|
_factory.Release);
|
|
BitmapFont font = fontLease.Resource;
|
|
Fault(WorldRenderCompositionPoint.DebugFontCreated);
|
|
var textLease = scope.Acquire(
|
|
"world HUD text renderer",
|
|
() => _factory.CreateTextRenderer(
|
|
_dependencies.GpuDevice,
|
|
_dependencies.GpuFrameSource,
|
|
shadersDirectory),
|
|
_factory.Release);
|
|
TextRenderer text = textLease.Resource;
|
|
Fault(WorldRenderCompositionPoint.TextRendererCreated);
|
|
|
|
_publication.PublishHudResources(font, text);
|
|
fontLease.Transfer();
|
|
textLease.Transfer();
|
|
Fault(WorldRenderCompositionPoint.HudResourcesPublished);
|
|
_dependencies.Log(
|
|
$"world-hud font: loaded {fontBytes.Length / 1024}KB, " +
|
|
$"atlas {font.AtlasWidth}x{font.AtlasHeight}, " +
|
|
$"lineHeight={font.LineHeight:F1}px (reserved for D.6 HUD)");
|
|
Fault(WorldRenderCompositionPoint.HudResourcesCompleted);
|
|
return (font, text);
|
|
}
|
|
|
|
private T AcquireAndPublish<T>(
|
|
CompositionAcquisitionScope scope,
|
|
string name,
|
|
Func<T> factory,
|
|
Action<T> publish,
|
|
WorldRenderCompositionPoint point)
|
|
where T : class, IDisposable
|
|
{
|
|
T value = scope.Acquire(name, factory, _factory.Release).Publish(publish);
|
|
Fault(point);
|
|
return value;
|
|
}
|
|
|
|
private void Fault(WorldRenderCompositionPoint point) =>
|
|
_faultInjection?.Invoke(point);
|
|
}
|