ACDREAM_RENDER_BACKEND=vulkan now runs the real GameWindow composition rather
than a second main(). All nine phases execute: DAT load, streaming, camera,
entity table, session, and the real retained UiHost drawing through the RHI.
No world renderers — they are raw GL until V4t and the world arm behind it.
The offline log is the client's own (acdream.pak opened, 6266 spells, Region
0x13000000, "loading world view centered on 0xA9B4FFFF", fourteen retail
LayoutDesc lines, streaming radii), and the captured frame is the retail
retained UI: vitals, combat/spell bar with DAT scarab icons, the nine-slot
toolbar, chat with tabs and Send, radar/compass with dat-font glyphs. Sampled
against the GL capture the widgets agree — chat interior RGBA (25,24,27,158)
vs (22,21,23,158), vitals bar (117,1,0) and toolbar slot (0,11,17) identical.
Three seams, as §5.5.9 specified:
1. Platform acquisition — already generic — publishes GameWindowGraphics
instead of a bare GL. Phases that still speak raw GL read Graphics.Gl and
take their Vulkan arm when it is null; each branch names the slice that
removes it.
2. VulkanHostInputCameraCompositionFactory is a new file and the whole of the
Phase-1 fork: four graphics members differ, input/camera/pointer delegate.
The default factory is chosen inside the phase from the platform result.
HostInputCameraResult gained backend-neutral Retirement and FrameSlots.
3. The frame root forks on one condition. The GL world-scene assembly is
unchanged, wrapped in `if (gl is not null)`; the Vulkan arm's graph is one
backbuffer clear pass computing the same RenderFrameFoundation from the same
clock and weather owners, then private presentation over it.
§5.5.9's three TextureCache couplings are unpicked: the constructor takes GL?
and rejects bindless without one, world entry points route through a Gl
property that throws naming V4t, and the (GlGpuTexture) VRAM-accounting cast
became a backend test. That cast's stated reason — DrawSprite's texture-unit
binding — was already stale, deleted at V6d.
VulkanBringUpHost is reduced to the capability-probe harness it is named for:
the instance/surface/device/swapchain sequence moved into VulkanGraphicsContext,
which the composition host and the harness now share. It is reached only with
ACDREAM_VULKAN_PROBE=1.
One latent Vulkan defect surfaced and is fixed here. The first composition-host
frame died with ErrorDeviceLost; validation named VUID-vkCmdDraw-None-08600 —
descriptor set 2 never bound. VulkanGpuPassEncoder bound sets 0/1/2 only as a
side effect of BindStorageBuffer/BindUniformBuffer, so a pass sampling the
texture table while binding no buffer — every retained-UI and debug-line pass —
drew with the table unbound. It survived V6c-V6g because the bring-up host
always drew VulkanRhiScene first and the UI pass inherited its binds; the
composition host has no 3-D scene. The fix is one line in the encoder's
constructor beside the viewport and scissor defaults, which exist for exactly
the same reason: a pass opens with complete binding state rather than depending
on what preceded it.
Gates: strict GL offline pixel gate against 46d893f7 measures 1.24e-05 (7 of
563,200 pixels), inside the documented 15-23 px / 4.1e-05 band, so GL behaviour
did not move. App tests 4,075/3 skips; complete Release suite 9,138/5 skips.
One full Vulkan run with VK_LAYER_KHRONOS_validation: zero errors, zero
warnings. Both Vulkan runs converged the ownership ledger — no [shutdown]
diagnostic on either stream. The reduced probe harness presented 34,811
validation-clean frames.
No divergence-register row: GL is the shipping backend and the pixel gate proves
it unmoved; the Vulkan arm is not a retail deviation but a backend under
construction.
Next is V4t, the texture stack, which the world arm cannot be written without.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
746 lines
28 KiB
C#
746 lines
28 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;
|
|
using Silk.NET.OpenGL;
|
|
using Shader = AcDream.App.Rendering.Shader;
|
|
|
|
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>Campaign V slice V6h made every raw-GL member nullable. They are all
|
|
/// present on GL and all absent on Vulkan, because the world renderers that own
|
|
/// them are still raw GL until slices V4t/V4c/V4d land the Vulkan world arm.
|
|
/// What survives on both backends is exactly the RHI-ported set — the texture
|
|
/// cache's UI path, the debug font, the text renderer and the debug lines — plus
|
|
/// the backend-neutral residency ledger and shader directory.</para>
|
|
/// </summary>
|
|
internal sealed record WorldRenderFoundation(
|
|
string ShadersDirectory,
|
|
BindlessSupport? Bindless,
|
|
TerrainAtlas? TerrainAtlas,
|
|
Shader? TerrainShader,
|
|
SceneLightingUboBinding? SceneLighting,
|
|
DebugLineRenderer DebugLines,
|
|
BitmapFont? DebugFont,
|
|
TextRenderer? TextRenderer,
|
|
TerrainModernRenderer? Terrain,
|
|
Shader? MeshShader,
|
|
WbMeshAdapter? MeshAdapter,
|
|
TextureCache TextureCache,
|
|
SamplerCache? Samplers,
|
|
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 PublishBindlessSupport(BindlessSupport value);
|
|
void PublishTerrainShader(Shader value);
|
|
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 PublishMeshShader(Shader value);
|
|
void PublishWbMeshAdapter(WbMeshAdapter value);
|
|
void PublishTextureCache(TextureCache value);
|
|
void PublishSamplerCache(SamplerCache value);
|
|
}
|
|
|
|
internal interface IWorldRenderCompositionFactory
|
|
{
|
|
void InitializeGlState(GL gl);
|
|
WorldRegionData LoadRegion(IDatReaderWriter dats);
|
|
void InitializeEnvironment(WorldEnvironmentController environment, Region region);
|
|
BindlessSupport RequireBindless(GL gl, Action<string> log);
|
|
TerrainAtlas AcquireTerrainAtlas(
|
|
IGameRenderResourceLifetime lifetime,
|
|
GL gl,
|
|
IDatReaderWriter dats,
|
|
BindlessSupport bindless);
|
|
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
|
|
Shader CreateTerrainShader(GL gl, string shadersDirectory);
|
|
SceneLightingUboBinding CreateSceneLighting(GL gl);
|
|
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);
|
|
TerrainModernRenderer CreateTerrain(
|
|
GL gl,
|
|
BindlessSupport bindless,
|
|
Shader shader,
|
|
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);
|
|
Shader CreateMeshShader(GL gl, string shadersDirectory);
|
|
WbMeshAdapter CreateMeshAdapter(
|
|
GL gl,
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats,
|
|
IPreparedAssetSource preparedAssets,
|
|
IGpuResourceRetirementQueue retirement,
|
|
ResidencyBudgetOptions budgets);
|
|
TextureCache CreateTextureCache(
|
|
GL? gl,
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats,
|
|
BindlessSupport? bindless,
|
|
IGpuResourceRetirementQueue retirement,
|
|
string diagnosticsDirectory,
|
|
ResidencyBudgetOptions budgets);
|
|
void RegisterResidencySources(
|
|
ResidencyManager manager,
|
|
WbMeshAdapter? meshes,
|
|
TextureCache textures,
|
|
IPreparedAssetSource preparedAssets,
|
|
IAnimationLoader animations,
|
|
DatSoundCache? audio);
|
|
SamplerCache CreateSamplerCache(GL gl);
|
|
void Release(IDisposable resource);
|
|
}
|
|
|
|
internal sealed class RetailWorldRenderCompositionFactory
|
|
: IWorldRenderCompositionFactory
|
|
{
|
|
public void InitializeGlState(GL gl)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(gl);
|
|
gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
|
|
gl.Enable(EnableCap.DepthTest);
|
|
}
|
|
|
|
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 BindlessSupport RequireBindless(GL gl, Action<string> log)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(gl);
|
|
ArgumentNullException.ThrowIfNull(log);
|
|
if (BindlessSupport.TryCreate(gl, out BindlessSupport? bindless))
|
|
{
|
|
if (bindless!.HasShaderDrawParameters(gl))
|
|
{
|
|
log("[N.5] modern path capabilities present " +
|
|
"(bindless + ARB_shader_draw_parameters)");
|
|
return bindless;
|
|
}
|
|
log("[N.5] GL_ARB_shader_draw_parameters not present — " +
|
|
"modern path not available");
|
|
}
|
|
else
|
|
{
|
|
log("[N.5] GL_ARB_bindless_texture not present — " +
|
|
"modern path not available");
|
|
}
|
|
|
|
throw new NotSupportedException(
|
|
"acdream requires GL_ARB_bindless_texture + " +
|
|
"GL_ARB_shader_draw_parameters (GL 4.3+ with bindless support). " +
|
|
"Your GPU/driver does not expose these extensions. If this is " +
|
|
"unexpected, please file a bug report with your GPU vendor + " +
|
|
"driver version.");
|
|
}
|
|
|
|
public TerrainAtlas AcquireTerrainAtlas(
|
|
IGameRenderResourceLifetime lifetime,
|
|
GL gl,
|
|
IDatReaderWriter dats,
|
|
BindlessSupport bindless)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(lifetime);
|
|
return lifetime.AcquireTerrainAtlas(
|
|
() => TerrainAtlas.Build(gl, dats, bindless));
|
|
}
|
|
|
|
public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) =>
|
|
atlas.SetAnisotropic(level);
|
|
|
|
public Shader CreateTerrainShader(GL gl, string shadersDirectory) =>
|
|
new(
|
|
gl,
|
|
Path.Combine(shadersDirectory, "terrain_modern.vert"),
|
|
Path.Combine(shadersDirectory, "terrain_modern.frag"),
|
|
includeCommonPreamble: true);
|
|
|
|
public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl);
|
|
|
|
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 CreateTerrain(
|
|
GL gl,
|
|
BindlessSupport bindless,
|
|
Shader shader,
|
|
TerrainAtlas atlas,
|
|
IGpuResourceRetirementQueue retirement) =>
|
|
new(gl, bindless, shader, 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 Shader CreateMeshShader(GL gl, string shadersDirectory) =>
|
|
new(
|
|
gl,
|
|
Path.Combine(shadersDirectory, "mesh_modern.vert"),
|
|
Path.Combine(shadersDirectory, "mesh_modern.frag"),
|
|
includeCommonPreamble: true);
|
|
|
|
public WbMeshAdapter CreateMeshAdapter(
|
|
GL gl,
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats,
|
|
IPreparedAssetSource preparedAssets,
|
|
IGpuResourceRetirementQueue retirement,
|
|
ResidencyBudgetOptions budgets) =>
|
|
new(
|
|
gl,
|
|
device,
|
|
dats,
|
|
preparedAssets,
|
|
NullLogger<WbMeshAdapter>.Instance,
|
|
retirement,
|
|
budgets);
|
|
|
|
public TextureCache CreateTextureCache(
|
|
GL? gl,
|
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
|
IDatReaderWriter dats,
|
|
BindlessSupport? bindless,
|
|
IGpuResourceRetirementQueue retirement,
|
|
string diagnosticsDirectory,
|
|
ResidencyBudgetOptions budgets) =>
|
|
new(
|
|
gl,
|
|
device,
|
|
dats,
|
|
bindless,
|
|
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 SamplerCache CreateSamplerCache(GL gl) => new(gl);
|
|
|
|
public void Release(IDisposable resource) => resource.Dispose();
|
|
}
|
|
|
|
internal enum WorldRenderCompositionPoint
|
|
{
|
|
GlStateInitialized,
|
|
RegionLoaded,
|
|
EnvironmentInitialized,
|
|
BindlessPublished,
|
|
TerrainAtlasAcquired,
|
|
TerrainShaderPublished,
|
|
SceneLightingPublished,
|
|
DebugLinesPublished,
|
|
DebugFontCreated,
|
|
TextRendererCreated,
|
|
HudResourcesPublished,
|
|
HudResourcesCompleted,
|
|
TerrainPublished,
|
|
TerrainBuildStatePublished,
|
|
MeshShaderPublished,
|
|
MeshAdapterPublished,
|
|
TextureCachePublished,
|
|
SamplerCachePublished,
|
|
}
|
|
|
|
/// <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
|
|
{
|
|
// Campaign V slice V6h: null on a backend with no GL context. Every
|
|
// world renderer below is still raw GL, so the Vulkan arm composes
|
|
// the RHI-ported subset only — the texture cache's UI path, the
|
|
// debug font, the text renderer, the debug lines — and leaves the
|
|
// world absent until slices V4t/V4c/V4d land it.
|
|
GL? gl = platform.Graphics.Gl;
|
|
var residency = new ResidencyManager(
|
|
_dependencies.ResidencyBudgets);
|
|
if (gl is not null)
|
|
_factory.InitializeGlState(gl);
|
|
Fault(WorldRenderCompositionPoint.GlStateInitialized);
|
|
|
|
WorldRegionData region = _factory.LoadRegion(content.Dats);
|
|
Fault(WorldRenderCompositionPoint.RegionLoaded);
|
|
_factory.InitializeEnvironment(_dependencies.Environment, region.Region);
|
|
Fault(WorldRenderCompositionPoint.EnvironmentInitialized);
|
|
|
|
BindlessSupport? bindless = gl is null
|
|
? null
|
|
: _factory.RequireBindless(gl, _dependencies.Log);
|
|
if (bindless is not null)
|
|
_publication.PublishBindlessSupport(bindless);
|
|
Fault(WorldRenderCompositionPoint.BindlessPublished);
|
|
|
|
TerrainAtlas? terrainAtlas = gl is null || bindless is null
|
|
? null
|
|
: _factory.AcquireTerrainAtlas(
|
|
_dependencies.RenderResources,
|
|
gl,
|
|
content.Dats,
|
|
bindless);
|
|
if (terrainAtlas is not null)
|
|
{
|
|
_factory.SetTerrainAnisotropic(
|
|
terrainAtlas,
|
|
settings.ResolvedQuality.AnisotropicLevel);
|
|
}
|
|
Fault(WorldRenderCompositionPoint.TerrainAtlasAcquired);
|
|
|
|
string shadersDirectory = Path.Combine(
|
|
AppContext.BaseDirectory,
|
|
"Rendering",
|
|
"Shaders");
|
|
Shader? terrainShader = AcquireAndPublishIf(
|
|
gl is not null,
|
|
scope,
|
|
"terrain shader",
|
|
() => _factory.CreateTerrainShader(gl!, shadersDirectory),
|
|
_publication.PublishTerrainShader,
|
|
WorldRenderCompositionPoint.TerrainShaderPublished);
|
|
SceneLightingUboBinding? sceneLighting = AcquireAndPublishIf(
|
|
gl is not null,
|
|
scope,
|
|
"scene lighting",
|
|
() => _factory.CreateSceneLighting(gl!),
|
|
_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);
|
|
|
|
TerrainModernRenderer? terrain = AcquireAndPublishIf(
|
|
gl is not null,
|
|
scope,
|
|
"terrain renderer",
|
|
() => _factory.CreateTerrain(
|
|
gl!,
|
|
bindless!,
|
|
terrainShader!,
|
|
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);
|
|
|
|
Shader? meshShader = AcquireAndPublishIf(
|
|
gl is not null,
|
|
scope,
|
|
"mesh shader",
|
|
() => _factory.CreateMeshShader(gl!, shadersDirectory),
|
|
_publication.PublishMeshShader,
|
|
WorldRenderCompositionPoint.MeshShaderPublished);
|
|
if (meshShader is not null)
|
|
_dependencies.Log("[N.5] mesh_modern shader loaded");
|
|
WbMeshAdapter? meshAdapter = AcquireAndPublishIf(
|
|
gl is not null,
|
|
scope,
|
|
"WB mesh adapter",
|
|
() => _factory.CreateMeshAdapter(
|
|
gl!,
|
|
_dependencies.GpuDevice,
|
|
content.Dats,
|
|
content.PreparedAssets,
|
|
_dependencies.ResourceRetirement,
|
|
residency.Budgets),
|
|
_publication.PublishWbMeshAdapter,
|
|
WorldRenderCompositionPoint.MeshAdapterPublished);
|
|
TextureCache textureCache = AcquireAndPublish(
|
|
scope,
|
|
"texture cache",
|
|
() => _factory.CreateTextureCache(
|
|
gl,
|
|
_dependencies.GpuDevice,
|
|
content.Dats,
|
|
bindless,
|
|
_dependencies.ResourceRetirement,
|
|
_dependencies.DiagnosticsDirectory,
|
|
residency.Budgets),
|
|
_publication.PublishTextureCache,
|
|
WorldRenderCompositionPoint.TextureCachePublished);
|
|
SamplerCache? samplers = AcquireAndPublishIf(
|
|
gl is not null,
|
|
scope,
|
|
"sampler cache",
|
|
() => _factory.CreateSamplerCache(gl!),
|
|
_publication.PublishSamplerCache,
|
|
WorldRenderCompositionPoint.SamplerCachePublished);
|
|
_factory.RegisterResidencySources(
|
|
residency,
|
|
meshAdapter,
|
|
textureCache,
|
|
content.PreparedAssets,
|
|
content.AnimationLoader,
|
|
content.Audio?.SoundCache);
|
|
|
|
scope.Complete();
|
|
_dependencies.Log(
|
|
meshAdapter is not null
|
|
? "[N.4+N.5] WB foundation + modern path active — " +
|
|
"routing all content through ObjectMeshManager."
|
|
: "[V6h] Vulkan composition host — RHI foundation active " +
|
|
"(retained UI, text, debug lines); no world renderers.");
|
|
return new WorldRenderResult(
|
|
terrainBuild,
|
|
new WorldRenderFoundation(
|
|
shadersDirectory,
|
|
bindless,
|
|
terrainAtlas,
|
|
terrainShader,
|
|
sceneLighting,
|
|
debugLines,
|
|
debugFont,
|
|
textRenderer,
|
|
terrain,
|
|
meshShader,
|
|
meshAdapter,
|
|
textureCache,
|
|
samplers,
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6h: acquires an owner the active backend may not have.
|
|
/// The fault point still fires on both arms so a failure-injection test
|
|
/// covers the same ordered sequence whichever backend composed it.
|
|
/// </summary>
|
|
private T? AcquireAndPublishIf<T>(
|
|
bool supported,
|
|
CompositionAcquisitionScope scope,
|
|
string name,
|
|
Func<T> factory,
|
|
Action<T> publish,
|
|
WorldRenderCompositionPoint point)
|
|
where T : class, IDisposable
|
|
{
|
|
T? value = supported
|
|
? scope.Acquire(name, factory, _factory.Release).Publish(publish)
|
|
: null;
|
|
Fault(point);
|
|
return value;
|
|
}
|
|
|
|
private void Fault(WorldRenderCompositionPoint point) =>
|
|
_faultInjection?.Invoke(point);
|
|
}
|