diff --git a/Ryujinx.Graphics.GAL/Extents2DF.cs b/Ryujinx.Graphics.GAL/Extents2DF.cs new file mode 100644 index 0000000000..8fb263c479 --- /dev/null +++ b/Ryujinx.Graphics.GAL/Extents2DF.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.Graphics.GAL +{ + public struct Extents2DF + { + public float X1 { get; } + public float Y1 { get; } + public float X2 { get; } + public float Y2 { get; } + + public Extents2DF(float x1, float y1, float x2, float y2) + { + X1 = x1; + Y1 = y1; + X2 = x2; + Y2 = y2; + } + } +} \ No newline at end of file diff --git a/Ryujinx.Graphics.GAL/IPipeline.cs b/Ryujinx.Graphics.GAL/IPipeline.cs index bfc432b1cf..b7da2c2167 100644 --- a/Ryujinx.Graphics.GAL/IPipeline.cs +++ b/Ryujinx.Graphics.GAL/IPipeline.cs @@ -32,6 +32,7 @@ namespace Ryujinx.Graphics.GAL int firstIndex, int firstVertex, int firstInstance); + void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion); void EndTransformFeedback(); diff --git a/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs index 47ceeb7d55..6111e32ceb 100644 --- a/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs +++ b/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs @@ -143,6 +143,8 @@ namespace Ryujinx.Graphics.GAL.Multithreading DrawCommand.Run(ref GetCommand(memory), threaded, renderer); _lookup[(int)CommandType.DrawIndexed] = (Span memory, ThreadedRenderer threaded, IRenderer renderer) => DrawIndexedCommand.Run(ref GetCommand(memory), threaded, renderer); + _lookup[(int)CommandType.DrawTexture] = (Span memory, ThreadedRenderer threaded, IRenderer renderer) => + DrawTextureCommand.Run(ref GetCommand(memory), threaded, renderer); _lookup[(int)CommandType.EndHostConditionalRendering] = (Span memory, ThreadedRenderer threaded, IRenderer renderer) => EndHostConditionalRenderingCommand.Run(renderer); _lookup[(int)CommandType.EndTransformFeedback] = (Span memory, ThreadedRenderer threaded, IRenderer renderer) => diff --git a/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs b/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs index ac73a3febe..4bceaa1edc 100644 --- a/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs +++ b/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs @@ -53,6 +53,7 @@ DispatchCompute, Draw, DrawIndexed, + DrawTexture, EndHostConditionalRendering, EndTransformFeedback, MultiDrawIndirectCount, diff --git a/Ryujinx.Graphics.GAL/Multithreading/Commands/DrawTextureCommand.cs b/Ryujinx.Graphics.GAL/Multithreading/Commands/DrawTextureCommand.cs new file mode 100644 index 0000000000..41a852bb33 --- /dev/null +++ b/Ryujinx.Graphics.GAL/Multithreading/Commands/DrawTextureCommand.cs @@ -0,0 +1,31 @@ +using Ryujinx.Graphics.GAL.Multithreading.Model; +using Ryujinx.Graphics.GAL.Multithreading.Resources; + +namespace Ryujinx.Graphics.GAL.Multithreading.Commands +{ + struct DrawTextureCommand : IGALCommand + { + public CommandType CommandType => CommandType.DrawTexture; + private TableRef _texture; + private TableRef _sampler; + private Extents2DF _srcRegion; + private Extents2DF _dstRegion; + + public void Set(TableRef texture, TableRef sampler, Extents2DF srcRegion, Extents2DF dstRegion) + { + _texture = texture; + _sampler = sampler; + _srcRegion = srcRegion; + _dstRegion = dstRegion; + } + + public static void Run(ref DrawTextureCommand command, ThreadedRenderer threaded, IRenderer renderer) + { + renderer.Pipeline.DrawTexture( + command._texture.GetAs(threaded)?.Base, + command._sampler.GetAs(threaded)?.Base, + command._srcRegion, + command._dstRegion); + } + } +} diff --git a/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs b/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs index 3c39a77f94..63a29b1bed 100644 --- a/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs +++ b/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs @@ -83,6 +83,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading _renderer.QueueCommand(); } + public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion) + { + _renderer.New().Set(Ref(texture), Ref(sampler), srcRegion, dstRegion); + _renderer.QueueCommand(); + } + public void EndHostConditionalRendering() { _renderer.New(); diff --git a/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs b/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs index a060c6c961..518e71ade5 100644 --- a/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs +++ b/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs @@ -319,6 +319,65 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _drawState.DrawIndexed = oldDrawIndexed; } + /// + /// Performs a texture draw with a source texture and sampler ID, along with source + /// and destination coordinates and sizes. + /// + /// 3D engine where this method is being called + /// Method call argument + public void DrawTexture(ThreedClass engine, int argument) + { + static float FixedToFloat(int fixedValue) + { + return fixedValue * (1f / 4096); + } + + float dstX0 = FixedToFloat(_state.State.DrawTextureDstX); + float dstY0 = FixedToFloat(_state.State.DrawTextureDstY); + float dstWidth = FixedToFloat(_state.State.DrawTextureDstWidth); + float dstHeight = FixedToFloat(_state.State.DrawTextureDstHeight); + + // TODO: Confirm behaviour on hardware. + // When this is active, the origin appears to be on the bottom. + if (_state.State.YControl.HasFlag(YControl.NegateY)) + { + dstY0 -= dstHeight; + } + + float dstX1 = dstX0 + dstWidth; + float dstY1 = dstY0 + dstHeight; + + float srcX0 = FixedToFloat(_state.State.DrawTextureSrcX); + float srcY0 = FixedToFloat(_state.State.DrawTextureSrcY); + float srcX1 = ((float)_state.State.DrawTextureDuDx / (1UL << 32)) * dstWidth + srcX0; + float srcY1 = ((float)_state.State.DrawTextureDvDy / (1UL << 32)) * dstHeight + srcY0; + + engine.UpdateState(); + + int textureId = _state.State.DrawTextureTextureId; + int samplerId = _state.State.DrawTextureSamplerId; + + (var texture, var sampler) = _channel.TextureManager.GetGraphicsTextureAndSampler(textureId, samplerId); + + srcX0 *= texture.ScaleFactor; + srcY0 *= texture.ScaleFactor; + srcX1 *= texture.ScaleFactor; + srcY1 *= texture.ScaleFactor; + + float dstScale = _channel.TextureManager.RenderTargetScale; + + dstX0 *= dstScale; + dstY0 *= dstScale; + dstX1 *= dstScale; + dstY1 *= dstScale; + + _context.Renderer.Pipeline.DrawTexture( + texture?.HostTexture, + sampler?.HostSampler, + new Extents2DF(srcX0, srcY0, srcX1, srcY1), + new Extents2DF(dstX0, dstY0, dstX1, dstY1)); + } + /// /// Performs a indirect multi-draw, with parameters from a GPU buffer. /// diff --git a/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs b/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs index 5478704acb..f3061c73ad 100644 --- a/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs +++ b/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClass.cs @@ -36,6 +36,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed { nameof(ThreedClassState.SyncpointAction), new RwCallback(IncrementSyncpoint, null) }, { nameof(ThreedClassState.TextureBarrier), new RwCallback(TextureBarrier, null) }, { nameof(ThreedClassState.TextureBarrierTiled), new RwCallback(TextureBarrierTiled, null) }, + { nameof(ThreedClassState.DrawTextureSrcY), new RwCallback(DrawTexture, null) }, { nameof(ThreedClassState.VbElementU8), new RwCallback(VbElementU8, null) }, { nameof(ThreedClassState.VbElementU16), new RwCallback(VbElementU16, null) }, { nameof(ThreedClassState.VbElementU32), new RwCallback(VbElementU32, null) }, @@ -251,6 +252,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed _context.Renderer.Pipeline.TextureBarrierTiled(); } + /// + /// Draws a texture, without needing to specify shader programs. + /// + /// Method call argument + private void DrawTexture(int argument) + { + _drawManager.DrawTexture(this, argument); + } + /// /// Pushes four 8-bit index buffer elements. /// diff --git a/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs b/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs index 58bc0957fb..165f507216 100644 --- a/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs +++ b/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs @@ -743,7 +743,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed public fixed uint ReservedF94[19]; public RtDepthStencilState RtDepthStencilState; public ScreenScissorState ScreenScissorState; - public fixed uint ReservedFFC[89]; + public fixed uint ReservedFFC[33]; + public int DrawTextureDstX; + public int DrawTextureDstY; + public int DrawTextureDstWidth; + public int DrawTextureDstHeight; + public long DrawTextureDuDx; + public long DrawTextureDvDy; + public int DrawTextureSamplerId; + public int DrawTextureTextureId; + public int DrawTextureSrcX; + public int DrawTextureSrcY; + public fixed uint Reserved10B0[44]; public Array16 VertexAttribState; public fixed uint Reserved11A0[31]; public RtControl RtControl; diff --git a/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs b/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs index 5862ea712e..621dc2e740 100644 --- a/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs +++ b/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs @@ -201,6 +201,22 @@ namespace Ryujinx.Graphics.Gpu.Image } } + /// + /// Gets a texture and a sampler from their respective pools from a texture ID and a sampler ID. + /// + /// ID of the texture + /// ID of the sampler + public (Texture, Sampler) GetTextureAndSampler(int textureId, int samplerId) + { + ulong texturePoolAddress = _texturePoolAddress; + + TexturePool texturePool = texturePoolAddress != 0 + ? _texturePoolCache.FindOrCreate(_channel, texturePoolAddress, _texturePoolMaximumId) + : null; + + return (texturePool.Get(textureId), _samplerPool.Get(samplerId)); + } + /// /// Updates the texture scale for a given texture or image. /// diff --git a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index a6373872b9..70cb57d09a 100644 --- a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -308,6 +308,16 @@ namespace Ryujinx.Graphics.Gpu.Image RenderTargetScale = targetScale; } + /// + /// Gets a texture and a sampler from their respective pools from a texture ID and a sampler ID. + /// + /// ID of the texture + /// ID of the sampler + public (Texture, Sampler) GetGraphicsTextureAndSampler(int textureId, int samplerId) + { + return _gpBindingsManager.GetTextureAndSampler(textureId, samplerId); + } + /// /// Commits bindings on the compute pipeline. /// diff --git a/Ryujinx.Graphics.OpenGL/DrawTextureEmulation.cs b/Ryujinx.Graphics.OpenGL/DrawTextureEmulation.cs new file mode 100644 index 0000000000..509e20fe25 --- /dev/null +++ b/Ryujinx.Graphics.OpenGL/DrawTextureEmulation.cs @@ -0,0 +1,138 @@ +using OpenTK.Graphics.OpenGL; +using Ryujinx.Graphics.OpenGL.Image; +using System; + +namespace Ryujinx.Graphics.OpenGL +{ + class DrawTextureEmulation + { + private const string VertexShader = @"#version 430 core + +uniform float srcX0; +uniform float srcY0; +uniform float srcX1; +uniform float srcY1; + +layout (location = 0) out vec2 texcoord; + +void main() +{ + bool x1 = (gl_VertexID & 1) != 0; + bool y1 = (gl_VertexID & 2) != 0; + gl_Position = vec4(x1 ? 1 : -1, y1 ? -1 : 1, 0, 1); + texcoord = vec2(x1 ? srcX1 : srcX0, y1 ? srcY1 : srcY0); +}"; + + private const string FragmentShader = @"#version 430 core + +layout (location = 0) uniform sampler2D tex; + +layout (location = 0) in vec2 texcoord; +layout (location = 0) out vec4 colour; + +void main() +{ + colour = texture(tex, texcoord); +}"; + + private int _vsHandle; + private int _fsHandle; + private int _programHandle; + private int _uniformSrcX0Location; + private int _uniformSrcY0Location; + private int _uniformSrcX1Location; + private int _uniformSrcY1Location; + private bool _initialized; + + public void Draw( + TextureView texture, + Sampler sampler, + float x0, + float y0, + float x1, + float y1, + float s0, + float t0, + float s1, + float t1) + { + EnsureInitialized(); + + GL.UseProgram(_programHandle); + + texture.Bind(0); + sampler.Bind(0); + + if (x0 > x1) + { + float temp = s0; + s0 = s1; + s1 = temp; + } + + if (y0 > y1) + { + float temp = t0; + t0 = t1; + t1 = temp; + } + + GL.Uniform1(_uniformSrcX0Location, s0); + GL.Uniform1(_uniformSrcY0Location, t0); + GL.Uniform1(_uniformSrcX1Location, s1); + GL.Uniform1(_uniformSrcY1Location, t1); + + GL.ViewportIndexed(0, MathF.Min(x0, x1), MathF.Min(y0, y1), MathF.Abs(x1 - x0), MathF.Abs(y1 - y0)); + + GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4); + } + + private void EnsureInitialized() + { + if (_initialized) + { + return; + } + + _initialized = true; + + _vsHandle = GL.CreateShader(ShaderType.VertexShader); + _fsHandle = GL.CreateShader(ShaderType.FragmentShader); + + GL.ShaderSource(_vsHandle, VertexShader); + GL.ShaderSource(_fsHandle, FragmentShader); + + GL.CompileShader(_vsHandle); + GL.CompileShader(_fsHandle); + + _programHandle = GL.CreateProgram(); + + GL.AttachShader(_programHandle, _vsHandle); + GL.AttachShader(_programHandle, _fsHandle); + + GL.LinkProgram(_programHandle); + + GL.DetachShader(_programHandle, _vsHandle); + GL.DetachShader(_programHandle, _fsHandle); + + _uniformSrcX0Location = GL.GetUniformLocation(_programHandle, "srcX0"); + _uniformSrcY0Location = GL.GetUniformLocation(_programHandle, "srcY0"); + _uniformSrcX1Location = GL.GetUniformLocation(_programHandle, "srcX1"); + _uniformSrcY1Location = GL.GetUniformLocation(_programHandle, "srcY1"); + } + + public void Dispose() + { + if (!_initialized) + { + return; + } + + GL.DeleteShader(_vsHandle); + GL.DeleteShader(_fsHandle); + GL.DeleteProgram(_programHandle); + + _initialized = false; + } + } +} diff --git a/Ryujinx.Graphics.OpenGL/HwCapabilities.cs b/Ryujinx.Graphics.OpenGL/HwCapabilities.cs index ec9bd4a201..773c9f634e 100644 --- a/Ryujinx.Graphics.OpenGL/HwCapabilities.cs +++ b/Ryujinx.Graphics.OpenGL/HwCapabilities.cs @@ -6,6 +6,7 @@ namespace Ryujinx.Graphics.OpenGL static class HwCapabilities { private static readonly Lazy _supportsAstcCompression = new Lazy(() => HasExtension("GL_KHR_texture_compression_astc_ldr")); + private static readonly Lazy _supportsDrawTexture = new Lazy(() => HasExtension("GL_NV_draw_texture")); private static readonly Lazy _supportsFragmentShaderInterlock = new Lazy(() => HasExtension("GL_ARB_fragment_shader_interlock")); private static readonly Lazy _supportsFragmentShaderOrdering = new Lazy(() => HasExtension("GL_INTEL_fragment_shader_ordering")); private static readonly Lazy _supportsImageLoadFormatted = new Lazy(() => HasExtension("GL_EXT_shader_image_load_formatted")); @@ -43,6 +44,7 @@ namespace Ryujinx.Graphics.OpenGL public static bool UsePersistentBufferForFlush => _gpuVendor.Value == GpuVendor.AmdWindows || _gpuVendor.Value == GpuVendor.Nvidia; public static bool SupportsAstcCompression => _supportsAstcCompression.Value; + public static bool SupportsDrawTexture => _supportsDrawTexture.Value; public static bool SupportsFragmentShaderInterlock => _supportsFragmentShaderInterlock.Value; public static bool SupportsFragmentShaderOrdering => _supportsFragmentShaderOrdering.Value; public static bool SupportsImageLoadFormatted => _supportsImageLoadFormatted.Value; diff --git a/Ryujinx.Graphics.OpenGL/Pipeline.cs b/Ryujinx.Graphics.OpenGL/Pipeline.cs index d0a509b474..aafc4db80d 100644 --- a/Ryujinx.Graphics.OpenGL/Pipeline.cs +++ b/Ryujinx.Graphics.OpenGL/Pipeline.cs @@ -12,6 +12,8 @@ namespace Ryujinx.Graphics.OpenGL { class Pipeline : IPipeline, IDisposable { + private readonly DrawTextureEmulation _drawTexture; + internal ulong DrawCount { get; private set; } private Program _program; @@ -29,6 +31,12 @@ namespace Ryujinx.Graphics.OpenGL private int _stencilFrontMask; private bool _depthMask; + private bool _depthTestEnable; + private bool _stencilTestEnable; + private bool _cullEnable; + + private float[] _viewportArray = Array.Empty(); + private double[] _depthRangeArray = Array.Empty(); private int _boundDrawFramebuffer; private int _boundReadFramebuffer; @@ -47,6 +55,7 @@ namespace Ryujinx.Graphics.OpenGL private Vector4[] _renderScale = new Vector4[65]; private TextureBase _unit0Texture; + private Sampler _unit0Sampler; private FrontFaceDirection _frontFace; private ClipOrigin _clipOrigin; @@ -67,6 +76,7 @@ namespace Ryujinx.Graphics.OpenGL internal Pipeline() { + _drawTexture = new DrawTextureEmulation(); _rasterizerDiscard = false; _clipOrigin = ClipOrigin.LowerLeft; _clipDepthMode = ClipDepthMode.NegativeOneToOne; @@ -544,6 +554,91 @@ namespace Ryujinx.Graphics.OpenGL } } + public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion) + { + if (texture is TextureView view && sampler is Sampler samp) + { + if (HwCapabilities.SupportsDrawTexture) + { + GL.NV.DrawTexture( + view.Handle, + samp.Handle, + dstRegion.X1, + dstRegion.Y1, + dstRegion.X2, + dstRegion.Y2, + 0, + srcRegion.X1 / view.Width, + srcRegion.Y1 / view.Height, + srcRegion.X2 / view.Width, + srcRegion.Y2 / view.Height); + } + else + { + static void Disable(EnableCap cap, bool enabled) + { + if (enabled) + { + GL.Disable(cap); + } + } + + static void Enable(EnableCap cap, bool enabled) + { + if (enabled) + { + GL.Enable(cap); + } + } + + Disable(EnableCap.CullFace, _cullEnable); + Disable(EnableCap.StencilTest, _stencilTestEnable); + Disable(EnableCap.DepthTest, _depthTestEnable); + + if (_depthMask) + { + GL.DepthMask(false); + } + + if (_tfEnabled) + { + GL.EndTransformFeedback(); + } + + _drawTexture.Draw( + view, + samp, + dstRegion.X1, + dstRegion.Y1, + dstRegion.X2, + dstRegion.Y2, + srcRegion.X1 / view.Width, + srcRegion.Y1 / view.Height, + srcRegion.X2 / view.Width, + srcRegion.Y2 / view.Height); + + _program?.Bind(); + _unit0Sampler?.Bind(0); + + GL.ViewportArray(0, 1, _viewportArray); + + Enable(EnableCap.CullFace, _cullEnable); + Enable(EnableCap.StencilTest, _stencilTestEnable); + Enable(EnableCap.DepthTest, _depthTestEnable); + + if (_depthMask) + { + GL.DepthMask(true); + } + + if (_tfEnabled) + { + GL.BeginTransformFeedback(_tfTopology); + } + } + } + } + public void EndTransformFeedback() { GL.EndTransformFeedback(); @@ -754,10 +849,13 @@ namespace Ryujinx.Graphics.OpenGL GL.DepthMask(depthTest.WriteEnable); _depthMask = depthTest.WriteEnable; + _depthTestEnable = depthTest.TestEnable; } public void SetFaceCulling(bool enable, Face face) { + _cullEnable = enable; + if (!enable) { GL.Disable(EnableCap.CullFace); @@ -994,7 +1092,14 @@ namespace Ryujinx.Graphics.OpenGL return; } - ((Sampler)sampler).Bind(binding); + Sampler samp = (Sampler)sampler; + + if (binding == 0) + { + _unit0Sampler = samp; + } + + samp.Bind(binding); } public void SetScissor(int index, bool enable, int x, int y, int width, int height) @@ -1023,6 +1128,8 @@ namespace Ryujinx.Graphics.OpenGL public void SetStencilTest(StencilTestDescriptor stencilTest) { + _stencilTestEnable = stencilTest.TestEnable; + if (!stencilTest.TestEnable) { GL.Disable(EnableCap.StencilTest); @@ -1152,9 +1259,11 @@ namespace Ryujinx.Graphics.OpenGL public void SetViewports(int first, ReadOnlySpan viewports) { - float[] viewportArray = new float[viewports.Length * 4]; + Array.Resize(ref _viewportArray, viewports.Length * 4); + Array.Resize(ref _depthRangeArray, viewports.Length * 2); - double[] depthRangeArray = new double[viewports.Length * 2]; + float[] viewportArray = _viewportArray; + double[] depthRangeArray = _depthRangeArray; for (int index = 0; index < viewports.Length; index++) { @@ -1186,7 +1295,6 @@ namespace Ryujinx.Graphics.OpenGL SetOrigin(flipY ? ClipOrigin.UpperLeft : ClipOrigin.LowerLeft); GL.ViewportArray(first, viewports.Length, viewportArray); - GL.DepthRangeArray(first, viewports.Length, depthRangeArray); } @@ -1307,10 +1415,7 @@ namespace Ryujinx.Graphics.OpenGL private void PrepareForDispatch() { - if (_unit0Texture != null) - { - _unit0Texture.Bind(0); - } + _unit0Texture?.Bind(0); } private void PreDraw() @@ -1318,11 +1423,7 @@ namespace Ryujinx.Graphics.OpenGL DrawCount++; _vertexArray.Validate(); - - if (_unit0Texture != null) - { - _unit0Texture.Bind(0); - } + _unit0Texture?.Bind(0); } private void PostDraw() @@ -1438,6 +1539,7 @@ namespace Ryujinx.Graphics.OpenGL _activeConditionalRender?.ReleaseHostAccess(); _framebuffer?.Dispose(); _vertexArray?.Dispose(); + _drawTexture.Dispose(); } } }