mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
Vector3, Vector4, Matrix4, and Quaternion are now gone. Use System.Numerics instead. This commit is just replacing usages, cleaning up using declarations, and moving over the (couple) helpers that are actually important.
1161 lines
42 KiB
C#
1161 lines
42 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using OpenToolkit.Graphics.OpenGL4;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Client.Utility;
|
|
using Robust.Shared.Graphics;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Utility;
|
|
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
|
|
|
|
namespace Robust.Client.Graphics.Clyde
|
|
{
|
|
internal partial class Clyde
|
|
{
|
|
// The amount of quads we can render with ushort indices, leaving open 65536 for primitive restart.
|
|
private const ushort MaxBatchQuads = (2 << 13) - 1; // In human terms: (2**16/4)-1 = 16383
|
|
|
|
//
|
|
// While rendering of most normal things (read: not grids or lighting),
|
|
// we record stuff into a queue of rendering commands.
|
|
// This both improves performance (I think because of CPU cache?)
|
|
// and allows us to do batching more nicely
|
|
// (make one fat vertex buffer of batching data and send it off quickly)
|
|
//
|
|
// This *basically* divides the renderer into 3 "states":
|
|
// 1. running through high level rendering code and taking simple rendering commands,
|
|
// probably user code (UI, overlays, maybe more in the future).
|
|
// Hereafter referred to as "queue"
|
|
// 2. actively going through queued rendering commands created during (queue)
|
|
// and submitting them to the GL driver.
|
|
// Hereafter referred to as "submit"
|
|
// 3. running fixed special-purpose rendering code like lighting, FOV and grids.
|
|
// Also minor switching and transition code between stages of the renderer.
|
|
// Hereafter referred to as "misc"
|
|
//
|
|
// (queue) always has to be followed by (submit) so that the queued commands actually get executed.
|
|
//
|
|
// Each state obviously has some amount of... state to keep track of,
|
|
// like transformation matrices.
|
|
// This is complicated and I'll my best to keep it straight in the comments.
|
|
//
|
|
|
|
// Set to true after lighting is rendered for the current viewport.
|
|
// Disabled again after the world rendering phase on a viewport.
|
|
private bool _lightingReady;
|
|
|
|
// The viewport we are currently rendering to.
|
|
private Viewport? _currentViewport;
|
|
|
|
// The current render target we're rendering to during queue state.
|
|
// This gets immediately updated when switching render targets during (queue) and (misc),
|
|
// but not during (submit).
|
|
private LoadedRenderTarget _currentRenderTarget;
|
|
|
|
// Current model matrix used by the (queue) state.
|
|
// This matrix is applied to most normal geometry coming in.
|
|
// Some is applied while the batch is being created (e.g. simple texture draw calls).
|
|
// For DrawPrimitives OTOH the model matrix is passed along with the render command so is applied in the shader.
|
|
private Matrix3x2 _currentMatrixModel = Matrix3x2.Identity;
|
|
|
|
// Buffers and data for the batching system. Written into during (queue) and processed during (submit).
|
|
private readonly Vertex2D[] BatchVertexData = new Vertex2D[MaxBatchQuads * 4];
|
|
|
|
// Only write from InitRenderingBatchBuffers!
|
|
private ushort[] BatchIndexData = default!;
|
|
private int BatchVertexIndex;
|
|
private int BatchIndexIndex;
|
|
|
|
// Contains information about the currently running batch.
|
|
// So we can flush it if the next draw call is incompatible.
|
|
private BatchMetaData? _batchMetaData;
|
|
|
|
// private LoadedTexture? _batchLoadedTexture;
|
|
// Contains the shader instance that's currently being used by the (queue) stage for new commands.
|
|
private ClydeHandle _queuedShader => _queuedShaderInstance.Handle;
|
|
|
|
private ClydeShaderInstance _queuedShaderInstance = default!;
|
|
|
|
// Current projection & view matrices that are being used ot render.
|
|
// This gets updated to keep track during (queue) and (misc), but not during (submit).
|
|
private Matrix3x2 _currentMatrixProj;
|
|
private Matrix3x2 _currentMatrixView;
|
|
|
|
// (queue) and (misc), current state of the scissor test. Null if disabled.
|
|
private UIBox2i? _currentScissorState;
|
|
|
|
/// <summary>
|
|
/// Tracks enabled GL capabilities for renderer state.
|
|
/// </summary>
|
|
private GLCaps _glCaps = GLCaps.None;
|
|
|
|
private bool IsStencilling
|
|
{
|
|
get => (_glCaps & GLCaps.Stencilling) == GLCaps.Stencilling;
|
|
set
|
|
{
|
|
if (value == IsStencilling)
|
|
return;
|
|
|
|
if (value)
|
|
{
|
|
_glCaps |= GLCaps.Stencilling;
|
|
GL.Enable(EnableCap.StencilTest);
|
|
}
|
|
else
|
|
{
|
|
_glCaps &= ~GLCaps.Stencilling;
|
|
GL.Disable(EnableCap.StencilTest);
|
|
}
|
|
|
|
CheckGlError();
|
|
}
|
|
}
|
|
|
|
private bool IsBlending
|
|
{
|
|
get => (_glCaps & GLCaps.Blending) == GLCaps.Blending;
|
|
set
|
|
{
|
|
if (value == IsBlending)
|
|
return;
|
|
|
|
if (value)
|
|
{
|
|
_glCaps |= GLCaps.Blending;
|
|
GL.Enable(EnableCap.Blend);
|
|
}
|
|
else
|
|
{
|
|
_glCaps &= ~GLCaps.Blending;
|
|
GL.Disable(EnableCap.Blend);
|
|
}
|
|
|
|
CheckGlError();
|
|
}
|
|
}
|
|
|
|
private bool IsScissoring
|
|
{
|
|
get => _currentScissorState != null;
|
|
}
|
|
|
|
private readonly RefList<RenderCommand> _queuedRenderCommands = new RefList<RenderCommand>();
|
|
|
|
private void InitRenderingBatchBuffers()
|
|
{
|
|
BatchIndexData = new ushort[MaxBatchQuads * GetQuadBatchIndexCount()];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates uniform constants shared to all shaders, such as time and pixel size.
|
|
/// </summary>
|
|
private void _updateUniformConstants(in Vector2i screenSize)
|
|
{
|
|
var constants = new UniformConstants(Vector2.One / screenSize, (float) _gameTiming.RealTime.TotalSeconds);
|
|
UniformConstantsUBO.Reallocate(constants);
|
|
}
|
|
|
|
private void CalcScreenMatrices(in Vector2i screenSize, out Matrix3x2 proj, out Matrix3x2 view)
|
|
{
|
|
proj = Matrix3x2.Identity;
|
|
proj.M11 = 2f / screenSize.X;
|
|
proj.M22 = -2f / screenSize.Y;
|
|
proj.M31 = -1;
|
|
proj.M32 = 1;
|
|
|
|
if (_currentRenderTarget.FlipY)
|
|
{
|
|
proj.M22 *= -1;
|
|
proj.M32 *= -1;
|
|
}
|
|
|
|
view = Matrix3x2.Identity;
|
|
}
|
|
|
|
private void CalcWorldMatrices(in Vector2i screenSize, in Vector2 renderScale, IEye eye,
|
|
out Matrix3x2 proj, out Matrix3x2 view)
|
|
{
|
|
eye.GetViewMatrix(out view, renderScale);
|
|
|
|
CalcWorldProjMatrix(screenSize, out proj);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void CalcWorldProjMatrix(in Vector2i screenSize, out Matrix3x2 proj)
|
|
{
|
|
proj = Matrix3x2.Identity;
|
|
proj.M11 = EyeManager.PixelsPerMeter * 2f / screenSize.X;
|
|
proj.M22 = EyeManager.PixelsPerMeter * 2f / screenSize.Y;
|
|
|
|
if (_currentRenderTarget.FlipY)
|
|
{
|
|
proj.M22 *= -1;
|
|
proj.M32 *= -1;
|
|
}
|
|
}
|
|
|
|
private void SetProjViewBuffer(in Matrix3x2 proj, in Matrix3x2 view)
|
|
{
|
|
// TODO: Fix perf here.
|
|
// This immediately causes a glBufferData() call every time this is changed.
|
|
// Which will be a real performance bottleneck later.
|
|
// Because this is an UBO, these matrices should be batched as well
|
|
// and switched out during command buffer submit by just modifying the bind points.
|
|
var combined = new ProjViewMatrices(proj, view);
|
|
ProjViewUBO.Reallocate(combined);
|
|
}
|
|
|
|
private void SetProjViewFull(in Matrix3x2 proj, in Matrix3x2 view)
|
|
{
|
|
_currentMatrixProj = proj;
|
|
_currentMatrixView = view;
|
|
|
|
SetProjViewBuffer(proj, view);
|
|
}
|
|
|
|
private void ProcessRenderCommands()
|
|
{
|
|
foreach (ref var command in _queuedRenderCommands)
|
|
{
|
|
switch (command.Type)
|
|
{
|
|
case RenderCommandType.DrawBatch:
|
|
DrawCommandBatch(ref command.DrawBatch);
|
|
break;
|
|
|
|
case RenderCommandType.Scissor:
|
|
SetScissorImmediate(command.Scissor.EnableScissor, command.Scissor.Scissor);
|
|
break;
|
|
|
|
case RenderCommandType.ProjViewMatrix:
|
|
SetProjViewBuffer(command.ProjView.ProjMatrix, command.ProjView.ViewMatrix);
|
|
break;
|
|
|
|
case RenderCommandType.RenderTarget:
|
|
var rt = _renderTargets[command.RenderTarget.RenderTarget];
|
|
BindRenderTargetImmediate(rt);
|
|
GL.Viewport(0, 0, rt.Size.X, rt.Size.Y);
|
|
CheckGlError();
|
|
break;
|
|
|
|
case RenderCommandType.Viewport:
|
|
ref var vp = ref command.Viewport.Viewport;
|
|
GL.Viewport(vp.Left, vp.Bottom, vp.Width, vp.Height);
|
|
CheckGlError();
|
|
break;
|
|
|
|
case RenderCommandType.Clear:
|
|
ref var color = ref command.Clear.Color;
|
|
GL.ClearColor(color.R, color.G, color.B, color.A);
|
|
CheckGlError();
|
|
GL.ClearStencil(command.Clear.Stencil);
|
|
CheckGlError();
|
|
GL.Clear(command.Clear.Mask);
|
|
CheckGlError();
|
|
break;
|
|
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawCommandBatch(ref RenderCommandDrawBatch command)
|
|
{
|
|
var loadedTexture = _loadedTextures[command.TextureId];
|
|
|
|
BindVertexArray(BatchVAO.Handle);
|
|
CheckGlError();
|
|
|
|
var (program, loaded) = ActivateShaderInstance(command.ShaderInstance);
|
|
SetupGlobalUniformsImmediate(program, loadedTexture.IsSrgb);
|
|
|
|
GL.ActiveTexture(TextureUnit.Texture0);
|
|
CheckGlError();
|
|
GL.BindTexture(TextureTarget.Texture2D, loadedTexture.OpenGLObject.Handle);
|
|
CheckGlError();
|
|
|
|
if (_lightingReady && loaded.HasLighting)
|
|
{
|
|
SetTexture(TextureUnit.Texture1, _currentViewport!.LightRenderTarget.Texture);
|
|
}
|
|
else
|
|
{
|
|
SetTexture(TextureUnit.Texture1, _stockTextureWhite);
|
|
}
|
|
|
|
program.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
|
|
program.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1);
|
|
|
|
// Model matrix becomes identity since it's built into the batch mesh.
|
|
program.SetUniformMaybe(UniIModelMatrix, command.ModelMatrix);
|
|
// Reset ModUV to ensure it's identity and doesn't touch anything.
|
|
program.SetUniformMaybe(UniIModUV, new Vector4(0, 0, 1, 1));
|
|
|
|
program.SetUniformMaybe(UniITexturePixelSize, Vector2.One / loadedTexture.Size);
|
|
|
|
SetBlendFunc(loaded.BlendMode);
|
|
|
|
var primitiveType = MapPrimitiveType(command.PrimitiveType);
|
|
if (command.Indexed)
|
|
{
|
|
GL.DrawElements(primitiveType, command.Count, DrawElementsType.UnsignedShort,
|
|
command.StartIndex * sizeof(ushort));
|
|
CheckGlError();
|
|
}
|
|
else
|
|
{
|
|
GL.DrawArrays(primitiveType, command.StartIndex, command.Count);
|
|
CheckGlError();
|
|
}
|
|
|
|
ResetBlendFunc();
|
|
GL.BlendEquation(BlendEquationMode.FuncAdd);
|
|
|
|
_debugStats.LastGLDrawCalls += 1;
|
|
}
|
|
|
|
private static PrimitiveType MapPrimitiveType(BatchPrimitiveType type)
|
|
{
|
|
return type switch
|
|
{
|
|
BatchPrimitiveType.TriangleList => PrimitiveType.Triangles,
|
|
BatchPrimitiveType.TriangleFan => PrimitiveType.TriangleFan,
|
|
BatchPrimitiveType.TriangleStrip => PrimitiveType.TriangleStrip,
|
|
BatchPrimitiveType.LineList => PrimitiveType.Lines,
|
|
BatchPrimitiveType.LineStrip => PrimitiveType.LineStrip,
|
|
BatchPrimitiveType.LineLoop => PrimitiveType.LineLoop,
|
|
BatchPrimitiveType.PointList => PrimitiveType.Points,
|
|
_ => PrimitiveType.Triangles
|
|
};
|
|
}
|
|
|
|
private void _drawQuad(Vector2 a, Vector2 b, in Matrix3x2 modelMatrix, GLShaderProgram program)
|
|
{
|
|
DrawQuadWithVao(QuadVAO, a, b, modelMatrix, program);
|
|
}
|
|
|
|
private void DrawQuadWithVao(GLHandle vao, Vector2 a, Vector2 b, in Matrix3x2 modelMatrix,
|
|
GLShaderProgram program)
|
|
{
|
|
BindVertexArray(vao.Handle);
|
|
CheckGlError();
|
|
|
|
var rectTransform = Matrix3x2.Identity;
|
|
(rectTransform.M11, rectTransform.M22) = b - a;
|
|
(rectTransform.M31, rectTransform.M32) = a;
|
|
rectTransform = rectTransform * modelMatrix;
|
|
program.SetUniformMaybe(UniIModelMatrix, rectTransform);
|
|
|
|
_debugStats.LastGLDrawCalls += 1;
|
|
GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);
|
|
CheckGlError();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Flushes the render handle, processing and re-pooling all the command lists.
|
|
/// </summary>
|
|
private void FlushRenderQueue()
|
|
{
|
|
FlushBatchQueue();
|
|
|
|
// Reset renderer state.
|
|
_currentMatrixModel = Matrix3x2.Identity;
|
|
_queuedShaderInstance = _defaultShader;
|
|
SetScissorFull(null);
|
|
}
|
|
|
|
private void FlushBatchQueue()
|
|
{
|
|
// Finish any batches that may have been WiP.
|
|
BreakBatch();
|
|
|
|
BindVertexArray(BatchVAO.Handle);
|
|
CheckGlError();
|
|
|
|
_debugStats.LargestBatchVertices = Math.Max(BatchVertexIndex, _debugStats.LargestBatchVertices);
|
|
_debugStats.LargestBatchIndices = Math.Max(BatchIndexIndex, _debugStats.LargestBatchIndices);
|
|
|
|
if (BatchVertexIndex != 0)
|
|
{
|
|
BatchVBO.Reallocate(new Span<Vertex2D>(BatchVertexData, 0, BatchVertexIndex));
|
|
BatchVertexIndex = 0;
|
|
|
|
if (BatchIndexIndex != 0)
|
|
{
|
|
BatchEBO.Reallocate(new Span<ushort>(BatchIndexData, 0, BatchIndexIndex));
|
|
}
|
|
|
|
BatchIndexIndex = 0;
|
|
}
|
|
|
|
ProcessRenderCommands();
|
|
_queuedRenderCommands.Clear();
|
|
}
|
|
|
|
private void SetScissorFull(UIBox2i? state)
|
|
{
|
|
if (state.HasValue)
|
|
{
|
|
SetScissorImmediate(true, state.Value);
|
|
}
|
|
else
|
|
{
|
|
SetScissorImmediate(false, default);
|
|
}
|
|
|
|
_currentScissorState = state;
|
|
}
|
|
|
|
private void SetScissorImmediate(bool enable, in UIBox2i box)
|
|
{
|
|
if (enable)
|
|
{
|
|
GL.Enable(EnableCap.ScissorTest);
|
|
}
|
|
else
|
|
{
|
|
GL.Disable(EnableCap.ScissorTest);
|
|
}
|
|
|
|
if (enable)
|
|
{
|
|
// Don't forget to flip it, these coordinates have bottom left as origin.
|
|
// TODO: Broken when rendering to non-screen render targets.
|
|
|
|
if (_currentRenderTarget.FlipY)
|
|
{
|
|
GL.Scissor(box.Left, box.Top, box.Width, box.Height);
|
|
}
|
|
else
|
|
{
|
|
GL.Scissor(box.Left, _currentRenderTarget.Size.Y - box.Bottom, box.Width, box.Height);
|
|
}
|
|
CheckGlError();
|
|
}
|
|
}
|
|
|
|
// NOTE: sRGB IS IN LINEAR IF FRAMEBUFFER_SRGB IS ACTIVE.
|
|
private void ClearFramebuffer(Color color, int stencil = 0, ClearBufferMask mask = ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit)
|
|
{
|
|
GL.ClearColor(color.ConvertOpenTK());
|
|
CheckGlError();
|
|
GL.ClearStencil(stencil);
|
|
CheckGlError();
|
|
GL.Clear(mask);
|
|
CheckGlError();
|
|
}
|
|
|
|
private Color ConvertClearFromSrgb(Color color)
|
|
{
|
|
if (!_hasGLSrgb)
|
|
return color;
|
|
|
|
return Color.FromSrgb(color);
|
|
}
|
|
|
|
private (GLShaderProgram, LoadedShaderInstance) ActivateShaderInstance(ClydeHandle handle)
|
|
{
|
|
var instance = _shaderInstances[handle];
|
|
var shader = _loadedShaders[instance.ShaderHandle];
|
|
var program = shader.Program;
|
|
|
|
program.Use();
|
|
IsStencilling = instance.Stencil.Enabled;
|
|
|
|
// Handle stencil parameters.
|
|
if (instance.Stencil.Enabled)
|
|
{
|
|
GL.StencilMask(instance.Stencil.WriteMask);
|
|
CheckGlError();
|
|
GL.StencilFunc(ToGLStencilFunc(instance.Stencil.Func), instance.Stencil.Ref, instance.Stencil.ReadMask);
|
|
CheckGlError();
|
|
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, ToGLStencilOp(instance.Stencil.Op));
|
|
CheckGlError();
|
|
}
|
|
|
|
if (instance.Parameters.Count == 0)
|
|
return (program, instance);
|
|
|
|
if (shader.LastInstance == instance && !instance.ParametersDirty)
|
|
return (program, instance);
|
|
|
|
shader.LastInstance = instance;
|
|
instance.ParametersDirty = false;
|
|
|
|
int textureUnitVal = 0;
|
|
foreach (var (name, value) in instance.Parameters)
|
|
{
|
|
if (!program.HasUniform(name))
|
|
{
|
|
// Can happen if the GLSL compiler removes uniforms due to them being unused.
|
|
// Safe to just ignore them then I'd say.
|
|
continue;
|
|
}
|
|
|
|
switch (value)
|
|
{
|
|
case float f:
|
|
program.SetUniform(name, f);
|
|
break;
|
|
case float[] fArr:
|
|
program.SetUniform(name, fArr);
|
|
break;
|
|
case Vector2 vector2:
|
|
program.SetUniform(name, vector2);
|
|
break;
|
|
case Vector2[] vector2Arr:
|
|
program.SetUniform(name, vector2Arr);
|
|
break;
|
|
case Vector3 vector3:
|
|
program.SetUniform(name, vector3);
|
|
break;
|
|
case Vector4 vector4:
|
|
program.SetUniform(name, vector4);
|
|
break;
|
|
case Color color:
|
|
program.SetUniform(name, color);
|
|
break;
|
|
case Color[] colorArr:
|
|
program.SetUniform(name, colorArr);
|
|
break;
|
|
case int i:
|
|
program.SetUniform(name, i);
|
|
break;
|
|
case Vector2i vector2I:
|
|
program.SetUniform(name, vector2I);
|
|
break;
|
|
case bool b:
|
|
program.SetUniform(name, b ? 1 : 0);
|
|
break;
|
|
case bool[] bArr:
|
|
program.SetUniform(name, bArr);
|
|
break;
|
|
case Matrix3x2 matrix3:
|
|
program.SetUniform(name, matrix3);
|
|
break;
|
|
case Matrix4x4 matrix4:
|
|
program.SetUniform(name, matrix4);
|
|
break;
|
|
case ClydeTexture clydeTexture:
|
|
//It's important to start at Texture6 here since DrawCommandBatch uses Texture0 and Texture1 immediately after calling this
|
|
//function! If passing in textures as uniforms ever stops working it might be since someone made it use all the way up to Texture6 too.
|
|
//Might change this in the future?
|
|
TextureUnit cTarget = TextureUnit.Texture6 + textureUnitVal;
|
|
SetTexture(cTarget, clydeTexture.TextureId);
|
|
program.SetUniformTexture(name, cTarget);
|
|
textureUnitVal++;
|
|
break;
|
|
default:
|
|
throw new InvalidOperationException($"Unable to handle shader parameter {name}: {value}");
|
|
}
|
|
}
|
|
|
|
return (program, instance);
|
|
}
|
|
|
|
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
|
|
private static bool StrictColorEquality(in Color a, in Color b)
|
|
{
|
|
return a.R == b.R && a.G == b.G && a.B == b.B && a.A == b.A;
|
|
}
|
|
|
|
private ref RenderCommand AllocRenderCommand(RenderCommandType type)
|
|
{
|
|
ref var command = ref _queuedRenderCommands.AllocAdd();
|
|
command.Type = type;
|
|
return ref command;
|
|
}
|
|
|
|
private void DrawSetModelTransform(in Matrix3x2 matrix)
|
|
{
|
|
_currentMatrixModel = matrix;
|
|
}
|
|
|
|
private Matrix3x2 DrawGetModelTransform()
|
|
{
|
|
return _currentMatrixModel;
|
|
}
|
|
|
|
private void DrawSetProjViewTransform(in Matrix3x2 proj, in Matrix3x2 view)
|
|
{
|
|
BreakBatch();
|
|
|
|
ref var command = ref AllocRenderCommand(RenderCommandType.ProjViewMatrix);
|
|
|
|
command.ProjView.ProjMatrix = proj;
|
|
command.ProjView.ViewMatrix = view;
|
|
|
|
_currentMatrixProj = proj;
|
|
_currentMatrixView = view;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws a texture quad to the screen.
|
|
/// </summary>
|
|
/// <param name="texture">Texture to draw.</param>
|
|
/// <param name="bl">Bottom left vertex of the quad in object space.</param>
|
|
/// <param name="br">Bottom right vertex of the quad in object space.</param>
|
|
/// <param name="tl">Top left vertex of the quad in object space.</param>
|
|
/// <param name="tr">Top right vertex of the quad in object space.</param>
|
|
/// <param name="modulate">A color to multiply the texture by when shading. Non-linear.</param>
|
|
/// <param name="texCoords">The four corners of the texture coordinates, matching the four vertices.</param>
|
|
private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr, in Color modulate,
|
|
in Box2 texCoords)
|
|
{
|
|
EnsureBatchSpaceAvailable(4, GetQuadBatchIndexCount());
|
|
EnsureBatchState(texture, true, GetQuadBatchPrimitiveType(), _queuedShader);
|
|
|
|
// TODO RENDERING
|
|
// It's probably better to do this on the GPU.
|
|
bl = Vector2.Transform(bl, _currentMatrixModel);
|
|
br = Vector2.Transform(br, _currentMatrixModel);
|
|
tr = Vector2.Transform(tr, _currentMatrixModel);
|
|
tl = tr + bl - br;
|
|
|
|
// TODO: split batch if necessary.
|
|
var vIdx = BatchVertexIndex;
|
|
BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, new Vector2(0, 0), modulate);
|
|
BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, new Vector2(1, 0), modulate);
|
|
BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, new Vector2(1, 1), modulate);
|
|
BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, new Vector2(0, 1), modulate);
|
|
BatchVertexIndex += 4;
|
|
QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx);
|
|
|
|
_debugStats.LastClydeDrawCalls += 1;
|
|
}
|
|
|
|
private void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ClydeHandle textureId,
|
|
ReadOnlySpan<ushort> indices, ReadOnlySpan<Vertex2D> vertices)
|
|
{
|
|
FinishBatch();
|
|
_batchMetaData = null;
|
|
|
|
EnsureBatchSpaceAvailable(vertices.Length, indices.Length);
|
|
|
|
vertices.CopyTo(BatchVertexData.AsSpan(BatchVertexIndex));
|
|
|
|
// We are weaving this into the batch buffers for performance (and simplicity).
|
|
// This means all indices have to be offset.
|
|
for (var i = 0; i < indices.Length; i++)
|
|
{
|
|
var o = BatchIndexIndex + i;
|
|
var index = indices[i];
|
|
if (index != PrimitiveRestartIndex) // Don't offset primitive restart.
|
|
{
|
|
index = (ushort) (index + BatchVertexIndex);
|
|
}
|
|
|
|
BatchIndexData[o] = index;
|
|
}
|
|
|
|
BatchVertexIndex += vertices.Length;
|
|
|
|
ref var command = ref AllocRenderCommand(RenderCommandType.DrawBatch);
|
|
|
|
command.DrawBatch.Indexed = true;
|
|
command.DrawBatch.StartIndex = BatchIndexIndex;
|
|
command.DrawBatch.PrimitiveType = MapDrawToBatchPrimitiveType(primitiveTopology);
|
|
command.DrawBatch.TextureId = textureId;
|
|
command.DrawBatch.ShaderInstance = _queuedShader;
|
|
|
|
command.DrawBatch.Count = indices.Length;
|
|
command.DrawBatch.ModelMatrix = _currentMatrixModel;
|
|
|
|
_debugStats.LastBatches += 1;
|
|
_debugStats.LastClydeDrawCalls += 1;
|
|
BatchIndexIndex += indices.Length;
|
|
}
|
|
|
|
private void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ClydeHandle textureId,
|
|
in ReadOnlySpan<Vertex2D> vertices)
|
|
{
|
|
FinishBatch();
|
|
_batchMetaData = null;
|
|
|
|
EnsureBatchSpaceAvailable(vertices.Length, 0);
|
|
|
|
vertices.CopyTo(BatchVertexData.AsSpan(BatchVertexIndex));
|
|
|
|
ref var command = ref AllocRenderCommand(RenderCommandType.DrawBatch);
|
|
|
|
command.DrawBatch.Indexed = false;
|
|
command.DrawBatch.StartIndex = BatchVertexIndex;
|
|
command.DrawBatch.PrimitiveType = MapDrawToBatchPrimitiveType(primitiveTopology);
|
|
command.DrawBatch.TextureId = textureId;
|
|
command.DrawBatch.ShaderInstance = _queuedShader;
|
|
|
|
command.DrawBatch.Count = vertices.Length;
|
|
command.DrawBatch.ModelMatrix = _currentMatrixModel;
|
|
|
|
_debugStats.LastBatches += 1;
|
|
_debugStats.LastClydeDrawCalls += 1;
|
|
BatchVertexIndex += vertices.Length;
|
|
}
|
|
|
|
private static BatchPrimitiveType MapDrawToBatchPrimitiveType(DrawPrimitiveTopology topology)
|
|
{
|
|
return topology switch
|
|
{
|
|
DrawPrimitiveTopology.TriangleList => BatchPrimitiveType.TriangleList,
|
|
DrawPrimitiveTopology.TriangleFan => BatchPrimitiveType.TriangleFan,
|
|
DrawPrimitiveTopology.TriangleStrip => BatchPrimitiveType.TriangleStrip,
|
|
DrawPrimitiveTopology.LineList => BatchPrimitiveType.LineList,
|
|
DrawPrimitiveTopology.LineStrip => BatchPrimitiveType.LineStrip,
|
|
DrawPrimitiveTopology.LineLoop => BatchPrimitiveType.LineLoop,
|
|
DrawPrimitiveTopology.PointList => BatchPrimitiveType.PointList,
|
|
_ => BatchPrimitiveType.TriangleList
|
|
};
|
|
}
|
|
|
|
private void DrawLine(Vector2 a, Vector2 b, Color color)
|
|
{
|
|
EnsureBatchSpaceAvailable(2, 0);
|
|
EnsureBatchState(_stockTextureWhite.TextureId, false, BatchPrimitiveType.LineList, _queuedShader);
|
|
|
|
a = Vector2.Transform(a, _currentMatrixModel);
|
|
b = Vector2.Transform(b, _currentMatrixModel);
|
|
|
|
// TODO: split batch if necessary.
|
|
var vIdx = BatchVertexIndex;
|
|
BatchVertexData[vIdx + 0] = new Vertex2D(a, Vector2.Zero, color);
|
|
BatchVertexData[vIdx + 1] = new Vertex2D(b, Vector2.Zero, color);
|
|
BatchVertexIndex += 2;
|
|
|
|
_debugStats.LastClydeDrawCalls += 1;
|
|
}
|
|
|
|
private void EnsureBatchSpaceAvailable(int vtx, int idx)
|
|
{
|
|
if (BatchVertexIndex + vtx >= BatchVertexData.Length || BatchIndexIndex + idx > BatchIndexData.Length)
|
|
{
|
|
FlushBatchQueue();
|
|
}
|
|
}
|
|
|
|
private void DrawSetScissor(UIBox2i? scissorBox)
|
|
{
|
|
BreakBatch();
|
|
|
|
ref var command = ref AllocRenderCommand(RenderCommandType.Scissor);
|
|
|
|
command.Scissor.EnableScissor = scissorBox.HasValue;
|
|
if (scissorBox.HasValue)
|
|
{
|
|
command.Scissor.Scissor = scissorBox.Value;
|
|
}
|
|
|
|
_currentScissorState = scissorBox;
|
|
}
|
|
|
|
private void DrawUseShader(ClydeShaderInstance instance)
|
|
{
|
|
_queuedShaderInstance = instance;
|
|
}
|
|
|
|
private void DrawClear(Color color, int stencil, ClearBufferMask mask)
|
|
{
|
|
BreakBatch();
|
|
|
|
ref var command = ref AllocRenderCommand(RenderCommandType.Clear);
|
|
|
|
command.Clear.Color = color;
|
|
command.Clear.Stencil = stencil;
|
|
command.Clear.Mask = mask;
|
|
}
|
|
|
|
private void DrawViewport(Box2i viewport)
|
|
{
|
|
BreakBatch();
|
|
|
|
ref var command = ref AllocRenderCommand(RenderCommandType.Viewport);
|
|
|
|
command.Viewport.Viewport = viewport;
|
|
}
|
|
|
|
private void DrawRenderTarget(ClydeHandle handle)
|
|
{
|
|
BreakBatch();
|
|
|
|
ref var command = ref AllocRenderCommand(RenderCommandType.RenderTarget);
|
|
|
|
command.RenderTarget.RenderTarget = handle;
|
|
|
|
_currentRenderTarget = _renderTargets[handle];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures that batching metadata matches the current batch.
|
|
/// If not, the current batch is finished and a new one is started.
|
|
/// </summary>
|
|
private void EnsureBatchState(ClydeHandle textureId, bool indexed,
|
|
BatchPrimitiveType primitiveType, ClydeHandle shaderInstance)
|
|
{
|
|
if (_batchMetaData.HasValue)
|
|
{
|
|
var metaData = _batchMetaData.Value;
|
|
if (metaData.TextureId == textureId &&
|
|
indexed == metaData.Indexed &&
|
|
metaData.PrimitiveType == primitiveType &&
|
|
metaData.ShaderInstance == shaderInstance)
|
|
{
|
|
// Data matches, don't have to do anything.
|
|
return;
|
|
}
|
|
|
|
// Data does not match. Finish batch...
|
|
FinishBatch();
|
|
}
|
|
|
|
// ... and start another.
|
|
_batchMetaData = new BatchMetaData(textureId, indexed, primitiveType,
|
|
indexed ? BatchIndexIndex : BatchVertexIndex, shaderInstance);
|
|
|
|
/*
|
|
if (textureId != default)
|
|
{
|
|
_batchLoadedTexture = _loadedTextures[textureId];
|
|
}
|
|
else
|
|
{
|
|
_batchLoadedTexture = null;
|
|
}
|
|
*/
|
|
}
|
|
|
|
private void FinishBatch()
|
|
{
|
|
if (!_batchMetaData.HasValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var metaData = _batchMetaData.Value;
|
|
|
|
var indexed = metaData.Indexed;
|
|
var currentIndex = indexed ? BatchIndexIndex : BatchVertexIndex;
|
|
|
|
ref var command = ref AllocRenderCommand(RenderCommandType.DrawBatch);
|
|
|
|
command.DrawBatch.Indexed = indexed;
|
|
command.DrawBatch.StartIndex = metaData.StartIndex;
|
|
command.DrawBatch.PrimitiveType = metaData.PrimitiveType;
|
|
command.DrawBatch.TextureId = metaData.TextureId;
|
|
command.DrawBatch.ShaderInstance = metaData.ShaderInstance;
|
|
|
|
command.DrawBatch.Count = currentIndex - metaData.StartIndex;
|
|
command.DrawBatch.ModelMatrix = Matrix3x2.Identity;
|
|
|
|
_debugStats.LastBatches += 1;
|
|
}
|
|
|
|
private static TKStencilOp ToGLStencilOp(StencilOp op)
|
|
{
|
|
return op switch
|
|
{
|
|
StencilOp.Keep => TKStencilOp.Keep,
|
|
StencilOp.Zero => TKStencilOp.Zero,
|
|
StencilOp.Replace => TKStencilOp.Replace,
|
|
StencilOp.IncrementClamp => TKStencilOp.Incr,
|
|
StencilOp.IncrementWrap => TKStencilOp.IncrWrap,
|
|
StencilOp.DecrementClamp => TKStencilOp.Decr,
|
|
StencilOp.DecrementWrap => TKStencilOp.DecrWrap,
|
|
StencilOp.Invert => TKStencilOp.Invert,
|
|
_ => throw new NotSupportedException()
|
|
};
|
|
}
|
|
|
|
private static StencilFunction ToGLStencilFunc(StencilFunc op)
|
|
{
|
|
return op switch
|
|
{
|
|
StencilFunc.Never => StencilFunction.Never,
|
|
StencilFunc.Always => StencilFunction.Always,
|
|
StencilFunc.Less => StencilFunction.Less,
|
|
StencilFunc.LessOrEqual => StencilFunction.Lequal,
|
|
StencilFunc.Greater => StencilFunction.Greater,
|
|
StencilFunc.GreaterOrEqual => StencilFunction.Gequal,
|
|
StencilFunc.NotEqual => StencilFunction.Notequal,
|
|
StencilFunc.Equal => StencilFunction.Equal,
|
|
_ => throw new NotSupportedException()
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renderer state that cannot be changed mid-batch has been modified and a new batch will have to be started.
|
|
/// </summary>
|
|
private void BreakBatch()
|
|
{
|
|
FinishBatch();
|
|
|
|
_batchMetaData = null;
|
|
}
|
|
|
|
private FullStoredRendererState PushRenderStateFull()
|
|
{
|
|
return new FullStoredRendererState(
|
|
_currentMatrixProj,
|
|
_currentMatrixView,
|
|
_currentBoundRenderTarget,
|
|
_currentRenderTarget,
|
|
_queuedShaderInstance,
|
|
_currentScissorState,
|
|
_glCaps);
|
|
}
|
|
|
|
private void PopRenderStateFull(in FullStoredRendererState state)
|
|
{
|
|
SetProjViewFull(state.ProjMatrix, state.ViewMatrix);
|
|
BindRenderTargetImmediate(state.BoundRenderTarget);
|
|
|
|
_queuedShaderInstance = state.QueuedShaderInstance;
|
|
_currentRenderTarget = state.RenderTarget;
|
|
var (width, height) = state.BoundRenderTarget.Size;
|
|
GL.Viewport(0, 0, width, height);
|
|
|
|
IsStencilling = (state.GLCaps & GLCaps.Stencilling) == GLCaps.Stencilling;
|
|
IsBlending = (state.GLCaps & GLCaps.Blending) == GLCaps.Blending;
|
|
|
|
SetScissorFull(state.ScissorState);
|
|
|
|
GL.ClearStencil(0xFF);
|
|
GL.StencilMask(0xFF);
|
|
GL.Clear(ClearBufferMask.StencilBufferBit);
|
|
}
|
|
|
|
private void SetViewportImmediate(Box2i box)
|
|
{
|
|
GL.Viewport(box.Left, box.Bottom, box.Width, box.Height);
|
|
CheckGlError();
|
|
}
|
|
|
|
private void ClearRenderState()
|
|
{
|
|
BatchVertexIndex = 0;
|
|
BatchIndexIndex = 0;
|
|
_queuedRenderCommands.Clear();
|
|
_currentViewport = null;
|
|
_lightingReady = false;
|
|
_currentMatrixModel = Matrix3x2.Identity;
|
|
SetScissorFull(null);
|
|
BindRenderTargetFull(_mainWindow!.RenderTarget);
|
|
_batchMetaData = null;
|
|
_queuedShaderInstance = _defaultShader;
|
|
|
|
GL.Viewport(0, 0, _mainWindow!.FramebufferSize.X, _mainWindow!.FramebufferSize.Y);
|
|
}
|
|
|
|
private void SetBlendFunc(ShaderBlendMode blend)
|
|
{
|
|
switch (blend)
|
|
{
|
|
case ShaderBlendMode.Add:
|
|
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.DstAlpha);
|
|
break;
|
|
case ShaderBlendMode.Subtract:
|
|
GL.BlendFuncSeparate(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.DstAlpha, BlendingFactorSrc.Zero, BlendingFactorDest.DstAlpha);
|
|
GL.BlendEquation(BlendEquationMode.FuncReverseSubtract);
|
|
break;
|
|
case ShaderBlendMode.Multiply:
|
|
GL.BlendFunc(BlendingFactor.DstColor, BlendingFactor.OneMinusSrcAlpha);
|
|
break;
|
|
case ShaderBlendMode.None:
|
|
GL.BlendFunc(BlendingFactor.One, BlendingFactor.Zero);
|
|
break;
|
|
case ShaderBlendMode.Normal:
|
|
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ResetBlendFunc()
|
|
{
|
|
GL.BlendFuncSeparate(
|
|
BlendingFactorSrc.SrcAlpha,
|
|
BlendingFactorDest.OneMinusSrcAlpha,
|
|
BlendingFactorSrc.One,
|
|
BlendingFactorDest.OneMinusSrcAlpha);
|
|
}
|
|
|
|
private void FenceRenderTarget(RenderTargetBase rt)
|
|
{
|
|
if (!_hasGLFenceSync || !rt.MakeGLFence)
|
|
return;
|
|
|
|
if (rt.LastGLSync != 0)
|
|
GL.DeleteSync(rt.LastGLSync);
|
|
|
|
rt.LastGLSync = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Explicit)]
|
|
private struct RenderCommand
|
|
{
|
|
// Use a tagged union to store all render commands.
|
|
// This significantly improves performance vs doing sum types via inheritance.
|
|
// Also means I don't have to declare a pool for every command type.
|
|
[FieldOffset(0)] public RenderCommandType Type;
|
|
|
|
[FieldOffset(4)] public RenderCommandDrawBatch DrawBatch;
|
|
[FieldOffset(4)] public RenderCommandProjViewMatrix ProjView;
|
|
[FieldOffset(4)] public RenderCommandScissor Scissor;
|
|
[FieldOffset(4)] public RenderCommandRenderTarget RenderTarget;
|
|
[FieldOffset(4)] public RenderCommandViewport Viewport;
|
|
[FieldOffset(4)] public RenderCommandClear Clear;
|
|
}
|
|
|
|
private struct RenderCommandDrawBatch
|
|
{
|
|
public ClydeHandle TextureId;
|
|
public ClydeHandle ShaderInstance;
|
|
|
|
public int StartIndex;
|
|
public int Count;
|
|
public bool Indexed;
|
|
public BatchPrimitiveType PrimitiveType;
|
|
|
|
// TODO: this makes the render commands so much more large please remove.
|
|
public Matrix3x2 ModelMatrix;
|
|
}
|
|
|
|
private struct RenderCommandProjViewMatrix
|
|
{
|
|
public Matrix3x2 ProjMatrix;
|
|
public Matrix3x2 ViewMatrix;
|
|
}
|
|
|
|
private struct RenderCommandScissor
|
|
{
|
|
public bool EnableScissor;
|
|
public UIBox2i Scissor;
|
|
}
|
|
|
|
private struct RenderCommandRenderTarget
|
|
{
|
|
public ClydeHandle RenderTarget;
|
|
}
|
|
|
|
private struct RenderCommandViewport
|
|
{
|
|
public Box2i Viewport;
|
|
}
|
|
|
|
private struct RenderCommandClear
|
|
{
|
|
public Color Color;
|
|
public int Stencil;
|
|
public ClearBufferMask Mask;
|
|
}
|
|
|
|
private enum RenderCommandType : byte
|
|
{
|
|
DrawBatch,
|
|
|
|
ProjViewMatrix,
|
|
|
|
//ResetViewMatrix,
|
|
//SwitchSpace,
|
|
Viewport,
|
|
|
|
Scissor,
|
|
RenderTarget,
|
|
|
|
Clear
|
|
}
|
|
|
|
private struct PopDebugGroup : IDisposable
|
|
{
|
|
private readonly Clyde _clyde;
|
|
|
|
public PopDebugGroup(Clyde clyde)
|
|
{
|
|
_clyde = clyde;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_clyde.PopDebugGroupMaybe();
|
|
}
|
|
}
|
|
|
|
private readonly struct BatchMetaData
|
|
{
|
|
public readonly ClydeHandle TextureId;
|
|
public readonly bool Indexed;
|
|
public readonly BatchPrimitiveType PrimitiveType;
|
|
public readonly int StartIndex;
|
|
public readonly ClydeHandle ShaderInstance;
|
|
|
|
public BatchMetaData(ClydeHandle textureId, bool indexed, BatchPrimitiveType primitiveType,
|
|
int startIndex, ClydeHandle shaderInstance)
|
|
{
|
|
TextureId = textureId;
|
|
Indexed = indexed;
|
|
PrimitiveType = primitiveType;
|
|
StartIndex = startIndex;
|
|
ShaderInstance = shaderInstance;
|
|
}
|
|
}
|
|
|
|
private enum BatchPrimitiveType : byte
|
|
{
|
|
TriangleList,
|
|
TriangleFan,
|
|
TriangleStrip,
|
|
LineList,
|
|
LineStrip,
|
|
LineLoop,
|
|
PointList,
|
|
}
|
|
|
|
private readonly struct FullStoredRendererState
|
|
{
|
|
public readonly Matrix3x2 ProjMatrix;
|
|
public readonly Matrix3x2 ViewMatrix;
|
|
public readonly LoadedRenderTarget BoundRenderTarget;
|
|
public readonly LoadedRenderTarget RenderTarget;
|
|
public readonly ClydeShaderInstance QueuedShaderInstance;
|
|
|
|
public readonly UIBox2i? ScissorState;
|
|
|
|
public readonly GLCaps GLCaps;
|
|
|
|
public FullStoredRendererState(
|
|
in Matrix3x2 projMatrix,
|
|
in Matrix3x2 viewMatrix,
|
|
LoadedRenderTarget boundRenderTarget,
|
|
LoadedRenderTarget renderTarget,
|
|
ClydeShaderInstance queuedShaderInstance,
|
|
UIBox2i? scissorState,
|
|
GLCaps glcaps
|
|
)
|
|
{
|
|
ProjMatrix = projMatrix;
|
|
ViewMatrix = viewMatrix;
|
|
BoundRenderTarget = boundRenderTarget;
|
|
RenderTarget = renderTarget;
|
|
QueuedShaderInstance = queuedShaderInstance;
|
|
|
|
ScissorState = scissorState;
|
|
GLCaps = glcaps;
|
|
}
|
|
}
|
|
|
|
[Flags]
|
|
private enum GLCaps : ushort
|
|
{
|
|
// If you add flags here make sure to update PopRenderState!
|
|
None = 0,
|
|
|
|
Blending = 1 << 0,
|
|
|
|
Stencilling = 1 << 2,
|
|
}
|
|
}
|
|
}
|