2019-10-13 08:02:07 +02:00
|
|
|
using OpenTK.Graphics.OpenGL;
|
2019-12-29 00:45:33 +01:00
|
|
|
using Ryujinx.Common.Logging;
|
2019-10-13 08:02:07 +02:00
|
|
|
using Ryujinx.Graphics.GAL;
|
2020-05-23 11:46:09 +02:00
|
|
|
using Ryujinx.Graphics.OpenGL.Image;
|
2020-05-04 04:24:59 +02:00
|
|
|
using Ryujinx.Graphics.OpenGL.Queries;
|
2019-10-13 08:02:07 +02:00
|
|
|
using Ryujinx.Graphics.Shader;
|
|
|
|
using System;
|
|
|
|
|
|
|
|
namespace Ryujinx.Graphics.OpenGL
|
|
|
|
{
|
2019-12-31 23:09:49 +01:00
|
|
|
class Pipeline : IPipeline, IDisposable
|
2019-10-13 08:02:07 +02:00
|
|
|
{
|
|
|
|
private Program _program;
|
|
|
|
|
2020-04-07 11:19:45 +02:00
|
|
|
private bool _rasterizerDiscard;
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
private VertexArray _vertexArray;
|
|
|
|
private Framebuffer _framebuffer;
|
|
|
|
|
|
|
|
private IntPtr _indexBaseOffset;
|
|
|
|
|
|
|
|
private DrawElementsType _elementsType;
|
|
|
|
|
|
|
|
private PrimitiveType _primitiveType;
|
|
|
|
|
2020-03-29 14:48:39 +02:00
|
|
|
private int _stencilFrontMask;
|
2019-10-13 08:02:07 +02:00
|
|
|
private bool _depthMask;
|
|
|
|
private bool _depthTest;
|
|
|
|
private bool _hasDepthBuffer;
|
|
|
|
|
2020-05-24 15:44:12 +02:00
|
|
|
private int _boundDrawFramebuffer;
|
|
|
|
private int _boundReadFramebuffer;
|
|
|
|
|
2020-07-26 05:03:40 +02:00
|
|
|
private int[] _fpIsBgra = new int[8];
|
2020-11-02 20:53:23 +01:00
|
|
|
private float[] _fpRenderScale = new float[65];
|
|
|
|
private float[] _cpRenderScale = new float[64];
|
2020-07-07 04:41:07 +02:00
|
|
|
|
2020-04-25 15:02:18 +02:00
|
|
|
private TextureBase _unit0Texture;
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2020-09-20 00:46:49 +02:00
|
|
|
private FrontFaceDirection _frontFace;
|
2020-03-29 14:48:39 +02:00
|
|
|
private ClipOrigin _clipOrigin;
|
2019-12-07 05:54:28 +01:00
|
|
|
private ClipDepthMode _clipDepthMode;
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2020-05-23 11:46:09 +02:00
|
|
|
private readonly uint[] _componentMasks;
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2020-04-07 11:19:45 +02:00
|
|
|
private bool _scissor0Enable = false;
|
2020-03-29 05:02:58 +02:00
|
|
|
|
2020-07-15 05:01:10 +02:00
|
|
|
private bool _tfEnabled;
|
2020-10-25 21:23:42 +01:00
|
|
|
private TransformFeedbackPrimitiveType _tfTopology;
|
|
|
|
|
|
|
|
private readonly BufferHandle[] _tfbs;
|
|
|
|
private readonly BufferRange[] _tfbTargets;
|
2020-07-15 05:01:10 +02:00
|
|
|
|
2020-09-20 00:46:49 +02:00
|
|
|
private ColorF _blendConstant;
|
2020-04-25 15:00:43 +02:00
|
|
|
|
2019-10-18 04:41:18 +02:00
|
|
|
internal Pipeline()
|
2019-10-13 08:02:07 +02:00
|
|
|
{
|
2020-04-07 11:19:45 +02:00
|
|
|
_rasterizerDiscard = false;
|
2020-03-29 14:48:39 +02:00
|
|
|
_clipOrigin = ClipOrigin.LowerLeft;
|
2019-12-07 05:54:28 +01:00
|
|
|
_clipDepthMode = ClipDepthMode.NegativeOneToOne;
|
2020-05-23 11:46:09 +02:00
|
|
|
|
|
|
|
_componentMasks = new uint[Constants.MaxRenderTargets];
|
|
|
|
|
|
|
|
for (int index = 0; index < Constants.MaxRenderTargets; index++)
|
|
|
|
{
|
|
|
|
_componentMasks[index] = 0xf;
|
|
|
|
}
|
2020-07-07 04:41:07 +02:00
|
|
|
|
|
|
|
for (int index = 0; index < _fpRenderScale.Length; index++)
|
|
|
|
{
|
|
|
|
_fpRenderScale[index] = 1f;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int index = 0; index < _cpRenderScale.Length; index++)
|
|
|
|
{
|
|
|
|
_cpRenderScale[index] = 1f;
|
|
|
|
}
|
2020-10-25 21:23:42 +01:00
|
|
|
|
|
|
|
_tfbs = new BufferHandle[Constants.MaxTransformFeedbackBuffers];
|
|
|
|
_tfbTargets = new BufferRange[Constants.MaxTransformFeedbackBuffers];
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
2020-01-12 23:12:40 +01:00
|
|
|
|
|
|
|
public void Barrier()
|
|
|
|
{
|
|
|
|
GL.MemoryBarrier(MemoryBarrierFlags.AllBarrierBits);
|
|
|
|
}
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2020-07-15 05:01:10 +02:00
|
|
|
public void BeginTransformFeedback(PrimitiveTopology topology)
|
|
|
|
{
|
2020-10-25 21:23:42 +01:00
|
|
|
GL.BeginTransformFeedback(_tfTopology = topology.ConvertToTfType());
|
2020-07-15 05:01:10 +02:00
|
|
|
_tfEnabled = true;
|
|
|
|
}
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
public void ClearRenderTargetColor(int index, uint componentMask, ColorF color)
|
|
|
|
{
|
|
|
|
GL.ColorMask(
|
|
|
|
index,
|
|
|
|
(componentMask & 1) != 0,
|
|
|
|
(componentMask & 2) != 0,
|
|
|
|
(componentMask & 4) != 0,
|
|
|
|
(componentMask & 8) != 0);
|
|
|
|
|
|
|
|
float[] colors = new float[] { color.Red, color.Green, color.Blue, color.Alpha };
|
|
|
|
|
|
|
|
GL.ClearBuffer(ClearBuffer.Color, index, colors);
|
|
|
|
|
|
|
|
RestoreComponentMask(index);
|
2020-03-29 14:48:39 +02:00
|
|
|
|
|
|
|
_framebuffer.SignalModified();
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
|
2019-12-29 18:41:50 +01:00
|
|
|
public void ClearRenderTargetDepthStencil(float depthValue, bool depthMask, int stencilValue, int stencilMask)
|
2019-10-13 08:02:07 +02:00
|
|
|
{
|
|
|
|
bool stencilMaskChanged =
|
|
|
|
stencilMask != 0 &&
|
|
|
|
stencilMask != _stencilFrontMask;
|
|
|
|
|
|
|
|
bool depthMaskChanged = depthMask && depthMask != _depthMask;
|
|
|
|
|
|
|
|
if (stencilMaskChanged)
|
|
|
|
{
|
|
|
|
GL.StencilMaskSeparate(StencilFace.Front, stencilMask);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (depthMaskChanged)
|
|
|
|
{
|
|
|
|
GL.DepthMask(depthMask);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (depthMask && stencilMask != 0)
|
|
|
|
{
|
|
|
|
GL.ClearBuffer(ClearBufferCombined.DepthStencil, 0, depthValue, stencilValue);
|
|
|
|
}
|
|
|
|
else if (depthMask)
|
|
|
|
{
|
|
|
|
GL.ClearBuffer(ClearBuffer.Depth, 0, ref depthValue);
|
|
|
|
}
|
|
|
|
else if (stencilMask != 0)
|
|
|
|
{
|
|
|
|
GL.ClearBuffer(ClearBuffer.Stencil, 0, ref stencilValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stencilMaskChanged)
|
|
|
|
{
|
|
|
|
GL.StencilMaskSeparate(StencilFace.Front, _stencilFrontMask);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (depthMaskChanged)
|
|
|
|
{
|
|
|
|
GL.DepthMask(_depthMask);
|
|
|
|
}
|
2020-03-29 14:48:39 +02:00
|
|
|
|
|
|
|
_framebuffer.SignalModified();
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
|
2020-05-23 11:46:09 +02:00
|
|
|
public void CopyBuffer(BufferHandle source, BufferHandle destination, int srcOffset, int dstOffset, int size)
|
|
|
|
{
|
|
|
|
Buffer.Copy(source, destination, srcOffset, dstOffset, size);
|
|
|
|
}
|
|
|
|
|
2019-12-29 18:41:50 +01:00
|
|
|
public void DispatchCompute(int groupsX, int groupsY, int groupsZ)
|
2019-10-18 04:41:18 +02:00
|
|
|
{
|
2019-11-27 04:41:22 +01:00
|
|
|
if (!_program.IsLinked)
|
|
|
|
{
|
2020-08-04 01:32:53 +02:00
|
|
|
Logger.Debug?.Print(LogClass.Gpu, "Dispatch error, shader not linked.");
|
2019-11-27 04:41:22 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
PrepareForDispatch();
|
|
|
|
|
2019-10-18 04:41:18 +02:00
|
|
|
GL.DispatchCompute(groupsX, groupsY, groupsZ);
|
|
|
|
}
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance)
|
|
|
|
{
|
|
|
|
if (!_program.IsLinked)
|
|
|
|
{
|
2020-08-04 01:32:53 +02:00
|
|
|
Logger.Debug?.Print(LogClass.Gpu, "Draw error, shader not linked.");
|
2019-10-13 08:02:07 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-25 21:23:42 +01:00
|
|
|
PreDraw();
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2019-10-15 05:10:20 +02:00
|
|
|
if (_primitiveType == PrimitiveType.Quads)
|
2019-10-13 08:02:07 +02:00
|
|
|
{
|
2019-10-15 05:10:20 +02:00
|
|
|
DrawQuadsImpl(vertexCount, instanceCount, firstVertex, firstInstance);
|
|
|
|
}
|
|
|
|
else if (_primitiveType == PrimitiveType.QuadStrip)
|
|
|
|
{
|
|
|
|
DrawQuadStripImpl(vertexCount, instanceCount, firstVertex, firstInstance);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
DrawImpl(vertexCount, instanceCount, firstVertex, firstInstance);
|
|
|
|
}
|
2020-03-29 14:48:39 +02:00
|
|
|
|
2020-10-25 21:23:42 +01:00
|
|
|
PostDraw();
|
2019-10-15 05:10:20 +02:00
|
|
|
}
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2019-10-15 05:10:20 +02:00
|
|
|
private void DrawQuadsImpl(
|
|
|
|
int vertexCount,
|
|
|
|
int instanceCount,
|
|
|
|
int firstVertex,
|
|
|
|
int firstInstance)
|
|
|
|
{
|
|
|
|
// TODO: Instanced rendering.
|
|
|
|
int quadsCount = vertexCount / 4;
|
|
|
|
|
|
|
|
int[] firsts = new int[quadsCount];
|
|
|
|
int[] counts = new int[quadsCount];
|
|
|
|
|
|
|
|
for (int quadIndex = 0; quadIndex < quadsCount; quadIndex++)
|
|
|
|
{
|
|
|
|
firsts[quadIndex] = firstVertex + quadIndex * 4;
|
|
|
|
counts[quadIndex] = 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.MultiDrawArrays(
|
|
|
|
PrimitiveType.TriangleFan,
|
|
|
|
firsts,
|
|
|
|
counts,
|
|
|
|
quadsCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void DrawQuadStripImpl(
|
|
|
|
int vertexCount,
|
|
|
|
int instanceCount,
|
|
|
|
int firstVertex,
|
|
|
|
int firstInstance)
|
|
|
|
{
|
|
|
|
int quadsCount = (vertexCount - 2) / 2;
|
|
|
|
|
2020-01-12 23:14:50 +01:00
|
|
|
if (firstInstance != 0 || instanceCount != 1)
|
2019-10-15 05:10:20 +02:00
|
|
|
{
|
2020-01-12 23:14:50 +01:00
|
|
|
for (int quadIndex = 0; quadIndex < quadsCount; quadIndex++)
|
|
|
|
{
|
|
|
|
GL.DrawArraysInstancedBaseInstance(PrimitiveType.TriangleFan, firstVertex + quadIndex * 2, 4, instanceCount, firstInstance);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
int[] firsts = new int[quadsCount];
|
|
|
|
int[] counts = new int[quadsCount];
|
|
|
|
|
|
|
|
firsts[0] = firstVertex;
|
|
|
|
counts[0] = 4;
|
|
|
|
|
|
|
|
for (int quadIndex = 1; quadIndex < quadsCount; quadIndex++)
|
|
|
|
{
|
|
|
|
firsts[quadIndex] = firstVertex + quadIndex * 2;
|
|
|
|
counts[quadIndex] = 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.MultiDrawArrays(
|
|
|
|
PrimitiveType.TriangleFan,
|
|
|
|
firsts,
|
|
|
|
counts,
|
|
|
|
quadsCount);
|
2019-10-15 05:10:20 +02:00
|
|
|
}
|
|
|
|
}
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2019-10-15 05:10:20 +02:00
|
|
|
private void DrawImpl(
|
|
|
|
int vertexCount,
|
|
|
|
int instanceCount,
|
|
|
|
int firstVertex,
|
|
|
|
int firstInstance)
|
|
|
|
{
|
|
|
|
if (firstInstance == 0 && instanceCount == 1)
|
|
|
|
{
|
|
|
|
GL.DrawArrays(_primitiveType, firstVertex, vertexCount);
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
else if (firstInstance == 0)
|
|
|
|
{
|
|
|
|
GL.DrawArraysInstanced(_primitiveType, firstVertex, vertexCount, instanceCount);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.DrawArraysInstancedBaseInstance(
|
|
|
|
_primitiveType,
|
|
|
|
firstVertex,
|
|
|
|
vertexCount,
|
|
|
|
instanceCount,
|
|
|
|
firstInstance);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void DrawIndexed(
|
|
|
|
int indexCount,
|
|
|
|
int instanceCount,
|
|
|
|
int firstIndex,
|
|
|
|
int firstVertex,
|
|
|
|
int firstInstance)
|
|
|
|
{
|
|
|
|
if (!_program.IsLinked)
|
|
|
|
{
|
2020-08-04 01:32:53 +02:00
|
|
|
Logger.Debug?.Print(LogClass.Gpu, "Draw error, shader not linked.");
|
2019-10-13 08:02:07 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-25 21:23:42 +01:00
|
|
|
PreDraw();
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2019-10-15 05:10:20 +02:00
|
|
|
int indexElemSize = 1;
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
switch (_elementsType)
|
|
|
|
{
|
2019-10-15 05:10:20 +02:00
|
|
|
case DrawElementsType.UnsignedShort: indexElemSize = 2; break;
|
2020-03-29 14:48:39 +02:00
|
|
|
case DrawElementsType.UnsignedInt: indexElemSize = 4; break;
|
2019-10-15 05:10:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
IntPtr indexBaseOffset = _indexBaseOffset + firstIndex * indexElemSize;
|
|
|
|
|
|
|
|
if (_primitiveType == PrimitiveType.Quads)
|
|
|
|
{
|
|
|
|
DrawQuadsIndexedImpl(
|
|
|
|
indexCount,
|
|
|
|
instanceCount,
|
|
|
|
indexBaseOffset,
|
|
|
|
indexElemSize,
|
|
|
|
firstVertex,
|
|
|
|
firstInstance);
|
|
|
|
}
|
|
|
|
else if (_primitiveType == PrimitiveType.QuadStrip)
|
|
|
|
{
|
|
|
|
DrawQuadStripIndexedImpl(
|
|
|
|
indexCount,
|
|
|
|
instanceCount,
|
|
|
|
indexBaseOffset,
|
|
|
|
indexElemSize,
|
|
|
|
firstVertex,
|
|
|
|
firstInstance);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
DrawIndexedImpl(
|
|
|
|
indexCount,
|
|
|
|
instanceCount,
|
|
|
|
indexBaseOffset,
|
|
|
|
firstVertex,
|
|
|
|
firstInstance);
|
|
|
|
}
|
2020-03-29 14:48:39 +02:00
|
|
|
|
2020-10-25 21:23:42 +01:00
|
|
|
PostDraw();
|
2019-10-15 05:10:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private void DrawQuadsIndexedImpl(
|
2020-03-29 14:48:39 +02:00
|
|
|
int indexCount,
|
|
|
|
int instanceCount,
|
2019-10-15 05:10:20 +02:00
|
|
|
IntPtr indexBaseOffset,
|
2020-03-29 14:48:39 +02:00
|
|
|
int indexElemSize,
|
|
|
|
int firstVertex,
|
|
|
|
int firstInstance)
|
2019-10-15 05:10:20 +02:00
|
|
|
{
|
|
|
|
int quadsCount = indexCount / 4;
|
|
|
|
|
2020-01-12 23:14:50 +01:00
|
|
|
if (firstInstance != 0 || instanceCount != 1)
|
|
|
|
{
|
|
|
|
if (firstVertex != 0 && firstInstance != 0)
|
|
|
|
{
|
|
|
|
for (int quadIndex = 0; quadIndex < quadsCount; quadIndex++)
|
|
|
|
{
|
|
|
|
GL.DrawElementsInstancedBaseVertexBaseInstance(
|
|
|
|
PrimitiveType.TriangleFan,
|
|
|
|
4,
|
|
|
|
_elementsType,
|
|
|
|
indexBaseOffset + quadIndex * 4 * indexElemSize,
|
|
|
|
instanceCount,
|
|
|
|
firstVertex,
|
|
|
|
firstInstance);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (firstInstance != 0)
|
|
|
|
{
|
|
|
|
for (int quadIndex = 0; quadIndex < quadsCount; quadIndex++)
|
|
|
|
{
|
|
|
|
GL.DrawElementsInstancedBaseInstance(
|
|
|
|
PrimitiveType.TriangleFan,
|
|
|
|
4,
|
|
|
|
_elementsType,
|
|
|
|
indexBaseOffset + quadIndex * 4 * indexElemSize,
|
|
|
|
instanceCount,
|
|
|
|
firstInstance);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for (int quadIndex = 0; quadIndex < quadsCount; quadIndex++)
|
|
|
|
{
|
|
|
|
GL.DrawElementsInstanced(
|
|
|
|
PrimitiveType.TriangleFan,
|
|
|
|
4,
|
|
|
|
_elementsType,
|
|
|
|
indexBaseOffset + quadIndex * 4 * indexElemSize,
|
|
|
|
instanceCount);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
IntPtr[] indices = new IntPtr[quadsCount];
|
2019-10-15 05:10:20 +02:00
|
|
|
|
2020-01-12 23:14:50 +01:00
|
|
|
int[] counts = new int[quadsCount];
|
2019-10-15 05:10:20 +02:00
|
|
|
|
2020-01-12 23:14:50 +01:00
|
|
|
int[] baseVertices = new int[quadsCount];
|
2019-10-15 05:10:20 +02:00
|
|
|
|
2020-01-12 23:14:50 +01:00
|
|
|
for (int quadIndex = 0; quadIndex < quadsCount; quadIndex++)
|
|
|
|
{
|
|
|
|
indices[quadIndex] = indexBaseOffset + quadIndex * 4 * indexElemSize;
|
2019-10-15 05:10:20 +02:00
|
|
|
|
2020-01-12 23:14:50 +01:00
|
|
|
counts[quadIndex] = 4;
|
2019-10-15 05:10:20 +02:00
|
|
|
|
2020-01-12 23:14:50 +01:00
|
|
|
baseVertices[quadIndex] = firstVertex;
|
|
|
|
}
|
2019-10-15 05:10:20 +02:00
|
|
|
|
2020-01-12 23:14:50 +01:00
|
|
|
GL.MultiDrawElementsBaseVertex(
|
|
|
|
PrimitiveType.TriangleFan,
|
|
|
|
counts,
|
|
|
|
_elementsType,
|
|
|
|
indices,
|
|
|
|
quadsCount,
|
|
|
|
baseVertices);
|
|
|
|
}
|
2019-10-15 05:10:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private void DrawQuadStripIndexedImpl(
|
2020-03-29 14:48:39 +02:00
|
|
|
int indexCount,
|
|
|
|
int instanceCount,
|
2019-10-15 05:10:20 +02:00
|
|
|
IntPtr indexBaseOffset,
|
2020-03-29 14:48:39 +02:00
|
|
|
int indexElemSize,
|
|
|
|
int firstVertex,
|
|
|
|
int firstInstance)
|
2019-10-15 05:10:20 +02:00
|
|
|
{
|
|
|
|
// TODO: Instanced rendering.
|
|
|
|
int quadsCount = (indexCount - 2) / 2;
|
|
|
|
|
|
|
|
IntPtr[] indices = new IntPtr[quadsCount];
|
|
|
|
|
|
|
|
int[] counts = new int[quadsCount];
|
|
|
|
|
|
|
|
int[] baseVertices = new int[quadsCount];
|
|
|
|
|
|
|
|
indices[0] = indexBaseOffset;
|
|
|
|
|
|
|
|
counts[0] = 4;
|
|
|
|
|
|
|
|
baseVertices[0] = firstVertex;
|
|
|
|
|
|
|
|
for (int quadIndex = 1; quadIndex < quadsCount; quadIndex++)
|
|
|
|
{
|
|
|
|
indices[quadIndex] = indexBaseOffset + quadIndex * 2 * indexElemSize;
|
|
|
|
|
|
|
|
counts[quadIndex] = 4;
|
|
|
|
|
|
|
|
baseVertices[quadIndex] = firstVertex;
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
|
2019-10-15 05:10:20 +02:00
|
|
|
GL.MultiDrawElementsBaseVertex(
|
|
|
|
PrimitiveType.TriangleFan,
|
|
|
|
counts,
|
|
|
|
_elementsType,
|
|
|
|
indices,
|
|
|
|
quadsCount,
|
|
|
|
baseVertices);
|
|
|
|
}
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2019-10-15 05:10:20 +02:00
|
|
|
private void DrawIndexedImpl(
|
2020-03-29 14:48:39 +02:00
|
|
|
int indexCount,
|
|
|
|
int instanceCount,
|
2019-10-15 05:10:20 +02:00
|
|
|
IntPtr indexBaseOffset,
|
2020-03-29 14:48:39 +02:00
|
|
|
int firstVertex,
|
|
|
|
int firstInstance)
|
2019-10-15 05:10:20 +02:00
|
|
|
{
|
2019-10-13 08:02:07 +02:00
|
|
|
if (firstInstance == 0 && firstVertex == 0 && instanceCount == 1)
|
|
|
|
{
|
|
|
|
GL.DrawElements(_primitiveType, indexCount, _elementsType, indexBaseOffset);
|
|
|
|
}
|
|
|
|
else if (firstInstance == 0 && instanceCount == 1)
|
|
|
|
{
|
|
|
|
GL.DrawElementsBaseVertex(
|
|
|
|
_primitiveType,
|
|
|
|
indexCount,
|
|
|
|
_elementsType,
|
|
|
|
indexBaseOffset,
|
|
|
|
firstVertex);
|
|
|
|
}
|
|
|
|
else if (firstInstance == 0 && firstVertex == 0)
|
|
|
|
{
|
|
|
|
GL.DrawElementsInstanced(
|
|
|
|
_primitiveType,
|
|
|
|
indexCount,
|
|
|
|
_elementsType,
|
|
|
|
indexBaseOffset,
|
|
|
|
instanceCount);
|
|
|
|
}
|
|
|
|
else if (firstInstance == 0)
|
|
|
|
{
|
|
|
|
GL.DrawElementsInstancedBaseVertex(
|
|
|
|
_primitiveType,
|
|
|
|
indexCount,
|
|
|
|
_elementsType,
|
|
|
|
indexBaseOffset,
|
|
|
|
instanceCount,
|
|
|
|
firstVertex);
|
|
|
|
}
|
|
|
|
else if (firstVertex == 0)
|
|
|
|
{
|
|
|
|
GL.DrawElementsInstancedBaseInstance(
|
|
|
|
_primitiveType,
|
|
|
|
indexCount,
|
|
|
|
_elementsType,
|
|
|
|
indexBaseOffset,
|
|
|
|
instanceCount,
|
|
|
|
firstInstance);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.DrawElementsInstancedBaseVertexBaseInstance(
|
|
|
|
_primitiveType,
|
|
|
|
indexCount,
|
|
|
|
_elementsType,
|
|
|
|
indexBaseOffset,
|
|
|
|
instanceCount,
|
|
|
|
firstVertex,
|
|
|
|
firstInstance);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-15 05:01:10 +02:00
|
|
|
public void EndTransformFeedback()
|
|
|
|
{
|
|
|
|
GL.EndTransformFeedback();
|
|
|
|
_tfEnabled = false;
|
|
|
|
}
|
|
|
|
|
2020-07-28 23:30:08 +02:00
|
|
|
public void SetAlphaTest(bool enable, float reference, CompareOp op)
|
|
|
|
{
|
|
|
|
if (!enable)
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.AlphaTest);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.AlphaFunc((AlphaFunction)op.Convert(), reference);
|
|
|
|
GL.Enable(EnableCap.AlphaTest);
|
|
|
|
}
|
|
|
|
|
2019-12-29 18:41:50 +01:00
|
|
|
public void SetBlendState(int index, BlendDescriptor blend)
|
2019-10-13 08:02:07 +02:00
|
|
|
{
|
2019-12-29 18:41:50 +01:00
|
|
|
if (!blend.Enable)
|
|
|
|
{
|
|
|
|
GL.Disable(IndexedEnableCap.Blend, index);
|
|
|
|
return;
|
|
|
|
}
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2019-12-29 18:41:50 +01:00
|
|
|
GL.BlendEquationSeparate(
|
|
|
|
index,
|
|
|
|
blend.ColorOp.Convert(),
|
|
|
|
blend.AlphaOp.Convert());
|
|
|
|
|
|
|
|
GL.BlendFuncSeparate(
|
|
|
|
index,
|
|
|
|
(BlendingFactorSrc)blend.ColorSrcFactor.Convert(),
|
|
|
|
(BlendingFactorDest)blend.ColorDstFactor.Convert(),
|
|
|
|
(BlendingFactorSrc)blend.AlphaSrcFactor.Convert(),
|
|
|
|
(BlendingFactorDest)blend.AlphaDstFactor.Convert());
|
|
|
|
|
2020-10-13 02:50:41 +02:00
|
|
|
static bool IsDualSource(BlendFactor factor)
|
|
|
|
{
|
|
|
|
switch (factor)
|
|
|
|
{
|
|
|
|
case BlendFactor.Src1Color:
|
|
|
|
case BlendFactor.Src1ColorGl:
|
|
|
|
case BlendFactor.Src1Alpha:
|
|
|
|
case BlendFactor.Src1AlphaGl:
|
|
|
|
case BlendFactor.OneMinusSrc1Color:
|
|
|
|
case BlendFactor.OneMinusSrc1ColorGl:
|
|
|
|
case BlendFactor.OneMinusSrc1Alpha:
|
|
|
|
case BlendFactor.OneMinusSrc1AlphaGl:
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
EnsureFramebuffer();
|
|
|
|
|
|
|
|
_framebuffer.SetDualSourceBlend(
|
|
|
|
IsDualSource(blend.ColorSrcFactor) ||
|
|
|
|
IsDualSource(blend.ColorDstFactor) ||
|
|
|
|
IsDualSource(blend.AlphaSrcFactor) ||
|
|
|
|
IsDualSource(blend.AlphaDstFactor));
|
|
|
|
|
2020-04-25 15:00:43 +02:00
|
|
|
if (_blendConstant != blend.BlendConstant)
|
|
|
|
{
|
|
|
|
_blendConstant = blend.BlendConstant;
|
|
|
|
|
|
|
|
GL.BlendColor(
|
|
|
|
blend.BlendConstant.Red,
|
|
|
|
blend.BlendConstant.Green,
|
|
|
|
blend.BlendConstant.Blue,
|
|
|
|
blend.BlendConstant.Alpha);
|
|
|
|
}
|
|
|
|
|
2019-12-29 18:41:50 +01:00
|
|
|
GL.Enable(IndexedEnableCap.Blend, index);
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void SetDepthBias(PolygonModeMask enables, float factor, float units, float clamp)
|
|
|
|
{
|
|
|
|
if ((enables & PolygonModeMask.Point) != 0)
|
|
|
|
{
|
|
|
|
GL.Enable(EnableCap.PolygonOffsetPoint);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.PolygonOffsetPoint);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((enables & PolygonModeMask.Line) != 0)
|
|
|
|
{
|
|
|
|
GL.Enable(EnableCap.PolygonOffsetLine);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.PolygonOffsetLine);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((enables & PolygonModeMask.Fill) != 0)
|
|
|
|
{
|
|
|
|
GL.Enable(EnableCap.PolygonOffsetFill);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.PolygonOffsetFill);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (enables == 0)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-26 23:11:28 +02:00
|
|
|
if (HwCapabilities.SupportsPolygonOffsetClamp)
|
|
|
|
{
|
|
|
|
GL.PolygonOffsetClamp(factor, units, clamp);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.PolygonOffset(factor, units);
|
|
|
|
}
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
|
2020-04-30 03:47:24 +02:00
|
|
|
public void SetDepthClamp(bool clamp)
|
2020-04-17 03:16:49 +02:00
|
|
|
{
|
|
|
|
if (!clamp)
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.DepthClamp);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.Enable(EnableCap.DepthClamp);
|
|
|
|
}
|
|
|
|
|
2019-12-07 05:54:28 +01:00
|
|
|
public void SetDepthMode(DepthMode mode)
|
|
|
|
{
|
|
|
|
ClipDepthMode depthMode = mode.Convert();
|
|
|
|
|
|
|
|
if (_clipDepthMode != depthMode)
|
|
|
|
{
|
|
|
|
_clipDepthMode = depthMode;
|
|
|
|
|
|
|
|
GL.ClipControl(_clipOrigin, depthMode);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
public void SetDepthTest(DepthTestDescriptor depthTest)
|
|
|
|
{
|
|
|
|
GL.DepthFunc((DepthFunction)depthTest.Func.Convert());
|
|
|
|
|
|
|
|
_depthMask = depthTest.WriteEnable;
|
|
|
|
_depthTest = depthTest.TestEnable;
|
|
|
|
|
|
|
|
UpdateDepthTest();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetFaceCulling(bool enable, Face face)
|
|
|
|
{
|
|
|
|
if (!enable)
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.CullFace);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.CullFace(face.Convert());
|
|
|
|
|
|
|
|
GL.Enable(EnableCap.CullFace);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetFrontFace(FrontFace frontFace)
|
|
|
|
{
|
2020-09-20 00:46:49 +02:00
|
|
|
SetFrontFace(_frontFace = frontFace.Convert());
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
public void SetImage(int binding, ITexture texture, Format imageFormat)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
if (texture == null)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
return;
|
|
|
|
}
|
2019-12-29 18:41:50 +01:00
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
TextureBase texBase = (TextureBase)texture;
|
2019-12-29 18:41:50 +01:00
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
SizedInternalFormat format = FormatTable.GetImageFormat(imageFormat);
|
|
|
|
|
|
|
|
if (format != 0)
|
|
|
|
{
|
|
|
|
GL.BindImageTexture(binding, texBase.Handle, 0, true, 0, TextureAccess.ReadWrite, format);
|
2019-12-29 18:41:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetIndexBuffer(BufferRange buffer, IndexType type)
|
|
|
|
{
|
|
|
|
_elementsType = type.Convert();
|
|
|
|
|
|
|
|
_indexBaseOffset = (IntPtr)buffer.Offset;
|
|
|
|
|
|
|
|
EnsureVertexArray();
|
|
|
|
|
2020-05-23 11:46:09 +02:00
|
|
|
_vertexArray.SetIndexBuffer(buffer.Handle);
|
2019-12-29 18:41:50 +01:00
|
|
|
}
|
|
|
|
|
2020-09-20 00:46:49 +02:00
|
|
|
public void SetLogicOpState(bool enable, LogicalOp op)
|
2020-05-28 01:03:07 +02:00
|
|
|
{
|
2020-09-20 00:46:49 +02:00
|
|
|
if (enable)
|
|
|
|
{
|
|
|
|
GL.Enable(EnableCap.ColorLogicOp);
|
2020-05-28 01:03:07 +02:00
|
|
|
|
2020-09-20 00:46:49 +02:00
|
|
|
GL.LogicOp((LogicOp)op.Convert());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.ColorLogicOp);
|
|
|
|
}
|
2020-05-28 01:03:07 +02:00
|
|
|
}
|
|
|
|
|
2020-07-21 02:59:13 +02:00
|
|
|
public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin)
|
2020-02-02 00:19:46 +01:00
|
|
|
{
|
2020-07-21 02:59:13 +02:00
|
|
|
// GL_POINT_SPRITE was deprecated in core profile 3.2+ and causes GL_INVALID_ENUM when set.
|
|
|
|
// As we don't know if the current context is core or compat, it's safer to keep this code.
|
|
|
|
if (enablePointSprite)
|
|
|
|
{
|
|
|
|
GL.Enable(EnableCap.PointSprite);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.PointSprite);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isProgramPointSize)
|
|
|
|
{
|
|
|
|
GL.Enable(EnableCap.ProgramPointSize);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.ProgramPointSize);
|
|
|
|
}
|
|
|
|
|
2020-07-26 05:03:40 +02:00
|
|
|
GL.PointParameter(origin == Origin.LowerLeft
|
|
|
|
? PointSpriteCoordOriginParameter.LowerLeft
|
2020-07-21 02:59:13 +02:00
|
|
|
: PointSpriteCoordOriginParameter.UpperLeft);
|
|
|
|
|
|
|
|
// Games seem to set point size to 0 which generates a GL_INVALID_VALUE
|
2020-07-26 05:03:40 +02:00
|
|
|
// From the spec, GL_INVALID_VALUE is generated if size is less than or equal to 0.
|
2020-07-21 02:59:13 +02:00
|
|
|
GL.PointSize(Math.Max(float.Epsilon, size));
|
2020-02-02 00:19:46 +01:00
|
|
|
}
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
public void SetPrimitiveRestart(bool enable, int index)
|
|
|
|
{
|
|
|
|
if (!enable)
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.PrimitiveRestart);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.PrimitiveRestartIndex(index);
|
|
|
|
|
|
|
|
GL.Enable(EnableCap.PrimitiveRestart);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetPrimitiveTopology(PrimitiveTopology topology)
|
|
|
|
{
|
|
|
|
_primitiveType = topology.Convert();
|
|
|
|
}
|
|
|
|
|
2019-12-29 18:41:50 +01:00
|
|
|
public void SetProgram(IProgram program)
|
|
|
|
{
|
|
|
|
_program = (Program)program;
|
2020-07-15 05:01:10 +02:00
|
|
|
|
|
|
|
if (_tfEnabled)
|
|
|
|
{
|
2020-10-25 21:23:42 +01:00
|
|
|
GL.EndTransformFeedback();
|
2020-07-15 05:01:10 +02:00
|
|
|
_program.Bind();
|
2020-10-25 21:23:42 +01:00
|
|
|
GL.BeginTransformFeedback(_tfTopology);
|
2020-07-15 05:01:10 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
_program.Bind();
|
|
|
|
}
|
2020-07-07 04:41:07 +02:00
|
|
|
|
2020-07-26 05:03:40 +02:00
|
|
|
UpdateFpIsBgra();
|
2020-07-07 04:41:07 +02:00
|
|
|
SetRenderTargetScale(_fpRenderScale[0]);
|
2019-12-29 18:41:50 +01:00
|
|
|
}
|
|
|
|
|
2020-04-07 11:19:45 +02:00
|
|
|
public void SetRasterizerDiscard(bool discard)
|
|
|
|
{
|
|
|
|
if (discard)
|
|
|
|
{
|
|
|
|
GL.Enable(EnableCap.RasterizerDiscard);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.RasterizerDiscard);
|
|
|
|
}
|
|
|
|
|
|
|
|
_rasterizerDiscard = discard;
|
|
|
|
}
|
|
|
|
|
2020-07-07 04:41:07 +02:00
|
|
|
public void SetRenderTargetScale(float scale)
|
|
|
|
{
|
|
|
|
_fpRenderScale[0] = scale;
|
|
|
|
|
|
|
|
if (_program != null && _program.FragmentRenderScaleUniform != -1)
|
|
|
|
{
|
|
|
|
GL.Uniform1(_program.FragmentRenderScaleUniform, 1, _fpRenderScale); // Just the first element.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-23 11:46:09 +02:00
|
|
|
public void SetRenderTargetColorMasks(ReadOnlySpan<uint> componentMasks)
|
2019-10-13 08:02:07 +02:00
|
|
|
{
|
|
|
|
for (int index = 0; index < componentMasks.Length; index++)
|
|
|
|
{
|
2020-05-23 11:46:09 +02:00
|
|
|
_componentMasks[index] = componentMasks[index];
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
RestoreComponentMask(index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetRenderTargets(ITexture[] colors, ITexture depthStencil)
|
|
|
|
{
|
|
|
|
EnsureFramebuffer();
|
|
|
|
|
|
|
|
for (int index = 0; index < colors.Length; index++)
|
|
|
|
{
|
|
|
|
TextureView color = (TextureView)colors[index];
|
|
|
|
|
|
|
|
_framebuffer.AttachColor(index, color);
|
2020-07-26 05:03:40 +02:00
|
|
|
|
|
|
|
_fpIsBgra[index] = color != null && color.Format.IsBgra8() ? 1 : 0;
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
|
2020-07-26 05:03:40 +02:00
|
|
|
UpdateFpIsBgra();
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
TextureView depthStencilView = (TextureView)depthStencil;
|
|
|
|
|
|
|
|
_framebuffer.AttachDepthStencil(depthStencilView);
|
|
|
|
_framebuffer.SetDrawBuffers(colors.Length);
|
|
|
|
|
|
|
|
_hasDepthBuffer = depthStencil != null && depthStencilView.Format != Format.S8Uint;
|
|
|
|
|
|
|
|
UpdateDepthTest();
|
|
|
|
}
|
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
public void SetSampler(int binding, ISampler sampler)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
if (sampler == null)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
return;
|
2019-12-29 18:41:50 +01:00
|
|
|
}
|
2020-11-08 12:10:00 +01:00
|
|
|
|
|
|
|
((Sampler)sampler).Bind(binding);
|
2019-12-29 18:41:50 +01:00
|
|
|
}
|
|
|
|
|
2020-03-29 05:02:58 +02:00
|
|
|
public void SetScissorEnable(int index, bool enable)
|
|
|
|
{
|
|
|
|
if (enable)
|
|
|
|
{
|
|
|
|
GL.Enable(IndexedEnableCap.ScissorTest, index);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(IndexedEnableCap.ScissorTest, index);
|
|
|
|
}
|
|
|
|
|
2020-04-07 11:19:45 +02:00
|
|
|
if (index == 0)
|
|
|
|
{
|
|
|
|
_scissor0Enable = enable;
|
|
|
|
}
|
2020-03-29 05:02:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void SetScissor(int index, int x, int y, int width, int height)
|
|
|
|
{
|
|
|
|
GL.ScissorIndexed(index, x, y, width, height);
|
|
|
|
}
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
public void SetStencilTest(StencilTestDescriptor stencilTest)
|
|
|
|
{
|
|
|
|
if (!stencilTest.TestEnable)
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.StencilTest);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.StencilOpSeparate(
|
|
|
|
StencilFace.Front,
|
|
|
|
stencilTest.FrontSFail.Convert(),
|
|
|
|
stencilTest.FrontDpFail.Convert(),
|
|
|
|
stencilTest.FrontDpPass.Convert());
|
|
|
|
|
|
|
|
GL.StencilFuncSeparate(
|
|
|
|
StencilFace.Front,
|
|
|
|
(StencilFunction)stencilTest.FrontFunc.Convert(),
|
|
|
|
stencilTest.FrontFuncRef,
|
|
|
|
stencilTest.FrontFuncMask);
|
|
|
|
|
|
|
|
GL.StencilMaskSeparate(StencilFace.Front, stencilTest.FrontMask);
|
|
|
|
|
|
|
|
GL.StencilOpSeparate(
|
|
|
|
StencilFace.Back,
|
|
|
|
stencilTest.BackSFail.Convert(),
|
|
|
|
stencilTest.BackDpFail.Convert(),
|
|
|
|
stencilTest.BackDpPass.Convert());
|
|
|
|
|
|
|
|
GL.StencilFuncSeparate(
|
|
|
|
StencilFace.Back,
|
|
|
|
(StencilFunction)stencilTest.BackFunc.Convert(),
|
|
|
|
stencilTest.BackFuncRef,
|
|
|
|
stencilTest.BackFuncMask);
|
|
|
|
|
|
|
|
GL.StencilMaskSeparate(StencilFace.Back, stencilTest.BackMask);
|
|
|
|
|
|
|
|
GL.Enable(EnableCap.StencilTest);
|
|
|
|
|
|
|
|
_stencilFrontMask = stencilTest.FrontMask;
|
|
|
|
}
|
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
public void SetStorageBuffers(ReadOnlySpan<BufferRange> buffers)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
SetBuffers(buffers, isStorage: true);
|
2019-12-29 18:41:50 +01:00
|
|
|
}
|
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
public void SetTexture(int binding, ITexture texture)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
if (texture == null)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
2019-12-29 18:41:50 +01:00
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
if (binding == 0)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
_unit0Texture = (TextureBase)texture;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
((TextureBase)texture).Bind(binding);
|
2019-12-29 18:41:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-25 21:23:42 +01:00
|
|
|
public void SetTransformFeedbackBuffers(ReadOnlySpan<BufferRange> buffers)
|
2020-07-15 05:01:10 +02:00
|
|
|
{
|
|
|
|
if (_tfEnabled)
|
|
|
|
{
|
2020-10-25 21:23:42 +01:00
|
|
|
GL.EndTransformFeedback();
|
2020-07-15 05:01:10 +02:00
|
|
|
}
|
2020-10-25 21:23:42 +01:00
|
|
|
|
|
|
|
int count = Math.Min(buffers.Length, Constants.MaxTransformFeedbackBuffers);
|
|
|
|
|
|
|
|
for (int i = 0; i < count; i++)
|
2020-07-15 05:01:10 +02:00
|
|
|
{
|
2020-10-25 21:23:42 +01:00
|
|
|
BufferRange buffer = buffers[i];
|
|
|
|
_tfbTargets[i] = buffer;
|
|
|
|
|
|
|
|
if (buffer.Handle == BufferHandle.Null)
|
|
|
|
{
|
|
|
|
GL.BindBufferBase(BufferRangeTarget.TransformFeedbackBuffer, i, 0);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_tfbs[i] == BufferHandle.Null)
|
|
|
|
{
|
|
|
|
_tfbs[i] = Buffer.Create();
|
|
|
|
}
|
|
|
|
|
|
|
|
Buffer.Resize(_tfbs[i], buffer.Size);
|
|
|
|
Buffer.Copy(buffer.Handle, _tfbs[i], buffer.Offset, 0, buffer.Size);
|
|
|
|
GL.BindBufferBase(BufferRangeTarget.TransformFeedbackBuffer, i, _tfbs[i].ToInt32());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_tfEnabled)
|
|
|
|
{
|
|
|
|
GL.BeginTransformFeedback(_tfTopology);
|
2020-07-15 05:01:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
public void SetUniformBuffers(ReadOnlySpan<BufferRange> buffers)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
SetBuffers(buffers, isStorage: false);
|
2019-12-29 18:41:50 +01:00
|
|
|
}
|
|
|
|
|
2020-05-04 04:04:49 +02:00
|
|
|
public void SetUserClipDistance(int index, bool enableClip)
|
|
|
|
{
|
|
|
|
if (!enableClip)
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.ClipDistance0 + index);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.Enable(EnableCap.ClipDistance0 + index);
|
|
|
|
}
|
|
|
|
|
2020-05-23 11:46:09 +02:00
|
|
|
public void SetVertexAttribs(ReadOnlySpan<VertexAttribDescriptor> vertexAttribs)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
|
|
|
EnsureVertexArray();
|
|
|
|
|
|
|
|
_vertexArray.SetVertexAttributes(vertexAttribs);
|
|
|
|
}
|
|
|
|
|
2020-05-23 11:46:09 +02:00
|
|
|
public void SetVertexBuffers(ReadOnlySpan<VertexBufferDescriptor> vertexBuffers)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
|
|
|
EnsureVertexArray();
|
|
|
|
|
|
|
|
_vertexArray.SetVertexBuffers(vertexBuffers);
|
|
|
|
}
|
|
|
|
|
2020-05-23 11:46:09 +02:00
|
|
|
public void SetViewports(int first, ReadOnlySpan<Viewport> viewports)
|
2019-10-13 08:02:07 +02:00
|
|
|
{
|
|
|
|
float[] viewportArray = new float[viewports.Length * 4];
|
|
|
|
|
|
|
|
double[] depthRangeArray = new double[viewports.Length * 2];
|
|
|
|
|
|
|
|
for (int index = 0; index < viewports.Length; index++)
|
|
|
|
{
|
|
|
|
int viewportElemIndex = index * 4;
|
|
|
|
|
|
|
|
Viewport viewport = viewports[index];
|
|
|
|
|
|
|
|
viewportArray[viewportElemIndex + 0] = viewport.Region.X;
|
2020-09-20 00:46:49 +02:00
|
|
|
viewportArray[viewportElemIndex + 1] = viewport.Region.Y + (viewport.Region.Height < 0 ? viewport.Region.Height : 0);
|
|
|
|
viewportArray[viewportElemIndex + 2] = viewport.Region.Width;
|
|
|
|
viewportArray[viewportElemIndex + 3] = MathF.Abs(viewport.Region.Height);
|
2019-10-13 08:02:07 +02:00
|
|
|
|
2020-05-28 01:03:07 +02:00
|
|
|
if (HwCapabilities.SupportsViewportSwizzle)
|
2019-10-13 08:02:07 +02:00
|
|
|
{
|
2020-05-28 01:03:07 +02:00
|
|
|
GL.NV.ViewportSwizzle(
|
|
|
|
index,
|
|
|
|
viewport.SwizzleX.Convert(),
|
|
|
|
viewport.SwizzleY.Convert(),
|
|
|
|
viewport.SwizzleZ.Convert(),
|
|
|
|
viewport.SwizzleW.Convert());
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
depthRangeArray[index * 2 + 0] = viewport.DepthNear;
|
|
|
|
depthRangeArray[index * 2 + 1] = viewport.DepthFar;
|
|
|
|
}
|
|
|
|
|
2020-09-20 00:46:49 +02:00
|
|
|
bool flipY = viewports.Length != 0 && viewports[0].Region.Height < 0;
|
|
|
|
|
|
|
|
SetOrigin(flipY ? ClipOrigin.UpperLeft : ClipOrigin.LowerLeft);
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
GL.ViewportArray(first, viewports.Length, viewportArray);
|
|
|
|
|
|
|
|
GL.DepthRangeArray(first, viewports.Length, depthRangeArray);
|
|
|
|
}
|
|
|
|
|
2019-10-18 04:41:18 +02:00
|
|
|
public void TextureBarrier()
|
|
|
|
{
|
|
|
|
GL.MemoryBarrier(MemoryBarrierFlags.TextureFetchBarrierBit);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void TextureBarrierTiled()
|
|
|
|
{
|
|
|
|
GL.MemoryBarrier(MemoryBarrierFlags.TextureFetchBarrierBit);
|
|
|
|
}
|
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
private void SetBuffers(ReadOnlySpan<BufferRange> buffers, bool isStorage)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
BufferRangeTarget target = isStorage ? BufferRangeTarget.ShaderStorageBuffer : BufferRangeTarget.UniformBuffer;
|
2019-12-29 18:41:50 +01:00
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
for (int index = 0; index < buffers.Length; index++)
|
2019-12-29 18:41:50 +01:00
|
|
|
{
|
2020-11-08 12:10:00 +01:00
|
|
|
BufferRange buffer = buffers[index];
|
2019-12-29 18:41:50 +01:00
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
if (buffer.Handle == BufferHandle.Null)
|
|
|
|
{
|
|
|
|
GL.BindBufferRange(target, index, 0, IntPtr.Zero, 0);
|
|
|
|
continue;
|
|
|
|
}
|
2019-12-29 18:41:50 +01:00
|
|
|
|
2020-11-08 12:10:00 +01:00
|
|
|
GL.BindBufferRange(target, index, buffer.Handle.ToInt32(), (IntPtr)buffer.Offset, buffer.Size);
|
2019-12-29 18:41:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
private void SetOrigin(ClipOrigin origin)
|
|
|
|
{
|
|
|
|
if (_clipOrigin != origin)
|
|
|
|
{
|
|
|
|
_clipOrigin = origin;
|
|
|
|
|
2019-12-07 05:54:28 +01:00
|
|
|
GL.ClipControl(origin, _clipDepthMode);
|
2020-09-20 00:46:49 +02:00
|
|
|
|
|
|
|
SetFrontFace(_frontFace);
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-20 00:46:49 +02:00
|
|
|
private void SetFrontFace(FrontFaceDirection frontFace)
|
|
|
|
{
|
|
|
|
// Changing clip origin will also change the front face to compensate
|
|
|
|
// for the flipped viewport, we flip it again here to compensate as
|
|
|
|
// this effect is undesirable for us.
|
|
|
|
if (_clipOrigin == ClipOrigin.UpperLeft)
|
|
|
|
{
|
|
|
|
frontFace = frontFace == FrontFaceDirection.Ccw ? FrontFaceDirection.Cw : FrontFaceDirection.Ccw;
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.FrontFace(frontFace);
|
|
|
|
}
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
private void EnsureVertexArray()
|
|
|
|
{
|
|
|
|
if (_vertexArray == null)
|
|
|
|
{
|
|
|
|
_vertexArray = new VertexArray();
|
|
|
|
|
|
|
|
_vertexArray.Bind();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void EnsureFramebuffer()
|
|
|
|
{
|
|
|
|
if (_framebuffer == null)
|
|
|
|
{
|
|
|
|
_framebuffer = new Framebuffer();
|
|
|
|
|
2020-05-24 15:44:12 +02:00
|
|
|
int boundHandle = _framebuffer.Bind();
|
|
|
|
_boundDrawFramebuffer = _boundReadFramebuffer = boundHandle;
|
2019-10-13 08:02:07 +02:00
|
|
|
|
|
|
|
GL.Enable(EnableCap.FramebufferSrgb);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-24 15:44:12 +02:00
|
|
|
internal (int drawHandle, int readHandle) GetBoundFramebuffers()
|
|
|
|
{
|
Memory Read/Write Tracking using Region Handles (#1272)
* WIP Range Tracking
- Texture invalidation seems to have large problems
- Buffer/Pool invalidation may have problems
- Mirror memory tracking puts an additional `add` in compiled code, we likely just want to make HLE access slower if this is the final solution.
- Native project is in the messiest possible location.
- [HACK] JIT memory access always uses native "fast" path
- [HACK] Trying some things with texture invalidation and views.
It works :)
Still a few hacks, messy things, slow things
More work in progress stuff (also move to memory project)
Quite a bit faster now.
- Unmapping GPU VA and CPU VA will now correctly update write tracking regions, and invalidate textures for the former.
- The Virtual range list is now non-overlapping like the physical one.
- Fixed some bugs where regions could leak.
- Introduced a weird bug that I still need to track down (consistent invalid buffer in MK8 ribbon road)
Move some stuff.
I think we'll eventually just put the dll and so for this in a nuget package.
Fix rebase.
[WIP] MultiRegionHandle variable size ranges
- Avoid reprotecting regions that change often (needs some tweaking)
- There's still a bug in buffers, somehow.
- Might want different api for minimum granularity
Fix rebase issue
Commit everything needed for software only tracking.
Remove native components.
Remove more native stuff.
Cleanup
Use a separate window for the background context, update opentk. (fixes linux)
Some experimental changes
Should get things working up to scratch - still need to try some things with flush/modification and res scale.
Include address with the region action.
Initial work to make range tracking work
Still a ton of bugs
Fix some issues with the new stuff.
* Fix texture flush instability
There's still some weird behaviour, but it's much improved without this. (textures with cpu modified data were flushing over it)
* Find the destination texture for Buffer->Texture full copy
Greatly improves performance for nvdec videos (with range tracking)
* Further improve texture tracking
* Disable Memory Tracking for view parents
This is a temporary approach to better match behaviour on master (where invalidations would be soaked up by views, rather than trigger twice)
The assumption is that when views are created to a texture, they will cover all of its data anyways. Of course, this can easily be improved in future.
* Introduce some tracking tests.
WIP
* Complete base tests.
* Add more tests for multiregion, fix existing test.
* Cleanup Part 1
* Remove unnecessary code from memory tracking
* Fix some inconsistencies with 3D texture rule.
* Add dispose tests.
* Use a background thread for the background context.
Rather than setting and unsetting a context as current, doing the work on a dedicated thread with signals seems to be a bit faster.
Also nerf the multithreading test a bit.
* Copy to texture with matching alignment
This extends the copy to work for some videos with unusual size, such as tutorial videos in SMO. It will only occur if the destination texture already exists at XCount size.
* Track reads for buffer copies. Synchronize new buffers before copying overlaps.
* Remove old texture flushing mechanisms.
Range tracking all the way, baby.
* Wake the background thread when disposing.
Avoids a deadlock when games are closed.
* Address Feedback 1
* Separate TextureCopy instance for background thread
Also `BackgroundContextWorker.InBackground` for a more sensible idenfifier for if we're in a background thread.
* Add missing XML docs.
* Address Feedback
* Maybe I should start drinking coffee.
* Some more feedback.
* Remove flush warning, Refocus window after making background context
2020-10-16 22:18:35 +02:00
|
|
|
if (BackgroundContextWorker.InBackground)
|
|
|
|
{
|
|
|
|
return (0, 0);
|
|
|
|
}
|
|
|
|
|
2020-05-24 15:44:12 +02:00
|
|
|
return (_boundDrawFramebuffer, _boundReadFramebuffer);
|
|
|
|
}
|
|
|
|
|
2020-07-26 05:03:40 +02:00
|
|
|
private void UpdateFpIsBgra()
|
|
|
|
{
|
|
|
|
if (_program != null)
|
|
|
|
{
|
|
|
|
GL.Uniform1(_program.FragmentIsBgraUniform, 8, _fpIsBgra);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
private void UpdateDepthTest()
|
|
|
|
{
|
|
|
|
// Enabling depth operations is only valid when we have
|
|
|
|
// a depth buffer, otherwise it's not allowed.
|
|
|
|
if (_hasDepthBuffer)
|
|
|
|
{
|
|
|
|
if (_depthTest)
|
|
|
|
{
|
|
|
|
GL.Enable(EnableCap.DepthTest);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.DepthTest);
|
|
|
|
}
|
|
|
|
|
|
|
|
GL.DepthMask(_depthMask);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
GL.Disable(EnableCap.DepthTest);
|
|
|
|
|
|
|
|
GL.DepthMask(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-02 20:53:23 +01:00
|
|
|
public void UpdateRenderScale(ShaderStage stage, float[] scales, int textureCount, int imageCount)
|
2020-07-26 05:03:40 +02:00
|
|
|
{
|
|
|
|
if (_program != null)
|
|
|
|
{
|
|
|
|
switch (stage)
|
|
|
|
{
|
|
|
|
case ShaderStage.Fragment:
|
|
|
|
if (_program.FragmentRenderScaleUniform != -1)
|
|
|
|
{
|
2020-11-02 20:53:23 +01:00
|
|
|
Array.Copy(scales, 0, _fpRenderScale, 1, textureCount + imageCount);
|
|
|
|
GL.Uniform1(_program.FragmentRenderScaleUniform, 1 + textureCount + imageCount, _fpRenderScale);
|
2020-07-26 05:03:40 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case ShaderStage.Compute:
|
|
|
|
if (_program.ComputeRenderScaleUniform != -1)
|
|
|
|
{
|
2020-11-02 20:53:23 +01:00
|
|
|
Array.Copy(scales, 0, _cpRenderScale, 0, textureCount + imageCount);
|
|
|
|
GL.Uniform1(_program.ComputeRenderScaleUniform, textureCount + imageCount, _cpRenderScale);
|
2020-07-26 05:03:40 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-27 04:41:22 +01:00
|
|
|
private void PrepareForDispatch()
|
|
|
|
{
|
|
|
|
if (_unit0Texture != null)
|
|
|
|
{
|
|
|
|
_unit0Texture.Bind(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-25 21:23:42 +01:00
|
|
|
private void PreDraw()
|
2019-10-13 08:02:07 +02:00
|
|
|
{
|
|
|
|
_vertexArray.Validate();
|
|
|
|
|
|
|
|
if (_unit0Texture != null)
|
|
|
|
{
|
|
|
|
_unit0Texture.Bind(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-25 21:23:42 +01:00
|
|
|
private void PostDraw()
|
|
|
|
{
|
|
|
|
_framebuffer?.SignalModified();
|
|
|
|
|
|
|
|
if (_tfEnabled)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < Constants.MaxTransformFeedbackBuffers; i++)
|
|
|
|
{
|
|
|
|
if (_tfbTargets[i].Handle != BufferHandle.Null)
|
|
|
|
{
|
|
|
|
Buffer.Copy(_tfbs[i], _tfbTargets[i].Handle, 0, _tfbTargets[i].Offset, _tfbTargets[i].Size);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-13 08:02:07 +02:00
|
|
|
private void RestoreComponentMask(int index)
|
|
|
|
{
|
2020-05-23 11:46:09 +02:00
|
|
|
GL.ColorMask(
|
|
|
|
index,
|
|
|
|
(_componentMasks[index] & 1u) != 0,
|
|
|
|
(_componentMasks[index] & 2u) != 0,
|
|
|
|
(_componentMasks[index] & 4u) != 0,
|
|
|
|
(_componentMasks[index] & 8u) != 0);
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
2019-12-31 23:09:49 +01:00
|
|
|
|
2020-04-07 11:19:45 +02:00
|
|
|
public void RestoreScissor0Enable()
|
2020-03-29 05:02:58 +02:00
|
|
|
{
|
2020-04-07 11:19:45 +02:00
|
|
|
if (_scissor0Enable)
|
2020-03-29 05:02:58 +02:00
|
|
|
{
|
2020-04-07 11:19:45 +02:00
|
|
|
GL.Enable(IndexedEnableCap.ScissorTest, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void RestoreRasterizerDiscard()
|
|
|
|
{
|
|
|
|
if (_rasterizerDiscard)
|
|
|
|
{
|
|
|
|
GL.Enable(EnableCap.RasterizerDiscard);
|
2020-03-29 05:02:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-04 04:24:59 +02:00
|
|
|
public bool TryHostConditionalRendering(ICounterEvent value, ulong compare, bool isEqual)
|
|
|
|
{
|
|
|
|
if (value is CounterQueueEvent)
|
|
|
|
{
|
|
|
|
// Compare an event and a constant value.
|
|
|
|
CounterQueueEvent evt = (CounterQueueEvent)value;
|
|
|
|
|
|
|
|
// Easy host conditional rendering when the check matches what GL can do:
|
|
|
|
// - Event is of type samples passed.
|
|
|
|
// - Result is not a combination of multiple queries.
|
|
|
|
// - Comparing against 0.
|
|
|
|
// - Event has not already been flushed.
|
|
|
|
|
|
|
|
if (evt.Disposed)
|
|
|
|
{
|
|
|
|
// If the event has been flushed, then just use the values on the CPU.
|
|
|
|
// The query object may already be repurposed for another draw (eg. begin + end).
|
2020-07-15 05:01:10 +02:00
|
|
|
return false;
|
2020-05-04 04:24:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (compare == 0 && evt.Type == QueryTarget.SamplesPassed && evt.ClearCounter)
|
|
|
|
{
|
|
|
|
GL.BeginConditionalRender(evt.Query, isEqual ? ConditionalRenderType.QueryNoWaitInverted : ConditionalRenderType.QueryNoWait);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-27 09:51:03 +02:00
|
|
|
// The GPU will flush the queries to CPU and evaluate the condition there instead.
|
|
|
|
|
|
|
|
GL.Flush(); // The thread will be stalled manually flushing the counter, so flush GL commands now.
|
2020-07-15 05:01:10 +02:00
|
|
|
return false;
|
2020-05-04 04:24:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public bool TryHostConditionalRendering(ICounterEvent value, ICounterEvent compare, bool isEqual)
|
|
|
|
{
|
2020-05-27 09:51:03 +02:00
|
|
|
GL.Flush(); // The GPU thread will be stalled manually flushing the counter, so flush GL commands now.
|
2020-05-04 04:24:59 +02:00
|
|
|
return false; // We don't currently have a way to compare two counters for conditional rendering.
|
|
|
|
}
|
|
|
|
|
|
|
|
public void EndHostConditionalRendering()
|
|
|
|
{
|
|
|
|
GL.EndConditionalRender();
|
|
|
|
}
|
|
|
|
|
2019-12-31 23:09:49 +01:00
|
|
|
public void Dispose()
|
|
|
|
{
|
2020-10-25 21:23:42 +01:00
|
|
|
for (int i = 0; i < Constants.MaxTransformFeedbackBuffers; i++)
|
|
|
|
{
|
|
|
|
if (_tfbs[i] != BufferHandle.Null)
|
|
|
|
{
|
|
|
|
Buffer.Delete(_tfbs[i]);
|
|
|
|
_tfbs[i] = BufferHandle.Null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-31 23:09:49 +01:00
|
|
|
_framebuffer?.Dispose();
|
|
|
|
_vertexArray?.Dispose();
|
|
|
|
}
|
2019-10-13 08:02:07 +02:00
|
|
|
}
|
|
|
|
}
|