Improved GPU command lists decoding (#499)
* Better implementation of the DMA pusher, misc fixes * Remove some debug code * Correct RGBX8 format * Add support for linked Texture Sampler Control * Attempt to fix upside down screen issue
This commit is contained in:
parent
b833183ef6
commit
d2bb458b51
25 changed files with 616 additions and 395 deletions
|
@ -409,9 +409,31 @@ namespace ChocolArm64.Memory
|
||||||
|
|
||||||
public void WriteBytes(long position, byte[] data)
|
public void WriteBytes(long position, byte[] data)
|
||||||
{
|
{
|
||||||
EnsureRangeIsValid(position, (uint)data.Length);
|
long endAddr = position + data.Length;
|
||||||
|
|
||||||
Marshal.Copy(data, 0, (IntPtr)TranslateWrite(position), data.Length);
|
if ((ulong)endAddr < (ulong)position)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
while ((ulong)position < (ulong)endAddr)
|
||||||
|
{
|
||||||
|
long pageLimit = (position + PageSize) & ~(long)PageMask;
|
||||||
|
|
||||||
|
if ((ulong)pageLimit > (ulong)endAddr)
|
||||||
|
{
|
||||||
|
pageLimit = endAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int copySize = (int)(pageLimit - position);
|
||||||
|
|
||||||
|
Marshal.Copy(data, offset, (IntPtr)TranslateWrite(position), copySize);
|
||||||
|
|
||||||
|
position += copySize;
|
||||||
|
offset += copySize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteBytes(long position, byte[] data, int startIndex, int size)
|
public void WriteBytes(long position, byte[] data, int startIndex, int size)
|
||||||
|
|
190
Ryujinx.Graphics/DmaPusher.cs
Normal file
190
Ryujinx.Graphics/DmaPusher.cs
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
using Ryujinx.Graphics.Memory;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Graphics
|
||||||
|
{
|
||||||
|
public class DmaPusher
|
||||||
|
{
|
||||||
|
private ConcurrentQueue<(NvGpuVmm, long)> IbBuffer;
|
||||||
|
|
||||||
|
private long DmaPut;
|
||||||
|
private long DmaGet;
|
||||||
|
|
||||||
|
private struct DmaState
|
||||||
|
{
|
||||||
|
public int Method;
|
||||||
|
public int SubChannel;
|
||||||
|
public int MethodCount;
|
||||||
|
public bool NonIncrementing;
|
||||||
|
public bool IncrementOnce;
|
||||||
|
public int LengthPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DmaState State;
|
||||||
|
|
||||||
|
private bool SliEnable;
|
||||||
|
private bool SliActive;
|
||||||
|
|
||||||
|
private bool IbEnable;
|
||||||
|
private bool NonMain;
|
||||||
|
|
||||||
|
private long DmaMGet;
|
||||||
|
|
||||||
|
private NvGpuVmm Vmm;
|
||||||
|
|
||||||
|
private NvGpu Gpu;
|
||||||
|
|
||||||
|
private AutoResetEvent Event;
|
||||||
|
|
||||||
|
public DmaPusher(NvGpu Gpu)
|
||||||
|
{
|
||||||
|
this.Gpu = Gpu;
|
||||||
|
|
||||||
|
IbBuffer = new ConcurrentQueue<(NvGpuVmm, long)>();
|
||||||
|
|
||||||
|
IbEnable = true;
|
||||||
|
|
||||||
|
Event = new AutoResetEvent(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Push(NvGpuVmm Vmm, long Entry)
|
||||||
|
{
|
||||||
|
IbBuffer.Enqueue((Vmm, Entry));
|
||||||
|
|
||||||
|
Event.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WaitForCommands()
|
||||||
|
{
|
||||||
|
return Event.WaitOne(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DispatchCalls()
|
||||||
|
{
|
||||||
|
while (Step());
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Step()
|
||||||
|
{
|
||||||
|
if (DmaGet != DmaPut)
|
||||||
|
{
|
||||||
|
int Word = Vmm.ReadInt32(DmaGet);
|
||||||
|
|
||||||
|
DmaGet += 4;
|
||||||
|
|
||||||
|
if (!NonMain)
|
||||||
|
{
|
||||||
|
DmaMGet = DmaGet;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (State.LengthPending != 0)
|
||||||
|
{
|
||||||
|
State.LengthPending = 0;
|
||||||
|
State.MethodCount = Word & 0xffffff;
|
||||||
|
}
|
||||||
|
else if (State.MethodCount != 0)
|
||||||
|
{
|
||||||
|
if (!SliEnable || SliActive)
|
||||||
|
{
|
||||||
|
CallMethod(Word);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!State.NonIncrementing)
|
||||||
|
{
|
||||||
|
State.Method++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (State.IncrementOnce)
|
||||||
|
{
|
||||||
|
State.NonIncrementing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
State.MethodCount--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int SumissionMode = (Word >> 29) & 7;
|
||||||
|
|
||||||
|
switch (SumissionMode)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
//Incrementing.
|
||||||
|
SetNonImmediateState(Word);
|
||||||
|
|
||||||
|
State.NonIncrementing = false;
|
||||||
|
State.IncrementOnce = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
//Non-incrementing.
|
||||||
|
SetNonImmediateState(Word);
|
||||||
|
|
||||||
|
State.NonIncrementing = true;
|
||||||
|
State.IncrementOnce = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
//Immediate.
|
||||||
|
State.Method = (Word >> 0) & 0x1fff;
|
||||||
|
State.SubChannel = (Word >> 13) & 7;
|
||||||
|
State.NonIncrementing = true;
|
||||||
|
State.IncrementOnce = false;
|
||||||
|
|
||||||
|
CallMethod((Word >> 16) & 0x1fff);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
//Increment-once.
|
||||||
|
SetNonImmediateState(Word);
|
||||||
|
|
||||||
|
State.NonIncrementing = false;
|
||||||
|
State.IncrementOnce = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (IbEnable && IbBuffer.TryDequeue(out (NvGpuVmm Vmm, long Entry) Tuple))
|
||||||
|
{
|
||||||
|
this.Vmm = Tuple.Vmm;
|
||||||
|
|
||||||
|
long Entry = Tuple.Entry;
|
||||||
|
|
||||||
|
int Length = (int)(Entry >> 42) & 0x1fffff;
|
||||||
|
|
||||||
|
DmaGet = Entry & 0xfffffffffc;
|
||||||
|
DmaPut = DmaGet + Length * 4;
|
||||||
|
|
||||||
|
NonMain = (Entry & (1L << 41)) != 0;
|
||||||
|
|
||||||
|
Gpu.ResourceManager.ClearPbCache();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetNonImmediateState(int Word)
|
||||||
|
{
|
||||||
|
State.Method = (Word >> 0) & 0x1fff;
|
||||||
|
State.SubChannel = (Word >> 13) & 7;
|
||||||
|
State.MethodCount = (Word >> 16) & 0x1fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CallMethod(int Argument)
|
||||||
|
{
|
||||||
|
Gpu.Fifo.CallMethod(Vmm, new GpuMethodCall(
|
||||||
|
State.Method,
|
||||||
|
Argument,
|
||||||
|
State.SubChannel,
|
||||||
|
State.MethodCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ namespace Ryujinx.Graphics.Gal
|
||||||
RGB5A1,
|
RGB5A1,
|
||||||
R8,
|
R8,
|
||||||
RG8,
|
RG8,
|
||||||
|
RGBX8,
|
||||||
RGBA8,
|
RGBA8,
|
||||||
BGRA8,
|
BGRA8,
|
||||||
RGB10A2,
|
RGB10A2,
|
||||||
|
@ -39,6 +40,7 @@ namespace Ryujinx.Graphics.Gal
|
||||||
RGBA32,
|
RGBA32,
|
||||||
R11G11B10,
|
R11G11B10,
|
||||||
D16,
|
D16,
|
||||||
|
D24,
|
||||||
D32,
|
D32,
|
||||||
D24S8,
|
D24S8,
|
||||||
D32S8,
|
D32S8,
|
||||||
|
|
|
@ -139,6 +139,7 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
||||||
case GalImageFormat.RG32 | GalImageFormat.Float: return (PixelInternalFormat.Rg32f, PixelFormat.Rg, PixelType.Float);
|
case GalImageFormat.RG32 | GalImageFormat.Float: return (PixelInternalFormat.Rg32f, PixelFormat.Rg, PixelType.Float);
|
||||||
case GalImageFormat.RG32 | GalImageFormat.Sint: return (PixelInternalFormat.Rg32i, PixelFormat.RgInteger, PixelType.Int);
|
case GalImageFormat.RG32 | GalImageFormat.Sint: return (PixelInternalFormat.Rg32i, PixelFormat.RgInteger, PixelType.Int);
|
||||||
case GalImageFormat.RG32 | GalImageFormat.Uint: return (PixelInternalFormat.Rg32ui, PixelFormat.RgInteger, PixelType.UnsignedInt);
|
case GalImageFormat.RG32 | GalImageFormat.Uint: return (PixelInternalFormat.Rg32ui, PixelFormat.RgInteger, PixelType.UnsignedInt);
|
||||||
|
case GalImageFormat.RGBX8 | GalImageFormat.Unorm: return (PixelInternalFormat.Rgb8, PixelFormat.Rgba, PixelType.UnsignedByte);
|
||||||
case GalImageFormat.RGBA8 | GalImageFormat.Snorm: return (PixelInternalFormat.Rgba8Snorm, PixelFormat.Rgba, PixelType.Byte);
|
case GalImageFormat.RGBA8 | GalImageFormat.Snorm: return (PixelInternalFormat.Rgba8Snorm, PixelFormat.Rgba, PixelType.Byte);
|
||||||
case GalImageFormat.RGBA8 | GalImageFormat.Unorm: return (PixelInternalFormat.Rgba8, PixelFormat.Rgba, PixelType.UnsignedByte);
|
case GalImageFormat.RGBA8 | GalImageFormat.Unorm: return (PixelInternalFormat.Rgba8, PixelFormat.Rgba, PixelType.UnsignedByte);
|
||||||
case GalImageFormat.RGBA8 | GalImageFormat.Sint: return (PixelInternalFormat.Rgba8i, PixelFormat.RgbaInteger, PixelType.Byte);
|
case GalImageFormat.RGBA8 | GalImageFormat.Sint: return (PixelInternalFormat.Rgba8i, PixelFormat.RgbaInteger, PixelType.Byte);
|
||||||
|
@ -174,10 +175,11 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
||||||
case GalImageFormat.R8 | GalImageFormat.Unorm: return (PixelInternalFormat.R8, PixelFormat.Red, PixelType.UnsignedByte);
|
case GalImageFormat.R8 | GalImageFormat.Unorm: return (PixelInternalFormat.R8, PixelFormat.Red, PixelType.UnsignedByte);
|
||||||
case GalImageFormat.R11G11B10 | GalImageFormat.Float: return (PixelInternalFormat.R11fG11fB10f, PixelFormat.Rgb, PixelType.UnsignedInt10F11F11FRev);
|
case GalImageFormat.R11G11B10 | GalImageFormat.Float: return (PixelInternalFormat.R11fG11fB10f, PixelFormat.Rgb, PixelType.UnsignedInt10F11F11FRev);
|
||||||
|
|
||||||
|
case GalImageFormat.D16 | GalImageFormat.Unorm: return (PixelInternalFormat.DepthComponent16, PixelFormat.DepthComponent, PixelType.UnsignedShort);
|
||||||
|
case GalImageFormat.D24 | GalImageFormat.Unorm: return (PixelInternalFormat.DepthComponent24, PixelFormat.DepthComponent, PixelType.UnsignedInt);
|
||||||
case GalImageFormat.D24S8 | GalImageFormat.Uint: return (PixelInternalFormat.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248);
|
case GalImageFormat.D24S8 | GalImageFormat.Uint: return (PixelInternalFormat.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248);
|
||||||
case GalImageFormat.D24S8 | GalImageFormat.Unorm: return (PixelInternalFormat.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248);
|
case GalImageFormat.D24S8 | GalImageFormat.Unorm: return (PixelInternalFormat.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248);
|
||||||
case GalImageFormat.D32 | GalImageFormat.Float: return (PixelInternalFormat.DepthComponent32f, PixelFormat.DepthComponent, PixelType.Float);
|
case GalImageFormat.D32 | GalImageFormat.Float: return (PixelInternalFormat.DepthComponent32f, PixelFormat.DepthComponent, PixelType.Float);
|
||||||
case GalImageFormat.D16 | GalImageFormat.Unorm: return (PixelInternalFormat.DepthComponent16, PixelFormat.DepthComponent, PixelType.UnsignedShort);
|
|
||||||
case GalImageFormat.D32S8 | GalImageFormat.Float: return (PixelInternalFormat.Depth32fStencil8, PixelFormat.DepthStencil, PixelType.Float32UnsignedInt248Rev);
|
case GalImageFormat.D32S8 | GalImageFormat.Float: return (PixelInternalFormat.Depth32fStencil8, PixelFormat.DepthStencil, PixelType.Float32UnsignedInt248Rev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -421,8 +421,6 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
||||||
|
|
||||||
ClearBufferMask Mask = GetClearMask(SrcTex);
|
ClearBufferMask Mask = GetClearMask(SrcTex);
|
||||||
|
|
||||||
GL.Clear(Mask);
|
|
||||||
|
|
||||||
GL.BlitFramebuffer(SrcX0, SrcY0, SrcX1, SrcY1, DstX0, DstY0, DstX1, DstY1, Mask, Filter);
|
GL.BlitFramebuffer(SrcX0, SrcY0, SrcX1, SrcY1, DstX0, DstY0, DstX1, DstY1, Mask, Filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
Ryujinx.Graphics/GpuMethodCall.cs
Normal file
24
Ryujinx.Graphics/GpuMethodCall.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
namespace Ryujinx.Graphics
|
||||||
|
{
|
||||||
|
struct GpuMethodCall
|
||||||
|
{
|
||||||
|
public int Method { get; private set; }
|
||||||
|
public int Argument { get; private set; }
|
||||||
|
public int SubChannel { get; private set; }
|
||||||
|
public int MethodCount { get; private set; }
|
||||||
|
|
||||||
|
public bool IsLastCall => MethodCount <= 1;
|
||||||
|
|
||||||
|
public GpuMethodCall(
|
||||||
|
int Method,
|
||||||
|
int Argument,
|
||||||
|
int SubChannel = 0,
|
||||||
|
int MethodCount = 0)
|
||||||
|
{
|
||||||
|
this.Method = Method;
|
||||||
|
this.Argument = Argument;
|
||||||
|
this.SubChannel = SubChannel;
|
||||||
|
this.MethodCount = MethodCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,7 +117,7 @@ namespace Ryujinx.Graphics
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool MemoryRegionModified(NvGpuVmm Vmm, long Position, long Size, NvGpuBufferType Type)
|
public bool MemoryRegionModified(NvGpuVmm Vmm, long Position, long Size, NvGpuBufferType Type)
|
||||||
{
|
{
|
||||||
HashSet<long> Uploaded = UploadedKeys[(int)Type];
|
HashSet<long> Uploaded = UploadedKeys[(int)Type];
|
||||||
|
|
||||||
|
@ -136,5 +136,10 @@ namespace Ryujinx.Graphics
|
||||||
UploadedKeys[Index].Clear();
|
UploadedKeys[Index].Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClearPbCache(NvGpuBufferType Type)
|
||||||
|
{
|
||||||
|
UploadedKeys[(int)Type].Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,6 @@ namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
int[] Registers { get; }
|
int[] Registers { get; }
|
||||||
|
|
||||||
void CallMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry);
|
void CallMethod(NvGpuVmm Vmm, GpuMethodCall MethCall);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Graphics.Memory;
|
using Ryujinx.Graphics.Memory;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -388,14 +389,11 @@ namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
int Value;
|
int Value;
|
||||||
|
|
||||||
//If we don't have any parameters in the FIFO,
|
if (!Fifo.TryDequeue(out Value))
|
||||||
//keep running the PFIFO engine until it writes the parameters.
|
|
||||||
while (!Fifo.TryDequeue(out Value))
|
|
||||||
{
|
{
|
||||||
if (!PFifo.Step())
|
Logger.PrintWarning(LogClass.Gpu, "Macro attempted to fetch an inexistent argument.");
|
||||||
{
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Value;
|
return Value;
|
||||||
|
@ -408,9 +406,9 @@ namespace Ryujinx.Graphics
|
||||||
|
|
||||||
private void Send(NvGpuVmm Vmm, int Value)
|
private void Send(NvGpuVmm Vmm, int Value)
|
||||||
{
|
{
|
||||||
NvGpuPBEntry PBEntry = new NvGpuPBEntry(MethAddr, 0, Value);
|
GpuMethodCall MethCall = new GpuMethodCall(MethAddr, Value);
|
||||||
|
|
||||||
Engine.CallMethod(Vmm, PBEntry);
|
Engine.CallMethod(Vmm, MethCall);
|
||||||
|
|
||||||
MethAddr += MethIncr;
|
MethAddr += MethIncr;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Memory
|
|
||||||
{
|
|
||||||
public struct NvGpuPBEntry
|
|
||||||
{
|
|
||||||
public int Method { get; private set; }
|
|
||||||
|
|
||||||
public int SubChannel { get; private set; }
|
|
||||||
|
|
||||||
private int[] m_Arguments;
|
|
||||||
|
|
||||||
public ReadOnlyCollection<int> Arguments => Array.AsReadOnly(m_Arguments);
|
|
||||||
|
|
||||||
public NvGpuPBEntry(int Method, int SubChannel, params int[] Arguments)
|
|
||||||
{
|
|
||||||
this.Method = Method;
|
|
||||||
this.SubChannel = SubChannel;
|
|
||||||
this.m_Arguments = Arguments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Memory
|
|
||||||
{
|
|
||||||
public static class NvGpuPushBuffer
|
|
||||||
{
|
|
||||||
private enum SubmissionMode
|
|
||||||
{
|
|
||||||
Incrementing = 1,
|
|
||||||
NonIncrementing = 3,
|
|
||||||
Immediate = 4,
|
|
||||||
IncrementOnce = 5
|
|
||||||
}
|
|
||||||
|
|
||||||
public static NvGpuPBEntry[] Decode(byte[] Data)
|
|
||||||
{
|
|
||||||
using (MemoryStream MS = new MemoryStream(Data))
|
|
||||||
{
|
|
||||||
BinaryReader Reader = new BinaryReader(MS);
|
|
||||||
|
|
||||||
List<NvGpuPBEntry> PushBuffer = new List<NvGpuPBEntry>();
|
|
||||||
|
|
||||||
bool CanRead() => MS.Position + 4 <= MS.Length;
|
|
||||||
|
|
||||||
while (CanRead())
|
|
||||||
{
|
|
||||||
int Packed = Reader.ReadInt32();
|
|
||||||
|
|
||||||
int Meth = (Packed >> 0) & 0x1fff;
|
|
||||||
int SubC = (Packed >> 13) & 7;
|
|
||||||
int Args = (Packed >> 16) & 0x1fff;
|
|
||||||
int Mode = (Packed >> 29) & 7;
|
|
||||||
|
|
||||||
switch ((SubmissionMode)Mode)
|
|
||||||
{
|
|
||||||
case SubmissionMode.Incrementing:
|
|
||||||
{
|
|
||||||
for (int Index = 0; Index < Args && CanRead(); Index++, Meth++)
|
|
||||||
{
|
|
||||||
PushBuffer.Add(new NvGpuPBEntry(Meth, SubC, Reader.ReadInt32()));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SubmissionMode.NonIncrementing:
|
|
||||||
{
|
|
||||||
int[] Arguments = new int[Args];
|
|
||||||
|
|
||||||
for (int Index = 0; Index < Arguments.Length; Index++)
|
|
||||||
{
|
|
||||||
if (!CanRead())
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Arguments[Index] = Reader.ReadInt32();
|
|
||||||
}
|
|
||||||
|
|
||||||
PushBuffer.Add(new NvGpuPBEntry(Meth, SubC, Arguments));
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SubmissionMode.Immediate:
|
|
||||||
{
|
|
||||||
PushBuffer.Add(new NvGpuPBEntry(Meth, SubC, Args));
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SubmissionMode.IncrementOnce:
|
|
||||||
{
|
|
||||||
if (CanRead())
|
|
||||||
{
|
|
||||||
PushBuffer.Add(new NvGpuPBEntry(Meth, SubC, Reader.ReadInt32()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CanRead() && Args > 1)
|
|
||||||
{
|
|
||||||
int[] Arguments = new int[Args - 1];
|
|
||||||
|
|
||||||
for (int Index = 0; Index < Arguments.Length && CanRead(); Index++)
|
|
||||||
{
|
|
||||||
Arguments[Index] = Reader.ReadInt32();
|
|
||||||
}
|
|
||||||
|
|
||||||
PushBuffer.Add(new NvGpuPBEntry(Meth + 1, SubC, Arguments));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PushBuffer.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,27 +5,54 @@ namespace Ryujinx.Graphics.Memory
|
||||||
{
|
{
|
||||||
class NvGpuVmmCache
|
class NvGpuVmmCache
|
||||||
{
|
{
|
||||||
private ValueRangeSet<int> CachedRanges;
|
private struct CachedResource
|
||||||
|
{
|
||||||
|
public long Key;
|
||||||
|
public int Mask;
|
||||||
|
|
||||||
|
public CachedResource(long Key, int Mask)
|
||||||
|
{
|
||||||
|
this.Key = Key;
|
||||||
|
this.Mask = Mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return (int)(Key * 23 + Mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is CachedResource Cached && Equals(Cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(CachedResource other)
|
||||||
|
{
|
||||||
|
return Key == other.Key && Mask == other.Mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueRangeSet<CachedResource> CachedRanges;
|
||||||
|
|
||||||
public NvGpuVmmCache()
|
public NvGpuVmmCache()
|
||||||
{
|
{
|
||||||
CachedRanges = new ValueRangeSet<int>();
|
CachedRanges = new ValueRangeSet<CachedResource>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsRegionModified(MemoryManager Memory, NvGpuBufferType BufferType, long PA, long Size)
|
public bool IsRegionModified(MemoryManager Memory, NvGpuBufferType BufferType, long Start, long Size)
|
||||||
{
|
{
|
||||||
(bool[] Modified, long ModifiedCount) = Memory.IsRegionModified(PA, Size);
|
(bool[] Modified, long ModifiedCount) = Memory.IsRegionModified(Start, Size);
|
||||||
|
|
||||||
//Remove all modified ranges.
|
//Remove all modified ranges.
|
||||||
int Index = 0;
|
int Index = 0;
|
||||||
|
|
||||||
long Position = PA & ~NvGpuVmm.PageMask;
|
long Position = Start & ~NvGpuVmm.PageMask;
|
||||||
|
|
||||||
while (ModifiedCount > 0)
|
while (ModifiedCount > 0)
|
||||||
{
|
{
|
||||||
if (Modified[Index++])
|
if (Modified[Index++])
|
||||||
{
|
{
|
||||||
CachedRanges.Remove(new ValueRange<int>(Position, Position + NvGpuVmm.PageSize));
|
CachedRanges.Remove(new ValueRange<CachedResource>(Position, Position + NvGpuVmm.PageSize));
|
||||||
|
|
||||||
ModifiedCount--;
|
ModifiedCount--;
|
||||||
}
|
}
|
||||||
|
@ -37,11 +64,19 @@ namespace Ryujinx.Graphics.Memory
|
||||||
//If the region is not yet present on the list, then a new ValueRange
|
//If the region is not yet present on the list, then a new ValueRange
|
||||||
//is directly added with the current resource type as the only bit set.
|
//is directly added with the current resource type as the only bit set.
|
||||||
//Otherwise, it just sets the bit for this new resource type on the current mask.
|
//Otherwise, it just sets the bit for this new resource type on the current mask.
|
||||||
|
//The physical address of the resource is used as key, those keys are used to keep
|
||||||
|
//track of resources that are already on the cache. A resource may be inside another
|
||||||
|
//resource, and in this case we should return true if the "sub-resource" was not
|
||||||
|
//yet cached.
|
||||||
int Mask = 1 << (int)BufferType;
|
int Mask = 1 << (int)BufferType;
|
||||||
|
|
||||||
ValueRange<int> NewCached = new ValueRange<int>(PA, PA + Size);
|
CachedResource NewCachedValue = new CachedResource(Start, Mask);
|
||||||
|
|
||||||
ValueRange<int>[] Ranges = CachedRanges.GetAllIntersections(NewCached);
|
ValueRange<CachedResource> NewCached = new ValueRange<CachedResource>(Start, Start + Size);
|
||||||
|
|
||||||
|
ValueRange<CachedResource>[] Ranges = CachedRanges.GetAllIntersections(NewCached);
|
||||||
|
|
||||||
|
bool IsKeyCached = Ranges.Length > 0 && Ranges[0].Value.Key == Start;
|
||||||
|
|
||||||
long LastEnd = NewCached.Start;
|
long LastEnd = NewCached.Start;
|
||||||
|
|
||||||
|
@ -49,23 +84,36 @@ namespace Ryujinx.Graphics.Memory
|
||||||
|
|
||||||
for (Index = 0; Index < Ranges.Length; Index++)
|
for (Index = 0; Index < Ranges.Length; Index++)
|
||||||
{
|
{
|
||||||
ValueRange<int> Current = Ranges[Index];
|
ValueRange<CachedResource> Current = Ranges[Index];
|
||||||
|
|
||||||
|
CachedResource Cached = Current.Value;
|
||||||
|
|
||||||
long RgStart = Math.Max(Current.Start, NewCached.Start);
|
long RgStart = Math.Max(Current.Start, NewCached.Start);
|
||||||
long RgEnd = Math.Min(Current.End, NewCached.End);
|
long RgEnd = Math.Min(Current.End, NewCached.End);
|
||||||
|
|
||||||
if ((Current.Value & Mask) == 0)
|
if ((Cached.Mask & Mask) != 0)
|
||||||
{
|
|
||||||
CachedRanges.Add(new ValueRange<int>(RgStart, RgEnd, Current.Value | Mask));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
Coverage += RgEnd - RgStart;
|
Coverage += RgEnd - RgStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Highest key value has priority, this prevents larger resources
|
||||||
|
//for completely invalidating smaller ones on the cache. For example,
|
||||||
|
//consider that a resource in the range [100, 200) was added, and then
|
||||||
|
//another one in the range [50, 200). We prevent the new resource from
|
||||||
|
//completely replacing the old one by spliting it like this:
|
||||||
|
//New resource key is added at [50, 100), old key is still present at [100, 200).
|
||||||
|
if (Cached.Key < Start)
|
||||||
|
{
|
||||||
|
Cached.Key = Start;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cached.Mask |= Mask;
|
||||||
|
|
||||||
|
CachedRanges.Add(new ValueRange<CachedResource>(RgStart, RgEnd, Cached));
|
||||||
|
|
||||||
if (RgStart > LastEnd)
|
if (RgStart > LastEnd)
|
||||||
{
|
{
|
||||||
CachedRanges.Add(new ValueRange<int>(LastEnd, RgStart, Mask));
|
CachedRanges.Add(new ValueRange<CachedResource>(LastEnd, RgStart, NewCachedValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
LastEnd = RgEnd;
|
LastEnd = RgEnd;
|
||||||
|
@ -73,10 +121,10 @@ namespace Ryujinx.Graphics.Memory
|
||||||
|
|
||||||
if (LastEnd < NewCached.End)
|
if (LastEnd < NewCached.End)
|
||||||
{
|
{
|
||||||
CachedRanges.Add(new ValueRange<int>(LastEnd, NewCached.End, Mask));
|
CachedRanges.Add(new ValueRange<CachedResource>(LastEnd, NewCached.End, NewCachedValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Coverage != Size;
|
return !IsKeyCached || Coverage != Size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,8 +8,9 @@ namespace Ryujinx.Graphics
|
||||||
|
|
||||||
public GpuResourceManager ResourceManager { get; private set; }
|
public GpuResourceManager ResourceManager { get; private set; }
|
||||||
|
|
||||||
public NvGpuFifo Fifo { get; private set; }
|
public DmaPusher Pusher { get; private set; }
|
||||||
|
|
||||||
|
internal NvGpuFifo Fifo { get; private set; }
|
||||||
internal NvGpuEngine2d Engine2d { get; private set; }
|
internal NvGpuEngine2d Engine2d { get; private set; }
|
||||||
internal NvGpuEngine3d Engine3d { get; private set; }
|
internal NvGpuEngine3d Engine3d { get; private set; }
|
||||||
internal NvGpuEngineM2mf EngineM2mf { get; private set; }
|
internal NvGpuEngineM2mf EngineM2mf { get; private set; }
|
||||||
|
@ -21,8 +22,9 @@ namespace Ryujinx.Graphics
|
||||||
|
|
||||||
ResourceManager = new GpuResourceManager(this);
|
ResourceManager = new GpuResourceManager(this);
|
||||||
|
|
||||||
Fifo = new NvGpuFifo(this);
|
Pusher = new DmaPusher(this);
|
||||||
|
|
||||||
|
Fifo = new NvGpuFifo(this);
|
||||||
Engine2d = new NvGpuEngine2d(this);
|
Engine2d = new NvGpuEngine2d(this);
|
||||||
Engine3d = new NvGpuEngine3d(this);
|
Engine3d = new NvGpuEngine3d(this);
|
||||||
EngineM2mf = new NvGpuEngineM2mf(this);
|
EngineM2mf = new NvGpuEngineM2mf(this);
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
using Ryujinx.Graphics.Gal;
|
using Ryujinx.Graphics.Gal;
|
||||||
using Ryujinx.Graphics.Memory;
|
using Ryujinx.Graphics.Memory;
|
||||||
using Ryujinx.Graphics.Texture;
|
using Ryujinx.Graphics.Texture;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics
|
namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
public class NvGpuEngine2d : INvGpuEngine
|
class NvGpuEngine2d : INvGpuEngine
|
||||||
{
|
{
|
||||||
private enum CopyOperation
|
private enum CopyOperation
|
||||||
{
|
{
|
||||||
|
@ -29,11 +28,11 @@ namespace Ryujinx.Graphics
|
||||||
Registers = new int[0x238];
|
Registers = new int[0x238];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CallMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
public void CallMethod(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
WriteRegister(PBEntry);
|
WriteRegister(MethCall);
|
||||||
|
|
||||||
if ((NvGpuEngine2dReg)PBEntry.Method == NvGpuEngine2dReg.BlitSrcYInt)
|
if ((NvGpuEngine2dReg)MethCall.Method == NvGpuEngine2dReg.BlitSrcYInt)
|
||||||
{
|
{
|
||||||
TextureCopy(Vmm);
|
TextureCopy(Vmm);
|
||||||
}
|
}
|
||||||
|
@ -43,6 +42,13 @@ namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
CopyOperation Operation = (CopyOperation)ReadRegister(NvGpuEngine2dReg.CopyOperation);
|
CopyOperation Operation = (CopyOperation)ReadRegister(NvGpuEngine2dReg.CopyOperation);
|
||||||
|
|
||||||
|
int DstFormat = ReadRegister(NvGpuEngine2dReg.DstFormat);
|
||||||
|
bool DstLinear = ReadRegister(NvGpuEngine2dReg.DstLinear) != 0;
|
||||||
|
int DstWidth = ReadRegister(NvGpuEngine2dReg.DstWidth);
|
||||||
|
int DstHeight = ReadRegister(NvGpuEngine2dReg.DstHeight);
|
||||||
|
int DstPitch = ReadRegister(NvGpuEngine2dReg.DstPitch);
|
||||||
|
int DstBlkDim = ReadRegister(NvGpuEngine2dReg.DstBlockDimensions);
|
||||||
|
|
||||||
int SrcFormat = ReadRegister(NvGpuEngine2dReg.SrcFormat);
|
int SrcFormat = ReadRegister(NvGpuEngine2dReg.SrcFormat);
|
||||||
bool SrcLinear = ReadRegister(NvGpuEngine2dReg.SrcLinear) != 0;
|
bool SrcLinear = ReadRegister(NvGpuEngine2dReg.SrcLinear) != 0;
|
||||||
int SrcWidth = ReadRegister(NvGpuEngine2dReg.SrcWidth);
|
int SrcWidth = ReadRegister(NvGpuEngine2dReg.SrcWidth);
|
||||||
|
@ -50,12 +56,13 @@ namespace Ryujinx.Graphics
|
||||||
int SrcPitch = ReadRegister(NvGpuEngine2dReg.SrcPitch);
|
int SrcPitch = ReadRegister(NvGpuEngine2dReg.SrcPitch);
|
||||||
int SrcBlkDim = ReadRegister(NvGpuEngine2dReg.SrcBlockDimensions);
|
int SrcBlkDim = ReadRegister(NvGpuEngine2dReg.SrcBlockDimensions);
|
||||||
|
|
||||||
int DstFormat = ReadRegister(NvGpuEngine2dReg.DstFormat);
|
int DstBlitX = ReadRegister(NvGpuEngine2dReg.BlitDstX);
|
||||||
bool DstLinear = ReadRegister(NvGpuEngine2dReg.DstLinear) != 0;
|
int DstBlitY = ReadRegister(NvGpuEngine2dReg.BlitDstY);
|
||||||
int DstWidth = ReadRegister(NvGpuEngine2dReg.DstWidth);
|
int DstBlitW = ReadRegister(NvGpuEngine2dReg.BlitDstW);
|
||||||
int DstHeight = ReadRegister(NvGpuEngine2dReg.DstHeight);
|
int DstBlitH = ReadRegister(NvGpuEngine2dReg.BlitDstH);
|
||||||
int DstPitch = ReadRegister(NvGpuEngine2dReg.DstPitch);
|
|
||||||
int DstBlkDim = ReadRegister(NvGpuEngine2dReg.DstBlockDimensions);
|
int SrcBlitX = ReadRegister(NvGpuEngine2dReg.BlitSrcXInt);
|
||||||
|
int SrcBlitY = ReadRegister(NvGpuEngine2dReg.BlitSrcYInt);
|
||||||
|
|
||||||
GalImageFormat SrcImgFormat = ImageUtils.ConvertSurface((GalSurfaceFormat)SrcFormat);
|
GalImageFormat SrcImgFormat = ImageUtils.ConvertSurface((GalSurfaceFormat)SrcFormat);
|
||||||
GalImageFormat DstImgFormat = ImageUtils.ConvertSurface((GalSurfaceFormat)DstFormat);
|
GalImageFormat DstImgFormat = ImageUtils.ConvertSurface((GalSurfaceFormat)DstFormat);
|
||||||
|
@ -86,23 +93,42 @@ namespace Ryujinx.Graphics
|
||||||
DstLayout,
|
DstLayout,
|
||||||
DstImgFormat);
|
DstImgFormat);
|
||||||
|
|
||||||
|
SrcTexture.Pitch = SrcPitch;
|
||||||
|
DstTexture.Pitch = DstPitch;
|
||||||
|
|
||||||
Gpu.ResourceManager.SendTexture(Vmm, SrcKey, SrcTexture);
|
Gpu.ResourceManager.SendTexture(Vmm, SrcKey, SrcTexture);
|
||||||
Gpu.ResourceManager.SendTexture(Vmm, DstKey, DstTexture);
|
Gpu.ResourceManager.SendTexture(Vmm, DstKey, DstTexture);
|
||||||
|
|
||||||
int Width = Math.Min(SrcWidth, DstWidth);
|
|
||||||
int Height = Math.Min(SrcHeight, DstHeight);
|
|
||||||
|
|
||||||
Gpu.Renderer.RenderTarget.Copy(
|
Gpu.Renderer.RenderTarget.Copy(
|
||||||
SrcKey,
|
SrcKey,
|
||||||
DstKey,
|
DstKey,
|
||||||
0,
|
SrcBlitX,
|
||||||
0,
|
SrcBlitY,
|
||||||
Width,
|
SrcBlitX + DstBlitW,
|
||||||
Height,
|
SrcBlitY + DstBlitH,
|
||||||
0,
|
DstBlitX,
|
||||||
0,
|
DstBlitY,
|
||||||
Width,
|
DstBlitX + DstBlitW,
|
||||||
Height);
|
DstBlitY + DstBlitH);
|
||||||
|
|
||||||
|
//Do a guest side copy aswell. This is necessary when
|
||||||
|
//the texture is modified by the guest, however it doesn't
|
||||||
|
//work when resources that the gpu can write to are copied,
|
||||||
|
//like framebuffers.
|
||||||
|
ImageUtils.CopyTexture(
|
||||||
|
Vmm,
|
||||||
|
SrcTexture,
|
||||||
|
DstTexture,
|
||||||
|
SrcAddress,
|
||||||
|
DstAddress,
|
||||||
|
SrcBlitX,
|
||||||
|
SrcBlitY,
|
||||||
|
DstBlitX,
|
||||||
|
DstBlitY,
|
||||||
|
DstBlitW,
|
||||||
|
DstBlitH);
|
||||||
|
|
||||||
|
Vmm.IsRegionModified(DstKey, ImageUtils.GetSize(DstTexture), NvGpuBufferType.Texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GalMemoryLayout GetLayout(bool Linear)
|
private static GalMemoryLayout GetLayout(bool Linear)
|
||||||
|
@ -119,14 +145,9 @@ namespace Ryujinx.Graphics
|
||||||
(uint)Registers[(int)Reg + 1];
|
(uint)Registers[(int)Reg + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteRegister(NvGpuPBEntry PBEntry)
|
private void WriteRegister(GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
int ArgsCount = PBEntry.Arguments.Count;
|
Registers[MethCall.Method] = MethCall.Argument;
|
||||||
|
|
||||||
if (ArgsCount > 0)
|
|
||||||
{
|
|
||||||
Registers[PBEntry.Method] = PBEntry.Arguments[ArgsCount - 1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int ReadRegister(NvGpuEngine2dReg Reg)
|
private int ReadRegister(NvGpuEngine2dReg Reg)
|
||||||
|
|
|
@ -7,7 +7,7 @@ using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics
|
namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
public class NvGpuEngine3d : INvGpuEngine
|
class NvGpuEngine3d : INvGpuEngine
|
||||||
{
|
{
|
||||||
public int[] Registers { get; private set; }
|
public int[] Registers { get; private set; }
|
||||||
|
|
||||||
|
@ -24,8 +24,6 @@ namespace Ryujinx.Graphics
|
||||||
|
|
||||||
private ConstBuffer[][] ConstBuffers;
|
private ConstBuffer[][] ConstBuffers;
|
||||||
|
|
||||||
private List<long>[] UploadedKeys;
|
|
||||||
|
|
||||||
private int CurrentInstance = 0;
|
private int CurrentInstance = 0;
|
||||||
|
|
||||||
public NvGpuEngine3d(NvGpu Gpu)
|
public NvGpuEngine3d(NvGpu Gpu)
|
||||||
|
@ -59,13 +57,6 @@ namespace Ryujinx.Graphics
|
||||||
ConstBuffers[Index] = new ConstBuffer[18];
|
ConstBuffers[Index] = new ConstBuffer[18];
|
||||||
}
|
}
|
||||||
|
|
||||||
UploadedKeys = new List<long>[(int)NvGpuBufferType.Count];
|
|
||||||
|
|
||||||
for (int i = 0; i < UploadedKeys.Length; i++)
|
|
||||||
{
|
|
||||||
UploadedKeys[i] = new List<long>();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Ensure that all components are enabled by default.
|
//Ensure that all components are enabled by default.
|
||||||
//FIXME: Is this correct?
|
//FIXME: Is this correct?
|
||||||
WriteRegister(NvGpuEngine3dReg.ColorMaskN, 0x1111);
|
WriteRegister(NvGpuEngine3dReg.ColorMaskN, 0x1111);
|
||||||
|
@ -81,27 +72,19 @@ namespace Ryujinx.Graphics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CallMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
public void CallMethod(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
if (Methods.TryGetValue(PBEntry.Method, out NvGpuMethod Method))
|
if (Methods.TryGetValue(MethCall.Method, out NvGpuMethod Method))
|
||||||
{
|
{
|
||||||
Method(Vmm, PBEntry);
|
Method(Vmm, MethCall);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WriteRegister(PBEntry);
|
WriteRegister(MethCall);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetCache()
|
private void VertexEndGl(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
|
||||||
foreach (List<long> Uploaded in UploadedKeys)
|
|
||||||
{
|
|
||||||
Uploaded.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void VertexEndGl(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
|
||||||
{
|
{
|
||||||
LockCaches();
|
LockCaches();
|
||||||
|
|
||||||
|
@ -152,13 +135,11 @@ namespace Ryujinx.Graphics
|
||||||
Gpu.Renderer.Texture.UnlockCache();
|
Gpu.Renderer.Texture.UnlockCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearBuffers(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void ClearBuffers(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
int Arg0 = PBEntry.Arguments[0];
|
int Attachment = (MethCall.Argument >> 6) & 0xf;
|
||||||
|
|
||||||
int Attachment = (Arg0 >> 6) & 0xf;
|
GalClearBufferFlags Flags = (GalClearBufferFlags)(MethCall.Argument & 0x3f);
|
||||||
|
|
||||||
GalClearBufferFlags Flags = (GalClearBufferFlags)(Arg0 & 0x3f);
|
|
||||||
|
|
||||||
float Red = ReadRegisterFloat(NvGpuEngine3dReg.ClearNColor + 0);
|
float Red = ReadRegisterFloat(NvGpuEngine3dReg.ClearNColor + 0);
|
||||||
float Green = ReadRegisterFloat(NvGpuEngine3dReg.ClearNColor + 1);
|
float Green = ReadRegisterFloat(NvGpuEngine3dReg.ClearNColor + 1);
|
||||||
|
@ -234,6 +215,15 @@ namespace Ryujinx.Graphics
|
||||||
|
|
||||||
State.FlipX = GetFlipSign(NvGpuEngine3dReg.ViewportNScaleX);
|
State.FlipX = GetFlipSign(NvGpuEngine3dReg.ViewportNScaleX);
|
||||||
State.FlipY = GetFlipSign(NvGpuEngine3dReg.ViewportNScaleY);
|
State.FlipY = GetFlipSign(NvGpuEngine3dReg.ViewportNScaleY);
|
||||||
|
|
||||||
|
int ScreenYControl = ReadRegister(NvGpuEngine3dReg.ScreenYControl);
|
||||||
|
|
||||||
|
bool NegateY = (ScreenYControl & 1) != 0;
|
||||||
|
|
||||||
|
if (NegateY)
|
||||||
|
{
|
||||||
|
State.FlipY = -State.FlipY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetZeta(NvGpuVmm Vmm)
|
private void SetZeta(NvGpuVmm Vmm)
|
||||||
|
@ -566,8 +556,11 @@ namespace Ryujinx.Graphics
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool LinkedTsc = ReadRegisterBool(NvGpuEngine3dReg.LinkedTsc);
|
||||||
|
|
||||||
int TicIndex = (TextureHandle >> 0) & 0xfffff;
|
int TicIndex = (TextureHandle >> 0) & 0xfffff;
|
||||||
int TscIndex = (TextureHandle >> 20) & 0xfff;
|
|
||||||
|
int TscIndex = LinkedTsc ? TicIndex : (TextureHandle >> 20) & 0xfff;
|
||||||
|
|
||||||
long TicPosition = MakeInt64From2xInt32(NvGpuEngine3dReg.TexHeaderPoolOffset);
|
long TicPosition = MakeInt64From2xInt32(NvGpuEngine3dReg.TexHeaderPoolOffset);
|
||||||
long TscPosition = MakeInt64From2xInt32(NvGpuEngine3dReg.TexSamplerPoolOffset);
|
long TscPosition = MakeInt64From2xInt32(NvGpuEngine3dReg.TexSamplerPoolOffset);
|
||||||
|
@ -618,7 +611,7 @@ namespace Ryujinx.Graphics
|
||||||
|
|
||||||
long Key = Vmm.GetPhysicalAddress(Cb.Position);
|
long Key = Vmm.GetPhysicalAddress(Cb.Position);
|
||||||
|
|
||||||
if (QueryKeyUpload(Vmm, Key, Cb.Size, NvGpuBufferType.ConstBuffer))
|
if (Gpu.ResourceManager.MemoryRegionModified(Vmm, Key, Cb.Size, NvGpuBufferType.ConstBuffer))
|
||||||
{
|
{
|
||||||
IntPtr Source = Vmm.GetHostAddress(Cb.Position, Cb.Size);
|
IntPtr Source = Vmm.GetHostAddress(Cb.Position, Cb.Size);
|
||||||
|
|
||||||
|
@ -661,7 +654,7 @@ namespace Ryujinx.Graphics
|
||||||
PrimType == GalPrimitiveType.Quads ||
|
PrimType == GalPrimitiveType.Quads ||
|
||||||
PrimType == GalPrimitiveType.QuadStrip;
|
PrimType == GalPrimitiveType.QuadStrip;
|
||||||
|
|
||||||
if (!IboCached || QueryKeyUpload(Vmm, IboKey, (uint)IbSize, NvGpuBufferType.Index))
|
if (!IboCached || Gpu.ResourceManager.MemoryRegionModified(Vmm, IboKey, (uint)IbSize, NvGpuBufferType.Index))
|
||||||
{
|
{
|
||||||
if (!UsesLegacyQuads)
|
if (!UsesLegacyQuads)
|
||||||
{
|
{
|
||||||
|
@ -778,7 +771,7 @@ namespace Ryujinx.Graphics
|
||||||
|
|
||||||
bool VboCached = Gpu.Renderer.Rasterizer.IsVboCached(VboKey, VbSize);
|
bool VboCached = Gpu.Renderer.Rasterizer.IsVboCached(VboKey, VbSize);
|
||||||
|
|
||||||
if (!VboCached || QueryKeyUpload(Vmm, VboKey, VbSize, NvGpuBufferType.Vertex))
|
if (!VboCached || Gpu.ResourceManager.MemoryRegionModified(Vmm, VboKey, VbSize, NvGpuBufferType.Vertex))
|
||||||
{
|
{
|
||||||
IntPtr DataAddress = Vmm.GetHostAddress(VertexPosition, VbSize);
|
IntPtr DataAddress = Vmm.GetHostAddress(VertexPosition, VbSize);
|
||||||
|
|
||||||
|
@ -877,9 +870,9 @@ namespace Ryujinx.Graphics
|
||||||
WriteCounterAndTimestamp
|
WriteCounterAndTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueryControl(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void QueryControl(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
WriteRegister(PBEntry);
|
WriteRegister(MethCall);
|
||||||
|
|
||||||
long Position = MakeInt64From2xInt32(NvGpuEngine3dReg.QueryAddress);
|
long Position = MakeInt64From2xInt32(NvGpuEngine3dReg.QueryAddress);
|
||||||
|
|
||||||
|
@ -909,29 +902,24 @@ namespace Ryujinx.Graphics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CbData(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void CbData(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
long Position = MakeInt64From2xInt32(NvGpuEngine3dReg.ConstBufferAddress);
|
long Position = MakeInt64From2xInt32(NvGpuEngine3dReg.ConstBufferAddress);
|
||||||
|
|
||||||
int Offset = ReadRegister(NvGpuEngine3dReg.ConstBufferOffset);
|
int Offset = ReadRegister(NvGpuEngine3dReg.ConstBufferOffset);
|
||||||
|
|
||||||
foreach (int Arg in PBEntry.Arguments)
|
Vmm.WriteInt32(Position + Offset, MethCall.Argument);
|
||||||
{
|
|
||||||
Vmm.WriteInt32(Position + Offset, Arg);
|
|
||||||
|
|
||||||
Offset += 4;
|
WriteRegister(NvGpuEngine3dReg.ConstBufferOffset, Offset + 4);
|
||||||
}
|
|
||||||
|
|
||||||
WriteRegister(NvGpuEngine3dReg.ConstBufferOffset, Offset);
|
Gpu.ResourceManager.ClearPbCache(NvGpuBufferType.ConstBuffer);
|
||||||
|
|
||||||
UploadedKeys[(int)NvGpuBufferType.ConstBuffer].Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CbBind(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void CbBind(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
int Stage = (PBEntry.Method - 0x904) >> 3;
|
int Stage = (MethCall.Method - 0x904) >> 3;
|
||||||
|
|
||||||
int Index = PBEntry.Arguments[0];
|
int Index = MethCall.Argument;
|
||||||
|
|
||||||
bool Enabled = (Index & 1) != 0;
|
bool Enabled = (Index & 1) != 0;
|
||||||
|
|
||||||
|
@ -970,14 +958,9 @@ namespace Ryujinx.Graphics
|
||||||
(uint)Registers[(int)Reg + 1];
|
(uint)Registers[(int)Reg + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteRegister(NvGpuPBEntry PBEntry)
|
private void WriteRegister(GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
int ArgsCount = PBEntry.Arguments.Count;
|
Registers[MethCall.Method] = MethCall.Argument;
|
||||||
|
|
||||||
if (ArgsCount > 0)
|
|
||||||
{
|
|
||||||
Registers[PBEntry.Method] = PBEntry.Arguments[ArgsCount - 1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int ReadRegister(NvGpuEngine3dReg Reg)
|
private int ReadRegister(NvGpuEngine3dReg Reg)
|
||||||
|
@ -999,19 +982,5 @@ namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
Registers[(int)Reg] = Value;
|
Registers[(int)Reg] = Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool QueryKeyUpload(NvGpuVmm Vmm, long Key, long Size, NvGpuBufferType Type)
|
|
||||||
{
|
|
||||||
List<long> Uploaded = UploadedKeys[(int)Type];
|
|
||||||
|
|
||||||
if (Uploaded.Contains(Key))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uploaded.Add(Key);
|
|
||||||
|
|
||||||
return Vmm.IsRegionModified(Key, Size, Type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ namespace Ryujinx.Graphics
|
||||||
ZetaHoriz = 0x48a,
|
ZetaHoriz = 0x48a,
|
||||||
ZetaVert = 0x48b,
|
ZetaVert = 0x48b,
|
||||||
ZetaArrayMode = 0x48c,
|
ZetaArrayMode = 0x48c,
|
||||||
|
LinkedTsc = 0x48d,
|
||||||
DepthTestEnable = 0x4b3,
|
DepthTestEnable = 0x4b3,
|
||||||
BlendIndependent = 0x4b9,
|
BlendIndependent = 0x4b9,
|
||||||
DepthWriteEnable = 0x4ba,
|
DepthWriteEnable = 0x4ba,
|
||||||
|
@ -57,6 +58,7 @@ namespace Ryujinx.Graphics
|
||||||
StencilFrontFuncRef = 0x4e5,
|
StencilFrontFuncRef = 0x4e5,
|
||||||
StencilFrontFuncMask = 0x4e6,
|
StencilFrontFuncMask = 0x4e6,
|
||||||
StencilFrontMask = 0x4e7,
|
StencilFrontMask = 0x4e7,
|
||||||
|
ScreenYControl = 0x4eb,
|
||||||
VertexArrayElemBase = 0x50d,
|
VertexArrayElemBase = 0x50d,
|
||||||
VertexArrayInstBase = 0x50e,
|
VertexArrayInstBase = 0x50e,
|
||||||
ZetaEnable = 0x54e,
|
ZetaEnable = 0x54e,
|
||||||
|
|
|
@ -4,7 +4,7 @@ using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics
|
namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
public class NvGpuEngineM2mf : INvGpuEngine
|
class NvGpuEngineM2mf : INvGpuEngine
|
||||||
{
|
{
|
||||||
public int[] Registers { get; private set; }
|
public int[] Registers { get; private set; }
|
||||||
|
|
||||||
|
@ -33,22 +33,22 @@ namespace Ryujinx.Graphics
|
||||||
AddMethod(0xc0, 1, 1, Execute);
|
AddMethod(0xc0, 1, 1, Execute);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CallMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
public void CallMethod(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
if (Methods.TryGetValue(PBEntry.Method, out NvGpuMethod Method))
|
if (Methods.TryGetValue(MethCall.Method, out NvGpuMethod Method))
|
||||||
{
|
{
|
||||||
Method(Vmm, PBEntry);
|
Method(Vmm, MethCall);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WriteRegister(PBEntry);
|
WriteRegister(MethCall);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Execute(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void Execute(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
//TODO: Some registers and copy modes are still not implemented.
|
//TODO: Some registers and copy modes are still not implemented.
|
||||||
int Control = PBEntry.Arguments[0];
|
int Control = MethCall.Argument;
|
||||||
|
|
||||||
bool SrcLinear = ((Control >> 7) & 1) != 0;
|
bool SrcLinear = ((Control >> 7) & 1) != 0;
|
||||||
bool DstLinear = ((Control >> 8) & 1) != 0;
|
bool DstLinear = ((Control >> 8) & 1) != 0;
|
||||||
|
@ -169,14 +169,9 @@ namespace Ryujinx.Graphics
|
||||||
(uint)Registers[(int)Reg + 1];
|
(uint)Registers[(int)Reg + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteRegister(NvGpuPBEntry PBEntry)
|
private void WriteRegister(GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
int ArgsCount = PBEntry.Arguments.Count;
|
Registers[MethCall.Method] = MethCall.Argument;
|
||||||
|
|
||||||
if (ArgsCount > 0)
|
|
||||||
{
|
|
||||||
Registers[PBEntry.Method] = PBEntry.Arguments[ArgsCount - 1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int ReadRegister(NvGpuEngineM2mfReg Reg)
|
private int ReadRegister(NvGpuEngineM2mfReg Reg)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
using Ryujinx.Graphics.Memory;
|
using Ryujinx.Graphics.Memory;
|
||||||
|
using Ryujinx.Graphics.Texture;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics
|
namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
public class NvGpuEngineP2mf : INvGpuEngine
|
class NvGpuEngineP2mf : INvGpuEngine
|
||||||
{
|
{
|
||||||
public int[] Registers { get; private set; }
|
public int[] Registers { get; private set; }
|
||||||
|
|
||||||
|
@ -12,7 +12,21 @@ namespace Ryujinx.Graphics
|
||||||
|
|
||||||
private Dictionary<int, NvGpuMethod> Methods;
|
private Dictionary<int, NvGpuMethod> Methods;
|
||||||
|
|
||||||
private ReadOnlyCollection<int> DataBuffer;
|
private int CopyStartX;
|
||||||
|
private int CopyStartY;
|
||||||
|
|
||||||
|
private int CopyWidth;
|
||||||
|
private int CopyHeight;
|
||||||
|
private int CopyGobBlockHeight;
|
||||||
|
|
||||||
|
private long CopyAddress;
|
||||||
|
|
||||||
|
private int CopyOffset;
|
||||||
|
private int CopySize;
|
||||||
|
|
||||||
|
private bool CopyLinear;
|
||||||
|
|
||||||
|
private byte[] Buffer;
|
||||||
|
|
||||||
public NvGpuEngineP2mf(NvGpu Gpu)
|
public NvGpuEngineP2mf(NvGpu Gpu)
|
||||||
{
|
{
|
||||||
|
@ -36,40 +50,90 @@ namespace Ryujinx.Graphics
|
||||||
AddMethod(0x6d, 1, 1, PushData);
|
AddMethod(0x6d, 1, 1, PushData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CallMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
public void CallMethod(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
if (Methods.TryGetValue(PBEntry.Method, out NvGpuMethod Method))
|
if (Methods.TryGetValue(MethCall.Method, out NvGpuMethod Method))
|
||||||
{
|
{
|
||||||
Method(Vmm, PBEntry);
|
Method(Vmm, MethCall);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WriteRegister(PBEntry);
|
WriteRegister(MethCall);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Execute(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void Execute(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
//TODO: Some registers and copy modes are still not implemented.
|
//TODO: Some registers and copy modes are still not implemented.
|
||||||
int Control = PBEntry.Arguments[0];
|
int Control = MethCall.Argument;
|
||||||
|
|
||||||
long DstAddress = MakeInt64From2xInt32(NvGpuEngineP2mfReg.DstAddress);
|
long DstAddress = MakeInt64From2xInt32(NvGpuEngineP2mfReg.DstAddress);
|
||||||
|
|
||||||
|
int DstPitch = ReadRegister(NvGpuEngineP2mfReg.DstPitch);
|
||||||
|
int DstBlkDim = ReadRegister(NvGpuEngineP2mfReg.DstBlockDim);
|
||||||
|
|
||||||
|
int DstX = ReadRegister(NvGpuEngineP2mfReg.DstX);
|
||||||
|
int DstY = ReadRegister(NvGpuEngineP2mfReg.DstY);
|
||||||
|
|
||||||
|
int DstWidth = ReadRegister(NvGpuEngineP2mfReg.DstWidth);
|
||||||
|
int DstHeight = ReadRegister(NvGpuEngineP2mfReg.DstHeight);
|
||||||
|
|
||||||
int LineLengthIn = ReadRegister(NvGpuEngineP2mfReg.LineLengthIn);
|
int LineLengthIn = ReadRegister(NvGpuEngineP2mfReg.LineLengthIn);
|
||||||
|
int LineCount = ReadRegister(NvGpuEngineP2mfReg.LineCount);
|
||||||
|
|
||||||
DataBuffer = null;
|
CopyLinear = (Control & 1) != 0;
|
||||||
|
|
||||||
Gpu.Fifo.Step();
|
CopyGobBlockHeight = 1 << ((DstBlkDim >> 4) & 0xf);
|
||||||
|
|
||||||
for (int Offset = 0; Offset < LineLengthIn; Offset += 4)
|
CopyStartX = DstX;
|
||||||
{
|
CopyStartY = DstY;
|
||||||
Vmm.WriteInt32(DstAddress + Offset, DataBuffer[Offset >> 2]);
|
|
||||||
}
|
CopyWidth = DstWidth;
|
||||||
|
CopyHeight = DstHeight;
|
||||||
|
|
||||||
|
CopyAddress = DstAddress;
|
||||||
|
|
||||||
|
CopyOffset = 0;
|
||||||
|
CopySize = LineLengthIn * LineCount;
|
||||||
|
|
||||||
|
Buffer = new byte[CopySize];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PushData(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void PushData(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
DataBuffer = PBEntry.Arguments;
|
if (Buffer == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int Shift = 0; Shift < 32 && CopyOffset < CopySize; Shift += 8, CopyOffset++)
|
||||||
|
{
|
||||||
|
Buffer[CopyOffset] = (byte)(MethCall.Argument >> Shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MethCall.IsLastCall)
|
||||||
|
{
|
||||||
|
if (CopyLinear)
|
||||||
|
{
|
||||||
|
Vmm.WriteBytes(CopyAddress, Buffer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BlockLinearSwizzle Swizzle = new BlockLinearSwizzle(CopyWidth, 1, CopyGobBlockHeight);
|
||||||
|
|
||||||
|
int SrcOffset = 0;
|
||||||
|
|
||||||
|
for (int Y = CopyStartY; Y < CopyHeight && SrcOffset < CopySize; Y++)
|
||||||
|
for (int X = CopyStartX; X < CopyWidth && SrcOffset < CopySize; X++)
|
||||||
|
{
|
||||||
|
int DstOffset = Swizzle.GetSwizzleOffset(X, Y);
|
||||||
|
|
||||||
|
Vmm.WriteByte(CopyAddress + DstOffset, Buffer[SrcOffset++]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private long MakeInt64From2xInt32(NvGpuEngineP2mfReg Reg)
|
private long MakeInt64From2xInt32(NvGpuEngineP2mfReg Reg)
|
||||||
|
@ -79,14 +143,9 @@ namespace Ryujinx.Graphics
|
||||||
(uint)Registers[(int)Reg + 1];
|
(uint)Registers[(int)Reg + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteRegister(NvGpuPBEntry PBEntry)
|
private void WriteRegister(GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
int ArgsCount = PBEntry.Arguments.Count;
|
Registers[MethCall.Method] = MethCall.Argument;
|
||||||
|
|
||||||
if (ArgsCount > 0)
|
|
||||||
{
|
|
||||||
Registers[PBEntry.Method] = PBEntry.Arguments[ArgsCount - 1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int ReadRegister(NvGpuEngineP2mfReg Reg)
|
private int ReadRegister(NvGpuEngineP2mfReg Reg)
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
using Ryujinx.Graphics.Memory;
|
using Ryujinx.Graphics.Memory;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics
|
namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
public class NvGpuFifo
|
class NvGpuFifo
|
||||||
{
|
{
|
||||||
private const int MacrosCount = 0x80;
|
private const int MacrosCount = 0x80;
|
||||||
private const int MacroIndexMask = MacrosCount - 1;
|
private const int MacroIndexMask = MacrosCount - 1;
|
||||||
|
@ -15,33 +13,47 @@ namespace Ryujinx.Graphics
|
||||||
|
|
||||||
private NvGpu Gpu;
|
private NvGpu Gpu;
|
||||||
|
|
||||||
private ConcurrentQueue<(NvGpuVmm, NvGpuPBEntry[])> BufferQueue;
|
|
||||||
|
|
||||||
private NvGpuEngine[] SubChannels;
|
private NvGpuEngine[] SubChannels;
|
||||||
|
|
||||||
public AutoResetEvent Event { get; private set; }
|
|
||||||
|
|
||||||
private struct CachedMacro
|
private struct CachedMacro
|
||||||
{
|
{
|
||||||
public int Position { get; private set; }
|
public int Position { get; private set; }
|
||||||
|
|
||||||
|
private bool ExecutionPending;
|
||||||
|
private int Argument;
|
||||||
|
|
||||||
private MacroInterpreter Interpreter;
|
private MacroInterpreter Interpreter;
|
||||||
|
|
||||||
public CachedMacro(NvGpuFifo PFifo, INvGpuEngine Engine, int Position)
|
public CachedMacro(NvGpuFifo PFifo, INvGpuEngine Engine, int Position)
|
||||||
{
|
{
|
||||||
this.Position = Position;
|
this.Position = Position;
|
||||||
|
|
||||||
|
ExecutionPending = false;
|
||||||
|
Argument = 0;
|
||||||
|
|
||||||
Interpreter = new MacroInterpreter(PFifo, Engine);
|
Interpreter = new MacroInterpreter(PFifo, Engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PushParam(int Param)
|
public void StartExecution(int Argument)
|
||||||
{
|
{
|
||||||
Interpreter?.Fifo.Enqueue(Param);
|
this.Argument = Argument;
|
||||||
|
|
||||||
|
ExecutionPending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute(NvGpuVmm Vmm, int[] Mme, int Param)
|
public void Execute(NvGpuVmm Vmm, int[] Mme)
|
||||||
{
|
{
|
||||||
Interpreter?.Execute(Vmm, Mme, Position, Param);
|
if (ExecutionPending)
|
||||||
|
{
|
||||||
|
ExecutionPending = false;
|
||||||
|
|
||||||
|
Interpreter?.Execute(Vmm, Mme, Position, Argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PushArgument(int Argument)
|
||||||
|
{
|
||||||
|
Interpreter?.Fifo.Enqueue(Argument);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,148 +68,109 @@ namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
this.Gpu = Gpu;
|
this.Gpu = Gpu;
|
||||||
|
|
||||||
BufferQueue = new ConcurrentQueue<(NvGpuVmm, NvGpuPBEntry[])>();
|
|
||||||
|
|
||||||
SubChannels = new NvGpuEngine[8];
|
SubChannels = new NvGpuEngine[8];
|
||||||
|
|
||||||
Macros = new CachedMacro[MacrosCount];
|
Macros = new CachedMacro[MacrosCount];
|
||||||
|
|
||||||
Mme = new int[MmeWords];
|
Mme = new int[MmeWords];
|
||||||
|
|
||||||
Event = new AutoResetEvent(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PushBuffer(NvGpuVmm Vmm, NvGpuPBEntry[] Buffer)
|
public void CallMethod(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
BufferQueue.Enqueue((Vmm, Buffer));
|
if ((NvGpuFifoMeth)MethCall.Method == NvGpuFifoMeth.BindChannel)
|
||||||
|
|
||||||
Event.Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DispatchCalls()
|
|
||||||
{
|
|
||||||
while (Step());
|
|
||||||
}
|
|
||||||
|
|
||||||
private (NvGpuVmm Vmm, NvGpuPBEntry[] Pb) Curr;
|
|
||||||
|
|
||||||
private int CurrPbEntryIndex;
|
|
||||||
|
|
||||||
public bool Step()
|
|
||||||
{
|
|
||||||
while (Curr.Pb == null || Curr.Pb.Length <= CurrPbEntryIndex)
|
|
||||||
{
|
{
|
||||||
if (!BufferQueue.TryDequeue(out Curr))
|
NvGpuEngine Engine = (NvGpuEngine)MethCall.Argument;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gpu.Engine3d.ResetCache();
|
SubChannels[MethCall.SubChannel] = Engine;
|
||||||
|
|
||||||
Gpu.ResourceManager.ClearPbCache();
|
|
||||||
|
|
||||||
CurrPbEntryIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
CallMethod(Curr.Vmm, Curr.Pb[CurrPbEntryIndex++]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CallMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
|
||||||
{
|
|
||||||
if ((NvGpuFifoMeth)PBEntry.Method == NvGpuFifoMeth.BindChannel)
|
|
||||||
{
|
|
||||||
NvGpuEngine Engine = (NvGpuEngine)PBEntry.Arguments[0];
|
|
||||||
|
|
||||||
SubChannels[PBEntry.SubChannel] = Engine;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
switch (SubChannels[PBEntry.SubChannel])
|
switch (SubChannels[MethCall.SubChannel])
|
||||||
{
|
{
|
||||||
case NvGpuEngine._2d: Call2dMethod (Vmm, PBEntry); break;
|
case NvGpuEngine._2d: Call2dMethod (Vmm, MethCall); break;
|
||||||
case NvGpuEngine._3d: Call3dMethod (Vmm, PBEntry); break;
|
case NvGpuEngine._3d: Call3dMethod (Vmm, MethCall); break;
|
||||||
case NvGpuEngine.P2mf: CallP2mfMethod(Vmm, PBEntry); break;
|
case NvGpuEngine.P2mf: CallP2mfMethod(Vmm, MethCall); break;
|
||||||
case NvGpuEngine.M2mf: CallM2mfMethod(Vmm, PBEntry); break;
|
case NvGpuEngine.M2mf: CallM2mfMethod(Vmm, MethCall); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Call2dMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void Call2dMethod(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
Gpu.Engine2d.CallMethod(Vmm, PBEntry);
|
Gpu.Engine2d.CallMethod(Vmm, MethCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Call3dMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void Call3dMethod(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
if (PBEntry.Method < 0x80)
|
if (MethCall.Method < 0x80)
|
||||||
{
|
{
|
||||||
switch ((NvGpuFifoMeth)PBEntry.Method)
|
switch ((NvGpuFifoMeth)MethCall.Method)
|
||||||
{
|
{
|
||||||
case NvGpuFifoMeth.SetMacroUploadAddress:
|
case NvGpuFifoMeth.SetMacroUploadAddress:
|
||||||
{
|
{
|
||||||
CurrMacroPosition = PBEntry.Arguments[0];
|
CurrMacroPosition = MethCall.Argument;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case NvGpuFifoMeth.SendMacroCodeData:
|
case NvGpuFifoMeth.SendMacroCodeData:
|
||||||
{
|
{
|
||||||
foreach (int Arg in PBEntry.Arguments)
|
Mme[CurrMacroPosition++] = MethCall.Argument;
|
||||||
{
|
|
||||||
Mme[CurrMacroPosition++] = Arg;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case NvGpuFifoMeth.SetMacroBindingIndex:
|
case NvGpuFifoMeth.SetMacroBindingIndex:
|
||||||
{
|
{
|
||||||
CurrMacroBindIndex = PBEntry.Arguments[0];
|
CurrMacroBindIndex = MethCall.Argument;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case NvGpuFifoMeth.BindMacro:
|
case NvGpuFifoMeth.BindMacro:
|
||||||
{
|
{
|
||||||
int Position = PBEntry.Arguments[0];
|
int Position = MethCall.Argument;
|
||||||
|
|
||||||
Macros[CurrMacroBindIndex] = new CachedMacro(this, Gpu.Engine3d, Position);
|
Macros[CurrMacroBindIndex] = new CachedMacro(this, Gpu.Engine3d, Position);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default: CallP2mfMethod(Vmm, MethCall); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (PBEntry.Method < 0xe00)
|
else if (MethCall.Method < 0xe00)
|
||||||
{
|
{
|
||||||
Gpu.Engine3d.CallMethod(Vmm, PBEntry);
|
Gpu.Engine3d.CallMethod(Vmm, MethCall);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int MacroIndex = (PBEntry.Method >> 1) & MacroIndexMask;
|
int MacroIndex = (MethCall.Method >> 1) & MacroIndexMask;
|
||||||
|
|
||||||
if ((PBEntry.Method & 1) != 0)
|
if ((MethCall.Method & 1) != 0)
|
||||||
{
|
{
|
||||||
foreach (int Arg in PBEntry.Arguments)
|
Macros[MacroIndex].PushArgument(MethCall.Argument);
|
||||||
{
|
|
||||||
Macros[MacroIndex].PushParam(Arg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Macros[MacroIndex].Execute(Vmm, Mme, PBEntry.Arguments[0]);
|
Macros[MacroIndex].StartExecution(MethCall.Argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MethCall.IsLastCall)
|
||||||
|
{
|
||||||
|
Macros[MacroIndex].Execute(Vmm, Mme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CallP2mfMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void CallP2mfMethod(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
Gpu.EngineP2mf.CallMethod(Vmm, PBEntry);
|
Gpu.EngineP2mf.CallMethod(Vmm, MethCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CallM2mfMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry)
|
private void CallM2mfMethod(NvGpuVmm Vmm, GpuMethodCall MethCall)
|
||||||
{
|
{
|
||||||
Gpu.EngineM2mf.CallMethod(Vmm, PBEntry);
|
Gpu.EngineM2mf.CallMethod(Vmm, MethCall);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,5 +2,5 @@ using Ryujinx.Graphics.Memory;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics
|
namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
delegate void NvGpuMethod(NvGpuVmm Vmm, NvGpuPBEntry PBEntry);
|
delegate void NvGpuMethod(NvGpuVmm Vmm, GpuMethodCall MethCall);
|
||||||
}
|
}
|
|
@ -95,6 +95,7 @@ namespace Ryujinx.Graphics.Texture
|
||||||
{ GalImageFormat.RGBA32, new ImageDescriptor(16, 1, 1, TargetBuffer.Color) },
|
{ GalImageFormat.RGBA32, new ImageDescriptor(16, 1, 1, TargetBuffer.Color) },
|
||||||
{ GalImageFormat.RGBA16, new ImageDescriptor(8, 1, 1, TargetBuffer.Color) },
|
{ GalImageFormat.RGBA16, new ImageDescriptor(8, 1, 1, TargetBuffer.Color) },
|
||||||
{ GalImageFormat.RG32, new ImageDescriptor(8, 1, 1, TargetBuffer.Color) },
|
{ GalImageFormat.RG32, new ImageDescriptor(8, 1, 1, TargetBuffer.Color) },
|
||||||
|
{ GalImageFormat.RGBX8, new ImageDescriptor(4, 1, 1, TargetBuffer.Color) },
|
||||||
{ GalImageFormat.RGBA8, new ImageDescriptor(4, 1, 1, TargetBuffer.Color) },
|
{ GalImageFormat.RGBA8, new ImageDescriptor(4, 1, 1, TargetBuffer.Color) },
|
||||||
{ GalImageFormat.BGRA8, new ImageDescriptor(4, 1, 1, TargetBuffer.Color) },
|
{ GalImageFormat.BGRA8, new ImageDescriptor(4, 1, 1, TargetBuffer.Color) },
|
||||||
{ GalImageFormat.RGB10A2, new ImageDescriptor(4, 1, 1, TargetBuffer.Color) },
|
{ GalImageFormat.RGB10A2, new ImageDescriptor(4, 1, 1, TargetBuffer.Color) },
|
||||||
|
@ -131,9 +132,10 @@ namespace Ryujinx.Graphics.Texture
|
||||||
{ GalImageFormat.Astc2D10x5, new ImageDescriptor(16, 10, 5, TargetBuffer.Color) },
|
{ GalImageFormat.Astc2D10x5, new ImageDescriptor(16, 10, 5, TargetBuffer.Color) },
|
||||||
{ GalImageFormat.Astc2D10x6, new ImageDescriptor(16, 10, 6, TargetBuffer.Color) },
|
{ GalImageFormat.Astc2D10x6, new ImageDescriptor(16, 10, 6, TargetBuffer.Color) },
|
||||||
|
|
||||||
|
{ GalImageFormat.D16, new ImageDescriptor(2, 1, 1, TargetBuffer.Depth) },
|
||||||
|
{ GalImageFormat.D24, new ImageDescriptor(4, 1, 1, TargetBuffer.Depth) },
|
||||||
{ GalImageFormat.D24S8, new ImageDescriptor(4, 1, 1, TargetBuffer.DepthStencil) },
|
{ GalImageFormat.D24S8, new ImageDescriptor(4, 1, 1, TargetBuffer.DepthStencil) },
|
||||||
{ GalImageFormat.D32, new ImageDescriptor(4, 1, 1, TargetBuffer.Depth) },
|
{ GalImageFormat.D32, new ImageDescriptor(4, 1, 1, TargetBuffer.Depth) },
|
||||||
{ GalImageFormat.D16, new ImageDescriptor(2, 1, 1, TargetBuffer.Depth) },
|
|
||||||
{ GalImageFormat.D32S8, new ImageDescriptor(8, 1, 1, TargetBuffer.DepthStencil) }
|
{ GalImageFormat.D32S8, new ImageDescriptor(8, 1, 1, TargetBuffer.DepthStencil) }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -198,6 +200,7 @@ namespace Ryujinx.Graphics.Texture
|
||||||
case GalSurfaceFormat.R8Uint: return GalImageFormat.R8 | Uint;
|
case GalSurfaceFormat.R8Uint: return GalImageFormat.R8 | Uint;
|
||||||
case GalSurfaceFormat.B5G6R5Unorm: return GalImageFormat.RGB565 | Unorm;
|
case GalSurfaceFormat.B5G6R5Unorm: return GalImageFormat.RGB565 | Unorm;
|
||||||
case GalSurfaceFormat.BGR5A1Unorm: return GalImageFormat.BGR5A1 | Unorm;
|
case GalSurfaceFormat.BGR5A1Unorm: return GalImageFormat.BGR5A1 | Unorm;
|
||||||
|
case GalSurfaceFormat.RGBX8Unorm: return GalImageFormat.RGBX8 | Unorm;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotImplementedException(Format.ToString());
|
throw new NotImplementedException(Format.ToString());
|
||||||
|
@ -210,6 +213,7 @@ namespace Ryujinx.Graphics.Texture
|
||||||
case GalZetaFormat.D32Float: return GalImageFormat.D32 | Float;
|
case GalZetaFormat.D32Float: return GalImageFormat.D32 | Float;
|
||||||
case GalZetaFormat.S8D24Unorm: return GalImageFormat.D24S8 | Unorm;
|
case GalZetaFormat.S8D24Unorm: return GalImageFormat.D24S8 | Unorm;
|
||||||
case GalZetaFormat.D16Unorm: return GalImageFormat.D16 | Unorm;
|
case GalZetaFormat.D16Unorm: return GalImageFormat.D16 | Unorm;
|
||||||
|
case GalZetaFormat.D24X8Unorm: return GalImageFormat.D24 | Unorm;
|
||||||
case GalZetaFormat.D24S8Unorm: return GalImageFormat.D24S8 | Unorm;
|
case GalZetaFormat.D24S8Unorm: return GalImageFormat.D24S8 | Unorm;
|
||||||
case GalZetaFormat.D32S8X24Float: return GalImageFormat.D32S8 | Float;
|
case GalZetaFormat.D32S8X24Float: return GalImageFormat.D32S8 | Float;
|
||||||
}
|
}
|
||||||
|
@ -247,7 +251,7 @@ namespace Ryujinx.Graphics.Texture
|
||||||
{
|
{
|
||||||
int OutOffs = Y * Pitch;
|
int OutOffs = Y * Pitch;
|
||||||
|
|
||||||
for (int X = 0; X < Width; X++)
|
for (int X = 0; X < Width; X++)
|
||||||
{
|
{
|
||||||
long Offset = (uint)Swizzle.GetSwizzleOffset(X, Y);
|
long Offset = (uint)Swizzle.GetSwizzleOffset(X, Y);
|
||||||
|
|
||||||
|
@ -283,6 +287,45 @@ namespace Ryujinx.Graphics.Texture
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool CopyTexture(
|
||||||
|
NvGpuVmm Vmm,
|
||||||
|
GalImage SrcImage,
|
||||||
|
GalImage DstImage,
|
||||||
|
long SrcAddress,
|
||||||
|
long DstAddress,
|
||||||
|
int SrcX,
|
||||||
|
int SrcY,
|
||||||
|
int DstX,
|
||||||
|
int DstY,
|
||||||
|
int Width,
|
||||||
|
int Height)
|
||||||
|
{
|
||||||
|
ISwizzle SrcSwizzle = TextureHelper.GetSwizzle(SrcImage);
|
||||||
|
ISwizzle DstSwizzle = TextureHelper.GetSwizzle(DstImage);
|
||||||
|
|
||||||
|
ImageDescriptor Desc = GetImageDescriptor(SrcImage.Format);
|
||||||
|
|
||||||
|
if (GetImageDescriptor(DstImage.Format).BytesPerPixel != Desc.BytesPerPixel)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BytesPerPixel = Desc.BytesPerPixel;
|
||||||
|
|
||||||
|
for (int Y = 0; Y < Height; Y++)
|
||||||
|
for (int X = 0; X < Width; X++)
|
||||||
|
{
|
||||||
|
long SrcOffset = (uint)SrcSwizzle.GetSwizzleOffset(SrcX + X, SrcY + Y);
|
||||||
|
long DstOffset = (uint)DstSwizzle.GetSwizzleOffset(DstX + X, DstY + Y);
|
||||||
|
|
||||||
|
byte[] Texel = Vmm.ReadBytes(SrcAddress + SrcOffset, BytesPerPixel);
|
||||||
|
|
||||||
|
Vmm.WriteBytes(DstAddress + DstOffset, Texel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static int GetSize(GalImage Image)
|
public static int GetSize(GalImage Image)
|
||||||
{
|
{
|
||||||
ImageDescriptor Desc = GetImageDescriptor(Image.Format);
|
ImageDescriptor Desc = GetImageDescriptor(Image.Format);
|
||||||
|
|
|
@ -76,7 +76,7 @@ namespace Ryujinx.Graphics
|
||||||
{
|
{
|
||||||
Ranges.RemoveAt(NewIndex + 1);
|
Ranges.RemoveAt(NewIndex + 1);
|
||||||
|
|
||||||
Ranges[NewIndex] = new ValueRange<T>(Range.Start, Next.End, Range.Value);
|
Ranges[NewIndex] = new ValueRange<T>(Ranges[NewIndex].Start, Next.End, Range.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -333,7 +333,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
{
|
{
|
||||||
Device.FileSystem.SetRomFs(RomfsStream);
|
Device.FileSystem.SetRomFs(RomfsStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pfs Exefs = new Pfs(ExefsStream);
|
Pfs Exefs = new Pfs(ExefsStream);
|
||||||
|
|
||||||
Npdm MetaData = null;
|
Npdm MetaData = null;
|
||||||
|
|
|
@ -181,15 +181,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvHostChannel
|
||||||
|
|
||||||
private static void PushGpfifo(ServiceCtx Context, NvGpuVmm Vmm, long Gpfifo)
|
private static void PushGpfifo(ServiceCtx Context, NvGpuVmm Vmm, long Gpfifo)
|
||||||
{
|
{
|
||||||
long VA = Gpfifo & 0xff_ffff_ffff;
|
Context.Device.Gpu.Pusher.Push(Vmm, Gpfifo);
|
||||||
|
|
||||||
int Size = (int)(Gpfifo >> 40) & 0x7ffffc;
|
|
||||||
|
|
||||||
byte[] Data = Vmm.ReadBytes(VA, Size);
|
|
||||||
|
|
||||||
NvGpuPBEntry[] PushBuffer = NvGpuPushBuffer.Decode(Data);
|
|
||||||
|
|
||||||
Context.Device.Gpu.Fifo.PushBuffer(Vmm, PushBuffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NvChannel GetChannel(ServiceCtx Context, NvChannelName Channel)
|
public static NvChannel GetChannel(ServiceCtx Context, NvChannelName Channel)
|
||||||
|
|
|
@ -88,12 +88,12 @@ namespace Ryujinx.HLE
|
||||||
|
|
||||||
public bool WaitFifo()
|
public bool WaitFifo()
|
||||||
{
|
{
|
||||||
return Gpu.Fifo.Event.WaitOne(8);
|
return Gpu.Pusher.WaitForCommands();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ProcessFrame()
|
public void ProcessFrame()
|
||||||
{
|
{
|
||||||
Gpu.Fifo.DispatchCalls();
|
Gpu.Pusher.DispatchCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Unload()
|
internal void Unload()
|
||||||
|
|
Reference in a new issue