refactor(app): compose world rendering startup

Move Region/environment, mandatory modern rendering, terrain, WB, texture, and sampler construction behind the typed Phase-4 composition boundary. Give every fallible GL constructor prefix retryable ownership so partial startup failure cannot leak or replay resource deletion while preserving the accepted render path and DAT inputs.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 16:46:05 +02:00
parent cd7b519f78
commit 6a2fe98cc4
22 changed files with 1983 additions and 634 deletions

View file

@ -0,0 +1,534 @@
using System.Collections.Concurrent;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.App.World;
using AcDream.Content;
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);
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);
internal sealed record WorldRenderResult(
WorldTerrainBuildContext TerrainBuild,
WorldRenderFoundation Foundation);
internal sealed record WorldRenderDependencies(
WorldEnvironmentController Environment,
IGameRenderResourceLifetime RenderResources,
IGpuResourceRetirementQueue ResourceRetirement,
uint InitialCenterLandblockId,
Action<string> Log);
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(GL gl, string shadersDirectory);
byte[]? TryLoadDebugFont();
BitmapFont CreateDebugFont(GL gl, byte[] bytes);
TextRenderer CreateTextRenderer(GL gl, string shadersDirectory);
TerrainModernRenderer CreateTerrain(
GL gl,
BindlessSupport bindless,
Shader shader,
TerrainAtlas atlas,
IGpuResourceRetirementQueue retirement);
WorldTerrainBuildContext CreateTerrainBuildContext(
uint initialCenterLandblockId,
float[] heightTable,
TerrainAtlas atlas);
Shader CreateMeshShader(GL gl, string shadersDirectory);
WbMeshAdapter CreateMeshAdapter(
GL gl,
IDatReaderWriter dats,
IGpuResourceRetirementQueue retirement);
TextureCache CreateTextureCache(
GL gl,
IDatReaderWriter dats,
BindlessSupport bindless,
IGpuResourceRetirementQueue retirement);
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"));
public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl);
public DebugLineRenderer CreateDebugLines(
GL gl,
string shadersDirectory) =>
new(gl, shadersDirectory);
public byte[]? TryLoadDebugFont() =>
BitmapFont.TryLoadSystemMonospaceFont();
public BitmapFont CreateDebugFont(GL gl, byte[] bytes) =>
new(gl, bytes, pixelHeight: 15f, atlasSize: 512);
public TextRenderer CreateTextRenderer(
GL gl,
string shadersDirectory) =>
new(gl, 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);
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"));
public WbMeshAdapter CreateMeshAdapter(
GL gl,
IDatReaderWriter dats,
IGpuResourceRetirementQueue retirement) =>
new(
gl,
dats,
NullLogger<WbMeshAdapter>.Instance,
retirement);
public TextureCache CreateTextureCache(
GL gl,
IDatReaderWriter dats,
BindlessSupport bindless,
IGpuResourceRetirementQueue retirement) =>
new(gl, dats, bindless, retirement);
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<GL, 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<GL, IInputContext> platform,
ContentEffectsAudioResult content,
SettingsDevToolsResult settings)
{
ArgumentNullException.ThrowIfNull(platform);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(settings);
var scope = new CompositionAcquisitionScope();
try
{
GL gl = platform.Graphics;
_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 = _factory.RequireBindless(gl, _dependencies.Log);
_publication.PublishBindlessSupport(bindless);
Fault(WorldRenderCompositionPoint.BindlessPublished);
TerrainAtlas terrainAtlas = _factory.AcquireTerrainAtlas(
_dependencies.RenderResources,
gl,
content.Dats,
bindless);
_factory.SetTerrainAnisotropic(
terrainAtlas,
settings.ResolvedQuality.AnisotropicLevel);
Fault(WorldRenderCompositionPoint.TerrainAtlasAcquired);
string shadersDirectory = Path.Combine(
AppContext.BaseDirectory,
"Rendering",
"Shaders");
Shader terrainShader = AcquireAndPublish(
scope,
"terrain shader",
() => _factory.CreateTerrainShader(gl, shadersDirectory),
_publication.PublishTerrainShader,
WorldRenderCompositionPoint.TerrainShaderPublished);
SceneLightingUboBinding sceneLighting = AcquireAndPublish(
scope,
"scene lighting",
() => _factory.CreateSceneLighting(gl),
_publication.PublishSceneLighting,
WorldRenderCompositionPoint.SceneLightingPublished);
DebugLineRenderer debugLines = AcquireAndPublish(
scope,
"debug lines",
() => _factory.CreateDebugLines(gl, shadersDirectory),
_publication.PublishDebugLines,
WorldRenderCompositionPoint.DebugLinesPublished);
(BitmapFont? debugFont, TextRenderer? textRenderer) =
ComposeOptionalHudResources(scope, gl, shadersDirectory);
TerrainModernRenderer terrain = AcquireAndPublish(
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 = AcquireAndPublish(
scope,
"mesh shader",
() => _factory.CreateMeshShader(gl, shadersDirectory),
_publication.PublishMeshShader,
WorldRenderCompositionPoint.MeshShaderPublished);
_dependencies.Log("[N.5] mesh_modern shader loaded");
WbMeshAdapter meshAdapter = AcquireAndPublish(
scope,
"WB mesh adapter",
() => _factory.CreateMeshAdapter(
gl,
content.Dats,
_dependencies.ResourceRetirement),
_publication.PublishWbMeshAdapter,
WorldRenderCompositionPoint.MeshAdapterPublished);
TextureCache textureCache = AcquireAndPublish(
scope,
"texture cache",
() => _factory.CreateTextureCache(
gl,
content.Dats,
bindless,
_dependencies.ResourceRetirement),
_publication.PublishTextureCache,
WorldRenderCompositionPoint.TextureCachePublished);
SamplerCache samplers = AcquireAndPublish(
scope,
"sampler cache",
() => _factory.CreateSamplerCache(gl),
_publication.PublishSamplerCache,
WorldRenderCompositionPoint.SamplerCachePublished);
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,
bindless,
terrainAtlas,
terrainShader,
sceneLighting,
debugLines,
debugFont,
textRenderer,
terrain,
meshShader,
meshAdapter,
textureCache,
samplers));
}
catch (Exception failure)
{
scope.RollbackAndThrow(failure);
throw new System.Diagnostics.UnreachableException();
}
}
private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(
CompositionAcquisitionScope scope,
GL gl,
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(gl, fontBytes),
_factory.Release);
BitmapFont font = fontLease.Resource;
Fault(WorldRenderCompositionPoint.DebugFontCreated);
var textLease = scope.Acquire(
"world HUD text renderer",
() => _factory.CreateTextRenderer(gl, 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);
}