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;
+ }
+}