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);
}

View file

@ -38,6 +38,7 @@ public sealed unsafe class BitmapFont : IDisposable
private readonly Glyph[] _glyphs;
private readonly int _firstChar;
private readonly int _numChars;
private readonly ResourceCleanupGroup _resources;
public uint TextureId { get; }
public float PixelHeight { get; }
@ -95,27 +96,65 @@ public sealed unsafe class BitmapFont : IDisposable
adv: bc.xadvance);
}
// Upload atlas as a single-channel GL texture (R8).
TextureId = _gl.GenTexture();
_gl.BindTexture(TextureTarget.Texture2D, TextureId);
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
fixed (byte* ptr = pixels)
// Upload atlas as a single-channel GL texture (R8). Publish the GL
// name into the construction ledger before any later upload/state
// command can fail.
var resources = new ResourceCleanupGroup();
uint texture = 0;
try
{
_gl.TexImage2D(TextureTarget.Texture2D, 0,
(int)InternalFormat.R8,
(uint)AtlasWidth, (uint)AtlasHeight, 0,
PixelFormat.Red, PixelType.UnsignedByte, ptr);
texture = GlResourceCommand.CreateTexture(_gl, "BitmapFont atlas");
uint ownedTexture = texture;
resources.Add(
"bitmap-font atlas",
() => GlResourceCommand.DeleteTexture(
_gl,
ownedTexture,
$"delete BitmapFont atlas {ownedTexture}"));
_gl.GetInteger(GetPName.TextureBinding2D, out int previousTexture);
_gl.GetInteger(GetPName.UnpackAlignment, out int previousAlignment);
GlResourceCommand.Execute(_gl, "initialize BitmapFont atlas", () =>
{
try
{
_gl.BindTexture(TextureTarget.Texture2D, texture);
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
fixed (byte* ptr = pixels)
{
_gl.TexImage2D(TextureTarget.Texture2D, 0,
(int)InternalFormat.R8,
(uint)AtlasWidth, (uint)AtlasHeight, 0,
PixelFormat.Red, PixelType.UnsignedByte, ptr);
}
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
(int)TextureMinFilter.Linear);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter,
(int)TextureMagFilter.Linear);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
(int)TextureWrapMode.ClampToEdge);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
(int)TextureWrapMode.ClampToEdge);
}
finally
{
_gl.PixelStore(
PixelStoreParameter.UnpackAlignment,
previousAlignment);
_gl.BindTexture(
TextureTarget.Texture2D,
unchecked((uint)previousTexture));
}
});
}
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
(int)TextureMinFilter.Linear);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter,
(int)TextureMagFilter.Linear);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
(int)TextureWrapMode.ClampToEdge);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
(int)TextureWrapMode.ClampToEdge);
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 4); // restore default
_gl.BindTexture(TextureTarget.Texture2D, 0);
catch (Exception constructionFailure)
{
resources.RollbackConstructionAndThrow(
"BitmapFont construction failed and its GL atlas did not cleanly roll back.",
constructionFailure);
}
TextureId = texture;
_resources = resources;
}
public bool TryGetGlyph(char c, out Glyph g)
@ -144,7 +183,7 @@ public sealed unsafe class BitmapFont : IDisposable
public void Dispose()
{
_gl.DeleteTexture(TextureId);
_resources.RetryCleanup();
}
/// <summary>

View file

@ -20,6 +20,7 @@ public sealed unsafe class DebugLineRenderer : IDisposable
private readonly Shader _shader;
private readonly uint _vao;
private readonly uint _vbo;
private readonly ResourceCleanupGroup _resources;
private readonly List<float> _buffer = new(4096);
private int _vertexCount;
@ -27,25 +28,67 @@ public sealed unsafe class DebugLineRenderer : IDisposable
public DebugLineRenderer(GL gl, string shaderDir)
{
_gl = gl;
_shader = new Shader(gl,
Path.Combine(shaderDir, "debug_line.vert"),
Path.Combine(shaderDir, "debug_line.frag"));
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
var resources = new ResourceCleanupGroup();
Shader? shader = null;
uint vao = 0;
uint vbo = 0;
try
{
shader = new Shader(gl,
Path.Combine(shaderDir, "debug_line.vert"),
Path.Combine(shaderDir, "debug_line.frag"));
resources.Add("debug-line shader", shader.Dispose);
vao = GlResourceCommand.CreateName(
gl,
"debug-line VAO",
gl.GenVertexArray,
gl.DeleteVertexArray);
uint ownedVao = vao;
resources.Add(
"debug-line VAO",
() => GlResourceCommand.DeleteVertexArray(
gl,
ownedVao,
$"delete debug-line VAO {ownedVao}"));
vbo = GlResourceCommand.CreateName(
gl,
"debug-line VBO",
gl.GenBuffer,
gl.DeleteBuffer);
uint ownedVbo = vbo;
resources.Add(
"debug-line VBO",
() => GlResourceCommand.DeleteBuffer(
gl,
ownedVbo,
$"delete debug-line VBO {ownedVbo}"));
_vao = _gl.GenVertexArray();
_vbo = _gl.GenBuffer();
GlResourceCommand.Execute(gl, "configure debug-line vertex state", () =>
{
gl.BindVertexArray(vao);
gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
// 24-byte stride: vec3 pos + vec3 color
gl.EnableVertexAttribArray(0);
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0);
gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)(3 * sizeof(float)));
gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
gl.BindVertexArray(0);
});
}
catch (Exception constructionFailure)
{
resources.RollbackConstructionAndThrow(
"DebugLineRenderer construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
}
_gl.BindVertexArray(_vao);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
// 24-byte stride: vec3 pos + vec3 color
_gl.EnableVertexAttribArray(0);
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0);
_gl.EnableVertexAttribArray(1);
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)(3 * sizeof(float)));
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
_gl.BindVertexArray(0);
_resources = resources;
_shader = shader!;
_vao = vao;
_vbo = vbo;
}
/// <summary>Clear accumulated lines. Call at the start of each frame.</summary>
@ -165,8 +208,6 @@ public sealed unsafe class DebugLineRenderer : IDisposable
public void Dispose()
{
_gl.DeleteVertexArray(_vao);
_gl.DeleteBuffer(_vbo);
_shader.Dispose();
_resources.RetryCleanup();
}
}

View file

@ -1,10 +1,15 @@
namespace AcDream.App.Rendering;
internal interface IGameRenderResourceLifetime
{
TerrainAtlas AcquireTerrainAtlas(Func<TerrainAtlas> factory);
}
/// <summary>
/// Sole lifetime owner for render resources that are borrowed by, but not
/// owned by, their renderers.
/// </summary>
internal sealed class GameRenderResourceLifetime
internal sealed class GameRenderResourceLifetime : IGameRenderResourceLifetime
{
private readonly OwnedResourceSlot<TerrainAtlas> _terrainAtlas = new();
private readonly OwnedResourceSlot<Shader> _skyShader = new();

View file

@ -1,6 +1,7 @@
using AcDream.Core.Plugins;
using AcDream.App.Composition;
using AcDream.App.Physics;
using AcDream.App.Rendering.Wb;
using AcDream.App.Settings;
using AcDream.App.World;
using AcDream.Content;
@ -17,7 +18,8 @@ public sealed class GameWindow :
IGameWindowPlatformPublication<GL, IInputContext>,
IGameWindowHostInputCameraPublication,
IGameWindowContentEffectsAudioPublication,
IGameWindowSettingsDevToolsPublication
IGameWindowSettingsDevToolsPublication,
IGameWindowWorldRenderPublication
{
private static double ClientTimerNow() =>
System.Diagnostics.Stopwatch.GetTimestamp()
@ -802,6 +804,84 @@ public sealed class GameWindow :
_debugVm = value.Debug;
}
void IGameWindowWorldRenderPublication.PublishBindlessSupport(
BindlessSupport value) =>
PublishCompositionOwner(
ref _bindlessSupport,
value,
"bindless support");
void IGameWindowWorldRenderPublication.PublishTerrainShader(Shader value) =>
PublishCompositionOwner(
ref _terrainModernShader,
value,
"terrain shader");
void IGameWindowWorldRenderPublication.PublishSceneLighting(
SceneLightingUboBinding value) =>
PublishCompositionOwner(
ref _sceneLightingUbo,
value,
"scene lighting");
void IGameWindowWorldRenderPublication.PublishDebugLines(
DebugLineRenderer value) =>
PublishCompositionOwner(ref _debugLines, value, "debug lines");
void IGameWindowWorldRenderPublication.PublishHudResources(
BitmapFont font,
TextRenderer text)
{
ArgumentNullException.ThrowIfNull(font);
ArgumentNullException.ThrowIfNull(text);
if (_debugFont is not null || _textRenderer is not null)
{
throw new InvalidOperationException(
"The GameWindow composition shell already owns world HUD resources.");
}
_debugFont = font;
_textRenderer = text;
}
void IGameWindowWorldRenderPublication.PublishTerrain(
TerrainModernRenderer value) =>
PublishCompositionOwner(ref _terrain, value, "terrain renderer");
void IGameWindowWorldRenderPublication.PublishTerrainBuildState(
float[] heightTable,
AcDream.Core.Terrain.TerrainBlendingContext blending,
System.Collections.Concurrent.ConcurrentDictionary<
uint,
AcDream.Core.Terrain.SurfaceInfo> surfaceCache)
{
ArgumentNullException.ThrowIfNull(heightTable);
ArgumentNullException.ThrowIfNull(blending);
ArgumentNullException.ThrowIfNull(surfaceCache);
if (_heightTable is not null || _blendCtx is not null || _surfaceCache is not null)
{
throw new InvalidOperationException(
"The GameWindow composition shell already owns terrain build state.");
}
_heightTable = heightTable;
_blendCtx = blending;
_surfaceCache = surfaceCache;
}
void IGameWindowWorldRenderPublication.PublishMeshShader(Shader value) =>
PublishCompositionOwner(ref _meshShader, value, "mesh shader");
void IGameWindowWorldRenderPublication.PublishWbMeshAdapter(
WbMeshAdapter value) =>
PublishCompositionOwner(ref _wbMeshAdapter, value, "WB mesh adapter");
void IGameWindowWorldRenderPublication.PublishTextureCache(
TextureCache value) =>
PublishCompositionOwner(ref _textureCache, value, "texture cache");
void IGameWindowWorldRenderPublication.PublishSamplerCache(
SamplerCache value) =>
PublishCompositionOwner(ref _samplerCache, value, "sampler cache");
private static void PublishCompositionOwner<T>(
ref T? destination,
T value,
@ -932,168 +1012,22 @@ public sealed class GameWindow :
Console.WriteLine),
this).Compose(platform, hostInputCamera, contentEffectsAudio);
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
_gl.Enable(EnableCap.DepthTest);
// Phase U.3: the 8 hardware clip planes (GL_CLIP_DISTANCE0..7) are NOT
// enabled here. Only the mesh_modern / terrain_modern vertex shaders write
// gl_ClipDistance[0..7]; the sky / particle / weather / UI / debug-line
// shaders write gl_Position but no gl_ClipDistance. Enabling a clip plane
// for a draw whose vertex shader doesn't write that plane's distance is
// UNDEFINED per the GL/GLSL spec (a driver may read the unwritten value as
// negative and clip the primitive away). So instead of a permanent global
// enable, OnRender brackets glEnable/glDisable(GL_CLIP_DISTANCE0..7) around
// ONLY the clip-writing world-geometry draws (terrain + entities, plus
// U.4's EnvCellRenderer.Render); everything else draws with clipping off.
string shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
// Phase N.5b: terrain_modern shader pair — bindless texture handles +
// glMultiDrawElementsIndirect dispatch path. The only terrain shader
// since Task 9 retired the legacy terrain.vert/.frag program.
_terrainModernShader = new Shader(_gl,
Path.Combine(shadersDir, "terrain_modern.vert"),
Path.Combine(shadersDir, "terrain_modern.frag"));
// Phase G.1/G.2: shared scene-lighting UBO. Stays bound at
// binding=1 for the lifetime of the process — every shader that
// declares `layout(std140, binding = 1) uniform SceneLighting`
// reads from this without further intervention.
_sceneLightingUbo = new SceneLightingUboBinding(_gl);
_debugLines = new DebugLineRenderer(_gl, shadersDir);
// Phase I.2: load a system monospace font + TextRenderer for the
// future world-space HUD (D.6). The custom DebugOverlay is gone;
// the ImGui DebugPanel handles all dev surfaces now. These fields
// are reserved for future work — currently unused at the renderer
// level. Skips silently if no font is available.
var fontBytes = BitmapFont.TryLoadSystemMonospaceFont();
if (fontBytes is not null)
{
_debugFont = new BitmapFont(_gl, fontBytes, pixelHeight: 15f, atlasSize: 512);
_textRenderer = new TextRenderer(_gl, shadersDir);
Console.WriteLine($"world-hud font: loaded {fontBytes.Length / 1024}KB, " +
$"atlas {_debugFont.AtlasWidth}x{_debugFont.AtlasHeight}, " +
$"lineHeight={_debugFont.LineHeight:F1}px (reserved for D.6 HUD)");
}
else
{
Console.WriteLine("world-hud font: no system monospace font found");
}
uint centerLandblockId = 0xA9B4FFFFu;
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
var region = contentEffectsAudio.Dats.Get<DatReaderWriter.DBObjs.Region>(
0x13000000u);
var heightTable = region?.LandDefs.LandHeightTable;
if (heightTable is null || heightTable.Length < 256)
throw new InvalidOperationException("Region.LandDefs.LandHeightTable missing or truncated");
// Parse and install the DAT-backed sky, retail day-group state, and
// offline clock seed through their single environment owner.
_worldEnvironment.Initialize(region!);
// N.5: detect ARB_bindless_texture + ARB_shader_draw_parameters BEFORE
// building the terrain atlas / renderer — both consume BindlessSupport
// (atlas via Texture2DArray bindless handles, renderer for SSBO uploads).
// The modern path (SSBO + glMultiDrawElementsIndirect + bindless textures)
// is mandatory as of Phase N.5 — missing extensions throw at startup with
// a clear error so users can file a real bug report rather than silently
// falling back to a half-working renderer.
if (AcDream.App.Rendering.Wb.BindlessSupport.TryCreate(_gl, out var bindless))
{
if (bindless!.HasShaderDrawParameters(_gl))
{
_bindlessSupport = bindless;
Console.WriteLine("[N.5] modern path capabilities present (bindless + ARB_shader_draw_parameters)");
}
else
{
Console.WriteLine("[N.5] GL_ARB_shader_draw_parameters not present — modern path not available");
}
}
else
{
Console.WriteLine("[N.5] GL_ARB_bindless_texture not present — modern path not available");
}
if (_bindlessSupport is null)
{
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.");
}
// Build the terrain atlas once from the Region dat. Phase N.5b: the
// atlas exposes bindless handles for the modern terrain path, so
// BindlessSupport is threaded through.
var terrainAtlas = _renderResourceLifetime.AcquireTerrainAtlas(
() => AcDream.App.Rendering.TerrainAtlas.Build(
_gl,
contentEffectsAudio.Dats,
_bindlessSupport));
// A.5 T22.5: apply anisotropic level from quality preset. Build()
// hard-codes 16x; override here to match the resolved quality so Low
// (4x) and Medium (8x) actually take effect.
terrainAtlas.SetAnisotropic(_runtimeSettings.ResolvedQuality.AnisotropicLevel);
_terrain = new TerrainModernRenderer(
_gl,
_bindlessSupport,
_terrainModernShader!,
terrainAtlas,
_gpuFrameFlights!);
int centerX = (int)((centerLandblockId >> 24) & 0xFFu);
int centerY = (int)((centerLandblockId >> 16) & 0xFFu);
// Build blending context from the terrain atlas. The worker-side
// LandblockBuildFactory captures this immutable table for each build.
var terrainTypeToLayerBytes = new Dictionary<uint, byte>(terrainAtlas.TerrainTypeToLayer.Count);
foreach (var kvp in terrainAtlas.TerrainTypeToLayer)
terrainTypeToLayerBytes[kvp.Key] = (byte)kvp.Value;
const uint RoadTypeEnumValue = 0x20; // TerrainTextureType.RoadType
byte roadLayer = terrainTypeToLayerBytes.TryGetValue(RoadTypeEnumValue, out var rl)
? rl
: AcDream.Core.Terrain.SurfaceInfo.None;
_blendCtx = new AcDream.Core.Terrain.TerrainBlendingContext(
TerrainTypeToLayer: terrainTypeToLayerBytes,
RoadLayer: roadLayer,
CornerAlphaLayers: terrainAtlas.CornerAlphaLayers,
SideAlphaLayers: terrainAtlas.SideAlphaLayers,
RoadAlphaLayers: terrainAtlas.RoadAlphaLayers,
CornerAlphaTCodes: terrainAtlas.CornerAlphaTCodes,
SideAlphaTCodes: terrainAtlas.SideAlphaTCodes,
RoadAlphaRCodes: terrainAtlas.RoadAlphaRCodes);
_heightTable = heightTable;
_surfaceCache = new System.Collections.Concurrent.ConcurrentDictionary<uint, AcDream.Core.Terrain.SurfaceInfo>();
// (Bindless detection moved above — must precede TerrainAtlas.Build /
// TerrainModernRenderer ctor so they can consume BindlessSupport.)
// Mesh shader always loads (modern path is the only path).
_meshShader = new Shader(_gl,
Path.Combine(shadersDir, "mesh_modern.vert"),
Path.Combine(shadersDir, "mesh_modern.frag"));
Console.WriteLine("[N.5] mesh_modern shader loaded");
_textureCache = new TextureCache(
_gl,
contentEffectsAudio.Dats,
_bindlessSupport,
_gpuFrameFlights!);
// Two persistent GL sampler objects (Repeat + ClampToEdge) so
// the sky pass can pick wrap mode per submesh without mutating
// shared per-texture wrap state. See SamplerCache + the
// WorldBuilder reference at
// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132.
_samplerCache = new SamplerCache(_gl);
const uint initialCenterLandblockId = 0xA9B4FFFFu;
WorldRenderResult worldRender = new WorldRenderCompositionPhase(
new WorldRenderDependencies(
_worldEnvironment,
_renderResourceLifetime,
_gpuFrameFlights!,
initialCenterLandblockId,
Console.WriteLine),
this).Compose(platform, contentEffectsAudio, settingsDevTools);
string shadersDir = worldRender.Foundation.ShadersDirectory;
TerrainAtlas terrainAtlas = worldRender.Foundation.TerrainAtlas;
int centerX = worldRender.TerrainBuild.InitialCenterX;
int centerY = worldRender.TerrainBuild.InitialCenterY;
Console.WriteLine(
$"loading world view centered on " +
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
// Retail ClientCombatSystem attack-request owner. This exists independently
// of the retained UI so keyboard combat keeps the same press/hold/release
@ -1456,22 +1390,6 @@ public sealed class GameWindow :
}
// Phase N.4+N.5 — WB rendering pipeline foundation. The modern path is
// mandatory as of N.5 ship amendment: WbMeshAdapter + WbDrawDispatcher
// always construct.
// Phase O (2026-05-21): WbMeshAdapter now consumes our DatCollection
// directly via DatCollectionAdapter. No second dat reader; index cache
// is shared with the rest of the client.
{
var wbLogger = Microsoft.Extensions.Logging.Abstractions.NullLogger<AcDream.App.Rendering.Wb.WbMeshAdapter>.Instance;
_wbMeshAdapter = new AcDream.App.Rendering.Wb.WbMeshAdapter(
_gl,
contentEffectsAudio.Dats,
wbLogger,
_gpuFrameFlights!);
Console.WriteLine("[N.4+N.5] WB foundation + modern path active — routing all content through ObjectMeshManager.");
}
// Phase N.4 Task 12: construct LandblockSpawnAdapter under the feature flag
// and rebuild _worldState so it threads the adapter in. _worldState starts
// as an unadorned GpuWorldState (field initializer); here we replace it with
@ -1937,7 +1855,7 @@ public sealed class GameWindow :
contentEffectsAudio.Dats,
skyShader,
_textureCache!,
_samplerCache);
_samplerCache!);
// Phase G.1 particle renderer — renders rain / snow / spell auras
// spawned into the shared ParticleSystem. Mode-1/no-degrade GfxObjs

View file

@ -221,35 +221,46 @@ public sealed unsafe class ParticleRenderer : IDisposable
handle => _particleGfxInfoByEmitter.Remove(handle),
handle => _textures?.ReleaseParticleTextureOwner(handle),
error => Console.Error.WriteLine($"[particles] {error}"));
_shader = new Shader(_gl,
System.IO.Path.Combine(shadersDir, "particle.vert"),
System.IO.Path.Combine(shadersDir, "particle.frag"));
if (_meshAdapter?.MeshManager?.GlobalBuffer is not null)
{
_meshShader = new Shader(_gl,
System.IO.Path.Combine(shadersDir, "particle_mesh.vert"),
System.IO.Path.Combine(shadersDir, "particle_mesh.frag"));
_meshTextureHandleLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureHandle");
_meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureLayer");
}
float[] quadVerts =
{
-0.5f, -0.5f, 0f, 0f,
0.5f, -0.5f, 1f, 0f,
0.5f, 0.5f, 1f, 1f,
-0.5f, 0.5f, 0f, 1f,
};
uint[] quadIdx = { 0, 1, 2, 0, 2, 3 };
uint quadVbo = 0;
uint quadEbo = 0;
uint uploadVao = 0;
bool vboAllocated = false;
bool eboAllocated = false;
var constructionResources = new ResourceCleanupGroup();
try
{
quadVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle quad VBO");
_shader = new Shader(_gl,
System.IO.Path.Combine(shadersDir, "particle.vert"),
System.IO.Path.Combine(shadersDir, "particle.frag"));
constructionResources.Add("particle shader", _shader.Dispose);
if (_meshAdapter?.MeshManager?.GlobalBuffer is not null)
{
_meshShader = new Shader(_gl,
System.IO.Path.Combine(shadersDir, "particle_mesh.vert"),
System.IO.Path.Combine(shadersDir, "particle_mesh.frag"));
constructionResources.Add(
"particle mesh shader",
_meshShader.Dispose);
_meshTextureHandleLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureHandle");
_meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureLayer");
}
float[] quadVerts =
{
-0.5f, -0.5f, 0f, 0f,
0.5f, -0.5f, 1f, 0f,
0.5f, 0.5f, 1f, 1f,
-0.5f, 0.5f, 0f, 1f,
};
uint[] quadIdx = { 0, 1, 2, 0, 2, 3 };
bool vboAllocated = false;
bool eboAllocated = false;
uint quadVbo = TrackedGlResource.CreateBuffer(
_gl,
"creating particle quad VBO");
RetryableGpuResourceRelease quadVboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
quadVbo,
() => vboAllocated ? quadVerts.Length * sizeof(float) : 0,
"rolling back particle quad VBO");
constructionResources.Add("particle quad VBO", quadVboRelease.Run);
fixed (void* p = quadVerts)
{
TrackedGlResource.AllocateBufferStorage(
@ -264,9 +275,31 @@ public sealed unsafe class ParticleRenderer : IDisposable
}
vboAllocated = true;
quadEbo = TrackedGlResource.CreateBuffer(_gl, "creating particle quad EBO");
uploadVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle upload VAO");
_gl.BindVertexArray(uploadVao);
uint quadEbo = TrackedGlResource.CreateBuffer(
_gl,
"creating particle quad EBO");
RetryableGpuResourceRelease quadEboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
quadEbo,
() => eboAllocated ? quadIdx.Length * sizeof(uint) : 0,
"rolling back particle quad EBO");
constructionResources.Add("particle quad EBO", quadEboRelease.Run);
uint uploadVao = TrackedGlResource.CreateVertexArray(
_gl,
"creating particle upload VAO");
RetryableGpuResourceRelease uploadVaoRelease =
TrackedGlResource.CreateRetryableVertexArrayDeletion(
_gl,
uploadVao,
"rolling back particle upload VAO");
constructionResources.Add("particle upload VAO", uploadVaoRelease.Run);
GlResourceCommand.Execute(
_gl,
"bind particle upload VAO",
() => _gl.BindVertexArray(uploadVao));
fixed (void* p = quadIdx)
{
TrackedGlResource.AllocateBufferStorage(
@ -280,44 +313,26 @@ public sealed unsafe class ParticleRenderer : IDisposable
"uploading particle quad EBO");
}
eboAllocated = true;
_gl.BindVertexArray(0);
TrackedGlResource.DeleteVertexArray(_gl, uploadVao, "deleting particle upload VAO");
uploadVao = 0;
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
GlResourceCommand.Execute(_gl, "finish particle static-buffer upload", () =>
{
_gl.BindVertexArray(0);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
});
uploadVaoRelease.Run();
_quadVbo = quadVbo;
_quadEbo = quadEbo;
}
catch (Exception creationFailure)
{
List<Exception>? cleanupFailures = null;
void Attempt(Action action)
{
try { action(); }
catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
}
if (uploadVao != 0)
Attempt(() => TrackedGlResource.DeleteVertexArray(_gl, uploadVao, "rolling back particle upload VAO"));
if (quadEbo != 0)
Attempt(() => TrackedGlResource.DeleteBuffer(
_gl,
quadEbo,
eboAllocated ? quadIdx.Length * sizeof(uint) : 0,
"rolling back particle quad EBO"));
if (quadVbo != 0)
Attempt(() => TrackedGlResource.DeleteBuffer(
_gl,
quadVbo,
vboAllocated ? quadVerts.Length * sizeof(float) : 0,
"rolling back particle quad VBO"));
if (cleanupFailures is not null)
{
cleanupFailures.Insert(0, creationFailure);
throw new AggregateException("Particle static-buffer creation and rollback failed.", cleanupFailures);
}
throw;
}
_particles.EmitterDied += OnEmitterDied;
_particles.EmitterDied += OnEmitterDied;
constructionResources.TransferAll();
}
catch (Exception constructionFailure)
{
constructionResources.RollbackConstructionAndThrow(
"ParticleRenderer construction failed and its shader prefix did not cleanly roll back.",
constructionFailure);
throw new System.Diagnostics.UnreachableException();
}
}
/// <summary>

View file

@ -88,6 +88,7 @@ void main() { } // depth-only: color writes are masked off by the caller state
private readonly int _locForceFarZ;
private readonly int _locDepthBias;
private readonly int _locDepthBiasEyeCapN;
private readonly ResourceCleanupGroup _resources;
private const int MaxFanVerts = 32;
private readonly float[] _scratch = new float[MaxFanVerts * 3];
@ -109,28 +110,52 @@ void main() { } // depth-only: color writes are masked off by the caller state
public PortalDepthMaskRenderer(GL gl)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
var resources = new ResourceCleanupGroup();
try
{
_program = ShaderProgramConstruction.Build(
new GlShaderProgramBuildApi(gl),
VertSrc,
FragSrc);
resources.Add(
"portal-depth program",
() => GlResourceCommand.DeleteProgram(
gl,
_program,
$"delete PortalDepthMask program {_program}"));
uint vs = Compile(ShaderType.VertexShader, VertSrc);
uint fs = Compile(ShaderType.FragmentShader, FragSrc);
_program = _gl.CreateProgram();
_gl.AttachShader(_program, vs);
_gl.AttachShader(_program, fs);
_gl.LinkProgram(_program);
_gl.GetProgram(_program, ProgramPropertyARB.LinkStatus, out int linked);
if (linked == 0)
throw new InvalidOperationException($"PortalDepthMask link failed: {_gl.GetProgramInfoLog(_program)}");
_gl.DeleteShader(vs);
_gl.DeleteShader(fs);
(
_locViewProjection,
_locPlaneCount,
_locPlanes,
_locForceFarZ,
_locDepthBias,
_locDepthBiasEyeCapN) = GlResourceCommand.Execute(
_gl,
"resolve PortalDepthMask uniforms",
() => (
_gl.GetUniformLocation(_program, "uViewProjection"),
_gl.GetUniformLocation(_program, "uPlaneCount"),
_gl.GetUniformLocation(_program, "uPlanes"),
_gl.GetUniformLocation(_program, "uForceFarZ"),
_gl.GetUniformLocation(_program, "uDepthBias"),
_gl.GetUniformLocation(_program, "uDepthBiasEyeCapN")));
_locViewProjection = _gl.GetUniformLocation(_program, "uViewProjection");
_locPlaneCount = _gl.GetUniformLocation(_program, "uPlaneCount");
_locPlanes = _gl.GetUniformLocation(_program, "uPlanes");
_locForceFarZ = _gl.GetUniformLocation(_program, "uForceFarZ");
_locDepthBias = _gl.GetUniformLocation(_program, "uDepthBias");
_locDepthBiasEyeCapN = _gl.GetUniformLocation(_program, "uDepthBiasEyeCapN");
for (int i = 0; i < _frameBuffers.Length; i++)
{
FrameBufferSet set = CreateFrameBufferSet(resources, i);
_frameBuffers[i] = set;
}
}
catch (Exception constructionFailure)
{
resources.RollbackConstructionAndThrow(
"PortalDepthMaskRenderer construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
throw new System.Diagnostics.UnreachableException();
}
for (int i = 0; i < _frameBuffers.Length; i++)
_frameBuffers[i] = CreateFrameBufferSet();
_resources = resources;
}
/// <summary>
@ -146,19 +171,34 @@ void main() { } // depth-only: color writes are masked off by the caller state
_activeFrameBuffer.UsedVertices = 0;
}
private FrameBufferSet CreateFrameBufferSet()
private FrameBufferSet CreateFrameBufferSet(
ResourceCleanupGroup resources,
int frameIndex)
{
uint vao = 0;
uint vbo = 0;
try
uint vao = TrackedGlResource.CreateVertexArray(
_gl,
"PortalDepthMask frame VAO creation");
RetryableGpuResourceRelease vaoRelease =
TrackedGlResource.CreateRetryableVertexArrayDeletion(
_gl,
vao,
"PortalDepthMask frame VAO disposal");
resources.Add($"portal-depth frame {frameIndex} VAO", vaoRelease.Run);
uint vbo = TrackedGlResource.CreateBuffer(
_gl,
"PortalDepthMask frame VBO creation");
var set = new FrameBufferSet { Vao = vao, Vbo = vbo };
RetryableGpuResourceRelease vboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
vbo,
() => set.CapacityBytes,
"PortalDepthMask frame VBO disposal");
resources.Add($"portal-depth frame {frameIndex} VBO", vboRelease.Run);
GlResourceCommand.Execute(_gl, "configure PortalDepthMask frame buffers", () =>
{
vao = TrackedGlResource.CreateVertexArray(
_gl,
"PortalDepthMask frame VAO creation");
vbo = TrackedGlResource.CreateBuffer(
_gl,
"PortalDepthMask frame VBO creation");
var set = new FrameBufferSet { Vao = vao, Vbo = vbo };
_gl.BindVertexArray(set.Vao);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo);
unsafe
@ -168,29 +208,8 @@ void main() { } // depth-only: color writes are masked off by the caller state
_gl.EnableVertexAttribArray(0);
_gl.BindVertexArray(0);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
return set;
}
catch
{
if (vbo != 0)
TrackedGlResource.DeleteBuffer(
_gl, vbo, 0, "PortalDepthMask frame VBO rollback");
if (vao != 0)
TrackedGlResource.DeleteVertexArray(
_gl, vao, "PortalDepthMask frame VAO rollback");
throw;
}
}
private uint Compile(ShaderType type, string src)
{
uint s = _gl.CreateShader(type);
_gl.ShaderSource(s, src);
_gl.CompileShader(s);
_gl.GetShader(s, ShaderParameterName.CompileStatus, out int ok);
if (ok == 0)
throw new InvalidOperationException($"PortalDepthMask {type} compile failed: {_gl.GetShaderInfoLog(s)}");
return s;
});
return set;
}
/// <summary>
@ -387,18 +406,6 @@ void main() { } // depth-only: color writes are masked off by the caller state
public void Dispose()
{
_gl.DeleteProgram(_program);
foreach (FrameBufferSet set in _frameBuffers)
{
TrackedGlResource.DeleteVertexArray(
_gl,
set.Vao,
"PortalDepthMask frame VAO disposal");
TrackedGlResource.DeleteBuffer(
_gl,
set.Vbo,
set.CapacityBytes,
"PortalDepthMask frame VBO disposal");
}
_resources.RetryCleanup();
}
}

View file

@ -1,5 +1,7 @@
namespace AcDream.App.Rendering;
using System.Runtime.ExceptionServices;
/// <summary>
/// Reverse-order, all-attempted cleanup owner used while a composite resource
/// is still under construction and after it becomes the aggregate owner.
@ -25,6 +27,15 @@ internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup
_entries.Add(new Entry(name, release));
}
public void TransferAll()
{
if (_running)
throw new InvalidOperationException(
"The resource cleanup group is currently releasing resources.");
foreach (Entry entry in _entries)
entry.Complete = true;
}
public void RetryCleanup()
{
if (_running || IsCleanupComplete)
@ -60,4 +71,26 @@ internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup
if (failures is not null)
throw new AggregateException("Composite resource cleanup remains incomplete.", failures);
}
public void RollbackConstructionAndThrow(
string message,
Exception constructionFailure)
{
ArgumentException.ThrowIfNullOrWhiteSpace(message);
ArgumentNullException.ThrowIfNull(constructionFailure);
try
{
RetryCleanup();
}
catch (Exception cleanupFailure)
{
throw new GlResourceConstructionException(
message,
this,
[constructionFailure, cleanupFailure]);
}
ExceptionDispatchInfo.Capture(constructionFailure).Throw();
throw new InvalidOperationException("Unreachable construction rollback path.");
}
}

View file

@ -31,6 +31,7 @@ namespace AcDream.App.Rendering;
public sealed class SamplerCache : IDisposable
{
private readonly GL _gl;
private readonly ResourceCleanupGroup _resources;
/// <summary>Sampler with WrapS = WrapT = Repeat. The default for textures uploaded by <see cref="TextureCache"/>.</summary>
public uint Wrap { get; }
@ -41,23 +42,62 @@ public sealed class SamplerCache : IDisposable
public SamplerCache(GL gl)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
var resources = new ResourceCleanupGroup();
uint wrap = 0;
uint clamp = 0;
try
{
wrap = GlResourceCommand.CreateName(
_gl,
"repeat sampler",
_gl.GenSampler,
_gl.DeleteSampler);
uint ownedWrap = wrap;
resources.Add(
"repeat sampler",
() => GlResourceCommand.Execute(
_gl,
$"delete repeat sampler {ownedWrap}",
() => _gl.DeleteSampler(ownedWrap)));
Configure(wrap, TextureWrapMode.Repeat, "repeat sampler");
Wrap = _gl.GenSampler();
_gl.SamplerParameter(Wrap, SamplerParameterI.WrapS, (int)TextureWrapMode.Repeat);
_gl.SamplerParameter(Wrap, SamplerParameterI.WrapT, (int)TextureWrapMode.Repeat);
_gl.SamplerParameter(Wrap, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear);
_gl.SamplerParameter(Wrap, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
clamp = GlResourceCommand.CreateName(
_gl,
"clamp sampler",
_gl.GenSampler,
_gl.DeleteSampler);
uint ownedClamp = clamp;
resources.Add(
"clamp sampler",
() => GlResourceCommand.Execute(
_gl,
$"delete clamp sampler {ownedClamp}",
() => _gl.DeleteSampler(ownedClamp)));
Configure(clamp, TextureWrapMode.ClampToEdge, "clamp sampler");
}
catch (Exception constructionFailure)
{
resources.RollbackConstructionAndThrow(
"SamplerCache construction failed and its sampler prefix did not cleanly roll back.",
constructionFailure);
}
Clamp = _gl.GenSampler();
_gl.SamplerParameter(Clamp, SamplerParameterI.WrapS, (int)TextureWrapMode.ClampToEdge);
_gl.SamplerParameter(Clamp, SamplerParameterI.WrapT, (int)TextureWrapMode.ClampToEdge);
_gl.SamplerParameter(Clamp, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear);
_gl.SamplerParameter(Clamp, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
Wrap = wrap;
Clamp = clamp;
_resources = resources;
}
private void Configure(uint sampler, TextureWrapMode wrap, string name) =>
GlResourceCommand.Execute(_gl, $"configure {name}", () =>
{
_gl.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrap);
_gl.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrap);
_gl.SamplerParameter(sampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear);
_gl.SamplerParameter(sampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
});
public void Dispose()
{
if (Wrap != 0) _gl.DeleteSampler(Wrap);
if (Clamp != 0) _gl.DeleteSampler(Clamp);
_resources.RetryCleanup();
}
}

View file

@ -144,42 +144,56 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
if (_uTexTilingLoc < 0)
throw new InvalidOperationException("terrain_modern.frag is missing the required uTexTiling uniform.");
var constructionResources = new ResourceCleanupGroup();
try
{
_globalVao = TrackedGlResource.CreateVertexArray(
_gl,
"creating terrain global VAO");
RetryableGpuResourceRelease globalVaoRelease =
TrackedGlResource.CreateRetryableVertexArrayDeletion(
_gl,
_globalVao,
"rolling back terrain global VAO");
constructionResources.Add(
"terrain global VAO",
globalVaoRelease.Run);
_globalVbo = TrackedGlResource.CreateBuffer(
_gl,
"creating terrain global vertex buffer");
RetryableGpuResourceRelease globalVboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
_globalVbo,
() => _globalVboCapacityBytes,
"rolling back terrain global vertex buffer");
constructionResources.Add(
"terrain global vertex buffer",
globalVboRelease.Run);
_globalEbo = TrackedGlResource.CreateBuffer(
_gl,
"creating terrain global index buffer");
RetryableGpuResourceRelease globalEboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
_globalEbo,
() => _globalEboCapacityBytes,
"rolling back terrain global index buffer");
constructionResources.Add(
"terrain global index buffer",
globalEboRelease.Run);
AllocateGpuBuffers(initialSlotCapacity);
ConfigureVao(_globalVao, _globalVbo, _globalEbo);
GlResourceCommand.Execute(
_gl,
"configure terrain global vertex array",
() => ConfigureVao(_globalVao, _globalVbo, _globalEbo));
constructionResources.TransferAll();
}
catch
catch (Exception constructionFailure)
{
TrackedGlResource.DeleteVertexArray(
_gl,
_globalVao,
"rolling back terrain global VAO");
TrackedGlResource.DeleteBuffer(
_gl,
_globalVbo,
_globalVboCapacityBytes,
"rolling back terrain global vertex buffer");
TrackedGlResource.DeleteBuffer(
_gl,
_globalEbo,
_globalEboCapacityBytes,
"rolling back terrain global index buffer");
_globalVao = 0;
_globalVbo = 0;
_globalEbo = 0;
_globalVboCapacityBytes = 0;
_globalEboCapacityBytes = 0;
throw;
constructionResources.RollbackConstructionAndThrow(
"TerrainModernRenderer construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
}
}

View file

@ -95,10 +95,31 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
ArgumentNullException.ThrowIfNull(retirementQueue);
if (bindless is not null)
{
_compositeTextures = new CompositeTextureArrayCache(gl, bindless, retirementQueue);
_particleTextures = new StandaloneBindlessTextureCache(
new ParticleTextureBackend(this),
retirementQueue);
var resources = new ResourceCleanupGroup();
CompositeTextureArrayCache? composite = null;
StandaloneBindlessTextureCache? particles = null;
try
{
composite = new CompositeTextureArrayCache(
gl,
bindless,
retirementQueue);
resources.Add("composite texture cache", composite.Dispose);
particles = new StandaloneBindlessTextureCache(
new ParticleTextureBackend(this),
retirementQueue);
resources.Add("particle texture cache", particles.Dispose);
resources.TransferAll();
}
catch (Exception constructionFailure)
{
resources.RollbackConstructionAndThrow(
"TextureCache construction failed and its child-cache prefix did not cleanly roll back.",
constructionFailure);
}
_compositeTextures = composite;
_particleTextures = particles;
}
}

View file

@ -1,4 +1,5 @@
using Chorizite.Core.Render;
using AcDream.App.Rendering;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
using System;
@ -161,7 +162,7 @@ namespace AcDream.App.Rendering.Wb {
if (string.IsNullOrWhiteSpace(vertShaderSource) || string.IsNullOrWhiteSpace(fragShaderSource)) {
_log.LogError($"Shader {Name} has no source code!");
return;
throw new InvalidOperationException($"Shader {Name} has no source code.");
}
if (_device.HasOpenGL43 && _device.HasBindless) {
@ -170,41 +171,47 @@ namespace AcDream.App.Rendering.Wb {
fragShaderSource = fragShaderSource.Replace("#version 330 core", replacement);
}
uint vertexShader = CompileShader(ShaderType.VertexShader, Name, vertShaderSource);
uint fragmentShader = CompileShader(ShaderType.FragmentShader, Name, fragShaderSource);
var resources = new ResourceCleanupGroup();
uint prog = 0;
bool accountingPublished = false;
try {
prog = ShaderProgramConstruction.Build(
new GlShaderProgramBuildApi(GL),
vertShaderSource,
fragShaderSource);
uint ownedProgram = prog;
var unpublishedProgramRelease = new RetryableGpuResourceRelease(
() => GlResourceCommand.DeleteProgram(
GL,
ownedProgram,
$"delete unpublished WB shader program {ownedProgram}"),
() => {
if (accountingPublished)
{
GpuMemoryTracker.TrackResourceDeallocation(
GpuResourceType.Shader);
}
});
resources.Add("WB shader program", unpublishedProgramRelease.Run);
var prog = GL.CreateProgram();
GLHelpers.CheckErrors(GL, true);
GL.AttachShader(prog, vertexShader);
GLHelpers.CheckErrors(GL, true);
GL.AttachShader(prog, fragmentShader);
GLHelpers.CheckErrors(GL, true);
GL.LinkProgram(prog);
GLHelpers.CheckErrors(GL, true);
// Bind SceneData uniform block to point 0 if it exists.
GlResourceCommand.Execute(GL, $"configure shader {Name} SceneData binding", () => {
uint sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData");
if (sceneDataIndex != uint.MaxValue)
GL.UniformBlockBinding(prog, sceneDataIndex, 0);
});
GL.GetProgram(prog, GLEnum.LinkStatus, out int success);
GLHelpers.CheckErrors(GL);
if (success != 1) {
var infoLog = GL.GetProgramInfoLog(prog);
_log.LogError($"Error: shader {Name} link failed: {infoLog}");
GL.DeleteProgram(prog);
return;
}
else {
_log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Shader);
accountingPublished = true;
resources.TransferAll();
} catch (Exception constructionFailure) {
_log.LogError(constructionFailure, "Failed to construct shader {ShaderName}", Name);
resources.RollbackConstructionAndThrow(
$"Shader {Name} construction failed and its GL program did not cleanly roll back.",
constructionFailure);
}
// Bind SceneData uniform block to point 0 if it exists
var sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData");
if (sceneDataIndex != uint.MaxValue) {
GL.UniformBlockBinding(prog, sceneDataIndex, 0);
GLHelpers.CheckErrors(GL);
}
GL.DeleteShader(vertexShader);
GLHelpers.CheckErrors(GL);
GL.DeleteShader(fragmentShader);
GLHelpers.CheckErrors(GL);
_log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
if (Program != 0) {
Unload();
@ -212,32 +219,12 @@ namespace AcDream.App.Rendering.Wb {
_uniformLocations.Clear();
_uniformValues.Clear();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Shader);
Program = prog;
ProgramId = prog;
NeedsLoad = false;
GLHelpers.CheckErrors(GL);
}
private uint CompileShader(ShaderType shaderType, string name, string shaderSource) {
uint shader = GL.CreateShader(shaderType);
GLHelpers.CheckErrors(GL);
GL.ShaderSource(shader, shaderSource);
GLHelpers.CheckErrors(GL);
GL.CompileShader(shader);
GLHelpers.CheckErrors(GL);
GL.GetShader(shader, ShaderParameterName.CompileStatus, out int success);
GLHelpers.CheckErrors(GL);
if (success != 1) {
var infoLog = GL.GetShaderInfoLog(shader);
_log.LogError($"Error: {name}:{shaderType} compilation failed: {infoLog}");
}
return shader;
}
public override void Bind() {
lock (_lock) {
SetActive();

View file

@ -28,30 +28,44 @@ namespace AcDream.App.Rendering.Wb {
/// <param name="usage">Buffer usage</param>
/// <param name="size">The size of the buffer, in bytes</param>
public unsafe ManagedGLUniformBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
_device = device;
_device = device ?? throw new ArgumentNullException(nameof(device));
ArgumentOutOfRangeException.ThrowIfLessThan(size, 1);
Size = size;
Usage = usage;
// Generate the buffer
bufferId = GL.GenBuffer();
if (bufferId == 0) {
throw new Exception("Failed to generate uniform buffer.");
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
uint buffer = 0;
bool allocated = false;
try {
buffer = TrackedGlResource.CreateBuffer(
GL,
"creating managed uniform buffer");
uint ownedBuffer = buffer;
RetryableGpuResourceRelease unpublishedBufferRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
GL,
ownedBuffer,
() => allocated ? Size : 0,
"rolling back managed uniform buffer");
resources.Add(
"managed uniform buffer",
unpublishedBufferRelease.Run);
TrackedGlResource.AllocateBufferStorage(
GL,
GLEnum.UniformBuffer,
buffer,
0,
Size,
GLEnum.DynamicDraw,
"allocating managed uniform buffer");
allocated = true;
resources.TransferAll();
} catch (Exception constructionFailure) {
resources.RollbackConstructionAndThrow(
"ManagedGLUniformBuffer construction failed and its GL buffer did not cleanly roll back.",
constructionFailure);
}
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
GLHelpers.CheckErrors(GL);
// Allocate the buffer with the specified size
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
GLHelpers.CheckErrors(GL);
GL.BufferData(
GLEnum.UniformBuffer,
(uint)Size,
(void*)0, // No initial data
GLEnum.DynamicDraw);
GLHelpers.CheckErrors(GL);
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
bufferId = buffer;
}
/// <inheritdoc />
@ -139,5 +153,25 @@ namespace AcDream.App.Rendering.Wb {
}
});
}
/// <summary>
/// Releases an unpublished constructor-owned buffer synchronously on
/// the GL thread. This is deliberately separate from ordinary queued
/// disposal so an enclosing constructor can prove rollback before it
/// propagates its failure.
/// </summary>
internal void DisposeImmediately() {
if (bufferId == 0)
return;
RetryableGpuResourceRelease release =
TrackedGlResource.CreateRetryableBufferDeletion(
GL,
bufferId,
Size,
"rolling back unpublished managed uniform buffer");
release.Run();
bufferId = 0;
}
}
}

View file

@ -1,6 +1,7 @@
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using AcDream.App.Rendering;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
@ -149,38 +150,49 @@ namespace AcDream.App.Rendering.Wb {
HasBindless = false;
}
GL.GenBuffers(1, out uint instanceVbo);
InstanceVBO = instanceVbo;
var resources = new ResourceCleanupGroup();
try {
InstanceVBO = CreateConstructionBuffer(resources, "WB instance buffer");
// Query this immutable device limit once. Atlas construction can
// happen hundreds of times during portal streaming; repeating a
// driver GetFloat for every texture serialized the upload burst.
if (renderSettings.EnableAnisotropicFiltering) {
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
MaxSupportedAnisotropy = Math.Max(0f, maxAniso);
// Query this immutable device limit once. Atlas construction can
// happen hundreds of times during portal streaming; repeating a
// driver GetFloat for every texture serialized the upload burst.
if (renderSettings.EnableAnisotropicFiltering) {
MaxSupportedAnisotropy = GlResourceCommand.Execute(
GL,
"query maximum texture anisotropy",
() => {
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
return Math.Max(0f, maxAniso);
});
}
WrapSampler = CreateConstructionSampler(
resources,
TextureWrapMode.Repeat,
"WB repeat sampler");
ClampSampler = CreateConstructionSampler(
resources,
TextureWrapMode.ClampToEdge,
"WB clamp sampler");
_sceneDataBuffer = new ManagedGLUniformBuffer(
this,
BufferUsage.Dynamic,
Marshal.SizeOf<SceneData>());
ManagedGLUniformBuffer ownedSceneDataBuffer = _sceneDataBuffer;
resources.Add(
"WB scene-data uniform buffer",
ownedSceneDataBuffer.DisposeImmediately);
InitializeSharedDebugResources(resources);
resources.TransferAll();
} catch (Exception constructionFailure) {
resources.RollbackConstructionAndThrow(
"OpenGLGraphicsDevice construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
}
// Create sampler objects for wrap vs clamp
WrapSampler = GL.GenSampler();
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.Repeat);
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.Repeat);
GL.SamplerParameter(WrapSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear);
GL.SamplerParameter(WrapSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
if (MaxSupportedAnisotropy > 0)
GL.SamplerParameter(WrapSampler, GLEnum.TextureMaxAnisotropy, MaxSupportedAnisotropy);
ClampSampler = GL.GenSampler();
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.ClampToEdge);
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.ClampToEdge);
GL.SamplerParameter(ClampSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear);
GL.SamplerParameter(ClampSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
if (MaxSupportedAnisotropy > 0)
GL.SamplerParameter(ClampSampler, GLEnum.TextureMaxAnisotropy, MaxSupportedAnisotropy);
_sceneDataBuffer = new ManagedGLUniformBuffer(this, BufferUsage.Dynamic, Marshal.SizeOf<SceneData>());
InitializeSharedDebugResources();
// ParticleBatcher is constructed post-ctor by WbMeshAdapter (WbMeshAdapter.cs:78)
// after the adapter has wired up all dependencies. The null! here is overridden
// immediately after construction; it is not observable as null at runtime.
@ -196,7 +208,49 @@ namespace AcDream.App.Rendering.Wb {
internal AcDream.App.Rendering.IGpuResourceRetirementQueue ResourceRetirement =>
_resourceRetirement;
private void InitializeSharedDebugResources() {
private uint CreateConstructionBuffer(ResourceCleanupGroup resources, string name) {
uint buffer = GlResourceCommand.CreateName(GL, name, GL.GenBuffer, GL.DeleteBuffer);
resources.Add(
name,
() => GlResourceCommand.DeleteBuffer(
GL,
buffer,
$"delete {name} {buffer}"));
return buffer;
}
private uint CreateConstructionSampler(
ResourceCleanupGroup resources,
TextureWrapMode wrapMode,
string name) {
uint sampler = GlResourceCommand.CreateName(GL, name, GL.GenSampler, GL.DeleteSampler);
resources.Add(
name,
() => GlResourceCommand.Execute(
GL,
$"delete {name} {sampler}",
() => GL.DeleteSampler(sampler)));
GlResourceCommand.Execute(GL, $"configure {name}", () => {
GL.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrapMode);
GL.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrapMode);
GL.SamplerParameter(
sampler,
SamplerParameterI.MinFilter,
(int)TextureMinFilter.LinearMipmapLinear);
GL.SamplerParameter(
sampler,
SamplerParameterI.MagFilter,
(int)TextureMagFilter.Linear);
if (MaxSupportedAnisotropy > 0)
GL.SamplerParameter(
sampler,
GLEnum.TextureMaxAnisotropy,
MaxSupportedAnisotropy);
});
return sampler;
}
private void InitializeSharedDebugResources(ResourceCleanupGroup resources) {
// Unit quad vertices for two triangles (0 to 1 for length, -0.5 to 0.5 for thickness)
float[] quadVertices = {
0.0f, -0.5f,
@ -207,53 +261,74 @@ namespace AcDream.App.Rendering.Wb {
0.0f, 0.5f
};
GL.GenBuffers(1, out uint quadVbo);
SharedQuadVBO = quadVbo;
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
fixed (float* pQuad = quadVertices) {
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(quadVertices.Length * sizeof(float)), pQuad, GLEnum.StaticDraw);
}
SharedQuadVBO = CreateConstructionBuffer(resources, "WB shared debug quad buffer");
SharedDebugInstanceVBO = CreateConstructionBuffer(
resources,
"WB shared debug instance buffer");
SharedDebugVAO = GlResourceCommand.CreateName(
GL,
"WB shared debug vertex array",
GL.GenVertexArray,
GL.DeleteVertexArray);
uint ownedDebugVao = SharedDebugVAO;
resources.Add(
"WB shared debug vertex array",
() => GlResourceCommand.DeleteVertexArray(
GL,
ownedDebugVao,
$"delete WB shared debug vertex array {ownedDebugVao}"));
GL.GenBuffers(1, out uint debugInstanceVbo);
SharedDebugInstanceVBO = debugInstanceVbo;
// Initial capacity for debug instances
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(1024 * 44), (void*)0, GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance)
GlResourceCommand.Execute(GL, "configure WB shared debug resources", () => {
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
fixed (float* pQuad = quadVertices) {
GL.BufferData(
GLEnum.ArrayBuffer,
(nuint)(quadVertices.Length * sizeof(float)),
pQuad,
GLEnum.StaticDraw);
}
GL.GenVertexArrays(1, out uint debugVao);
SharedDebugVAO = debugVao;
GL.BindVertexArray(SharedDebugVAO);
// Initial capacity for debug instances.
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
GL.BufferData(
GLEnum.ArrayBuffer,
(nuint)(1024 * 44),
(void*)0,
GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance)
// Quad Pos attribute (location 0)
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
GL.EnableVertexAttribArray(0);
GL.VertexAttribPointer(0, 2, GLEnum.Float, false, 2 * sizeof(float), (void*)0);
GL.BindVertexArray(SharedDebugVAO);
// Instance attributes
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
uint lineInstanceSize = 44; // Marshal.SizeOf<LineInstance>() - we'll hardcode or use a constant later
// Quad Pos attribute (location 0)
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
GL.EnableVertexAttribArray(0);
GL.VertexAttribPointer(0, 2, GLEnum.Float, false, 2 * sizeof(float), (void*)0);
// aStart (location 1)
GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(1, 3, GLEnum.Float, false, lineInstanceSize, (void*)0);
GL.VertexAttribDivisor(1, 1);
// Instance attributes
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
uint lineInstanceSize = 44;
// aEnd (location 2)
GL.EnableVertexAttribArray(2);
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12); // OffsetOf End
GL.VertexAttribDivisor(2, 1);
// aStart (location 1)
GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(1, 3, GLEnum.Float, false, lineInstanceSize, (void*)0);
GL.VertexAttribDivisor(1, 1);
// aColor (location 3)
GL.EnableVertexAttribArray(3);
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24); // OffsetOf Color
GL.VertexAttribDivisor(3, 1);
// aEnd (location 2)
GL.EnableVertexAttribArray(2);
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12);
GL.VertexAttribDivisor(2, 1);
// aThickness (location 4)
GL.EnableVertexAttribArray(4);
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40); // OffsetOf Thickness
GL.VertexAttribDivisor(4, 1);
// aColor (location 3)
GL.EnableVertexAttribArray(3);
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24);
GL.VertexAttribDivisor(3, 1);
GL.BindVertexArray(0);
// aThickness (location 4)
GL.EnableVertexAttribArray(4);
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40);
GL.VertexAttribDivisor(4, 1);
GL.BindVertexArray(0);
});
}
public void EnsureInstanceBufferCapacity(int count, int stride, bool forceOrphan = false) {

View file

@ -32,6 +32,7 @@ namespace AcDream.App.Rendering.Wb {
private readonly uint _ibo;
private readonly uint _instanceVbo;
private readonly IShader _shader;
private readonly ResourceCleanupGroup _resources;
private readonly ParticleInstance[] _instanceData = new ParticleInstance[MAX_PARTICLES_TOTAL];
private readonly List<ParticleRenderData> _allParticles = new();
private int _currentInstanceCount = 0;
@ -43,91 +44,108 @@ namespace AcDream.App.Rendering.Wb {
private Vector3 _cameraRight;
public ParticleBatcher(OpenGLGraphicsDevice graphicsDevice) {
_graphicsDevice = graphicsDevice;
_graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice));
var gl = _graphicsDevice.GL;
var resources = new ResourceCleanupGroup();
IShader? shader = null;
uint vao = 0;
uint vbo = 0;
uint ibo = 0;
uint instanceVbo = 0;
try {
var vertSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.vert");
var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag");
shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource);
if (shader is IDisposable disposableShader)
resources.Add("WB particle shader", disposableShader.Dispose);
var vertSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.vert");
var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag");
_shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource);
// Create quad vertices - centered to match ACViewer expansion logic
float[] vertices = {
-0.5f, 0.0f, -0.5f, 0.0f, 1.0f,
0.5f, 0.0f, -0.5f, 1.0f, 1.0f,
0.5f, 0.0f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.0f, 0.5f, 0.0f, 0.0f
};
ushort[] indices = { 0, 1, 2, 2, 3, 0 };
// Create quad vertices - centered to match ACViewer expansion logic
float[] vertices = {
// x, y, z, u, v
-0.5f, 0.0f, -0.5f, 0.0f, 1.0f,
0.5f, 0.0f, -0.5f, 1.0f, 1.0f,
0.5f, 0.0f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.0f, 0.5f, 0.0f, 0.0f
};
vao = CreateVertexArray(resources, gl, "WB particle VAO");
vbo = CreateBuffer(resources, gl, "WB particle vertex buffer");
ibo = CreateBuffer(resources, gl, "WB particle index buffer");
instanceVbo = CreateBuffer(resources, gl, "WB particle instance buffer");
ushort[] indices = { 0, 1, 2, 2, 3, 0 };
GlResourceCommand.Execute(gl, "configure WB particle batcher", () => {
gl.BindVertexArray(vao);
gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
fixed (float* p = vertices) {
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw);
}
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ibo);
fixed (ushort* p = indices) {
gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw);
}
_vao = gl.GenVertexArray();
gl.BindVertexArray(_vao);
gl.EnableVertexAttribArray(0);
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)0);
gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)(3 * sizeof(float)));
_vbo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
unsafe {
fixed (float* p = vertices) {
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw);
}
gl.BindBuffer(BufferTargetARB.ArrayBuffer, instanceVbo);
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf<ParticleInstance>()), (void*)0, BufferUsageARB.DynamicDraw);
uint stride = (uint)Marshal.SizeOf<ParticleInstance>();
gl.EnableVertexAttribArray(2);
gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
gl.VertexAttribDivisor(2, 1);
gl.EnableVertexAttribArray(3);
gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
gl.VertexAttribDivisor(3, 1);
gl.EnableVertexAttribArray(4);
gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
gl.VertexAttribDivisor(4, 1);
gl.EnableVertexAttribArray(5);
gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float)));
gl.VertexAttribDivisor(5, 1);
gl.EnableVertexAttribArray(6);
gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float)));
gl.VertexAttribDivisor(6, 1);
gl.EnableVertexAttribArray(7);
gl.VertexAttribPointer(7, 1, VertexAttribPointerType.Float, false, stride, (void*)(13 * sizeof(float)));
gl.VertexAttribDivisor(7, 1);
gl.BindVertexArray(0);
});
shader.Bind();
shader.SetUniform("uTextureArray", 0);
shader.Unbind();
} catch (Exception constructionFailure) {
resources.RollbackConstructionAndThrow(
"ParticleBatcher construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
}
_ibo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ibo);
unsafe {
fixed (ushort* p = indices) {
gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw);
}
}
_resources = resources;
_shader = shader!;
_vao = vao;
_vbo = vbo;
_ibo = ibo;
_instanceVbo = instanceVbo;
}
// Quad attributes
gl.EnableVertexAttribArray(0);
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)0);
gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)(3 * sizeof(float)));
private static uint CreateBuffer(
ResourceCleanupGroup resources,
GL gl,
string name) {
uint buffer = GlResourceCommand.CreateName(gl, name, gl.GenBuffer, gl.DeleteBuffer);
resources.Add(name, () => GlResourceCommand.DeleteBuffer(gl, buffer, $"delete {name} {buffer}"));
return buffer;
}
// Instance attributes
_instanceVbo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf<ParticleInstance>()), (void*)0, BufferUsageARB.DynamicDraw);
uint stride = (uint)Marshal.SizeOf<ParticleInstance>();
// iPosition
gl.EnableVertexAttribArray(2);
gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
gl.VertexAttribDivisor(2, 1);
// iScaleOpacityActive
gl.EnableVertexAttribArray(3);
gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
gl.VertexAttribDivisor(3, 1);
// iTextureIndex
gl.EnableVertexAttribArray(4);
gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
gl.VertexAttribDivisor(4, 1);
// iRotation (Quaternion)
gl.EnableVertexAttribArray(5);
gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float)));
gl.VertexAttribDivisor(5, 1);
// iSize
gl.EnableVertexAttribArray(6);
gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float)));
gl.VertexAttribDivisor(6, 1);
// iIsBillboard
gl.EnableVertexAttribArray(7);
gl.VertexAttribPointer(7, 1, VertexAttribPointerType.Float, false, stride, (void*)(13 * sizeof(float)));
gl.VertexAttribDivisor(7, 1);
gl.BindVertexArray(0);
_shader.Bind();
_shader.SetUniform("uTextureArray", 0);
_shader.Unbind();
private static uint CreateVertexArray(
ResourceCleanupGroup resources,
GL gl,
string name) {
uint vao = GlResourceCommand.CreateName(gl, name, gl.GenVertexArray, gl.DeleteVertexArray);
resources.Add(name, () => GlResourceCommand.DeleteVertexArray(gl, vao, $"delete {name} {vao}"));
return vao;
}
public void Begin(Matrix4x4 viewProjection, Vector3 cameraUp, Vector3 cameraRight) {
@ -216,12 +234,7 @@ namespace AcDream.App.Rendering.Wb {
}
public void Dispose() {
var gl = _graphicsDevice.GL;
gl.DeleteVertexArray(_vao);
gl.DeleteBuffer(_vbo);
gl.DeleteBuffer(_instanceVbo);
gl.DeleteBuffer(_ibo);
(_shader as IDisposable)?.Dispose();
_resources.RetryCleanup();
}
}
}

View file

@ -16,11 +16,22 @@ internal static unsafe class TrackedGlResource
GL gl,
uint buffer,
long capacityBytes,
string context) =>
CreateRetryableBufferDeletion(
gl,
buffer,
() => capacityBytes,
context);
public static RetryableGpuResourceRelease CreateRetryableBufferDeletion(
GL gl,
uint buffer,
Func<long> capacityBytes,
string context)
{
if (buffer == 0)
throw new ArgumentOutOfRangeException(nameof(buffer));
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
ArgumentNullException.ThrowIfNull(capacityBytes);
return new RetryableGpuResourceRelease(
() => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"),
() =>
@ -34,8 +45,10 @@ internal static unsafe class TrackedGlResource
},
() =>
{
if (capacityBytes != 0)
GpuMemoryTracker.TrackDeallocation(capacityBytes, GpuResourceType.Buffer);
long bytes = capacityBytes();
ArgumentOutOfRangeException.ThrowIfNegative(bytes);
if (bytes != 0)
GpuMemoryTracker.TrackDeallocation(bytes, GpuResourceType.Buffer);
},
() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer));
}

View file

@ -122,18 +122,48 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
_dats = dats;
_resourceRetirement = resourceRetirement;
_graphicsDevice = new OpenGLGraphicsDevice(
gl,
logger,
new DebugRenderSettings(),
resourceRetirement);
_graphicsDevice.ParticleBatcher = new ParticleBatcher(_graphicsDevice);
// ConsoleErrorLogger surfaces WB's silently-caught exceptions
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
_meshManager = new ObjectMeshManager(
_graphicsDevice,
dats,
new ConsoleErrorLogger<ObjectMeshManager>());
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
OpenGLGraphicsDevice? graphicsDevice = null;
ObjectMeshManager? meshManager = null;
try
{
graphicsDevice = new OpenGLGraphicsDevice(
gl,
logger,
new DebugRenderSettings(),
resourceRetirement);
OpenGLGraphicsDevice ownedGraphicsDevice = graphicsDevice;
var graphicsDeviceRelease = new RetryableGpuResourceRelease(
ownedGraphicsDevice.Dispose,
() =>
{
ownedGraphicsDevice.ProcessGLQueue();
if (ownedGraphicsDevice.HasPendingGLWork)
{
throw new InvalidOperationException(
"WB graphics-device construction cleanup still has queued GL work.");
}
});
resources.Add("WB graphics device", graphicsDeviceRelease.Run);
graphicsDevice.ParticleBatcher = new ParticleBatcher(graphicsDevice);
// ConsoleErrorLogger surfaces WB's silently-caught exceptions
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
meshManager = new ObjectMeshManager(
graphicsDevice,
dats,
new ConsoleErrorLogger<ObjectMeshManager>());
resources.Add("WB object mesh manager", meshManager.Dispose);
resources.TransferAll();
}
catch (Exception constructionFailure)
{
resources.RollbackConstructionAndThrow(
"WbMeshAdapter construction failed and its WB/GL prefix did not cleanly roll back.",
constructionFailure);
}
_graphicsDevice = graphicsDevice;
_meshManager = meshManager;
}
/// <summary>