feat(O-T3): extract GL infrastructure to AcDream.App
Phase O Task 3 — verbatim-copy GL infra from Chorizite.OpenGLSDLBackend
into src/AcDream.App/Rendering/Wb/ (namespace AcDream.App.Rendering.Wb).
18 files extracted (all namespace-changed; no algorithm changes):
OpenGLGraphicsDevice, ManagedGLTexture, ManagedGLTextureArray,
ManagedGLVertexBuffer, ManagedGLIndexBuffer, ManagedGLVertexArray,
ManagedGLFrameBuffer, ManagedGLUniformBuffer, GLSLShader, GLHelpers,
GLStateScope, GpuMemoryTracker, SceneData, DebugRenderSettings,
TextureParameters, TextureFormatExtensions, BufferUsageExtensions,
EmbeddedResourceReader.
3 internals promoted to public (O-D9):
EmbeddedResourceReader, TextureFormatExtensions, BufferUsageExtensions.
SixLabors.ImageSharp not reachable: TextureHelpers was placed in
AcDream.Core (no GL/ImageSharp dep); only the GL types went to App.
TextureHelpers.GetCompressedLayerSize added to AcDream.Core.Rendering.Wb
(was in Chorizite.OpenGLSDLBackend.Lib.TextureHelpers; uses
Chorizite.Core.Render.Enums.TextureFormat which Core gets transitively
via the still-present WB project refs).
T3/T4 boundary interims:
- WbMeshAdapter._graphicsDevice stays Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice
(T4 will swap it when ObjectMeshManager is extracted).
- OpenGLGraphicsDevice.ParticleBatcher deferred to null! (T4 extracts
ParticleBatcher alongside ObjectMeshManager; can't pass `this` of our
new type to the WB-original ctor before T4).
- ManagedGLTextureArray uses our TextureHelpers via explicit alias.
- IUniformBuffer is in Chorizite.Core.dll under Chorizite.OpenGLSDLBackend
namespace (unusual packaging); resolved via type alias.
- AcDream.App.csproj gets explicit Chorizite.Core 0.0.18 PackageReference
(IUniformBuffer + other Chorizite.Core types now used directly in App).
Build green. Test baseline 1147+8 maintained (1902 passing, 8 pre-existing
MotionInterpreterTests failures unrelated to T3).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
16bc10c99d
commit
4cc38805b5
21 changed files with 3018 additions and 4 deletions
|
|
@ -14,6 +14,10 @@
|
|||
<InternalsVisibleTo Include="AcDream.App.Tests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- T3: extracted GL infra (ManagedGLUniformBuffer, OpenGLGraphicsDevice) directly
|
||||
implement IUniformBuffer from Chorizite.Core; NuGet PackageReferences are not
|
||||
forwarded from ProjectReferences so we must declare it explicitly here. -->
|
||||
<PackageReference Include="Chorizite.Core" Version="0.0.18" />
|
||||
<PackageReference Include="Silk.NET.OpenGL" Version="2.23.0" />
|
||||
<PackageReference Include="Silk.NET.OpenGL.Extensions.ARB" Version="2.23.0" />
|
||||
<PackageReference Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
|
|
|
|||
27
src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs
Normal file
27
src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public static class BufferUsageExtensions {
|
||||
/// <summary>
|
||||
/// Converts a BufferUsage to a GL BufferUsageARB
|
||||
/// </summary>
|
||||
/// <param name="usage"></param>
|
||||
/// <returns></returns>
|
||||
public static GLEnum ToGL(this BufferUsage usage) {
|
||||
switch (usage) {
|
||||
case BufferUsage.Static:
|
||||
return GLEnum.StaticDraw;
|
||||
case BufferUsage.Dynamic:
|
||||
return GLEnum.DynamicDraw;
|
||||
default:
|
||||
return GLEnum.StaticDraw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs
Normal file
28
src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
// Extracted verbatim from WorldBuilder.Shared/Models/DebugRenderSettings.cs.
|
||||
// LandscapeColorsSettings dependency (editor-only, CommunityToolkit.Mvvm) stripped;
|
||||
// default color values inlined from LandscapeColorsSettings field initializers.
|
||||
public class DebugRenderSettings {
|
||||
public bool ShowBoundingBoxes { get; set; } = false;
|
||||
public bool SelectVertices { get; set; } = true;
|
||||
public bool SelectBuildings { get; set; } = true;
|
||||
public bool SelectStaticObjects { get; set; } = true;
|
||||
public bool SelectScenery { get; set; } = false;
|
||||
public bool SelectEnvCells { get; set; } = true;
|
||||
public bool SelectEnvCellStaticObjects { get; set; } = true;
|
||||
public bool SelectPortals { get; set; } = true;
|
||||
public bool ShowDisqualifiedScenery { get; set; } = true;
|
||||
public bool EnableAnisotropicFiltering { get; set; } = true;
|
||||
|
||||
// Default colors inlined from LandscapeColorsSettings field initializers
|
||||
public Vector4 VertexColor { get; set; } = new Vector4(0.7882353f, 0.34901962f, 0.2901961f, 1.0f);
|
||||
public Vector4 BuildingColor { get; set; } = new Vector4(0.76862746f, 0.5803922f, 0.25882354f, 1.0f);
|
||||
public Vector4 StaticObjectColor { get; set; } = new Vector4(0.37254903f, 0.88235295f, 0.9019608f, 1.0f);
|
||||
public Vector4 SceneryColor { get; set; } = new Vector4(0.45490196f, 0.72156864f, 0.32156864f, 1.0f);
|
||||
public Vector4 EnvCellColor { get; set; } = new Vector4(0.5294118f, 0.44705883f, 0.7882353f, 1.0f);
|
||||
public Vector4 EnvCellStaticObjectColor { get; set; } = new Vector4(0f, 0.49803922f, 1f, 1.0f);
|
||||
public Vector4 PortalColor { get; set; } = new Vector4(1f, 0f, 1f, 1.0f);
|
||||
}
|
||||
}
|
||||
22
src/AcDream.App/Rendering/Wb/EmbeddedResourceReader.cs
Normal file
22
src/AcDream.App/Rendering/Wb/EmbeddedResourceReader.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
|
||||
public static class EmbeddedResourceReader {
|
||||
internal static string GetEmbeddedResource(string filename) {
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = "Chorizite.OpenGLSDLBackend." + filename;
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException($"Could not find embedded resource '{resourceName}'");
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
254
src/AcDream.App/Rendering/Wb/GLHelpers.cs
Normal file
254
src/AcDream.App/Rendering/Wb/GLHelpers.cs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.Core.Native;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public static class GLHelpers {
|
||||
public static OpenGLGraphicsDevice? Device { get; set; }
|
||||
public static ILogger? Logger { get; set; }
|
||||
|
||||
public static void Init(OpenGLGraphicsDevice device, ILogger logger) {
|
||||
Logger = logger;
|
||||
Device = device;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static bool _loggedVersion = false;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CheckErrors(GL gl, bool logErrors = false, [CallerMemberName] string callerName = "",
|
||||
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
|
||||
var error = gl.GetError();
|
||||
if (error != GLEnum.NoError) {
|
||||
if (!_loggedVersion) {
|
||||
_loggedVersion = true;
|
||||
var version = gl.GetStringS(GLEnum.Version);
|
||||
var vendor = gl.GetStringS(GLEnum.Vendor);
|
||||
var renderer = gl.GetStringS(GLEnum.Renderer);
|
||||
Logger?.LogInformation($"GL Version: {version}, Vendor: {vendor}, Renderer: {renderer}");
|
||||
}
|
||||
string errorDetails = GetErrorDetails(error);
|
||||
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
|
||||
|
||||
var program = (uint)gl.GetInteger(GLEnum.CurrentProgram);
|
||||
var vao = gl.GetInteger(GLEnum.VertexArrayBinding);
|
||||
var activeTex = gl.GetInteger(GLEnum.ActiveTexture);
|
||||
var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
string extraInfo = "";
|
||||
if (program != 0) {
|
||||
bool isProgram = gl.IsProgram(program);
|
||||
gl.GetProgram(program, GLEnum.LinkStatus, out int linkStatus);
|
||||
gl.GetProgram(program, GLEnum.DeleteStatus, out int deleteStatus);
|
||||
gl.GetProgram(program, GLEnum.ValidateStatus, out int validateStatus);
|
||||
extraInfo = $", IsProg: {isProgram}, Link: {linkStatus}, Del: {deleteStatus}, Valid: {validateStatus}";
|
||||
}
|
||||
|
||||
string message = $"OpenGL Error: {error} ({errorDetails}) at {location}. Thread: {threadId}, Program: {program}{extraInfo}, VAO: {vao}, ActiveTex: {activeTex}";
|
||||
|
||||
Logger?.LogError(message);
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
#else
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CheckErrors(GL gl, bool logErrors = false, string callerName = "",
|
||||
string callerFile = "", int callerLine = 0) {
|
||||
}
|
||||
#endif
|
||||
|
||||
public static string GetErrorDetails(GLEnum error) {
|
||||
return error switch {
|
||||
GLEnum.InvalidEnum => "Invalid enum - An unacceptable value is specified for an enumerated argument",
|
||||
GLEnum.InvalidValue => "Invalid value - A numeric argument is out of range",
|
||||
GLEnum.InvalidOperation =>
|
||||
"Invalid operation - The specified operation is not allowed in the current state",
|
||||
GLEnum.StackOverflow => "Stack overflow - An operation would cause an internal stack to overflow",
|
||||
GLEnum.StackUnderflow => "Stack underflow - An operation would cause an internal stack to underflow",
|
||||
GLEnum.OutOfMemory => "Out of memory - There is not enough memory left to execute the command",
|
||||
GLEnum.InvalidFramebufferOperation =>
|
||||
"Invalid framebuffer operation - The framebuffer object is not complete",
|
||||
GLEnum.ContextLost => "Context lost - The OpenGL context has been lost due to a graphics card reset",
|
||||
_ => "Unknown error"
|
||||
};
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// Checks for OpenGL errors and provides context-specific information
|
||||
/// </summary>
|
||||
public static void CheckErrorsWithContext(GL gl, string context, [CallerMemberName] string callerName = "",
|
||||
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
|
||||
var error = gl.GetError();
|
||||
if (error != GLEnum.NoError) {
|
||||
string errorDetails = GetErrorDetails(error);
|
||||
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
|
||||
string message = $"OpenGL Error: {error} ({errorDetails})\nContext: {context}\nLocation: {location}";
|
||||
|
||||
Logger?.LogError(message);
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
#else
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CheckErrorsWithContext(GL gl, string context, string callerName = "",
|
||||
string callerFile = "", int callerLine = 0) {
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets detailed information about the current texture state for debugging
|
||||
/// </summary>
|
||||
public static string GetTextureDebugInfo(GL gl, GLEnum target) {
|
||||
var info = new System.Text.StringBuilder();
|
||||
info.AppendLine($"Texture Debug Info for {target}:");
|
||||
|
||||
try {
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureWidth, out int width);
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureHeight, out int height);
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureDepthExt, out int depth);
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureInternalFormat, out int format);
|
||||
|
||||
info.AppendLine($" Dimensions: {width}x{height}x{depth}");
|
||||
info.AppendLine($" Internal Format: {(InternalFormat)format}");
|
||||
|
||||
gl.GetTexParameter(target, GetTextureParameter.TextureMinFilter, out int minFilter);
|
||||
gl.GetTexParameter(target, GetTextureParameter.TextureMagFilter, out int magFilter);
|
||||
info.AppendLine($" Min Filter: {(TextureMinFilter)minFilter}");
|
||||
info.AppendLine($" Mag Filter: {(TextureMagFilter)magFilter}");
|
||||
|
||||
// Get max mipmap level
|
||||
gl.GetTexParameter(target, GetTextureParameter.TextureMaxLevelSgis, out int maxLevel);
|
||||
info.AppendLine($" Max Level: {maxLevel}");
|
||||
|
||||
// Check completeness
|
||||
int maxMipLevel = (int)Math.Floor(Math.Log2(Math.Max(width, height)));
|
||||
info.AppendLine($" Calculated Max Mip Level: {maxMipLevel}");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
info.AppendLine($" Error getting texture info: {ex.Message}");
|
||||
}
|
||||
|
||||
return info.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates texture completeness for mipmapping
|
||||
/// </summary>
|
||||
public static bool ValidateTextureMipmapStatus(GL gl, GLEnum target, out string errorMessage) {
|
||||
try {
|
||||
gl.GetTexLevelParameter(target, 0, GetTextureParameter.TextureWidth, out int width);
|
||||
gl.GetTexLevelParameter(target, 0, GetTextureParameter.TextureHeight, out int height);
|
||||
gl.GetTexLevelParameter(target, 0, GetTextureParameter.TextureInternalFormat, out int format);
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
errorMessage = "Texture has zero dimensions";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if format is valid for mipmap generation
|
||||
var internalFormat = (InternalFormat)format;
|
||||
if (IsCompressedFormat(internalFormat)) {
|
||||
errorMessage = $"Compressed format {internalFormat} does not support automatic mipmap generation";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = String.Empty;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
errorMessage = $"Exception during validation: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCompressedFormat(InternalFormat format) {
|
||||
return format == InternalFormat.CompressedRgbaS3TCDxt1Ext ||
|
||||
format == InternalFormat.CompressedRgbaS3TCDxt3Ext ||
|
||||
format == InternalFormat.CompressedRgbaS3TCDxt5Ext ||
|
||||
format == InternalFormat.CompressedRgbS3TCDxt1Ext ||
|
||||
format == InternalFormat.CompressedSrgbAlphaS3TCDxt1Ext ||
|
||||
format == InternalFormat.CompressedSrgbAlphaS3TCDxt3Ext ||
|
||||
format == InternalFormat.CompressedSrgbAlphaS3TCDxt5Ext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs current OpenGL state for debugging
|
||||
/// </summary>
|
||||
public static void LogGLState(GL gl, string context = "") {
|
||||
var state = new System.Text.StringBuilder();
|
||||
state.AppendLine($"=== OpenGL State ({context}) ===");
|
||||
|
||||
try {
|
||||
state.AppendLine(
|
||||
$"Active Texture Unit: GL_TEXTURE{gl.GetInteger(GetPName.ActiveTexture) - (int)GLEnum.Texture0}");
|
||||
state.AppendLine($"Bound 2D Array Texture: {gl.GetInteger(GetPName.TextureBinding2DArray)}");
|
||||
state.AppendLine($"Current Program: {gl.GetInteger(GetPName.CurrentProgram)}");
|
||||
|
||||
gl.GetInteger(GetPName.MaxTextureSize, out int maxTexSize);
|
||||
state.AppendLine($"Max Texture Size: {maxTexSize}");
|
||||
|
||||
gl.GetInteger(GetPName.Max3DTextureSize, out int max3DSize);
|
||||
state.AppendLine($"Max 3D Texture Size: {max3DSize}");
|
||||
|
||||
gl.GetInteger(GetPName.MaxArrayTextureLayers, out int maxLayers);
|
||||
state.AppendLine($"Max Array Texture Layers: {maxLayers}");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
state.AppendLine($"Error getting GL state: {ex.Message}");
|
||||
}
|
||||
|
||||
state.AppendLine("======================");
|
||||
Logger?.LogInformation(state.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicit defaults to prevent Avalonia state leakage into our custom rendering pipeline.
|
||||
/// Call this at the start of complex render cycles immediately inside a GLStateScope.
|
||||
/// </summary>
|
||||
public static void SetupDefaultRenderState(GL gl) {
|
||||
gl.BindSampler(0, 0);
|
||||
gl.BindSampler(1, 0);
|
||||
gl.BindSampler(2, 0);
|
||||
|
||||
gl.ActiveTexture(TextureUnit.Texture1);
|
||||
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
gl.ActiveTexture(TextureUnit.Texture2);
|
||||
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
gl.ActiveTexture(TextureUnit.Texture0); // End on Texture0
|
||||
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
|
||||
gl.BindVertexArray(0);
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
|
||||
gl.UseProgram(0);
|
||||
|
||||
gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
gl.PixelStore(PixelStoreParameter.UnpackRowLength, 0);
|
||||
gl.PixelStore(PixelStoreParameter.UnpackSkipRows, 0);
|
||||
gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, 0);
|
||||
|
||||
gl.Disable(EnableCap.StencilTest);
|
||||
gl.BlendColor(0, 0, 0, 0);
|
||||
gl.PolygonMode(GLEnum.FrontAndBack, PolygonMode.Fill);
|
||||
|
||||
// Disable Avalonia/Skia specific states
|
||||
gl.Disable(EnableCap.SampleAlphaToCoverage);
|
||||
gl.Disable(EnableCap.SampleAlphaToOne);
|
||||
gl.Disable(EnableCap.Multisample);
|
||||
gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
|
||||
gl.LineWidth(1.0f);
|
||||
gl.PolygonOffset(0f, 0f);
|
||||
gl.Disable(EnableCap.PolygonOffsetFill);
|
||||
gl.Disable((EnableCap)GLEnum.ProgramPointSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
271
src/AcDream.App/Rendering/Wb/GLSLShader.cs
Normal file
271
src/AcDream.App/Rendering/Wb/GLSLShader.cs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
|
||||
public unsafe class GLSLShader : BaseShader, IDisposable {
|
||||
private OpenGLGraphicsDevice _device;
|
||||
private Dictionary<string, int> _uniformLocations = [];
|
||||
private Dictionary<int, object> _uniformValues = [];
|
||||
private readonly object _lock = new();
|
||||
private GL GL => _device.GL;
|
||||
public uint Program { get; protected set; }
|
||||
|
||||
public bool HasUniform(string name) {
|
||||
lock (_lock) {
|
||||
return GetUniformLocation(Program, name) != -1;
|
||||
}
|
||||
}
|
||||
|
||||
public GLSLShader(OpenGLGraphicsDevice device, string name, string vertSource, string fragSource, ILogger log) : base(name, vertSource, fragSource, log) {
|
||||
_device = device;
|
||||
|
||||
Load(vertSource, fragSource);
|
||||
}
|
||||
|
||||
public GLSLShader(OpenGLGraphicsDevice device, string name, string shaderDirectory, ILogger log) : base(name, shaderDirectory, log) {
|
||||
_device = device;
|
||||
|
||||
Load();
|
||||
}
|
||||
|
||||
public override void Dispose() {
|
||||
Unload();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
private int GetUniformLocation(uint program, string name) {
|
||||
lock (_lock) {
|
||||
if (!_uniformLocations.ContainsKey(name)) {
|
||||
_uniformLocations.Add(name, GL.GetUniformLocation(program, name));
|
||||
}
|
||||
return _uniformLocations[name];
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Matrix4x4 m) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation(Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Matrix4x4 mCached && mCached == m) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = m;
|
||||
|
||||
GL.UniformMatrix4(loc, 1, false, (float*)&m);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, int v) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is int vCached && vCached == v) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = v;
|
||||
|
||||
GL.Uniform1(loc, v);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Vector2 vec) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector2 vCached && vCached == vec) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = vec;
|
||||
|
||||
GL.Uniform2(loc, vec);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Vector3 vec) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector3 vCached && vCached == vec) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = vec;
|
||||
|
||||
GL.Uniform3(loc, vec);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void SetUniform(string location, Vector3[] vecs) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
fixed (float* v = &vecs[0].X) {
|
||||
GL.Uniform3(loc, (uint)vecs.Length, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Vector4 vec) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector4 vCached && vCached == vec) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = vec;
|
||||
|
||||
GL.Uniform4(loc, vec);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, float v) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is float vCached && vCached == v) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = v;
|
||||
|
||||
GL.Uniform1(loc, v);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, float[] vs) {
|
||||
lock (_lock) {
|
||||
fixed (float* v = &vs[0]) {
|
||||
GL.Uniform1(GetUniformLocation((uint)Program, location), (uint)vs.Length, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Load(string vertShaderSource, string fragShaderSource) {
|
||||
|
||||
if (string.IsNullOrWhiteSpace(vertShaderSource) || string.IsNullOrWhiteSpace(fragShaderSource)) {
|
||||
_log.LogError($"Shader {Name} has no source code!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_device.HasOpenGL43 && _device.HasBindless) {
|
||||
string replacement = "#version 430 core\n#extension GL_ARB_bindless_texture : require";
|
||||
vertShaderSource = vertShaderSource.Replace("#version 330 core", replacement);
|
||||
fragShaderSource = fragShaderSource.Replace("#version 330 core", replacement);
|
||||
}
|
||||
|
||||
uint vertexShader = CompileShader(ShaderType.VertexShader, Name, vertShaderSource);
|
||||
uint fragmentShader = CompileShader(ShaderType.FragmentShader, Name, fragShaderSource);
|
||||
|
||||
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);
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if (Program != 0) {
|
||||
Unload();
|
||||
}
|
||||
_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();
|
||||
if (Program != 0) {
|
||||
GL.UseProgram((uint)Program);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unbind() {
|
||||
lock (_lock) {
|
||||
GL.UseProgram(0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Unload() {
|
||||
lock (_lock) {
|
||||
if (Program != 0) {
|
||||
var prog = Program;
|
||||
Program = 0;
|
||||
ProgramId = 0;
|
||||
_device.QueueGLAction(gl => {
|
||||
gl.DeleteProgram(prog);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Shader);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
230
src/AcDream.App/Rendering/Wb/GLStateScope.cs
Normal file
230
src/AcDream.App/Rendering/Wb/GLStateScope.cs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// A RAII scope for saving and restoring OpenGL state.
|
||||
/// </summary>
|
||||
public unsafe struct GLStateScope : IDisposable {
|
||||
private readonly GL _gl;
|
||||
private fixed int _viewport[4];
|
||||
private bool _scissorTest;
|
||||
private fixed int _scissorBox[4];
|
||||
private bool _depthTest;
|
||||
private int _depthFunc;
|
||||
private bool _depthMask;
|
||||
private bool _cullFace;
|
||||
private int _cullFaceMode;
|
||||
private int _frontFace;
|
||||
private bool _blend;
|
||||
private int _blendSrc;
|
||||
private int _blendDst;
|
||||
private int _blendEquation;
|
||||
|
||||
// Extended state
|
||||
private int _blendSrcAlpha;
|
||||
private int _blendDstAlpha;
|
||||
private int _blendEquationAlpha;
|
||||
private fixed byte _colorMask[4];
|
||||
private fixed float _clearColor[4];
|
||||
private float _clearDepth;
|
||||
private int _currentProgram;
|
||||
private int _vertexArrayBinding;
|
||||
private int _arrayBufferBinding;
|
||||
private int _elementArrayBufferBinding;
|
||||
private int _activeTexture;
|
||||
private int _textureBinding2D;
|
||||
private bool _stencilTest;
|
||||
private int _stencilFunc;
|
||||
private int _stencilRef;
|
||||
private int _stencilValueMask;
|
||||
private int _stencilFail;
|
||||
private int _stencilPassDepthFail;
|
||||
private int _stencilPassDepthPass;
|
||||
private int _stencilWritemask;
|
||||
private int _unpackAlignment;
|
||||
private int _packAlignment;
|
||||
|
||||
private int _drawFramebufferBinding;
|
||||
|
||||
// Skia / Avalonia extra state protections
|
||||
private fixed float _blendColor[4];
|
||||
private int _polygonMode;
|
||||
private bool _sampleAlphaToCoverage;
|
||||
private bool _multisample;
|
||||
private bool _primitiveRestart;
|
||||
private int _readFramebufferBinding;
|
||||
private int _uniformBufferBinding0;
|
||||
private float _lineWidth;
|
||||
private bool _programPointSize;
|
||||
private int _samplerBinding0;
|
||||
private int _samplerBinding1;
|
||||
private int _samplerBinding2;
|
||||
private int _unpackRowLength;
|
||||
private int _unpackSkipRows;
|
||||
private int _unpackSkipPixels;
|
||||
private bool _sampleAlphaToOne;
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Captures the current OpenGL state.
|
||||
/// </summary>
|
||||
/// <param name="gl"></param>
|
||||
public GLStateScope(GL gl) {
|
||||
_gl = gl;
|
||||
_isDisposed = false;
|
||||
|
||||
fixed (int* v = _viewport) _gl.GetInteger(GetPName.Viewport, v);
|
||||
_scissorTest = _gl.IsEnabled(EnableCap.ScissorTest);
|
||||
fixed (int* s = _scissorBox) _gl.GetInteger(GetPName.ScissorBox, s);
|
||||
|
||||
_depthTest = _gl.IsEnabled(EnableCap.DepthTest);
|
||||
_gl.GetInteger(GetPName.DepthFunc, out _depthFunc);
|
||||
byte depthMask = 0;
|
||||
_gl.GetBoolean((GetPName)GLEnum.DepthWritemask, (bool*)&depthMask);
|
||||
_depthMask = depthMask != 0;
|
||||
|
||||
_cullFace = _gl.IsEnabled(EnableCap.CullFace);
|
||||
_gl.GetInteger(GetPName.CullFaceMode, out _cullFaceMode);
|
||||
_gl.GetInteger(GetPName.FrontFace, out _frontFace);
|
||||
|
||||
_blend = _gl.IsEnabled(EnableCap.Blend);
|
||||
_gl.GetInteger(GetPName.BlendSrcRgb, out _blendSrc);
|
||||
_gl.GetInteger(GetPName.BlendDstRgb, out _blendDst);
|
||||
_gl.GetInteger(GetPName.BlendSrcAlpha, out _blendSrcAlpha);
|
||||
_gl.GetInteger(GetPName.BlendDstAlpha, out _blendDstAlpha);
|
||||
_gl.GetInteger(GetPName.BlendEquationRgb, out _blendEquation);
|
||||
_gl.GetInteger(GetPName.BlendEquationAlpha, out _blendEquationAlpha);
|
||||
|
||||
fixed (byte* c = _colorMask) _gl.GetBoolean((GetPName)GLEnum.ColorWritemask, (bool*)c);
|
||||
fixed (float* cc = _clearColor) _gl.GetFloat(GetPName.ColorClearValue, cc);
|
||||
_gl.GetFloat(GetPName.DepthClearValue, out _clearDepth);
|
||||
|
||||
_gl.GetInteger(GetPName.CurrentProgram, out _currentProgram);
|
||||
_gl.GetInteger(GetPName.VertexArrayBinding, out _vertexArrayBinding);
|
||||
_gl.GetInteger(GetPName.ArrayBufferBinding, out _arrayBufferBinding);
|
||||
_gl.GetInteger(GetPName.ElementArrayBufferBinding, out _elementArrayBufferBinding);
|
||||
|
||||
_gl.GetInteger(GetPName.ActiveTexture, out _activeTexture);
|
||||
_gl.GetInteger(GetPName.TextureBinding2D, out _textureBinding2D);
|
||||
|
||||
_stencilTest = _gl.IsEnabled(EnableCap.StencilTest);
|
||||
_gl.GetInteger(GetPName.StencilFunc, out _stencilFunc);
|
||||
_gl.GetInteger(GetPName.StencilRef, out _stencilRef);
|
||||
_gl.GetInteger(GetPName.StencilValueMask, out _stencilValueMask);
|
||||
_gl.GetInteger(GetPName.StencilFail, out _stencilFail);
|
||||
_gl.GetInteger(GetPName.StencilPassDepthFail, out _stencilPassDepthFail);
|
||||
_gl.GetInteger(GetPName.StencilPassDepthPass, out _stencilPassDepthPass);
|
||||
_gl.GetInteger(GetPName.StencilWritemask, out _stencilWritemask);
|
||||
|
||||
_gl.GetInteger(GetPName.UnpackAlignment, out _unpackAlignment);
|
||||
_gl.GetInteger(GetPName.PackAlignment, out _packAlignment);
|
||||
|
||||
_gl.GetInteger(GetPName.DrawFramebufferBinding, out _drawFramebufferBinding);
|
||||
|
||||
fixed (float* bc = _blendColor) _gl.GetFloat(GetPName.BlendColor, bc);
|
||||
_gl.GetInteger(GetPName.PolygonMode, out _polygonMode);
|
||||
_sampleAlphaToCoverage = _gl.IsEnabled(EnableCap.SampleAlphaToCoverage);
|
||||
_multisample = _gl.IsEnabled(EnableCap.Multisample);
|
||||
_primitiveRestart = _gl.IsEnabled((EnableCap)GLEnum.PrimitiveRestart);
|
||||
_gl.GetInteger(GetPName.ReadFramebufferBinding, out _readFramebufferBinding);
|
||||
|
||||
_gl.GetInteger(GetPName.UniformBufferBinding, out _uniformBufferBinding0);
|
||||
|
||||
_gl.GetFloat(GetPName.LineWidth, out _lineWidth);
|
||||
_programPointSize = _gl.IsEnabled((EnableCap)GLEnum.ProgramPointSize);
|
||||
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding0);
|
||||
_gl.ActiveTexture(TextureUnit.Texture1);
|
||||
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding1);
|
||||
_gl.ActiveTexture(TextureUnit.Texture2);
|
||||
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding2);
|
||||
_gl.ActiveTexture((TextureUnit)_activeTexture);
|
||||
|
||||
_gl.GetInteger((GetPName)GLEnum.UnpackRowLength, out _unpackRowLength);
|
||||
_gl.GetInteger((GetPName)GLEnum.UnpackSkipRows, out _unpackSkipRows);
|
||||
_gl.GetInteger((GetPName)GLEnum.UnpackSkipPixels, out _unpackSkipPixels);
|
||||
_sampleAlphaToOne = _gl.IsEnabled(EnableCap.SampleAlphaToOne);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores only the scissor state from the scope.
|
||||
/// </summary>
|
||||
public void RestoreScissor() {
|
||||
if (_scissorTest) _gl.Enable(EnableCap.ScissorTest);
|
||||
else _gl.Disable(EnableCap.ScissorTest);
|
||||
_gl.Scissor(_scissorBox[0], _scissorBox[1], (uint)_scissorBox[2], (uint)_scissorBox[3]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the captured OpenGL state.
|
||||
/// </summary>
|
||||
public void Dispose() {
|
||||
if (_isDisposed) return;
|
||||
|
||||
// Restoring state
|
||||
if (_currentProgram != 0) _gl.UseProgram((uint)_currentProgram); else _gl.UseProgram(0);
|
||||
|
||||
_gl.BindVertexArray((uint)_vertexArrayBinding);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBufferBinding);
|
||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, (uint)_elementArrayBufferBinding);
|
||||
_gl.BindBuffer(GLEnum.UniformBuffer, (uint)_uniformBufferBinding0);
|
||||
|
||||
_gl.ActiveTexture((TextureUnit)_activeTexture);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, (uint)_textureBinding2D);
|
||||
|
||||
if (_stencilTest) _gl.Enable(EnableCap.StencilTest); else _gl.Disable(EnableCap.StencilTest);
|
||||
_gl.StencilFunc((StencilFunction)_stencilFunc, _stencilRef, (uint)_stencilValueMask);
|
||||
_gl.StencilOp((StencilOp)_stencilFail, (StencilOp)_stencilPassDepthFail, (StencilOp)_stencilPassDepthPass);
|
||||
_gl.StencilMask((uint)_stencilWritemask);
|
||||
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, _unpackAlignment);
|
||||
_gl.PixelStore(PixelStoreParameter.PackAlignment, _packAlignment);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackRowLength, _unpackRowLength);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackSkipRows, _unpackSkipRows);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, _unpackSkipPixels);
|
||||
|
||||
_gl.ClearColor(_clearColor[0], _clearColor[1], _clearColor[2], _clearColor[3]);
|
||||
_gl.ClearDepth(_clearDepth);
|
||||
|
||||
_gl.Viewport(_viewport[0], _viewport[1], (uint)_viewport[2], (uint)_viewport[3]);
|
||||
RestoreScissor();
|
||||
|
||||
if (_depthTest) _gl.Enable(EnableCap.DepthTest); else _gl.Disable(EnableCap.DepthTest);
|
||||
_gl.DepthFunc((DepthFunction)_depthFunc);
|
||||
_gl.DepthMask(_depthMask);
|
||||
|
||||
if (_cullFace) _gl.Enable(EnableCap.CullFace); else _gl.Disable(EnableCap.CullFace);
|
||||
_gl.CullFace((TriangleFace)_cullFaceMode);
|
||||
_gl.FrontFace((FrontFaceDirection)_frontFace);
|
||||
|
||||
if (_blend) _gl.Enable(EnableCap.Blend); else _gl.Disable(EnableCap.Blend);
|
||||
_gl.BlendFuncSeparate((BlendingFactor)_blendSrc, (BlendingFactor)_blendDst, (BlendingFactor)_blendSrcAlpha, (BlendingFactor)_blendDstAlpha);
|
||||
_gl.BlendEquationSeparate((BlendEquationModeEXT)_blendEquation, (BlendEquationModeEXT)_blendEquationAlpha);
|
||||
_gl.BlendColor(_blendColor[0], _blendColor[1], _blendColor[2], _blendColor[3]);
|
||||
|
||||
_gl.ColorMask(_colorMask[0] != 0, _colorMask[1] != 0, _colorMask[2] != 0, _colorMask[3] != 0);
|
||||
|
||||
_gl.PolygonMode(GLEnum.FrontAndBack, (PolygonMode)_polygonMode);
|
||||
|
||||
if (_sampleAlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage); else _gl.Disable(EnableCap.SampleAlphaToCoverage);
|
||||
if (_sampleAlphaToOne) _gl.Enable(EnableCap.SampleAlphaToOne); else _gl.Disable(EnableCap.SampleAlphaToOne);
|
||||
if (_multisample) _gl.Enable(EnableCap.Multisample); else _gl.Disable(EnableCap.Multisample);
|
||||
if (_primitiveRestart) _gl.Enable((EnableCap)GLEnum.PrimitiveRestart); else _gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
|
||||
if (_programPointSize) _gl.Enable((EnableCap)GLEnum.ProgramPointSize); else _gl.Disable((EnableCap)GLEnum.ProgramPointSize);
|
||||
|
||||
_gl.LineWidth(_lineWidth);
|
||||
|
||||
_gl.BindSampler(0, (uint)_samplerBinding0);
|
||||
_gl.BindSampler(1, (uint)_samplerBinding1);
|
||||
_gl.BindSampler(2, (uint)_samplerBinding2);
|
||||
|
||||
_gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, (uint)_drawFramebufferBinding);
|
||||
_gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, (uint)_readFramebufferBinding);
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs
Normal file
83
src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Resource types for GPU memory tracking.
|
||||
/// </summary>
|
||||
public enum GpuResourceType {
|
||||
Texture,
|
||||
Buffer,
|
||||
Shader,
|
||||
VAO,
|
||||
FBO,
|
||||
RBO,
|
||||
Other
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Details about a GPU resource type.
|
||||
/// </summary>
|
||||
public record GpuResourceDetails(GpuResourceType Type, int Count, long Bytes);
|
||||
|
||||
/// <summary>
|
||||
/// Details about a specific named buffer.
|
||||
/// </summary>
|
||||
public record NamedBufferDetails(string Name, long CapacityBytes, long UsedBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Tracks manual VRAM allocations for buffers and textures.
|
||||
/// </summary>
|
||||
public static class GpuMemoryTracker {
|
||||
private static long _allocatedBytes;
|
||||
private static readonly long[] _allocatedBytesByType = new long[Enum.GetValues<GpuResourceType>().Length];
|
||||
private static readonly int[] _resourceCountsByType = new int[Enum.GetValues<GpuResourceType>().Length];
|
||||
private static readonly ConcurrentDictionary<string, NamedBufferDetails> _namedBuffers = new();
|
||||
|
||||
public static long AllocatedBytes => Interlocked.Read(ref _allocatedBytes);
|
||||
|
||||
public static int VaoCount => _resourceCountsByType[(int)GpuResourceType.VAO];
|
||||
public static int ShaderCount => _resourceCountsByType[(int)GpuResourceType.Shader];
|
||||
public static int BufferCount => _resourceCountsByType[(int)GpuResourceType.Buffer];
|
||||
public static int TextureCount => _resourceCountsByType[(int)GpuResourceType.Texture];
|
||||
public static int FboCount => _resourceCountsByType[(int)GpuResourceType.FBO];
|
||||
public static int RboCount => _resourceCountsByType[(int)GpuResourceType.RBO];
|
||||
|
||||
public static void TrackAllocation(long sizeInBytes, GpuResourceType type = GpuResourceType.Other) {
|
||||
Interlocked.Add(ref _allocatedBytes, sizeInBytes);
|
||||
Interlocked.Add(ref _allocatedBytesByType[(int)type], sizeInBytes);
|
||||
}
|
||||
|
||||
public static void TrackDeallocation(long sizeInBytes, GpuResourceType type = GpuResourceType.Other) {
|
||||
Interlocked.Add(ref _allocatedBytes, -sizeInBytes);
|
||||
Interlocked.Add(ref _allocatedBytesByType[(int)type], -sizeInBytes);
|
||||
}
|
||||
|
||||
public static void TrackResourceAllocation(GpuResourceType type) => Interlocked.Increment(ref _resourceCountsByType[(int)type]);
|
||||
public static void TrackResourceDeallocation(GpuResourceType type) => Interlocked.Decrement(ref _resourceCountsByType[(int)type]);
|
||||
|
||||
public static void TrackNamedBuffer(string name, long capacityBytes, long usedBytes) {
|
||||
_namedBuffers[name] = new NamedBufferDetails(name, capacityBytes, usedBytes);
|
||||
}
|
||||
|
||||
public static void UntrackNamedBuffer(string name) {
|
||||
_namedBuffers.TryRemove(name, out _);
|
||||
}
|
||||
|
||||
public static IEnumerable<NamedBufferDetails> GetNamedBufferDetails() => _namedBuffers.Values.OrderBy(b => b.Name);
|
||||
|
||||
public static IEnumerable<GpuResourceDetails> GetDetails() {
|
||||
var types = Enum.GetValues<GpuResourceType>();
|
||||
foreach (var type in types) {
|
||||
yield return new GpuResourceDetails(
|
||||
type,
|
||||
_resourceCountsByType[(int)type],
|
||||
Interlocked.Read(ref _allocatedBytesByType[(int)type])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs
Normal file
104
src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET.
|
||||
/// </summary>
|
||||
public class ManagedGLFramebuffer : IFramebuffer {
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private GL _gl => _device.GL;
|
||||
private readonly uint _fboId;
|
||||
private readonly uint _depthStencilRenderbuffer; // 0 if not used
|
||||
private readonly ITexture _texture;
|
||||
private readonly int _width;
|
||||
private readonly int _height;
|
||||
|
||||
public ITexture Texture => _texture;
|
||||
public IntPtr NativeHandle => new IntPtr(_fboId);
|
||||
|
||||
public ManagedGLFramebuffer(OpenGLGraphicsDevice device, ITexture texture, int width, int height, bool hasDepthStencil) {
|
||||
_device = device;
|
||||
_texture = texture;
|
||||
_width = width;
|
||||
_height = height;
|
||||
|
||||
// Generate and bind the framebuffer
|
||||
_fboId = _gl.GenFramebuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.FBO);
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fboId);
|
||||
|
||||
// Attach the texture as the color attachment
|
||||
_gl.FramebufferTexture2D(
|
||||
FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.ColorAttachment0,
|
||||
TextureTarget.Texture2D,
|
||||
(uint)texture.NativePtr.ToInt32(),
|
||||
0
|
||||
);
|
||||
|
||||
// Create and attach a depth-stencil renderbuffer if requested
|
||||
if (true || hasDepthStencil) {
|
||||
_depthStencilRenderbuffer = _gl.GenRenderbuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.RBO);
|
||||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthStencilRenderbuffer);
|
||||
_gl.RenderbufferStorage(
|
||||
RenderbufferTarget.Renderbuffer,
|
||||
InternalFormat.Depth24Stencil8,
|
||||
(uint)width,
|
||||
(uint)height
|
||||
);
|
||||
_gl.FramebufferRenderbuffer(
|
||||
FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.DepthStencilAttachment,
|
||||
RenderbufferTarget.Renderbuffer,
|
||||
_depthStencilRenderbuffer
|
||||
);
|
||||
GpuMemoryTracker.TrackAllocation(_width * _height * 4, GpuResourceType.RBO); // Depth24Stencil8 is 4 bytes per pixel
|
||||
}
|
||||
|
||||
// Check framebuffer completeness
|
||||
var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
|
||||
if (status != GLEnum.FramebufferComplete) {
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
_gl.DeleteFramebuffer(_fboId);
|
||||
if (_depthStencilRenderbuffer != 0) {
|
||||
_gl.DeleteRenderbuffer(_depthStencilRenderbuffer);
|
||||
}
|
||||
throw new InvalidOperationException($"Framebuffer creation failed: {status}");
|
||||
}
|
||||
|
||||
var error = _gl.GetError();
|
||||
if (error != GLEnum.NoError) {
|
||||
throw new InvalidOperationException($"OpenGL error during framebuffer setup: {error}");
|
||||
}
|
||||
|
||||
// Unbind the framebuffer
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
var fboId = _fboId;
|
||||
var depthStencilRenderbuffer = _depthStencilRenderbuffer;
|
||||
var width = _width;
|
||||
var height = _height;
|
||||
|
||||
_device.QueueGLAction(gl => {
|
||||
if (fboId != 0) {
|
||||
gl.DeleteFramebuffer(fboId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.FBO);
|
||||
}
|
||||
if (depthStencilRenderbuffer != 0) {
|
||||
gl.DeleteRenderbuffer(depthStencilRenderbuffer);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.RBO);
|
||||
GpuMemoryTracker.TrackDeallocation(width * height * 4, GpuResourceType.RBO);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
Normal file
185
src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
using Silk.NET.OpenGL;
|
||||
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL index buffer
|
||||
/// </summary>
|
||||
public unsafe class ManagedGLIndexBuffer : IIndexBuffer {
|
||||
private uint bufferId;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private void* _mappedPtr;
|
||||
private GL GL => _device.GL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BufferUsage Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedGLIndexBuffer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="usage">Buffer usage</param>
|
||||
/// <param name="size">The size of the buffer, in bytes</param>
|
||||
public unsafe ManagedGLIndexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
|
||||
_device = device;
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
|
||||
// Generate the buffer
|
||||
bufferId = GL.GenBuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Allocate the buffer with the specified size but no initial data
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (_device.HasBufferStorage) {
|
||||
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
|
||||
GL.BufferStorage(GLEnum.ElementArrayBuffer, (uint)Size, (void*)0, flags);
|
||||
_mappedPtr = GL.MapBufferRange(GLEnum.ElementArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
|
||||
} else {
|
||||
GL.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)Size, (void*)0, Usage.ToGL());
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetData(uint[] data) {
|
||||
SetData(data.AsSpan());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData(Span<uint> data) {
|
||||
uint dataSize = (uint)data.Length * sizeof(uint);
|
||||
|
||||
// Ensure the buffer size is sufficient
|
||||
if (dataSize > Size) {
|
||||
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<uint> mappedSpan = new Span<uint>(_mappedPtr, data.Length);
|
||||
data.CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
fixed (uint* dataPtr = &data[0]) {
|
||||
GL.BufferData(GLEnum.ElementArrayBuffer, dataSize, (void*)dataPtr, Usage.ToGL());
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData(Span<uint> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
|
||||
if (Usage != BufferUsage.Dynamic) {
|
||||
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
|
||||
}
|
||||
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
|
||||
|
||||
if (dataSizeBytes == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we're not trying to write past the end of the buffer
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<uint> mappedSpan = new Span<uint>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
|
||||
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
|
||||
GL.BufferSubData(
|
||||
GLEnum.ElementArrayBuffer,
|
||||
destinationOffsetBytes,
|
||||
dataSizeBytes,
|
||||
(void*)dataPtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData(uint[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
|
||||
if (Usage != BufferUsage.Dynamic) {
|
||||
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
|
||||
}
|
||||
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
|
||||
|
||||
if (dataSizeBytes == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we're not trying to write past the end of the buffer
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
|
||||
GL.BufferSubData(
|
||||
GLEnum.ElementArrayBuffer,
|
||||
destinationOffsetBytes,
|
||||
dataSizeBytes,
|
||||
(void*)dataPtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Bind() {
|
||||
BaseObjectRenderManager.CurrentIBO = 0;
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Unbind() {
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public unsafe void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (bufferId != 0) {
|
||||
GL.DeleteBuffer(bufferId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = 0;
|
||||
_mappedPtr = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
204
src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
Normal file
204
src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public unsafe class ManagedGLTexture : ITexture {
|
||||
private uint _texture;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
|
||||
private GL GL => (_device as OpenGLGraphicsDevice).GL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr NativePtr => (IntPtr)_texture;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Width { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Height { get; private set; }
|
||||
|
||||
public TextureFormat Format => TextureFormat.RGBA8;
|
||||
public ulong BindlessHandle { get; private set; }
|
||||
public ulong BindlessWrapHandle { get; private set; }
|
||||
public ulong BindlessClampHandle { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ManagedGLTexture(OpenGLGraphicsDevice device, byte[]? source, int width, int height, TextureParameters? texParams = null) {
|
||||
var p = texParams ?? TextureParameters.Default;
|
||||
_device = device;
|
||||
_texture = GL.GenTexture();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
|
||||
Width = width;
|
||||
Height = height;
|
||||
GL.BindTexture(GLEnum.Texture2D, _texture);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
int maxDimension = Math.Max(width, height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
|
||||
if (_device.HasTextureStorage) {
|
||||
GL.TexStorage2D(GLEnum.Texture2D, (uint)mipLevels, GLEnum.Rgba8, (uint)width, (uint)height);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
else {
|
||||
GL.TexImage2D(GLEnum.Texture2D, 0, (int)InternalFormat.Rgba8, (uint)width, (uint)height, 0, PixelFormat.Rgba, (PixelType)0x1401, (void*)0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapS, (int)p.WrapS);
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapT, (int)p.WrapT);
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMinFilter, (int)p.MinFilter);
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (p.EnableAnisotropicFiltering && _device.RenderSettings.EnableAnisotropicFiltering)
|
||||
{
|
||||
float maxAnisotropy = 0f;
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out maxAnisotropy);
|
||||
|
||||
if (maxAnisotropy > 0)
|
||||
{
|
||||
GL.TexParameter(GLEnum.Texture2D, GLEnum.TextureMaxAnisotropy, maxAnisotropy);
|
||||
}
|
||||
}
|
||||
|
||||
if (p.EnableMipmaps) {
|
||||
GL.GenerateMipmap(GLEnum.Texture2D);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.BindTexture(GLEnum.Texture2D, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(CalculateSize(), GpuResourceType.Texture);
|
||||
|
||||
if (_device.HasBindless && _device.BindlessExtension != null) {
|
||||
BindlessHandle = _device.BindlessExtension.GetTextureHandle(_texture);
|
||||
BindlessWrapHandle = _device.BindlessExtension.GetTextureSamplerHandle(_texture, _device.WrapSampler);
|
||||
BindlessClampHandle = _device.BindlessExtension.GetTextureSamplerHandle(_texture, _device.ClampSampler);
|
||||
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessWrapHandle);
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessClampHandle);
|
||||
}
|
||||
}
|
||||
|
||||
private long CalculateSize() {
|
||||
int maxDimension = Math.Max(Width, Height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
long totalSize = 0;
|
||||
|
||||
for (int i = 0; i < mipLevels; i++) {
|
||||
int w = Math.Max(1, Width >> i);
|
||||
int h = Math.Max(1, Height >> i);
|
||||
totalSize += (long)w * h * 4;
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ManagedGLTexture(OpenGLGraphicsDevice device, string file) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetData(Rectangle rectangle, byte[] data) {
|
||||
if (_texture == 0) return;
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
BaseObjectRenderManager.CurrentAtlas = 0;
|
||||
|
||||
GL.GetInteger(GLEnum.TextureBinding2D, out int oldBinding);
|
||||
GL.BindTexture(GLEnum.Texture2D, _texture);
|
||||
|
||||
bool wasResident = false;
|
||||
if (BindlessHandle != 0 && _device.BindlessExtension != null && _device.BindlessExtension.IsTextureHandleResident(BindlessHandle)) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
|
||||
wasResident = true;
|
||||
}
|
||||
|
||||
fixed (byte* ptr = data) {
|
||||
GL.TexSubImage2D(
|
||||
GLEnum.Texture2D,
|
||||
0, // level
|
||||
rectangle.X,
|
||||
rectangle.Y,
|
||||
(uint)rectangle.Width,
|
||||
(uint)rectangle.Height,
|
||||
PixelFormat.Rgba,
|
||||
PixelType.UnsignedByte,
|
||||
ptr
|
||||
);
|
||||
}
|
||||
|
||||
// Generate mipmaps if needed
|
||||
GL.GenerateMipmap(GLEnum.Texture2D);
|
||||
|
||||
if (wasResident && BindlessHandle != 0 && _device.BindlessExtension != null) {
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
|
||||
}
|
||||
|
||||
GL.BindTexture(GLEnum.Texture2D, (uint)oldBinding);
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Bind(int slot = 0) {
|
||||
if (slot == 0) {
|
||||
BaseObjectRenderManager.CurrentAtlas = 0;
|
||||
}
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
|
||||
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture(targetTextureUnit);
|
||||
}
|
||||
|
||||
GL.BindSampler((uint)slot, 0);
|
||||
GL.BindTexture(GLEnum.Texture2D, (uint)NativePtr);
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindTexture(GLEnum.Texture2D, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected void ReleaseTexture() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (_device.BindlessExtension != null) {
|
||||
if (BindlessHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
|
||||
BindlessHandle = 0;
|
||||
}
|
||||
if (BindlessWrapHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessWrapHandle);
|
||||
BindlessWrapHandle = 0;
|
||||
}
|
||||
if (BindlessClampHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessClampHandle);
|
||||
BindlessClampHandle = 0;
|
||||
}
|
||||
}
|
||||
if (_texture != 0) {
|
||||
GL.DeleteTexture(_texture);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
GpuMemoryTracker.TrackDeallocation(CalculateSize(), GpuResourceType.Texture);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
_texture = 0;
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
ReleaseTexture();
|
||||
}
|
||||
}
|
||||
}
|
||||
436
src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
Normal file
436
src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
using AcDream.Core.Rendering.Wb;
|
||||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly
|
||||
using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public class ManagedGLTextureArray : ITextureArray {
|
||||
private readonly bool[] _usedLayers;
|
||||
private readonly GL GL;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private readonly ILogger _logger;
|
||||
private static int _nextId = 0;
|
||||
private bool _needsMipmapRegeneration = false;
|
||||
private readonly bool _isCompressed;
|
||||
private int _mipmapDirtyCount = 0;
|
||||
private readonly object _mipmapLock = new object();
|
||||
private uint _pboId;
|
||||
private int _pboSize;
|
||||
private readonly List<TextureLayerUpdate> _pendingUpdates = new();
|
||||
|
||||
private struct TextureLayerUpdate {
|
||||
public int Layer;
|
||||
public int Offset;
|
||||
public int Size;
|
||||
public PixelFormat? UploadPixelFormat;
|
||||
public PixelType? UploadPixelType;
|
||||
}
|
||||
|
||||
public int Slot { get; } = _nextId++;
|
||||
public int Width { get; private set; }
|
||||
public int Height { get; private set; }
|
||||
public int Size { get; private set; }
|
||||
public TextureFormat Format { get; private set; }
|
||||
public nint NativePtr { get; private set; }
|
||||
public ulong BindlessHandle { get; private set; }
|
||||
public ulong BindlessWrapHandle { get; private set; }
|
||||
public ulong BindlessClampHandle { get; private set; }
|
||||
public long TotalSizeInBytes => CalculateTotalSize();
|
||||
|
||||
public ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
|
||||
int size, ILogger logger, TextureParameters? texParams = null) {
|
||||
var p = texParams ?? TextureParameters.Default;
|
||||
if (width <= 0 || height <= 0 || size <= 0) {
|
||||
throw new ArgumentException($"Invalid texture array dimensions: {width}x{height}x{size}");
|
||||
}
|
||||
|
||||
Format = format;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Size = size;
|
||||
_usedLayers = new bool[size];
|
||||
_device = graphicsDevice;
|
||||
GL = graphicsDevice.GL;
|
||||
_logger = logger;
|
||||
_isCompressed = IsCompressedFormat(format);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
NativePtr = (nint)GL.GenTexture();
|
||||
if (NativePtr == 0) {
|
||||
throw new InvalidOperationException("Failed to generate texture array.");
|
||||
}
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
int maxDimension = Math.Max(width, height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
|
||||
GL.TexStorage3D(GLEnum.Texture2DArray, (uint)mipLevels, format.ToGL(), (uint)width, (uint)height,
|
||||
(uint)size);
|
||||
GLHelpers.CheckErrorsWithContext(GL,
|
||||
$"Creating texture array storage (Format={format}, Size={width}x{height}x{size}, MipLevels={mipLevels})");
|
||||
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMinFilter,
|
||||
(int)p.MinFilter);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMaxLevel, (int)mipLevels - 1);
|
||||
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
|
||||
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapS, (int)p.WrapS);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapT, (int)p.WrapT);
|
||||
|
||||
if (p.EnableAnisotropicFiltering && graphicsDevice.RenderSettings.EnableAnisotropicFiltering) {
|
||||
float maxAnisotropy = 0f;
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out maxAnisotropy);
|
||||
|
||||
if (maxAnisotropy > 0) {
|
||||
GL.TexParameter(GLEnum.Texture2DArray, GLEnum.TextureMaxAnisotropy, maxAnisotropy);
|
||||
}
|
||||
}
|
||||
|
||||
// Set texture swizzle for single-channel formats
|
||||
if (format == TextureFormat.A8) {
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleR, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleG, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleB, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleA, (int)GLEnum.Red);
|
||||
}
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(CalculateTotalSize(), GpuResourceType.Texture);
|
||||
|
||||
if (_device.HasBindless && _device.BindlessExtension != null) {
|
||||
BindlessHandle = _device.BindlessExtension.GetTextureHandle((uint)NativePtr);
|
||||
BindlessWrapHandle = _device.BindlessExtension.GetTextureSamplerHandle((uint)NativePtr, _device.WrapSampler);
|
||||
BindlessClampHandle = _device.BindlessExtension.GetTextureSamplerHandle((uint)NativePtr, _device.ClampSampler);
|
||||
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessWrapHandle);
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessClampHandle);
|
||||
}
|
||||
|
||||
_pboId = GL.GenBuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
public long CalculateTotalSize() {
|
||||
int maxDimension = Math.Max(Width, Height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
long layerSize = GetExpectedDataSize();
|
||||
long totalSize = 0;
|
||||
|
||||
for (int i = 0; i < mipLevels; i++) {
|
||||
int w = Math.Max(1, Width >> i);
|
||||
int h = Math.Max(1, Height >> i);
|
||||
if (_isCompressed) {
|
||||
totalSize += TextureHelpers.GetCompressedLayerSize(w, h, Format) * Size;
|
||||
}
|
||||
else {
|
||||
totalSize += (long)w * h * (layerSize / (Width * Height)) * Size;
|
||||
}
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
private bool IsCompressedFormat(TextureFormat format) {
|
||||
return format == TextureFormat.DXT1 ||
|
||||
format == TextureFormat.DXT3 ||
|
||||
format == TextureFormat.DXT5;
|
||||
}
|
||||
|
||||
public void Bind(int slot = 0) {
|
||||
if (NativePtr == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
|
||||
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture(targetTextureUnit);
|
||||
}
|
||||
|
||||
GL.BindSampler((uint)slot, 0);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public unsafe int AddLayer(byte[] data) {
|
||||
return AddLayer(data, null, null);
|
||||
}
|
||||
|
||||
public unsafe int AddLayer(byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
||||
for (int i = 0; i < _usedLayers.Length; i++) {
|
||||
if (!_usedLayers[i]) {
|
||||
UpdateLayerInternal(i, data, uploadPixelFormat, uploadPixelType);
|
||||
_usedLayers[i] = true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No free layers available in texture array (Slot={Slot}, Size={Width}x{Height}x{Size}).");
|
||||
}
|
||||
|
||||
public unsafe int AddLayer(Span<byte> data) {
|
||||
return AddLayer(data.ToArray());
|
||||
}
|
||||
|
||||
public void UpdateLayer(int layer, byte[] data) {
|
||||
UpdateLayer(layer, data, null, null);
|
||||
}
|
||||
|
||||
public void UpdateLayer(int layer, byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
||||
UpdateLayerInternal(layer, data, uploadPixelFormat, uploadPixelType);
|
||||
_usedLayers[layer] = true;
|
||||
}
|
||||
|
||||
private unsafe void UpdateLayerInternal(int layer, byte[] data, PixelFormat? uploadPixelFormat,
|
||||
PixelType? uploadPixelType) {
|
||||
if (NativePtr == 0) {
|
||||
throw new InvalidOperationException("Texture array not created.");
|
||||
}
|
||||
|
||||
if (layer < 0 || layer >= Size) {
|
||||
throw new ArgumentOutOfRangeException(nameof(layer),
|
||||
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
||||
}
|
||||
|
||||
int currentPboOffset = 0;
|
||||
lock (_mipmapLock) {
|
||||
if (_pendingUpdates.Count > 0) {
|
||||
var lastUpdate = _pendingUpdates[^1];
|
||||
currentPboOffset = lastUpdate.Offset + lastUpdate.Size;
|
||||
}
|
||||
|
||||
// Align to 4 bytes for safety
|
||||
currentPboOffset = (currentPboOffset + 3) & ~3;
|
||||
|
||||
if (currentPboOffset + data.Length > _pboSize) {
|
||||
// Flush existing updates first because BufferData will orphan/clear the PBO
|
||||
if (_pendingUpdates.Count > 0) {
|
||||
ProcessDirtyUpdatesInternal();
|
||||
}
|
||||
currentPboOffset = 0;
|
||||
|
||||
int newSize = Math.Max(_pboSize * 2, data.Length);
|
||||
newSize = Math.Max(newSize, GetExpectedDataSize() * 4); // Initial size 4 layers
|
||||
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
|
||||
GL.BufferData(GLEnum.PixelUnpackBuffer, (nuint)newSize, (void*)0, GLEnum.StreamDraw);
|
||||
|
||||
if (_pboSize > 0) {
|
||||
GpuMemoryTracker.TrackDeallocation(_pboSize, GpuResourceType.Buffer);
|
||||
}
|
||||
_pboSize = newSize;
|
||||
GpuMemoryTracker.TrackAllocation(_pboSize, GpuResourceType.Buffer);
|
||||
}
|
||||
else {
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
|
||||
}
|
||||
|
||||
fixed (byte* ptr = data) {
|
||||
GL.BufferSubData(GLEnum.PixelUnpackBuffer, (nint)currentPboOffset, (nuint)data.Length, ptr);
|
||||
}
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
||||
|
||||
_pendingUpdates.Add(new TextureLayerUpdate {
|
||||
Layer = layer,
|
||||
Offset = currentPboOffset,
|
||||
Size = data.Length,
|
||||
UploadPixelFormat = uploadPixelFormat,
|
||||
UploadPixelType = uploadPixelType
|
||||
});
|
||||
|
||||
_needsMipmapRegeneration = true;
|
||||
_mipmapDirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessDirtyUpdates() {
|
||||
lock (_mipmapLock) {
|
||||
ProcessDirtyUpdatesInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ProcessDirtyUpdatesInternal() {
|
||||
if (_pendingUpdates.Count == 0 && !_needsMipmapRegeneration) return;
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
BaseObjectRenderManager.CurrentAtlas = 0;
|
||||
|
||||
GL.GetInteger(GLEnum.TextureBinding2DArray, out int oldBinding);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
|
||||
bool wasResident = false;
|
||||
if (BindlessHandle != 0 && _device.BindlessExtension != null && _device.BindlessExtension.IsTextureHandleResident(BindlessHandle)) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
|
||||
wasResident = true;
|
||||
}
|
||||
|
||||
if (_pendingUpdates.Count > 0) {
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, _pboId);
|
||||
|
||||
foreach (var update in _pendingUpdates) {
|
||||
if (_isCompressed) {
|
||||
var internalFormat = Format.ToCompressedGL();
|
||||
GL.CompressedTexSubImage3D(GLEnum.Texture2DArray, 0, 0, 0, update.Layer,
|
||||
(uint)Width, (uint)Height, 1, internalFormat, (uint)update.Size, (void*)update.Offset);
|
||||
}
|
||||
else {
|
||||
var pixelFormat = update.UploadPixelFormat ?? Format.ToPixelFormat();
|
||||
var pixelType = update.UploadPixelType ?? Format.ToPixelType();
|
||||
GL.TexSubImage3D(GLEnum.Texture2DArray, 0, 0, 0, update.Layer, (uint)Width, (uint)Height, 1,
|
||||
pixelFormat, pixelType, (void*)update.Offset);
|
||||
}
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
||||
_pendingUpdates.Clear();
|
||||
}
|
||||
|
||||
if (_needsMipmapRegeneration && _mipmapDirtyCount > 0) {
|
||||
if (_isCompressed) {
|
||||
_logger.LogDebug("Skipping automatic mipmap generation for compressed texture array (Slot={Slot})", Slot);
|
||||
}
|
||||
else if (!GLHelpers.ValidateTextureMipmapStatus(GL, GLEnum.Texture2DArray, out var errorMessage)) {
|
||||
_logger.LogWarning("Mipmap validation failed for texture array (Slot={Slot}): {Error}", Slot, errorMessage);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
GL.GenerateMipmap(GLEnum.Texture2DArray);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogWarning(ex, "Failed to generate mipmaps for texture array (Slot={Slot}).", Slot);
|
||||
}
|
||||
}
|
||||
_mipmapDirtyCount = 0;
|
||||
_needsMipmapRegeneration = false;
|
||||
}
|
||||
|
||||
if (wasResident && BindlessHandle != 0 && _device.BindlessExtension != null) {
|
||||
_device.BindlessExtension.MakeTextureHandleResident(BindlessHandle);
|
||||
}
|
||||
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)oldBinding);
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
private void ClearLayerForMipmap(int layer) {
|
||||
// Upload a single black/transparent pixel to make layer defined
|
||||
byte[] clearData = new byte[GetExpectedDataSize()];
|
||||
Array.Clear(clearData, 0, clearData.Length); // Zero-fill (black/transparent)
|
||||
UpdateLayerInternal(layer, clearData, null, null);
|
||||
}
|
||||
|
||||
private int GetExpectedDataSize() {
|
||||
if (_isCompressed) {
|
||||
return TextureHelpers.GetCompressedLayerSize(Width, Height, Format);
|
||||
}
|
||||
|
||||
return Format switch {
|
||||
TextureFormat.RGBA8 => Width * Height * 4,
|
||||
TextureFormat.RGB8 => Width * Height * 3,
|
||||
TextureFormat.A8 => Width * Height * 1,
|
||||
TextureFormat.Rgba32f => Width * Height * 16,
|
||||
_ => throw new NotSupportedException($"Unsupported format {Format}")
|
||||
};
|
||||
}
|
||||
|
||||
public void RemoveLayer(int layer) {
|
||||
if (layer < 0 || layer >= Size) {
|
||||
throw new ArgumentOutOfRangeException(nameof(layer),
|
||||
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
||||
}
|
||||
|
||||
if (!_usedLayers[layer]) {
|
||||
throw new InvalidOperationException($"Layer {layer} is already free (Slot={Slot}).");
|
||||
}
|
||||
|
||||
_usedLayers[layer] = false;
|
||||
|
||||
// Make layer defined for mipmap completeness (uncompressed only)
|
||||
if (!_isCompressed) {
|
||||
ClearLayerForMipmap(layer);
|
||||
}
|
||||
|
||||
lock (_mipmapLock) {
|
||||
_mipmapDirtyCount++; // Mark dirty to regen
|
||||
_needsMipmapRegeneration = true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLayerUsed(int layer) {
|
||||
if (layer < 0 || layer >= Size) return false;
|
||||
return _usedLayers[layer];
|
||||
}
|
||||
|
||||
public int GetUsedLayerCount() {
|
||||
return _usedLayers.Count(x => x);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void GenerateMipmaps() {
|
||||
_needsMipmapRegeneration = true;
|
||||
lock (_mipmapLock) {
|
||||
_mipmapDirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (_device.BindlessExtension != null) {
|
||||
if (BindlessHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessHandle);
|
||||
BindlessHandle = 0;
|
||||
}
|
||||
if (BindlessWrapHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessWrapHandle);
|
||||
BindlessWrapHandle = 0;
|
||||
}
|
||||
if (BindlessClampHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(BindlessClampHandle);
|
||||
BindlessClampHandle = 0;
|
||||
}
|
||||
}
|
||||
if (NativePtr != 0) {
|
||||
GL.DeleteTexture((uint)NativePtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(CalculateTotalSize(), GpuResourceType.Texture);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
NativePtr = 0;
|
||||
}
|
||||
if (_pboId != 0) {
|
||||
GL.DeleteBuffer(_pboId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
if (_pboSize > 0) {
|
||||
GpuMemoryTracker.TrackDeallocation(_pboSize, GpuResourceType.Buffer);
|
||||
}
|
||||
_pboId = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
143
src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs
Normal file
143
src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Runtime.InteropServices;
|
||||
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
|
||||
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
|
||||
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL uniform buffer
|
||||
/// </summary>
|
||||
public unsafe class ManagedGLUniformBuffer : IUniformBuffer {
|
||||
private uint bufferId;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private GL GL => _device.GL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BufferUsage Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedGLUniformBuffer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="device">Graphics device</param>
|
||||
/// <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;
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
|
||||
// Generate the buffer
|
||||
bufferId = GL.GenBuffer();
|
||||
if (bufferId == 0) {
|
||||
throw new Exception("Failed to generate uniform buffer.");
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(T[] data) where T : unmanaged {
|
||||
SetData(data.AsSpan());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(Span<T> data) where T : unmanaged {
|
||||
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Ensure the buffer size is sufficient
|
||||
if (dataSize > Size) {
|
||||
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
fixed (T* ptr = data) {
|
||||
GL.BufferSubData(GLEnum.UniformBuffer, 0, (nuint)dataSize, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
|
||||
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Validate buffer bounds
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
fixed (T* ptr = data.Slice(sourceOffsetElements, lengthElements)) {
|
||||
GL.BufferSubData(GLEnum.UniformBuffer, (nint)destinationOffsetBytes, (nuint)dataSizeBytes, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a single piece of data in the buffer.
|
||||
/// </summary>
|
||||
public unsafe void SetData<T>(ref T data) where T : unmanaged {
|
||||
fixed (T* pData = &data) {
|
||||
SetData(new Span<T>(pData, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the buffer to the specified binding point.
|
||||
/// </summary>
|
||||
/// <param name="bindingPoint">The binding point to bind to</param>
|
||||
public void Bind(uint bindingPoint) {
|
||||
GL.BindBufferBase(GLEnum.UniformBuffer, bindingPoint, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Bind() {
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Unbind() {
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (bufferId != 0) {
|
||||
GL.DeleteBuffer(bufferId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs
Normal file
77
src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VertexAttribType = Silk.NET.OpenGL.VertexAttribType;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public unsafe class ManagedGLVertexArray : IVertexArray {
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private GL GL => _device.GL;
|
||||
private uint _vaoId = 0;
|
||||
|
||||
public ManagedGLVertexArray(OpenGLGraphicsDevice device, IVertexBuffer buffer, VertexFormat format) {
|
||||
_device = device;
|
||||
|
||||
// Generate the vertex array
|
||||
_vaoId = GL.GenVertexArray();
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (_vaoId == 0) {
|
||||
throw new Exception("Failed to generate vertex array.");
|
||||
}
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO);
|
||||
|
||||
SetVertexBuffer(buffer, format);
|
||||
}
|
||||
|
||||
public void SetVertexBuffer(IVertexBuffer buffer, VertexFormat format) {
|
||||
GL.BindVertexArray(_vaoId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
buffer.Bind();
|
||||
for (int i = 0; i < format.Attributes.Length; i++) {
|
||||
var attr = format.Attributes[i];
|
||||
GL.EnableVertexAttribArray((uint)i);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.VertexAttribPointer((uint)i, attr.Size, Convert(attr.Type), attr.Normalized, (uint)format.Stride, attr.Offset);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
GL.BindVertexArray(0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
private GLEnum Convert(Chorizite.Core.Render.Enums.VertexAttribType type) => type switch {
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.Float => GLEnum.Float,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.Int => GLEnum.Int,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedInt => GLEnum.UnsignedInt,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedByte => GLEnum.UnsignedByte,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.Byte => GLEnum.Byte,
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
|
||||
public void Bind() {
|
||||
GL.BindVertexArray(_vaoId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindVertexArray(0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (_vaoId != 0) {
|
||||
GL.DeleteVertexArray(_vaoId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
|
||||
_vaoId = 0;
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs
Normal file
185
src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Buffers;
|
||||
using System.Runtime.InteropServices;
|
||||
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL vertex buffer
|
||||
/// </summary>
|
||||
public unsafe class ManagedGLVertexBuffer : IVertexBuffer {
|
||||
private uint bufferId;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private void* _mappedPtr;
|
||||
private GL GL => _device.GL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BufferUsage Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedGLVertexBuffer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="usage">Buffer usage</param>
|
||||
/// <param name="size">The size of the buffer, in bytes</param>
|
||||
public unsafe ManagedGLVertexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
|
||||
_device = device;
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
|
||||
// Generate the buffer
|
||||
bufferId = GL.GenBuffer();
|
||||
if (bufferId == 0) {
|
||||
throw new Exception("Failed to generate vertex buffer.");
|
||||
}
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Allocate the buffer with the specified size but no initial data
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (_device.HasBufferStorage) {
|
||||
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
|
||||
GL.BufferStorage(GLEnum.ArrayBuffer, (uint)Size, (void*)0, flags);
|
||||
_mappedPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
|
||||
} else {
|
||||
GL.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(uint)Size,
|
||||
(void*)0, // No initial data
|
||||
Usage.ToGL());
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(T[] data) where T : IVertex {
|
||||
SetData(data.AsSpan());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(Span<T> data) where T : IVertex {
|
||||
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Ensure the buffer size is sufficient
|
||||
if (dataSize > Size) {
|
||||
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<T> mappedSpan = new Span<T>(_mappedPtr, data.Length);
|
||||
data.CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Map the buffer for writing
|
||||
void* mappedPtr = GL.MapBufferRange(
|
||||
GLEnum.ArrayBuffer,
|
||||
0, // offset
|
||||
dataSize,
|
||||
MapBufferAccessMask.WriteBit | MapBufferAccessMask.InvalidateBufferBit // Overwrite entire buffer
|
||||
);
|
||||
|
||||
if (mappedPtr == null) {
|
||||
throw new Exception("Failed to map buffer for writing.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Copy data directly to mapped memory
|
||||
Span<T> mappedSpan = new Span<T>(mappedPtr, data.Length);
|
||||
data.CopyTo(mappedSpan);
|
||||
}
|
||||
finally {
|
||||
// Unmap the buffer
|
||||
GL.UnmapBuffer(GLEnum.ArrayBuffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
|
||||
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
|
||||
if (Usage != BufferUsage.Dynamic) {
|
||||
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
|
||||
}
|
||||
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Validate buffer bounds
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<T> mappedSpan = new Span<T>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
|
||||
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Map the specific range of the buffer
|
||||
void* mappedPtr = GL.MapBufferRange(
|
||||
GLEnum.ArrayBuffer,
|
||||
destinationOffsetBytes,
|
||||
dataSizeBytes,
|
||||
MapBufferAccessMask.WriteBit // Write access for partial update
|
||||
);
|
||||
|
||||
if (mappedPtr == null) {
|
||||
throw new Exception("Failed to map buffer for writing.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Copy the specified range of data to the mapped memory
|
||||
Span<T> mappedSpan = new Span<T>(mappedPtr, lengthElements);
|
||||
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
|
||||
}
|
||||
finally {
|
||||
// Unmap the buffer
|
||||
GL.UnmapBuffer(GLEnum.ArrayBuffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Bind() {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public unsafe void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (bufferId != 0) {
|
||||
GL.DeleteBuffer(bufferId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = 0;
|
||||
_mappedPtr = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
633
src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
Normal file
633
src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
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;
|
||||
|
||||
public GL GL { get; }
|
||||
public DebugRenderSettings RenderSettings => _renderSettings;
|
||||
|
||||
private readonly ConcurrentQueue<Action<GL>> _glThreadQueue = new();
|
||||
|
||||
public void QueueGLAction(Action<GL> action) {
|
||||
_glThreadQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
public void ProcessGLQueue() {
|
||||
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; }
|
||||
|
||||
public Chorizite.OpenGLSDLBackend.Lib.ParticleBatcher ParticleBatcher { get; private set; } = null!;
|
||||
|
||||
/// <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; }
|
||||
|
||||
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!;
|
||||
GL = null!;
|
||||
}
|
||||
|
||||
public OpenGLGraphicsDevice(GL gl, ILogger log, DebugRenderSettings renderSettings, bool allowBindless = true) : base() {
|
||||
_log = log;
|
||||
_renderSettings = renderSettings;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
GL.GenBuffers(1, out uint instanceVbo);
|
||||
InstanceVBO = instanceVbo;
|
||||
|
||||
// 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 (renderSettings.EnableAnisotropicFiltering) {
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
|
||||
if (maxAniso > 0) GL.SamplerParameter(WrapSampler, GLEnum.TextureMaxAnisotropy, maxAniso);
|
||||
}
|
||||
|
||||
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 (renderSettings.EnableAnisotropicFiltering) {
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
|
||||
if (maxAniso > 0) GL.SamplerParameter(ClampSampler, GLEnum.TextureMaxAnisotropy, maxAniso);
|
||||
}
|
||||
|
||||
_sceneDataBuffer = new ManagedGLUniformBuffer(this, BufferUsage.Dynamic, Marshal.SizeOf<SceneData>());
|
||||
|
||||
InitializeSharedDebugResources();
|
||||
|
||||
// T3 interim: ParticleBatcher (Chorizite.OpenGLSDLBackend.Lib.ParticleBatcher) is a T4 type
|
||||
// (Particle batcher + emitter extraction is in T4). It expects the WB-original
|
||||
// OpenGLGraphicsDevice type; we cannot pass `this` until T4 extracts it alongside us.
|
||||
// The property is set to null! here; T4 will restore the real construction.
|
||||
ParticleBatcher = null!;
|
||||
}
|
||||
|
||||
private void InitializeSharedDebugResources() {
|
||||
// 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
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
GL.GenVertexArrays(1, out uint debugVao);
|
||||
SharedDebugVAO = debugVao;
|
||||
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; // Marshal.SizeOf<LineInstance>() - we'll hardcode or use a constant later
|
||||
|
||||
// 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); // OffsetOf End
|
||||
GL.VertexAttribDivisor(2, 1);
|
||||
|
||||
// aColor (location 3)
|
||||
GL.EnableVertexAttribArray(3);
|
||||
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24); // OffsetOf Color
|
||||
GL.VertexAttribDivisor(3, 1);
|
||||
|
||||
// aThickness (location 4)
|
||||
GL.EnableVertexAttribArray(4);
|
||||
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40); // OffsetOf Thickness
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (wrapSampler != 0) {
|
||||
gl.DeleteSampler(wrapSampler);
|
||||
}
|
||||
if (clampSampler != 0) {
|
||||
gl.DeleteSampler(clampSampler);
|
||||
}
|
||||
});
|
||||
|
||||
InstanceVBO = 0;
|
||||
InstanceVBOPtr = null;
|
||||
WrapSampler = 0;
|
||||
ClampSampler = 0;
|
||||
_sceneDataBuffer?.Dispose();
|
||||
_sceneDataBuffer = null;
|
||||
ParticleBatcher?.Dispose();
|
||||
}
|
||||
|
||||
public override IUniformBuffer CreateUniformBuffer(BufferUsage usage, int size) {
|
||||
return (IUniformBuffer)new ManagedGLUniformBuffer(this, usage, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/AcDream.App/Rendering/Wb/SceneData.cs
Normal file
24
src/AcDream.App/Rendering/Wb/SceneData.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Global scene data for Uniform Buffer Object (UBO)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 16)]
|
||||
public struct SceneData {
|
||||
public Matrix4x4 View; // 64 bytes
|
||||
public Matrix4x4 Projection; // 64 bytes
|
||||
public Matrix4x4 ViewProjection; // 64 bytes
|
||||
public Vector3 CameraPosition; // 12 bytes
|
||||
private float _padding1; // 4 bytes
|
||||
public Vector3 LightDirection; // 12 bytes
|
||||
private float _padding2; // 4 bytes
|
||||
public Vector3 SunlightColor; // 12 bytes
|
||||
private float _padding3; // 4 bytes
|
||||
public Vector3 AmbientColor; // 12 bytes
|
||||
public float SpecularPower; // 4 bytes
|
||||
public Vector2 ViewportSize; // 8 bytes
|
||||
private Vector2 _padding4; // 8 bytes
|
||||
}
|
||||
}
|
||||
55
src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs
Normal file
55
src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public static class TextureFormatExtensions {
|
||||
public static SizedInternalFormat ToGL(this Chorizite.Core.Render.Enums.TextureFormat format) {
|
||||
return format switch {
|
||||
TextureFormat.RGBA8 => SizedInternalFormat.Rgba8,
|
||||
TextureFormat.RGB8 => SizedInternalFormat.Rgb8,
|
||||
TextureFormat.A8 => SizedInternalFormat.R8,
|
||||
TextureFormat.Rgba32f => SizedInternalFormat.Rgba32f,
|
||||
TextureFormat.DXT1 => SizedInternalFormat.CompressedRgbaS3TCDxt1Ext,
|
||||
TextureFormat.DXT3 => SizedInternalFormat.CompressedRgbaS3TCDxt3Ext,
|
||||
TextureFormat.DXT5 => SizedInternalFormat.CompressedRgbaS3TCDxt5Ext,
|
||||
_ => throw new NotSupportedException($"Texture format {format} is not supported."),
|
||||
};
|
||||
}
|
||||
|
||||
public static InternalFormat ToCompressedGL(this Chorizite.Core.Render.Enums.TextureFormat format) {
|
||||
return format switch {
|
||||
TextureFormat.DXT1 => InternalFormat.CompressedRgbaS3TCDxt1Ext,
|
||||
TextureFormat.DXT3 => InternalFormat.CompressedRgbaS3TCDxt3Ext,
|
||||
TextureFormat.DXT5 => InternalFormat.CompressedRgbaS3TCDxt5Ext,
|
||||
_ => throw new NotSupportedException($"Texture format {format} does not support compression."),
|
||||
};
|
||||
}
|
||||
|
||||
public static PixelFormat ToPixelFormat(this Chorizite.Core.Render.Enums.TextureFormat format) {
|
||||
return format switch {
|
||||
Chorizite.Core.Render.Enums.TextureFormat.RGBA8 => PixelFormat.Rgba,
|
||||
Chorizite.Core.Render.Enums.TextureFormat.RGB8 => PixelFormat.Rgb,
|
||||
Chorizite.Core.Render.Enums.TextureFormat.A8 => PixelFormat.Red,
|
||||
Chorizite.Core.Render.Enums.TextureFormat.Rgba32f => PixelFormat.Rgba,
|
||||
_ => throw new NotSupportedException($"Texture format {format} is not supported."),
|
||||
};
|
||||
}
|
||||
|
||||
public static PixelType ToPixelType(this Chorizite.Core.Render.Enums.TextureFormat format) {
|
||||
return format switch {
|
||||
TextureFormat.RGBA8 => PixelType.UnsignedByte,
|
||||
TextureFormat.RGB8 => PixelType.UnsignedByte,
|
||||
TextureFormat.A8 => PixelType.UnsignedByte,
|
||||
TextureFormat.Rgba32f => PixelType.Float,
|
||||
_ => throw new NotSupportedException($"Texture format {format} is not supported."),
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsCompressed(this Chorizite.Core.Render.Enums.TextureFormat format) {
|
||||
return format == Chorizite.Core.Render.Enums.TextureFormat.DXT1 ||
|
||||
format == Chorizite.Core.Render.Enums.TextureFormat.DXT3 ||
|
||||
format == Chorizite.Core.Render.Enums.TextureFormat.DXT5;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/AcDream.App/Rendering/Wb/TextureParameters.cs
Normal file
35
src/AcDream.App/Rendering/Wb/TextureParameters.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Configurable OpenGL texture parameters for wrap mode, filtering, mipmaps, and anisotropic filtering.
|
||||
/// </summary>
|
||||
public struct TextureParameters {
|
||||
public TextureWrapMode WrapS;
|
||||
public TextureWrapMode WrapT;
|
||||
public TextureMinFilter MinFilter;
|
||||
public TextureMagFilter MagFilter;
|
||||
public bool EnableMipmaps;
|
||||
public bool EnableAnisotropicFiltering;
|
||||
|
||||
/// <summary>Standard tiling textures — Repeat + trilinear + aniso.</summary>
|
||||
public static readonly TextureParameters Default = new() {
|
||||
WrapS = TextureWrapMode.Repeat,
|
||||
WrapT = TextureWrapMode.Repeat,
|
||||
MinFilter = TextureMinFilter.LinearMipmapLinear,
|
||||
MagFilter = TextureMagFilter.Linear,
|
||||
EnableMipmaps = true,
|
||||
EnableAnisotropicFiltering = true,
|
||||
};
|
||||
|
||||
/// <summary>Non-tiling textures (alpha maps, fonts, UI, object atlases) — ClampToEdge + trilinear + aniso.</summary>
|
||||
public static readonly TextureParameters ClampToEdge = new() {
|
||||
WrapS = TextureWrapMode.ClampToEdge,
|
||||
WrapT = TextureWrapMode.ClampToEdge,
|
||||
MinFilter = TextureMinFilter.LinearMipmapLinear,
|
||||
MagFilter = TextureMagFilter.Linear,
|
||||
EnableMipmaps = true,
|
||||
EnableAnisotropicFiltering = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -4,14 +4,12 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Rendering;
|
||||
using Chorizite.OpenGLSDLBackend;
|
||||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Silk.NET.OpenGL;
|
||||
using WorldBuilder.Shared.Models;
|
||||
using WorldBuilder.Shared.Services;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
|
@ -30,7 +28,9 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// </summary>
|
||||
public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||
{
|
||||
private readonly OpenGLGraphicsDevice? _graphicsDevice;
|
||||
// T3 interim: ObjectMeshManager (T4-to-be-extracted) still expects the WB-original type.
|
||||
// Will become AcDream.App.Rendering.Wb.OpenGLGraphicsDevice when T4 is done.
|
||||
private readonly Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice? _graphicsDevice;
|
||||
private readonly DefaultDatReaderWriter? _wbDats;
|
||||
private readonly ObjectMeshManager? _meshManager;
|
||||
private readonly DatCollection? _dats;
|
||||
|
|
@ -75,7 +75,9 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_dats = dats;
|
||||
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
|
||||
// T3 interim: construct the WB-original device for ObjectMeshManager compatibility.
|
||||
// Will swap to AcDream.App.Rendering.Wb.OpenGLGraphicsDevice when T4 extracts ObjectMeshManager.
|
||||
_graphicsDevice = new Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice(gl, logger, new WorldBuilder.Shared.Models.DebugRenderSettings());
|
||||
_wbDats = new DefaultDatReaderWriter(datDir);
|
||||
// Phase 2 diagnostic — replace NullLogger with a Console-backed
|
||||
// logger so WB's internal catch block at ObjectMeshManager.cs:589
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.Core.Rendering.Wb {
|
||||
|
|
@ -157,5 +158,16 @@ namespace AcDream.Core.Rendering.Wb {
|
|||
int b = color565 & 31;
|
||||
return new byte[] { (byte)(r * 255 / 31), (byte)(g * 255 / 63), (byte)(b * 255 / 31), 255 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expected compressed data size for a texture layer.
|
||||
/// Extracted from Chorizite.OpenGLSDLBackend.Lib.TextureHelpers (MIT).
|
||||
/// </summary>
|
||||
public static int GetCompressedLayerSize(int width, int height, TextureFormat format) {
|
||||
int blocksWide = Math.Max(1, (width + 3) / 4);
|
||||
int blocksHigh = Math.Max(1, (height + 3) / 4);
|
||||
int blockSize = format == TextureFormat.DXT1 ? 8 : 16;
|
||||
return blocksWide * blocksHigh * blockSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue