From 935edf0db811d4ae88a410ec47595994122d09c8 Mon Sep 17 00:00:00 2001 From: NepuShiro Date: Thu, 21 May 2026 05:01:06 -0500 Subject: [PATCH] Add easy textures --- SDL3 TestingSuite.csproj | 1 + SDL3/OpenGLTexture.cs | 208 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 SDL3/OpenGLTexture.cs diff --git a/SDL3 TestingSuite.csproj b/SDL3 TestingSuite.csproj index e4d10f2..27eea50 100644 --- a/SDL3 TestingSuite.csproj +++ b/SDL3 TestingSuite.csproj @@ -19,6 +19,7 @@ + diff --git a/SDL3/OpenGLTexture.cs b/SDL3/OpenGLTexture.cs new file mode 100644 index 0000000..2b484eb --- /dev/null +++ b/SDL3/OpenGLTexture.cs @@ -0,0 +1,208 @@ +using System.Numerics; +using Elements.Assets; +using Hexa.NET.ImGui; +using Hexa.NET.OpenGL; +using SDL3; + +namespace SDL3_TestingSuite.SDL3; + +public sealed unsafe class OpenGLTexture : IDisposable +{ + private readonly GL _gl; + + public uint Handle { get; private set; } + public int Width { get; } + public int Height { get; } + + public ImTextureRef ImGuiTexture => new ImTextureRef(null, (nint)Handle); + public Vector2 Size => new Vector2(Width, Height); + + public string? FileName; + public string? FilePath; + public string? Extension; + + private OpenGLTexture(GL gl, uint handle, int width, int height, string? path = null) + { + _gl = gl; + Handle = handle; + Width = width; + Height = height; + + if (path == null) return; + FileName = Path.GetFileName(path); + FilePath = path; + Extension = Path.GetExtension(path); + } + + public static OpenGLTexture FromFile(GL gl, string path) + { + nint surface = string.Equals(Path.GetExtension(path), ".bmp", StringComparison.OrdinalIgnoreCase) + ? SDL.LoadBMP(path) + : Image.Load(path); + + if (surface == nint.Zero) + { + throw new InvalidOperationException($"Failed to load image '{path}': {SDL.GetError()}"); + } + + try + { + return FromSurface(gl, surface, path); + } + finally + { + SDL.DestroySurface(surface); + } + } + + public static OpenGLTexture FromBitmap2D(GL gl, Bitmap2D bmp) + { + if (bmp == null) throw new ArgumentNullException(nameof(bmp)); + + int width = bmp.Size.x; + int height = bmp.Size.y; + // Ensure we upload RGBA8 as unsigned bytes + byte[] pixels = new byte[width * height * 4]; + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + var p = bmp.GetPixel32(x, y); + int idx = (y * width + x) * 4; + pixels[idx + 0] = p.r; + pixels[idx + 1] = p.g; + pixels[idx + 2] = p.b; + pixels[idx + 3] = p.a; + } + } + + uint tex = UploadRgba32FromPixels(gl, pixels, width, height); + return new OpenGLTexture(gl, tex, width, height); + } + + public static OpenGLTexture FromSurface(GL gl, nint surface, string? path = null) + { + if (surface == nint.Zero) + { + throw new ArgumentException("Surface cannot be null.", nameof(surface)); + } + + SDL.PixelFormat uploadFormat = BitConverter.IsLittleEndian + ? SDL.PixelFormat.ABGR8888 + : SDL.PixelFormat.RGBA8888; + + nint converted = SDL.ConvertSurface(surface, uploadFormat); + if (converted == nint.Zero) + { + throw new InvalidOperationException($"Failed to convert image surface to RGBA32: {SDL.GetError()}"); + } + + try + { + SDL.Surface* convertedSurface = (SDL.Surface*)converted; + bool locked = false; + + if (SDL.MustLock(*convertedSurface)) + { + if (!SDL.LockSurface(converted)) + { + throw new InvalidOperationException($"Failed to lock image surface: {SDL.GetError()}"); + } + + locked = true; + } + + try + { + uint texture = UploadRgba32(gl, convertedSurface); + return new OpenGLTexture(gl, texture, convertedSurface->Width, convertedSurface->Height, path); + } + finally + { + if (locked) + { + SDL.UnlockSurface(converted); + } + } + } + finally + { + SDL.DestroySurface(converted); + } + } + + private static uint UploadRgba32(GL gl, SDL.Surface* surface) + { + uint texture = 0; + gl.GenTextures(1, ref texture); + + gl.GetIntegerv(GLGetPName.TextureBinding2D, out int previousTexture); + gl.BindTexture(GLTextureTarget.Texture2D, texture); + + gl.TexParameteri(GLTextureTarget.Texture2D, GLTextureParameterName.MinFilter, (int)GLTextureMinFilter.Linear); + gl.TexParameteri(GLTextureTarget.Texture2D, GLTextureParameterName.MagFilter, (int)GLTextureMagFilter.Linear); + gl.TexParameteri(GLTextureTarget.Texture2D, GLTextureParameterName.WrapS, (int)GLEnum.ClampToEdge); + gl.TexParameteri(GLTextureTarget.Texture2D, GLTextureParameterName.WrapT, (int)GLEnum.ClampToEdge); + + gl.PixelStorei(GLPixelStoreParameter.UnpackAlignment, 1); + gl.PixelStorei(GLPixelStoreParameter.UnpackRowLength, surface->Pitch / 4); + gl.TexImage2D( + GLTextureTarget.Texture2D, + 0, + GLInternalFormat.Rgba, + surface->Width, + surface->Height, + 0, + GLPixelFormat.Rgba, + GLPixelType.UnsignedByte, + surface->Pixels); + gl.PixelStorei(GLPixelStoreParameter.UnpackRowLength, 0); + + gl.BindTexture(GLTextureTarget.Texture2D, (uint)previousTexture); + return texture; + } + + private static uint UploadRgba32FromPixels(GL gl, byte[] pixels, int width, int height) + { + uint texture = 0; + gl.GenTextures(1, ref texture); + + gl.GetIntegerv(GLGetPName.TextureBinding2D, out int previousTexture); + gl.BindTexture(GLTextureTarget.Texture2D, texture); + + gl.TexParameteri(GLTextureTarget.Texture2D, GLTextureParameterName.MinFilter, (int)GLTextureMinFilter.Linear); + gl.TexParameteri(GLTextureTarget.Texture2D, GLTextureParameterName.MagFilter, (int)GLTextureMagFilter.Linear); + gl.TexParameteri(GLTextureTarget.Texture2D, GLTextureParameterName.WrapS, (int)GLEnum.ClampToEdge); + gl.TexParameteri(GLTextureTarget.Texture2D, GLTextureParameterName.WrapT, (int)GLEnum.ClampToEdge); + + gl.PixelStorei(GLPixelStoreParameter.UnpackAlignment, 1); + + fixed (byte* ptr = &pixels[0]) + { + gl.TexImage2D( + GLTextureTarget.Texture2D, + 0, + GLInternalFormat.Rgba, + width, + height, + 0, + GLPixelFormat.Rgba, + GLPixelType.UnsignedByte, + (nint)ptr); + } + + gl.BindTexture(GLTextureTarget.Texture2D, (uint)previousTexture); + return texture; + } + + public void Dispose() + { + if (Handle == 0) + { + return; + } + + _gl.DeleteTexture(Handle); + Handle = 0; + } +}