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

@ -721,10 +721,17 @@ atomic update/render frame-root slot, and a prepare-aware portal fallback and
transfer slot. GL construction and state mutations now use checked commit transfer slot. GL construction and state mutations now use checked commit
boundaries with exact retry ownership for failed names, bindless residency, boundaries with exact retry ownership for failed names, bindless residency,
and texture-binding restoration. `GameWindow` is 3,689 raw lines / 162 fields / and texture-binding restoration. `GameWindow` is 3,689 raw lines / 162 fields /
37 methods. Checkpoints IL remain active, with ordered production composition 37 methods at H. Checkpoint I.1I.5 now provide the executable nine-phase
next. The Checkpoint H production App build is warning-free, its 61 focused oracle, platform/host/content/settings phases, and the production world/render
ownership tests pass, the complete Release suite passes 7,606 tests / 5 skips, phase. `WorldRenderCompositionPhase` owns Region/environment, the mandatory
and all three final corrected-diff reviews are clean. modern-renderer foundation, immutable terrain-worker inputs, and WB/texture/
sampler construction; every Phase-4 GL constructor has retryable prefix
ownership. `GameWindow` is 3,522 raw lines after I.5. Checkpoints I.6L remain
active, with retained UI and live presentation composition next. The current
production App build is warning-free; the App gate passes 3,373 tests / 3
intentional skips and the complete Release suite passes 7,745 tests / 5
intentional skips. I.5's corrected-diff behavior, architecture, and
adversarial reviews are clean.
### 4.4 Exit criteria ### 4.4 Exit criteria

View file

@ -253,6 +253,27 @@ Small implementation commits may be used while the checkpoint is active, but
each must build and preserve production startup. The final I commit closes the each must build and preserve production startup. The final I commit closes the
ledger only after the complete pipeline is live. ledger only after the complete pipeline is live.
### Current implementation progress — 2026-07-22
- I.1I.4 are committed: the executable pipeline/rollback oracle, platform
acquisition, host input/camera, content/effects/audio, and settings/devtools
phases are the production startup path.
- I.5 is complete. `WorldRenderCompositionPhase` now owns the ordered Region,
environment, mandatory bindless, terrain-atlas, shader, lighting, debug/HUD,
terrain, WB, texture, and sampler foundation. `GameWindow` is a write-only
publisher for this phase and no longer contains the Phase-4 construction
body.
- Every fallible GL constructor in the Phase-4 prefix now publishes each name
into retryable construction ownership before later GL work. Failure testing
covers every composition boundary, every disposable publication, partial HUD
construction, reverse cleanup, retry, and no replay.
- The I.5 tree is 3,522 raw `GameWindow.cs` lines. The App gate passes 3,373
tests / 3 intentional skips; the complete Release suite passes 7,745 tests /
5 intentional skips. Production App builds with zero warnings/errors; the
solution retains only the 17 warnings tracked by #228.
- I.6 (retained UI plus live-presentation composition) is next. Checkpoint I
remains active until I.9 closes the complete nine-phase pipeline.
## 6. Automated acceptance ## 6. Automated acceptance
### Platform and pipeline ### Platform and pipeline

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 Glyph[] _glyphs;
private readonly int _firstChar; private readonly int _firstChar;
private readonly int _numChars; private readonly int _numChars;
private readonly ResourceCleanupGroup _resources;
public uint TextureId { get; } public uint TextureId { get; }
public float PixelHeight { get; } public float PixelHeight { get; }
@ -95,9 +96,28 @@ public sealed unsafe class BitmapFont : IDisposable
adv: bc.xadvance); adv: bc.xadvance);
} }
// Upload atlas as a single-channel GL texture (R8). // Upload atlas as a single-channel GL texture (R8). Publish the GL
TextureId = _gl.GenTexture(); // name into the construction ledger before any later upload/state
_gl.BindTexture(TextureTarget.Texture2D, TextureId); // command can fail.
var resources = new ResourceCleanupGroup();
uint texture = 0;
try
{
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); _gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
fixed (byte* ptr = pixels) fixed (byte* ptr = pixels)
{ {
@ -114,8 +134,27 @@ public sealed unsafe class BitmapFont : IDisposable
(int)TextureWrapMode.ClampToEdge); (int)TextureWrapMode.ClampToEdge);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
(int)TextureWrapMode.ClampToEdge); (int)TextureWrapMode.ClampToEdge);
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 4); // restore default }
_gl.BindTexture(TextureTarget.Texture2D, 0); finally
{
_gl.PixelStore(
PixelStoreParameter.UnpackAlignment,
previousAlignment);
_gl.BindTexture(
TextureTarget.Texture2D,
unchecked((uint)previousTexture));
}
});
}
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) public bool TryGetGlyph(char c, out Glyph g)
@ -144,7 +183,7 @@ public sealed unsafe class BitmapFont : IDisposable
public void Dispose() public void Dispose()
{ {
_gl.DeleteTexture(TextureId); _resources.RetryCleanup();
} }
/// <summary> /// <summary>

View file

@ -20,6 +20,7 @@ public sealed unsafe class DebugLineRenderer : IDisposable
private readonly Shader _shader; private readonly Shader _shader;
private readonly uint _vao; private readonly uint _vao;
private readonly uint _vbo; private readonly uint _vbo;
private readonly ResourceCleanupGroup _resources;
private readonly List<float> _buffer = new(4096); private readonly List<float> _buffer = new(4096);
private int _vertexCount; private int _vertexCount;
@ -27,25 +28,67 @@ public sealed unsafe class DebugLineRenderer : IDisposable
public DebugLineRenderer(GL gl, string shaderDir) public DebugLineRenderer(GL gl, string shaderDir)
{ {
_gl = gl; _gl = gl ?? throw new ArgumentNullException(nameof(gl));
_shader = new Shader(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.vert"),
Path.Combine(shaderDir, "debug_line.frag")); 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(); GlResourceCommand.Execute(gl, "configure debug-line vertex state", () =>
_vbo = _gl.GenBuffer(); {
gl.BindVertexArray(vao);
_gl.BindVertexArray(_vao); gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
// 24-byte stride: vec3 pos + vec3 color // 24-byte stride: vec3 pos + vec3 color
_gl.EnableVertexAttribArray(0); gl.EnableVertexAttribArray(0);
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0); gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0);
_gl.EnableVertexAttribArray(1); gl.EnableVertexAttribArray(1);
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)(3 * sizeof(float))); 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.BindBuffer(BufferTargetARB.ArrayBuffer, 0); _resources = resources;
_gl.BindVertexArray(0); _shader = shader!;
_vao = vao;
_vbo = vbo;
} }
/// <summary>Clear accumulated lines. Call at the start of each frame.</summary> /// <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() public void Dispose()
{ {
_gl.DeleteVertexArray(_vao); _resources.RetryCleanup();
_gl.DeleteBuffer(_vbo);
_shader.Dispose();
} }
} }

View file

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

View file

@ -1,6 +1,7 @@
using AcDream.Core.Plugins; using AcDream.Core.Plugins;
using AcDream.App.Composition; using AcDream.App.Composition;
using AcDream.App.Physics; using AcDream.App.Physics;
using AcDream.App.Rendering.Wb;
using AcDream.App.Settings; using AcDream.App.Settings;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Content; using AcDream.Content;
@ -17,7 +18,8 @@ public sealed class GameWindow :
IGameWindowPlatformPublication<GL, IInputContext>, IGameWindowPlatformPublication<GL, IInputContext>,
IGameWindowHostInputCameraPublication, IGameWindowHostInputCameraPublication,
IGameWindowContentEffectsAudioPublication, IGameWindowContentEffectsAudioPublication,
IGameWindowSettingsDevToolsPublication IGameWindowSettingsDevToolsPublication,
IGameWindowWorldRenderPublication
{ {
private static double ClientTimerNow() => private static double ClientTimerNow() =>
System.Diagnostics.Stopwatch.GetTimestamp() System.Diagnostics.Stopwatch.GetTimestamp()
@ -802,6 +804,84 @@ public sealed class GameWindow :
_debugVm = value.Debug; _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>( private static void PublishCompositionOwner<T>(
ref T? destination, ref T? destination,
T value, T value,
@ -932,168 +1012,22 @@ public sealed class GameWindow :
Console.WriteLine), Console.WriteLine),
this).Compose(platform, hostInputCamera, contentEffectsAudio); this).Compose(platform, hostInputCamera, contentEffectsAudio);
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f); const uint initialCenterLandblockId = 0xA9B4FFFFu;
_gl.Enable(EnableCap.DepthTest); WorldRenderResult worldRender = new WorldRenderCompositionPhase(
new WorldRenderDependencies(
// Phase U.3: the 8 hardware clip planes (GL_CLIP_DISTANCE0..7) are NOT _worldEnvironment,
// enabled here. Only the mesh_modern / terrain_modern vertex shaders write _renderResourceLifetime,
// gl_ClipDistance[0..7]; the sky / particle / weather / UI / debug-line _gpuFrameFlights!,
// shaders write gl_Position but no gl_ClipDistance. Enabling a clip plane initialCenterLandblockId,
// for a draw whose vertex shader doesn't write that plane's distance is Console.WriteLine),
// UNDEFINED per the GL/GLSL spec (a driver may read the unwritten value as this).Compose(platform, contentEffectsAudio, settingsDevTools);
// negative and clip the primitive away). So instead of a permanent global string shadersDir = worldRender.Foundation.ShadersDirectory;
// enable, OnRender brackets glEnable/glDisable(GL_CLIP_DISTANCE0..7) around TerrainAtlas terrainAtlas = worldRender.Foundation.TerrainAtlas;
// ONLY the clip-writing world-geometry draws (terrain + entities, plus int centerX = worldRender.TerrainBuild.InitialCenterX;
// U.4's EnvCellRenderer.Render); everything else draws with clipping off. int centerY = worldRender.TerrainBuild.InitialCenterY;
Console.WriteLine(
string shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders"); $"loading world view centered on " +
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
// 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);
// Retail ClientCombatSystem attack-request owner. This exists independently // Retail ClientCombatSystem attack-request owner. This exists independently
// of the retained UI so keyboard combat keeps the same press/hold/release // 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 // Phase N.4 Task 12: construct LandblockSpawnAdapter under the feature flag
// and rebuild _worldState so it threads the adapter in. _worldState starts // and rebuild _worldState so it threads the adapter in. _worldState starts
// as an unadorned GpuWorldState (field initializer); here we replace it with // as an unadorned GpuWorldState (field initializer); here we replace it with
@ -1937,7 +1855,7 @@ public sealed class GameWindow :
contentEffectsAudio.Dats, contentEffectsAudio.Dats,
skyShader, skyShader,
_textureCache!, _textureCache!,
_samplerCache); _samplerCache!);
// Phase G.1 particle renderer — renders rain / snow / spell auras // Phase G.1 particle renderer — renders rain / snow / spell auras
// spawned into the shared ParticleSystem. Mode-1/no-degrade GfxObjs // spawned into the shared ParticleSystem. Mode-1/no-degrade GfxObjs

View file

@ -221,14 +221,21 @@ public sealed unsafe class ParticleRenderer : IDisposable
handle => _particleGfxInfoByEmitter.Remove(handle), handle => _particleGfxInfoByEmitter.Remove(handle),
handle => _textures?.ReleaseParticleTextureOwner(handle), handle => _textures?.ReleaseParticleTextureOwner(handle),
error => Console.Error.WriteLine($"[particles] {error}")); error => Console.Error.WriteLine($"[particles] {error}"));
var constructionResources = new ResourceCleanupGroup();
try
{
_shader = new Shader(_gl, _shader = new Shader(_gl,
System.IO.Path.Combine(shadersDir, "particle.vert"), System.IO.Path.Combine(shadersDir, "particle.vert"),
System.IO.Path.Combine(shadersDir, "particle.frag")); System.IO.Path.Combine(shadersDir, "particle.frag"));
constructionResources.Add("particle shader", _shader.Dispose);
if (_meshAdapter?.MeshManager?.GlobalBuffer is not null) if (_meshAdapter?.MeshManager?.GlobalBuffer is not null)
{ {
_meshShader = new Shader(_gl, _meshShader = new Shader(_gl,
System.IO.Path.Combine(shadersDir, "particle_mesh.vert"), System.IO.Path.Combine(shadersDir, "particle_mesh.vert"),
System.IO.Path.Combine(shadersDir, "particle_mesh.frag")); System.IO.Path.Combine(shadersDir, "particle_mesh.frag"));
constructionResources.Add(
"particle mesh shader",
_meshShader.Dispose);
_meshTextureHandleLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureHandle"); _meshTextureHandleLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureHandle");
_meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureLayer"); _meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureLayer");
} }
@ -242,14 +249,18 @@ public sealed unsafe class ParticleRenderer : IDisposable
}; };
uint[] quadIdx = { 0, 1, 2, 0, 2, 3 }; uint[] quadIdx = { 0, 1, 2, 0, 2, 3 };
uint quadVbo = 0;
uint quadEbo = 0;
uint uploadVao = 0;
bool vboAllocated = false; bool vboAllocated = false;
bool eboAllocated = false; bool eboAllocated = false;
try uint quadVbo = TrackedGlResource.CreateBuffer(
{ _gl,
quadVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle quad VBO"); "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) fixed (void* p = quadVerts)
{ {
TrackedGlResource.AllocateBufferStorage( TrackedGlResource.AllocateBufferStorage(
@ -264,9 +275,31 @@ public sealed unsafe class ParticleRenderer : IDisposable
} }
vboAllocated = true; vboAllocated = true;
quadEbo = TrackedGlResource.CreateBuffer(_gl, "creating particle quad EBO"); uint quadEbo = TrackedGlResource.CreateBuffer(
uploadVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle upload VAO"); _gl,
_gl.BindVertexArray(uploadVao); "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) fixed (void* p = quadIdx)
{ {
TrackedGlResource.AllocateBufferStorage( TrackedGlResource.AllocateBufferStorage(
@ -280,44 +313,26 @@ public sealed unsafe class ParticleRenderer : IDisposable
"uploading particle quad EBO"); "uploading particle quad EBO");
} }
eboAllocated = true; eboAllocated = true;
GlResourceCommand.Execute(_gl, "finish particle static-buffer upload", () =>
{
_gl.BindVertexArray(0); _gl.BindVertexArray(0);
TrackedGlResource.DeleteVertexArray(_gl, uploadVao, "deleting particle upload VAO");
uploadVao = 0;
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
});
uploadVaoRelease.Run();
_quadVbo = quadVbo; _quadVbo = quadVbo;
_quadEbo = quadEbo; _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> /// <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 _locForceFarZ;
private readonly int _locDepthBias; private readonly int _locDepthBias;
private readonly int _locDepthBiasEyeCapN; private readonly int _locDepthBiasEyeCapN;
private readonly ResourceCleanupGroup _resources;
private const int MaxFanVerts = 32; private const int MaxFanVerts = 32;
private readonly float[] _scratch = new float[MaxFanVerts * 3]; 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) public PortalDepthMaskRenderer(GL gl)
{ {
_gl = gl ?? throw new ArgumentNullException(nameof(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); _locViewProjection,
_program = _gl.CreateProgram(); _locPlaneCount,
_gl.AttachShader(_program, vs); _locPlanes,
_gl.AttachShader(_program, fs); _locForceFarZ,
_gl.LinkProgram(_program); _locDepthBias,
_gl.GetProgram(_program, ProgramPropertyARB.LinkStatus, out int linked); _locDepthBiasEyeCapN) = GlResourceCommand.Execute(
if (linked == 0) _gl,
throw new InvalidOperationException($"PortalDepthMask link failed: {_gl.GetProgramInfoLog(_program)}"); "resolve PortalDepthMask uniforms",
_gl.DeleteShader(vs); () => (
_gl.DeleteShader(fs); _gl.GetUniformLocation(_program, "uViewProjection"),
_gl.GetUniformLocation(_program, "uPlaneCount"),
_locViewProjection = _gl.GetUniformLocation(_program, "uViewProjection"); _gl.GetUniformLocation(_program, "uPlanes"),
_locPlaneCount = _gl.GetUniformLocation(_program, "uPlaneCount"); _gl.GetUniformLocation(_program, "uForceFarZ"),
_locPlanes = _gl.GetUniformLocation(_program, "uPlanes"); _gl.GetUniformLocation(_program, "uDepthBias"),
_locForceFarZ = _gl.GetUniformLocation(_program, "uForceFarZ"); _gl.GetUniformLocation(_program, "uDepthBiasEyeCapN")));
_locDepthBias = _gl.GetUniformLocation(_program, "uDepthBias");
_locDepthBiasEyeCapN = _gl.GetUniformLocation(_program, "uDepthBiasEyeCapN");
for (int i = 0; i < _frameBuffers.Length; i++) for (int i = 0; i < _frameBuffers.Length; i++)
_frameBuffers[i] = CreateFrameBufferSet(); {
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();
}
_resources = resources;
} }
/// <summary> /// <summary>
@ -146,19 +171,34 @@ void main() { } // depth-only: color writes are masked off by the caller state
_activeFrameBuffer.UsedVertices = 0; _activeFrameBuffer.UsedVertices = 0;
} }
private FrameBufferSet CreateFrameBufferSet() private FrameBufferSet CreateFrameBufferSet(
ResourceCleanupGroup resources,
int frameIndex)
{ {
uint vao = 0; uint vao = TrackedGlResource.CreateVertexArray(
uint vbo = 0;
try
{
vao = TrackedGlResource.CreateVertexArray(
_gl, _gl,
"PortalDepthMask frame VAO creation"); "PortalDepthMask frame VAO creation");
vbo = TrackedGlResource.CreateBuffer( RetryableGpuResourceRelease vaoRelease =
TrackedGlResource.CreateRetryableVertexArrayDeletion(
_gl,
vao,
"PortalDepthMask frame VAO disposal");
resources.Add($"portal-depth frame {frameIndex} VAO", vaoRelease.Run);
uint vbo = TrackedGlResource.CreateBuffer(
_gl, _gl,
"PortalDepthMask frame VBO creation"); "PortalDepthMask frame VBO creation");
var set = new FrameBufferSet { Vao = vao, Vbo = vbo }; 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", () =>
{
_gl.BindVertexArray(set.Vao); _gl.BindVertexArray(set.Vao);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo);
unsafe unsafe
@ -168,30 +208,9 @@ void main() { } // depth-only: color writes are masked off by the caller state
_gl.EnableVertexAttribArray(0); _gl.EnableVertexAttribArray(0);
_gl.BindVertexArray(0); _gl.BindVertexArray(0);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
});
return set; 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;
}
/// <summary> /// <summary>
/// #117 (2026-06-11): the mark-pass depth bias, in NDC, toward the /// #117 (2026-06-11): the mark-pass depth bias, in NDC, toward the
@ -387,18 +406,6 @@ void main() { } // depth-only: color writes are masked off by the caller state
public void Dispose() public void Dispose()
{ {
_gl.DeleteProgram(_program); _resources.RetryCleanup();
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");
}
} }
} }

View file

@ -1,5 +1,7 @@
namespace AcDream.App.Rendering; namespace AcDream.App.Rendering;
using System.Runtime.ExceptionServices;
/// <summary> /// <summary>
/// Reverse-order, all-attempted cleanup owner used while a composite resource /// Reverse-order, all-attempted cleanup owner used while a composite resource
/// is still under construction and after it becomes the aggregate owner. /// 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)); _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() public void RetryCleanup()
{ {
if (_running || IsCleanupComplete) if (_running || IsCleanupComplete)
@ -60,4 +71,26 @@ internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup
if (failures is not null) if (failures is not null)
throw new AggregateException("Composite resource cleanup remains incomplete.", failures); 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 public sealed class SamplerCache : IDisposable
{ {
private readonly GL _gl; private readonly GL _gl;
private readonly ResourceCleanupGroup _resources;
/// <summary>Sampler with WrapS = WrapT = Repeat. The default for textures uploaded by <see cref="TextureCache"/>.</summary> /// <summary>Sampler with WrapS = WrapT = Repeat. The default for textures uploaded by <see cref="TextureCache"/>.</summary>
public uint Wrap { get; } public uint Wrap { get; }
@ -41,23 +42,62 @@ public sealed class SamplerCache : IDisposable
public SamplerCache(GL gl) public SamplerCache(GL gl)
{ {
_gl = gl ?? throw new ArgumentNullException(nameof(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(); clamp = GlResourceCommand.CreateName(
_gl.SamplerParameter(Wrap, SamplerParameterI.WrapS, (int)TextureWrapMode.Repeat); _gl,
_gl.SamplerParameter(Wrap, SamplerParameterI.WrapT, (int)TextureWrapMode.Repeat); "clamp sampler",
_gl.SamplerParameter(Wrap, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear); _gl.GenSampler,
_gl.SamplerParameter(Wrap, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear); _gl.DeleteSampler);
uint ownedClamp = clamp;
Clamp = _gl.GenSampler(); resources.Add(
_gl.SamplerParameter(Clamp, SamplerParameterI.WrapS, (int)TextureWrapMode.ClampToEdge); "clamp sampler",
_gl.SamplerParameter(Clamp, SamplerParameterI.WrapT, (int)TextureWrapMode.ClampToEdge); () => GlResourceCommand.Execute(
_gl.SamplerParameter(Clamp, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear); _gl,
_gl.SamplerParameter(Clamp, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear); $"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);
}
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() public void Dispose()
{ {
if (Wrap != 0) _gl.DeleteSampler(Wrap); _resources.RetryCleanup();
if (Clamp != 0) _gl.DeleteSampler(Clamp);
} }
} }

View file

@ -144,42 +144,56 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
if (_uTexTilingLoc < 0) if (_uTexTilingLoc < 0)
throw new InvalidOperationException("terrain_modern.frag is missing the required uTexTiling uniform."); throw new InvalidOperationException("terrain_modern.frag is missing the required uTexTiling uniform.");
var constructionResources = new ResourceCleanupGroup();
try try
{ {
_globalVao = TrackedGlResource.CreateVertexArray( _globalVao = TrackedGlResource.CreateVertexArray(
_gl, _gl,
"creating terrain global VAO"); "creating terrain global VAO");
_globalVbo = TrackedGlResource.CreateBuffer( RetryableGpuResourceRelease globalVaoRelease =
_gl, TrackedGlResource.CreateRetryableVertexArrayDeletion(
"creating terrain global vertex buffer");
_globalEbo = TrackedGlResource.CreateBuffer(
_gl,
"creating terrain global index buffer");
AllocateGpuBuffers(initialSlotCapacity);
ConfigureVao(_globalVao, _globalVbo, _globalEbo);
}
catch
{
TrackedGlResource.DeleteVertexArray(
_gl, _gl,
_globalVao, _globalVao,
"rolling back terrain global VAO"); "rolling back terrain global VAO");
TrackedGlResource.DeleteBuffer( constructionResources.Add(
"terrain global VAO",
globalVaoRelease.Run);
_globalVbo = TrackedGlResource.CreateBuffer(
_gl,
"creating terrain global vertex buffer");
RetryableGpuResourceRelease globalVboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl, _gl,
_globalVbo, _globalVbo,
_globalVboCapacityBytes, () => _globalVboCapacityBytes,
"rolling back terrain global vertex buffer"); "rolling back terrain global vertex buffer");
TrackedGlResource.DeleteBuffer( constructionResources.Add(
"terrain global vertex buffer",
globalVboRelease.Run);
_globalEbo = TrackedGlResource.CreateBuffer(
_gl,
"creating terrain global index buffer");
RetryableGpuResourceRelease globalEboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl, _gl,
_globalEbo, _globalEbo,
_globalEboCapacityBytes, () => _globalEboCapacityBytes,
"rolling back terrain global index buffer"); "rolling back terrain global index buffer");
_globalVao = 0; constructionResources.Add(
_globalVbo = 0; "terrain global index buffer",
_globalEbo = 0; globalEboRelease.Run);
_globalVboCapacityBytes = 0; AllocateGpuBuffers(initialSlotCapacity);
_globalEboCapacityBytes = 0; GlResourceCommand.Execute(
throw; _gl,
"configure terrain global vertex array",
() => ConfigureVao(_globalVao, _globalVbo, _globalEbo));
constructionResources.TransferAll();
}
catch (Exception constructionFailure)
{
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); ArgumentNullException.ThrowIfNull(retirementQueue);
if (bindless is not null) if (bindless is not null)
{ {
_compositeTextures = new CompositeTextureArrayCache(gl, bindless, retirementQueue); var resources = new ResourceCleanupGroup();
_particleTextures = new StandaloneBindlessTextureCache( 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), new ParticleTextureBackend(this),
retirementQueue); 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 Chorizite.Core.Render;
using AcDream.App.Rendering;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL; using Silk.NET.OpenGL;
using System; using System;
@ -161,7 +162,7 @@ namespace AcDream.App.Rendering.Wb {
if (string.IsNullOrWhiteSpace(vertShaderSource) || string.IsNullOrWhiteSpace(fragShaderSource)) { if (string.IsNullOrWhiteSpace(vertShaderSource) || string.IsNullOrWhiteSpace(fragShaderSource)) {
_log.LogError($"Shader {Name} has no source code!"); _log.LogError($"Shader {Name} has no source code!");
return; throw new InvalidOperationException($"Shader {Name} has no source code.");
} }
if (_device.HasOpenGL43 && _device.HasBindless) { if (_device.HasOpenGL43 && _device.HasBindless) {
@ -170,41 +171,47 @@ namespace AcDream.App.Rendering.Wb {
fragShaderSource = fragShaderSource.Replace("#version 330 core", replacement); fragShaderSource = fragShaderSource.Replace("#version 330 core", replacement);
} }
uint vertexShader = CompileShader(ShaderType.VertexShader, Name, vertShaderSource); var resources = new ResourceCleanupGroup();
uint fragmentShader = CompileShader(ShaderType.FragmentShader, Name, fragShaderSource); uint prog = 0;
bool accountingPublished = false;
var prog = GL.CreateProgram(); try {
GLHelpers.CheckErrors(GL, true); prog = ShaderProgramConstruction.Build(
GL.AttachShader(prog, vertexShader); new GlShaderProgramBuildApi(GL),
GLHelpers.CheckErrors(GL, true); vertShaderSource,
GL.AttachShader(prog, fragmentShader); fragShaderSource);
GLHelpers.CheckErrors(GL, true); uint ownedProgram = prog;
GL.LinkProgram(prog); var unpublishedProgramRelease = new RetryableGpuResourceRelease(
GLHelpers.CheckErrors(GL, true); () => GlResourceCommand.DeleteProgram(
GL,
GL.GetProgram(prog, GLEnum.LinkStatus, out int success); ownedProgram,
GLHelpers.CheckErrors(GL); $"delete unpublished WB shader program {ownedProgram}"),
if (success != 1) { () => {
var infoLog = GL.GetProgramInfoLog(prog); if (accountingPublished)
_log.LogError($"Error: shader {Name} link failed: {infoLog}"); {
GL.DeleteProgram(prog); GpuMemoryTracker.TrackResourceDeallocation(
return; GpuResourceType.Shader);
}
else {
_log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
} }
});
resources.Add("WB shader program", unpublishedProgramRelease.Run);
// Bind SceneData uniform block to point 0 if it exists // Bind SceneData uniform block to point 0 if it exists.
var sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData"); GlResourceCommand.Execute(GL, $"configure shader {Name} SceneData binding", () => {
if (sceneDataIndex != uint.MaxValue) { uint sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData");
if (sceneDataIndex != uint.MaxValue)
GL.UniformBlockBinding(prog, sceneDataIndex, 0); GL.UniformBlockBinding(prog, sceneDataIndex, 0);
GLHelpers.CheckErrors(GL); });
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);
} }
GL.DeleteShader(vertexShader); _log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
GLHelpers.CheckErrors(GL);
GL.DeleteShader(fragmentShader);
GLHelpers.CheckErrors(GL);
if (Program != 0) { if (Program != 0) {
Unload(); Unload();
@ -212,32 +219,12 @@ namespace AcDream.App.Rendering.Wb {
_uniformLocations.Clear(); _uniformLocations.Clear();
_uniformValues.Clear(); _uniformValues.Clear();
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Shader);
Program = prog; Program = prog;
ProgramId = prog; ProgramId = prog;
NeedsLoad = false; NeedsLoad = false;
GLHelpers.CheckErrors(GL); 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() { public override void Bind() {
lock (_lock) { lock (_lock) {
SetActive(); SetActive();

View file

@ -28,30 +28,44 @@ namespace AcDream.App.Rendering.Wb {
/// <param name="usage">Buffer usage</param> /// <param name="usage">Buffer usage</param>
/// <param name="size">The size of the buffer, in bytes</param> /// <param name="size">The size of the buffer, in bytes</param>
public unsafe ManagedGLUniformBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) { public unsafe ManagedGLUniformBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
_device = device; _device = device ?? throw new ArgumentNullException(nameof(device));
ArgumentOutOfRangeException.ThrowIfLessThan(size, 1);
Size = size; Size = size;
Usage = usage; Usage = usage;
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
// Generate the buffer uint buffer = 0;
bufferId = GL.GenBuffer(); bool allocated = false;
if (bufferId == 0) { try {
throw new Exception("Failed to generate uniform buffer."); buffer = TrackedGlResource.CreateBuffer(
} GL,
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer); "creating managed uniform buffer");
GLHelpers.CheckErrors(GL); uint ownedBuffer = buffer;
RetryableGpuResourceRelease unpublishedBufferRelease =
// Allocate the buffer with the specified size TrackedGlResource.CreateRetryableBufferDeletion(
GL.BindBuffer(GLEnum.UniformBuffer, bufferId); GL,
GLHelpers.CheckErrors(GL); ownedBuffer,
() => allocated ? Size : 0,
GL.BufferData( "rolling back managed uniform buffer");
resources.Add(
"managed uniform buffer",
unpublishedBufferRelease.Run);
TrackedGlResource.AllocateBufferStorage(
GL,
GLEnum.UniformBuffer, GLEnum.UniformBuffer,
(uint)Size, buffer,
(void*)0, // No initial data 0,
GLEnum.DynamicDraw); Size,
GLHelpers.CheckErrors(GL); 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.TrackAllocation(Size, GpuResourceType.Buffer); bufferId = buffer;
} }
/// <inheritdoc /> /// <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;
using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex; using Chorizite.Core.Render.Vertex;
using AcDream.App.Rendering;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL; using Silk.NET.OpenGL;
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace // IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
@ -149,37 +150,48 @@ namespace AcDream.App.Rendering.Wb {
HasBindless = false; HasBindless = false;
} }
GL.GenBuffers(1, out uint instanceVbo); var resources = new ResourceCleanupGroup();
InstanceVBO = instanceVbo; try {
InstanceVBO = CreateConstructionBuffer(resources, "WB instance buffer");
// Query this immutable device limit once. Atlas construction can // Query this immutable device limit once. Atlas construction can
// happen hundreds of times during portal streaming; repeating a // happen hundreds of times during portal streaming; repeating a
// driver GetFloat for every texture serialized the upload burst. // driver GetFloat for every texture serialized the upload burst.
if (renderSettings.EnableAnisotropicFiltering) { if (renderSettings.EnableAnisotropicFiltering) {
MaxSupportedAnisotropy = GlResourceCommand.Execute(
GL,
"query maximum texture anisotropy",
() => {
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso); GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
MaxSupportedAnisotropy = Math.Max(0f, maxAniso); return Math.Max(0f, maxAniso);
});
} }
// Create sampler objects for wrap vs clamp WrapSampler = CreateConstructionSampler(
WrapSampler = GL.GenSampler(); resources,
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.Repeat); TextureWrapMode.Repeat,
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.Repeat); "WB repeat sampler");
GL.SamplerParameter(WrapSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear); ClampSampler = CreateConstructionSampler(
GL.SamplerParameter(WrapSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear); resources,
if (MaxSupportedAnisotropy > 0) TextureWrapMode.ClampToEdge,
GL.SamplerParameter(WrapSampler, GLEnum.TextureMaxAnisotropy, MaxSupportedAnisotropy); "WB clamp sampler");
ClampSampler = GL.GenSampler(); _sceneDataBuffer = new ManagedGLUniformBuffer(
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.ClampToEdge); this,
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.ClampToEdge); BufferUsage.Dynamic,
GL.SamplerParameter(ClampSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear); Marshal.SizeOf<SceneData>());
GL.SamplerParameter(ClampSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear); ManagedGLUniformBuffer ownedSceneDataBuffer = _sceneDataBuffer;
if (MaxSupportedAnisotropy > 0) resources.Add(
GL.SamplerParameter(ClampSampler, GLEnum.TextureMaxAnisotropy, MaxSupportedAnisotropy); "WB scene-data uniform buffer",
ownedSceneDataBuffer.DisposeImmediately);
_sceneDataBuffer = new ManagedGLUniformBuffer(this, BufferUsage.Dynamic, Marshal.SizeOf<SceneData>()); InitializeSharedDebugResources(resources);
resources.TransferAll();
InitializeSharedDebugResources(); } catch (Exception constructionFailure) {
resources.RollbackConstructionAndThrow(
"OpenGLGraphicsDevice construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
}
// ParticleBatcher is constructed post-ctor by WbMeshAdapter (WbMeshAdapter.cs:78) // ParticleBatcher is constructed post-ctor by WbMeshAdapter (WbMeshAdapter.cs:78)
// after the adapter has wired up all dependencies. The null! here is overridden // after the adapter has wired up all dependencies. The null! here is overridden
@ -196,7 +208,49 @@ namespace AcDream.App.Rendering.Wb {
internal AcDream.App.Rendering.IGpuResourceRetirementQueue ResourceRetirement => internal AcDream.App.Rendering.IGpuResourceRetirementQueue ResourceRetirement =>
_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) // Unit quad vertices for two triangles (0 to 1 for length, -0.5 to 0.5 for thickness)
float[] quadVertices = { float[] quadVertices = {
0.0f, -0.5f, 0.0f, -0.5f,
@ -207,21 +261,41 @@ namespace AcDream.App.Rendering.Wb {
0.0f, 0.5f 0.0f, 0.5f
}; };
GL.GenBuffers(1, out uint quadVbo); SharedQuadVBO = CreateConstructionBuffer(resources, "WB shared debug quad buffer");
SharedQuadVBO = quadVbo; 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}"));
GlResourceCommand.Execute(GL, "configure WB shared debug resources", () => {
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO); GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
fixed (float* pQuad = quadVertices) { fixed (float* pQuad = quadVertices) {
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(quadVertices.Length * sizeof(float)), pQuad, GLEnum.StaticDraw); GL.BufferData(
GLEnum.ArrayBuffer,
(nuint)(quadVertices.Length * sizeof(float)),
pQuad,
GLEnum.StaticDraw);
} }
GL.GenBuffers(1, out uint debugInstanceVbo); // Initial capacity for debug instances.
SharedDebugInstanceVBO = debugInstanceVbo;
// Initial capacity for debug instances
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO); GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(1024 * 44), (void*)0, GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance) GL.BufferData(
GLEnum.ArrayBuffer,
(nuint)(1024 * 44),
(void*)0,
GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance)
GL.GenVertexArrays(1, out uint debugVao);
SharedDebugVAO = debugVao;
GL.BindVertexArray(SharedDebugVAO); GL.BindVertexArray(SharedDebugVAO);
// Quad Pos attribute (location 0) // Quad Pos attribute (location 0)
@ -231,7 +305,7 @@ namespace AcDream.App.Rendering.Wb {
// Instance attributes // Instance attributes
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO); GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
uint lineInstanceSize = 44; // Marshal.SizeOf<LineInstance>() - we'll hardcode or use a constant later uint lineInstanceSize = 44;
// aStart (location 1) // aStart (location 1)
GL.EnableVertexAttribArray(1); GL.EnableVertexAttribArray(1);
@ -240,20 +314,21 @@ namespace AcDream.App.Rendering.Wb {
// aEnd (location 2) // aEnd (location 2)
GL.EnableVertexAttribArray(2); GL.EnableVertexAttribArray(2);
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12); // OffsetOf End GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12);
GL.VertexAttribDivisor(2, 1); GL.VertexAttribDivisor(2, 1);
// aColor (location 3) // aColor (location 3)
GL.EnableVertexAttribArray(3); GL.EnableVertexAttribArray(3);
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24); // OffsetOf Color GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24);
GL.VertexAttribDivisor(3, 1); GL.VertexAttribDivisor(3, 1);
// aThickness (location 4) // aThickness (location 4)
GL.EnableVertexAttribArray(4); GL.EnableVertexAttribArray(4);
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40); // OffsetOf Thickness GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40);
GL.VertexAttribDivisor(4, 1); GL.VertexAttribDivisor(4, 1);
GL.BindVertexArray(0); GL.BindVertexArray(0);
});
} }
public void EnsureInstanceBufferCapacity(int count, int stride, bool forceOrphan = false) { 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 _ibo;
private readonly uint _instanceVbo; private readonly uint _instanceVbo;
private readonly IShader _shader; private readonly IShader _shader;
private readonly ResourceCleanupGroup _resources;
private readonly ParticleInstance[] _instanceData = new ParticleInstance[MAX_PARTICLES_TOTAL]; private readonly ParticleInstance[] _instanceData = new ParticleInstance[MAX_PARTICLES_TOTAL];
private readonly List<ParticleRenderData> _allParticles = new(); private readonly List<ParticleRenderData> _allParticles = new();
private int _currentInstanceCount = 0; private int _currentInstanceCount = 0;
@ -43,91 +44,108 @@ namespace AcDream.App.Rendering.Wb {
private Vector3 _cameraRight; private Vector3 _cameraRight;
public ParticleBatcher(OpenGLGraphicsDevice graphicsDevice) { public ParticleBatcher(OpenGLGraphicsDevice graphicsDevice) {
_graphicsDevice = graphicsDevice; _graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice));
var gl = _graphicsDevice.GL; 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 vertSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.vert");
var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag"); var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag");
_shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource); shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource);
if (shader is IDisposable disposableShader)
resources.Add("WB particle shader", disposableShader.Dispose);
// Create quad vertices - centered to match ACViewer expansion logic // Create quad vertices - centered to match ACViewer expansion logic
float[] vertices = { float[] vertices = {
// x, y, z, u, v
-0.5f, 0.0f, -0.5f, 0.0f, 1.0f, -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, 1.0f,
0.5f, 0.0f, 0.5f, 1.0f, 0.0f, 0.5f, 0.0f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.0f, 0.5f, 0.0f, 0.0f -0.5f, 0.0f, 0.5f, 0.0f, 0.0f
}; };
ushort[] indices = { 0, 1, 2, 2, 3, 0 }; ushort[] indices = { 0, 1, 2, 2, 3, 0 };
_vao = gl.GenVertexArray(); vao = CreateVertexArray(resources, gl, "WB particle VAO");
gl.BindVertexArray(_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");
_vbo = gl.GenBuffer(); GlResourceCommand.Execute(gl, "configure WB particle batcher", () => {
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); gl.BindVertexArray(vao);
unsafe { gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
fixed (float* p = vertices) { fixed (float* p = vertices) {
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw); gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw);
} }
} gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ibo);
_ibo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ibo);
unsafe {
fixed (ushort* p = indices) { fixed (ushort* p = indices) {
gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw); gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw);
} }
}
// Quad attributes
gl.EnableVertexAttribArray(0); gl.EnableVertexAttribArray(0);
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)0); gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)0);
gl.EnableVertexAttribArray(1); gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)(3 * sizeof(float))); gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)(3 * sizeof(float)));
// Instance attributes gl.BindBuffer(BufferTargetARB.ArrayBuffer, instanceVbo);
_instanceVbo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf<ParticleInstance>()), (void*)0, BufferUsageARB.DynamicDraw); gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf<ParticleInstance>()), (void*)0, BufferUsageARB.DynamicDraw);
uint stride = (uint)Marshal.SizeOf<ParticleInstance>(); uint stride = (uint)Marshal.SizeOf<ParticleInstance>();
// iPosition
gl.EnableVertexAttribArray(2); gl.EnableVertexAttribArray(2);
gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0); gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
gl.VertexAttribDivisor(2, 1); gl.VertexAttribDivisor(2, 1);
// iScaleOpacityActive
gl.EnableVertexAttribArray(3); gl.EnableVertexAttribArray(3);
gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float))); gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
gl.VertexAttribDivisor(3, 1); gl.VertexAttribDivisor(3, 1);
// iTextureIndex
gl.EnableVertexAttribArray(4); gl.EnableVertexAttribArray(4);
gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float))); gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
gl.VertexAttribDivisor(4, 1); gl.VertexAttribDivisor(4, 1);
// iRotation (Quaternion)
gl.EnableVertexAttribArray(5); gl.EnableVertexAttribArray(5);
gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float))); gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float)));
gl.VertexAttribDivisor(5, 1); gl.VertexAttribDivisor(5, 1);
// iSize
gl.EnableVertexAttribArray(6); gl.EnableVertexAttribArray(6);
gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float))); gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float)));
gl.VertexAttribDivisor(6, 1); gl.VertexAttribDivisor(6, 1);
// iIsBillboard
gl.EnableVertexAttribArray(7); gl.EnableVertexAttribArray(7);
gl.VertexAttribPointer(7, 1, VertexAttribPointerType.Float, false, stride, (void*)(13 * sizeof(float))); gl.VertexAttribPointer(7, 1, VertexAttribPointerType.Float, false, stride, (void*)(13 * sizeof(float)));
gl.VertexAttribDivisor(7, 1); gl.VertexAttribDivisor(7, 1);
gl.BindVertexArray(0); gl.BindVertexArray(0);
});
_shader.Bind(); shader.Bind();
_shader.SetUniform("uTextureArray", 0); shader.SetUniform("uTextureArray", 0);
_shader.Unbind(); shader.Unbind();
} catch (Exception constructionFailure) {
resources.RollbackConstructionAndThrow(
"ParticleBatcher construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
}
_resources = resources;
_shader = shader!;
_vao = vao;
_vbo = vbo;
_ibo = ibo;
_instanceVbo = instanceVbo;
}
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;
}
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) { public void Begin(Matrix4x4 viewProjection, Vector3 cameraUp, Vector3 cameraRight) {
@ -216,12 +234,7 @@ namespace AcDream.App.Rendering.Wb {
} }
public void Dispose() { public void Dispose() {
var gl = _graphicsDevice.GL; _resources.RetryCleanup();
gl.DeleteVertexArray(_vao);
gl.DeleteBuffer(_vbo);
gl.DeleteBuffer(_instanceVbo);
gl.DeleteBuffer(_ibo);
(_shader as IDisposable)?.Dispose();
} }
} }
} }

View file

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

View file

@ -122,18 +122,48 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
_dats = dats; _dats = dats;
_resourceRetirement = resourceRetirement; _resourceRetirement = resourceRetirement;
_graphicsDevice = new OpenGLGraphicsDevice( var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
OpenGLGraphicsDevice? graphicsDevice = null;
ObjectMeshManager? meshManager = null;
try
{
graphicsDevice = new OpenGLGraphicsDevice(
gl, gl,
logger, logger,
new DebugRenderSettings(), new DebugRenderSettings(),
resourceRetirement); resourceRetirement);
_graphicsDevice.ParticleBatcher = new ParticleBatcher(_graphicsDevice); 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 // ConsoleErrorLogger surfaces WB's silently-caught exceptions
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589). // (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
_meshManager = new ObjectMeshManager( meshManager = new ObjectMeshManager(
_graphicsDevice, graphicsDevice,
dats, dats,
new ConsoleErrorLogger<ObjectMeshManager>()); 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> /// <summary>

View file

@ -0,0 +1,361 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using AcDream.App.Composition;
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.DBObjs;
using Silk.NET.Input;
using Silk.NET.OpenGL;
using Shader = AcDream.App.Rendering.Shader;
namespace AcDream.App.Tests.Composition;
public sealed class WorldRenderCompositionTests
{
[Fact]
public void SuccessPublishesExactModernFoundationInFrozenOrder()
{
var fixture = new Fixture();
WorldRenderResult result = fixture.Compose();
Assert.Equal(Enum.GetValues<WorldRenderCompositionPoint>(), fixture.Points);
Assert.Same(fixture.Publication.Terrain, result.Foundation.Terrain);
Assert.Same(fixture.Publication.MeshAdapter, result.Foundation.MeshAdapter);
Assert.Same(fixture.Publication.TextureCache, result.Foundation.TextureCache);
Assert.Same(fixture.Lifetime.Atlas, result.Foundation.TerrainAtlas);
Assert.Equal(QualitySettings.From(QualityPreset.High).AnisotropicLevel,
fixture.Factory.AnisotropicLevel);
Assert.Equal(1, fixture.Lifetime.AcquireCalls);
Assert.Empty(fixture.Factory.Releases);
}
[Fact]
public void MissingDebugFontSkipsOnlyTheOptionalHudPrefix()
{
var fixture = new Fixture(hasFont: false);
WorldRenderResult result = fixture.Compose();
Assert.Null(result.Foundation.DebugFont);
Assert.Null(result.Foundation.TextRenderer);
Assert.DoesNotContain(WorldRenderCompositionPoint.DebugFontCreated, fixture.Points);
Assert.DoesNotContain(WorldRenderCompositionPoint.TextRendererCreated, fixture.Points);
Assert.DoesNotContain(WorldRenderCompositionPoint.HudResourcesPublished, fixture.Points);
Assert.Contains(WorldRenderCompositionPoint.HudResourcesCompleted, fixture.Points);
}
[Theory]
[MemberData(nameof(FailurePoints))]
public void FaultAfterEachBoundaryStopsTheExactSuffix(int pointValue)
{
var point = (WorldRenderCompositionPoint)pointValue;
var fixture = new Fixture(failurePoint: point);
Assert.Throws<InvalidOperationException>(fixture.Compose);
Assert.Equal(
Enum.GetValues<WorldRenderCompositionPoint>()
.TakeWhile(candidate => candidate <= point),
fixture.Points);
}
public static TheoryData<int> FailurePoints()
{
var data = new TheoryData<int>();
foreach (WorldRenderCompositionPoint point in
Enum.GetValues<WorldRenderCompositionPoint>())
{
data.Add((int)point);
}
return data;
}
[Theory]
[InlineData("terrain shader", "terrain shader")]
[InlineData("scene lighting", "scene lighting")]
[InlineData("debug lines", "debug lines")]
[InlineData("HUD", "text renderer|debug font")]
[InlineData("terrain", "terrain")]
[InlineData("mesh shader", "mesh shader")]
[InlineData("WB mesh adapter", "WB mesh adapter")]
[InlineData("texture cache", "texture cache")]
[InlineData("sampler cache", "sampler cache")]
public void FailedPublicationRollsBackOnlyItsUnpublishedResourcePrefix(
string publication,
string expectedReleaseOrder)
{
var fixture = new Fixture(publicationFailure: publication);
Assert.Throws<InvalidOperationException>(fixture.Compose);
Assert.Equal(
expectedReleaseOrder.Split('|'),
fixture.Factory.Releases);
}
[Fact]
public void PartialHudConstructionRollsBackFontWhenTextCreationFails()
{
var fixture = new Fixture(
failurePoint: WorldRenderCompositionPoint.DebugFontCreated);
Assert.Throws<InvalidOperationException>(fixture.Compose);
Assert.Equal(["debug font"], fixture.Factory.Releases);
}
[Fact]
public void GameWindowUsesPhaseAndNoLongerBuildsWorldFoundationInline()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Contains("new WorldRenderCompositionPhase(", source,
StringComparison.Ordinal);
Assert.DoesNotContain("BindlessSupport.TryCreate(_gl", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_terrainModernShader = new Shader", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_wbMeshAdapter = new", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_textureCache = new TextureCache", source,
StringComparison.Ordinal);
}
private sealed class Fixture
{
private readonly WorldRenderCompositionPoint? _failurePoint;
public Fixture(
bool hasFont = true,
WorldRenderCompositionPoint? failurePoint = null,
string? publicationFailure = null)
{
_failurePoint = failurePoint;
Factory = new Factory(hasFont);
Publication = new Publication(publicationFailure);
Lifetime = new RenderLifetime(Factory.Atlas);
var content = (ContentEffectsAudioResult)
RuntimeHelpers.GetUninitializedObject(typeof(ContentEffectsAudioResult));
Content = content;
}
public Factory Factory { get; }
public Publication Publication { get; }
public RenderLifetime Lifetime { get; }
public List<WorldRenderCompositionPoint> Points { get; } = [];
public ContentEffectsAudioResult Content { get; }
public WorldRenderResult Compose() =>
new WorldRenderCompositionPhase(
new WorldRenderDependencies(
new WorldEnvironmentController(),
Lifetime,
ImmediateGpuResourceRetirementQueue.Instance,
0xA9B4FFFFu,
_ => { }),
Publication,
Factory,
point =>
{
Points.Add(point);
if (point == _failurePoint)
throw new InvalidOperationException($"fault at {point}");
}).Compose(
new GameWindowPlatformResult<GL, IInputContext>(null!, null!),
Content,
new SettingsDevToolsResult(
QualitySettings.From(QualityPreset.High),
null));
}
private sealed class RenderLifetime(TerrainAtlas atlas)
: IGameRenderResourceLifetime
{
public TerrainAtlas Atlas { get; } = atlas;
public int AcquireCalls { get; private set; }
public TerrainAtlas AcquireTerrainAtlas(Func<TerrainAtlas> factory)
{
AcquireCalls++;
return Atlas;
}
}
private sealed class Factory(bool hasFont) : IWorldRenderCompositionFactory
{
private readonly Dictionary<IDisposable, string> _names =
new(ReferenceEqualityComparer.Instance);
public TerrainAtlas Atlas { get; } = Stub<TerrainAtlas>();
public int AnisotropicLevel { get; private set; }
public List<string> Releases { get; } = [];
public void InitializeGlState(GL gl) { }
public WorldRegionData LoadRegion(IDatReaderWriter dats) =>
new(Stub<Region>(), new float[256]);
public void InitializeEnvironment(
WorldEnvironmentController environment,
Region region) { }
public BindlessSupport RequireBindless(GL gl, Action<string> log) =>
Stub<BindlessSupport>();
public TerrainAtlas AcquireTerrainAtlas(
IGameRenderResourceLifetime lifetime,
GL gl,
IDatReaderWriter dats,
BindlessSupport bindless) =>
lifetime.AcquireTerrainAtlas(() => Atlas);
public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) =>
AnisotropicLevel = level;
public Shader CreateTerrainShader(GL gl, string shadersDirectory) =>
Resource<Shader>("terrain shader");
public SceneLightingUboBinding CreateSceneLighting(GL gl) =>
Resource<SceneLightingUboBinding>("scene lighting");
public DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory) =>
Resource<DebugLineRenderer>("debug lines");
public byte[]? TryLoadDebugFont() => hasFont ? [1] : null;
public BitmapFont CreateDebugFont(GL gl, byte[] bytes) =>
Resource<BitmapFont>("debug font");
public TextRenderer CreateTextRenderer(GL gl, string shadersDirectory) =>
Resource<TextRenderer>("text renderer");
public TerrainModernRenderer CreateTerrain(
GL gl,
BindlessSupport bindless,
Shader shader,
TerrainAtlas atlas,
IGpuResourceRetirementQueue retirement) =>
Resource<TerrainModernRenderer>("terrain");
public WorldTerrainBuildContext CreateTerrainBuildContext(
uint initialCenterLandblockId,
float[] heightTable,
TerrainAtlas atlas) =>
new(
initialCenterLandblockId,
0xA9,
0xB4,
heightTable,
Stub<TerrainBlendingContext>(),
new ConcurrentDictionary<uint, SurfaceInfo>());
public Shader CreateMeshShader(GL gl, string shadersDirectory) =>
Resource<Shader>("mesh shader");
public WbMeshAdapter CreateMeshAdapter(
GL gl,
IDatReaderWriter dats,
IGpuResourceRetirementQueue retirement) =>
Resource<WbMeshAdapter>("WB mesh adapter");
public TextureCache CreateTextureCache(
GL gl,
IDatReaderWriter dats,
BindlessSupport bindless,
IGpuResourceRetirementQueue retirement) =>
Resource<TextureCache>("texture cache");
public SamplerCache CreateSamplerCache(GL gl) =>
Resource<SamplerCache>("sampler cache");
public void Release(IDisposable resource)
{
Releases.Add(_names[resource]);
}
private T Resource<T>(string name)
where T : class, IDisposable
{
T value = Stub<T>();
_names.Add(value, name);
return value;
}
}
private sealed class Publication(string? failure) : IGameWindowWorldRenderPublication
{
public TerrainModernRenderer? Terrain { get; private set; }
public WbMeshAdapter? MeshAdapter { get; private set; }
public TextureCache? TextureCache { get; private set; }
public void PublishBindlessSupport(BindlessSupport value) =>
Fail("bindless");
public void PublishTerrainShader(Shader value) =>
Fail("terrain shader");
public void PublishSceneLighting(SceneLightingUboBinding value) =>
Fail("scene lighting");
public void PublishDebugLines(DebugLineRenderer value) =>
Fail("debug lines");
public void PublishHudResources(BitmapFont font, TextRenderer text) =>
Fail("HUD");
public void PublishTerrain(TerrainModernRenderer value)
{
Fail("terrain");
Terrain = value;
}
public void PublishTerrainBuildState(
float[] heightTable,
TerrainBlendingContext blending,
ConcurrentDictionary<uint, SurfaceInfo> surfaceCache) =>
Fail("terrain build state");
public void PublishMeshShader(Shader value) => Fail("mesh shader");
public void PublishWbMeshAdapter(WbMeshAdapter value)
{
Fail("WB mesh adapter");
MeshAdapter = value;
}
public void PublishTextureCache(TextureCache value)
{
Fail("texture cache");
TextureCache = value;
}
public void PublishSamplerCache(SamplerCache value) =>
Fail("sampler cache");
private void Fail(string point)
{
if (string.Equals(failure, point, StringComparison.Ordinal))
throw new InvalidOperationException($"publication failed at {point}");
}
}
private static T Stub<T>() where T : class =>
(T)RuntimeHelpers.GetUninitializedObject(typeof(T));
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -120,12 +120,30 @@ public sealed class GameWindowSlice8BoundaryTests
string sessionFactory = MethodBody( string sessionFactory = MethodBody(
"private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(", "private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(",
"private AcDream.App.Net.LiveInventorySessionBindings"); "private AcDream.App.Net.LiveInventorySessionBindings");
string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"WorldRenderComposition.cs"));
Assert.Contains( Assert.Contains(
"private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment =", "private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment =",
source, source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains("_worldEnvironment.Initialize(region!);", load, StringComparison.Ordinal); Assert.Contains(
"new WorldRenderDependencies(\n _worldEnvironment,",
load.Replace("\r\n", "\n", StringComparison.Ordinal),
StringComparison.Ordinal);
string worldCompose = Slice(
worldPhase,
"public WorldRenderResult Compose(",
"private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(");
AssertAppearsInOrder(
worldCompose,
"WorldRegionData region = _factory.LoadRegion(content.Dats);",
"_factory.InitializeEnvironment(_dependencies.Environment, region.Region);");
Assert.Contains("environment.Initialize(region);", worldPhase, StringComparison.Ordinal);
AssertAppearsInOrder( AssertAppearsInOrder(
sessionFactory, sessionFactory,
"new AcDream.App.Net.LiveEnvironmentSessionSink(", "new AcDream.App.Net.LiveEnvironmentSessionSink(",
@ -278,7 +296,7 @@ public sealed class GameWindowSlice8BoundaryTests
load, load,
"new SettingsDevToolsCompositionPhase(", "new SettingsDevToolsCompositionPhase(",
"this).Compose(platform, hostInputCamera, contentEffectsAudio);", "this).Compose(platform, hostInputCamera, contentEffectsAudio);",
"TerrainAtlas.Build(", "new WorldRenderCompositionPhase(",
"_runtimeSettings.BindRuntimeTargets(", "_runtimeSettings.BindRuntimeTargets(",
"_liveSessionHost.Start(_options)"); "_liveSessionHost.Start(_options)");
string settingsPhase = File.ReadAllText(Path.Combine( string settingsPhase = File.ReadAllText(Path.Combine(
@ -291,6 +309,17 @@ public sealed class GameWindowSlice8BoundaryTests
settingsPhase, settingsPhase,
"_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);", "_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);",
"_dependencies.DevTools is { } optional"); "_dependencies.DevTools is { } optional");
string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"WorldRenderComposition.cs"));
AssertAppearsInOrder(
worldPhase,
"TerrainAtlas.Build(gl, dats, bindless)",
"settings.ResolvedQuality.AnisotropicLevel",
"_factory.CreateTerrain(");
AssertAppearsInOrder( AssertAppearsInOrder(
shutdown, shutdown,
"_runtimeSettings.UnbindViewModel()", "_runtimeSettings.UnbindViewModel()",
@ -440,17 +469,28 @@ public sealed class GameWindowSlice8BoundaryTests
"AcDream.App", "AcDream.App",
"Rendering", "Rendering",
"TerrainAtlas.cs")); "TerrainAtlas.cs"));
string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"WorldRenderComposition.cs"));
AssertAppearsInOrder( AssertAppearsInOrder(
load, load,
"_renderResourceLifetime.AcquireTerrainAtlas(", "new WorldRenderCompositionPhase(",
"_terrain = new TerrainModernRenderer(",
"_portalTunnelFallback.AcquirePrepared(", "_portalTunnelFallback.AcquirePrepared(",
"static portalTunnel => portalTunnel.PrepareResources());", "static portalTunnel => portalTunnel.PrepareResources());",
"_renderResourceLifetime.AcquireSkyShader(", "_renderResourceLifetime.AcquireSkyShader(",
"_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(", "_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(",
"_portalTunnelFallback.Transfer(", "_portalTunnelFallback.Transfer(",
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);"); "_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);");
AssertAppearsInOrder(
worldPhase,
"lifetime.AcquireTerrainAtlas(",
"TerrainAtlas.Build(gl, dats, bindless)",
"TerrainModernRenderer CreateTerrain(",
"TerrainModernRenderer terrain = AcquireAndPublish(");
AssertAppearsInOrder( AssertAppearsInOrder(
shutdown, shutdown,
"_frameGraphs.Withdraw();", "_frameGraphs.Withdraw();",

View file

@ -29,6 +29,107 @@ public sealed class ResourceCleanupGroupTests
Assert.Equal(["last", "middle", "first", "middle"], calls); Assert.Equal(["last", "middle", "first", "middle"], calls);
} }
[Fact]
public void TransferAllLeavesPublishedResourcesUntouched()
{
int releaseCalls = 0;
var resources = new ResourceCleanupGroup();
resources.Add("published", () => releaseCalls++);
resources.TransferAll();
resources.RetryCleanup();
Assert.True(resources.IsCleanupComplete);
Assert.Equal(0, releaseCalls);
Assert.Throws<InvalidOperationException>(() =>
resources.Add("late", () => { }));
}
[Fact]
public void FailedConstructionRollbackRetainsOnlyUnreleasedOwnership()
{
int releases = 0;
bool releaseFails = true;
var resources = new ResourceCleanupGroup();
resources.Add("retryable", () =>
{
releases++;
if (releaseFails)
throw new InvalidOperationException("release failed");
});
GlResourceConstructionException failure =
Assert.Throws<GlResourceConstructionException>(() =>
resources.RollbackConstructionAndThrow(
"construction failed",
new InvalidOperationException("original failure")));
Assert.False(failure.IsCleanupComplete);
Assert.Equal(1, releases);
releaseFails = false;
failure.RetryCleanup();
failure.RetryCleanup();
Assert.True(failure.IsCleanupComplete);
Assert.Equal(2, releases);
}
[Fact]
public void WorldRenderFoundationConstructorsPublishEveryOwnedPrefix()
{
string root = FindRepoRoot();
string graphicsDevice = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"Wb",
"OpenGLGraphicsDevice.cs"));
string shader = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"Wb",
"GLSLShader.cs"));
string uniformBuffer = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"Wb",
"ManagedGLUniformBuffer.cs"));
AssertAppearsInOrder(
graphicsDevice,
"var resources = new ResourceCleanupGroup();",
"InstanceVBO = CreateConstructionBuffer(resources",
"WrapSampler = CreateConstructionSampler(",
"ClampSampler = CreateConstructionSampler(",
"_sceneDataBuffer = new ManagedGLUniformBuffer(",
"resources.Add(",
"\"WB scene-data uniform buffer\"",
"InitializeSharedDebugResources(resources);",
"resources.TransferAll();",
"resources.RollbackConstructionAndThrow(");
AssertAppearsInOrder(
shader,
"ShaderProgramConstruction.Build(",
"resources.Add(\"WB shader program\"",
"configure shader {Name} SceneData binding",
"resources.TransferAll();",
"resources.RollbackConstructionAndThrow(");
AssertAppearsInOrder(
uniformBuffer,
"TrackedGlResource.CreateBuffer(",
"resources.Add(",
"TrackedGlResource.AllocateBufferStorage(",
"resources.TransferAll();",
"resources.RollbackConstructionAndThrow(");
Assert.Contains("internal void DisposeImmediately()", uniformBuffer,
StringComparison.Ordinal);
}
[Fact] [Fact]
public void TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork() public void TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork()
{ {