acdream/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
Erik 5b3d72a90c refactor(render): Campaign V slice V6i-2 commit 3 — the mesh pipeline stops naming a backend
Plan §5.5.10 recorded the blocker as a fact about types: "WbMeshAdapter owns an
OpenGLGraphicsDevice, so it is not constructible on Vulkan until slice V4t" —
which is the entire reason NullWbMeshAdapter exists. §5.5.12 item 6 then measured
how wide that dependency really is, and the answer is seven members out of a
760-line class: a GL context, the retirement queue, the shared instance VBO, and
two capability flags.

IMeshPipelineDevice is exactly that surface. OpenGLGraphicsDevice declares it and
every member already existed under a GL-specific name, so the shipping backend
executes not one changed statement — these are aliases, not behaviour.

Two casts moved, and they are what actually blocked construction:

- ObjectMeshManager downcast IGpuDevice to GlGpuDevice in its CONSTRUCTOR, so a
  Vulkan-composed pipeline threw before running a statement. V4t put it there
  because the class registered bindless handles itself; commit 2 moved that into
  the array, leaving the field a pass-through for the raw-GL renderers' handle
  table. The cast now lives on that one property and names the backend it was
  composed against instead of reporting a failed cast.
- The atlas array factory is selected by IWorldTextureArrayFactory.For, which is
  the one place the texture stack branches on a backend.

MeshPipelineDeviceSeamTests proves the decoupling rather than describing it: it
builds ObjectMeshManager against a device whose Gl is null, asserts it constructs,
asserts construction built no GL object, asserts the handle table refuses by name,
and asserts the factory picks the RHI arm. A reflection test pins the seam's
member set so a later slice cannot quietly widen it back out — the whole value
here is that it is narrow.

What this does NOT claim: the mesh pipeline does not RUN on Vulkan. Its upload
bodies are still raw GL — GlobalMeshBuffer, the VAO/IBO construction, the layer
transfers — and they now fail through one RequireGl() accessor that names the
slice that owns porting them, instead of failing at construction. WbMeshAdapter
still creates an OpenGLGraphicsDevice in its GL constructor, because there is no
second implementation to create yet. Those bodies are items 3–5 of §5.5.12's
remainder list, along with RetailPViewPassExecutor and the three world renderers'
submission arms.

§5.5.13 reports the whole of V6i-2 and the slice table gains its V6i row.

Gates: Release build; App tests 4,109 / 3 skips (the 4,086 baseline plus 23 across
the three commits); complete Release suite 9,172 / 5; strict GL offline pixel gate
vs 0ca802cd 1.60e-05 (9 px of 563,200 — the low end of the documented 9–31 px
control band, and fewer than a same-commit control has measured); GL connected
tools/run-repeat-connected-gate.ps1 -Runs 3 at 3/3 RENDERED on the desktop witness
and 3/3 on the client capture; one Vulkan composition-host run with
VK_LAYER_KHRONOS_validation proven inserted by the loader at zero errors, zero
warnings, no [shutdown] diagnostic, and a captured frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:09:48 +02:00

776 lines
33 KiB
C#

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
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
using Silk.NET.OpenGL.Extensions.ARB;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using PolygonMode = Silk.NET.OpenGL.PolygonMode;
using PrimitiveType = Silk.NET.OpenGL.PrimitiveType;
namespace AcDream.App.Rendering.Wb {
/// <summary>
/// OpenGL graphics device
/// </summary>
public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice, IMeshPipelineDevice {
private readonly ILogger _log;
private readonly DebugRenderSettings _renderSettings;
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue _resourceRetirement;
public GL GL { get; }
public DebugRenderSettings RenderSettings => _renderSettings;
private readonly ConcurrentQueue<Action<GL>> _glThreadQueue = new();
private readonly ConcurrentQueue<Action<GL>> _nextGlThreadQueue = new();
internal bool HasPendingGLWork =>
!_glThreadQueue.IsEmpty || !_nextGlThreadQueue.IsEmpty;
// Campaign V slice V6i-2: IMeshPipelineDevice. Every member below already
// existed under a GL-specific name; these are aliases, not behaviour, so
// the shipping backend executes exactly the statements it executed
// before. See the interface for what the mesh pipeline actually needs
// and what still has to move before it has a second implementation.
GL? IMeshPipelineDevice.Gl => GL;
AcDream.App.Rendering.IGpuResourceRetirementQueue IMeshPipelineDevice.ResourceRetirement =>
_resourceRetirement;
bool IMeshPipelineDevice.HasPendingWork => HasPendingGLWork;
void IMeshPipelineDevice.ProcessQueue() => ProcessGLQueue();
public void QueueGLAction(Action<GL> action) {
_glThreadQueue.Enqueue(action);
}
internal void QueueGLActionForNextPass(Action<GL> action) {
ArgumentNullException.ThrowIfNull(action);
_nextGlThreadQueue.Enqueue(action);
}
public void ProcessGLQueue() {
// Retry the prior pass before ordinary work (notably sampler
// deletion), but process only the captured generation so a
// persistent driver failure cannot spin this frame forever.
int retryCount = _nextGlThreadQueue.Count;
for (int i = 0; i < retryCount && _nextGlThreadQueue.TryDequeue(out Action<GL>? retry); i++) {
try {
retry(GL);
} catch (Exception ex) {
_log.LogError(ex, "Error processing retryable GL queue action");
}
}
// Normal actions retain drain-to-empty semantics because teardown
// actions intentionally enqueue dependent atlas releases here.
// A persistent retryable error must not starve unrelated uploads
// and releases forever: the retry generation remains bounded to
// one attempt per pass, while ordinary work still makes progress.
while (_glThreadQueue.TryDequeue(out var action)) {
try {
action(GL);
} catch (Exception ex) {
_log.LogError(ex, "Error processing GL queue action");
}
}
}
public bool HasBindless { get; private set; }
public bool HasOpenGL43 { get; private set; }
public bool HasBufferStorage { get; private set; }
public bool HasTextureStorage { get; private set; }
public ArbBindlessTexture? BindlessExtension { get; private set; }
public uint InstanceVBO { get; private set; }
public void* InstanceVBOPtr { get; private set; }
public uint SharedQuadVBO { get; private set; }
public uint SharedDebugVAO { get; private set; }
public uint SharedDebugInstanceVBO { get; private set; }
/// <summary>OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs).</summary>
public uint WrapSampler { get; private set; }
/// <summary>OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs).</summary>
public uint ClampSampler { get; private set; }
internal float MaxSupportedAnisotropy { get; private set; }
private ManagedGLUniformBuffer? _sceneDataBuffer;
/// <summary>Shared SceneData UBO.</summary>
public ManagedGLUniformBuffer SceneDataBuffer => _sceneDataBuffer!;
private SceneData _currentSceneData;
public SceneData CurrentSceneData => _currentSceneData;
public void SetSceneData(ref SceneData data) {
_currentSceneData = data;
SceneDataBuffer.SetData(ref data);
}
private int _instanceBufferCapacity = 0;
private int _instanceBufferStride = 0;
/// <inheritdoc />
public override IntPtr NativeDevice { get; }
protected OpenGLGraphicsDevice() : base() {
_log = null!;
_renderSettings = null!;
_resourceRetirement = null!;
GL = null!;
}
public OpenGLGraphicsDevice(GL gl, ILogger log, DebugRenderSettings renderSettings, bool allowBindless = true)
: this(gl, log, renderSettings, AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance, allowBindless) {
}
internal OpenGLGraphicsDevice(
GL gl,
ILogger log,
DebugRenderSettings renderSettings,
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
bool allowBindless = true) : base() {
_log = log;
_renderSettings = renderSettings;
_resourceRetirement = resourceRetirement ?? throw new ArgumentNullException(nameof(resourceRetirement));
GL = gl;
GLHelpers.Init(this, log);
try {
GL.GetInteger(GLEnum.MajorVersion, out int major);
GL.GetInteger(GLEnum.MinorVersion, out int minor);
HasOpenGL43 = major > 4 || (major == 4 && minor >= 3);
HasTextureStorage = major > 4 || (major == 4 && minor >= 2) || GL.IsExtensionPresent("GL_ARB_texture_storage");
HasBufferStorage = major > 4 || (major == 4 && minor >= 4) || GL.IsExtensionPresent("GL_ARB_buffer_storage");
if (allowBindless && GL.TryGetExtension(out ArbBindlessTexture ext)) {
BindlessExtension = ext;
HasBindless = true;
} else {
HasBindless = false;
}
} catch {
HasOpenGL43 = false;
HasBindless = false;
}
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) {
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);
}
}
/// <summary>
/// Retires a GL resource only after every submitted draw that could
/// reference it has completed on the GPU.
/// </summary>
internal void RetireGpuResource(Action release) => _resourceRetirement.Retire(release);
internal AcDream.App.Rendering.IGpuResourceRetirementQueue ResourceRetirement =>
_resourceRetirement;
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,
1.0f, -0.5f,
1.0f, 0.5f,
0.0f, -0.5f,
1.0f, 0.5f,
0.0f, 0.5f
};
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}"));
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);
}
// 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)
GL.BindVertexArray(SharedDebugVAO);
// 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);
// Instance attributes
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
uint lineInstanceSize = 44;
// aStart (location 1)
GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(1, 3, GLEnum.Float, false, lineInstanceSize, (void*)0);
GL.VertexAttribDivisor(1, 1);
// aEnd (location 2)
GL.EnableVertexAttribArray(2);
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12);
GL.VertexAttribDivisor(2, 1);
// aColor (location 3)
GL.EnableVertexAttribArray(3);
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24);
GL.VertexAttribDivisor(3, 1);
// 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) {
if (count <= _instanceBufferCapacity && !forceOrphan) return;
if (_instanceBufferCapacity > 0) {
GpuMemoryTracker.TrackDeallocation(_instanceBufferCapacity * _instanceBufferStride);
}
_instanceBufferCapacity = Math.Max(count, 256);
_instanceBufferStride = stride;
if (HasBufferStorage) {
if (InstanceVBO != 0) {
GL.DeleteBuffer(InstanceVBO);
}
GL.GenBuffers(1, out uint instanceVbo);
InstanceVBO = instanceVbo;
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
GL.BufferStorage(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride), (void*)0, flags);
InstanceVBOPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)(_instanceBufferCapacity * _instanceBufferStride), MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride),
(void*)null, GLEnum.DynamicDraw);
InstanceVBOPtr = null;
}
GpuMemoryTracker.TrackAllocation(_instanceBufferCapacity * _instanceBufferStride);
}
public void UpdateInstanceBuffer<T>(List<T> data) where T : unmanaged {
EnsureInstanceBufferCapacity(data.Count, Marshal.SizeOf<T>(), true);
var span = CollectionsMarshal.AsSpan(data);
if (InstanceVBOPtr != null) {
var destSpan = new Span<T>(InstanceVBOPtr, data.Count);
span.CopyTo(destSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
fixed (T* ptr = span) {
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Count * Marshal.SizeOf<T>()), ptr);
}
}
}
public void UpdateInstanceBuffer<T>(Span<T> data) where T : unmanaged {
EnsureInstanceBufferCapacity(data.Length, Marshal.SizeOf<T>(), true);
if (InstanceVBOPtr != null) {
var destSpan = new Span<T>(InstanceVBOPtr, data.Length);
data.CopyTo(destSpan);
} else {
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
fixed (T* ptr = data) {
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Length * Marshal.SizeOf<T>()), ptr);
}
}
}
/// <inheritdoc />
public override void Clear(ColorVec color, ClearFlags flags, float depth, int stencil) {
GL.ClearColor(color.R, color.G, color.B, color.A);
GLHelpers.CheckErrors(GL);
GL.Clear((uint)Convert(flags));
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public override IIndexBuffer CreateIndexBuffer(int size,
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
return new ManagedGLIndexBuffer(this, usage, size);
}
/// <inheritdoc />
public override IVertexBuffer CreateVertexBuffer(int size,
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
return new ManagedGLVertexBuffer(this, usage, size);
}
/// <inheritdoc />
public override IVertexArray CreateArrayBuffer(IVertexBuffer vertexBuffer, VertexFormat format) {
return new ManagedGLVertexArray(this, vertexBuffer, format);
}
/// <inheritdoc />
public override void DrawElements(Chorizite.Core.Render.Enums.PrimitiveType type, int numElements, int indiceOffset = 0) {
GL.DrawElements(Convert(type), (uint)numElements, GLEnum.UnsignedInt, (void*)(indiceOffset * sizeof(uint)));
GLHelpers.CheckErrors(GL);
}
public override IShader CreateShader(string name, string vertexCode, string fragmentCode) {
var key = $"{GL.GetHashCode()}_{name}_{vertexCode.GetHashCode()}_{fragmentCode.GetHashCode()}";
while (true) {
if (_shaderCache.TryGetValue(key, out var existing)) {
if (existing is SharedShader shared && shared.TryIncrement()) {
return existing;
}
}
var inner = new GLSLShader(this, name, vertexCode, fragmentCode, _log);
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
if (_shaderCache.TryAdd(key, newShader)) {
return newShader;
}
// Someone else added it first, dispose ours and try again
newShader.DisposeInternal();
}
}
/// <inheritdoc />
public override IShader CreateShader(string name, string shaderDirectory) {
var key = $"{GL.GetHashCode()}_{name}";
while (true) {
if (_shaderCache.TryGetValue(key, out var existing)) {
if (existing is SharedShader shared && shared.TryIncrement()) {
return existing;
}
}
var inner = new GLSLShader(this, name, shaderDirectory, _log);
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
if (_shaderCache.TryAdd(key, newShader)) {
return newShader;
}
// Someone else added it first, dispose ours and try again
newShader.DisposeInternal();
}
}
private static readonly ConcurrentDictionary<string, IShader> _shaderCache = new();
private class SharedShader : IShader, IDisposable {
private readonly IShader _shader;
private readonly Action _onDispose;
private int _refCount = 1;
public string Name => _shader.Name;
public uint ProgramId => _shader.ProgramId;
public SharedShader(IShader shader, Action onDispose) {
_shader = shader;
_onDispose = onDispose;
}
public bool TryIncrement() {
while (true) {
int current = _refCount;
if (current <= 0) return false;
if (Interlocked.CompareExchange(ref _refCount, current + 1, current) == current) {
return true;
}
}
}
public void Bind() => _shader.Bind();
public void Unbind() => _shader.Unbind();
public void Load(string vertexSource, string fragmentSource) => _shader.Load(vertexSource, fragmentSource);
public void SetUniform(string name, int value) => _shader.SetUniform(name, value);
public void SetUniform(string name, float value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Vector2 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Vector3 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Vector4 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, Matrix4x4 value) => _shader.SetUniform(name, value);
public void SetUniform(string name, float[] values) => _shader.SetUniform(name, values);
public void DisposeInternal() {
_refCount = 0;
(_shader as IDisposable)?.Dispose();
}
public void Dispose() {
if (Interlocked.Decrement(ref _refCount) == 0) {
(_shader as IDisposable)?.Dispose();
_onDispose();
}
}
}
/// <inheritdoc />
public override ITexture
CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data = null) {
if (format != TextureFormat.RGBA8) {
throw new NotImplementedException($"Texture format {format} is not supported.");
}
return new ManagedGLTexture(this, data, width, height);
}
/// <summary>
/// Creates a texture with custom texture parameters.
/// </summary>
public ITexture CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data, TextureParameters texParams) {
if (format != TextureFormat.RGBA8) {
throw new NotImplementedException($"Texture format {format} is not supported.");
}
return new ManagedGLTexture(this, data, width, height, texParams);
}
/// <inheritdoc />
public override ITexture? CreateTextureInternal(TextureFormat format, string filename) {
if (format != TextureFormat.RGBA8) {
throw new NotImplementedException($"Texture format {format} is not supported.");
}
return new ManagedGLTexture(this, filename);
}
/// <inheritdoc />
public override ITextureArray
CreateTextureArrayInternal(TextureFormat format, int width, int height, int size) {
return new ManagedGLTextureArray(this, format, width, height, size, _log);
}
/// <summary>
/// Creates a texture array with custom texture parameters.
/// </summary>
public ITextureArray CreateTextureArrayInternal(TextureFormat format, int width, int height, int size, TextureParameters texParams) {
return new ManagedGLTextureArray(this, format, width, height, size, _log, texParams);
}
/// <inheritdoc />
public override void BeginFrame() {
GL.Viewport(Viewport.X, Viewport.Y, (uint)Viewport.Width, (uint)Viewport.Height);
GLHelpers.CheckErrors(GL);
GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
GLHelpers.CheckErrors(GL);
}
/// <inheritdoc />
public override void EndFrame() {
}
/// <inheritdoc />
protected override void SetRenderStateInternal(RenderState state, bool enabled) {
switch (state) {
case RenderState.AlphaBlend:
if (enabled) GL.Enable(EnableCap.Blend);
else GL.Disable(EnableCap.Blend);
GLHelpers.CheckErrors(GL);
break;
case RenderState.DepthTest:
if (enabled) GL.Enable(EnableCap.DepthTest);
else GL.Disable(EnableCap.DepthTest);
GLHelpers.CheckErrors(GL);
break;
case RenderState.ScissorTest:
if (enabled) GL.Enable(EnableCap.ScissorTest);
else GL.Disable(EnableCap.ScissorTest);
GLHelpers.CheckErrors(GL);
break;
case RenderState.DepthWrite:
if (enabled) GL.DepthMask(true);
else GL.DepthMask(false);
GLHelpers.CheckErrors(GL);
break;
case RenderState.Fog:
break;
case RenderState.Lighting:
break;
}
}
/// <inheritdoc />
protected override void SetBlendFactorInternal(BlendFactor srcBlendFactor, BlendFactor dstBlendFactor) {
GL.BlendFunc(Convert(srcBlendFactor), Convert(dstBlendFactor));
GLHelpers.CheckErrors(GL);
}
protected override void SetScissorRectInternal(Rectangle scissor) {
var gtop = (int)Viewport.Height - scissor.Y - scissor.Height;
GL.Scissor(scissor.X, gtop, (uint)scissor.Width, (uint)scissor.Height);
GLHelpers.CheckErrors(GL);
}
protected override void SetViewportInternal(Rectangle viewport) {
GL.Viewport(viewport.X, viewport.Y, (uint)viewport.Width, (uint)viewport.Height);
GLHelpers.CheckErrors(GL);
}
protected override void SetPolygonModeInternal(Chorizite.Core.Render.Enums.PolygonMode polygonMode) {
GL.PolygonMode(GLEnum.FrontAndBack, Convert(polygonMode));
GLHelpers.CheckErrors(GL);
}
protected override void SetCullModeInternal(CullMode cullMode) {
switch (cullMode) {
case CullMode.None:
GL.Disable(EnableCap.CullFace);
break;
case CullMode.Front:
GL.Enable(EnableCap.CullFace);
GL.CullFace(GLEnum.Front);
break;
case CullMode.Back:
GL.Enable(EnableCap.CullFace);
GL.CullFace(GLEnum.Back);
break;
}
}
private GLEnum Convert(Chorizite.Core.Render.Enums.PolygonMode mode) {
switch (mode) {
case Chorizite.Core.Render.Enums.PolygonMode.Fill:
return GLEnum.Fill;
case Chorizite.Core.Render.Enums.PolygonMode.Line:
return GLEnum.Line;
case Chorizite.Core.Render.Enums.PolygonMode.Point:
return GLEnum.Point;
default:
return GLEnum.Fill;
}
}
private GLEnum Convert(ClearFlags flags) {
GLEnum mask = 0;
if ((flags & ClearFlags.Color) == ClearFlags.Color) mask |= GLEnum.ColorBufferBit;
if ((flags & ClearFlags.Depth) == ClearFlags.Depth) mask |= GLEnum.DepthBufferBit;
if ((flags & ClearFlags.Stencil) == ClearFlags.Stencil) mask |= GLEnum.StencilBufferBit;
return mask;
}
private GLEnum Convert(BlendFactor factor) {
switch (factor) {
case BlendFactor.One:
return GLEnum.One;
case BlendFactor.SrcAlpha:
return GLEnum.SrcAlpha;
case BlendFactor.OneMinusSrcAlpha:
return GLEnum.OneMinusSrcAlpha;
case BlendFactor.DstAlpha:
return GLEnum.DstAlpha;
case BlendFactor.OneMinusDstAlpha:
return GLEnum.OneMinusDstAlpha;
default:
return GLEnum.One;
}
}
private PrimitiveType Convert(Chorizite.Core.Render.Enums.PrimitiveType type) {
switch (type) {
case Chorizite.Core.Render.Enums.PrimitiveType.PointList:
return PrimitiveType.Points;
case Chorizite.Core.Render.Enums.PrimitiveType.LineList:
return PrimitiveType.Lines;
case Chorizite.Core.Render.Enums.PrimitiveType.LineStrip:
return PrimitiveType.LineStrip;
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleList:
return PrimitiveType.Triangles;
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleStrip:
return PrimitiveType.TriangleStrip;
default:
throw new NotImplementedException($"Primitive type {type} is not supported.");
}
}
/// <inheritdoc />
public override IFramebuffer CreateFramebuffer(ITexture texture, int width, int height,
bool hasDepthStencil = true) {
if (texture == null) {
throw new ArgumentNullException(nameof(texture));
}
if (width <= 0 || height <= 0) {
throw new ArgumentException("Width and height must be positive.");
}
return new ManagedGLFramebuffer(this, texture, width, height, hasDepthStencil);
}
/// <inheritdoc />
public override void BindFramebuffer(IFramebuffer? framebuffer) {
uint fboId = framebuffer != null ? (uint)framebuffer.NativeHandle.ToInt32() : 0;
GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId);
}
/// <inheritdoc />
public override void Dispose() {
var instanceVBO = InstanceVBO;
var instanceBufferCapacity = _instanceBufferCapacity;
var instanceBufferStride = _instanceBufferStride;
var wrapSampler = WrapSampler;
var clampSampler = ClampSampler;
var sharedQuadVbo = SharedQuadVBO;
var sharedDebugInstanceVbo = SharedDebugInstanceVBO;
var sharedDebugVao = SharedDebugVAO;
QueueGLAction(gl => {
if (sharedQuadVbo != 0) gl.DeleteBuffer(sharedQuadVbo);
if (sharedDebugInstanceVbo != 0) gl.DeleteBuffer(sharedDebugInstanceVbo);
if (sharedDebugVao != 0) gl.DeleteVertexArray(sharedDebugVao);
if (instanceVBO != 0) {
gl.DeleteBuffer(instanceVBO);
if (instanceBufferCapacity > 0) {
GpuMemoryTracker.TrackDeallocation(instanceBufferCapacity * instanceBufferStride);
}
}
});
// Bindless texture-array retirements embed these samplers in their
// resident handles. Ordinary GL work must keep flowing when one
// retry is sick, but sampler deletion itself is dependency-ordered
// behind the retry queue. Requeue into the next generation (never
// the drain-to-empty ordinary queue) to remain one attempt/frame.
Action<GL>? deleteSamplersWhenSafe = null;
deleteSamplersWhenSafe = gl => {
if (!_nextGlThreadQueue.IsEmpty) {
QueueGLActionForNextPass(deleteSamplersWhenSafe!);
return;
}
if (wrapSampler != 0)
gl.DeleteSampler(wrapSampler);
if (clampSampler != 0)
gl.DeleteSampler(clampSampler);
};
QueueGLActionForNextPass(deleteSamplersWhenSafe);
InstanceVBO = 0;
InstanceVBOPtr = null;
WrapSampler = 0;
ClampSampler = 0;
_sceneDataBuffer?.Dispose();
_sceneDataBuffer = null;
}
public override IUniformBuffer CreateUniformBuffer(BufferUsage usage, int size) {
return (IUniformBuffer)new ManagedGLUniformBuffer(this, usage, size);
}
}
}