ParticleBatcher/ParticleEmitterRenderer/ActiveParticleEmitter (src/AcDream.App/Rendering/Wb/) are an earlier WorldBuilder-derived particle-preview design with zero live call sites: ParticleBatcher.Begin/AddParticle/Flush/End are only called from ParticleEmitterRenderer.Render, which is only called from ActiveParticleEmitter.Render, and `new ActiveParticleEmitter` has zero call sites anywhere in the repo (verified by grep across src/tests/tools). The only wiring was OpenGLGraphicsDevice.ParticleBatcher (property + null-init + Dispose) and a single assignment in WbMeshAdapter.cs. ParticleBatcher's constructor unconditionally allocated a ParticleInstance[65536] managed array (~3.5 MiB) plus a matching GPU instance buffer, a shader, a VAO, and 3 more GL buffers, none tracked by GpuMemoryTracker or ever drawn from. ParticleEmitterRenderer's per-particle-type physics (Particle::Init vector-space resolution, CalculatePosition integration) duplicates, without retail address citations, what src/AcDream.Core/Vfx/ParticleSystem.cs already implements with Particle::Init (0x0051c930) / Particle::Update (0x0051c290) citations and the same ParticleType switch — the production path (ParticleSystem.cs + AcDream.App/Rendering/ParticleRenderer.cs) supersedes it. No unique retail knowledge is lost. EmbeddedResourceReader.cs and the wb_particle.vert/frag shader sources are deleted alongside it: EmbeddedResourceReader's own doc comment says it exists solely so ParticleBatcher/ParticleEmitterRenderer can load WB-style shader resource names, and GetEmbeddedResource had no other caller in the repo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 4afedafd085a5fadd1867418d3ec71610d9d15a7)
762 lines
32 KiB
C#
762 lines
32 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 {
|
|
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;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|