using System; using System.IO; using System.Collections.Generic; using Quik.VertexGenerator; using static Quik.OpenGL.GLEnum; using Quik.Media; using System.Linq; namespace Quik.OpenGL { public class GL21Driver : IDisposable { private int program; private int v2Position; private int fZIndex; private int v2TexPos; private int fTexLayer; private int v4Color; private int m4Transforms; private int fMaxZ; private int iEnableSdf; private int iEnableTexture; private int iAlphaDiscard; private int fSdfThreshold; private int tx2d; private int tx2darray; private bool isDiposed; private readonly Dictionary data = new Dictionary(); private readonly TextureManager textures = new TextureManager(); public bool IsInit { get; private set; } = false; public event Action OnGCDispose; public GL21Driver() { } ~GL21Driver() { Dispose(false); } public void Init() { if (IsInit) return; int vs = CreateShader(GL_VERTEX_SHADER, "Quik.res.gl21.vert"); int fs = CreateShader(GL_FRAGMENT_SHADER, "Quik.res.gl21.frag"); program = GL.CreateProgram(); GL.AttachShader(program, vs); GL.AttachShader(program, fs); GL.LinkProgram(program); if (CheckProgram(program, out string msg) == false) { GraphicsException ex = new GraphicsException("Could not link shader program."); ex.Data.Add("Program Info Log", msg); } GL.DeleteShader(vs); GL.DeleteShader(fs); v2Position = GL.GetAttribLocation(program, nameof(v2Position)); fZIndex = GL.GetAttribLocation(program, nameof(fZIndex)); v2TexPos = GL.GetAttribLocation(program, nameof(v2TexPos)); fTexLayer = GL.GetAttribLocation(program, nameof(fTexLayer)); v4Color = GL.GetAttribLocation(program, nameof(v4Color)); m4Transforms = GL.GetUniformLocation(program, nameof(m4Transforms)); fMaxZ = GL.GetUniformLocation(program, nameof(fMaxZ)); fSdfThreshold = GL.GetUniformLocation(program, nameof(fSdfThreshold)); iEnableSdf = GL.GetUniformLocation(program, nameof(iEnableSdf)); iEnableTexture = GL.GetUniformLocation(program, nameof(iEnableTexture)); iAlphaDiscard = GL.GetUniformLocation(program, nameof(iAlphaDiscard)); tx2d = GL.GetUniformLocation(program, nameof(tx2d)); tx2darray = GL.GetUniformLocation(program, nameof(tx2darray)); IsInit = true; } private void AssertInit() { if (!IsInit) throw new InvalidOperationException("Initialize the driver first."); } public void Draw(DrawQueue queue, in QRectangle view) { AssertInit(); if (!data.TryGetValue(queue, out DrawData draw)) { draw = new DrawData(this, queue); data.Add(queue, draw); } // This already binds the vertex array for me. draw.PrepareFrame(); QVec2 size = view.Size; QMat4.Orthographic(out QMat4 matrix, view); GL.UseProgram(program); GL.Uniform1(fMaxZ, (float)(queue.ZDepth+1)); GL.UniformMatrix4(m4Transforms, false, in matrix); GL.Uniform1(fSdfThreshold, 0.0f); GL.Uniform1(tx2d, 0); GL.Enable(GL_BLEND); GL.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); foreach (DrawCall call in queue) { GL.Viewport( (int)call.Bounds.Left, (int)(view.Bottom - call.Bounds.Bottom), (int)call.Bounds.Right, (int)(view.Bottom - call.Bounds.Top)); GL.ActiveTexture(GL_TEXTURE0); GL.BindTexture(GL_TEXTURE_2D, 0); if (call.Texture != null) { GL.Uniform1(iEnableSdf, call.Texture.IsSdf ? 1 : 0); GL.Uniform1(iAlphaDiscard, 1); if (call.Texture.Depth > 1) { GL.Uniform1(iEnableTexture, 3); GL.ActiveTexture(GL_TEXTURE1); GL.BindTexture(GL_TEXTURE_2D_ARRAY, textures.GetTexture(call.Texture)); } else { GL.Uniform1(iEnableTexture, 2); GL.ActiveTexture(GL_TEXTURE0); GL.BindTexture(GL_TEXTURE_2D, textures.GetTexture(call.Texture)); } } else { GL.Uniform1(iEnableTexture, 0); } GL.DrawElements(GL_TRIANGLES, call.Count, GL_UNSIGNED_INT, sizeof(int)*call.Start); } } public void ClearDrawQueue(DrawQueue queue) { AssertInit(); if (!data.TryGetValue(queue, out DrawData draw)) return; draw.Dispose(); data.Remove(queue); } private static int CreateShader(GLEnum type, string name) { StreamReader source = new StreamReader(typeof(GL21Driver).Assembly.GetManifestResourceStream(name)); string text = source.ReadToEnd(); source.Dispose(); int shader = GL.CreateShader(type); GL.ShaderSource(shader, text); GL.CompileShader(shader); if (CheckShader(shader, out string msg) == false) { GraphicsException ex = new GraphicsException($"Failed to compile {type} shader stage."); ex.Data.Add("Shader Stage", type); ex.Data.Add("Shader Info Log", msg); ex.Data.Add("Shader Source", text); throw ex; } return shader; } private static bool CheckShader(int shader, out string message) { message = string.Empty; GL.GetShader(shader, GL_COMPILE_STATUS, out int i); if (i != (int)GL_TRUE) { message = GL.GetShaderInfoLog(shader); return false; } return true; } private static bool CheckProgram(int program, out string message) { message = string.Empty; GL.GetProgram(program, GL_LINK_STATUS, out int i); if (i != (int)GL_OK) { message = GL.GetProgramInfoLog(program); return false; } return true; } private void Dispose(bool disposing) { if (isDiposed) return; if (!IsInit) { isDiposed = true; return; } if (!disposing) { if (OnGCDispose == null) { throw new Exception("This object must strictly be disposed from the owning thread, not GC"); } else { OnGCDispose(this); return; } } GL.DeleteProgram(program); foreach (DrawData datum in data.Values) { datum.Dispose(); } isDiposed = true; GC.SuppressFinalize(this); } public void Dispose() => Dispose(true); private class DrawData : IDisposable { public DrawQueue Queue { get; } public int VertexArray { get; } private readonly GL21Driver driver; private int vbo1, vbo2; private int ebo1, ebo2; public DrawData(GL21Driver driver, DrawQueue queue) { Queue = queue; this.driver = driver; VertexArray = GL.GenVertexArray(); GL.GenBuffers(1, out vbo1); GL.GenBuffers(1, out vbo2); GL.GenBuffers(1, out ebo1); GL.GenBuffers(1, out ebo2); isDisposed = false; } public void PrepareFrame() { int vbo, ebo; vbo = Swap(ref vbo1, ref vbo2); ebo = Swap(ref ebo1, ref ebo2); if (Queue.VertexCount == 0 || Queue.ElementCount == 0) return; GL.BindVertexArray(VertexArray); GL.BindBuffer(GL_ARRAY_BUFFER, vbo); GL.BufferData(GL_ARRAY_BUFFER, QuikVertex.Stride * Queue.VertexCount, Queue.VertexArray, GL_STREAM_DRAW); GL.VertexAttribPointer(driver.v2Position, 2, GL_FLOAT, false, QuikVertex.Stride, QuikVertex.PositionOffset); GL.VertexAttribPointer(driver.fZIndex, 1, GL_UNSIGNED_INT, false, QuikVertex.Stride, QuikVertex.ZIndexOffset); GL.VertexAttribPointer(driver.v2TexPos, 2, GL_FLOAT, false, QuikVertex.Stride, QuikVertex.TextureCoordinatesOffset); GL.VertexAttribPointer(driver.fTexLayer, 1, GL_FLOAT, false, QuikVertex.Stride, QuikVertex.TextureLayerOffset); GL.VertexAttribPointer(driver.v4Color, 4, GL_UNSIGNED_BYTE, true, QuikVertex.Stride, QuikVertex.ColorOffset); GL.EnableVertexAttribArray(driver.v2Position); GL.EnableVertexAttribArray(driver.fZIndex); GL.EnableVertexAttribArray(driver.v2TexPos); GL.EnableVertexAttribArray(driver.v4Color); GL.EnableVertexAttribArray(driver.fTexLayer); GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, Queue.ElementCount * sizeof(int), Queue.ElementArray, GL_STREAM_DRAW); int Swap(ref int a, ref int b) { a ^= b; b ^= a; a ^= b; return a; } } private bool isDisposed; public void Dispose() { if (isDisposed) return; GL.DeleteVertexArray(VertexArray); GL.DeleteBuffer(vbo1); GL.DeleteBuffer(vbo2); GL.DeleteBuffer(ebo1); GL.DeleteBuffer(ebo2); } } } internal class TextureManager : IDisposable { private readonly Dictionary textures = new Dictionary(); private readonly HashSet imagesNotUsed = new HashSet(); private bool isDisposed = false; public void BeginFrame() { if (imagesNotUsed.Count > 0) { foreach (QImage image in imagesNotUsed) { GL.DeleteTexture(textures[image]); } imagesNotUsed.Clear(); } foreach (QImage image in textures.Keys) { imagesNotUsed.Add(image); } } public int GetTexture(QImage image) { if (textures.TryGetValue(image, out int texture)) { return texture; } if (image.Depth > 1) { texture = UploadTexture3d(image); } else { texture = UploadTexture2d(image); } return textures[image] = texture; } public int UploadTexture3d(QImage image3d) { int texture = GL.GenTexture(); GL.BindTexture(GL_TEXTURE_2D_ARRAY, texture); image3d.LockBits3d(out QImageLock lck, QImageLockOptions.Default); GL.TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, lck.Width, lck.Height, lck.Depth, 0, s_InternalFormat[lck.Format], s_PixelType[lck.Format], lck.ImagePtr); image3d.UnlockBits(); GL.TexParameter(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); GL.TexParameter(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); return texture; } public int UploadTexture2d(QImage image2d) { int texture = GL.GenTexture(); GL.BindTexture(GL_TEXTURE_2D, texture); image2d.LockBits2d(out QImageLock lck, QImageLockOptions.Default); GL.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, lck.Width, lck.Height, 0, s_InternalFormat[lck.Format], s_PixelType[lck.Format], lck.ImagePtr); image2d.UnlockBits(); GL.TexParameter(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); GL.TexParameter(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); return texture; } public void Dispose() { if (isDisposed) return; isDisposed = true; int[] ids = textures.Values.ToArray(); GL.DeleteTextures(ids); } private static readonly Dictionary s_InternalFormat = new Dictionary() { [QImageFormat.AlphaF] = GL_ALPHA, [QImageFormat.AlphaU8] = GL_ALPHA, [QImageFormat.RedF] = GL_RED, [QImageFormat.RedU8] = GL_RED, [QImageFormat.RgbF] = GL_RGB, [QImageFormat.RgbU8] = GL_RGB, [QImageFormat.RgbaU8] = GL_RGBA, [QImageFormat.RgbaF] = GL_RGBA, }; private static readonly Dictionary s_PixelType = new Dictionary() { [QImageFormat.AlphaF] = GL_FLOAT, [QImageFormat.RedF] = GL_FLOAT, [QImageFormat.RgbF] = GL_FLOAT, [QImageFormat.RgbaF] = GL_FLOAT, [QImageFormat.AlphaU8] = GL_UNSIGNED_BYTE, [QImageFormat.RedU8] = GL_UNSIGNED_BYTE, [QImageFormat.RgbU8] = GL_UNSIGNED_BYTE, [QImageFormat.RgbaU8] = GL_UNSIGNED_BYTE, }; } }