430 lines
14 KiB
C#
430 lines
14 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
using Quik.VertexGenerator;
|
|
using static Quik.OpenGL.GLEnum;
|
|
using Quik.Media;
|
|
using System.Linq;
|
|
using System.Diagnostics;
|
|
|
|
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<DrawQueue, DrawData> data = new Dictionary<DrawQueue, DrawData>();
|
|
private readonly TextureManager textures = new TextureManager();
|
|
|
|
public bool IsInit { get; private set; } = false;
|
|
public event Action<GL21Driver> 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.5f);
|
|
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<QImage, int> textures = new Dictionary<QImage, int>();
|
|
private readonly HashSet<QImage> imagesNotUsed = new HashSet<QImage>();
|
|
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<QImageFormat, GLEnum> s_InternalFormat = new Dictionary<QImageFormat, GLEnum>()
|
|
{
|
|
[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<QImageFormat, GLEnum> s_PixelType = new Dictionary<QImageFormat, GLEnum>()
|
|
{
|
|
[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,
|
|
};
|
|
}
|
|
} |