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

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

View file

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

View file

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

View file

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

View file

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

View file

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